File size: 6,913 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | /**
* 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,
};
}
|