DocDoeAI / src /lib /teaching /physicsSourcePacks.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
9.48 kB
import {
getChapters,
getDefinitions,
getFormulas,
getNumericalPatterns,
getQuestions,
getVisualLessonFrames,
type ChapterSummary,
} from "@/lib/knowledge-base/sslc-physics";
import type { LessonSeedSourceRef } from "./physicsLessonSeeds";
import { getMagneticEffectEvidencePack } from "./source-overrides/magneticEffectEvidence";
export type PhysicsChapterSourcePack = {
subject: "Physics";
chapterId: string;
chapterTitle: string;
sourceBook: string;
pageRange?: string;
sourceRefs: LessonSeedSourceRef[];
extractedSignals: {
formulas: string[];
definitions: string[];
possibleQuestions: string[];
keyTerms: string[];
diagrams: string[];
numericalPatterns: string[];
};
coverageConfidence: "high" | "medium" | "low";
warnings: string[];
/** True when the extracted signals were repaired from a manual source-evidence override (Phase 6I). */
sourceRepaired?: boolean;
};
/**
* Manual source-evidence overrides (Phase 6I). These do NOT create lesson seeds
* and do NOT make a chapter teachable — they only repair thin auto-extraction so
* the source pack reflects the real (source-referenced) chapter scope.
*/
type SourceEvidenceOverride = {
definitions: string[];
keyTerms: string[];
diagrams: string[];
possibleQuestions: string[];
};
function magneticEffectOverride(): SourceEvidenceOverride {
const pack = getMagneticEffectEvidencePack();
return {
definitions: pack.definitions.map((d) => `${d.term}: ${d.definition}`),
keyTerms: pack.keyTerms,
diagrams: pack.diagrams.map((d) => `${d.title}: ${d.visualDescription}`),
possibleQuestions: pack.questions.map((q) => q.question),
};
}
function evidenceOverrideFor(chapterId: string): SourceEvidenceOverride | null {
if (chapterId === "phy-p1-c4") return magneticEffectOverride();
return null;
}
const SOURCE_BOOK = "Kerala SCERT Physics, Standard X (2025)";
const MAX_SIGNAL_LENGTH = 180;
const MAX_SIGNALS_PER_KIND = 20;
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function getChapterTitle(chapter: ChapterSummary): string {
const maybeTitle = chapter as ChapterSummary & { title?: string };
return chapter.chapter_title?.trim() || maybeTitle.title?.trim() || chapter.chapter_id;
}
function compactText(value: string): string {
return value.replace(/\s+/g, " ").trim().slice(0, MAX_SIGNAL_LENGTH);
}
function unique(values: string[]): string[] {
return Array.from(new Set(values.map(compactText).filter(Boolean))).slice(0, MAX_SIGNALS_PER_KIND);
}
function firstText(record: Record<string, unknown>, keys: string[]): string {
for (const key of keys) {
const value = readString(record[key]);
if (value) return value;
}
return "";
}
function sourcePageRange(chapter: ChapterSummary): string | undefined {
const printedStart = Number(chapter.printed_start_page);
const printedEnd = Number(chapter.printed_end_page);
if (Number.isFinite(printedStart) && printedStart > 0) {
return Number.isFinite(printedEnd) && printedEnd >= printedStart
? `${printedStart}-${printedEnd}`
: String(printedStart);
}
const pdfStart = Number(chapter.pdf_start_page);
const pdfEnd = Number(chapter.pdf_end_page);
if (Number.isFinite(pdfStart) && pdfStart > 0) {
return Number.isFinite(pdfEnd) && pdfEnd >= pdfStart ? `${pdfStart}-${pdfEnd}` : String(pdfStart);
}
return undefined;
}
function buildSourceRef(chapter: ChapterSummary): LessonSeedSourceRef {
const pageRange = sourcePageRange(chapter);
const chapterTitle = getChapterTitle(chapter);
return {
label: pageRange
? `Kerala SSLC Physics - ${chapterTitle} (pp. ${pageRange})`
: `Kerala SSLC Physics - ${chapterTitle}`,
sourceBook: SOURCE_BOOK,
chapterTitle,
pageRange,
url: null,
confidence: chapter.needs_review ? "needs_review" : "source_backed",
};
}
function formulaSignals(chapterId: string): string[] {
return unique(
(getFormulas(chapterId) ?? []).map((item) => {
const record = asRecord(item);
const formula = firstText(record, ["formula", "formula_used", "equation", "expression"]);
const name = firstText(record, ["name", "title", "formula_name", "pattern_name"]);
return formula && name ? `${name}: ${formula}` : formula || name;
}),
);
}
function definitionSignals(chapterId: string): string[] {
return unique(
getDefinitions(chapterId).map((item) => {
const term = item.term?.trim() ?? "";
const definition = item.definition?.trim() ?? "";
return definition ? `${term}: ${definition}` : term;
}),
);
}
function questionSignals(chapterId: string): string[] {
return unique(getQuestions(chapterId).map((item) => item.question));
}
function termSignals(chapterId: string): string[] {
const fromDefinitions = getDefinitions(chapterId).map((item) => item.term);
const fromQuestions = getQuestions(chapterId).flatMap((item) => item.keywords ?? []);
const fromFormulas = (getFormulas(chapterId) ?? []).map((item) => firstText(asRecord(item), ["name", "title"]));
return unique([...fromDefinitions, ...fromQuestions, ...fromFormulas]);
}
function numericalSignals(chapterId: string): string[] {
return unique(
(getNumericalPatterns(chapterId) ?? []).map((item) => {
const record = asRecord(item);
const pattern = firstText(record, ["pattern_name", "title", "name"]);
const formula = firstText(record, ["formula_used", "formula", "equation"]);
return pattern && formula ? `${pattern}: ${formula}` : pattern || formula;
}),
);
}
function diagramSignals(chapterId: string): string[] {
return unique(
(getVisualLessonFrames(chapterId) ?? []).flatMap((item) => {
const record = asRecord(item);
const title = firstText(record, ["title", "frame_title", "name"]);
const idea = firstText(record, ["diagramDescription", "description", "teacherAction", "visual_idea"]);
return [title, idea];
}),
);
}
function buildWarnings(signals: PhysicsChapterSourcePack["extractedSignals"], sourceRefs: LessonSeedSourceRef[]): string[] {
const warnings: string[] = [];
if (sourceRefs.length === 0) warnings.push("Missing chapter source reference.");
if (signals.definitions.length === 0) warnings.push("No extracted definitions; concept missions need human curation.");
if (signals.formulas.length === 0) warnings.push("No extracted formulas; formula/numerical missions need review.");
if (signals.possibleQuestions.length < 10) warnings.push("Thin possible-question coverage.");
if (signals.diagrams.length === 0) warnings.push("No extracted visual-board/diagram signal.");
if (signals.numericalPatterns.length === 0) warnings.push("No extracted numerical pattern.");
return warnings;
}
function confidenceFor(signals: PhysicsChapterSourcePack["extractedSignals"], sourceRefs: LessonSeedSourceRef[]): "high" | "medium" | "low" {
if (sourceRefs.length === 0) return "low";
if (signals.definitions.length === 0 && signals.formulas.length === 0) return "low";
const signalScore = [
signals.definitions.length > 0,
signals.formulas.length > 0,
signals.possibleQuestions.length >= 10,
signals.diagrams.length > 0,
signals.numericalPatterns.length > 0,
].filter(Boolean).length;
if (signalScore >= 4) return "high";
if (signalScore >= 2) return "medium";
return "low";
}
function buildPack(chapter: ChapterSummary): PhysicsChapterSourcePack {
const sourceRefs = [buildSourceRef(chapter)];
const extractedSignals: PhysicsChapterSourcePack["extractedSignals"] = {
formulas: formulaSignals(chapter.chapter_id),
definitions: definitionSignals(chapter.chapter_id),
possibleQuestions: questionSignals(chapter.chapter_id),
keyTerms: termSignals(chapter.chapter_id),
diagrams: diagramSignals(chapter.chapter_id),
numericalPatterns: numericalSignals(chapter.chapter_id),
};
const override = evidenceOverrideFor(chapter.chapter_id);
if (override) {
extractedSignals.definitions = unique([...extractedSignals.definitions, ...override.definitions]);
extractedSignals.keyTerms = unique([...extractedSignals.keyTerms, ...override.keyTerms]);
extractedSignals.diagrams = unique([...extractedSignals.diagrams, ...override.diagrams]);
extractedSignals.possibleQuestions = unique([...extractedSignals.possibleQuestions, ...override.possibleQuestions]);
}
const warnings = buildWarnings(extractedSignals, sourceRefs);
if (override) {
warnings.push(
"Source manually repaired from the extracted textbook question bank and figure references (Phase 6I evidence override).",
);
}
return {
subject: "Physics",
chapterId: chapter.chapter_id,
chapterTitle: getChapterTitle(chapter),
sourceBook: SOURCE_BOOK,
pageRange: sourceRefs[0]?.pageRange,
sourceRefs,
extractedSignals,
coverageConfidence: confidenceFor(extractedSignals, sourceRefs),
warnings,
sourceRepaired: Boolean(override),
};
}
export function getPhysicsSourcePacks(): PhysicsChapterSourcePack[] {
return getChapters().map(buildPack);
}
export function getPhysicsSourcePack(chapterId: string): PhysicsChapterSourcePack | null {
return getPhysicsSourcePacks().find((pack) => pack.chapterId === chapterId) ?? null;
}