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.
- package/README.md +108 -0
- package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
- package/dist/analytics/AnalyticsEmitter.js +5 -0
- package/dist/compiler/cli.d.ts +189 -0
- package/dist/compiler/cli.js +552 -0
- package/dist/compiler/collections.d.ts +70 -0
- package/dist/compiler/collections.js +383 -0
- package/dist/compiler/course-config.d.ts +31 -0
- package/dist/compiler/course-config.js +0 -0
- package/dist/compiler/lesson-derivation.d.ts +89 -0
- package/dist/compiler/lesson-derivation.js +91 -0
- package/dist/compiler/manifest.d.ts +178 -0
- package/dist/compiler/manifest.js +345 -0
- package/dist/compiler/resolution-report.d.ts +45 -0
- package/dist/compiler/resolution-report.js +97 -0
- package/dist/curriculum/validation.d.ts +76 -0
- package/dist/curriculum/validation.js +320 -0
- package/dist/progress/ProgressRepository.d.ts +53 -0
- package/dist/progress/ProgressRepository.js +12 -0
- package/dist/reference/base-library.mjs +27 -0
- package/dist/reference/citation-format.mjs +133 -0
- package/dist/reference/consistency.d.ts +64 -0
- package/dist/reference/consistency.js +234 -0
- package/dist/reference/curriculum-alignment.d.ts +7 -0
- package/dist/reference/curriculum-alignment.js +166 -0
- package/dist/reference/equations-validate.d.ts +50 -0
- package/dist/reference/equations-validate.js +90 -0
- package/dist/reference/equations.mjs +144 -0
- package/dist/reference/gate-math.d.ts +41 -0
- package/dist/reference/gate-math.js +151 -0
- package/dist/reference/katex-options.mjs +91 -0
- package/dist/reference/lab-math-scope.d.ts +21 -0
- package/dist/reference/lab-math-scope.js +28 -0
- package/dist/reference/label.d.ts +11 -0
- package/dist/reference/label.js +38 -0
- package/dist/reference/lint-ignore.d.ts +32 -0
- package/dist/reference/lint-ignore.js +39 -0
- package/dist/reference/math-glyphs.mjs +830 -0
- package/dist/reference/prose.d.ts +15 -0
- package/dist/reference/prose.js +12 -0
- package/dist/reference/references.d.ts +17 -0
- package/dist/reference/references.js +42 -0
- package/dist/reference/registry.d.ts +6 -0
- package/dist/reference/registry.js +548 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +753 -0
- package/dist/reference/render-lab-math.d.ts +21 -0
- package/dist/reference/render-lab-math.js +58 -0
- package/dist/reference/types.d.ts +151 -0
- package/dist/reference/types.js +0 -0
- package/dist/reference/units-vocab.d.ts +26 -0
- package/dist/reference/units-vocab.js +119 -0
- package/dist/session/PreferenceStore.d.ts +28 -0
- package/dist/session/PreferenceStore.js +30 -0
- package/dist/session/user-context.d.ts +7 -0
- package/dist/session/user-context.js +10 -0
- package/dist/session/user.d.ts +21 -0
- package/dist/session/user.js +8 -0
- package/package.json +53 -0
- package/src/analytics/AnalyticsEmitter.ts +23 -0
- package/src/compiler/cli.ts +866 -0
- package/src/compiler/collections.ts +554 -0
- package/src/compiler/course-config.ts +32 -0
- package/src/compiler/lesson-derivation.ts +186 -0
- package/src/compiler/manifest.ts +601 -0
- package/src/compiler/resolution-report.ts +150 -0
- package/src/components/CurriculumMap.astro +208 -0
- package/src/components/HoverPanel.astro +67 -0
- package/src/components/assessments/AssessmentRunner.tsx +320 -0
- package/src/components/assessments/AssessmentSet.astro +54 -0
- package/src/components/citation/CitationLayer.astro +173 -0
- package/src/components/examples/CompactExample.astro +34 -0
- package/src/components/examples/CompactExamples.astro +253 -0
- package/src/components/hover-panel.ts +273 -0
- package/src/components/notation/NotationGlossary.astro +412 -0
- package/src/components/notation/NotationLayer.astro +594 -0
- package/src/components/notation/NotationMath.astro +24 -0
- package/src/components/notation/SafeMathNode.astro +23 -0
- package/src/components/notation/render-notation-math.ts +223 -0
- package/src/components/starlight/EquationEnhancer.astro +72 -0
- package/src/components/starlight/LayoutHeader.astro +102 -0
- package/src/components/starlight/LayoutPageSidebar.astro +33 -0
- package/src/components/starlight/LayoutSidebar.astro +29 -0
- package/src/components/starlight/LessonFooter.astro +58 -0
- package/src/content-config.ts +259 -0
- package/src/curriculum/validation.ts +481 -0
- package/src/progress/ProgressRepository.ts +64 -0
- package/src/reference/base-library.mjs +27 -0
- package/src/reference/citation-format.mjs +133 -0
- package/src/reference/consistency.ts +375 -0
- package/src/reference/curriculum-alignment.ts +228 -0
- package/src/reference/equations-validate.ts +167 -0
- package/src/reference/equations.mjs +144 -0
- package/src/reference/gate-math.ts +250 -0
- package/src/reference/katex-options.mjs +91 -0
- package/src/reference/lab-math-scope.ts +54 -0
- package/src/reference/label.ts +47 -0
- package/src/reference/lint-ignore.ts +75 -0
- package/src/reference/math-glyphs.mjs +830 -0
- package/src/reference/prose.ts +21 -0
- package/src/reference/references.ts +82 -0
- package/src/reference/registry.ts +723 -0
- package/src/reference/rehype-fail-katex-errors.mjs +55 -0
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +753 -0
- package/src/reference/render-lab-math.ts +99 -0
- package/src/reference/types.ts +182 -0
- package/src/reference/units-vocab.ts +143 -0
- package/src/session/PreferenceStore.ts +54 -0
- package/src/session/user-context.ts +18 -0
- package/src/session/user.ts +24 -0
- 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
|
+
};
|