File size: 3,971 Bytes
65d68f5
 
 
 
66680b1
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66680b1
 
 
 
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66680b1
 
 
 
 
 
 
 
65d68f5
 
 
 
 
 
 
 
 
66680b1
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
66680b1
65d68f5
 
 
 
 
 
 
 
 
66680b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
export type TeachLessonSection = {
  id: string;
  title: string;
  markdown: string;
  boardPoints?: string[];
};

export type TeachLessonCheckpoint = {
  prompt: string;
  answer: string;
  explanation: string;
};

export type TeachLessonNotes = {
  chapterId: string;
  missionId: string;
  title: string;
  method: string;
  goals: string[];
  sections: TeachLessonSection[];
  /** Chapter notebooks are not evidence that every mission is curated. */
  scope?: "chapter" | "mission";
  /** Explicit authored section identities, never inferred from title similarity. */
  missionSectionIds?: Record<string, string[]>;
  aliases?: string[];
  recall?: {
    question: string;
    expectedIdea: string;
    correctFeedback?: string;
    incorrectFeedback?: string;
  };
  practice?: { question: string; hint: string };
  exam?: {
    question: string;
    answer: string;
    points: string[];
    keywords: string[];
  };
  checkpoint?: TeachLessonCheckpoint[];
};

export const TEACH_METHOD =
  "Learn → Understand → Solve → Exam Answer → Weakness Fix";

const NOTES: TeachLessonNotes[] = [];

export function registerTeachLessonNotes(notes: TeachLessonNotes) {
  for (const [mission, ids] of Object.entries(notes.missionSectionIds ?? {})) {
    if (
      !mission.trim() || !ids.length || new Set(ids).size !== ids.length ||
      ids.some((id) => notes.sections.filter((section) => section.id === id).length !== 1)
    ) {
      throw new Error(`Invalid teach section mapping: ${notes.chapterId}/${mission}`);
    }
  }
  NOTES.push(notes);
}

export function physicsTeachNotes(
  notes: Omit<TeachLessonNotes, "method" | "missionId"> & { missionId?: string },
): TeachLessonNotes {
  const full: TeachLessonNotes = {
    missionId: "M1",
    method: TEACH_METHOD,
    scope: notes.missionId ? "mission" : "chapter",
    ...notes,
  };
  registerTeachLessonNotes(full);
  return full;
}

export const mathTeachNotes = physicsTeachNotes;
export const chemistryTeachNotes = physicsTeachNotes;
export const historyTeachNotes = physicsTeachNotes;
export const geographyTeachNotes = physicsTeachNotes;
export const sslcTeachNotes = physicsTeachNotes;

export function getTeachLessonNotes(
  chapterId: string | null | undefined,
  missionId?: string,
): TeachLessonNotes | null {
  if (!chapterId) return null;
  const id = chapterId.trim().toLowerCase();
  const matches = NOTES.filter((notes) => {
    const ids = [notes.chapterId, ...(notes.aliases ?? [])].map((value) =>
      value.toLowerCase(),
    );
    return ids.includes(id);
  });
  // Omitting the mission is an explicit chapter-notebook request. A lesson
  // always passes its mission and must never receive this broad fallback.
  if (missionId === undefined) {
    return matches.find((notes) => notes.scope === "chapter") ?? null;
  }
  const mission = missionId.trim().toUpperCase();
  const exact = matches.find((notes) => notes.scope !== "chapter"
    && notes.missionId.toUpperCase() === mission);
  if (exact) return exact;
  const chapter = matches.find((notes) => notes.scope === "chapter"
    && notes.missionSectionIds?.[mission]);
  const ids = chapter?.missionSectionIds?.[mission];
  if (process.env.DEBUG_FIX === "1") {
    console.debug("[FIX:mission-teach-scope]", {
      chapterId: id, missionId: mission, mapped: Boolean(ids), sectionCount: ids?.length ?? 0,
    });
  }
  if (!chapter || !ids) return null;
  const sections = ids.map((sectionId) => chapter.sections.find((section) => section.id === sectionId)!);
  // Do not carry chapter-wide practice, recall or exam prompts into a mission.
  return {
    chapterId: chapter.chapterId, missionId: mission, scope: "mission",
    title: `${chapter.title} · ${mission}`, method: chapter.method,
    goals: sections.map((section) => section.title), sections,
  };
}

export function teachBeatTitles(
  notes: TeachLessonNotes | null | undefined,
): string[] {
  return notes?.sections.map((section) => section.title) ?? [];
}