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,383 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative } from "node:path";
|
|
3
|
+
import { parseFrontmatter } from "@astrojs/markdown-remark";
|
|
4
|
+
import { isSubstantiveMeaning } from "../reference/prose";
|
|
5
|
+
import { parseEquationRef } from "../reference/equations.mjs";
|
|
6
|
+
import { resolveLabel } from "../reference/label";
|
|
7
|
+
import {
|
|
8
|
+
deriveRequires,
|
|
9
|
+
deriveSources,
|
|
10
|
+
isLessonSlug,
|
|
11
|
+
lessonIdFromSlug,
|
|
12
|
+
parseCheckList
|
|
13
|
+
} from "./lesson-derivation";
|
|
14
|
+
const REPOSITORY_ROOT = process.cwd();
|
|
15
|
+
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
16
|
+
function filesBelow(directory, extensions) {
|
|
17
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
18
|
+
return entries.flatMap((entry) => {
|
|
19
|
+
const path = join(directory, entry.name);
|
|
20
|
+
if (entry.isDirectory()) return filesBelow(path, extensions);
|
|
21
|
+
return entry.isFile() && extensions.some((extension) => entry.name.endsWith(extension)) ? [path] : [];
|
|
22
|
+
}).sort();
|
|
23
|
+
}
|
|
24
|
+
function parseDocument(path) {
|
|
25
|
+
const parsed = parseFrontmatter(readFileSync(path, "utf8"));
|
|
26
|
+
return {
|
|
27
|
+
file: relative(REPOSITORY_ROOT, path),
|
|
28
|
+
data: parsed.frontmatter ?? {},
|
|
29
|
+
body: parsed.content.replace(/^\n/, "")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function record(value, label) {
|
|
33
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
34
|
+
throw new TypeError(`${label} must be an object`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function string(value, label) {
|
|
39
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
40
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function optionalString(value, label) {
|
|
45
|
+
return value === void 0 ? void 0 : string(value, label);
|
|
46
|
+
}
|
|
47
|
+
function meaningText(value, label) {
|
|
48
|
+
const text = string(value, label);
|
|
49
|
+
if (!isSubstantiveMeaning(text)) {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
`${label} must be substantive prose (at least four words, not a placeholder)`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
function strings(value, label) {
|
|
57
|
+
if (value === void 0) return [];
|
|
58
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
59
|
+
throw new TypeError(`${label} must be an array of strings`);
|
|
60
|
+
}
|
|
61
|
+
return [...value];
|
|
62
|
+
}
|
|
63
|
+
function sourceIds(value, label) {
|
|
64
|
+
if (value === void 0) return [];
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
throw new TypeError(`${label} must be an array`);
|
|
67
|
+
}
|
|
68
|
+
return value.map((item, index) => {
|
|
69
|
+
if (typeof item === "string") return item;
|
|
70
|
+
if (item !== null && typeof item === "object" && "id" in item) {
|
|
71
|
+
return string(item.id, `${label}[${index}].id`);
|
|
72
|
+
}
|
|
73
|
+
throw new TypeError(`${label}[${index}] must be an id or { id, locator }`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function boolean(value, label) {
|
|
77
|
+
if (typeof value !== "boolean") {
|
|
78
|
+
throw new TypeError(`${label} must be a boolean`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function status(value, label) {
|
|
83
|
+
if (value !== "draft" && value !== "in-review" && value !== "reviewed") {
|
|
84
|
+
throw new TypeError(`${label} must be draft, in-review, or reviewed`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function alignment(value, label) {
|
|
89
|
+
const data = record(value, label);
|
|
90
|
+
if (data.kind === "general") {
|
|
91
|
+
return {
|
|
92
|
+
kind: "general",
|
|
93
|
+
rationale: string(data.rationale, `${label}.rationale`)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (data.kind === "competency") {
|
|
97
|
+
return {
|
|
98
|
+
kind: "competency",
|
|
99
|
+
introducedByCompetency: string(
|
|
100
|
+
data.introducedByCompetency,
|
|
101
|
+
`${label}.introducedByCompetency`
|
|
102
|
+
),
|
|
103
|
+
introducedInLesson: string(
|
|
104
|
+
data.introducedInLesson,
|
|
105
|
+
`${label}.introducedInLesson`
|
|
106
|
+
)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
throw new TypeError(`${label}.kind must be competency or general`);
|
|
110
|
+
}
|
|
111
|
+
function sourceSpan(file, text, index) {
|
|
112
|
+
if (text === void 0 || index === void 0) return { file };
|
|
113
|
+
const before = text.slice(0, index);
|
|
114
|
+
const lines = before.split("\n");
|
|
115
|
+
return {
|
|
116
|
+
file,
|
|
117
|
+
line: lines.length,
|
|
118
|
+
column: (lines.at(-1)?.length ?? 0) + 1
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function maskMarkdownLiterals(text) {
|
|
122
|
+
const mask = (value) => value.replace(/[^\n]/g, " ");
|
|
123
|
+
return text.replace(
|
|
124
|
+
/(^|\n)[ \t]{0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n[ \t]{0,3}\2[^\n]*(?=\n|$)|$)/g,
|
|
125
|
+
mask
|
|
126
|
+
).replace(/^(?: {4}|\t).*$/gm, mask).replace(/(`+)(?!`)([^\n]*?)\1/g, mask);
|
|
127
|
+
}
|
|
128
|
+
function extractNotationReferences(text, file, kind) {
|
|
129
|
+
const references = [];
|
|
130
|
+
const searchable = maskMarkdownLiterals(text);
|
|
131
|
+
const patterns = [
|
|
132
|
+
[/\[\[\s*([^\][]+?)\s*\]\]/g, kind ?? "prose"],
|
|
133
|
+
[/\\explain\{([^{}]+)\}/g, kind ?? "math"]
|
|
134
|
+
];
|
|
135
|
+
for (const [pattern, referenceKind] of patterns) {
|
|
136
|
+
for (const match of searchable.matchAll(pattern)) {
|
|
137
|
+
const key = (match[1] ?? "").trim();
|
|
138
|
+
if (parseEquationRef(key)) continue;
|
|
139
|
+
references.push({
|
|
140
|
+
key,
|
|
141
|
+
kind: referenceKind,
|
|
142
|
+
source: sourceSpan(file, text, match.index)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return references.sort(
|
|
147
|
+
(left, right) => (left.source.line ?? 0) - (right.source.line ?? 0) || (left.source.column ?? 0) - (right.source.column ?? 0) || left.key.localeCompare(right.key)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
function jsonEntries(directory) {
|
|
151
|
+
return filesBelow(directory, [".json"]).map((path) => {
|
|
152
|
+
const entry = JSON.parse(readFileSync(path, "utf8"));
|
|
153
|
+
const filenameId = basename(path, ".json");
|
|
154
|
+
if (entry.id !== filenameId) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`${relative(REPOSITORY_ROOT, path)} declares id ${entry.id}; expected filename ${filenameId}.json`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return entry;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function docSlug(path) {
|
|
163
|
+
return relative(join(CONTENT_ROOT, "docs"), path).replace(/\\/g, "/");
|
|
164
|
+
}
|
|
165
|
+
function lessonCheckIds(lessonPath) {
|
|
166
|
+
const checksPath = lessonPath.replace(/\.mdx?$/, ".checks.yml");
|
|
167
|
+
return existsSync(checksPath) ? parseCheckList(readFileSync(checksPath, "utf8")) : [];
|
|
168
|
+
}
|
|
169
|
+
function lessonEntries(prerequisitesById) {
|
|
170
|
+
const lessons = [];
|
|
171
|
+
for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
|
|
172
|
+
const slug = docSlug(path);
|
|
173
|
+
if (!isLessonSlug(slug)) continue;
|
|
174
|
+
const { data, body } = parseDocument(path);
|
|
175
|
+
const teaches = data.teaches ?? [];
|
|
176
|
+
lessons.push({
|
|
177
|
+
id: lessonIdFromSlug(slug),
|
|
178
|
+
status: data.editorialStatus ?? "draft",
|
|
179
|
+
requires: deriveRequires(teaches, prerequisitesById),
|
|
180
|
+
teaches,
|
|
181
|
+
assessments: lessonCheckIds(path),
|
|
182
|
+
sources: deriveSources(body),
|
|
183
|
+
assumptions: data.assumptions ?? [],
|
|
184
|
+
body
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return lessons;
|
|
188
|
+
}
|
|
189
|
+
async function loadCurriculumCatalog() {
|
|
190
|
+
const competencies = jsonEntries(
|
|
191
|
+
join(CONTENT_ROOT, "competencies")
|
|
192
|
+
);
|
|
193
|
+
const prerequisitesById = new Map(
|
|
194
|
+
competencies.map((competency) => [
|
|
195
|
+
competency.id,
|
|
196
|
+
competency.prerequisites ?? []
|
|
197
|
+
])
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
competencies,
|
|
201
|
+
lessons: lessonEntries(prerequisitesById),
|
|
202
|
+
assessments: jsonEntries(
|
|
203
|
+
join(CONTENT_ROOT, "assessments")
|
|
204
|
+
),
|
|
205
|
+
sources: jsonEntries(join(CONTENT_ROOT, "sources")),
|
|
206
|
+
tracks: jsonEntries(join(CONTENT_ROOT, "tracks"))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function localDefinition(raw, file, index) {
|
|
210
|
+
const at = `${file} notation.local[${index}]`;
|
|
211
|
+
const data = record(raw, at);
|
|
212
|
+
const key = string(data.key, `${at}.key`);
|
|
213
|
+
const latex = string(data.latex, `${at}.latex`);
|
|
214
|
+
const meaning = meaningText(data.meaning, `${at}.meaning`);
|
|
215
|
+
const formula = optionalString(data.formula, `${at}.formula`);
|
|
216
|
+
const units = optionalString(data.units, `${at}.units`);
|
|
217
|
+
const definitionText = [meaning, formula].filter((value) => value !== void 0).join("\n");
|
|
218
|
+
return {
|
|
219
|
+
key,
|
|
220
|
+
notation: latex,
|
|
221
|
+
label: resolveLabel(key, optionalString(data.label, `${at}.label`)),
|
|
222
|
+
summary: meaning,
|
|
223
|
+
sources: sourceIds(data.sources, `${at}.sources`),
|
|
224
|
+
seeAlso: strings(data.seeAlso, `${at}.seeAlso`),
|
|
225
|
+
alignment: data.alignment === void 0 ? { kind: "general", rationale: "Lesson-local symbol." } : alignment(data.alignment, `${at}.alignment`),
|
|
226
|
+
references: extractNotationReferences(definitionText, file, "definition"),
|
|
227
|
+
source: { file },
|
|
228
|
+
...formula === void 0 ? {} : { formula },
|
|
229
|
+
...units === void 0 ? {} : { units }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function sharedNotationEntries() {
|
|
233
|
+
const directory = join(CONTENT_ROOT, "notation");
|
|
234
|
+
return filesBelow(directory, [".md"]).map((path) => {
|
|
235
|
+
const { file, data, body } = parseDocument(path);
|
|
236
|
+
const key = string(data.key, `${file} key`);
|
|
237
|
+
const filenameKey = basename(path, ".md");
|
|
238
|
+
if (key !== filenameKey) {
|
|
239
|
+
throw new Error(`${file} declares key ${key}; expected ${filenameKey}`);
|
|
240
|
+
}
|
|
241
|
+
const units = optionalString(data.units, `${file} units`);
|
|
242
|
+
const formula = optionalString(data.formula, `${file} formula`);
|
|
243
|
+
const meaning = meaningText(data.meaning, `${file} meaning`);
|
|
244
|
+
return {
|
|
245
|
+
key,
|
|
246
|
+
notation: string(data.latex, `${file} latex`),
|
|
247
|
+
label: resolveLabel(key, optionalString(data.label, `${file} label`)),
|
|
248
|
+
summary: meaning,
|
|
249
|
+
aliases: strings(data.aliases, `${file} aliases`),
|
|
250
|
+
domain: string(data.domain, `${file} domain`),
|
|
251
|
+
sources: sourceIds(data.sources, `${file} sources`),
|
|
252
|
+
seeAlso: strings(data.seeAlso, `${file} seeAlso`),
|
|
253
|
+
alignment: alignment(data.alignment, `${file} alignment`),
|
|
254
|
+
status: status(data.editorialStatus, `${file} editorialStatus`),
|
|
255
|
+
aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
|
|
256
|
+
body,
|
|
257
|
+
references: extractNotationReferences(body, file, void 0),
|
|
258
|
+
source: { file },
|
|
259
|
+
...formula === void 0 ? {} : { formula },
|
|
260
|
+
...units === void 0 ? {} : { units }
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function notationLessonEntries() {
|
|
265
|
+
const lessons = [];
|
|
266
|
+
for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
|
|
267
|
+
const slug = docSlug(path);
|
|
268
|
+
if (!isLessonSlug(slug)) continue;
|
|
269
|
+
const { file, data, body } = parseDocument(path);
|
|
270
|
+
const notation = record(data.notation ?? {}, `${file} notation`);
|
|
271
|
+
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
272
|
+
lessons.push({
|
|
273
|
+
lessonId: lessonIdFromSlug(slug),
|
|
274
|
+
status: status(data.editorialStatus, `${file} editorialStatus`),
|
|
275
|
+
localDefinitions: local.map(
|
|
276
|
+
(entry, index) => localDefinition(entry, file, index)
|
|
277
|
+
),
|
|
278
|
+
references: extractNotationReferences(body, file, void 0),
|
|
279
|
+
body,
|
|
280
|
+
source: { file }
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return lessons;
|
|
284
|
+
}
|
|
285
|
+
async function loadNotationRegistryInput() {
|
|
286
|
+
return {
|
|
287
|
+
sharedDefinitions: sharedNotationEntries(),
|
|
288
|
+
lessons: notationLessonEntries()
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function loadNotationDefinitions() {
|
|
292
|
+
return filesBelow(join(CONTENT_ROOT, "notation"), [".md"]).map(
|
|
293
|
+
(path) => parseDocument(path).data
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function loadSourceRecords() {
|
|
297
|
+
return jsonEntries(
|
|
298
|
+
join(CONTENT_ROOT, "sources")
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
function loadLessonChecks(lessonId) {
|
|
302
|
+
const slug = lessonId.replace(/\./g, "/");
|
|
303
|
+
for (const extension of [".mdx", ".md"]) {
|
|
304
|
+
const lessonPath = join(CONTENT_ROOT, "docs", `${slug}${extension}`);
|
|
305
|
+
if (existsSync(lessonPath)) return lessonCheckIds(lessonPath);
|
|
306
|
+
}
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
function specSources(value) {
|
|
310
|
+
if (!Array.isArray(value)) return [];
|
|
311
|
+
return value.map((entry) => {
|
|
312
|
+
if (typeof entry === "string") return { id: entry, hasLocator: false };
|
|
313
|
+
const record2 = entry ?? {};
|
|
314
|
+
return {
|
|
315
|
+
id: typeof record2.id === "string" ? record2.id : "",
|
|
316
|
+
hasLocator: typeof record2.locator === "string" && record2.locator.trim().length > 0
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function loadNotationSpecs() {
|
|
321
|
+
const specs = [];
|
|
322
|
+
for (const path of filesBelow(join(CONTENT_ROOT, "notation"), [".md"])) {
|
|
323
|
+
const { file, data } = parseDocument(path);
|
|
324
|
+
const key = typeof data.key === "string" ? data.key : basename(path, ".md");
|
|
325
|
+
specs.push({
|
|
326
|
+
id: `shared:${key}`,
|
|
327
|
+
key,
|
|
328
|
+
scope: "shared",
|
|
329
|
+
file,
|
|
330
|
+
latex: typeof data.latex === "string" ? data.latex : "",
|
|
331
|
+
...typeof data.units === "string" ? { units: data.units } : {},
|
|
332
|
+
dimensionless: data.dimensionless === true,
|
|
333
|
+
...typeof data.formula === "string" ? { formula: data.formula } : {},
|
|
334
|
+
sources: specSources(data.sources)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
|
|
338
|
+
const slug = docSlug(path);
|
|
339
|
+
if (!isLessonSlug(slug)) continue;
|
|
340
|
+
const { file, data } = parseDocument(path);
|
|
341
|
+
const lessonId = lessonIdFromSlug(slug);
|
|
342
|
+
const local = data.notation?.local;
|
|
343
|
+
if (!Array.isArray(local)) continue;
|
|
344
|
+
for (const raw of local) {
|
|
345
|
+
const entry = raw ?? {};
|
|
346
|
+
const key = typeof entry.key === "string" ? entry.key : "";
|
|
347
|
+
specs.push({
|
|
348
|
+
id: `page:${lessonId}:${key}`,
|
|
349
|
+
key,
|
|
350
|
+
scope: "local",
|
|
351
|
+
lessonId,
|
|
352
|
+
file,
|
|
353
|
+
latex: typeof entry.latex === "string" ? entry.latex : "",
|
|
354
|
+
...typeof entry.units === "string" ? { units: entry.units } : {},
|
|
355
|
+
dimensionless: entry.dimensionless === true,
|
|
356
|
+
...typeof entry.formula === "string" ? { formula: entry.formula } : {},
|
|
357
|
+
sources: specSources(entry.sources)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return specs.sort((left, right) => left.id.localeCompare(right.id));
|
|
362
|
+
}
|
|
363
|
+
function notationSourceLocatorGaps() {
|
|
364
|
+
let bare = 0;
|
|
365
|
+
let total = 0;
|
|
366
|
+
for (const spec of loadNotationSpecs()) {
|
|
367
|
+
for (const source of spec.sources) {
|
|
368
|
+
total += 1;
|
|
369
|
+
if (!source.hasLocator) bare += 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { bare, total };
|
|
373
|
+
}
|
|
374
|
+
export {
|
|
375
|
+
extractNotationReferences,
|
|
376
|
+
loadCurriculumCatalog,
|
|
377
|
+
loadLessonChecks,
|
|
378
|
+
loadNotationDefinitions,
|
|
379
|
+
loadNotationRegistryInput,
|
|
380
|
+
loadNotationSpecs,
|
|
381
|
+
loadSourceRecords,
|
|
382
|
+
notationSourceLocatorGaps
|
|
383
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape `content/course.config.ts` must satisfy (Phase F).
|
|
3
|
+
*
|
|
4
|
+
* The engine never imports the course's config file; the wiring layer
|
|
5
|
+
* (`astro.config.mjs`, `scripts/*`, `src/content.config.ts`) imports the plain
|
|
6
|
+
* data value and passes it here, typed against `CourseConfig`. A second course
|
|
7
|
+
* supplies its own value of this shape.
|
|
8
|
+
*/
|
|
9
|
+
interface CourseSection {
|
|
10
|
+
/** Lesson-slug prefix, e.g. `bonds` for `bonds/yield-to-maturity`. */
|
|
11
|
+
readonly dir: string;
|
|
12
|
+
/** Sidebar group heading. */
|
|
13
|
+
readonly label: string;
|
|
14
|
+
}
|
|
15
|
+
interface CourseConventions {
|
|
16
|
+
/** Notation key a lesson's stated sign convention must resolve to. */
|
|
17
|
+
readonly signConventionKey: string;
|
|
18
|
+
/** Competency id that also counts as declaring the cash-flow perspective. */
|
|
19
|
+
readonly cashFlowPerspectiveCompetency: string;
|
|
20
|
+
}
|
|
21
|
+
interface CourseConfig {
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
/** Sidebar section groups, in order. */
|
|
25
|
+
readonly sections: readonly CourseSection[];
|
|
26
|
+
/** Namespaces used by competency / notation `domain` fields. */
|
|
27
|
+
readonly domains: readonly string[];
|
|
28
|
+
readonly conventions: CourseConventions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { CourseConfig, CourseConventions, CourseSection };
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivations for facts that used to be authored in lesson frontmatter
|
|
3
|
+
* (Phase C2). Nothing here reads the filesystem; `collections.ts` supplies the
|
|
4
|
+
* inputs and every consumer (the CLI validator, the Astro build, the Starlight
|
|
5
|
+
* layout, the tests) goes through that one loader.
|
|
6
|
+
*
|
|
7
|
+
* Authored per lesson after C2: `title`, `description`, `teaches`, `assumptions`,
|
|
8
|
+
* `editorialStatus`, `notation.local`. Everything below is derived:
|
|
9
|
+
*
|
|
10
|
+
* - `lessonId` — the doc slug, `/` → `.` (`foundations/discount-factors`).
|
|
11
|
+
* - `requires` — direct competency-graph prerequisites of `teaches`, minus
|
|
12
|
+
* what the lesson teaches itself. `teaches` is kept
|
|
13
|
+
* topologically ordered, so the intra-lesson subtraction is
|
|
14
|
+
* safe for the `lesson-order` check.
|
|
15
|
+
* - `sources` — the `[@…]` ids in the body (the validator already
|
|
16
|
+
* required frontmatter `sources` to equal this set exactly).
|
|
17
|
+
* - `assessments` — the id list in the colocated `<lesson>.checks.yml`.
|
|
18
|
+
* - `sidebar` order — per section, lessons in the order the tracks introduce
|
|
19
|
+
* them (a stable merge of every track's `lessons`).
|
|
20
|
+
*
|
|
21
|
+
* `notation.uses` is retired (D3): the completeness gate compiles every
|
|
22
|
+
* lesson's math against the page glyph table — `notation.local` plus the
|
|
23
|
+
* shared keys the body names with `[[key]]` / `\explain` — so an import list
|
|
24
|
+
* is redundant.
|
|
25
|
+
*/
|
|
26
|
+
/** `foundations/discount-factors` → `foundations.discount-factors`. */
|
|
27
|
+
declare function lessonIdFromSlug(slug: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* A docs entry is a lesson when it lives in a section directory. The three
|
|
30
|
+
* top-level pages (`index`, `curriculum-map`, `glossary`) are not lessons; a
|
|
31
|
+
* future `<section>/index` page would not be one either.
|
|
32
|
+
*/
|
|
33
|
+
declare function isLessonSlug(slug: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Direct prerequisites of everything the lesson `teaches`, minus the taught
|
|
36
|
+
* competencies themselves, sorted. Editorial "you should also know" extras that
|
|
37
|
+
* a few lessons used to list by hand are intentionally not reconstructed — a
|
|
38
|
+
* real dependency belongs in the competency graph, not per lesson.
|
|
39
|
+
*/
|
|
40
|
+
declare function deriveRequires(teaches: readonly string[], prerequisitesById: ReadonlyMap<string, readonly string[]>): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Source ids cited with `[@id]` (optionally `[@id; locator]`) in a lesson body,
|
|
43
|
+
* with fenced and inline code stripped first so a syntax example does not
|
|
44
|
+
* count. Mirrors the reader in `curriculum/validation.ts`.
|
|
45
|
+
*/
|
|
46
|
+
declare function citedSourceIds(body: string): Set<string>;
|
|
47
|
+
/** Cited source ids as a sorted array — the derived frontmatter `sources`. */
|
|
48
|
+
declare function deriveSources(body: string): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Parse a colocated `<lesson>.checks.yml`. The file is a flat YAML list of
|
|
51
|
+
* assessment ids (`- discount-factor-check`); `[]` and an empty file mean no
|
|
52
|
+
* assessments. Deliberately tiny — a colocated check *spec* with inline items
|
|
53
|
+
* is a later step; C2 only relocates the id list.
|
|
54
|
+
*/
|
|
55
|
+
declare function parseCheckList(text: string): string[];
|
|
56
|
+
interface TrackLike {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly lessons: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One global lesson order from every track: start with the tracks sorted by id,
|
|
62
|
+
* then splice each later track's new lessons in right after the last lesson from
|
|
63
|
+
* that track already placed. Lessons shared between tracks keep their first
|
|
64
|
+
* track's position; a lesson in no track is absent (callers append leftovers).
|
|
65
|
+
*/
|
|
66
|
+
declare function mergeTrackOrder(tracks: readonly TrackLike[]): string[];
|
|
67
|
+
/** One sidebar section group. Data lives in `content/course.config.ts` (Phase F). */
|
|
68
|
+
interface SidebarSection {
|
|
69
|
+
readonly dir: string;
|
|
70
|
+
readonly label: string;
|
|
71
|
+
}
|
|
72
|
+
type SidebarGroup = {
|
|
73
|
+
readonly label: string;
|
|
74
|
+
readonly items: readonly string[];
|
|
75
|
+
} | {
|
|
76
|
+
readonly label: string;
|
|
77
|
+
readonly items: readonly {
|
|
78
|
+
readonly label: string;
|
|
79
|
+
readonly link: string;
|
|
80
|
+
}[];
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The Starlight `sidebar` config: one group per section, lessons ordered by the
|
|
84
|
+
* merged track order (ties broken by slug), then the static Reference group.
|
|
85
|
+
* Replaces seven `autogenerate` blocks that read an authored `sidebar.order`.
|
|
86
|
+
*/
|
|
87
|
+
declare function buildSidebar(tracks: readonly TrackLike[], lessonSlugs: readonly string[], sections: readonly SidebarSection[]): SidebarGroup[];
|
|
88
|
+
|
|
89
|
+
export { type SidebarGroup, type SidebarSection, buildSidebar, citedSourceIds, deriveRequires, deriveSources, isLessonSlug, lessonIdFromSlug, mergeTrackOrder, parseCheckList };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function lessonIdFromSlug(slug) {
|
|
2
|
+
return slug.replace(/\.(md|mdx)$/, "").replace(/\//g, ".");
|
|
3
|
+
}
|
|
4
|
+
function isLessonSlug(slug) {
|
|
5
|
+
const clean = slug.replace(/\.(md|mdx)$/, "");
|
|
6
|
+
return clean.includes("/") && !clean.endsWith("/index");
|
|
7
|
+
}
|
|
8
|
+
function deriveRequires(teaches, prerequisitesById) {
|
|
9
|
+
const taught = new Set(teaches);
|
|
10
|
+
const required = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const competencyId of teaches) {
|
|
12
|
+
for (const prerequisiteId of prerequisitesById.get(competencyId) ?? []) {
|
|
13
|
+
if (!taught.has(prerequisiteId)) required.add(prerequisiteId);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return [...required].sort();
|
|
17
|
+
}
|
|
18
|
+
function citedSourceIds(body) {
|
|
19
|
+
const withoutCode = body.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
20
|
+
const ids = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const match of withoutCode.matchAll(
|
|
22
|
+
/\[@([a-z0-9][a-z0-9.-]*)(?:;[^\]]*)?\]/g
|
|
23
|
+
)) {
|
|
24
|
+
ids.add((match[1] ?? "").trim());
|
|
25
|
+
}
|
|
26
|
+
return ids;
|
|
27
|
+
}
|
|
28
|
+
function deriveSources(body) {
|
|
29
|
+
return [...citedSourceIds(body)].sort();
|
|
30
|
+
}
|
|
31
|
+
function parseCheckList(text) {
|
|
32
|
+
const ids = [];
|
|
33
|
+
for (const rawLine of text.split("\n")) {
|
|
34
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
35
|
+
if (line === "" || line === "[]") continue;
|
|
36
|
+
const match = line.match(/^-\s+(['"]?)([A-Za-z0-9][A-Za-z0-9._-]*)\1$/);
|
|
37
|
+
if (!match) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`checks.yml: expected "- <assessment-id>" lines, got ${JSON.stringify(rawLine)}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
ids.push(match[2]);
|
|
43
|
+
}
|
|
44
|
+
return ids;
|
|
45
|
+
}
|
|
46
|
+
function mergeTrackOrder(tracks) {
|
|
47
|
+
const ordered = [];
|
|
48
|
+
for (const track of [...tracks].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
49
|
+
let anchor = -1;
|
|
50
|
+
for (const lessonId of track.lessons) {
|
|
51
|
+
const at = ordered.indexOf(lessonId);
|
|
52
|
+
if (at !== -1) {
|
|
53
|
+
anchor = at;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
ordered.splice(anchor + 1, 0, lessonId);
|
|
57
|
+
anchor += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return ordered;
|
|
61
|
+
}
|
|
62
|
+
function buildSidebar(tracks, lessonSlugs, sections) {
|
|
63
|
+
const rank = new Map(
|
|
64
|
+
mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index])
|
|
65
|
+
);
|
|
66
|
+
const positionOf = (slug) => rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
|
|
67
|
+
const groups = sections.map(({ dir, label }) => ({
|
|
68
|
+
label,
|
|
69
|
+
items: lessonSlugs.filter((slug) => slug.startsWith(`${dir}/`)).sort(
|
|
70
|
+
(left, right) => positionOf(left) - positionOf(right) || left.localeCompare(right)
|
|
71
|
+
)
|
|
72
|
+
}));
|
|
73
|
+
groups.push({
|
|
74
|
+
label: "Reference",
|
|
75
|
+
items: [
|
|
76
|
+
{ label: "Curriculum map", link: "/curriculum-map/" },
|
|
77
|
+
{ label: "Notation glossary", link: "/glossary/" }
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
return groups;
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
buildSidebar,
|
|
84
|
+
citedSourceIds,
|
|
85
|
+
deriveRequires,
|
|
86
|
+
deriveSources,
|
|
87
|
+
isLessonSlug,
|
|
88
|
+
lessonIdFromSlug,
|
|
89
|
+
mergeTrackOrder,
|
|
90
|
+
parseCheckList
|
|
91
|
+
};
|