explico 0.2.0 → 0.3.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/CHANGELOG.md +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +77 -2
- package/dist/compiler/manifest.js +115 -18
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +51 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +42 -11
- package/dist/reference/equations.mjs +58 -17
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +199 -9
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +239 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/notation/render-notation-math.ts +13 -2
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +4 -2
- package/src/components/table/Table.astro +104 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +83 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +58 -17
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +199 -9
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +115 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The course's **knowledge version** — semver over the learning-facing content,
|
|
3
|
+
* independent of the `explico` engine's own package version.
|
|
4
|
+
*
|
|
5
|
+
* The declared value lives in `content/course.config.ts` (`about.version`); the
|
|
6
|
+
* history and the human-readable notes live in `content/CHANGELOG.md`
|
|
7
|
+
* (Keep a Changelog shape). This module parses that file and checks the two
|
|
8
|
+
* agree — rule C3 in `docs/authoring/versioning.md`. Every finding is a Tier-3
|
|
9
|
+
* warning: `pnpm validate:content` prints it, `manifest.diagnostics.knowledge`
|
|
10
|
+
* records it, and the build still passes. The resolved entry is what the `[?]`
|
|
11
|
+
* About panel publishes to readers (`manifest.about.knowledge`).
|
|
12
|
+
*
|
|
13
|
+
* Pure: `parseKnowledgeChangelog` takes text, `resolveKnowledgeVersion` takes
|
|
14
|
+
* text + the declared version. The caller (`compileManifest`) does the file IO.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A released entry from `content/CHANGELOG.md`, newest first in file order. */
|
|
18
|
+
export interface KnowledgeChangelogEntry {
|
|
19
|
+
readonly version: string;
|
|
20
|
+
/** The `- YYYY-MM-DD` part of the heading, if present. */
|
|
21
|
+
readonly date?: string;
|
|
22
|
+
/** Bullet lines under the heading, marker stripped, in file order. */
|
|
23
|
+
readonly notes: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type KnowledgeVersionCode =
|
|
27
|
+
| 'knowledge-changelog-missing'
|
|
28
|
+
| 'knowledge-version-invalid'
|
|
29
|
+
| 'knowledge-version-mismatch';
|
|
30
|
+
|
|
31
|
+
export interface KnowledgeVersionDiagnostic {
|
|
32
|
+
readonly code: KnowledgeVersionCode;
|
|
33
|
+
/** Tier 3 — a knowledge-version drift never fails the build. */
|
|
34
|
+
readonly severity: 'warning';
|
|
35
|
+
readonly message: string;
|
|
36
|
+
readonly file?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface KnowledgeVersion {
|
|
40
|
+
/** The declared `about.version`, echoed for consumers. */
|
|
41
|
+
readonly version: string;
|
|
42
|
+
/** The newest released `## [x.y.z]` entry, when the changelog has one. */
|
|
43
|
+
readonly latest?: KnowledgeChangelogEntry;
|
|
44
|
+
readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `1.2.3`, optionally `-rc.1` / `+build.5` — the shape `about.version` and a
|
|
48
|
+
// `## [x.y.z]` heading must both take.
|
|
49
|
+
const SEMVER_RE =
|
|
50
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
51
|
+
const HEADING_RE = /^##\s+\[([^\]]+)\]\s*(?:[-–—]\s*(\S.*?))?\s*$/;
|
|
52
|
+
const BULLET_RE = /^[-*]\s+(.*\S)\s*$/;
|
|
53
|
+
|
|
54
|
+
export function isKnowledgeSemver(value: string): boolean {
|
|
55
|
+
return SEMVER_RE.test(value.trim());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every released entry in a `content/CHANGELOG.md` body, in file order (newest
|
|
60
|
+
* first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
|
|
61
|
+
*/
|
|
62
|
+
export function parseKnowledgeChangelog(
|
|
63
|
+
text: string,
|
|
64
|
+
): KnowledgeChangelogEntry[] {
|
|
65
|
+
const lines = text.split(/\r?\n/);
|
|
66
|
+
const entries: {
|
|
67
|
+
version: string;
|
|
68
|
+
date?: string;
|
|
69
|
+
notes: string[];
|
|
70
|
+
}[] = [];
|
|
71
|
+
let current: { version: string; date?: string; notes: string[] } | null =
|
|
72
|
+
null;
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const heading = HEADING_RE.exec(line);
|
|
76
|
+
if (heading) {
|
|
77
|
+
const token = (heading[1] ?? '').trim();
|
|
78
|
+
if (/^unreleased$/i.test(token)) {
|
|
79
|
+
current = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
current = { version: token, notes: [] };
|
|
83
|
+
const date = (heading[2] ?? '').trim();
|
|
84
|
+
if (date) current.date = date;
|
|
85
|
+
entries.push(current);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// A top-level `#` or a deeper `###` heading ends the current entry's notes.
|
|
89
|
+
if (/^#{1,3}\s/.test(line) && !line.startsWith('## [')) {
|
|
90
|
+
current = null;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!current) continue;
|
|
94
|
+
const bullet = BULLET_RE.exec(line);
|
|
95
|
+
if (bullet) current.notes.push(bullet[1] ?? '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return entries.map((entry) => ({
|
|
99
|
+
version: entry.version,
|
|
100
|
+
...(entry.date === undefined ? {} : { date: entry.date }),
|
|
101
|
+
notes: [...entry.notes],
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check the declared knowledge version against `content/CHANGELOG.md`.
|
|
107
|
+
*
|
|
108
|
+
* @param options.changelogText the file body, or `undefined` when it is absent
|
|
109
|
+
* @param options.declaredVersion `courseConfig.about.version`
|
|
110
|
+
* @param options.file repo-relative path, for diagnostic messages
|
|
111
|
+
*/
|
|
112
|
+
export function resolveKnowledgeVersion(options: {
|
|
113
|
+
readonly changelogText: string | undefined;
|
|
114
|
+
readonly declaredVersion: string;
|
|
115
|
+
readonly file?: string;
|
|
116
|
+
}): KnowledgeVersion {
|
|
117
|
+
const { changelogText, declaredVersion } = options;
|
|
118
|
+
const file = options.file ?? 'content/CHANGELOG.md';
|
|
119
|
+
const diagnostics: KnowledgeVersionDiagnostic[] = [];
|
|
120
|
+
const warn = (code: KnowledgeVersionCode, message: string): void => {
|
|
121
|
+
diagnostics.push({ code, severity: 'warning', message, file });
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!isKnowledgeSemver(declaredVersion)) {
|
|
125
|
+
warn(
|
|
126
|
+
'knowledge-version-invalid',
|
|
127
|
+
`about.version ${JSON.stringify(declaredVersion)} in content/course.config.ts is not a semver \`x.y.z\` string.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (changelogText === undefined) {
|
|
132
|
+
warn(
|
|
133
|
+
'knowledge-changelog-missing',
|
|
134
|
+
`No ${file}. Add a Keep a Changelog file whose newest \`## [x.y.z]\` heading is ${JSON.stringify(declaredVersion)} (rule C1).`,
|
|
135
|
+
);
|
|
136
|
+
return { version: declaredVersion, diagnostics };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const entries = parseKnowledgeChangelog(changelogText);
|
|
140
|
+
const latest = entries[0];
|
|
141
|
+
if (!latest) {
|
|
142
|
+
warn(
|
|
143
|
+
'knowledge-changelog-missing',
|
|
144
|
+
`${file} has no released \`## [x.y.z]\` entry — only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C4).`,
|
|
145
|
+
);
|
|
146
|
+
return { version: declaredVersion, diagnostics };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isKnowledgeSemver(declaredVersion) && latest.version !== declaredVersion) {
|
|
150
|
+
warn(
|
|
151
|
+
'knowledge-version-mismatch',
|
|
152
|
+
`about.version is ${JSON.stringify(declaredVersion)} but the newest ${file} entry is ${JSON.stringify(latest.version)}. Bump one to match the other (rule C3).`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { version: declaredVersion, latest, diagnostics };
|
|
157
|
+
}
|
package/src/compiler/manifest.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
readdirSync,
|
|
35
35
|
writeFileSync,
|
|
36
36
|
} from 'node:fs';
|
|
37
|
+
import { createRequire } from 'node:module';
|
|
37
38
|
import { dirname, join, relative } from 'node:path';
|
|
38
39
|
import { parseFrontmatter } from '@astrojs/markdown-remark';
|
|
39
40
|
|
|
@@ -51,6 +52,16 @@ import {
|
|
|
51
52
|
type EquationDiagnostic,
|
|
52
53
|
type EquationLabelRecord,
|
|
53
54
|
} from '../reference/equations-validate';
|
|
55
|
+
import {
|
|
56
|
+
validateTables,
|
|
57
|
+
type TableDiagnostic,
|
|
58
|
+
type TableLabelRecord,
|
|
59
|
+
} from '../reference/tables-validate';
|
|
60
|
+
import {
|
|
61
|
+
validateFigures,
|
|
62
|
+
type FigureDiagnostic,
|
|
63
|
+
type FigureLabelRecord,
|
|
64
|
+
} from '../reference/figures-validate';
|
|
54
65
|
import {
|
|
55
66
|
gateContentMath,
|
|
56
67
|
type MathGateDiagnostic,
|
|
@@ -70,6 +81,10 @@ import type {
|
|
|
70
81
|
NotationPageBundle,
|
|
71
82
|
} from '../reference/types';
|
|
72
83
|
import type { CourseConfig } from './course-config';
|
|
84
|
+
import {
|
|
85
|
+
resolveKnowledgeVersion,
|
|
86
|
+
type KnowledgeVersionDiagnostic,
|
|
87
|
+
} from './knowledge-version';
|
|
73
88
|
import {
|
|
74
89
|
loadCurriculumCatalog,
|
|
75
90
|
loadNotationDefinitions,
|
|
@@ -81,6 +96,7 @@ import {
|
|
|
81
96
|
buildSidebar,
|
|
82
97
|
deriveSources,
|
|
83
98
|
isLessonSlug,
|
|
99
|
+
lessonIdFromSlug,
|
|
84
100
|
type SidebarGroup,
|
|
85
101
|
} from './lesson-derivation';
|
|
86
102
|
|
|
@@ -92,8 +108,16 @@ import {
|
|
|
92
108
|
* `diagnostics.consistency` (Tier-3 corpus checks).
|
|
93
109
|
* v3 (D7): `equations` (labelled display equations + cross-page number table),
|
|
94
110
|
* `lessons[].equations`, and `diagnostics.equations`.
|
|
111
|
+
* v4 (F-tables-figures): `tables`/`figures` (same shape as `equations`, for
|
|
112
|
+
* `<Table>`/`<Figure>`), `lessons[].tables`/`lessons[].figures`, and
|
|
113
|
+
* `diagnostics.tables`/`diagnostics.figures`.
|
|
114
|
+
* v5 (about): `about` (engine identity from `explico`'s `package.json`, the
|
|
115
|
+
* course's reader-facing `about` block, and the knowledge version resolved
|
|
116
|
+
* against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
|
|
117
|
+
* v6 (chapter numbering): `lessons[].chapter` — the lesson's 1-based course
|
|
118
|
+
* ordinal, the chapter in every equation/table/figure number (`(2.1)`).
|
|
95
119
|
*/
|
|
96
|
-
export const MANIFEST_SCHEMA_VERSION =
|
|
120
|
+
export const MANIFEST_SCHEMA_VERSION = 6;
|
|
97
121
|
|
|
98
122
|
const REPOSITORY_ROOT = process.cwd();
|
|
99
123
|
// Content is the top-level `content/` folder; the build output is `build/`
|
|
@@ -101,6 +125,33 @@ const REPOSITORY_ROOT = process.cwd();
|
|
|
101
125
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, 'content');
|
|
102
126
|
export const MANIFEST_PATH = join(REPOSITORY_ROOT, 'build', 'manifest.json');
|
|
103
127
|
|
|
128
|
+
interface EnginePackage {
|
|
129
|
+
readonly name: string;
|
|
130
|
+
readonly version: string;
|
|
131
|
+
readonly homepage?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `explico`'s own identity — the golden source for the engine version the About
|
|
136
|
+
* panel shows (the README points here, not a literal).
|
|
137
|
+
*
|
|
138
|
+
* Resolved as the bare specifier `explico/package.json` (a published `exports`
|
|
139
|
+
* entry, and a package self-reference inside this repo) through `createRequire`,
|
|
140
|
+
* so it survives being re-bundled by a course's Astro build — a relative
|
|
141
|
+
* `../../package.json` would follow the *bundle* location, not the source's.
|
|
142
|
+
* Lazy + cached: importing this module for `readManifestSync` must stay free of
|
|
143
|
+
* filesystem side effects; only `compileManifest` needs the value.
|
|
144
|
+
*/
|
|
145
|
+
let enginePackage: EnginePackage | undefined;
|
|
146
|
+
function readEnginePackage(): EnginePackage {
|
|
147
|
+
if (!enginePackage) {
|
|
148
|
+
enginePackage = createRequire(import.meta.url)(
|
|
149
|
+
'explico/package.json',
|
|
150
|
+
) as EnginePackage;
|
|
151
|
+
}
|
|
152
|
+
return enginePackage;
|
|
153
|
+
}
|
|
154
|
+
|
|
104
155
|
// --- shape ------------------------------------------------------------
|
|
105
156
|
|
|
106
157
|
export interface ManifestCitation {
|
|
@@ -154,6 +205,12 @@ export interface ManifestLesson {
|
|
|
154
205
|
readonly id: string;
|
|
155
206
|
/** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
|
|
156
207
|
readonly slug: string;
|
|
208
|
+
/**
|
|
209
|
+
* The lesson's 1-based ordinal in the course (merged track order, then any
|
|
210
|
+
* unranked lessons by slug). It is the chapter in every equation, table, and
|
|
211
|
+
* figure number on the page — `(2.1)` is the first equation of chapter 2.
|
|
212
|
+
*/
|
|
213
|
+
readonly chapter: number;
|
|
157
214
|
readonly file: string;
|
|
158
215
|
readonly title: string;
|
|
159
216
|
readonly description: string;
|
|
@@ -172,6 +229,16 @@ export interface ManifestLesson {
|
|
|
172
229
|
readonly key: string;
|
|
173
230
|
readonly number: string;
|
|
174
231
|
}[];
|
|
232
|
+
/** Keyed `<Table>` elements in appearance order. */
|
|
233
|
+
readonly tables: readonly {
|
|
234
|
+
readonly key: string;
|
|
235
|
+
readonly number: string;
|
|
236
|
+
}[];
|
|
237
|
+
/** Keyed `<Figure>` elements in appearance order. */
|
|
238
|
+
readonly figures: readonly {
|
|
239
|
+
readonly key: string;
|
|
240
|
+
readonly number: string;
|
|
241
|
+
}[];
|
|
175
242
|
readonly notation: {
|
|
176
243
|
readonly bindings: readonly {
|
|
177
244
|
readonly key: string;
|
|
@@ -184,8 +251,37 @@ export interface ManifestLesson {
|
|
|
184
251
|
};
|
|
185
252
|
}
|
|
186
253
|
|
|
254
|
+
/** The reader-facing "about this material" bundle — the `[?]` panel's data. */
|
|
255
|
+
export interface ManifestAbout {
|
|
256
|
+
/** `explico`'s own identity, from its `package.json` (the version's golden source). */
|
|
257
|
+
readonly engine: {
|
|
258
|
+
readonly name: string;
|
|
259
|
+
readonly version: string;
|
|
260
|
+
readonly homepage?: string;
|
|
261
|
+
};
|
|
262
|
+
readonly material: { readonly name: string; readonly summary: string };
|
|
263
|
+
/** The course's knowledge version — semver over the content, engine-independent. */
|
|
264
|
+
readonly knowledge: {
|
|
265
|
+
readonly version: string;
|
|
266
|
+
/** Date of the newest `content/CHANGELOG.md` entry, if it has one. */
|
|
267
|
+
readonly date?: string;
|
|
268
|
+
/** Bullet notes from that entry. */
|
|
269
|
+
readonly notes: readonly string[];
|
|
270
|
+
/** Content fingerprint at compile time — ties the shown version to a build. */
|
|
271
|
+
readonly contentTree: string;
|
|
272
|
+
};
|
|
273
|
+
readonly authors: readonly {
|
|
274
|
+
readonly name: string;
|
|
275
|
+
readonly contact?: string;
|
|
276
|
+
}[];
|
|
277
|
+
readonly disclaimer: string;
|
|
278
|
+
readonly changelogHref?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
187
281
|
export interface Manifest {
|
|
188
282
|
readonly schemaVersion: number;
|
|
283
|
+
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
284
|
+
readonly about: ManifestAbout;
|
|
189
285
|
readonly notation: {
|
|
190
286
|
readonly definitions: readonly NotationDefinitionRecord[];
|
|
191
287
|
readonly bundles: readonly NotationPageBundle[];
|
|
@@ -207,6 +303,16 @@ export interface Manifest {
|
|
|
207
303
|
/** `slug -> { eqKey: number }` — passed to `remarkNotation` for `[[slug#eq:key]]`. */
|
|
208
304
|
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
209
305
|
};
|
|
306
|
+
/** Keyed-float identity for `<Table>` — same shape as `equations`. */
|
|
307
|
+
readonly tables: {
|
|
308
|
+
readonly labels: readonly TableLabelRecord[];
|
|
309
|
+
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
310
|
+
};
|
|
311
|
+
/** Keyed-float identity for `<Figure>` — same shape as `equations`. */
|
|
312
|
+
readonly figures: {
|
|
313
|
+
readonly labels: readonly FigureLabelRecord[];
|
|
314
|
+
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
315
|
+
};
|
|
210
316
|
readonly diagnostics: {
|
|
211
317
|
readonly curriculum: readonly CurriculumIssue[];
|
|
212
318
|
readonly notation: readonly NotationDiagnostic[];
|
|
@@ -214,8 +320,14 @@ export interface Manifest {
|
|
|
214
320
|
readonly math: readonly MathGateDiagnostic[];
|
|
215
321
|
/** Tier-3 corpus consistency checks (D6). Warnings only. */
|
|
216
322
|
readonly consistency: readonly ConsistencyDiagnostic[];
|
|
323
|
+
/** Knowledge-version checks (C3): `about.version` vs `content/CHANGELOG.md`. Warnings only. */
|
|
324
|
+
readonly knowledge: readonly KnowledgeVersionDiagnostic[];
|
|
217
325
|
/** Equation-identity checks (D7): `eq-duplicate-key`, `eq-ref-resolves` (error); `eq-key-unused` (warning). */
|
|
218
326
|
readonly equations: readonly EquationDiagnostic[];
|
|
327
|
+
/** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
|
|
328
|
+
readonly tables: readonly TableDiagnostic[];
|
|
329
|
+
/** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
|
|
330
|
+
readonly figures: readonly FigureDiagnostic[];
|
|
219
331
|
};
|
|
220
332
|
readonly hashes: {
|
|
221
333
|
/** `sha256` of every content source file, keyed by repo-relative path. */
|
|
@@ -395,6 +507,22 @@ function lessonFrontmatterById(): Map<
|
|
|
395
507
|
export async function compileManifest(
|
|
396
508
|
courseConfig: CourseConfig,
|
|
397
509
|
): Promise<Manifest> {
|
|
510
|
+
// `about` became required in explico 0.3.0. A course upgrading from 0.2.x
|
|
511
|
+
// hits this before the raw `Cannot read properties of undefined` further
|
|
512
|
+
// down; the message names the fix and the guide.
|
|
513
|
+
if (
|
|
514
|
+
courseConfig.about === undefined ||
|
|
515
|
+
courseConfig.about.version === undefined ||
|
|
516
|
+
courseConfig.about.authors === undefined ||
|
|
517
|
+
courseConfig.about.disclaimer === undefined
|
|
518
|
+
) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
"course.config.ts is missing a complete `about` block (required since explico 0.3.0): " +
|
|
521
|
+
"`about: { version, authors: [{ name }], disclaimer, materialName?, summary?, changelogHref? }`. " +
|
|
522
|
+
'See docs/authoring/migrating.md (§ 0.2.x → 0.3.0) in the installed package.',
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
398
526
|
const [catalog, notationInput] = await Promise.all([
|
|
399
527
|
loadCurriculumCatalog(),
|
|
400
528
|
loadNotationRegistryInput(),
|
|
@@ -452,20 +580,57 @@ export async function compileManifest(
|
|
|
452
580
|
);
|
|
453
581
|
const frontmatterById = lessonFrontmatterById();
|
|
454
582
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
583
|
+
const lessonSlugs = [...notationInput.lessons]
|
|
584
|
+
.map((lesson) => lesson.lessonId.replace(/\./g, '/'))
|
|
585
|
+
.sort();
|
|
586
|
+
const sidebar = buildSidebar(
|
|
587
|
+
catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
|
|
588
|
+
lessonSlugs,
|
|
589
|
+
courseConfig.sections,
|
|
462
590
|
);
|
|
591
|
+
|
|
592
|
+
// A lesson's chapter is its position when the sidebar is read top to bottom
|
|
593
|
+
// (sections in config order, lessons within a section by merged track order
|
|
594
|
+
// then slug). It prefixes every equation / table / figure number on the page
|
|
595
|
+
// (`(2.1)` = first equation of chapter 2). Derived from `sidebar` so there is
|
|
596
|
+
// one lesson-ordering source, not two.
|
|
597
|
+
const chapterByLessonId = new Map<string, number>();
|
|
598
|
+
let chapterCursor = 0;
|
|
599
|
+
for (const group of sidebar) {
|
|
600
|
+
for (const item of group.items) {
|
|
601
|
+
if (typeof item !== 'string') continue; // the static Reference group
|
|
602
|
+
chapterByLessonId.set(lessonIdFromSlug(item), ++chapterCursor);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const keyedFloatLessonInputs = notationInput.lessons.map((lesson) => ({
|
|
607
|
+
lessonId: lesson.lessonId,
|
|
608
|
+
slug: lesson.lessonId.replace(/\./g, '/'),
|
|
609
|
+
file: lesson.source.file,
|
|
610
|
+
body: lesson.body,
|
|
611
|
+
chapter: chapterByLessonId.get(lesson.lessonId),
|
|
612
|
+
}));
|
|
613
|
+
const equationValidation = validateEquations(keyedFloatLessonInputs);
|
|
463
614
|
const equationsBySlug = new Map(
|
|
464
615
|
Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
|
|
465
616
|
slug,
|
|
466
617
|
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
467
618
|
]),
|
|
468
619
|
);
|
|
620
|
+
const tableValidation = validateTables(keyedFloatLessonInputs);
|
|
621
|
+
const tablesBySlug = new Map(
|
|
622
|
+
Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
|
|
623
|
+
slug,
|
|
624
|
+
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
625
|
+
]),
|
|
626
|
+
);
|
|
627
|
+
const figureValidation = validateFigures(keyedFloatLessonInputs);
|
|
628
|
+
const figuresBySlug = new Map(
|
|
629
|
+
Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
|
|
630
|
+
slug,
|
|
631
|
+
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
632
|
+
]),
|
|
633
|
+
);
|
|
469
634
|
|
|
470
635
|
const lessons: ManifestLesson[] = [];
|
|
471
636
|
for (const notationLesson of notationInput.lessons) {
|
|
@@ -480,6 +645,10 @@ export async function compileManifest(
|
|
|
480
645
|
lessons.push({
|
|
481
646
|
id,
|
|
482
647
|
slug: id.replace(/\./g, '/'),
|
|
648
|
+
// 0 = the lesson is not placed in any configured section; a curriculum
|
|
649
|
+
// diagnostic covers that separately, and both numbering paths then fall
|
|
650
|
+
// back to the legacy `##`-section scheme.
|
|
651
|
+
chapter: chapterByLessonId.get(id) ?? 0,
|
|
483
652
|
file: notationLesson.source.file,
|
|
484
653
|
title: frontmatter.title,
|
|
485
654
|
description: frontmatter.description,
|
|
@@ -491,6 +660,8 @@ export async function compileManifest(
|
|
|
491
660
|
sources: deriveSources(notationLesson.body),
|
|
492
661
|
citations: lessonCitations(notationLesson.body),
|
|
493
662
|
equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
663
|
+
tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
664
|
+
figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
494
665
|
prereqEdges: sortEdges(
|
|
495
666
|
teaches.flatMap((to) =>
|
|
496
667
|
(prerequisitesById.get(to) ?? [])
|
|
@@ -519,6 +690,9 @@ export async function compileManifest(
|
|
|
519
690
|
catalog,
|
|
520
691
|
lintIgnore: loadLintIgnore(),
|
|
521
692
|
conventions: courseConfig.conventions,
|
|
693
|
+
...(courseConfig.operators === undefined
|
|
694
|
+
? {}
|
|
695
|
+
: { operators: courseConfig.operators }),
|
|
522
696
|
});
|
|
523
697
|
|
|
524
698
|
const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
|
|
@@ -530,17 +704,54 @@ export async function compileManifest(
|
|
|
530
704
|
const sources = [...loadSourceRecords()].sort(byId);
|
|
531
705
|
const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
|
|
532
706
|
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
707
|
+
const hashes = contentHashes();
|
|
708
|
+
|
|
709
|
+
// Knowledge version (C3): `about.version` vs the newest `content/CHANGELOG.md`
|
|
710
|
+
// entry. Every finding is a Tier-3 warning; the resolved entry feeds the
|
|
711
|
+
// `[?]` About panel.
|
|
712
|
+
const knowledgeChangelogPath = join(CONTENT_ROOT, 'CHANGELOG.md');
|
|
713
|
+
const knowledge = resolveKnowledgeVersion({
|
|
714
|
+
changelogText: existsSync(knowledgeChangelogPath)
|
|
715
|
+
? readFileSync(knowledgeChangelogPath, 'utf8')
|
|
716
|
+
: undefined,
|
|
717
|
+
declaredVersion: courseConfig.about.version,
|
|
718
|
+
file: relative(REPOSITORY_ROOT, knowledgeChangelogPath).replace(/\\/g, '/'),
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const enginePkg = readEnginePackage();
|
|
722
|
+
const about: ManifestAbout = {
|
|
723
|
+
engine: {
|
|
724
|
+
name: enginePkg.name,
|
|
725
|
+
version: enginePkg.version,
|
|
726
|
+
...(enginePkg.homepage === undefined
|
|
727
|
+
? {}
|
|
728
|
+
: { homepage: enginePkg.homepage }),
|
|
729
|
+
},
|
|
730
|
+
material: {
|
|
731
|
+
name: courseConfig.about.materialName ?? courseConfig.title,
|
|
732
|
+
summary: courseConfig.about.summary ?? courseConfig.description,
|
|
733
|
+
},
|
|
734
|
+
knowledge: {
|
|
735
|
+
version: knowledge.version,
|
|
736
|
+
...(knowledge.latest?.date === undefined
|
|
737
|
+
? {}
|
|
738
|
+
: { date: knowledge.latest.date }),
|
|
739
|
+
notes: knowledge.latest?.notes ?? [],
|
|
740
|
+
contentTree: hashes.contentTree,
|
|
741
|
+
},
|
|
742
|
+
authors: courseConfig.about.authors.map((author) => ({
|
|
743
|
+
name: author.name,
|
|
744
|
+
...(author.contact === undefined ? {} : { contact: author.contact }),
|
|
745
|
+
})),
|
|
746
|
+
disclaimer: courseConfig.about.disclaimer,
|
|
747
|
+
...(courseConfig.about.changelogHref === undefined
|
|
748
|
+
? {}
|
|
749
|
+
: { changelogHref: courseConfig.about.changelogHref }),
|
|
750
|
+
};
|
|
541
751
|
|
|
542
752
|
return {
|
|
543
753
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
754
|
+
about,
|
|
544
755
|
notation: {
|
|
545
756
|
definitions: registry.definitions,
|
|
546
757
|
bundles: registry.bundles,
|
|
@@ -558,6 +769,14 @@ export async function compileManifest(
|
|
|
558
769
|
labels: equationValidation.labels,
|
|
559
770
|
numbersBySlug: equationValidation.numbersBySlug,
|
|
560
771
|
},
|
|
772
|
+
tables: {
|
|
773
|
+
labels: tableValidation.labels,
|
|
774
|
+
numbersBySlug: tableValidation.numbersBySlug,
|
|
775
|
+
},
|
|
776
|
+
figures: {
|
|
777
|
+
labels: figureValidation.labels,
|
|
778
|
+
numbersBySlug: figureValidation.numbersBySlug,
|
|
779
|
+
},
|
|
561
780
|
diagnostics: {
|
|
562
781
|
curriculum: curriculumIssues,
|
|
563
782
|
notation: registry.diagnostics,
|
|
@@ -565,8 +784,11 @@ export async function compileManifest(
|
|
|
565
784
|
math: mathIssues,
|
|
566
785
|
consistency: consistencyIssues,
|
|
567
786
|
equations: equationValidation.diagnostics,
|
|
787
|
+
tables: tableValidation.diagnostics,
|
|
788
|
+
figures: figureValidation.diagnostics,
|
|
789
|
+
knowledge: knowledge.diagnostics,
|
|
568
790
|
},
|
|
569
|
-
hashes
|
|
791
|
+
hashes,
|
|
570
792
|
};
|
|
571
793
|
}
|
|
572
794
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import { loadManifest } from '../compiler/manifest';
|
|
4
4
|
import { isLessonSlug, lessonIdFromSlug } from '../compiler/lesson-derivation';
|
|
5
|
+
import Diagram, { type DiagramEdge, type DiagramNode } from './diagram/Diagram.astro';
|
|
5
6
|
|
|
6
7
|
const [tracks, docs, competencies, manifest] = await Promise.all([
|
|
7
8
|
getCollection('tracks'),
|
|
@@ -22,6 +23,20 @@ const lessonById = new Map(
|
|
|
22
23
|
const competencyById = new Map(
|
|
23
24
|
competencies.map((entry) => [entry.data.id, entry.data]),
|
|
24
25
|
);
|
|
26
|
+
|
|
27
|
+
// The dependency graph is read straight off the authored `prerequisites`
|
|
28
|
+
// lists — no separate content to keep in sync, and the same data the build's
|
|
29
|
+
// DAG/cycle validator already checks.
|
|
30
|
+
const competencyNodes: DiagramNode[] = competencies.map(({ data }) => ({
|
|
31
|
+
id: data.id,
|
|
32
|
+
text: data.title,
|
|
33
|
+
}));
|
|
34
|
+
const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
|
|
35
|
+
data.prerequisites.map((prerequisiteId) => ({
|
|
36
|
+
from: prerequisiteId,
|
|
37
|
+
to: data.id,
|
|
38
|
+
})),
|
|
39
|
+
);
|
|
25
40
|
---
|
|
26
41
|
|
|
27
42
|
{
|
|
@@ -110,6 +125,18 @@ const competencyById = new Map(
|
|
|
110
125
|
The build rejects unknown references, cycles, invalid lesson ordering, and
|
|
111
126
|
insufficient assessment coverage.
|
|
112
127
|
</p>
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
competencyNodes.length > 0 && (
|
|
131
|
+
<Diagram
|
|
132
|
+
id="competency-graph"
|
|
133
|
+
label="Competency dependency graph"
|
|
134
|
+
nodes={competencyNodes}
|
|
135
|
+
edges={competencyEdges}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
113
140
|
<ol class="competency-map">
|
|
114
141
|
{
|
|
115
142
|
competencies
|