File size: 8,512 Bytes
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | /**
* Phase 6K β Physics full-subject QA + exam-readiness report.
*
* Proves DocDoe Physics is usable as a paid tuition product by checking every
* curated seed across all 7 chapters for coverage, source refs, bilingual
* support, visual boards, exam-answer quality and honesty (no fake claims, no
* curated-PYQ). It reuses the seed validator and never weakens it.
*
* This module is read-only: it computes a report, it does not mutate seeds.
*/
import { getAllLessonSeeds, type TuitionLessonSeed } from "./physicsLessonSeeds";
import { validateLessonSeed } from "./lessonSeedValidation";
export type PhysicsQaCoverage = {
hasConcept: boolean;
hasFormulaOrRule: boolean;
hasVisualBoard: boolean;
hasNumerical: boolean;
hasExamPractice: boolean;
hasRevision: boolean;
hasMalayalamSupport: boolean;
hasManglishSupport: boolean;
};
export type PhysicsQaChapter = {
chapterId: string;
chapterTitle: string;
seedCount: number;
coverage: PhysicsQaCoverage;
warnings: string[];
needsHumanReviewSeeds: string[];
};
export type PhysicsQaReport = {
totalChapters: number;
totalSeeds: number;
chapters: PhysicsQaChapter[];
globalWarnings: string[];
productionReadiness: {
status: "ready" | "ready_with_review_flags" | "not_ready";
blockers: string[];
recommendedFixes: string[];
};
};
// Placeholder phrases that would betray raw auto-extraction leaking into a seed.
const GENERIC_PHRASES = [
"note 1",
"core concept",
"important keyword",
"standard physics calculation",
"refer to formulas.json",
"textbook derivation",
"lorem ipsum",
];
function coverageFor(seeds: TuitionLessonSeed[]): PhysicsQaCoverage {
const types = new Set(seeds.map((s) => s.contentType));
// Day 20 β a chapter can satisfy exam-answer/revision coverage two ways:
// a dedicated mission of that contentType (the original structure), OR the
// SSLC teaching-order structure where EVERY mission carries its own board
// exam answers and its own recap/must-remember block (Sound Waves after the
// reorder). What matters for the student is that the coverage exists, not
// which mission label carries it.
const everyMissionHasExamAnswers = seeds.length > 0 && seeds.every((s) => s.examAnswers.length > 0);
const everyMissionHasRecap = seeds.length > 0 && seeds.every((s) => s.recap.summary.length > 0 && s.recap.mustRemember.length > 0);
return {
hasConcept: types.has("concept"),
hasFormulaOrRule: types.has("formula") || seeds.some((s) => s.formulas.length > 0),
hasVisualBoard: seeds.some((s) => (s.visualBoard?.frames?.length ?? 0) > 0),
hasNumerical: types.has("numerical"),
hasExamPractice: types.has("exam_answer") || everyMissionHasExamAnswers,
hasRevision: types.has("revision") || everyMissionHasRecap,
hasMalayalamSupport: seeds.every((s) => Boolean(s.teach.malayalamSupport?.trim())),
hasManglishSupport: seeds.every((s) => Boolean(s.teach.manglishSupport?.trim())),
};
}
function seedQaWarnings(seed: TuitionLessonSeed): string[] {
const warnings: string[] = [];
const where = seed.missionId || "<no id>";
// Reuse the hard validator (source refs, 11 steps, exam keywords, quiz
// answers/explanations, formula symbols/units, fake-guarantee, curated PYQ).
for (const error of validateLessonSeed(seed).errors) {
warnings.push(`[${where}] ${error}`);
}
if (!seed.missionTitle?.trim()) warnings.push(`[${where}] Empty mission title.`);
if (!seed.goal?.trim()) warnings.push(`[${where}] Empty goal.`);
// Every seed must carry at least one bilingual support snippet.
if (!seed.teach.malayalamSupport?.trim() && !seed.teach.manglishSupport?.trim()) {
warnings.push(`[${where}] Missing both Malayalam and Manglish support.`);
}
// Visual board must have frames.
if ((seed.visualBoard?.frames?.length ?? 0) === 0) {
warnings.push(`[${where}] Visual board has no frames.`);
}
// needsHumanReview must explain itself.
if (seed.qualityFlags.needsHumanReview && !seed.qualityFlags.reason?.trim()) {
warnings.push(`[${where}] needsHumanReview is set without a reason.`);
}
// Generic/placeholder wording must not survive into a curated seed.
const text = JSON.stringify(seed).toLowerCase();
for (const phrase of GENERIC_PHRASES) {
if (text.includes(phrase)) warnings.push(`[${where}] Contains generic/placeholder wording: "${phrase}".`);
}
return warnings;
}
function chapterCoverageWarnings(coverage: PhysicsQaCoverage): string[] {
const warnings: string[] = [];
if (!coverage.hasConcept) warnings.push("No concept mission.");
if (!coverage.hasExamPractice) warnings.push("No exam-answer mission.");
if (!coverage.hasRevision) warnings.push("No revision mission.");
if (!coverage.hasVisualBoard) warnings.push("No visual-board-supported mission.");
if (!coverage.hasMalayalamSupport) warnings.push("Some seed is missing Malayalam support.");
if (!coverage.hasManglishSupport) warnings.push("Some seed is missing Manglish support.");
return warnings;
}
export function buildPhysicsQaReport(): PhysicsQaReport {
// Day 20 β the seed pool now includes the first Chemistry draft; this
// report is the PHYSICS exam-readiness QA, so it scopes to Physics seeds.
const seeds = getAllLessonSeeds().filter((seed) => seed.subject === "Physics");
const byChapter = new Map<string, TuitionLessonSeed[]>();
for (const seed of seeds) {
byChapter.set(seed.chapterId, [...(byChapter.get(seed.chapterId) ?? []), seed]);
}
const blockers: string[] = [];
const chapters: PhysicsQaChapter[] = Array.from(byChapter.entries()).map(([chapterId, chapterSeeds]) => {
const coverage = coverageFor(chapterSeeds);
const seedWarnings = chapterSeeds.flatMap(seedQaWarnings);
const coverageWarnings = chapterCoverageWarnings(coverage);
const warnings = [...coverageWarnings, ...seedWarnings];
const needsHumanReviewSeeds = chapterSeeds
.filter((s) => s.qualityFlags.needsHumanReview)
.map((s) => s.missionId);
// Blockers: anything that makes the chapter unsafe to sell.
if (chapterSeeds.length === 0) blockers.push(`${chapterId}: chapter has zero seeds.`);
for (const w of coverageWarnings) {
if (!w.startsWith("Some seed is missing")) blockers.push(`${chapterId}: ${w}`);
else blockers.push(`${chapterId}: ${w}`);
}
// Any hard validator error is a blocker (source refs, 11 steps, fake claims, curated PYQ, etc.).
for (const seed of chapterSeeds) {
const errors = validateLessonSeed(seed).errors;
if (errors.length > 0) blockers.push(`${chapterId}/${seed.missionId}: ${errors.length} validation error(s).`);
if (seed.qualityFlags.needsHumanReview && !seed.qualityFlags.reason?.trim()) {
blockers.push(`${chapterId}/${seed.missionId}: needsHumanReview without a reason.`);
}
}
return {
chapterId,
chapterTitle: chapterSeeds[0]?.chapterTitle ?? chapterId,
seedCount: chapterSeeds.length,
coverage,
warnings,
needsHumanReviewSeeds,
};
});
const globalWarnings: string[] = [];
if (byChapter.size !== 7) globalWarnings.push(`Expected 7 curated Physics chapters, found ${byChapter.size}.`);
const qualitativeNoNumerical = chapters.filter((c) => !c.coverage.hasNumerical).map((c) => c.chapterId);
if (qualitativeNoNumerical.length) {
globalWarnings.push(
`Chapters without a numerical mission (qualitative β informational, not a blocker): ${qualitativeNoNumerical.join(", ")}.`,
);
}
const reviewFlagged = chapters.flatMap((c) => c.needsHumanReviewSeeds.map((m) => `${c.chapterId}/${m}`));
const uniqueBlockers = Array.from(new Set(blockers));
let status: PhysicsQaReport["productionReadiness"]["status"];
if (uniqueBlockers.length > 0) {
status = "not_ready";
} else if (reviewFlagged.length > 0) {
status = "ready_with_review_flags";
} else {
status = "ready";
}
const recommendedFixes: string[] = [];
for (const flagged of reviewFlagged) {
recommendedFixes.push(`Verify ${flagged} against the SCERT textbook, then clear its needsHumanReview flag.`);
}
if (uniqueBlockers.length > 0) {
recommendedFixes.push("Resolve all blockers above before selling Physics as a paid product.");
}
return {
totalChapters: byChapter.size,
totalSeeds: seeds.length,
chapters,
globalWarnings,
productionReadiness: {
status,
blockers: uniqueBlockers,
recommendedFixes,
},
};
}
|