Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 1x 5x 5x 5x 27x 5x 5x 1x 4x 4x 3x 3x 3x 3x 3x 8x 8x 1x 27x 27x 27x 27x 27x 27x 27x 27x 27x 3x 3x 3x 9x 6x 6x 6x 3x 3x 8x 8x 20x 20x 20x 20x 20x 20x 20x 2x | import { readFile } from "node:fs/promises";
import path from "node:path";
import { parse as parseYaml } from "yaml";
import { CourseContentLoader, type LoadedCourseContentSnapshot } from "./course-content-loader";
import { validateInlineFocus } from "./inline-focus-validator";
import { tutorQualityCasesSchema } from "./tutor-quality-schema";
import { BUILT_IN_TUTOR_STRATEGY, type EffectiveTutorStrategy } from "../tutor/strategy/effective-tutor-strategy";
import {
validateExampleTopicActivations,
validateTutorContentQuality,
type CatalogExample,
type ResolvedTutorQualityCase,
type TutorContentQualityIssue,
} from "./tutor-quality-validator";
const LOCAL_REVISION = "0".repeat(40);
const LOCAL_REPOSITORY = "local/course-content";
export async function validateTutorCourseContentDirectory(directory: string): Promise<TutorContentQualityIssue[]> {
const root = path.resolve(directory);
let loaded: Awaited<ReturnType<CourseContentLoader["load"]>>;
try {
const loader = new CourseContentLoader({
fetchText: async (url, maxBytes) => readBoundedLocalFile(root, url, maxBytes),
});
loaded = await loader.load(LOCAL_REPOSITORY, LOCAL_REVISION);
} catch {
return [directoryIssue("invalid-course-content-bundle", "Course Content bundle could not be loaded")];
}
if (loaded.tutor.status !== "valid") {
return [directoryIssue("invalid-course-content-bundle", "Course Content Tutor bundle is absent or invalid")];
}
try {
const qualitySource = await readFile(path.join(root, "tutor/quality-cases.yaml"), "utf8");
const parsed = tutorQualityCasesSchema.safeParse(parseYaml(qualitySource));
Iif (!parsed.success) {
return [directoryIssue("invalid-quality-cases", "Tutor quality case manifest is invalid")];
}
const resolution = resolveCases(parsed.data.cases, loaded);
Iif (resolution.issues.length > 0) return resolution.issues;
return [
...validateTutorContentQuality(loaded.tutor.topics, resolution.cases),
...validateExampleTopicActivations(loaded.tutor.topics, catalogExamples(loaded)),
...validateInlineFocus(loaded.examples.flatMap((example) => {
const main = example.files.find(({ name }) => name === example.main);
return main ? [{ id: example.id, code: main.content, annotation: example.tutorAnnotation }] : [];
})),
];
} catch {
return [directoryIssue("invalid-quality-cases", "Tutor quality case manifest could not be loaded")];
}
}
async function readBoundedLocalFile(root: string, url: URL, maxBytes: number): Promise<string> {
const revisionMarker = `/${LOCAL_REVISION}/`;
const markerIndex = url.pathname.indexOf(revisionMarker);
Iif (markerIndex < 0) throw new Error("Invalid local Course Content URL");
const relativePath = decodeURIComponent(url.pathname.slice(markerIndex + revisionMarker.length));
const filePath = path.resolve(root, relativePath);
Iif (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) throw new Error("Path escapes Course Content root");
const source = await readFile(filePath, "utf8");
Iif (Buffer.byteLength(source, "utf8") > maxBytes) throw new Error("Course Content file exceeds size limit");
return source;
}
function resolveCases(
cases: readonly { id: string; example: string; expectedTopics: readonly string[]; forbiddenTopics: readonly string[] }[],
loaded: LoadedCourseContentSnapshot,
): { cases: ResolvedTutorQualityCase[]; issues: TutorContentQualityIssue[] } {
const resolved: ResolvedTutorQualityCase[] = [];
const issues: TutorContentQualityIssue[] = [];
for (const qualityCase of cases) {
const example = loaded.examples.find(({ id }) => id === qualityCase.example);
const main = example?.files.find(({ name }) => name === example.main);
Iif (!example || !main) {
issues.push({
code: "quality-case-example-not-found",
message: `Quality case ${qualityCase.id} references missing Example ${qualityCase.example}`,
caseId: qualityCase.id,
});
continue;
}
resolved.push({
id: qualityCase.id,
exampleId: qualityCase.example,
code: main.content,
expectedTopics: qualityCase.expectedTopics,
forbiddenTopics: qualityCase.forbiddenTopics,
learnStrategy: resolveCaseStrategy(loaded, example.tutorAnnotation?.strategy, "LEARN"),
deepenStrategy: resolveCaseStrategy(loaded, example.tutorAnnotation?.strategy, "DEEPEN"),
});
}
return { cases: resolved, issues };
}
// Every Example's main sketch, as the Tutor receives it, with its effective LEARN strategy.
function catalogExamples(loaded: LoadedCourseContentSnapshot): CatalogExample[] {
return loaded.examples.flatMap((example) => {
const main = example.files.find(({ name }) => name === example.main);
return main
? [{ id: example.id, code: main.content, learnStrategy: resolveCaseStrategy(loaded, example.tutorAnnotation?.strategy, "LEARN") }]
: [];
});
}
function resolveCaseStrategy(
loaded: LoadedCourseContentSnapshot,
exampleStrategyId: string | undefined,
phase: "LEARN" | "DEEPEN",
): EffectiveTutorStrategy {
Iif (loaded.tutor.status !== "valid") return BUILT_IN_TUTOR_STRATEGY;
const perExample = loaded.tutor.strategies.find(({ id }) => id === exampleStrategyId);
Iif (perExample) return perExample;
const manifest = loaded.tutor.manifest;
const phaseStrategyId = phase === "DEEPEN" && manifest.schemaVersion === 2
? manifest.phaseStrategies?.deepen
: undefined;
const strategyId = phaseStrategyId ?? manifest.defaultStrategy;
return loaded.tutor.strategies.find(({ id }) => id === strategyId) ?? BUILT_IN_TUTOR_STRATEGY;
}
function directoryIssue(
code: "invalid-course-content-bundle" | "invalid-quality-cases",
message: string,
): TutorContentQualityIssue {
return { code, message };
}
|