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,178 @@
1
+ import { CurriculumIssue } from '../curriculum/validation.js';
2
+ import { ConsistencyDiagnostic } from '../reference/consistency.js';
3
+ import { EquationLabelRecord, EquationDiagnostic } from '../reference/equations-validate.js';
4
+ import { MathGateDiagnostic } from '../reference/gate-math.js';
5
+ import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus } from '../reference/types.js';
6
+ import { CourseConfig } from './course-config.js';
7
+ import { SidebarGroup } from './lesson-derivation.js';
8
+ import './collections.js';
9
+ import '../reference/lint-ignore.js';
10
+
11
+ /**
12
+ * The one compiler manifest (Phase D5).
13
+ *
14
+ * `compileManifest()` walks the content tree exactly once — through the single
15
+ * loader in `collections.ts` — builds the notation registry, runs every
16
+ * diagnostic pass, and assembles one deterministic JSON document. Everything
17
+ * downstream reads that document and nothing else:
18
+ *
19
+ * - `pnpm validate:content` (`scripts/compile-manifest.ts`) regenerates and
20
+ * writes it, then reports diagnostics.
21
+ * - `astro.config.mjs` reads the sidebar order and the raw notation / source
22
+ * records the remark adapters need.
23
+ * - The page components (`LessonFooter`, `NotationGlossary`, `CurriculumMap`,
24
+ * `AssessmentSet`) read resolved bundles, backlinks and lesson metadata.
25
+ * None re-reads the filesystem or rebuilds the registry.
26
+ * - `pnpm build` runs `validate:content` first, so the file is always current
27
+ * before `astro build`.
28
+ *
29
+ * The MDX render path (`getCollection` / `render(entry)`) is unchanged: the
30
+ * manifest replaces the *derived* structures, not the Markdown pipeline.
31
+ *
32
+ * Determinism: `serializeManifest` sorts object keys recursively, every array
33
+ * is ordered by a stable key, paths are repo-relative, and nothing records a
34
+ * timestamp. Two compiles of one tree produce a byte-identical file and
35
+ * therefore a byte-identical `dist/`. This is the contract a future MCP server
36
+ * or LMS importer consumes — see `docs/architecture.md` section 15.
37
+ */
38
+
39
+ /**
40
+ * Bump when a consumer-visible shape changes. A client asserts against this
41
+ * before trusting the document (`manifest-version-compat`, Tier 5).
42
+ *
43
+ * v2 (D6): `lessons[].notation.resolution` (symbol → key → scope → span) and
44
+ * `diagnostics.consistency` (Tier-3 corpus checks).
45
+ * v3 (D7): `equations` (labelled display equations + cross-page number table),
46
+ * `lessons[].equations`, and `diagnostics.equations`.
47
+ */
48
+ declare const MANIFEST_SCHEMA_VERSION = 3;
49
+ declare const MANIFEST_PATH: string;
50
+ interface ManifestCitation {
51
+ readonly id: string;
52
+ readonly locator?: string;
53
+ }
54
+ interface PrereqEdge {
55
+ readonly from: string;
56
+ readonly to: string;
57
+ }
58
+ /**
59
+ * One resolved notation reference occurrence — the row of the committed
60
+ * per-lesson resolution report (`resolution/<lessonId>.notation.json`).
61
+ */
62
+ interface ManifestNotationResolution {
63
+ /** The resolved definition's LaTeX glyph, or the raw key if unresolved. */
64
+ readonly symbol: string;
65
+ readonly key: string;
66
+ readonly scope: 'shared' | 'local' | 'unresolved';
67
+ readonly definitionId: string | null;
68
+ readonly kind: 'prose' | 'math' | 'definition';
69
+ readonly line: number | null;
70
+ readonly column: number | null;
71
+ }
72
+ /** One resolved notation entry, denormalized for the lesson footer. */
73
+ interface ManifestLessonNotation {
74
+ readonly id: string;
75
+ readonly key: string;
76
+ readonly scope: 'shared' | 'local';
77
+ readonly notation: string;
78
+ readonly label: string;
79
+ readonly summary: string;
80
+ readonly editorialStatus: EditorialStatus;
81
+ readonly seeAlso: readonly string[];
82
+ readonly units?: string;
83
+ readonly formula?: string;
84
+ }
85
+ interface ManifestLesson {
86
+ readonly id: string;
87
+ /** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
88
+ readonly slug: string;
89
+ readonly file: string;
90
+ readonly title: string;
91
+ readonly description: string;
92
+ readonly status: EditorialStatus;
93
+ /** Ordered — the curriculum contract. */
94
+ readonly teaches: readonly string[];
95
+ readonly requires: readonly string[];
96
+ readonly assumptions: readonly string[];
97
+ readonly assessments: readonly string[];
98
+ readonly sources: readonly string[];
99
+ readonly citations: readonly ManifestCitation[];
100
+ /** Competency prerequisite edges this lesson introduces. */
101
+ readonly prereqEdges: readonly PrereqEdge[];
102
+ /** Labelled display equations in appearance order (D7). */
103
+ readonly equations: readonly {
104
+ readonly key: string;
105
+ readonly number: string;
106
+ }[];
107
+ readonly notation: {
108
+ readonly bindings: readonly {
109
+ readonly key: string;
110
+ readonly definitionId: string;
111
+ }[];
112
+ readonly definitionIds: readonly string[];
113
+ readonly definitions: readonly ManifestLessonNotation[];
114
+ /** Every `[[key]]` / `\explain{key}` occurrence, resolved and located. */
115
+ readonly resolution: readonly ManifestNotationResolution[];
116
+ };
117
+ }
118
+ interface Manifest {
119
+ readonly schemaVersion: number;
120
+ readonly notation: {
121
+ readonly definitions: readonly NotationDefinitionRecord[];
122
+ readonly bundles: readonly NotationPageBundle[];
123
+ readonly backlinks: readonly NotationBacklink[];
124
+ readonly diagnostics: readonly NotationDiagnostic[];
125
+ /** Raw notation frontmatter for `remarkNotation`, ordered by key. */
126
+ readonly raw: readonly Record<string, unknown>[];
127
+ };
128
+ readonly competencies: readonly Record<string, unknown>[];
129
+ readonly sources: readonly Record<string, unknown>[];
130
+ readonly tracks: readonly Record<string, unknown>[];
131
+ readonly sidebar: readonly SidebarGroup[];
132
+ readonly lessons: readonly ManifestLesson[];
133
+ /** Full competency prerequisite graph. */
134
+ readonly prereqEdges: readonly PrereqEdge[];
135
+ /** Equation identity (D7): every labelled display equation + the render-path lookup. */
136
+ readonly equations: {
137
+ readonly labels: readonly EquationLabelRecord[];
138
+ /** `slug -> { eqKey: number }` — passed to `remarkNotation` for `[[slug#eq:key]]`. */
139
+ readonly numbersBySlug: Record<string, Record<string, string>>;
140
+ };
141
+ readonly diagnostics: {
142
+ readonly curriculum: readonly CurriculumIssue[];
143
+ readonly notation: readonly NotationDiagnostic[];
144
+ readonly alignment: readonly NotationDiagnostic[];
145
+ readonly math: readonly MathGateDiagnostic[];
146
+ /** Tier-3 corpus consistency checks (D6). Warnings only. */
147
+ readonly consistency: readonly ConsistencyDiagnostic[];
148
+ /** Equation-identity checks (D7): `eq-duplicate-key`, `eq-ref-resolves` (error); `eq-key-unused` (warning). */
149
+ readonly equations: readonly EquationDiagnostic[];
150
+ };
151
+ readonly hashes: {
152
+ /** `sha256` of every content source file, keyed by repo-relative path. */
153
+ readonly content: Record<string, string>;
154
+ /** Single fingerprint of the content tree. */
155
+ readonly contentTree: string;
156
+ };
157
+ }
158
+ /**
159
+ * The engine never imports the course's config file — the caller passes it.
160
+ * `astro.config.mjs` and `scripts/*` import `content/course.config.ts` (they are
161
+ * the wiring layer); tests pass a `CourseConfig` value directly.
162
+ */
163
+ declare function compileManifest(courseConfig: CourseConfig): Promise<Manifest>;
164
+ /** JSON with recursively sorted object keys — the byte-identical contract. */
165
+ declare function serializeManifest(manifest: Manifest): string;
166
+ declare function writeManifest(manifest: Manifest): void;
167
+ declare function readManifestSync(): Manifest;
168
+ /**
169
+ * The reader every consumer uses. Reads `build/manifest.json` when it exists —
170
+ * the case for every page component, since `pnpm build` / `pnpm verify` run
171
+ * `validate:content` first. Only a cold `astro dev` misses, and `astro.config.mjs`
172
+ * passes `courseConfig` there so this can compile once; an engine consumer that
173
+ * hits a cold miss with no config gets a clear "run validate:content" error
174
+ * rather than the engine reaching into the course tree.
175
+ */
176
+ declare function loadManifest(courseConfig?: CourseConfig): Promise<Manifest>;
177
+
178
+ export { MANIFEST_PATH, MANIFEST_SCHEMA_VERSION, type Manifest, type ManifestCitation, type ManifestLesson, type ManifestLessonNotation, type ManifestNotationResolution, type PrereqEdge, compileManifest, loadManifest, readManifestSync, serializeManifest, writeManifest };
@@ -0,0 +1,345 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ writeFileSync
8
+ } from "node:fs";
9
+ import { dirname, join, relative } from "node:path";
10
+ import { parseFrontmatter } from "@astrojs/markdown-remark";
11
+ import {
12
+ validateCurriculum
13
+ } from "../curriculum/validation";
14
+ import {
15
+ runConsistencyChecks
16
+ } from "../reference/consistency";
17
+ import { validateNotationAlignment } from "../reference/curriculum-alignment";
18
+ import {
19
+ validateEquations
20
+ } from "../reference/equations-validate";
21
+ import {
22
+ gateContentMath
23
+ } from "../reference/gate-math";
24
+ import { loadLintIgnore } from "../reference/lint-ignore";
25
+ import { buildNotationRegistry } from "../reference/registry";
26
+ import { resolveReferenceId } from "../reference/references";
27
+ import {
28
+ loadCurriculumCatalog,
29
+ loadNotationDefinitions,
30
+ loadNotationRegistryInput,
31
+ loadNotationSpecs,
32
+ loadSourceRecords
33
+ } from "./collections";
34
+ import {
35
+ buildSidebar,
36
+ deriveSources,
37
+ isLessonSlug
38
+ } from "./lesson-derivation";
39
+ const MANIFEST_SCHEMA_VERSION = 3;
40
+ const REPOSITORY_ROOT = process.cwd();
41
+ const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
42
+ const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
43
+ const CITE_PATTERN = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
44
+ function lessonCitations(body) {
45
+ const withoutCode = body.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
46
+ const seen = /* @__PURE__ */ new Set();
47
+ const citations = [];
48
+ for (const match of withoutCode.matchAll(CITE_PATTERN)) {
49
+ const id = (match[1] ?? "").trim();
50
+ const locator = (match[2] ?? "").replace(/\s+/g, " ").trim() || void 0;
51
+ const dedupeKey = `${id} ${locator ?? ""}`;
52
+ if (seen.has(dedupeKey)) continue;
53
+ seen.add(dedupeKey);
54
+ citations.push(locator === void 0 ? { id } : { id, locator });
55
+ }
56
+ return citations;
57
+ }
58
+ function contentHashes() {
59
+ const files = [];
60
+ const walk = (directory) => {
61
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
62
+ const path = join(directory, entry.name);
63
+ if (entry.isDirectory()) walk(path);
64
+ else if (entry.isFile()) files.push(path);
65
+ }
66
+ };
67
+ walk(CONTENT_ROOT);
68
+ files.sort();
69
+ const content = {};
70
+ const tree = createHash("sha256");
71
+ for (const path of files) {
72
+ const relPath = relative(REPOSITORY_ROOT, path).replace(/\\/g, "/");
73
+ const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
74
+ content[relPath] = `sha256:${digest}`;
75
+ tree.update(`${relPath} ${digest}
76
+ `);
77
+ }
78
+ return { content, contentTree: `sha256:${tree.digest("hex")}` };
79
+ }
80
+ function lessonNotationResolution(lesson, definitionsById, availableDefinitionIds) {
81
+ const scope = { kind: "page", lessonId: lesson.lessonId };
82
+ return lesson.references.map((reference) => {
83
+ const definitionId = resolveReferenceId(
84
+ reference.key,
85
+ scope,
86
+ availableDefinitionIds
87
+ );
88
+ const definition = definitionId === void 0 ? void 0 : definitionsById.get(definitionId);
89
+ return {
90
+ symbol: definition?.notation ?? reference.key,
91
+ key: reference.key,
92
+ scope: definition ? definition.kind === "shared" ? "shared" : "local" : "unresolved",
93
+ definitionId: definitionId ?? null,
94
+ kind: reference.kind,
95
+ line: reference.source.line ?? null,
96
+ column: reference.source.column ?? null
97
+ };
98
+ });
99
+ }
100
+ function lessonNotationBundle(bundle, definitionsById, resolution) {
101
+ const definitions = [];
102
+ for (const definitionId of bundle?.definitionIds ?? []) {
103
+ const definition = definitionsById.get(definitionId);
104
+ if (!definition) continue;
105
+ definitions.push({
106
+ id: definition.id,
107
+ key: definition.key,
108
+ scope: definition.kind === "shared" ? "shared" : "local",
109
+ notation: definition.notation,
110
+ label: definition.label,
111
+ summary: definition.summary,
112
+ editorialStatus: definition.status,
113
+ seeAlso: [...definition.seeAlso],
114
+ ...definition.units === void 0 ? {} : { units: definition.units },
115
+ ...definition.formula === void 0 ? {} : { formula: definition.formula }
116
+ });
117
+ }
118
+ return {
119
+ bindings: [...bundle?.bindings ?? []].map((binding) => ({ ...binding })),
120
+ definitionIds: [...bundle?.definitionIds ?? []],
121
+ definitions,
122
+ resolution: [...resolution]
123
+ };
124
+ }
125
+ function lessonFrontmatterById() {
126
+ const base = join(CONTENT_ROOT, "docs");
127
+ const result = /* @__PURE__ */ new Map();
128
+ const walk = (directory) => {
129
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
130
+ const path = join(directory, entry.name);
131
+ if (entry.isDirectory()) {
132
+ walk(path);
133
+ continue;
134
+ }
135
+ if (!entry.isFile() || !/\.mdx?$/.test(entry.name)) continue;
136
+ const slug = relative(base, path).replace(/\\/g, "/");
137
+ if (!isLessonSlug(slug)) continue;
138
+ const id = slug.replace(/\.(md|mdx)$/, "").replace(/\//g, ".");
139
+ const data = parseFrontmatter(readFileSync(path, "utf8")).frontmatter ?? {};
140
+ result.set(id, {
141
+ title: typeof data.title === "string" ? data.title : id,
142
+ description: typeof data.description === "string" ? data.description : ""
143
+ });
144
+ }
145
+ };
146
+ walk(base);
147
+ return result;
148
+ }
149
+ async function compileManifest(courseConfig) {
150
+ const [catalog, notationInput] = await Promise.all([
151
+ loadCurriculumCatalog(),
152
+ loadNotationRegistryInput()
153
+ ]);
154
+ const registry = buildNotationRegistry(notationInput);
155
+ const definitionsById = new Map(
156
+ registry.definitions.map((definition) => [definition.id, definition])
157
+ );
158
+ const availableDefinitionIds = new Set(definitionsById.keys());
159
+ const bundlesByLesson = new Map(
160
+ registry.bundles.map((bundle) => [bundle.lessonId, bundle])
161
+ );
162
+ const curriculumIssues = validateCurriculum(catalog);
163
+ const alignmentIssues = validateNotationAlignment(registry, catalog);
164
+ const mathIssues = gateContentMath({
165
+ notationInput,
166
+ assessments: catalog.assessments,
167
+ lessonAssessments: catalog.lessons.map((lesson) => ({
168
+ lessonId: lesson.id,
169
+ assessmentIds: lesson.assessments
170
+ }))
171
+ });
172
+ const prerequisitesById = new Map(
173
+ catalog.competencies.map((competency) => [
174
+ competency.id,
175
+ competency.prerequisites ?? []
176
+ ])
177
+ );
178
+ const sortEdges = (edges) => edges.sort(
179
+ (a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)
180
+ );
181
+ const prereqEdges = sortEdges(
182
+ [...prerequisitesById].flatMap(
183
+ ([to, prerequisites]) => prerequisites.map((from) => ({ from, to }))
184
+ )
185
+ );
186
+ const lessonMetaById = new Map(
187
+ catalog.lessons.map((lesson) => [lesson.id, lesson])
188
+ );
189
+ const frontmatterById = lessonFrontmatterById();
190
+ const equationValidation = validateEquations(
191
+ notationInput.lessons.map((lesson) => ({
192
+ lessonId: lesson.lessonId,
193
+ slug: lesson.lessonId.replace(/\./g, "/"),
194
+ file: lesson.source.file,
195
+ body: lesson.body
196
+ }))
197
+ );
198
+ const equationsBySlug = new Map(
199
+ Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
200
+ slug,
201
+ Object.entries(table).map(([key, number]) => ({ key, number }))
202
+ ])
203
+ );
204
+ const lessons = [];
205
+ for (const notationLesson of notationInput.lessons) {
206
+ const id = notationLesson.lessonId;
207
+ const meta = lessonMetaById.get(id);
208
+ if (!meta) continue;
209
+ const teaches = meta.teaches;
210
+ const frontmatter = frontmatterById.get(id) ?? {
211
+ title: id,
212
+ description: ""
213
+ };
214
+ lessons.push({
215
+ id,
216
+ slug: id.replace(/\./g, "/"),
217
+ file: notationLesson.source.file,
218
+ title: frontmatter.title,
219
+ description: frontmatter.description,
220
+ status: meta.status,
221
+ teaches: [...teaches],
222
+ requires: [...meta.requires],
223
+ assumptions: [...meta.assumptions],
224
+ assessments: [...meta.assessments],
225
+ sources: deriveSources(notationLesson.body),
226
+ citations: lessonCitations(notationLesson.body),
227
+ equations: equationsBySlug.get(id.replace(/\./g, "/")) ?? [],
228
+ prereqEdges: sortEdges(
229
+ teaches.flatMap(
230
+ (to) => (prerequisitesById.get(to) ?? []).filter((from) => !teaches.includes(from)).map((from) => ({ from, to }))
231
+ )
232
+ ),
233
+ notation: lessonNotationBundle(
234
+ bundlesByLesson.get(id),
235
+ definitionsById,
236
+ lessonNotationResolution(
237
+ notationLesson,
238
+ definitionsById,
239
+ availableDefinitionIds
240
+ )
241
+ )
242
+ });
243
+ }
244
+ lessons.sort((a, b) => a.id.localeCompare(b.id));
245
+ const consistencyIssues = runConsistencyChecks({
246
+ registry,
247
+ notationInput,
248
+ specs: loadNotationSpecs(),
249
+ catalog,
250
+ lintIgnore: loadLintIgnore(),
251
+ conventions: courseConfig.conventions
252
+ });
253
+ const rawNotation = [...loadNotationDefinitions()].sort(
254
+ (a, b) => String(a.key).localeCompare(String(b.key))
255
+ );
256
+ const competencies = catalog.competencies.map((entry) => ({ ...entry })).sort(byId);
257
+ const sources = [...loadSourceRecords()].sort(byId);
258
+ const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
259
+ const lessonSlugs = [...notationInput.lessons].map((lesson) => lesson.lessonId.replace(/\./g, "/")).sort();
260
+ const sidebar = buildSidebar(
261
+ catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
262
+ lessonSlugs,
263
+ courseConfig.sections
264
+ );
265
+ return {
266
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
267
+ notation: {
268
+ definitions: registry.definitions,
269
+ bundles: registry.bundles,
270
+ backlinks: registry.backlinks,
271
+ diagnostics: registry.diagnostics,
272
+ raw: rawNotation
273
+ },
274
+ competencies,
275
+ sources,
276
+ tracks,
277
+ sidebar,
278
+ lessons,
279
+ prereqEdges,
280
+ equations: {
281
+ labels: equationValidation.labels,
282
+ numbersBySlug: equationValidation.numbersBySlug
283
+ },
284
+ diagnostics: {
285
+ curriculum: curriculumIssues,
286
+ notation: registry.diagnostics,
287
+ alignment: alignmentIssues,
288
+ math: mathIssues,
289
+ consistency: consistencyIssues,
290
+ equations: equationValidation.diagnostics
291
+ },
292
+ hashes: contentHashes()
293
+ };
294
+ }
295
+ function byId(a, b) {
296
+ return String(a.id).localeCompare(String(b.id));
297
+ }
298
+ function serializeManifest(manifest) {
299
+ const sortKeys = (value) => {
300
+ if (Array.isArray(value)) return value.map(sortKeys);
301
+ if (value && typeof value === "object") {
302
+ return Object.fromEntries(
303
+ Object.keys(value).sort().map((key) => [
304
+ key,
305
+ sortKeys(value[key])
306
+ ])
307
+ );
308
+ }
309
+ return value;
310
+ };
311
+ return `${JSON.stringify(sortKeys(manifest), null, 2)}
312
+ `;
313
+ }
314
+ function writeManifest(manifest) {
315
+ mkdirSync(dirname(MANIFEST_PATH), { recursive: true });
316
+ writeFileSync(MANIFEST_PATH, serializeManifest(manifest));
317
+ }
318
+ function readManifestSync() {
319
+ if (!existsSync(MANIFEST_PATH)) {
320
+ throw new Error(
321
+ `Manifest not found at ${relative(REPOSITORY_ROOT, MANIFEST_PATH)}. Run \`pnpm validate:content\` first.`
322
+ );
323
+ }
324
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
325
+ }
326
+ async function loadManifest(courseConfig) {
327
+ if (existsSync(MANIFEST_PATH)) return readManifestSync();
328
+ if (!courseConfig) {
329
+ throw new Error(
330
+ `${relative(REPOSITORY_ROOT, MANIFEST_PATH)} not found. Run \`pnpm validate:content\` first.`
331
+ );
332
+ }
333
+ const manifest = await compileManifest(courseConfig);
334
+ writeManifest(manifest);
335
+ return manifest;
336
+ }
337
+ export {
338
+ MANIFEST_PATH,
339
+ MANIFEST_SCHEMA_VERSION,
340
+ compileManifest,
341
+ loadManifest,
342
+ readManifestSync,
343
+ serializeManifest,
344
+ writeManifest
345
+ };
@@ -0,0 +1,45 @@
1
+ import { Manifest } from './manifest.js';
2
+ import '../curriculum/validation.js';
3
+ import '../reference/consistency.js';
4
+ import './course-config.js';
5
+ import './collections.js';
6
+ import '../reference/types.js';
7
+ import '../reference/lint-ignore.js';
8
+ import '../reference/equations-validate.js';
9
+ import '../reference/gate-math.js';
10
+ import './lesson-derivation.js';
11
+
12
+ /**
13
+ * The committed per-lesson resolution report (Step D6 (d)).
14
+ *
15
+ * `compileManifest()` already resolves every `[[key]]` / `\explain{key}`
16
+ * occurrence to a definition, scope, and source span
17
+ * (`manifest.lessons[].notation.resolution`). That lives in the git-ignored
18
+ * `.generated/manifest.json`, so this module projects it into
19
+ * `resolution/<lessonId>.notation.json` — one small, diff-checkable file per
20
+ * lesson — plus `resolution/consistency-report.json` for the Tier-3 corpus
21
+ * checks. `pnpm validate:content` regenerates them and **fails if a committed
22
+ * file is stale**, so a drift between content and the checked-in report cannot
23
+ * pass CI unnoticed.
24
+ *
25
+ * The reports are deterministic: keys are sorted recursively, rows follow the
26
+ * manifest's stable ordering, and nothing records a timestamp or a content
27
+ * hash — a file changes only when a resolution or a finding changes.
28
+ */
29
+
30
+ declare const RESOLUTION_DIR: string;
31
+ /** `{ 'build/resolution/foo.notation.json': '…', … }` — the full expected tree. */
32
+ declare function buildResolutionReports(manifest: Manifest): Map<string, string>;
33
+ interface ResolutionReportSync {
34
+ readonly written: number;
35
+ /** Repo-relative paths that were out of date (created, changed, or removed). */
36
+ readonly stale: readonly string[];
37
+ }
38
+ /**
39
+ * Write every expected report and prune orphans. Returns the paths that did not
40
+ * already match on disk — the caller turns a non-empty list into a build
41
+ * failure so the checked-in reports stay honest.
42
+ */
43
+ declare function syncResolutionReports(manifest: Manifest): ResolutionReportSync;
44
+
45
+ export { RESOLUTION_DIR, type ResolutionReportSync, buildResolutionReports, syncResolutionReports };
@@ -0,0 +1,97 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ rmSync,
7
+ writeFileSync
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+ import {
11
+ consistencyCounts
12
+ } from "../reference/consistency";
13
+ import { MANIFEST_SCHEMA_VERSION } from "./manifest";
14
+ const REPOSITORY_ROOT = process.cwd();
15
+ const RESOLUTION_DIR = join(REPOSITORY_ROOT, "build", "resolution");
16
+ const RESOLUTION_PREFIX = "build/resolution/";
17
+ const CONSISTENCY_FILE = "consistency-report.json";
18
+ function stableJson(value) {
19
+ const sortKeys = (node) => {
20
+ if (Array.isArray(node)) return node.map(sortKeys);
21
+ if (node && typeof node === "object") {
22
+ return Object.fromEntries(
23
+ Object.keys(node).sort().map((key) => [
24
+ key,
25
+ sortKeys(node[key])
26
+ ])
27
+ );
28
+ }
29
+ return node;
30
+ };
31
+ return `${JSON.stringify(sortKeys(value), null, 2)}
32
+ `;
33
+ }
34
+ function buildResolutionReports(manifest) {
35
+ const files = /* @__PURE__ */ new Map();
36
+ const localKeysByLesson = /* @__PURE__ */ new Map();
37
+ for (const definition of manifest.notation.definitions) {
38
+ if (definition.kind !== "local") continue;
39
+ const list = localKeysByLesson.get(definition.lessonId) ?? [];
40
+ list.push(definition.key);
41
+ localKeysByLesson.set(definition.lessonId, list);
42
+ }
43
+ for (const lesson of manifest.lessons) {
44
+ files.set(
45
+ `${RESOLUTION_PREFIX}${lesson.id}.notation.json`,
46
+ stableJson({
47
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
48
+ lesson: lesson.id,
49
+ file: lesson.file,
50
+ localKeys: [...localKeysByLesson.get(lesson.id) ?? []].sort(),
51
+ references: lesson.notation.resolution
52
+ })
53
+ );
54
+ }
55
+ files.set(
56
+ `${RESOLUTION_PREFIX}${CONSISTENCY_FILE}`,
57
+ stableJson({
58
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
59
+ generatedBy: "pnpm validate:content",
60
+ counts: consistencyCounts(manifest.diagnostics.consistency),
61
+ diagnostics: manifest.diagnostics.consistency
62
+ })
63
+ );
64
+ return files;
65
+ }
66
+ function syncResolutionReports(manifest) {
67
+ const expected = buildResolutionReports(manifest);
68
+ mkdirSync(RESOLUTION_DIR, { recursive: true });
69
+ const onDisk = new Set(
70
+ existsSync(RESOLUTION_DIR) ? readdirSync(RESOLUTION_DIR).filter((name) => name.endsWith(".json")) : []
71
+ );
72
+ const stale = [];
73
+ for (const [relPath, content] of [...expected].sort(
74
+ ([a], [b]) => a.localeCompare(b)
75
+ )) {
76
+ const name = relPath.slice(RESOLUTION_PREFIX.length);
77
+ const absolute = join(RESOLUTION_DIR, name);
78
+ onDisk.delete(name);
79
+ const current = existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
80
+ if (current !== content) {
81
+ writeFileSync(absolute, content);
82
+ stale.push(
83
+ `${relPath} (${current === void 0 ? "missing" : "changed"})`
84
+ );
85
+ }
86
+ }
87
+ for (const orphan of [...onDisk].sort()) {
88
+ rmSync(join(RESOLUTION_DIR, orphan));
89
+ stale.push(`${RESOLUTION_PREFIX}${orphan} (stale, removed)`);
90
+ }
91
+ return { written: expected.size, stale };
92
+ }
93
+ export {
94
+ RESOLUTION_DIR,
95
+ buildResolutionReports,
96
+ syncResolutionReports
97
+ };