DocDoeAI / src /video-engine /physics /validation.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
4.17 kB
import type { LessonContract, VisualAction, VisualBeatGraph } from "./contracts";
export type EngineValidationIssue = {
severity: "error" | "warning";
code: string;
message: string;
beatId?: string;
};
const IMPLEMENTED_ACTIONS = new Set<VisualAction["type"]>([
"add_object",
"remove_object",
"move_object",
"draw_arrow",
"write_text",
"write_equation",
"reveal_label",
"highlight",
"dim",
"focus",
"plot_graph",
"animate_particles",
"trace_wave",
"show_measurement",
"question",
"thinking_pause",
"hint",
"answer_reveal",
"clear_region",
"mascot_reaction",
]);
export function validateVisualBeatGraph(graph: VisualBeatGraph): EngineValidationIssue[] {
const issues: EngineValidationIssue[] = [];
let previousEnd = 0;
const questions = new Map<string, { beatId: string; order: number }>();
const answers = new Map<string, { beatId: string; order: number }>();
graph.beats.forEach((beat, index) => {
if (beat.order !== index + 1) {
issues.push({ severity: "error", code: "BEAT_ORDER", message: "Beat order must be contiguous.", beatId: beat.beatId });
}
if (beat.estimatedStartMs !== previousEnd) {
issues.push({ severity: "error", code: "TIMELINE_GAP", message: "Beat timeline must be contiguous.", beatId: beat.beatId });
}
if (beat.estimatedEndMs <= beat.estimatedStartMs) {
issues.push({ severity: "error", code: "INVALID_DURATION", message: "Beat duration must be positive.", beatId: beat.beatId });
}
if (!beat.sourceReferences.length) {
issues.push({ severity: "error", code: "UNGROUNDED_BEAT", message: "Every beat requires a textbook source.", beatId: beat.beatId });
}
for (const ref of beat.sourceReferences) {
if (!ref.sourceId || !ref.chapterId || !ref.pageNumber || ref.excerptHash.length < 16) {
issues.push({ severity: "error", code: "INVALID_SOURCE_REF", message: "Source reference is incomplete.", beatId: beat.beatId });
}
}
for (const action of beat.actions) {
if (!IMPLEMENTED_ACTIONS.has(action.type)) {
issues.push({ severity: "error", code: "UNSUPPORTED_ACTION", message: `Action ${action.type} is not implemented.`, beatId: beat.beatId });
}
if (action.type === "question") questions.set(action.objectId, { beatId: beat.beatId, order: beat.order });
if (action.type === "answer_reveal") answers.set(action.objectId, { beatId: beat.beatId, order: beat.order });
}
previousEnd = beat.estimatedEndMs;
});
if (previousEnd !== graph.durationMs) {
issues.push({ severity: "error", code: "DURATION_MISMATCH", message: "Graph duration must equal the final beat end." });
}
for (const [objectId, question] of questions) {
const answer = answers.get(objectId);
if (!answer || answer.order <= question.order) {
issues.push({ severity: "error", code: "ANSWER_REVEAL_ORDER", message: `Question ${objectId} must be answered in a later beat.`, beatId: question.beatId });
}
}
return issues;
}
export function validateLessonContract(contract: LessonContract, graph: VisualBeatGraph): EngineValidationIssue[] {
const issues = validateVisualBeatGraph(graph);
if (contract.lessonId !== graph.lessonId) {
issues.push({ severity: "error", code: "LESSON_ID_MISMATCH", message: "Lesson and graph IDs differ." });
}
if (!contract.objectives.length || !contract.recap.length) {
issues.push({ severity: "error", code: "INCOMPLETE_TEACHING_CONTRACT", message: "Objectives and recap are required." });
}
if (contract.teachingSequence.length !== graph.beats.length) {
issues.push({ severity: "error", code: "BEAT_COUNT_MISMATCH", message: "Teaching sequence and visual graph differ." });
}
const covered = new Set(contract.sourceCoverage.flatMap((entry) => entry.narrationBeatIds));
for (const beat of graph.beats) {
if (!covered.has(beat.beatId)) {
issues.push({ severity: "warning", code: "COVERAGE_GAP", message: "Beat is not listed in the coverage matrix.", beatId: beat.beatId });
}
}
return issues;
}
export function implementedVisualActions() {
return [...IMPLEMENTED_ACTIONS].sort();
}