explico 0.2.0 → 0.4.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 (83) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +67 -7
  3. package/dist/compiler/astro.d.ts +30 -0
  4. package/dist/compiler/astro.js +79 -0
  5. package/dist/compiler/cli.d.ts +16 -0
  6. package/dist/compiler/cli.js +43 -9
  7. package/dist/compiler/collections.js +4 -1
  8. package/dist/compiler/course-config.d.ts +80 -4
  9. package/dist/compiler/course-config.js +12 -0
  10. package/dist/compiler/knowledge-version.d.ts +58 -0
  11. package/dist/compiler/knowledge-version.js +80 -0
  12. package/dist/compiler/lesson-derivation.d.ts +1 -1
  13. package/dist/compiler/lesson-derivation.js +2 -2
  14. package/dist/compiler/manifest.d.ts +98 -2
  15. package/dist/compiler/manifest.js +153 -24
  16. package/dist/compiler/resolution-report.d.ts +4 -0
  17. package/dist/reference/base-operators.d.mts +38 -0
  18. package/dist/reference/base-operators.mjs +103 -0
  19. package/dist/reference/consistency.d.ts +7 -2
  20. package/dist/reference/consistency.js +51 -2
  21. package/dist/reference/diagram-layout.d.ts +63 -0
  22. package/dist/reference/diagram-layout.js +122 -0
  23. package/dist/reference/equations-validate.d.ts +10 -0
  24. package/dist/reference/equations-validate.js +1 -1
  25. package/dist/reference/equations.d.mts +80 -13
  26. package/dist/reference/equations.mjs +109 -19
  27. package/dist/reference/figures-validate.d.ts +21 -0
  28. package/dist/reference/figures-validate.js +13 -0
  29. package/dist/reference/keyed-float-validate.d.ts +73 -0
  30. package/dist/reference/keyed-float-validate.js +110 -0
  31. package/dist/reference/keyed-jsx-floats.d.mts +64 -0
  32. package/dist/reference/keyed-jsx-floats.mjs +191 -0
  33. package/dist/reference/math-glyphs.d.mts +18 -0
  34. package/dist/reference/math-glyphs.mjs +92 -0
  35. package/dist/reference/remark-notation.d.mts +61 -0
  36. package/dist/reference/remark-notation.mjs +344 -10
  37. package/dist/reference/tables-validate.d.ts +21 -0
  38. package/dist/reference/tables-validate.js +13 -0
  39. package/dist/reference/units-vocab.js +19 -1
  40. package/docs/VERSIONING.md +66 -0
  41. package/docs/authoring/README.md +100 -0
  42. package/docs/authoring/agents.md +63 -0
  43. package/docs/authoring/migrating.md +169 -0
  44. package/docs/authoring/notation.md +297 -0
  45. package/docs/authoring/structure.md +139 -0
  46. package/docs/authoring/versioning.md +116 -0
  47. package/docs/authoring/visualizations.md +195 -0
  48. package/package.json +20 -5
  49. package/src/compiler/astro.ts +127 -0
  50. package/src/compiler/cli.ts +52 -7
  51. package/src/compiler/collections.ts +12 -3
  52. package/src/compiler/course-config.ts +93 -3
  53. package/src/compiler/knowledge-version.ts +157 -0
  54. package/src/compiler/lesson-derivation.ts +2 -2
  55. package/src/compiler/manifest.ts +320 -26
  56. package/src/components/CurriculumMap.astro +27 -0
  57. package/src/components/diagram/Diagram.astro +317 -0
  58. package/src/components/examples/CompactExamples.astro +1 -1
  59. package/src/components/figure/Figure.astro +104 -0
  60. package/src/components/notation/NotationLayer.astro +11 -0
  61. package/src/components/notation/render-notation-math.ts +13 -2
  62. package/src/components/reference/FloatRefLayer.astro +671 -0
  63. package/src/components/starlight/AboutPanel.astro +255 -0
  64. package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
  65. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  66. package/src/components/starlight/LayoutSidebar.astro +62 -1
  67. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  68. package/src/components/starlight/LessonFooter.astro +15 -2
  69. package/src/components/table/Table.astro +105 -0
  70. package/src/reference/base-operators.mjs +103 -0
  71. package/src/reference/consistency.ts +83 -2
  72. package/src/reference/diagram-layout.ts +246 -0
  73. package/src/reference/equations-validate.ts +8 -1
  74. package/src/reference/equations.mjs +109 -19
  75. package/src/reference/figures-validate.ts +40 -0
  76. package/src/reference/keyed-float-validate.ts +210 -0
  77. package/src/reference/keyed-jsx-floats.mjs +191 -0
  78. package/src/reference/math-glyphs.mjs +92 -0
  79. package/src/reference/remark-notation.mjs +344 -10
  80. package/src/reference/tables-validate.ts +40 -0
  81. package/src/reference/units-vocab.ts +18 -0
  82. package/src/styles/global.css +244 -7
  83. package/src/components/starlight/EquationEnhancer.astro +0 -87
package/CHANGELOG.md ADDED
@@ -0,0 +1,173 @@
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.4.0] - 2026-09-09
18
+
19
+ ### Breaking
20
+
21
+ - **Equation, table, and figure numbers are now `‹part›.‹n›`, not
22
+ `‹lesson›.‹n›`.** A *part* is a sidebar group; `n` counts that kind of float
23
+ across the whole part, running unbroken from one chapter into the next.
24
+ Previously the prefix was the lesson's ordinal down the entire course and `n`
25
+ restarted on every page, so a sixteen-lesson course had sixteen prefixes.
26
+ Every number in an existing course changes. Nothing an author wrote changes —
27
+ numbers were never hand-typed — but a published link of the form `#eq-2-4`
28
+ (the positional anchor of an *unkeyed* equation) moves; a keyed `#eq-<key>`
29
+ anchor is unaffected. Manifest schema is now **v7**: `lessons[].part` is new,
30
+ `lessons[].chapter` is redefined as the lesson's ordinal *within its part*,
31
+ and `lessons[].numbering` carries the per-kind running counts.
32
+ - **`courseConfig.sections` is renamed `courseConfig.parts`.** "Section" now
33
+ only ever means an `##` heading inside a lesson. The old name still works and
34
+ is removed in the next MINOR release; `CourseSection` is a deprecated alias
35
+ for `CoursePart`.
36
+
37
+ ### Added
38
+
39
+ - [`docs/authoring/structure.md`](docs/authoring/structure.md) — the structural
40
+ taxonomy the rest of the docs speak in: course → part → chapter → section →
41
+ subsection, which levels a number counts, and why the numbering invariant is
42
+ build-enforced. Two levels were previously both called "section", and two
43
+ were both called "chapter".
44
+ - `remarkNotation` accepts a `manifest` option that supplies every numbering
45
+ input at once — `equations`, `tables`, `figures`, and the part/offset
46
+ placement map, all read off the same compiled manifest. This is now the recommended wiring in
47
+ `astro.config.mjs`; the individual options remain, and an explicit one still
48
+ wins over the manifest. `numberingFromManifest(manifest)` is exported for
49
+ callers that need the derived object on its own.
50
+ - `explicoContentRefresh(courseConfig, manifest)` recompiles the manifest and
51
+ restarts Astro development after any `content/` change, independent of the
52
+ editor making it.
53
+
54
+ ### Fixed
55
+
56
+ - **Equation, table, and figure numbers no longer disagree between a page and
57
+ the references to it (WERN).** A course that passed the manifest number tables
58
+ to `remarkNotation` without the placement map silently numbered every page by
59
+ `##` section — restarting at `1.1` on each page — while cross-page references
60
+ kept reading the manifest, so one equation was `(2.2)` on its own page and
61
+ `(4.5)` everywhere else. `remarkNotation` now cross-checks the number
62
+ it computes for each keyed equation / table / figure against the manifest
63
+ number for the same key and fails the build on any disagreement, naming the
64
+ key, both numbers, and the missing placement. The same omission was
65
+ present in `explico content check`, which therefore validated lessons under a
66
+ numbering scheme the real build never used; it now shares the manifest wiring.
67
+
68
+ ### Changed
69
+
70
+ - Parts in the left sidebar now start collapsed, retain compact chapter icons
71
+ with hover/focus labels, and expand to the full chapter names when the part
72
+ title is opened. (Previously described as "chapter groups" with "subchapter"
73
+ icons — the same UI, named per the taxonomy.)
74
+
75
+ ### Fixed
76
+
77
+ - `loadManifest(courseConfig)` now always compiles current course content, and
78
+ `LessonFooter` rejects local-notation key drift. Current math hover markers can
79
+ no longer be silently paired with stale cards from a persisted manifest.
80
+ - Notation hover markers inside `\sqrt{…}` (and under an extensible arrow such
81
+ as `\xrightarrow`) are reachable again: KaTeX paints the radical/arrow SVG as
82
+ an absolutely-positioned overlay on top of the content it decorates, which
83
+ was swallowing the pointer. `NotationLayer` now sets `pointer-events: none`
84
+ on that overlay layer, matching the existing fraction/subscript workaround.
85
+
86
+ ## [0.3.0] - 2026-09-05
87
+
88
+ ### Breaking
89
+
90
+ - `CourseConfig` now requires an `about` block (`materialName?`, `summary?`,
91
+ `version`, `authors`, `disclaimer`, `changelogHref?`). It carries the course's
92
+ reader-facing identity and its **knowledge version**. Add one to
93
+ `content/course.config.ts`.
94
+ - Manifest gains `manifest.about` (engine identity + the course `about` block +
95
+ the knowledge version resolved from `content/CHANGELOG.md`) and
96
+ `manifest.diagnostics.knowledge`; `schemaVersion` bumped. Additive — existing
97
+ fields are unchanged.
98
+ - `EquationRefLayer` / `EquationEnhancer` are replaced by `FloatRefLayer` +
99
+ `KeyedFloatEnhancer`, one hover/pin/jump engine shared by equation, table, and
100
+ figure references. Update the wiring in your Starlight `Footer` override.
101
+
102
+ ### Added
103
+
104
+ - **Figures and tables as first-class keyed floats.** New `Figure` and `Table`
105
+ components with appearance-order numbering, `#fig-<id>` / `#tab-<id>` anchors,
106
+ and `[[fig-<id>]]` / `[[tab-<id>]]` cross-references that highlight the target
107
+ when it is on screen and preview it when it is not — the same model equations
108
+ already had. New build gates `reference/figures-validate.ts`,
109
+ `reference/tables-validate.ts`, `reference/keyed-float-validate.ts`, and the
110
+ `reference/keyed-jsx-floats.mjs` remark pass.
111
+ - **About `[?]` panel.** `components/starlight/AboutPanel.astro` +
112
+ `LayoutSocialIcons.astro` render a header control, on every route, showing the
113
+ material name and summary, the knowledge version and date, the engine version,
114
+ the authors and contact, the disclaimer, and a link to the content changelog.
115
+ Wire it with `components: { SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro' }`.
116
+ - **Knowledge versioning.** `compiler/knowledge-version.ts` parses
117
+ `content/CHANGELOG.md` and checks it against `about.version`; drift is a Tier-3
118
+ `knowledge-version-mismatch` / `knowledge-changelog-missing` /
119
+ `knowledge-version-invalid` warning (never fails the build). Governed by new
120
+ authoring area **C** (`docs/authoring/versioning.md`, rules C1–C4).
121
+ - `Diagram` component (Dagre + build-time KaTeX), with an `LR` direction and an
122
+ optional hand-written `description` for the text fallback.
123
+ - Notation engine: a base **operator** library (`reference/base-operators.mjs`)
124
+ alongside the base glyph library, wider automatic glyph resolution in
125
+ `reference/math-glyphs.mjs`, and `\explain{key}{latex}` support inside notation
126
+ `formula:` fields and `Diagram` node formulas.
127
+ - `content status` now reports the course's knowledge version.
128
+ - Course-author docs in the published package: `docs/authoring/agents.md`
129
+ (working on a course repo with an AI agent) and `docs/authoring/migrating.md`
130
+ (per-version upgrade steps). The published `files` are `dist`, `src`,
131
+ `README.md`, `CHANGELOG.md`, `docs/VERSIONING.md`, `docs/authoring/` — the
132
+ repo's own `AGENTS.md` is engine-development guidance and is **no longer
133
+ shipped** (it described `src/` internals a consumer does not have).
134
+ - `compileManifest` throws a named error, pointing at `migrating.md`, when a
135
+ course upgrading from 0.2.x has no `about` block — instead of a bare
136
+ `Cannot read properties of undefined`.
137
+ - `scripts/agent/` (`verify`, `fresh`, `shot`) and a generated, staleness-gated
138
+ repo map in the repo's `AGENTS.md` (`scripts/agent/gen-map.mjs`) — repo tooling,
139
+ not part of the package.
140
+
141
+ ### Changed
142
+
143
+ - Deep-linkable heading anchors (`rehype-heading-anchors` +
144
+ `HeadingAnchorEnhancer`) and numbered lesson equations are on by default.
145
+ - `remark-notation` and `consistency` gained substantial coverage for the
146
+ keyed-float syntax, operator resolution, and `\explain` handling.
147
+
148
+ ## [0.2.0] - 2026-09-04
149
+
150
+ - Two-tier notation vocabulary: named operators (rule N8) are a Tier-3
151
+ `operator-explained` warning against a shipped baseline plus a per-course
152
+ `operators.allow` / `operators.deny` list, softer than the identifier gate.
153
+ - Heading deep links: `rehype-heading-anchors` + `HeadingAnchorEnhancer.astro`.
154
+ - `popup-stack.ts` — nested, independently positioned notation popups.
155
+
156
+ ## [0.1.1] - 2026-09-03
157
+
158
+ - Make the published build load under Node ESM (`.mjs` extension fixups, `dist/`
159
+ resolution).
160
+
161
+ ## [0.1.0] - 2026-09-03
162
+
163
+ - Initial import of the explico engine, extracted from the `equations` project:
164
+ the one manifest compiler, the notation / citation / equation reference
165
+ system with its completeness gate, the curriculum-graph validator, the
166
+ Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
167
+
168
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.4.0...HEAD
169
+ [0.4.0]: https://github.com/danieltuzes/explico/compare/v0.3.0...v0.4.0
170
+ [0.3.0]: https://github.com/danieltuzes/explico/compare/v0.2.0...v0.3.0
171
+ [0.2.0]: https://github.com/danieltuzes/explico/compare/v0.1.1...v0.2.0
172
+ [0.1.1]: https://github.com/danieltuzes/explico/compare/v0.1.0...v0.1.1
173
+ [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
@@ -58,6 +69,7 @@ export { collections } from 'explico/content-config';
58
69
 
59
70
  ```js
60
71
  // astro.config.mjs
72
+ import { explicoContentRefresh } from 'explico/compiler/astro.js';
61
73
  import { loadManifest } from 'explico/compiler/manifest.js';
62
74
  import remarkNotation from 'explico/reference/remark-notation.mjs';
63
75
  import remarkCitation from 'explico/reference/remark-citation.mjs';
@@ -66,11 +78,30 @@ import rehypeFailKatexErrors from 'explico/reference/rehype-fail-katex-errors.mj
66
78
  import { courseConfig } from './content/course.config.ts';
67
79
 
68
80
  const manifest = await loadManifest(courseConfig);
69
- // … starlight({ customCss: ['explico/styles/global.css'],
70
- // components: { Footer: 'explico/components/starlight/LessonFooter.astro', … } })
71
- // … markdown.remarkPlugins / rehypePlugins from the manifest
81
+ // markdown.remarkPlugins: [
82
+ // remarkMath,
83
+ // // One `manifest` carries every numbering input — the equation / table /
84
+ // // figure number tables *and* the part each lesson sits in. Pass the
85
+ // // tables without the placement and the page numbers its equations by `##`
86
+ // // section while references to it read the part scheme, so the same
87
+ // // equation reads (2.2) at home and (4.5) abroad; the plugin rejects that
88
+ // // at build time rather than shipping it.
89
+ // [remarkNotation, { definitions: manifest.notation.raw, manifest }],
90
+ // [remarkCitation, { sources: manifest.sources }],
91
+ // ]
92
+ // … integrations: [explicoContentRefresh(courseConfig, manifest),
93
+ // starlight({ customCss: ['explico/styles/global.css'],
94
+ // components: { Footer: 'explico/components/starlight/LessonFooter.astro',
95
+ // SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro', … } })
96
+ // … rehypePlugins: [[rehypeKatex, createNotationKatexOptions()], rehypeFailKatexErrors]
72
97
  ```
73
98
 
99
+ `loadManifest(courseConfig)` always recompiles current content before Astro
100
+ starts. `explicoContentRefresh(courseConfig, manifest)` recompiles the manifest
101
+ and restarts the development server when any file under `content/` is added,
102
+ changed, or removed, keeping Markdown transforms and UI components on the same
103
+ model without an editor-specific task.
104
+
74
105
  ```jsonc
75
106
  // package.json — the read-side CLI
76
107
  "scripts": { "validate:content": "tsx scripts/compile-manifest.ts" }
@@ -92,6 +123,19 @@ discounting → bonds → credit/CDS → derivatives and options.
92
123
  | `src/content-config.ts` | the Zod collection schemas (Astro re-exports them) |
93
124
  | `src/styles/` | `global.css` (notation panel, keyed-equation flash, KaTeX) |
94
125
 
126
+ ## Authoring
127
+
128
+ Course content follows the rules in [`docs/authoring/`](docs/authoring/) —
129
+ strong recommendations for human and AI authors covering notation spelling
130
+ (`\mathcal{N}` for the normal, `\mathbb{P}` for probability, upright `\mathrm{d}`
131
+ for the differential, …), which library draws a diagram or
132
+ chart (`Diagram` for graphs; SVG → D3 → Observable Plot for charts), how
133
+ randomised interactive figures pick their axes, and how the course's knowledge
134
+ version and `content/CHANGELOG.md` move with a content change (area `C`,
135
+ surfaced to readers in the About `[?]` panel).
136
+ [`docs/authoring/README.md`](docs/authoring/README.md) is the governance: rule
137
+ IDs, enforcement levels, and how to amend.
138
+
95
139
  ## Build
96
140
 
97
141
  ```bash
@@ -102,7 +146,23 @@ pnpm build # tsup → dist/ (ESM + .d.ts) for the .ts core; .astro / .tsx / .c
102
146
  Published `exports` resolve to `dist/` for the typed core and to `src/` for the
103
147
  Astro components and styles (see `publishConfig` in `package.json`).
104
148
 
105
- ## Status
149
+ ## Versioning
150
+
151
+ The engine follows [semver](https://semver.org). The **golden source** for the
152
+ version is the installed `package.json` (`explico`'s own `version`); the About
153
+ `[?]` panel reads it at build time and the docs never repeat a number.
154
+
155
+ - [`CHANGELOG.md`](CHANGELOG.md) — what changed between engine versions.
156
+ - [`docs/VERSIONING.md`](docs/VERSIONING.md) — what a MAJOR / MINOR / PATCH bump
157
+ means here, the public surface it is measured against, and the pre-1.0 clause.
158
+
159
+ A course built on explico carries an **independent knowledge version** over its
160
+ content — `about.version` in `content/course.config.ts`, tracked in the course's
161
+ `content/CHANGELOG.md`, surfaced to readers in the same `[?]` panel. See
162
+ [`docs/authoring/versioning.md`](docs/authoring/versioning.md).
163
+
164
+ For AI authors: [`AGENTS.md`](AGENTS.md) says which changelog and which version a
165
+ given change updates.
106
166
 
107
- `0.1.x` — extracted from the `equations` project. API not yet stable; the
108
- license is undecided (`UNLICENSED` until chosen).
167
+ Status: extracted from the `equations` project; API not yet stable (pre-1.0);
168
+ the license is undecided (`UNLICENSED` until chosen).
@@ -0,0 +1,30 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { CourseConfig } from './course-config.js';
3
+ import { Manifest } from './manifest.js';
4
+ import '../curriculum/validation.js';
5
+ import '../reference/consistency.js';
6
+ import './collections.js';
7
+ import '../reference/types.js';
8
+ import '../reference/lint-ignore.js';
9
+ import '../reference/equations-validate.js';
10
+ import '../reference/tables-validate.js';
11
+ import '../reference/keyed-float-validate.js';
12
+ import '../reference/figures-validate.js';
13
+ import '../reference/gate-math.js';
14
+ import '../reference/gloss.js';
15
+ import './knowledge-version.js';
16
+ import './lesson-derivation.js';
17
+
18
+ /** Fail loudly instead of rendering math triggers whose local cards are stale. */
19
+ declare function assertLocalNotationParity(lessonId: string, authoredKeys: readonly string[], renderedKeys: readonly string[]): void;
20
+ /**
21
+ * Restart Astro when an authored content file changes. Astro reloads MDX on
22
+ * its own, but Explico's manifest is created by `astro.config.mjs`; a restart
23
+ * is what makes the remark plugins and page UI consume the same fresh model.
24
+ *
25
+ * Add this integration once to `integrations` in `astro.config.mjs`. The
26
+ * restart is automatic and editor-agnostic (change/add/unlink all work).
27
+ */
28
+ declare function explicoContentRefresh(courseConfig: CourseConfig, manifest: Manifest): AstroIntegration;
29
+
30
+ export { assertLocalNotationParity, explicoContentRefresh };
@@ -0,0 +1,79 @@
1
+ import { resolve, sep } from "node:path";
2
+ import {
3
+ compileManifest,
4
+ writeManifest
5
+ } from "./manifest.js";
6
+ const watchedServers = /* @__PURE__ */ new WeakSet();
7
+ function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
8
+ const authored = [...new Set(authoredKeys)].sort();
9
+ const rendered = [...new Set(renderedKeys)].sort();
10
+ if (authored.length === rendered.length && authored.every((key, index) => key === rendered[index])) {
11
+ return;
12
+ }
13
+ throw new Error(
14
+ `Stale Explico manifest for ${lessonId}: lesson notation.local has [${authored.join(", ")}], but the page notation bundle has [${rendered.join(", ")}]. Pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations.`
15
+ );
16
+ }
17
+ function replaceInPlace(target, source) {
18
+ if (Array.isArray(target) && Array.isArray(source)) {
19
+ target.splice(0, target.length, ...source);
20
+ return;
21
+ }
22
+ if (!target || !source || typeof target !== "object" || typeof source !== "object") {
23
+ return;
24
+ }
25
+ const mutable = target;
26
+ const fresh = source;
27
+ for (const key of Object.keys(mutable)) {
28
+ if (!(key in fresh)) delete mutable[key];
29
+ }
30
+ for (const [key, value] of Object.entries(fresh)) {
31
+ const current = mutable[key];
32
+ if (Array.isArray(current) && Array.isArray(value) || current && value && typeof current === "object" && typeof value === "object" && !Array.isArray(current) && !Array.isArray(value)) {
33
+ replaceInPlace(current, value);
34
+ } else {
35
+ mutable[key] = value;
36
+ }
37
+ }
38
+ }
39
+ function explicoContentRefresh(courseConfig, manifest) {
40
+ return {
41
+ name: "explico-content-refresh",
42
+ hooks: {
43
+ "astro:server:setup": ({ server }) => {
44
+ if (watchedServers.has(server)) return;
45
+ watchedServers.add(server);
46
+ const contentRoot = `${resolve(process.cwd(), "content")}${sep}`;
47
+ let timer;
48
+ let restarting = false;
49
+ const refresh = (path) => {
50
+ const absolute = resolve(path);
51
+ if (!absolute.startsWith(contentRoot) || restarting) return;
52
+ if (timer) clearTimeout(timer);
53
+ timer = setTimeout(async () => {
54
+ restarting = true;
55
+ try {
56
+ const fresh = await compileManifest(courseConfig);
57
+ writeManifest(fresh);
58
+ replaceInPlace(manifest, fresh);
59
+ await server.restart();
60
+ } catch (error) {
61
+ server.config.logger.error(
62
+ `Explico could not refresh the content manifest: ${error instanceof Error ? error.message : String(error)}`
63
+ );
64
+ } finally {
65
+ restarting = false;
66
+ }
67
+ }, 50);
68
+ };
69
+ server.watcher.on("add", refresh);
70
+ server.watcher.on("change", refresh);
71
+ server.watcher.on("unlink", refresh);
72
+ }
73
+ }
74
+ };
75
+ }
76
+ export {
77
+ assertLocalNotationParity,
78
+ explicoContentRefresh
79
+ };
@@ -6,8 +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';
10
13
  import '../reference/gloss.js';
14
+ import './knowledge-version.js';
11
15
  import './lesson-derivation.js';
12
16
 
13
17
  /**
@@ -91,6 +95,8 @@ interface DomainFileSignatures {
91
95
  readonly signatures: readonly string[];
92
96
  }
93
97
  interface StatusReport {
98
+ /** The course's published knowledge version (`about.version`). */
99
+ readonly knowledgeVersion: string;
94
100
  readonly counts: {
95
101
  readonly lessons: number;
96
102
  readonly competencies: number;
@@ -99,6 +105,8 @@ interface StatusReport {
99
105
  readonly tracks: number;
100
106
  readonly notationDefinitions: number;
101
107
  readonly keyedEquations: number;
108
+ readonly keyedTables: number;
109
+ readonly keyedFigures: number;
102
110
  };
103
111
  readonly draftDebt: {
104
112
  readonly lessons: readonly string[];
@@ -145,6 +153,14 @@ interface ContextReport {
145
153
  key: string;
146
154
  number: string;
147
155
  }[];
156
+ readonly tables: readonly {
157
+ key: string;
158
+ number: string;
159
+ }[];
160
+ readonly figures: readonly {
161
+ key: string;
162
+ number: string;
163
+ }[];
148
164
  readonly sources: readonly {
149
165
  id: string;
150
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,14 +115,17 @@ 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
- [
105
- remarkNotation,
106
- {
107
- definitions: manifest.notation.raw,
108
- equations: manifest.equations.numbersBySlug
109
- }
110
- ],
124
+ // `manifest` carries the number tables *and* the part/offset placement
125
+ // together, so this check numbers a page exactly as the Astro build
126
+ // does. Passing the tables alone silently numbered pages by `##` section
127
+ // here while cross-page references read the part-scheme manifest.
128
+ [remarkNotation, { definitions: manifest.notation.raw, manifest }],
111
129
  [remarkCitation, { sources: manifest.sources }]
112
130
  ],
113
131
  rehypePlugins: [
@@ -279,16 +297,28 @@ function contentStatus(manifest) {
279
297
  ...manifest.diagnostics.alignment.map((d) => d.severity),
280
298
  ...manifest.diagnostics.math.map((d) => d.severity),
281
299
  ...manifest.diagnostics.equations.map((d) => d.severity),
282
- ...manifest.diagnostics.consistency.map(() => "warning")
300
+ ...manifest.diagnostics.tables.map((d) => d.severity),
301
+ ...manifest.diagnostics.figures.map((d) => d.severity),
302
+ ...manifest.diagnostics.consistency.map(() => "warning"),
303
+ ...manifest.diagnostics.knowledge.map((d) => d.severity)
283
304
  ];
284
305
  const keyedEquations = manifest.lessons.reduce(
285
306
  (count, lesson) => count + lesson.equations.length,
286
307
  0
287
308
  );
309
+ const keyedTables = manifest.lessons.reduce(
310
+ (count, lesson) => count + lesson.tables.length,
311
+ 0
312
+ );
313
+ const keyedFigures = manifest.lessons.reduce(
314
+ (count, lesson) => count + lesson.figures.length,
315
+ 0
316
+ );
288
317
  const assessments = new Set(
289
318
  manifest.lessons.flatMap((lesson) => lesson.assessments)
290
319
  ).size;
291
320
  return {
321
+ knowledgeVersion: manifest.about.knowledge.version,
292
322
  counts: {
293
323
  lessons: manifest.lessons.length,
294
324
  competencies: manifest.competencies.length,
@@ -296,7 +326,9 @@ function contentStatus(manifest) {
296
326
  sources: manifest.sources.length,
297
327
  tracks: manifest.tracks.length,
298
328
  notationDefinitions: manifest.notation.definitions.length,
299
- keyedEquations
329
+ keyedEquations,
330
+ keyedTables,
331
+ keyedFigures
300
332
  },
301
333
  draftDebt: { lessons: draftLessons, notation: draftNotation },
302
334
  orphans,
@@ -350,6 +382,8 @@ function contentContext(manifest, rawId) {
350
382
  ...definition.formula === void 0 ? {} : { formula: definition.formula }
351
383
  })),
352
384
  equations: lesson.equations.map((equation) => ({ ...equation })),
385
+ tables: lesson.tables.map((table) => ({ ...table })),
386
+ figures: lesson.figures.map((figure) => ({ ...figure })),
353
387
  sources: lesson.sources.map((id) => {
354
388
  const record = sourceById.get(id);
355
389
  return {
@@ -3,6 +3,7 @@ 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";
7
8
  import { resolveGlosses } from "../reference/gloss.js";
8
9
  import {
@@ -136,7 +137,9 @@ function extractNotationReferences(text, file, kind) {
136
137
  for (const [pattern, referenceKind] of patterns) {
137
138
  for (const match of searchable.matchAll(pattern)) {
138
139
  const key = (match[1] ?? "").trim();
139
- if (parseEquationRef(key)) continue;
140
+ if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" })) {
141
+ continue;
142
+ }
140
143
  references.push({
141
144
  key,
142
145
  kind: referenceKind,