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,
  };
}