DocDoeAI / src /lib /teaching /magneticEffectCoverageGate.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
6.91 kB
/**
* 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,
};
}