explico 0.1.1 → 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 +17 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +31 -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 +87 -3
- package/dist/compiler/manifest.js +133 -20
- package/dist/compiler/resolution-report.d.ts +5 -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 +142 -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 +76 -18
- package/dist/reference/equations.mjs +118 -41
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -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/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +228 -32
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/types.d.ts +25 -2
- 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 +52 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +270 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -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/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +66 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +8 -2
- package/src/components/table/Table.astro +104 -0
- package/src/content-config.ts +38 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +236 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +118 -41
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -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/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +228 -32
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/types.ts +26 -1
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +193 -3
- package/src/components/starlight/EquationEnhancer.astro +0 -72
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,13 +52,26 @@ 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,
|
|
57
68
|
} from '../reference/gate-math';
|
|
58
69
|
import { loadLintIgnore } from '../reference/lint-ignore';
|
|
59
70
|
import { buildNotationRegistry } from '../reference/registry';
|
|
71
|
+
import { formulaGlyphScope } from '../reference/gloss';
|
|
72
|
+
import type { GlyphScopeEntry } from '../reference/gloss';
|
|
60
73
|
import { resolveReferenceId } from '../reference/references';
|
|
74
|
+
import type { NotationGlossEntry } from '../reference/types';
|
|
61
75
|
import type {
|
|
62
76
|
EditorialStatus,
|
|
63
77
|
NotationBacklink,
|
|
@@ -67,6 +81,10 @@ import type {
|
|
|
67
81
|
NotationPageBundle,
|
|
68
82
|
} from '../reference/types';
|
|
69
83
|
import type { CourseConfig } from './course-config';
|
|
84
|
+
import {
|
|
85
|
+
resolveKnowledgeVersion,
|
|
86
|
+
type KnowledgeVersionDiagnostic,
|
|
87
|
+
} from './knowledge-version';
|
|
70
88
|
import {
|
|
71
89
|
loadCurriculumCatalog,
|
|
72
90
|
loadNotationDefinitions,
|
|
@@ -78,6 +96,7 @@ import {
|
|
|
78
96
|
buildSidebar,
|
|
79
97
|
deriveSources,
|
|
80
98
|
isLessonSlug,
|
|
99
|
+
lessonIdFromSlug,
|
|
81
100
|
type SidebarGroup,
|
|
82
101
|
} from './lesson-derivation';
|
|
83
102
|
|
|
@@ -89,8 +108,16 @@ import {
|
|
|
89
108
|
* `diagnostics.consistency` (Tier-3 corpus checks).
|
|
90
109
|
* v3 (D7): `equations` (labelled display equations + cross-page number table),
|
|
91
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)`).
|
|
92
119
|
*/
|
|
93
|
-
export const MANIFEST_SCHEMA_VERSION =
|
|
120
|
+
export const MANIFEST_SCHEMA_VERSION = 6;
|
|
94
121
|
|
|
95
122
|
const REPOSITORY_ROOT = process.cwd();
|
|
96
123
|
// Content is the top-level `content/` folder; the build output is `build/`
|
|
@@ -98,6 +125,33 @@ const REPOSITORY_ROOT = process.cwd();
|
|
|
98
125
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, 'content');
|
|
99
126
|
export const MANIFEST_PATH = join(REPOSITORY_ROOT, 'build', 'manifest.json');
|
|
100
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
|
+
|
|
101
155
|
// --- shape ------------------------------------------------------------
|
|
102
156
|
|
|
103
157
|
export interface ManifestCitation {
|
|
@@ -137,12 +191,26 @@ export interface ManifestLessonNotation {
|
|
|
137
191
|
readonly seeAlso: readonly string[];
|
|
138
192
|
readonly units?: string;
|
|
139
193
|
readonly formula?: string;
|
|
194
|
+
/** Symbols the formula names that have no card of their own (D15). */
|
|
195
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
196
|
+
/**
|
|
197
|
+
* Entry-scoped glyph table for `formula` — the entry, its glosses, and any
|
|
198
|
+
* card the formula names. Precomputed here because the whole card set lives
|
|
199
|
+
* in the compiler, not in the page component.
|
|
200
|
+
*/
|
|
201
|
+
readonly formulaScope: readonly GlyphScopeEntry[];
|
|
140
202
|
}
|
|
141
203
|
|
|
142
204
|
export interface ManifestLesson {
|
|
143
205
|
readonly id: string;
|
|
144
206
|
/** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
|
|
145
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;
|
|
146
214
|
readonly file: string;
|
|
147
215
|
readonly title: string;
|
|
148
216
|
readonly description: string;
|
|
@@ -161,6 +229,16 @@ export interface ManifestLesson {
|
|
|
161
229
|
readonly key: string;
|
|
162
230
|
readonly number: string;
|
|
163
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
|
+
}[];
|
|
164
242
|
readonly notation: {
|
|
165
243
|
readonly bindings: readonly {
|
|
166
244
|
readonly key: string;
|
|
@@ -173,8 +251,37 @@ export interface ManifestLesson {
|
|
|
173
251
|
};
|
|
174
252
|
}
|
|
175
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
|
+
|
|
176
281
|
export interface Manifest {
|
|
177
282
|
readonly schemaVersion: number;
|
|
283
|
+
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
284
|
+
readonly about: ManifestAbout;
|
|
178
285
|
readonly notation: {
|
|
179
286
|
readonly definitions: readonly NotationDefinitionRecord[];
|
|
180
287
|
readonly bundles: readonly NotationPageBundle[];
|
|
@@ -196,6 +303,16 @@ export interface Manifest {
|
|
|
196
303
|
/** `slug -> { eqKey: number }` — passed to `remarkNotation` for `[[slug#eq:key]]`. */
|
|
197
304
|
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
198
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
|
+
};
|
|
199
316
|
readonly diagnostics: {
|
|
200
317
|
readonly curriculum: readonly CurriculumIssue[];
|
|
201
318
|
readonly notation: readonly NotationDiagnostic[];
|
|
@@ -203,8 +320,14 @@ export interface Manifest {
|
|
|
203
320
|
readonly math: readonly MathGateDiagnostic[];
|
|
204
321
|
/** Tier-3 corpus consistency checks (D6). Warnings only. */
|
|
205
322
|
readonly consistency: readonly ConsistencyDiagnostic[];
|
|
323
|
+
/** Knowledge-version checks (C3): `about.version` vs `content/CHANGELOG.md`. Warnings only. */
|
|
324
|
+
readonly knowledge: readonly KnowledgeVersionDiagnostic[];
|
|
206
325
|
/** Equation-identity checks (D7): `eq-duplicate-key`, `eq-ref-resolves` (error); `eq-key-unused` (warning). */
|
|
207
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[];
|
|
208
331
|
};
|
|
209
332
|
readonly hashes: {
|
|
210
333
|
/** `sha256` of every content source file, keyed by repo-relative path. */
|
|
@@ -306,6 +429,7 @@ function lessonNotationBundle(
|
|
|
306
429
|
bundle: NotationPageBundle | undefined,
|
|
307
430
|
definitionsById: ReadonlyMap<string, NotationDefinitionRecord>,
|
|
308
431
|
resolution: readonly ManifestNotationResolution[],
|
|
432
|
+
cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
|
|
309
433
|
): ManifestLesson['notation'] {
|
|
310
434
|
const definitions: ManifestLessonNotation[] = [];
|
|
311
435
|
for (const definitionId of bundle?.definitionIds ?? []) {
|
|
@@ -320,6 +444,16 @@ function lessonNotationBundle(
|
|
|
320
444
|
summary: definition.summary,
|
|
321
445
|
editorialStatus: definition.status,
|
|
322
446
|
seeAlso: [...definition.seeAlso],
|
|
447
|
+
glosses: [...definition.glosses],
|
|
448
|
+
formulaScope: formulaGlyphScope({
|
|
449
|
+
key: definition.key,
|
|
450
|
+
notation: definition.notation,
|
|
451
|
+
glosses: definition.glosses,
|
|
452
|
+
cardsByKey,
|
|
453
|
+
...(definition.formula === undefined
|
|
454
|
+
? {}
|
|
455
|
+
: { formula: definition.formula }),
|
|
456
|
+
}),
|
|
323
457
|
...(definition.units === undefined ? {} : { units: definition.units }),
|
|
324
458
|
...(definition.formula === undefined
|
|
325
459
|
? {}
|
|
@@ -373,12 +507,36 @@ function lessonFrontmatterById(): Map<
|
|
|
373
507
|
export async function compileManifest(
|
|
374
508
|
courseConfig: CourseConfig,
|
|
375
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
|
+
|
|
376
526
|
const [catalog, notationInput] = await Promise.all([
|
|
377
527
|
loadCurriculumCatalog(),
|
|
378
528
|
loadNotationRegistryInput(),
|
|
379
529
|
]);
|
|
380
530
|
|
|
381
531
|
const registry = buildNotationRegistry(notationInput);
|
|
532
|
+
// Cards a `formula` may name with `\explain{key}` (D15). Shared entries
|
|
533
|
+
// only: a page-local symbol is not addressable from another entry.
|
|
534
|
+
const cardsByKey = new Map<string, GlyphScopeEntry>(
|
|
535
|
+
notationInput.sharedDefinitions.map((definition) => [
|
|
536
|
+
definition.key,
|
|
537
|
+
{ key: definition.key, notation: definition.notation },
|
|
538
|
+
]),
|
|
539
|
+
);
|
|
382
540
|
const definitionsById = new Map(
|
|
383
541
|
registry.definitions.map((definition) => [definition.id, definition]),
|
|
384
542
|
);
|
|
@@ -422,20 +580,57 @@ export async function compileManifest(
|
|
|
422
580
|
);
|
|
423
581
|
const frontmatterById = lessonFrontmatterById();
|
|
424
582
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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,
|
|
432
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);
|
|
433
614
|
const equationsBySlug = new Map(
|
|
434
615
|
Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
|
|
435
616
|
slug,
|
|
436
617
|
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
437
618
|
]),
|
|
438
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
|
+
);
|
|
439
634
|
|
|
440
635
|
const lessons: ManifestLesson[] = [];
|
|
441
636
|
for (const notationLesson of notationInput.lessons) {
|
|
@@ -450,6 +645,10 @@ export async function compileManifest(
|
|
|
450
645
|
lessons.push({
|
|
451
646
|
id,
|
|
452
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,
|
|
453
652
|
file: notationLesson.source.file,
|
|
454
653
|
title: frontmatter.title,
|
|
455
654
|
description: frontmatter.description,
|
|
@@ -461,6 +660,8 @@ export async function compileManifest(
|
|
|
461
660
|
sources: deriveSources(notationLesson.body),
|
|
462
661
|
citations: lessonCitations(notationLesson.body),
|
|
463
662
|
equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
663
|
+
tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
664
|
+
figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
|
|
464
665
|
prereqEdges: sortEdges(
|
|
465
666
|
teaches.flatMap((to) =>
|
|
466
667
|
(prerequisitesById.get(to) ?? [])
|
|
@@ -476,6 +677,7 @@ export async function compileManifest(
|
|
|
476
677
|
definitionsById,
|
|
477
678
|
availableDefinitionIds,
|
|
478
679
|
),
|
|
680
|
+
cardsByKey,
|
|
479
681
|
),
|
|
480
682
|
});
|
|
481
683
|
}
|
|
@@ -488,6 +690,9 @@ export async function compileManifest(
|
|
|
488
690
|
catalog,
|
|
489
691
|
lintIgnore: loadLintIgnore(),
|
|
490
692
|
conventions: courseConfig.conventions,
|
|
693
|
+
...(courseConfig.operators === undefined
|
|
694
|
+
? {}
|
|
695
|
+
: { operators: courseConfig.operators }),
|
|
491
696
|
});
|
|
492
697
|
|
|
493
698
|
const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
|
|
@@ -499,17 +704,54 @@ export async function compileManifest(
|
|
|
499
704
|
const sources = [...loadSourceRecords()].sort(byId);
|
|
500
705
|
const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
|
|
501
706
|
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
};
|
|
510
751
|
|
|
511
752
|
return {
|
|
512
753
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
754
|
+
about,
|
|
513
755
|
notation: {
|
|
514
756
|
definitions: registry.definitions,
|
|
515
757
|
bundles: registry.bundles,
|
|
@@ -527,6 +769,14 @@ export async function compileManifest(
|
|
|
527
769
|
labels: equationValidation.labels,
|
|
528
770
|
numbersBySlug: equationValidation.numbersBySlug,
|
|
529
771
|
},
|
|
772
|
+
tables: {
|
|
773
|
+
labels: tableValidation.labels,
|
|
774
|
+
numbersBySlug: tableValidation.numbersBySlug,
|
|
775
|
+
},
|
|
776
|
+
figures: {
|
|
777
|
+
labels: figureValidation.labels,
|
|
778
|
+
numbersBySlug: figureValidation.numbersBySlug,
|
|
779
|
+
},
|
|
530
780
|
diagnostics: {
|
|
531
781
|
curriculum: curriculumIssues,
|
|
532
782
|
notation: registry.diagnostics,
|
|
@@ -534,8 +784,11 @@ export async function compileManifest(
|
|
|
534
784
|
math: mathIssues,
|
|
535
785
|
consistency: consistencyIssues,
|
|
536
786
|
equations: equationValidation.diagnostics,
|
|
787
|
+
tables: tableValidation.diagnostics,
|
|
788
|
+
figures: figureValidation.diagnostics,
|
|
789
|
+
knowledge: knowledge.diagnostics,
|
|
537
790
|
},
|
|
538
|
-
hashes
|
|
791
|
+
hashes,
|
|
539
792
|
};
|
|
540
793
|
}
|
|
541
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
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* Extra attributes (e.g. `data-notation-panel`, `data-citation-panel`) are
|
|
12
12
|
* forwarded to the `<aside>` so each layer keeps the hook its tests and pin
|
|
13
13
|
* controls already reference.
|
|
14
|
+
*
|
|
15
|
+
* `tabindex="-1"`: never in the tab order, but a valid target for
|
|
16
|
+
* `panel.focus()`. A layer that lets the reader click deeper into the panel's
|
|
17
|
+
* own content (D15's in-panel navigation) needs somewhere stable to move
|
|
18
|
+
* focus to before it replaces whatever the reader just clicked — otherwise
|
|
19
|
+
* removing the focused node drops focus to nothing, and the shared
|
|
20
|
+
* `focusout` handler in `./hover-panel.ts` reads that as "the reader left"
|
|
21
|
+
* and closes the panel it was just asked to update.
|
|
14
22
|
*/
|
|
15
23
|
export interface Props {
|
|
16
24
|
/** Element id — pin controls point `aria-controls` at it. */
|
|
@@ -31,8 +39,17 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
31
39
|
aria-live="polite"
|
|
32
40
|
aria-atomic="true"
|
|
33
41
|
hidden
|
|
42
|
+
tabindex="-1"
|
|
34
43
|
{...rest}
|
|
35
44
|
>
|
|
45
|
+
<button
|
|
46
|
+
class="hover-panel-close"
|
|
47
|
+
type="button"
|
|
48
|
+
data-hover-panel-close
|
|
49
|
+
aria-label="Close"
|
|
50
|
+
>
|
|
51
|
+
<span aria-hidden="true">×</span>
|
|
52
|
+
</button>
|
|
36
53
|
<slot />
|
|
37
54
|
</aside>
|
|
38
55
|
|
|
@@ -48,6 +65,7 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
48
65
|
width: min(23rem, calc(100vw - 1.5rem));
|
|
49
66
|
overflow: auto;
|
|
50
67
|
padding: 1rem;
|
|
68
|
+
padding-inline-end: 2.4rem;
|
|
51
69
|
border: 1px solid var(--sl-color-gray-5);
|
|
52
70
|
border-top: 0.25rem solid var(--sl-color-accent);
|
|
53
71
|
border-radius: 0.65rem;
|
|
@@ -57,6 +75,30 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
57
75
|
font-size: var(--sl-text-sm);
|
|
58
76
|
}
|
|
59
77
|
|
|
78
|
+
.hover-panel-close {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: 0.5rem;
|
|
81
|
+
right: 0.5rem;
|
|
82
|
+
width: 1.6rem;
|
|
83
|
+
height: 1.6rem;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
padding: 0;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: 0.35rem;
|
|
90
|
+
color: var(--sl-color-gray-3);
|
|
91
|
+
background: transparent;
|
|
92
|
+
font-size: 1.15rem;
|
|
93
|
+
line-height: 1;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.hover-panel-close:hover {
|
|
98
|
+
color: var(--sl-color-text);
|
|
99
|
+
background: var(--sl-color-gray-6);
|
|
100
|
+
}
|
|
101
|
+
|
|
60
102
|
/* Slotted fields carry the owning layer's scope, not this component's, so
|
|
61
103
|
the descendant match must be global. */
|
|
62
104
|
.hover-panel :global(:focus-visible) {
|