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; 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 & { 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) ?? []; }