DocDoeAI / src /lib /tuition /teachLessonNotes.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
66680b1
Raw History Blame Contribute Delete
3.97 kB
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) ?? [];
}