explico 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +108 -0
  2. package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
  3. package/dist/analytics/AnalyticsEmitter.js +5 -0
  4. package/dist/compiler/cli.d.ts +189 -0
  5. package/dist/compiler/cli.js +552 -0
  6. package/dist/compiler/collections.d.ts +70 -0
  7. package/dist/compiler/collections.js +383 -0
  8. package/dist/compiler/course-config.d.ts +31 -0
  9. package/dist/compiler/course-config.js +0 -0
  10. package/dist/compiler/lesson-derivation.d.ts +89 -0
  11. package/dist/compiler/lesson-derivation.js +91 -0
  12. package/dist/compiler/manifest.d.ts +178 -0
  13. package/dist/compiler/manifest.js +345 -0
  14. package/dist/compiler/resolution-report.d.ts +45 -0
  15. package/dist/compiler/resolution-report.js +97 -0
  16. package/dist/curriculum/validation.d.ts +76 -0
  17. package/dist/curriculum/validation.js +320 -0
  18. package/dist/progress/ProgressRepository.d.ts +53 -0
  19. package/dist/progress/ProgressRepository.js +12 -0
  20. package/dist/reference/base-library.mjs +27 -0
  21. package/dist/reference/citation-format.mjs +133 -0
  22. package/dist/reference/consistency.d.ts +64 -0
  23. package/dist/reference/consistency.js +234 -0
  24. package/dist/reference/curriculum-alignment.d.ts +7 -0
  25. package/dist/reference/curriculum-alignment.js +166 -0
  26. package/dist/reference/equations-validate.d.ts +50 -0
  27. package/dist/reference/equations-validate.js +90 -0
  28. package/dist/reference/equations.mjs +144 -0
  29. package/dist/reference/gate-math.d.ts +41 -0
  30. package/dist/reference/gate-math.js +151 -0
  31. package/dist/reference/katex-options.mjs +91 -0
  32. package/dist/reference/lab-math-scope.d.ts +21 -0
  33. package/dist/reference/lab-math-scope.js +28 -0
  34. package/dist/reference/label.d.ts +11 -0
  35. package/dist/reference/label.js +38 -0
  36. package/dist/reference/lint-ignore.d.ts +32 -0
  37. package/dist/reference/lint-ignore.js +39 -0
  38. package/dist/reference/math-glyphs.mjs +830 -0
  39. package/dist/reference/prose.d.ts +15 -0
  40. package/dist/reference/prose.js +12 -0
  41. package/dist/reference/references.d.ts +17 -0
  42. package/dist/reference/references.js +42 -0
  43. package/dist/reference/registry.d.ts +6 -0
  44. package/dist/reference/registry.js +548 -0
  45. package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
  46. package/dist/reference/remark-citation.mjs +0 -0
  47. package/dist/reference/remark-notation.mjs +753 -0
  48. package/dist/reference/render-lab-math.d.ts +21 -0
  49. package/dist/reference/render-lab-math.js +58 -0
  50. package/dist/reference/types.d.ts +151 -0
  51. package/dist/reference/types.js +0 -0
  52. package/dist/reference/units-vocab.d.ts +26 -0
  53. package/dist/reference/units-vocab.js +119 -0
  54. package/dist/session/PreferenceStore.d.ts +28 -0
  55. package/dist/session/PreferenceStore.js +30 -0
  56. package/dist/session/user-context.d.ts +7 -0
  57. package/dist/session/user-context.js +10 -0
  58. package/dist/session/user.d.ts +21 -0
  59. package/dist/session/user.js +8 -0
  60. package/package.json +53 -0
  61. package/src/analytics/AnalyticsEmitter.ts +23 -0
  62. package/src/compiler/cli.ts +866 -0
  63. package/src/compiler/collections.ts +554 -0
  64. package/src/compiler/course-config.ts +32 -0
  65. package/src/compiler/lesson-derivation.ts +186 -0
  66. package/src/compiler/manifest.ts +601 -0
  67. package/src/compiler/resolution-report.ts +150 -0
  68. package/src/components/CurriculumMap.astro +208 -0
  69. package/src/components/HoverPanel.astro +67 -0
  70. package/src/components/assessments/AssessmentRunner.tsx +320 -0
  71. package/src/components/assessments/AssessmentSet.astro +54 -0
  72. package/src/components/citation/CitationLayer.astro +173 -0
  73. package/src/components/examples/CompactExample.astro +34 -0
  74. package/src/components/examples/CompactExamples.astro +253 -0
  75. package/src/components/hover-panel.ts +273 -0
  76. package/src/components/notation/NotationGlossary.astro +412 -0
  77. package/src/components/notation/NotationLayer.astro +594 -0
  78. package/src/components/notation/NotationMath.astro +24 -0
  79. package/src/components/notation/SafeMathNode.astro +23 -0
  80. package/src/components/notation/render-notation-math.ts +223 -0
  81. package/src/components/starlight/EquationEnhancer.astro +72 -0
  82. package/src/components/starlight/LayoutHeader.astro +102 -0
  83. package/src/components/starlight/LayoutPageSidebar.astro +33 -0
  84. package/src/components/starlight/LayoutSidebar.astro +29 -0
  85. package/src/components/starlight/LessonFooter.astro +58 -0
  86. package/src/content-config.ts +259 -0
  87. package/src/curriculum/validation.ts +481 -0
  88. package/src/progress/ProgressRepository.ts +64 -0
  89. package/src/reference/base-library.mjs +27 -0
  90. package/src/reference/citation-format.mjs +133 -0
  91. package/src/reference/consistency.ts +375 -0
  92. package/src/reference/curriculum-alignment.ts +228 -0
  93. package/src/reference/equations-validate.ts +167 -0
  94. package/src/reference/equations.mjs +144 -0
  95. package/src/reference/gate-math.ts +250 -0
  96. package/src/reference/katex-options.mjs +91 -0
  97. package/src/reference/lab-math-scope.ts +54 -0
  98. package/src/reference/label.ts +47 -0
  99. package/src/reference/lint-ignore.ts +75 -0
  100. package/src/reference/math-glyphs.mjs +830 -0
  101. package/src/reference/prose.ts +21 -0
  102. package/src/reference/references.ts +82 -0
  103. package/src/reference/registry.ts +723 -0
  104. package/src/reference/rehype-fail-katex-errors.mjs +55 -0
  105. package/src/reference/remark-citation.mjs +0 -0
  106. package/src/reference/remark-notation.mjs +753 -0
  107. package/src/reference/render-lab-math.ts +99 -0
  108. package/src/reference/types.ts +182 -0
  109. package/src/reference/units-vocab.ts +143 -0
  110. package/src/session/PreferenceStore.ts +54 -0
  111. package/src/session/user-context.ts +18 -0
  112. package/src/session/user.ts +24 -0
  113. package/src/styles/global.css +763 -0
@@ -0,0 +1,552 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import {
6
+ createMarkdownProcessor,
7
+ parseFrontmatter
8
+ } from "@astrojs/markdown-remark";
9
+ import rehypeKatex from "rehype-katex";
10
+ import remarkMath from "remark-math";
11
+ import { createNotationKatexOptions } from "../reference/katex-options.mjs";
12
+ import rehypeFailKatexErrors from "../reference/rehype-fail-katex-errors.mjs";
13
+ import remarkCitation from "../reference/remark-citation.mjs";
14
+ import remarkNotation from "../reference/remark-notation.mjs";
15
+ import { isLessonSlug, lessonIdFromSlug } from "./lesson-derivation";
16
+ const REPOSITORY_ROOT = process.cwd();
17
+ const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
18
+ const DOCS_ROOT = join(CONTENT_ROOT, "docs");
19
+ const DOMAIN_ROOT = join(CONTENT_ROOT, "domain");
20
+ class ContentCliError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(message);
24
+ this.name = "ContentCliError";
25
+ this.code = code;
26
+ }
27
+ }
28
+ function mentions(message, lessonId, file) {
29
+ return message.includes(lessonId) || message.includes(file) || message.includes(file.replace(/^content\/docs\//, ""));
30
+ }
31
+ function lessonDiagnostics(manifest, lesson) {
32
+ const out = [];
33
+ const { file, id } = lesson;
34
+ const assessments = new Set(lesson.assessments);
35
+ const withFile = (value) => value === void 0 ? {} : { file: value };
36
+ for (const issue of manifest.diagnostics.curriculum) {
37
+ if (!mentions(issue.message, id, file)) continue;
38
+ out.push({
39
+ code: `curriculum:${issue.kind}`,
40
+ severity: issue.severity === "warning" ? "warning" : "error",
41
+ message: issue.message
42
+ });
43
+ }
44
+ for (const diagnostic of [
45
+ ...manifest.diagnostics.notation.map((d) => ["notation", d]),
46
+ ...manifest.diagnostics.alignment.map((d) => ["alignment", d])
47
+ ]) {
48
+ const [group, entry] = diagnostic;
49
+ const entryFile = entry.source?.file;
50
+ if (entry.lessonId !== id && entryFile !== file && !mentions(entry.message, id, file)) {
51
+ continue;
52
+ }
53
+ out.push({
54
+ code: `${group}:${entry.code}`,
55
+ severity: entry.severity === "warning" ? "warning" : "error",
56
+ message: entry.message,
57
+ ...withFile(entryFile)
58
+ });
59
+ }
60
+ for (const diagnostic of manifest.diagnostics.math) {
61
+ const forLesson = diagnostic.file === file || diagnostic.context === "assessment" && [...assessments].some(
62
+ (assessmentId) => diagnostic.file.startsWith(`${assessmentId} `)
63
+ );
64
+ if (!forLesson) continue;
65
+ out.push({
66
+ code: `math:${diagnostic.context}`,
67
+ severity: diagnostic.severity,
68
+ message: diagnostic.message,
69
+ file: diagnostic.file,
70
+ ...diagnostic.line === void 0 ? {} : { line: diagnostic.line }
71
+ });
72
+ }
73
+ for (const diagnostic of manifest.diagnostics.equations) {
74
+ if (diagnostic.lessonId !== id && diagnostic.file !== file && !mentions(diagnostic.message, id, file)) {
75
+ continue;
76
+ }
77
+ out.push({
78
+ code: `equations:${diagnostic.code}`,
79
+ severity: diagnostic.severity === "warning" ? "warning" : "error",
80
+ message: diagnostic.message,
81
+ ...withFile(diagnostic.file)
82
+ });
83
+ }
84
+ for (const diagnostic of manifest.diagnostics.consistency) {
85
+ if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
86
+ out.push({
87
+ code: `consistency:${diagnostic.code}`,
88
+ severity: "warning",
89
+ message: diagnostic.message,
90
+ ...withFile(diagnostic.file)
91
+ });
92
+ }
93
+ return out.sort(
94
+ (a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message)
95
+ );
96
+ }
97
+ let processorPromise;
98
+ function processorFor(manifest) {
99
+ processorPromise ??= createMarkdownProcessor({
100
+ syntaxHighlight: false,
101
+ smartypants: false,
102
+ remarkPlugins: [
103
+ remarkMath,
104
+ [
105
+ remarkNotation,
106
+ {
107
+ definitions: manifest.notation.raw,
108
+ equations: manifest.equations.numbersBySlug
109
+ }
110
+ ],
111
+ [remarkCitation, { sources: manifest.sources }]
112
+ ],
113
+ rehypePlugins: [
114
+ [rehypeKatex, createNotationKatexOptions()],
115
+ rehypeFailKatexErrors
116
+ ]
117
+ });
118
+ return processorPromise;
119
+ }
120
+ function stripEsm(body) {
121
+ return body.replace(/^(?:import|export)\s[^\n]*\n?/gm, "");
122
+ }
123
+ function renderErrorCode(message) {
124
+ if (/Unresolved notation/i.test(message)) return "render:notation";
125
+ if (/KaTeX|ParseError|katex-error/i.test(message)) return "render:katex";
126
+ if (/Unknown notation key|Malformed notation|Invalid notation key/i.test(
127
+ message
128
+ )) {
129
+ return "render:notation";
130
+ }
131
+ if (/equation reference|display equation|equation label eq:/i.test(message)) {
132
+ return "render:equation";
133
+ }
134
+ if (/Unknown (?:source|citation)|Malformed citation/i.test(message)) {
135
+ return "render:citation";
136
+ }
137
+ return "render:mdx";
138
+ }
139
+ async function renderLesson(manifest, input) {
140
+ const processor = await processorFor(manifest);
141
+ const lessonId = lessonIdFromSlug(
142
+ relative(DOCS_ROOT, input.path).replace(/\\/g, "/")
143
+ );
144
+ try {
145
+ await processor.render(stripEsm(input.body), {
146
+ frontmatter: { ...input.frontmatter, lessonId },
147
+ fileURL: pathToFileURL(input.path)
148
+ });
149
+ return { rendered: true, diagnostics: [] };
150
+ } catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ return {
153
+ rendered: false,
154
+ diagnostics: [
155
+ { code: renderErrorCode(message), severity: "error", message }
156
+ ]
157
+ };
158
+ }
159
+ }
160
+ function estimateTokens(text) {
161
+ return Math.ceil(text.length / 4);
162
+ }
163
+ function readIfExists(path) {
164
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
165
+ }
166
+ function walkFiles(directory) {
167
+ if (!existsSync(directory)) return [];
168
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
169
+ const path = join(directory, entry.name);
170
+ if (entry.isDirectory()) return walkFiles(path);
171
+ return entry.isFile() ? [path] : [];
172
+ });
173
+ }
174
+ function docSetTokens() {
175
+ return ["README.md", "AGENTS.md", join("docs", "architecture.md")].map(
176
+ (relPath) => estimateTokens(readIfExists(join(REPOSITORY_ROOT, relPath)))
177
+ ).reduce((sum, value) => sum + value, 0);
178
+ }
179
+ function corpusTokens() {
180
+ return walkFiles(CONTENT_ROOT).map((path) => estimateTokens(readFileSync(path, "utf8"))).reduce((sum, value) => sum + value, 0);
181
+ }
182
+ function tokenCounts(contextPayload) {
183
+ const context = estimateTokens(JSON.stringify(contextPayload));
184
+ const docSet = docSetTokens();
185
+ const corpus = corpusTokens();
186
+ const baseline = docSet + corpus;
187
+ return {
188
+ context,
189
+ docSet,
190
+ corpus,
191
+ ratio: baseline === 0 ? 0 : Math.round(context / baseline * 1e4) / 1e4
192
+ };
193
+ }
194
+ function normalizeLessonId(raw) {
195
+ const trimmed = raw.trim().replace(/\.mdx?$/, "");
196
+ if (trimmed.includes("/")) return trimmed.replace(/\//g, ".");
197
+ return trimmed;
198
+ }
199
+ function findLesson(manifest, rawId) {
200
+ const id = normalizeLessonId(rawId);
201
+ const lesson = manifest.lessons.find((entry) => entry.id === id);
202
+ if (!lesson) {
203
+ throw new ContentCliError(
204
+ "unknown-lesson",
205
+ `No lesson ${JSON.stringify(id)}. Known ids: ${manifest.lessons.map((entry) => entry.id).join(", ")}`
206
+ );
207
+ }
208
+ return lesson;
209
+ }
210
+ function lessonAbsolutePath(lesson) {
211
+ return join(REPOSITORY_ROOT, lesson.file);
212
+ }
213
+ function lessonRaw(lesson) {
214
+ const parsed = parseFrontmatter(
215
+ readFileSync(lessonAbsolutePath(lesson), "utf8")
216
+ );
217
+ return {
218
+ frontmatter: parsed.frontmatter ?? {},
219
+ body: parsed.content.replace(/^\n/, "")
220
+ };
221
+ }
222
+ const DOMAIN_DIRS_BY_SECTION = {
223
+ foundations: [".", "rates", "valuation"],
224
+ bonds: ["bonds", "rates"],
225
+ rates: ["rates"],
226
+ derivatives: ["derivatives", "valuation"],
227
+ "bond-options": ["bonds", "derivatives", "valuation"],
228
+ credit: ["credit"],
229
+ cds: ["cds", "credit"]
230
+ };
231
+ const EXPORT_FN = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*(\([\s\S]*?\))\s*(?::\s*([^\n{]+?))?\s*\{/g;
232
+ const EXPORT_ARROW = /export\s+const\s+([A-Za-z0-9_]+)\s*(?::\s*[^=\n]+)?=\s*(?:async\s*)?(\([\s\S]*?\))\s*(?::\s*([^=\n]+?))?\s*=>/g;
233
+ function signaturesInFile(path) {
234
+ const source = readFileSync(path, "utf8");
235
+ const out = [];
236
+ for (const pattern of [EXPORT_FN, EXPORT_ARROW]) {
237
+ pattern.lastIndex = 0;
238
+ let match;
239
+ while (match = pattern.exec(source)) {
240
+ const params = (match[2] ?? "").replace(/\s+/g, " ").trim();
241
+ const ret = (match[3] ?? "").replace(/\s+/g, " ").trim();
242
+ out.push(`${match[1]}${params}${ret ? `: ${ret}` : ""}`);
243
+ }
244
+ }
245
+ return out.sort();
246
+ }
247
+ function domainSignaturesForSection(section) {
248
+ const dirs = DOMAIN_DIRS_BY_SECTION[section] ?? [section];
249
+ const files = /* @__PURE__ */ new Set();
250
+ for (const dir of dirs) {
251
+ const base = dir === "." ? DOMAIN_ROOT : join(DOMAIN_ROOT, dir);
252
+ if (dir === ".") {
253
+ for (const entry of existsSync(base) ? readdirSync(base) : []) {
254
+ if (entry.endsWith(".ts")) files.add(join(base, entry));
255
+ }
256
+ } else {
257
+ for (const path of walkFiles(base)) {
258
+ if (path.endsWith(".ts")) files.add(path);
259
+ }
260
+ }
261
+ }
262
+ return [...files].sort().map((path) => ({
263
+ file: relative(REPOSITORY_ROOT, path).replace(/\\/g, "/"),
264
+ signatures: signaturesInFile(path)
265
+ })).filter((entry) => entry.signatures.length > 0);
266
+ }
267
+ function contentStatus(manifest) {
268
+ const draftLessons = manifest.lessons.filter((lesson) => lesson.status === "draft").map((lesson) => lesson.id).sort();
269
+ const draftNotation = manifest.notation.definitions.filter((definition) => definition.status === "draft").map((definition) => definition.id).sort();
270
+ const orphans = manifest.notation.diagnostics.filter((diagnostic) => diagnostic.code === "unused-definition").map((diagnostic) => ({
271
+ code: `notation:${diagnostic.code}`,
272
+ severity: diagnostic.severity === "warning" ? "warning" : "error",
273
+ message: diagnostic.message,
274
+ ...diagnostic.source?.file === void 0 ? {} : { file: diagnostic.source.file }
275
+ })).sort((a, b) => a.message.localeCompare(b.message));
276
+ const allDiagnostics = [
277
+ ...manifest.diagnostics.curriculum.map((d) => d.severity ?? "error"),
278
+ ...manifest.diagnostics.notation.map((d) => d.severity),
279
+ ...manifest.diagnostics.alignment.map((d) => d.severity),
280
+ ...manifest.diagnostics.math.map((d) => d.severity),
281
+ ...manifest.diagnostics.equations.map((d) => d.severity),
282
+ ...manifest.diagnostics.consistency.map(() => "warning")
283
+ ];
284
+ const keyedEquations = manifest.lessons.reduce(
285
+ (count, lesson) => count + lesson.equations.length,
286
+ 0
287
+ );
288
+ const assessments = new Set(
289
+ manifest.lessons.flatMap((lesson) => lesson.assessments)
290
+ ).size;
291
+ return {
292
+ counts: {
293
+ lessons: manifest.lessons.length,
294
+ competencies: manifest.competencies.length,
295
+ assessments,
296
+ sources: manifest.sources.length,
297
+ tracks: manifest.tracks.length,
298
+ notationDefinitions: manifest.notation.definitions.length,
299
+ keyedEquations
300
+ },
301
+ draftDebt: { lessons: draftLessons, notation: draftNotation },
302
+ orphans,
303
+ diagnostics: {
304
+ errors: allDiagnostics.filter((severity) => severity === "error").length,
305
+ warnings: allDiagnostics.filter((severity) => severity === "warning").length
306
+ }
307
+ };
308
+ }
309
+ function contentContext(manifest, rawId) {
310
+ const lesson = findLesson(manifest, rawId);
311
+ const { frontmatter, body } = lessonRaw(lesson);
312
+ const outcomeById = new Map(
313
+ manifest.competencies.map((competency) => [
314
+ String(competency.id),
315
+ String(competency.outcome ?? "")
316
+ ])
317
+ );
318
+ const outcomesFor = (ids) => ids.filter((id) => outcomeById.has(id)).map((id) => ({ id, outcome: outcomeById.get(id) ?? "" }));
319
+ const sourceById = new Map(
320
+ manifest.sources.map((source) => [String(source.id), source])
321
+ );
322
+ const citedLocatorsById = /* @__PURE__ */ new Map();
323
+ for (const citation of lesson.citations) {
324
+ const list = citedLocatorsById.get(citation.id) ?? [];
325
+ if (citation.locator) list.push(citation.locator);
326
+ citedLocatorsById.set(citation.id, list);
327
+ }
328
+ const contextWithoutTokens = {
329
+ lesson: {
330
+ id: lesson.id,
331
+ slug: lesson.slug,
332
+ file: lesson.file,
333
+ title: lesson.title,
334
+ description: lesson.description,
335
+ status: lesson.status,
336
+ teaches: lesson.teaches,
337
+ requires: lesson.requires,
338
+ assumptions: lesson.assumptions,
339
+ notationLocal: frontmatter.notation?.local ?? [],
340
+ body
341
+ },
342
+ taughtOutcomes: outcomesFor(lesson.teaches),
343
+ prerequisiteOutcomes: outcomesFor(lesson.requires),
344
+ notation: lesson.notation.definitions.map((definition) => ({
345
+ key: definition.key,
346
+ latex: definition.notation,
347
+ meaning: definition.summary,
348
+ scope: definition.scope,
349
+ ...definition.units === void 0 ? {} : { units: definition.units },
350
+ ...definition.formula === void 0 ? {} : { formula: definition.formula }
351
+ })),
352
+ equations: lesson.equations.map((equation) => ({ ...equation })),
353
+ sources: lesson.sources.map((id) => {
354
+ const record = sourceById.get(id);
355
+ return {
356
+ id,
357
+ title: String(record?.title ?? id),
358
+ type: String(record?.type ?? "unknown"),
359
+ ...typeof record?.locator === "string" ? { recordLocator: record.locator } : {},
360
+ citedLocators: citedLocatorsById.get(id) ?? []
361
+ };
362
+ }),
363
+ domainSignatures: domainSignaturesForSection(
364
+ lesson.slug.split("/")[0] ?? ""
365
+ ),
366
+ diagnostics: lessonDiagnostics(manifest, lesson)
367
+ };
368
+ return {
369
+ ...contextWithoutTokens,
370
+ tokens: tokenCounts(contextWithoutTokens)
371
+ };
372
+ }
373
+ async function checkOneLesson(manifest, lesson) {
374
+ const manifestDiagnostics = lessonDiagnostics(manifest, lesson);
375
+ const { frontmatter, body } = lessonRaw(lesson);
376
+ const render = await renderLesson(manifest, {
377
+ body,
378
+ frontmatter,
379
+ path: lessonAbsolutePath(lesson)
380
+ });
381
+ const diagnostics = [...manifestDiagnostics, ...render.diagnostics].sort(
382
+ (a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message)
383
+ );
384
+ return {
385
+ lesson: lesson.id,
386
+ file: lesson.file,
387
+ ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
388
+ rendered: render.rendered,
389
+ diagnostics
390
+ };
391
+ }
392
+ function changedLessonIds() {
393
+ let output;
394
+ try {
395
+ const tracked = execFileSync(
396
+ "git",
397
+ ["diff", "--name-only", "HEAD", "--", "content/docs"],
398
+ { cwd: REPOSITORY_ROOT, encoding: "utf8" }
399
+ );
400
+ const untracked = execFileSync(
401
+ "git",
402
+ ["ls-files", "--others", "--exclude-standard", "--", "content/docs"],
403
+ { cwd: REPOSITORY_ROOT, encoding: "utf8" }
404
+ );
405
+ output = `${tracked}
406
+ ${untracked}`;
407
+ } catch {
408
+ throw new ContentCliError(
409
+ "no-git",
410
+ "Could not read git status for --changed. Run inside a git working tree."
411
+ );
412
+ }
413
+ const ids = /* @__PURE__ */ new Set();
414
+ for (const line of output.split("\n")) {
415
+ const path = line.trim();
416
+ if (!path) continue;
417
+ const slug = path.replace(/^content\/docs\//, "").replace(/\.checks\.yml$/, ".mdx").replace(/\.mdx?$/, "");
418
+ if (!isLessonSlug(slug)) continue;
419
+ ids.add(slug.replace(/\//g, "."));
420
+ }
421
+ return [...ids].sort();
422
+ }
423
+ async function contentCheck(manifest, options) {
424
+ let targets;
425
+ if (options.changed) {
426
+ const ids = changedLessonIds();
427
+ targets = manifest.lessons.filter((lesson) => ids.includes(lesson.id));
428
+ } else {
429
+ if (!options.lesson) {
430
+ throw new ContentCliError(
431
+ "usage",
432
+ "content check needs a <lesson> id or --changed."
433
+ );
434
+ }
435
+ targets = [findLesson(manifest, options.lesson)];
436
+ }
437
+ const lessons = await Promise.all(
438
+ targets.map((lesson) => checkOneLesson(manifest, lesson))
439
+ );
440
+ return { ok: lessons.every((entry) => entry.ok), lessons };
441
+ }
442
+ const ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
443
+ function scaffoldLesson(rawId) {
444
+ const slug = rawId.trim().replace(/\.mdx?$/, "").replace(/\./g, "/");
445
+ if (!slug.includes("/")) {
446
+ throw new ContentCliError(
447
+ "invalid-id",
448
+ `A lesson id is <section>/<slug> (or <section>.<slug>), got ${JSON.stringify(rawId)}.`
449
+ );
450
+ }
451
+ if (!isLessonSlug(slug) || slug.split("/").some((part) => !ID_PATTERN.test(part))) {
452
+ throw new ContentCliError(
453
+ "invalid-id",
454
+ `Lesson slug ${JSON.stringify(slug)} is not a valid <section>/<slug>.`
455
+ );
456
+ }
457
+ const relPath = `content/docs/${slug}.mdx`;
458
+ const checksPath = `content/docs/${slug}.checks.yml`;
459
+ if (existsSync(join(REPOSITORY_ROOT, relPath))) {
460
+ throw new ContentCliError(
461
+ "exists",
462
+ `${relPath} already exists; not overwriting.`
463
+ );
464
+ }
465
+ const title = (slug.split("/").at(-1) ?? slug).split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
466
+ const contents = `---
467
+ title: ${title}
468
+ description: One sentence on what the reader can do after this lesson.
469
+ editorialStatus: draft
470
+ teaches: []
471
+ assumptions: []
472
+ ---
473
+
474
+ import CompactExample from '../../../components/examples/CompactExample.astro';
475
+ import AssessmentSet from '../../../components/assessments/AssessmentSet.astro';
476
+
477
+ ## What you will be able to do
478
+
479
+ After this lesson, you should be able to:
480
+
481
+ - <!-- one observable outcome per teaches entry -->
482
+
483
+ ## Intuition
484
+
485
+ <!-- draft prose; every symbol introduced with [[key]] before its first math use -->
486
+
487
+ ## Model
488
+
489
+ <!-- the reproducible worked example; every numeral tagged to a content/domain/ call or [@source; locator] -->
490
+ `;
491
+ return {
492
+ path: relPath,
493
+ contents,
494
+ companions: [
495
+ {
496
+ path: checksPath,
497
+ contents: `# Assessment sets for this lesson. Ids resolve against content/assessments/.
498
+ []
499
+ `
500
+ }
501
+ ]
502
+ };
503
+ }
504
+ function scaffoldTerm(rawKey) {
505
+ const key = rawKey.trim();
506
+ if (!ID_PATTERN.test(key)) {
507
+ throw new ContentCliError(
508
+ "invalid-id",
509
+ `A notation key is lower-case, dot/hyphen separated, got ${JSON.stringify(rawKey)}.`
510
+ );
511
+ }
512
+ const relPath = `content/notation/${key}.md`;
513
+ if (existsSync(join(REPOSITORY_ROOT, relPath))) {
514
+ throw new ContentCliError(
515
+ "exists",
516
+ `${relPath} already exists; not overwriting.`
517
+ );
518
+ }
519
+ const contents = `---
520
+ key: ${key}
521
+ latex: 'x'
522
+ meaning: Describe the quantity this symbol denotes.
523
+ domain: general
524
+ units: stated currency
525
+ sources: []
526
+ seeAlso: []
527
+ alignment:
528
+ kind: general
529
+ rationale: Draft scaffold \u2014 set a real alignment before review.
530
+ editorialStatus: draft
531
+ aiAssisted: true
532
+ ---
533
+
534
+ Draft definition for [[${key}]]. Replace this body and the placeholder
535
+ frontmatter before moving the entry past \`draft\`.
536
+ `;
537
+ return { path: relPath, contents, companions: [] };
538
+ }
539
+ export {
540
+ ContentCliError,
541
+ changedLessonIds,
542
+ contentCheck,
543
+ contentContext,
544
+ contentStatus,
545
+ estimateTokens,
546
+ findLesson,
547
+ lessonDiagnostics,
548
+ normalizeLessonId,
549
+ renderLesson,
550
+ scaffoldLesson,
551
+ scaffoldTerm
552
+ };
@@ -0,0 +1,70 @@
1
+ import { CurriculumCatalog } from '../curriculum/validation.js';
2
+ import { NotationReferenceInput, NotationRegistryInput } from '../reference/types.js';
3
+
4
+ /**
5
+ * The single path from content files to typed data.
6
+ *
7
+ * Every consumer — the `validate:content` CLI, the Astro build (`astro.config.mjs`),
8
+ * the Starlight footer, the lab math scope, and the tests — reads content through
9
+ * this module. It walks `src/content/` once and parses frontmatter with Astro's
10
+ * own `parseFrontmatter` (the same parser the content collections use), so there
11
+ * is no second Markdown/YAML reader and `gray-matter` is no longer a dependency.
12
+ *
13
+ * Replaces `scripts/curriculum-files.ts`, `scripts/notation-files.ts`, and the
14
+ * inline `readdirSync`/`gray-matter` notation + source loads in `astro.config.mjs`.
15
+ */
16
+
17
+ /**
18
+ * Notation references (`[[key]]` in prose, `\explain{key}` in math) in a body,
19
+ * ignoring code fences and inline code, sorted by position.
20
+ */
21
+ declare function extractNotationReferences(text: string, file: string, kind: NotationReferenceInput['kind'] | undefined): NotationReferenceInput[];
22
+ declare function loadCurriculumCatalog(): Promise<CurriculumCatalog>;
23
+ declare function loadNotationRegistryInput(): Promise<NotationRegistryInput>;
24
+ /**
25
+ * Notation frontmatter objects for `remark-notation`'s `definitions` option.
26
+ * Returned synchronously because the remark transform reads the loader inline.
27
+ */
28
+ declare function loadNotationDefinitions(): Record<string, unknown>[];
29
+ /** Parsed `src/content/sources/*.json` records for `remark-citation`. */
30
+ declare function loadSourceRecords(): Record<string, unknown>[];
31
+ /**
32
+ * Assessment ids for one lesson, from its colocated `<name>.checks.yml`. The
33
+ * lesson layout uses this to auto-place `<AssessmentSet>`; nothing is authored
34
+ * in the MDX body.
35
+ */
36
+ declare function loadLessonChecks(lessonId: string): string[];
37
+ /**
38
+ * Flat view of every notation entry (shared + page-local) for the consistency
39
+ * checks (Step D6): the fields those checks read that the registry input drops
40
+ * or normalizes — the raw `dimensionless` marker and the per-source locator
41
+ * shape. One walk of the same files the loader already parses.
42
+ */
43
+ interface NotationSpec {
44
+ /** `shared:<key>` or `page:<lessonId>:<key>` — matches the registry id. */
45
+ readonly id: string;
46
+ readonly key: string;
47
+ readonly scope: 'shared' | 'local';
48
+ readonly lessonId?: string;
49
+ readonly file: string;
50
+ readonly latex: string;
51
+ readonly units?: string;
52
+ readonly dimensionless: boolean;
53
+ readonly formula?: string;
54
+ readonly sources: readonly {
55
+ readonly id: string;
56
+ readonly hasLocator: boolean;
57
+ }[];
58
+ }
59
+ declare function loadNotationSpecs(): NotationSpec[];
60
+ /**
61
+ * Notation `sources` entries still written as a bare id rather than
62
+ * `{ id, locator }` (Phase C1b shape). Formalized as the `notation-source-locator`
63
+ * consistency check in Step D6; kept here for the CLI summary line.
64
+ */
65
+ declare function notationSourceLocatorGaps(): {
66
+ bare: number;
67
+ total: number;
68
+ };
69
+
70
+ export { type NotationSpec, extractNotationReferences, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadSourceRecords, notationSourceLocatorGaps };