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.
Files changed (94) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +46 -5
  3. package/dist/compiler/cli.d.ts +17 -0
  4. package/dist/compiler/cli.js +41 -3
  5. package/dist/compiler/collections.js +31 -1
  6. package/dist/compiler/course-config.d.ts +52 -1
  7. package/dist/compiler/knowledge-version.d.ts +58 -0
  8. package/dist/compiler/knowledge-version.js +80 -0
  9. package/dist/compiler/manifest.d.ts +87 -3
  10. package/dist/compiler/manifest.js +133 -20
  11. package/dist/compiler/resolution-report.d.ts +5 -0
  12. package/dist/reference/base-operators.d.mts +38 -0
  13. package/dist/reference/base-operators.mjs +103 -0
  14. package/dist/reference/consistency.d.ts +7 -2
  15. package/dist/reference/consistency.js +142 -2
  16. package/dist/reference/diagram-layout.d.ts +63 -0
  17. package/dist/reference/diagram-layout.js +122 -0
  18. package/dist/reference/equations-validate.d.ts +5 -0
  19. package/dist/reference/equations-validate.js +1 -1
  20. package/dist/reference/equations.d.mts +76 -18
  21. package/dist/reference/equations.mjs +118 -41
  22. package/dist/reference/figures-validate.d.ts +21 -0
  23. package/dist/reference/figures-validate.js +13 -0
  24. package/dist/reference/gate-math.d.ts +5 -4
  25. package/dist/reference/gate-math.js +73 -2
  26. package/dist/reference/gloss.d.ts +87 -0
  27. package/dist/reference/gloss.js +59 -0
  28. package/dist/reference/keyed-float-validate.d.ts +67 -0
  29. package/dist/reference/keyed-float-validate.js +110 -0
  30. package/dist/reference/keyed-jsx-floats.d.mts +56 -0
  31. package/dist/reference/keyed-jsx-floats.mjs +167 -0
  32. package/dist/reference/math-glyphs.d.mts +18 -0
  33. package/dist/reference/math-glyphs.mjs +92 -0
  34. package/dist/reference/registry.js +25 -0
  35. package/dist/reference/rehype-heading-anchors.d.mts +1 -0
  36. package/dist/reference/rehype-heading-anchors.mjs +144 -0
  37. package/dist/reference/remark-notation.d.mts +12 -0
  38. package/dist/reference/remark-notation.mjs +228 -32
  39. package/dist/reference/tables-validate.d.ts +21 -0
  40. package/dist/reference/tables-validate.js +13 -0
  41. package/dist/reference/types.d.ts +25 -2
  42. package/dist/reference/units-vocab.js +19 -1
  43. package/docs/VERSIONING.md +66 -0
  44. package/docs/authoring/README.md +96 -0
  45. package/docs/authoring/agents.md +63 -0
  46. package/docs/authoring/migrating.md +107 -0
  47. package/docs/authoring/notation.md +296 -0
  48. package/docs/authoring/versioning.md +116 -0
  49. package/docs/authoring/visualizations.md +195 -0
  50. package/package.json +20 -5
  51. package/src/compiler/cli.ts +49 -0
  52. package/src/compiler/collections.ts +52 -3
  53. package/src/compiler/course-config.ts +54 -0
  54. package/src/compiler/knowledge-version.ts +157 -0
  55. package/src/compiler/manifest.ts +270 -17
  56. package/src/components/CurriculumMap.astro +27 -0
  57. package/src/components/HoverPanel.astro +42 -0
  58. package/src/components/citation/CitationLayer.astro +1 -0
  59. package/src/components/diagram/Diagram.astro +317 -0
  60. package/src/components/examples/CompactExamples.astro +1 -1
  61. package/src/components/figure/Figure.astro +104 -0
  62. package/src/components/hover-panel.ts +71 -5
  63. package/src/components/notation/NotationFormula.astro +154 -0
  64. package/src/components/notation/NotationGlossary.astro +60 -0
  65. package/src/components/notation/NotationLayer.astro +248 -103
  66. package/src/components/notation/render-notation-math.ts +66 -0
  67. package/src/components/popup-stack.ts +345 -0
  68. package/src/components/reference/FloatRefLayer.astro +671 -0
  69. package/src/components/starlight/AboutPanel.astro +255 -0
  70. package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
  71. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  72. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  73. package/src/components/starlight/LessonFooter.astro +8 -2
  74. package/src/components/table/Table.astro +104 -0
  75. package/src/content-config.ts +38 -0
  76. package/src/reference/base-operators.mjs +103 -0
  77. package/src/reference/consistency.ts +236 -2
  78. package/src/reference/diagram-layout.ts +246 -0
  79. package/src/reference/equations-validate.ts +6 -1
  80. package/src/reference/equations.mjs +118 -41
  81. package/src/reference/figures-validate.ts +40 -0
  82. package/src/reference/gate-math.ts +104 -7
  83. package/src/reference/gloss.ts +153 -0
  84. package/src/reference/keyed-float-validate.ts +207 -0
  85. package/src/reference/keyed-jsx-floats.mjs +167 -0
  86. package/src/reference/math-glyphs.mjs +92 -0
  87. package/src/reference/registry.ts +43 -0
  88. package/src/reference/rehype-heading-anchors.mjs +144 -0
  89. package/src/reference/remark-notation.mjs +228 -32
  90. package/src/reference/tables-validate.ts +40 -0
  91. package/src/reference/types.ts +26 -1
  92. package/src/reference/units-vocab.ts +18 -0
  93. package/src/styles/global.css +193 -3
  94. package/src/components/starlight/EquationEnhancer.astro +0 -72
package/CHANGELOG.md ADDED
@@ -0,0 +1,102 @@
1
+ # Changelog
2
+
3
+ All notable changes to the **`explico` engine** are recorded here. The installed
4
+ `package.json` `version` is the single source of truth for what you have; this
5
+ file says what changed between versions and [`docs/VERSIONING.md`](docs/VERSIONING.md)
6
+ says what a version bump means.
7
+
8
+ A course built on explico keeps its **own** knowledge version — see the course's
9
+ `content/CHANGELOG.md` and [`docs/authoring/versioning.md`](docs/authoring/versioning.md).
10
+
11
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
12
+ This project is pre-1.0: a MINOR bump may carry a breaking change, always listed
13
+ under **Breaking** below; a PATCH never breaks.
14
+
15
+ ## [Unreleased]
16
+
17
+ ## [0.3.0] - 2026-09-05
18
+
19
+ ### Breaking
20
+
21
+ - `CourseConfig` now requires an `about` block (`materialName?`, `summary?`,
22
+ `version`, `authors`, `disclaimer`, `changelogHref?`). It carries the course's
23
+ reader-facing identity and its **knowledge version**. Add one to
24
+ `content/course.config.ts`.
25
+ - Manifest gains `manifest.about` (engine identity + the course `about` block +
26
+ the knowledge version resolved from `content/CHANGELOG.md`) and
27
+ `manifest.diagnostics.knowledge`; `schemaVersion` bumped. Additive — existing
28
+ fields are unchanged.
29
+ - `EquationRefLayer` / `EquationEnhancer` are replaced by `FloatRefLayer` +
30
+ `KeyedFloatEnhancer`, one hover/pin/jump engine shared by equation, table, and
31
+ figure references. Update the wiring in your Starlight `Footer` override.
32
+
33
+ ### Added
34
+
35
+ - **Figures and tables as first-class keyed floats.** New `Figure` and `Table`
36
+ components with appearance-order numbering, `#fig-<id>` / `#tab-<id>` anchors,
37
+ and `[[fig-<id>]]` / `[[tab-<id>]]` cross-references that highlight the target
38
+ when it is on screen and preview it when it is not — the same model equations
39
+ already had. New build gates `reference/figures-validate.ts`,
40
+ `reference/tables-validate.ts`, `reference/keyed-float-validate.ts`, and the
41
+ `reference/keyed-jsx-floats.mjs` remark pass.
42
+ - **About `[?]` panel.** `components/starlight/AboutPanel.astro` +
43
+ `LayoutSocialIcons.astro` render a header control, on every route, showing the
44
+ material name and summary, the knowledge version and date, the engine version,
45
+ the authors and contact, the disclaimer, and a link to the content changelog.
46
+ Wire it with `components: { SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro' }`.
47
+ - **Knowledge versioning.** `compiler/knowledge-version.ts` parses
48
+ `content/CHANGELOG.md` and checks it against `about.version`; drift is a Tier-3
49
+ `knowledge-version-mismatch` / `knowledge-changelog-missing` /
50
+ `knowledge-version-invalid` warning (never fails the build). Governed by new
51
+ authoring area **C** (`docs/authoring/versioning.md`, rules C1–C4).
52
+ - `Diagram` component (Dagre + build-time KaTeX), with an `LR` direction and an
53
+ optional hand-written `description` for the text fallback.
54
+ - Notation engine: a base **operator** library (`reference/base-operators.mjs`)
55
+ alongside the base glyph library, wider automatic glyph resolution in
56
+ `reference/math-glyphs.mjs`, and `\explain{key}{latex}` support inside notation
57
+ `formula:` fields and `Diagram` node formulas.
58
+ - `content status` now reports the course's knowledge version.
59
+ - Course-author docs in the published package: `docs/authoring/agents.md`
60
+ (working on a course repo with an AI agent) and `docs/authoring/migrating.md`
61
+ (per-version upgrade steps). The published `files` are `dist`, `src`,
62
+ `README.md`, `CHANGELOG.md`, `docs/VERSIONING.md`, `docs/authoring/` — the
63
+ repo's own `AGENTS.md` is engine-development guidance and is **no longer
64
+ shipped** (it described `src/` internals a consumer does not have).
65
+ - `compileManifest` throws a named error, pointing at `migrating.md`, when a
66
+ course upgrading from 0.2.x has no `about` block — instead of a bare
67
+ `Cannot read properties of undefined`.
68
+ - `scripts/agent/` (`verify`, `fresh`, `shot`) and a generated, staleness-gated
69
+ repo map in the repo's `AGENTS.md` (`scripts/agent/gen-map.mjs`) — repo tooling,
70
+ not part of the package.
71
+
72
+ ### Changed
73
+
74
+ - Deep-linkable heading anchors (`rehype-heading-anchors` +
75
+ `HeadingAnchorEnhancer`) and numbered lesson equations are on by default.
76
+ - `remark-notation` and `consistency` gained substantial coverage for the
77
+ keyed-float syntax, operator resolution, and `\explain` handling.
78
+
79
+ ## [0.2.0] - 2026-09-04
80
+
81
+ - Two-tier notation vocabulary: named operators (rule N8) are a Tier-3
82
+ `operator-explained` warning against a shipped baseline plus a per-course
83
+ `operators.allow` / `operators.deny` list, softer than the identifier gate.
84
+ - Heading deep links: `rehype-heading-anchors` + `HeadingAnchorEnhancer.astro`.
85
+ - `popup-stack.ts` — nested, independently positioned notation popups.
86
+
87
+ ## [0.1.1] - 2026-09-03
88
+
89
+ - Make the published build load under Node ESM (`.mjs` extension fixups, `dist/`
90
+ resolution).
91
+
92
+ ## [0.1.0] - 2026-09-03
93
+
94
+ - Initial import of the explico engine, extracted from the `equations` project:
95
+ the one manifest compiler, the notation / citation / equation reference
96
+ system with its completeness gate, the curriculum-graph validator, the
97
+ Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
98
+
99
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.2.0...HEAD
100
+ [0.2.0]: https://github.com/danieltuzes/explico/compare/v0.1.1...v0.2.0
101
+ [0.1.1]: https://github.com/danieltuzes/explico/compare/v0.1.0...v0.1.1
102
+ [0.1.0]: https://github.com/danieltuzes/explico/releases/tag/v0.1.0
package/README.md CHANGED
@@ -14,7 +14,10 @@ What it gives a course:
14
14
  meaning; a **completeness gate** fails the build if any identifier in any
15
15
  rendered-math context (lesson body, `notation.formula`, `checks.yml`,
16
16
  assessment prompts, component slots) does not resolve to a key. One meaning
17
- per glyph per page, enforced corpus-wide.
17
+ per glyph per page, enforced corpus-wide. Named operators get the same
18
+ treatment one tier softer: a lesson's `\operatorname{Var}` is a warning
19
+ (`operator-explained`) until it carries an entry or the course lists it in
20
+ `operators.allow`; a shipped baseline (`\exp`, `\int`, trig, …) is exempt.
18
21
  - **Inline citations** — `[@source-id; locator]` → numbered marker + generated
19
22
  reference list, resolved against a schema-checked source collection.
20
23
  - **Equation identity** — `\label{eq:key}` gives a display equation a stable
@@ -28,6 +31,14 @@ What it gives a course:
28
31
  - **Shared Astro/Starlight UI** — a hover-panel primitive shared by the
29
32
  notation and citation layers, a glossary, a curriculum map, compact examples,
30
33
  layout overrides.
34
+ - **Interactive figures, with room to grow** — a `labMathScope`/`renderLabMath`
35
+ seam compiles a `\slot{}`-templated formula against the page's real notation
36
+ at build time, so a client-side island only ever patches numbers into
37
+ pre-rendered KaTeX. Hand-rolled SVG is the default chart (zero dependency,
38
+ covers a single custom figure); reach for D3 once a figure needs real
39
+ tick-labeled axes, log scales, or several series sharing a scale; Observable
40
+ Plot pays off once a course is authoring many charts per lecture. Pick per
41
+ figure — nothing in the engine assumes one plotting approach.
31
42
  - **LMS / analytics / identity seams** — framework-free interfaces shipped as
32
43
  no-ops; a host supplies real implementations without touching a component.
33
44
  - **A `content` CLI** — `status`, `context <lesson>`, `check <lesson>` (compiles
@@ -67,7 +78,8 @@ import { courseConfig } from './content/course.config.ts';
67
78
 
68
79
  const manifest = await loadManifest(courseConfig);
69
80
  // … starlight({ customCss: ['explico/styles/global.css'],
70
- // components: { Footer: 'explico/components/starlight/LessonFooter.astro', … } })
81
+ // components: { Footer: 'explico/components/starlight/LessonFooter.astro',
82
+ // SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro', … } })
71
83
  // … markdown.remarkPlugins / rehypePlugins from the manifest
72
84
  ```
73
85
 
@@ -92,6 +104,19 @@ discounting → bonds → credit/CDS → derivatives and options.
92
104
  | `src/content-config.ts` | the Zod collection schemas (Astro re-exports them) |
93
105
  | `src/styles/` | `global.css` (notation panel, keyed-equation flash, KaTeX) |
94
106
 
107
+ ## Authoring
108
+
109
+ Course content follows the rules in [`docs/authoring/`](docs/authoring/) —
110
+ strong recommendations for human and AI authors covering notation spelling
111
+ (`\mathcal{N}` for the normal, `\mathbb{P}` for probability, upright `\mathrm{d}`
112
+ for the differential, …), which library draws a diagram or
113
+ chart (`Diagram` for graphs; SVG → D3 → Observable Plot for charts), how
114
+ randomised interactive figures pick their axes, and how the course's knowledge
115
+ version and `content/CHANGELOG.md` move with a content change (area `C`,
116
+ surfaced to readers in the About `[?]` panel).
117
+ [`docs/authoring/README.md`](docs/authoring/README.md) is the governance: rule
118
+ IDs, enforcement levels, and how to amend.
119
+
95
120
  ## Build
96
121
 
97
122
  ```bash
@@ -102,7 +127,23 @@ pnpm build # tsup → dist/ (ESM + .d.ts) for the .ts core; .astro / .tsx / .c
102
127
  Published `exports` resolve to `dist/` for the typed core and to `src/` for the
103
128
  Astro components and styles (see `publishConfig` in `package.json`).
104
129
 
105
- ## Status
130
+ ## Versioning
131
+
132
+ The engine follows [semver](https://semver.org). The **golden source** for the
133
+ version is the installed `package.json` (`explico`'s own `version`); the About
134
+ `[?]` panel reads it at build time and the docs never repeat a number.
135
+
136
+ - [`CHANGELOG.md`](CHANGELOG.md) — what changed between engine versions.
137
+ - [`docs/VERSIONING.md`](docs/VERSIONING.md) — what a MAJOR / MINOR / PATCH bump
138
+ means here, the public surface it is measured against, and the pre-1.0 clause.
139
+
140
+ A course built on explico carries an **independent knowledge version** over its
141
+ content — `about.version` in `content/course.config.ts`, tracked in the course's
142
+ `content/CHANGELOG.md`, surfaced to readers in the same `[?]` panel. See
143
+ [`docs/authoring/versioning.md`](docs/authoring/versioning.md).
144
+
145
+ For AI authors: [`AGENTS.md`](AGENTS.md) says which changelog and which version a
146
+ given change updates.
106
147
 
107
- `0.1.x` — extracted from the `equations` project. API not yet stable; the
108
- license is undecided (`UNLICENSED` until chosen).
148
+ Status: extracted from the `equations` project; API not yet stable (pre-1.0);
149
+ the license is undecided (`UNLICENSED` until chosen).
@@ -6,7 +6,12 @@ import './collections.js';
6
6
  import '../reference/types.js';
7
7
  import '../reference/lint-ignore.js';
8
8
  import '../reference/equations-validate.js';
9
+ import '../reference/tables-validate.js';
10
+ import '../reference/keyed-float-validate.js';
11
+ import '../reference/figures-validate.js';
9
12
  import '../reference/gate-math.js';
13
+ import '../reference/gloss.js';
14
+ import './knowledge-version.js';
10
15
  import './lesson-derivation.js';
11
16
 
12
17
  /**
@@ -90,6 +95,8 @@ interface DomainFileSignatures {
90
95
  readonly signatures: readonly string[];
91
96
  }
92
97
  interface StatusReport {
98
+ /** The course's published knowledge version (`about.version`). */
99
+ readonly knowledgeVersion: string;
93
100
  readonly counts: {
94
101
  readonly lessons: number;
95
102
  readonly competencies: number;
@@ -98,6 +105,8 @@ interface StatusReport {
98
105
  readonly tracks: number;
99
106
  readonly notationDefinitions: number;
100
107
  readonly keyedEquations: number;
108
+ readonly keyedTables: number;
109
+ readonly keyedFigures: number;
101
110
  };
102
111
  readonly draftDebt: {
103
112
  readonly lessons: readonly string[];
@@ -144,6 +153,14 @@ interface ContextReport {
144
153
  key: string;
145
154
  number: string;
146
155
  }[];
156
+ readonly tables: readonly {
157
+ key: string;
158
+ number: string;
159
+ }[];
160
+ readonly figures: readonly {
161
+ key: string;
162
+ number: string;
163
+ }[];
147
164
  readonly sources: readonly {
148
165
  id: string;
149
166
  title: string;
@@ -8,6 +8,7 @@ import {
8
8
  } from "@astrojs/markdown-remark";
9
9
  import rehypeKatex from "rehype-katex";
10
10
  import remarkMath from "remark-math";
11
+ import remarkMdx from "remark-mdx";
11
12
  import { createNotationKatexOptions } from "../reference/katex-options.mjs";
12
13
  import rehypeFailKatexErrors from "../reference/rehype-fail-katex-errors.mjs";
13
14
  import remarkCitation from "../reference/remark-citation.mjs";
@@ -81,6 +82,20 @@ function lessonDiagnostics(manifest, lesson) {
81
82
  ...withFile(diagnostic.file)
82
83
  });
83
84
  }
85
+ for (const diagnostic of [
86
+ ...manifest.diagnostics.tables,
87
+ ...manifest.diagnostics.figures
88
+ ]) {
89
+ if (diagnostic.lessonId !== id && diagnostic.file !== file && !mentions(diagnostic.message, id, file)) {
90
+ continue;
91
+ }
92
+ out.push({
93
+ code: diagnostic.code,
94
+ severity: diagnostic.severity === "warning" ? "warning" : "error",
95
+ message: diagnostic.message,
96
+ ...withFile(diagnostic.file)
97
+ });
98
+ }
84
99
  for (const diagnostic of manifest.diagnostics.consistency) {
85
100
  if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
86
101
  out.push({
@@ -100,12 +115,19 @@ function processorFor(manifest) {
100
115
  syntaxHighlight: false,
101
116
  smartypants: false,
102
117
  remarkPlugins: [
118
+ // Syntax only (no JSX evaluation) — teaches the parser `<Table>` /
119
+ // `<Figure>` / `<CompactExample>` are `mdxJsxFlowElement` nodes, same
120
+ // as the real Astro/MDX build, so `remarkNotation` sees the same tree
121
+ // shape here that it does there (D7/F-tables-figures parity).
122
+ remarkMdx,
103
123
  remarkMath,
104
124
  [
105
125
  remarkNotation,
106
126
  {
107
127
  definitions: manifest.notation.raw,
108
- equations: manifest.equations.numbersBySlug
128
+ equations: manifest.equations.numbersBySlug,
129
+ tables: manifest.tables.numbersBySlug,
130
+ figures: manifest.figures.numbersBySlug
109
131
  }
110
132
  ],
111
133
  [remarkCitation, { sources: manifest.sources }]
@@ -279,16 +301,28 @@ function contentStatus(manifest) {
279
301
  ...manifest.diagnostics.alignment.map((d) => d.severity),
280
302
  ...manifest.diagnostics.math.map((d) => d.severity),
281
303
  ...manifest.diagnostics.equations.map((d) => d.severity),
282
- ...manifest.diagnostics.consistency.map(() => "warning")
304
+ ...manifest.diagnostics.tables.map((d) => d.severity),
305
+ ...manifest.diagnostics.figures.map((d) => d.severity),
306
+ ...manifest.diagnostics.consistency.map(() => "warning"),
307
+ ...manifest.diagnostics.knowledge.map((d) => d.severity)
283
308
  ];
284
309
  const keyedEquations = manifest.lessons.reduce(
285
310
  (count, lesson) => count + lesson.equations.length,
286
311
  0
287
312
  );
313
+ const keyedTables = manifest.lessons.reduce(
314
+ (count, lesson) => count + lesson.tables.length,
315
+ 0
316
+ );
317
+ const keyedFigures = manifest.lessons.reduce(
318
+ (count, lesson) => count + lesson.figures.length,
319
+ 0
320
+ );
288
321
  const assessments = new Set(
289
322
  manifest.lessons.flatMap((lesson) => lesson.assessments)
290
323
  ).size;
291
324
  return {
325
+ knowledgeVersion: manifest.about.knowledge.version,
292
326
  counts: {
293
327
  lessons: manifest.lessons.length,
294
328
  competencies: manifest.competencies.length,
@@ -296,7 +330,9 @@ function contentStatus(manifest) {
296
330
  sources: manifest.sources.length,
297
331
  tracks: manifest.tracks.length,
298
332
  notationDefinitions: manifest.notation.definitions.length,
299
- keyedEquations
333
+ keyedEquations,
334
+ keyedTables,
335
+ keyedFigures
300
336
  },
301
337
  draftDebt: { lessons: draftLessons, notation: draftNotation },
302
338
  orphans,
@@ -350,6 +386,8 @@ function contentContext(manifest, rawId) {
350
386
  ...definition.formula === void 0 ? {} : { formula: definition.formula }
351
387
  })),
352
388
  equations: lesson.equations.map((equation) => ({ ...equation })),
389
+ tables: lesson.tables.map((table) => ({ ...table })),
390
+ figures: lesson.figures.map((figure) => ({ ...figure })),
353
391
  sources: lesson.sources.map((id) => {
354
392
  const record = sourceById.get(id);
355
393
  return {
@@ -3,7 +3,9 @@ import { basename, join, relative } from "node:path";
3
3
  import { parseFrontmatter } from "@astrojs/markdown-remark";
4
4
  import { isSubstantiveMeaning } from "../reference/prose.js";
5
5
  import { parseEquationRef } from "../reference/equations.mjs";
6
+ import { parseKeyedFloatRef } from "../reference/keyed-jsx-floats.mjs";
6
7
  import { resolveLabel } from "../reference/label.js";
8
+ import { resolveGlosses } from "../reference/gloss.js";
7
9
  import {
8
10
  deriveRequires,
9
11
  deriveSources,
@@ -135,7 +137,9 @@ function extractNotationReferences(text, file, kind) {
135
137
  for (const [pattern, referenceKind] of patterns) {
136
138
  for (const match of searchable.matchAll(pattern)) {
137
139
  const key = (match[1] ?? "").trim();
138
- if (parseEquationRef(key)) continue;
140
+ if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" })) {
141
+ continue;
142
+ }
139
143
  references.push({
140
144
  key,
141
145
  kind: referenceKind,
@@ -206,6 +210,30 @@ async function loadCurriculumCatalog() {
206
210
  tracks: jsonEntries(join(CONTENT_ROOT, "tracks"))
207
211
  };
208
212
  }
213
+ function glossEntries(raw, ownerKey, at) {
214
+ if (raw === void 0) return [];
215
+ if (!Array.isArray(raw)) throw new Error(`${at} must be an array`);
216
+ const authored = raw.map((value, index) => {
217
+ const data = record(value, `${at}[${index}]`);
218
+ const units = optionalString(data.units, `${at}[${index}].units`);
219
+ return {
220
+ latex: string(data.latex, `${at}[${index}].latex`),
221
+ name: string(data.name, `${at}[${index}].name`),
222
+ ...units === void 0 ? {} : { units }
223
+ };
224
+ });
225
+ const glosses = resolveGlosses(ownerKey, authored);
226
+ const seen = /* @__PURE__ */ new Set();
227
+ for (const gloss of glosses) {
228
+ if (seen.has(gloss.key)) {
229
+ throw new Error(
230
+ `${at} declares two glosses named ${JSON.stringify(gloss.label)}`
231
+ );
232
+ }
233
+ seen.add(gloss.key);
234
+ }
235
+ return glosses;
236
+ }
209
237
  function localDefinition(raw, file, index) {
210
238
  const at = `${file} notation.local[${index}]`;
211
239
  const data = record(raw, at);
@@ -224,6 +252,7 @@ function localDefinition(raw, file, index) {
224
252
  seeAlso: strings(data.seeAlso, `${at}.seeAlso`),
225
253
  alignment: data.alignment === void 0 ? { kind: "general", rationale: "Lesson-local symbol." } : alignment(data.alignment, `${at}.alignment`),
226
254
  references: extractNotationReferences(definitionText, file, "definition"),
255
+ glosses: glossEntries(data.glosses, key, `${at}.glosses`),
227
256
  source: { file },
228
257
  ...formula === void 0 ? {} : { formula },
229
258
  ...units === void 0 ? {} : { units }
@@ -255,6 +284,7 @@ function sharedNotationEntries() {
255
284
  aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
256
285
  body,
257
286
  references: extractNotationReferences(body, file, void 0),
287
+ glosses: glossEntries(data.glosses, key, `${file} glosses`),
258
288
  source: { file },
259
289
  ...formula === void 0 ? {} : { formula },
260
290
  ...units === void 0 ? {} : { units }
@@ -18,6 +18,53 @@ interface CourseConventions {
18
18
  /** Competency id that also counts as declaring the cash-flow perspective. */
19
19
  readonly cashFlowPerspectiveCompetency: string;
20
20
  }
21
+ /**
22
+ * A course's edits to the shipped named-operator baseline (`BASE_OPERATORS`),
23
+ * consumed by the `operator-explained` consistency check (rule N8). An operator
24
+ * a lesson uses is fine when it is on the baseline plus `allow` minus `deny`;
25
+ * otherwise it needs a notation entry the reader can hover.
26
+ *
27
+ * Names are the KaTeX command without its backslash (`erf`, not `\erf`) for a
28
+ * built-in, and the verbatim body for an `\operatorname{…}` operator (`Var`).
29
+ */
30
+ interface CourseOperators {
31
+ /** Operators this course also treats as universal (`Var`, `Cov`, `sgn`, …). */
32
+ readonly allow?: readonly string[];
33
+ /** Baseline operators this course wants explained anyway. */
34
+ readonly deny?: readonly string[];
35
+ }
36
+ /** One person or team credited for the study material. */
37
+ interface CourseAuthor {
38
+ readonly name: string;
39
+ /**
40
+ * Email, URL, or handle. Rendered in the About panel as a link when it looks
41
+ * like a `mailto:` or `https:` target, plain text otherwise.
42
+ */
43
+ readonly contact?: string;
44
+ }
45
+ /**
46
+ * The reader-facing "about this material" block, surfaced by the `[?]` panel in
47
+ * the header (`components/starlight/AboutPanel.astro`) on every page.
48
+ *
49
+ * `version` is the course's **knowledge version** — semver over the
50
+ * learning-facing content, independent of the `explico` engine's own version.
51
+ * It must equal the newest entry in `content/CHANGELOG.md`; a mismatch is a
52
+ * Tier-3 `knowledge-version-mismatch` warning (rule C3, `docs/authoring/versioning.md`).
53
+ */
54
+ interface CourseAbout {
55
+ /** Display name of the material. Defaults to {@link CourseConfig.title}. */
56
+ readonly materialName?: string;
57
+ /** One or two sentences on what the material covers. Defaults to {@link CourseConfig.description}. */
58
+ readonly summary?: string;
59
+ /** Knowledge version — semver; equals the latest `content/CHANGELOG.md` heading. */
60
+ readonly version: string;
61
+ /** Who authored / maintains the material. At least one entry. */
62
+ readonly authors: readonly CourseAuthor[];
63
+ /** Shown verbatim in the About panel — scope, warranty, AI-assistance note. */
64
+ readonly disclaimer: string;
65
+ /** Link to the published content changelog (repo blob, docs page, …). */
66
+ readonly changelogHref?: string;
67
+ }
21
68
  interface CourseConfig {
22
69
  readonly title: string;
23
70
  readonly description: string;
@@ -26,6 +73,10 @@ interface CourseConfig {
26
73
  /** Namespaces used by competency / notation `domain` fields. */
27
74
  readonly domains: readonly string[];
28
75
  readonly conventions: CourseConventions;
76
+ /** Reader-facing identity + knowledge version (the `[?]` About panel). */
77
+ readonly about: CourseAbout;
78
+ /** Edits to the shipped named-operator baseline (rule N8). Optional. */
79
+ readonly operators?: CourseOperators;
29
80
  }
30
81
 
31
- export type { CourseConfig, CourseConventions, CourseSection };
82
+ export type { CourseAbout, CourseAuthor, CourseConfig, CourseConventions, CourseOperators, CourseSection };
@@ -0,0 +1,58 @@
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
+ /** A released entry from `content/CHANGELOG.md`, newest first in file order. */
17
+ interface KnowledgeChangelogEntry {
18
+ readonly version: string;
19
+ /** The `- YYYY-MM-DD` part of the heading, if present. */
20
+ readonly date?: string;
21
+ /** Bullet lines under the heading, marker stripped, in file order. */
22
+ readonly notes: readonly string[];
23
+ }
24
+ type KnowledgeVersionCode = 'knowledge-changelog-missing' | 'knowledge-version-invalid' | 'knowledge-version-mismatch';
25
+ interface KnowledgeVersionDiagnostic {
26
+ readonly code: KnowledgeVersionCode;
27
+ /** Tier 3 — a knowledge-version drift never fails the build. */
28
+ readonly severity: 'warning';
29
+ readonly message: string;
30
+ readonly file?: string;
31
+ }
32
+ interface KnowledgeVersion {
33
+ /** The declared `about.version`, echoed for consumers. */
34
+ readonly version: string;
35
+ /** The newest released `## [x.y.z]` entry, when the changelog has one. */
36
+ readonly latest?: KnowledgeChangelogEntry;
37
+ readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
38
+ }
39
+ declare function isKnowledgeSemver(value: string): boolean;
40
+ /**
41
+ * Every released entry in a `content/CHANGELOG.md` body, in file order (newest
42
+ * first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
43
+ */
44
+ declare function parseKnowledgeChangelog(text: string): KnowledgeChangelogEntry[];
45
+ /**
46
+ * Check the declared knowledge version against `content/CHANGELOG.md`.
47
+ *
48
+ * @param options.changelogText the file body, or `undefined` when it is absent
49
+ * @param options.declaredVersion `courseConfig.about.version`
50
+ * @param options.file repo-relative path, for diagnostic messages
51
+ */
52
+ declare function resolveKnowledgeVersion(options: {
53
+ readonly changelogText: string | undefined;
54
+ readonly declaredVersion: string;
55
+ readonly file?: string;
56
+ }): KnowledgeVersion;
57
+
58
+ export { type KnowledgeChangelogEntry, type KnowledgeVersion, type KnowledgeVersionCode, type KnowledgeVersionDiagnostic, isKnowledgeSemver, parseKnowledgeChangelog, resolveKnowledgeVersion };
@@ -0,0 +1,80 @@
1
+ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
2
+ const HEADING_RE = /^##\s+\[([^\]]+)\]\s*(?:[-–—]\s*(\S.*?))?\s*$/;
3
+ const BULLET_RE = /^[-*]\s+(.*\S)\s*$/;
4
+ function isKnowledgeSemver(value) {
5
+ return SEMVER_RE.test(value.trim());
6
+ }
7
+ function parseKnowledgeChangelog(text) {
8
+ const lines = text.split(/\r?\n/);
9
+ const entries = [];
10
+ let current = null;
11
+ for (const line of lines) {
12
+ const heading = HEADING_RE.exec(line);
13
+ if (heading) {
14
+ const token = (heading[1] ?? "").trim();
15
+ if (/^unreleased$/i.test(token)) {
16
+ current = null;
17
+ continue;
18
+ }
19
+ current = { version: token, notes: [] };
20
+ const date = (heading[2] ?? "").trim();
21
+ if (date) current.date = date;
22
+ entries.push(current);
23
+ continue;
24
+ }
25
+ if (/^#{1,3}\s/.test(line) && !line.startsWith("## [")) {
26
+ current = null;
27
+ continue;
28
+ }
29
+ if (!current) continue;
30
+ const bullet = BULLET_RE.exec(line);
31
+ if (bullet) current.notes.push(bullet[1] ?? "");
32
+ }
33
+ return entries.map((entry) => ({
34
+ version: entry.version,
35
+ ...entry.date === void 0 ? {} : { date: entry.date },
36
+ notes: [...entry.notes]
37
+ }));
38
+ }
39
+ function resolveKnowledgeVersion(options) {
40
+ const { changelogText, declaredVersion } = options;
41
+ const file = options.file ?? "content/CHANGELOG.md";
42
+ const diagnostics = [];
43
+ const warn = (code, message) => {
44
+ diagnostics.push({ code, severity: "warning", message, file });
45
+ };
46
+ if (!isKnowledgeSemver(declaredVersion)) {
47
+ warn(
48
+ "knowledge-version-invalid",
49
+ `about.version ${JSON.stringify(declaredVersion)} in content/course.config.ts is not a semver \`x.y.z\` string.`
50
+ );
51
+ }
52
+ if (changelogText === void 0) {
53
+ warn(
54
+ "knowledge-changelog-missing",
55
+ `No ${file}. Add a Keep a Changelog file whose newest \`## [x.y.z]\` heading is ${JSON.stringify(declaredVersion)} (rule C1).`
56
+ );
57
+ return { version: declaredVersion, diagnostics };
58
+ }
59
+ const entries = parseKnowledgeChangelog(changelogText);
60
+ const latest = entries[0];
61
+ if (!latest) {
62
+ warn(
63
+ "knowledge-changelog-missing",
64
+ `${file} has no released \`## [x.y.z]\` entry \u2014 only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C4).`
65
+ );
66
+ return { version: declaredVersion, diagnostics };
67
+ }
68
+ if (isKnowledgeSemver(declaredVersion) && latest.version !== declaredVersion) {
69
+ warn(
70
+ "knowledge-version-mismatch",
71
+ `about.version is ${JSON.stringify(declaredVersion)} but the newest ${file} entry is ${JSON.stringify(latest.version)}. Bump one to match the other (rule C3).`
72
+ );
73
+ }
74
+ return { version: declaredVersion, latest, diagnostics };
75
+ }
76
+ export {
77
+ isKnowledgeSemver,
78
+ parseKnowledgeChangelog,
79
+ resolveKnowledgeVersion
80
+ };