DocDoeAI / src /lib /teaching /physicsLessonSeedFactory.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
16.6 kB
import {
chaptersWithPhysicsSeeds,
getAllLessonSeeds,
type LessonSeedContentType,
type TuitionLessonSeed,
} from "./physicsLessonSeeds";
import {
getPhysicsMissionBlueprint,
getPhysicsMissionBlueprints,
getMissingBlueprintRequirements,
type PhysicsMissionBlueprintType,
} from "./physicsMissionBlueprints";
import { getPhysicsSourcePack, getPhysicsSourcePacks, type PhysicsChapterSourcePack } from "./physicsSourcePacks";
import { evaluateMagneticEffectCoverageGate } from "./magneticEffectCoverageGate";
/**
* Curation status distinguishes *source readiness* from *teachable curation*:
* - "curated" β€” final teachable seeds exist and are registered.
* - "curation_ready" β€” source repaired + evidence gate passed; final seeds MISSING (Phase 6J).
* - "draftable" β€” source strong enough to draft review-only, not gate-passed.
* - "blocked" β€” source too thin; needs manual source repair first.
*/
export type ChapterCurationStatus = "curated" | "curation_ready" | "draftable" | "blocked";
export type DraftSeedStatus =
| "curated"
| "draft_needs_review"
| "blocked_missing_source"
| "blocked_missing_required_sections";
export type PhysicsDraftLessonSeed = {
missionId: string;
chapterId: string;
chapterTitle: string;
missionTitle: string;
status: DraftSeedStatus;
canTeach: boolean;
missingFields: string[];
warnings: string[];
seed?: TuitionLessonSeed;
};
export type PhysicsSeedCoverage = {
chapterId: string;
chapterTitle: string;
curatedSeedCount: number;
missionIds: string[];
missionTypes: LessonSeedContentType[];
};
export type PhysicsDraftSeedPlan = {
chapterId: string;
chapterTitle: string;
status: DraftSeedStatus;
sourceConfidence: PhysicsChapterSourcePack["coverageConfidence"] | "missing";
recommendedBlueprints: PhysicsMissionBlueprintType[];
missingSignals: string[];
warnings: string[];
nextAction: string;
};
export type PhysicsSeedFactoryReport = {
totalPhysicsChapters: number;
curatedChapters: string[];
uncuratedChapters: string[];
blockedChapters: Array<{ chapterId: string; reason: string }>;
draftableChapters: string[];
/** Chapters whose thin extraction was repaired by a manual evidence override (Phase 6I). */
sourceRepairedChapters: string[];
/** Source-repaired + gate-passed chapters awaiting final seed authoring (not teachable yet). */
curationReadyChapters: string[];
nextRecommendedBatch: string[];
/** Human-readable next action so source repair is never confused with final curation. */
nextRecommendedAction: string;
coverageByChapter: Array<{
chapterId: string;
chapterTitle: string;
sourceConfidence: PhysicsChapterSourcePack["coverageConfidence"];
curatedSeedCount: number;
curationStatus: ChapterCurationStatus;
sourceRepaired: boolean;
finalSeedsPresent: boolean;
missingMissionTypes: PhysicsMissionBlueprintType[];
warnings: string[];
}>;
};
export type PhysicsSeedFactoryStatus = {
officialCuratedChapters: string[];
uncuratedChapters: string[];
draftableChapters: string[];
blockedChapters: Array<{ chapterId: string; reason: string }>;
officialSeedCount: number;
};
function unique<T>(values: T[]): T[] {
return Array.from(new Set(values));
}
function curatedSeedsFor(chapterId: string): TuitionLessonSeed[] {
return getAllLessonSeeds().filter((seed) => seed.chapterId === chapterId);
}
function missionTypeFromContentType(contentType: LessonSeedContentType): PhysicsMissionBlueprintType {
if (contentType === "concept") return "concept";
if (contentType === "diagram") return "diagram";
if (contentType === "formula") return "formula";
if (contentType === "numerical") return "numerical";
if (contentType === "exam_answer") return "exam_answer";
if (contentType === "revision") return "revision";
if (contentType === "pyq_placeholder") return "pyq_placeholder";
return "concept";
}
function recommendedBlueprintsFor(pack: PhysicsChapterSourcePack): PhysicsMissionBlueprintType[] {
const signals = pack.extractedSignals;
const recommended: PhysicsMissionBlueprintType[] = [];
if (signals.definitions.length || signals.keyTerms.length) recommended.push("concept");
if (signals.diagrams.length) recommended.push("diagram");
if (signals.formulas.length) recommended.push("formula");
if (signals.formulas.length && signals.numericalPatterns.length) recommended.push("numerical");
if (signals.possibleQuestions.length) recommended.push("exam_answer");
if (signals.definitions.length || signals.formulas.length || signals.possibleQuestions.length) recommended.push("revision");
return recommended.length ? unique(recommended) : ["concept", "exam_answer", "revision"];
}
function missingSignalsFor(pack: PhysicsChapterSourcePack, blueprint: PhysicsMissionBlueprintType): string[] {
const signals = pack.extractedSignals;
const missing: string[] = [];
const requiredSignals = getPhysicsMissionBlueprint(blueprint).requiredSignals;
if (requiredSignals.includes("definitions") && signals.definitions.length === 0) missing.push("definitions");
if (requiredSignals.includes("keyTerms") && signals.keyTerms.length === 0) missing.push("key terms");
if (requiredSignals.includes("formulas") && signals.formulas.length === 0) missing.push("formulas");
if (requiredSignals.includes("visualBoard") && signals.diagrams.length === 0) missing.push("visual board frames");
if (requiredSignals.includes("solvedExample") && signals.numericalPatterns.length === 0) missing.push("solved numerical example");
if (requiredSignals.includes("examAnswers") && signals.possibleQuestions.length === 0) missing.push("exam answer questions");
if (requiredSignals.includes("keywords") && signals.keyTerms.length === 0) missing.push("exam keywords");
if (requiredSignals.includes("flashcards") && signals.definitions.length === 0) missing.push("flashcards/revision anchors");
if (requiredSignals.includes("weakAreaTags") && signals.keyTerms.length === 0) missing.push("weak area tags");
if (requiredSignals.includes("curatedPyqEvidence")) missing.push("curated PYQ evidence");
return unique(missing);
}
function draftMissingFields(pack: PhysicsChapterSourcePack, blueprint: PhysicsMissionBlueprintType): string[] {
return unique([
"human-authored 11-step lesson seed",
"human review approval",
...missingSignalsFor(pack, blueprint),
...getMissingBlueprintRequirements(blueprint, {
sourceRefs: pack.sourceRefs,
formulas: pack.extractedSignals.formulas.map((formula) => ({ formula, symbols: [] })),
solvedExamples: pack.extractedSignals.numericalPatterns,
visualFrames: pack.extractedSignals.diagrams,
examAnswers: pack.extractedSignals.possibleQuestions.map(() => ({ keywords: pack.extractedSignals.keyTerms })),
flashcards: pack.extractedSignals.definitions,
weakAreaTags: pack.extractedSignals.keyTerms,
pyqEvidenceStatus: "not_curated",
}),
]);
}
export function getCuratedPhysicsSeedCoverage(): PhysicsSeedCoverage[] {
const byChapter = new Map<string, TuitionLessonSeed[]>();
for (const seed of getAllLessonSeeds()) {
byChapter.set(seed.chapterId, [...(byChapter.get(seed.chapterId) ?? []), seed]);
}
return Array.from(byChapter.entries()).map(([chapterId, seeds]) => ({
chapterId,
chapterTitle: seeds[0]?.chapterTitle ?? chapterId,
curatedSeedCount: seeds.length,
missionIds: seeds.map((seed) => seed.missionId),
missionTypes: unique(seeds.map((seed) => seed.contentType)),
}));
}
export function getUncuratedPhysicsChapters(): PhysicsChapterSourcePack[] {
const curated = new Set(chaptersWithPhysicsSeeds());
return getPhysicsSourcePacks().filter((pack) => !curated.has(pack.chapterId));
}
export function createPhysicsDraftSeedPlan(chapterId: string): PhysicsDraftSeedPlan {
const pack = getPhysicsSourcePack(chapterId);
if (!pack) {
return {
chapterId,
chapterTitle: chapterId,
status: "blocked_missing_source",
sourceConfidence: "missing",
recommendedBlueprints: [],
missingSignals: ["source pack"],
warnings: ["No Physics source pack exists for this chapter."],
nextAction: "Locate source metadata before drafting any lesson seed.",
};
}
if (chaptersWithPhysicsSeeds().includes(chapterId)) {
return {
chapterId,
chapterTitle: pack.chapterTitle,
status: "curated",
sourceConfidence: pack.coverageConfidence,
recommendedBlueprints: [],
missingSignals: [],
warnings: pack.warnings,
nextAction: "Already curated. Do not regenerate from the factory.",
};
}
const recommendedBlueprints = recommendedBlueprintsFor(pack);
const missingSignals = unique(recommendedBlueprints.flatMap((blueprint) => missingSignalsFor(pack, blueprint)));
const blocked = pack.coverageConfidence === "low";
return {
chapterId,
chapterTitle: pack.chapterTitle,
status: blocked ? "blocked_missing_required_sections" : "draft_needs_review",
sourceConfidence: pack.coverageConfidence,
recommendedBlueprints,
missingSignals,
warnings: pack.warnings,
nextAction: blocked
? "Create a human curation TODO before drafting. Extracted signals are too thin for safe seed drafting."
: "Draft review-only seeds from the recommended blueprints; keep canTeach=false until human-authored content validates.",
};
}
export function createPhysicsDraftSeedsForChapter(chapterId: string): PhysicsDraftLessonSeed[] {
const pack = getPhysicsSourcePack(chapterId);
if (!pack) {
return [
{
missionId: "TODO",
chapterId,
chapterTitle: chapterId,
missionTitle: "Source pack missing",
status: "blocked_missing_source",
canTeach: false,
missingFields: ["source pack", "sourceRefs"],
warnings: ["No Physics source pack exists for this chapter."],
},
];
}
const curatedSeeds = curatedSeedsFor(chapterId);
if (curatedSeeds.length > 0) {
return curatedSeeds.map((seed) => ({
missionId: seed.missionId,
chapterId: seed.chapterId,
chapterTitle: seed.chapterTitle,
missionTitle: seed.missionTitle,
status: "curated",
canTeach: seed.canTeach,
missingFields: [],
warnings: seed.qualityFlags.reason ? [seed.qualityFlags.reason] : [],
seed,
}));
}
const plan = createPhysicsDraftSeedPlan(chapterId);
if (plan.status === "blocked_missing_required_sections") {
return [
{
missionId: "TODO",
chapterId: pack.chapterId,
chapterTitle: pack.chapterTitle,
missionTitle: `Review source pack for ${pack.chapterTitle}`,
status: "blocked_missing_required_sections",
canTeach: false,
missingFields: unique(["strong source signals", ...plan.missingSignals, "human-authored 11-step lesson seed"]),
warnings: plan.warnings,
},
];
}
return plan.recommendedBlueprints.map((blueprint, index) => ({
missionId: `D${index + 1}`,
chapterId: pack.chapterId,
chapterTitle: pack.chapterTitle,
missionTitle: `${getPhysicsMissionBlueprint(blueprint).title} - ${pack.chapterTitle}`,
status: "draft_needs_review",
canTeach: false,
missingFields: draftMissingFields(pack, blueprint),
warnings: unique([
...plan.warnings,
"Draft output is review-only and is not registered in Tuition Mode.",
"No official canTeach=true seed is produced by the factory.",
]),
}));
}
/**
* Curation status per chapter. Distinguishes source repair (Phase 6I) from final
* teachable curation. A source-repaired chapter that passes its evidence gate is
* "curation_ready" β€” its final seeds are still MISSING and it is NOT teachable.
*/
function curationStatusFor(pack: PhysicsChapterSourcePack, hasSeeds: boolean): ChapterCurationStatus {
if (hasSeeds) return "curated";
if (pack.sourceRepaired) {
if (pack.chapterId === "phy-p1-c4") {
const gate = evaluateMagneticEffectCoverageGate();
if (gate.status === "curation_ready") return "curation_ready";
if (gate.status === "draftable") return "draftable";
return "blocked";
}
return "draftable";
}
return pack.coverageConfidence === "low" ? "blocked" : "draftable";
}
export function createPhysicsBatchCurationReport(): PhysicsSeedFactoryReport {
const packs = getPhysicsSourcePacks();
const coverage = getCuratedPhysicsSeedCoverage();
const curatedByChapter = new Map(coverage.map((item) => [item.chapterId, item]));
const curatedChapters = coverage.map((item) => item.chapterId);
const uncurated = packs.filter((pack) => !curatedByChapter.has(pack.chapterId));
const statusByChapter = new Map<string, ChapterCurationStatus>(
packs.map((pack) => [pack.chapterId, curationStatusFor(pack, curatedByChapter.has(pack.chapterId))]),
);
const blockedChapters = packs
.filter((pack) => statusByChapter.get(pack.chapterId) === "blocked")
.map((pack) => ({ chapterId: pack.chapterId, reason: pack.warnings[0] ?? "Source too thin to draft." }));
const draftableChapters = packs
.filter((pack) => statusByChapter.get(pack.chapterId) === "draftable")
.map((pack) => pack.chapterId);
const curationReadyChapters = packs
.filter((pack) => statusByChapter.get(pack.chapterId) === "curation_ready")
.map((pack) => pack.chapterId);
const sourceRepairedChapters = packs.filter((pack) => pack.sourceRepaired).map((pack) => pack.chapterId);
// Priority of the next real work:
// 1. curation_ready chapters β†’ author final teaching seeds (Phase 6J).
// 2. draftable chapters β†’ draft review-only seeds.
// 3. blocked chapters β†’ repair the source first.
let nextRecommendedBatch: string[];
let nextRecommendedAction: string;
if (curationReadyChapters.length > 0) {
nextRecommendedBatch = curationReadyChapters.slice(0, 2);
nextRecommendedAction = `Phase 6J: curate final lesson seeds for ${curationReadyChapters.join(", ")} (source repaired, final seeds missing, not teachable yet).`;
} else if (draftableChapters.length > 0) {
nextRecommendedBatch = draftableChapters.slice(0, 2);
nextRecommendedAction = `Draft review-only seeds for ${draftableChapters.slice(0, 2).join(", ")}.`;
} else if (blockedChapters.length > 0) {
nextRecommendedBatch = blockedChapters.map((chapter) => chapter.chapterId).slice(0, 2);
nextRecommendedAction = `Repair the source layer for ${blockedChapters.map((c) => c.chapterId).slice(0, 2).join(", ")} before drafting.`;
} else {
nextRecommendedBatch = [];
nextRecommendedAction =
"Physics is complete β€” all 7 chapters are curated. Next: a Physics QA pass, or begin Chemistry source repair/curation.";
}
return {
totalPhysicsChapters: packs.length,
curatedChapters,
uncuratedChapters: uncurated.map((pack) => pack.chapterId),
blockedChapters,
draftableChapters,
sourceRepairedChapters,
curationReadyChapters,
nextRecommendedBatch,
nextRecommendedAction,
coverageByChapter: packs.map((pack) => {
const item = curatedByChapter.get(pack.chapterId);
const existingMissionTypes = new Set((item?.missionTypes ?? []).map(missionTypeFromContentType));
const missingMissionTypes = getPhysicsMissionBlueprints()
.filter((blueprint) => blueprint.canBeTeachable)
.map((blueprint) => blueprint.type)
.filter((type) => !existingMissionTypes.has(type));
return {
chapterId: pack.chapterId,
chapterTitle: pack.chapterTitle,
sourceConfidence: pack.coverageConfidence,
curatedSeedCount: item?.curatedSeedCount ?? 0,
curationStatus: statusByChapter.get(pack.chapterId) ?? "blocked",
sourceRepaired: Boolean(pack.sourceRepaired),
finalSeedsPresent: (item?.curatedSeedCount ?? 0) > 0,
missingMissionTypes,
warnings: pack.warnings,
};
}),
};
}
export function getPhysicsSeedFactoryStatus(): PhysicsSeedFactoryStatus {
const report = createPhysicsBatchCurationReport();
return {
officialCuratedChapters: report.curatedChapters,
uncuratedChapters: report.uncuratedChapters,
draftableChapters: report.draftableChapters,
blockedChapters: report.blockedChapters,
// Day 20 β€” the seed pool now includes the first Chemistry draft; this
// factory reports PHYSICS curation status, so count Physics seeds only.
officialSeedCount: getAllLessonSeeds().filter((seed) => seed.subject === "Physics").length,
};
}