/** * Phase 6I — Magnetic Effect coverage-readiness gate. * * Evaluates the reconstructed Magnetic Effect evidence pack and decides whether * Phase 6J (final lesson-seed authoring) can safely start. This gate NEVER sets * canTeach, never registers seeds, and never claims curated PYQ. It only reports * source-readiness. */ import { getMagneticEffectEvidencePack, type EvidenceConfidence, type MagneticChapterEvidencePack, type MagneticEvidenceRef, } from "./source-overrides/magneticEffectEvidence"; export type MagneticCoverageGateResult = { chapterId: "phy-p1-c4"; status: "blocked" | "draftable" | "curation_ready" | "needs_manual_source_review"; score: number; requiredTopics: Array<{ topicId: string; topicTitle: string; status: "covered" | "partial" | "missing"; evidenceCount: number; warnings: string[]; }>; missingCriticalTopics: string[]; sourceQualityWarnings: string[]; canStartPhase6J: boolean; }; const FAKE_PREDICTION_PATTERNS = [ /guaranteed question/i, /100\s*%\s*com(e|ing)/i, /sure question/i, /surely (will )?be asked/i, /definitely (will )?come/i, /confirm(ed)? question/i, /exact prediction/i, ]; // The topics a safe Phase 6J seed set must be able to teach from source. const REQUIRED_TOPIC_IDS = [ "magnetic-field-around-conductor", "direction-of-field-right-hand-rule", "solenoid", "electromagnet", "force-on-current-carrying-conductor", "electric-motor", ] as const; function isSourceBacked(confidence: EvidenceConfidence): boolean { return confidence === "high" || confidence === "medium"; } function allRefs(pack: MagneticChapterEvidencePack): MagneticEvidenceRef[] { return [ ...pack.definitions.flatMap((d) => d.sourceRefs), ...pack.formulasAndRules.flatMap((f) => f.sourceRefs), ...pack.diagrams.flatMap((d) => d.sourceRefs), ...pack.questions.flatMap((q) => q.sourceRefs), ...pack.activities.flatMap((a) => a.sourceRefs), ]; } export function evaluateMagneticEffectCoverageGate( pack: MagneticChapterEvidencePack = getMagneticEffectEvidencePack(), ): MagneticCoverageGateResult { const sourceQualityWarnings: string[] = []; const sourceBackedDefinitions = pack.definitions.filter((d) => isSourceBacked(d.confidence)); const diagramAndActivityCount = pack.diagrams.length + pack.activities.length; const sourceBackedQuestions = pack.questions.filter((q) => isSourceBacked(q.confidence)); // --- Hard requirement checks ------------------------------------------------- const checks = { enoughDefinitions: sourceBackedDefinitions.length >= 5, enoughDiagrams: diagramAndActivityCount >= 3, enoughQuestions: sourceBackedQuestions.length >= 5, allSourceRefsPresent: allRefs(pack).length > 0 && [...pack.definitions, ...pack.formulasAndRules, ...pack.diagrams, ...pack.questions, ...pack.activities].every( (item) => item.sourceRefs.length > 0, ), noEmptySourceMeta: allRefs(pack).every((r) => r.sourceBook.trim().length > 0 && r.chapterTitle.trim().length > 0), pageRangeKnown: Boolean(pack.pageRange && pack.pageRange.trim().length > 0), }; if (!checks.enoughDefinitions) sourceQualityWarnings.push("Fewer than 5 source-backed definitions/key concepts."); if (!checks.enoughDiagrams) sourceQualityWarnings.push("Fewer than 3 diagram/activity evidence items."); if (!checks.enoughQuestions) sourceQualityWarnings.push("Fewer than 5 source-backed question evidence items."); if (!checks.allSourceRefsPresent) sourceQualityWarnings.push("Some evidence item is missing sourceRefs."); if (!checks.noEmptySourceMeta) sourceQualityWarnings.push("An evidence sourceRef has an empty sourceBook/chapterTitle."); if (!checks.pageRangeKnown) sourceQualityWarnings.push("Chapter page range is unknown."); // --- Honesty guards ---------------------------------------------------------- const packText = JSON.stringify(pack); for (const pattern of FAKE_PREDICTION_PATTERNS) { if (pattern.test(packText)) { sourceQualityWarnings.push(`Evidence contains fake-prediction language (matched ${pattern}).`); } } // --- Required-topic coverage ------------------------------------------------- const requiredTopics = REQUIRED_TOPIC_IDS.map((topicId) => { const topic = pack.coverageMap.find((t) => t.topicId === topicId); if (!topic) { return { topicId, topicTitle: topicId, status: "missing" as const, evidenceCount: 0, warnings: ["Topic not present in the coverage map."] }; } const status: "covered" | "partial" | "missing" = topic.status === "source_backed" ? "covered" : topic.status === "not_found" ? "missing" : "partial"; return { topicId: topic.topicId, topicTitle: topic.topicTitle, status, evidenceCount: topic.evidenceIds.length, warnings: topic.missingPieces, }; }); const missingCriticalTopics = requiredTopics.filter((t) => t.status === "missing").map((t) => t.topicId); const partialCriticalTopics = requiredTopics.filter((t) => t.status === "partial").map((t) => t.topicId); // --- Score ------------------------------------------------------------------- const hardChecksPassed = Object.values(checks).filter(Boolean).length; const coveredCritical = requiredTopics.filter((t) => t.status === "covered").length; const score = Math.round( ((hardChecksPassed / Object.keys(checks).length) * 0.5 + (coveredCritical / REQUIRED_TOPIC_IDS.length) * 0.5) * 100, ); // --- Status decision --------------------------------------------------------- const allHardChecksPass = hardChecksPassed === Object.keys(checks).length; const noFakeLanguage = !sourceQualityWarnings.some((w) => w.includes("fake-prediction")); const noMissingCritical = missingCriticalTopics.length === 0; let status: MagneticCoverageGateResult["status"]; let canStartPhase6J: boolean; if (!noFakeLanguage || !checks.allSourceRefsPresent || !checks.noEmptySourceMeta) { status = "needs_manual_source_review"; canStartPhase6J = false; } else if (allHardChecksPass && noMissingCritical) { status = "curation_ready"; canStartPhase6J = true; } else if (checks.enoughDefinitions && checks.enoughQuestions && noMissingCritical) { // Enough core evidence to draft, even if some quality checks are soft. status = "draftable"; canStartPhase6J = true; } else if (missingCriticalTopics.length > 0 || (!checks.enoughDefinitions && !checks.enoughQuestions)) { status = "blocked"; canStartPhase6J = false; } else { status = "draftable"; canStartPhase6J = false; } if (partialCriticalTopics.length > 0) { sourceQualityWarnings.push(`Partial source for: ${partialCriticalTopics.join(", ")}.`); } return { chapterId: "phy-p1-c4", status, score, requiredTopics, missingCriticalTopics, sourceQualityWarnings, canStartPhase6J, }; }