explico 0.5.0 → 0.7.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 (88) hide show
  1. package/CHANGELOG.md +295 -7
  2. package/README.md +16 -2
  3. package/dist/compiler/astro.d.ts +13 -2
  4. package/dist/compiler/astro.js +39 -0
  5. package/dist/compiler/cli.d.ts +22 -3
  6. package/dist/compiler/cli.js +71 -10
  7. package/dist/compiler/course-config.d.ts +31 -1
  8. package/dist/compiler/course-config.js +7 -0
  9. package/dist/compiler/exports/standalone-html.js +13 -0
  10. package/dist/compiler/logging.d.ts +109 -0
  11. package/dist/compiler/logging.js +107 -0
  12. package/dist/compiler/manifest.d.ts +16 -4
  13. package/dist/compiler/manifest.js +7 -3
  14. package/dist/reference/base-abbreviations.mjs +8 -2
  15. package/dist/reference/consistency.d.ts +8 -10
  16. package/dist/reference/consistency.js +3 -47
  17. package/dist/reference/delimiter-size.d.ts +39 -0
  18. package/dist/reference/delimiter-size.js +177 -0
  19. package/dist/reference/diagrams-validate.d.ts +1 -1
  20. package/dist/reference/equations-validate.d.ts +2 -2
  21. package/dist/reference/equations-validate.js +1 -1
  22. package/dist/reference/figures-validate.d.ts +1 -1
  23. package/dist/reference/gate-heading-length.d.ts +12 -0
  24. package/dist/reference/gate-heading-length.js +79 -0
  25. package/dist/reference/gate-math.d.ts +8 -2
  26. package/dist/reference/gate-math.js +30 -7
  27. package/dist/reference/keyed-float-validate.d.ts +2 -2
  28. package/dist/reference/keyed-float-validate.js +1 -1
  29. package/dist/reference/math-glyphs.d.mts +12 -0
  30. package/dist/reference/math-glyphs.mjs +183 -42
  31. package/dist/reference/references.js +2 -1
  32. package/dist/reference/registry.js +1 -1
  33. package/dist/reference/rehype-base-links.d.mts +23 -0
  34. package/dist/reference/rehype-base-links.mjs +40 -0
  35. package/dist/reference/remark-abbreviations.mjs +10 -12
  36. package/dist/reference/remark-citation.d.mts +5 -1
  37. package/dist/reference/remark-citation.mjs +0 -0
  38. package/dist/reference/render-lab-math.d.ts +18 -1
  39. package/dist/reference/render-lab-math.js +10 -3
  40. package/dist/reference/tables-validate.d.ts +1 -1
  41. package/dist/reference/types.d.ts +1 -1
  42. package/docs/VERSIONING.md +24 -4
  43. package/docs/authoring/README.md +14 -2
  44. package/docs/authoring/agents.md +7 -3
  45. package/docs/authoring/components.md +4 -1
  46. package/docs/authoring/migrating.md +477 -8
  47. package/docs/authoring/notation.md +194 -7
  48. package/docs/authoring/prose.md +64 -0
  49. package/docs/authoring/structure-rules.md +69 -0
  50. package/docs/authoring/visualizations.md +2 -2
  51. package/package.json +1 -1
  52. package/src/compiler/astro.ts +89 -0
  53. package/src/compiler/cli.ts +144 -12
  54. package/src/compiler/course-config.ts +40 -0
  55. package/src/compiler/exports/standalone-html.ts +15 -0
  56. package/src/compiler/logging.ts +248 -0
  57. package/src/compiler/manifest.ts +20 -3
  58. package/src/components/diagram/Diagram.astro +0 -55
  59. package/src/components/memo/Memo.astro +2 -5
  60. package/src/components/memo/ShortNote.astro +2 -5
  61. package/src/components/notation/NotationFormula.astro +199 -10
  62. package/src/components/notation/NotationGlossary.astro +89 -58
  63. package/src/components/notation/NotationLayer.astro +238 -156
  64. package/src/components/scroll-animation.ts +160 -0
  65. package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
  66. package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
  67. package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
  68. package/src/components/starlight/LessonFooter.astro +5 -1
  69. package/src/content-config.ts +16 -1
  70. package/src/reference/base-abbreviations.mjs +8 -2
  71. package/src/reference/consistency.ts +10 -66
  72. package/src/reference/delimiter-size.ts +301 -0
  73. package/src/reference/diagrams-validate.ts +1 -1
  74. package/src/reference/equations-validate.ts +3 -3
  75. package/src/reference/figures-validate.ts +1 -1
  76. package/src/reference/gate-heading-length.ts +131 -0
  77. package/src/reference/gate-math.ts +48 -8
  78. package/src/reference/keyed-float-validate.ts +3 -3
  79. package/src/reference/math-glyphs.mjs +183 -42
  80. package/src/reference/references.ts +2 -1
  81. package/src/reference/registry.ts +1 -1
  82. package/src/reference/rehype-base-links.mjs +40 -0
  83. package/src/reference/remark-abbreviations.mjs +10 -12
  84. package/src/reference/remark-citation.mjs +0 -0
  85. package/src/reference/render-lab-math.ts +24 -1
  86. package/src/reference/tables-validate.ts +1 -1
  87. package/src/reference/types.ts +1 -1
  88. package/src/styles/global.css +120 -15
@@ -38,8 +38,8 @@ passed now fails.
38
38
  `CHANGELOG.md`).
39
39
 
40
40
  **PATCH** — a bug fix, a docs change, an internal refactor with no surface
41
- change, or a new **Tier-3 warning** (warnings never fail a build, so surfacing
42
- one does not break a consumer).
41
+ change, or a new **Tier-3 warning/info diagnostic** (neither fails a build, so
42
+ surfacing one does not break a consumer).
43
43
 
44
44
  ### Pre-1.0
45
45
 
@@ -66,6 +66,21 @@ whether the bump is MAJOR, MINOR, or PATCH. The section must do one of two thing
66
66
  - state explicitly that no course changes are required, followed by the normal
67
67
  verification commands.
68
68
 
69
+ **Every build-enforced/authoring rule and every manifest-schema bump the
70
+ release's `CHANGELOG.md` entry names gets its own migration coverage.** 0.5.0
71
+ shipped with only rule N14 and the schema-version bump covered — N9, N10, N13,
72
+ and N15 were all in the changelog and all missing from `migrating.md`, and the
73
+ schema-chain statement itself skipped the v8 and v9 bumps. A migration section
74
+ that has *some* ordered steps is not the same as a *complete* one: derive it by
75
+ walking the release's complete changelog entry line by line, not just its
76
+ headline items. `pnpm check:release-migration` now cross-checks this
77
+ mechanically (see below) — it greps the changelog entry for `rule N9`-style
78
+ mentions and `is now v9`-style schema bumps and fails if the matching version's
79
+ `migrating.md` section doesn't name each one the same way (`rule N9`, `v9`).
80
+ Keep both files' phrasing in that shape; a rule discussed without the word
81
+ "rule" immediately before its ID, or a schema bump not written `is now vN`,
82
+ won't be picked up by either side of the check.
83
+
69
84
  The changelog explains what changed and why; it is not a substitute for the
70
85
  migration section. The migration section, changelog entry, version bump, and
71
86
  package are one atomic release. An agent upgrading across several versions
@@ -77,12 +92,17 @@ applies every intervening section in order.
77
92
  section of `CHANGELOG.md`.
78
93
  2. Add the section for `x.y.z` to `docs/authoring/migrating.md`, derived from the
79
94
  complete release diff. Write explicit no-action instructions when nothing in
80
- a downstream course needs to change.
95
+ a downstream course needs to change. Walk the `CHANGELOG.md` entry you're
96
+ about to finalize bullet by bullet and give every `rule N#` it names, and
97
+ every `is now vN` schema bump, its own coverage — not only the entry's
98
+ headline item.
81
99
  3. `CHANGELOG.md`: rename `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD`, add a
82
100
  fresh empty `## [Unreleased]`, and fix up the compare links at the bottom.
83
101
  4. Bump `package.json` `version` (`npm version x.y.z --no-git-tag-version`).
84
102
  5. Run `pnpm check:release-migration`. It fails when the new version has no
85
- migration section or the shipped-file configuration excludes the guide.
103
+ migration section, the shipped-file configuration excludes the guide, or a
104
+ `rule N#` / `is now vN` mention in the `CHANGELOG.md` entry has no match in
105
+ the `migrating.md` section.
86
106
  6. Run `pnpm build && pnpm test`, then inspect `pnpm pack --dry-run` and confirm
87
107
  that `docs/authoring/migrating.md` and `docs/authoring/agents.md` are listed.
88
108
  7. Commit, then `git tag vx.y.z`.
@@ -22,6 +22,12 @@ Two kinds of rule live here:
22
22
  - **Review-only** — nothing checks it automatically; a reviewer does. Breaking
23
23
  one is a review comment, not a broken build.
24
24
 
25
+ Automated diagnostics use three severities. An **error** means the content is
26
+ incorrect, ambiguous, unexplainable, or cannot render reliably and therefore
27
+ blocks validation. A **warning** is non-blocking but marks a rule, readability,
28
+ or maintenance problem that should be repaired. **Info** is optional advice;
29
+ leaving it in place can be intentional and creates no repair debt.
30
+
25
31
  Every rule is written to be falsifiable: you can look at a lesson and say
26
32
  whether it complies.
27
33
 
@@ -43,7 +49,7 @@ speak in: what a part, a chapter, a section, and a subsection are, and which of
43
49
  them an equation number counts.
44
50
 
45
51
  Each rule has a stable ID — a letter for its area (`N` notation,
46
- `V` visualizations, `C` versioning) plus a number — and this shape:
52
+ `V` visualizations, `C` versioning, `S` structure, `P` prose) plus a number — and this shape:
47
53
 
48
54
  > **N1 — One-line imperative.**
49
55
  > _Rationale._ Why the rule exists; the failure it prevents.
@@ -74,6 +80,8 @@ file table.
74
80
  | [notation.md](notation.md) | `N` | how math symbols are written and bound |
75
81
  | [visualizations.md](visualizations.md) | `V` | diagrams, charts, interactive figures, randomness |
76
82
  | [versioning.md](versioning.md) | `C` | the knowledge version and `content/CHANGELOG.md` |
83
+ | [structure-rules.md](structure-rules.md) | `S` | heading and figure/diagram/memo/short-note title length |
84
+ | [prose.md](prose.md) | `P` | how a lesson's sentences are worded |
77
85
  | [components.md](components.md) | — | _catalog_: inline/block features available in a lesson body — tooltips, tables, figures, diagrams, memos, abbreviations, citations |
78
86
  | [agents.md](agents.md) | — | _guide_: working on a course repo (with an AI agent) |
79
87
  | [migrating.md](migrating.md) | — | _guide_: per-version steps when bumping the `explico` dep |
@@ -96,7 +104,9 @@ file table.
96
104
  | [N12](notation.md#n12) | Math syntax never carries the sentence's own trailing punctuation | Review-only |
97
105
  | [N13](notation.md#n13) | A nonlinear function always takes a parenthesized argument | Build-enforced (`resolveMathGlyphs`) |
98
106
  | [N14](notation.md#n14) | An abbreviation-shaped word in prose is universal, or explained | Build-enforced (`gate-abbreviations.ts`) |
99
- | [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use `\left`/`\right` sizing | Build-enforced (`resolveMathGlyphs`) |
107
+ | [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use scalable sizing | Build-enforced (`resolveMathGlyphs`) |
108
+ | [N16](notation.md#n16) | AI-generated stylistic grouping alternates delimiter types | Review-only |
109
+ | [N17](notation.md#n17) | Nested scalable delimiters grow strictly from inside to outside | Tier-3 warning (`delimiter-size-hierarchy`) |
100
110
  | [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
101
111
  | [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
102
112
  | [V3](visualizations.md#v3) | Interactive figures: island + `renderLabMath` seam + text alt | Review-only |
@@ -106,3 +116,5 @@ file table.
106
116
  | [C2](versioning.md#c2) | The knowledge version is semver over learning-facing content | Review-only |
107
117
  | [C3](versioning.md#c3) | `about.version` equals the newest `content/CHANGELOG.md` heading | Build-enforced (Tier-3 `knowledge-version-mismatch`) |
108
118
  | [C4](versioning.md#c4) | On release, move `[Unreleased]` and bump `about.version` together | Review-only |
119
+ | [S1](structure-rules.md#s1) | No heading, or figure/diagram/memo/short-note title, over 65 characters | Build-enforced (`gate-heading-length.ts`) for body/floats + review for `frontmatter.title` |
120
+ | [P1](prose.md#p1) | "or" never introduces a second name for the same thing | Review-only |
@@ -7,7 +7,9 @@ particular to your layout.
7
7
 
8
8
  This is a guide, not a rule area. The enforceable rules are in
9
9
  [`notation.md`](notation.md) (`N`), [`visualizations.md`](visualizations.md)
10
- (`V`), and [`versioning.md`](versioning.md) (`C`). When the engine's behaviour
10
+ (`V`), [`versioning.md`](versioning.md) (`C`),
11
+ [`structure-rules.md`](structure-rules.md) (`S`), and [`prose.md`](prose.md)
12
+ (`P`). When the engine's behaviour
11
13
  and a rule disagree, the engine wins — see [`README.md`](README.md).
12
14
 
13
15
  ## The loop
@@ -15,8 +17,10 @@ and a rule disagree, the engine wins — see [`README.md`](README.md).
15
17
  - **Start from the CLI, not a file hunt.** `pnpm --filter <course> content
16
18
  context <lesson>` prints a lesson plus exactly its notation, references, and
17
19
  assumptions. `pnpm content status` is the draft-debt overview. `pnpm content
18
- check <lesson> | --changed` is schema + refs + MDX→KaTeX, exits non-zero on an
19
- error. Read those before opening files.
20
+ diagnostics info` lists everything; `warning` and `error` raise the minimum
21
+ severity. `pnpm content warnings` lists warnings only.
22
+ `pnpm content check <lesson> | --changed` is schema + refs + MDX→KaTeX, exits
23
+ non-zero on an error. Read those before opening files.
20
24
  - **Validate, twice.** `pnpm --filter <course> validate:content` regenerates
21
25
  `build/resolution/*` and then fails on the same run that changed them. Run it
22
26
  again; the second run is clean. Commit the regenerated `build/` and
@@ -92,7 +92,10 @@ Two naming traps worth flagging for an AI author:
92
92
  - **`<Figure title="…">` is a visible short label in the collapsible title
93
93
  bar, not the HTML `title` attribute's hover tooltip.** It always renders on
94
94
  the page; it is a different feature from the [native tooltip](#inline-zero-registration)
95
- above even though it reuses the same prop name.
95
+ above even though it reuses the same prop name. Keep it short — the whole
96
+ caption sentence does not belong here; see [S1](structure-rules.md#s1). An
97
+ omitted `title` silently falls back to `caption` in full, which is exactly
98
+ the failure S1 exists to catch.
96
99
  - **A raw Markdown pipe table or a raw `<table>`, or a `<Diagram>` with no
97
100
  `id`,** renders fine but never enters keyed-float validation — no number,
98
101
  no `[[…]]` target. Wrap it (`<Table>`/`<Figure>`) or give the diagram an
@@ -33,9 +33,417 @@ required config block throws with the section of this file to read.
33
33
 
34
34
  ---
35
35
 
36
+ ## 0.6.0 → 0.7.0
37
+
38
+ ### 1. Manifest schema is now v15: diagnostic severity has three levels
39
+
40
+ `MANIFEST_SCHEMA_VERSION` is 15 (v14 → v15). Public diagnostic severity unions
41
+ now include `info`, so a client that treated "not an error" as "a warning"
42
+ needs three edits:
43
+
44
+ 1. A client asserting `MANIFEST_SCHEMA_VERSION` by exact equality expects 15.
45
+ 2. Widen any hand-written `'error' | 'warning'` type over manifest diagnostics
46
+ to `'error' | 'warning' | 'info'`. `ConsistencyDiagnostic.severity` is now
47
+ `'warning' | 'info'`.
48
+ 3. In `scripts/compile-manifest.ts` (and any other consumer), filter on
49
+ `severity === 'warning'` before printing or counting warnings. A script that
50
+ counts all of `manifest.diagnostics.consistency` as "Tier-3 findings" now
51
+ over-reports:
52
+
53
+ ```diff
54
+ -const consistencyByCode = consistencyCounts(consistency);
55
+ -const consistencyTotal = consistency.length;
56
+ +const consistencyWarnings = consistency.filter(
57
+ + (diagnostic) => diagnostic.severity === 'warning',
58
+ +);
59
+ +const consistencyByCode = consistencyCounts(consistencyWarnings);
60
+ +const consistencyTotal = consistencyWarnings.length;
61
+ ```
62
+
63
+ These codes moved from `warning` to `info`: `eq-key-unused`, every keyed-float
64
+ `*-key-unused`, `unused-definition`, `weak-local`, `gloss-wants-promoting`, and
65
+ `card-wants-demoting`. Their code strings and container fields did not change,
66
+ the `lint-ignore.yml` lines that already worked for `weak-local`,
67
+ `gloss-wants-promoting`, and `card-wants-demoting` still match, and nothing
68
+ blocks the build. They no longer appear in `pnpm content warnings`; read them
69
+ with `pnpm content diagnostics info`.
70
+
71
+ Run `pnpm validate:content` twice. The committed `build/resolution/*` files
72
+ (including `consistency-report.json`) change because those findings now carry
73
+ `info` severity and `numerals-tagged` findings are gone (item 2); commit them.
74
+
75
+ ### 2. Retired diagnostic: `numerals-tagged`
76
+
77
+ `ConsistencyCode` no longer contains `numerals-tagged`, and the engine never
78
+ emits it. Delete every `numerals-tagged` line from `lint-ignore.yml` (a stale
79
+ line is inert but misleads the next reader) and every course script, test, or
80
+ type narrowing that names the code — `tsc`/`astro check` flags the latter as a
81
+ comparison against a non-member of the union.
82
+
83
+ ### 3. `explicoContentRefresh()` fails the build on a missing remark plugin (rule N14 and its siblings)
84
+
85
+ This is the enforcement half of rule N14's Astro wiring (see item 4 of the
86
+ 0.4.x → 0.5.0 section). Its `astro:config:setup` hook now compares the manifest
87
+ against the configured remark plugins and throws, naming each gap, when a
88
+ non-empty table has no matching plugin registered:
89
+
90
+ | Manifest table is non-empty | Plugin that must be registered |
91
+ | --- | --- |
92
+ | `manifest.abbreviations` | `[remarkAbbreviations, { abbreviations: manifest.abbreviations }]` from `explico/reference/remark-abbreviations.mjs` |
93
+ | `manifest.sources` | `[remarkCitation, { sources: manifest.sources }]` from `explico/reference/remark-citation.mjs` |
94
+ | `manifest.notation.raw` | `[remarkNotation, createRemarkNotationOptions(manifest)]` from `explico/reference/remark-notation.mjs` |
95
+
96
+ Check `astro.config.mjs` against that table. The check reads both wiring
97
+ surfaces Astro offers — the flat `markdown.remarkPlugins` array and a
98
+ `markdown.processor` such as `unified({ remarkPlugins: [...] })` — and accepts
99
+ either the bare plugin or a `[plugin, options]` tuple. It compares by identity
100
+ with the module the `explico/reference/*.mjs` specifier resolves to, so register
101
+ the imported function itself rather than a local wrapper around it. A course
102
+ that was already fully wired needs no edit; one that skipped a plugin was
103
+ silently rendering triggers as plain text and now fails `astro build` /
104
+ `astro dev` until it adds the missing registration.
105
+
106
+ ### 4. Removed markup: popup "Copy LaTeX", glossary `#` copy-link, formula scroll region
107
+
108
+ None of these has a replacement selector; update course-owned CSS and
109
+ end-to-end tests that target them:
110
+
111
+ - `.notation-popup-copy` / `[data-popup-copy]` — the button in a notation
112
+ popup. Copying is now a click on the equation itself (item 8).
113
+ - `[data-glossary-anchor]` and its "Link copied" flash — a glossary entry's `#`
114
+ button that wrote its URL to the clipboard. The entry title is now a link to
115
+ `#notation-<key>` (`.glossary-term-link`), and a hover-revealed link-icon
116
+ anchor (`.glossary-anchor`) simply navigates; nothing is copied.
117
+ - `NotationFormula`'s `tabindex="0"` scroll region moved from
118
+ `.notation-formula-math` to a nested `.notation-formula-scroll`. A test that
119
+ focuses or scrolls `.notation-formula-math` must target the inner element.
120
+
121
+ No source or config change is needed beyond selectors.
122
+
123
+ ### 5. Rule N14: every occurrence of a catalogued abbreviation is now wrapped
124
+
125
+ Two changes ride under rule N14, neither of which needs a config edit:
126
+
127
+ - **Rendered output changed.** `remarkAbbreviations` used to wrap only the first
128
+ occurrence of each catalogued abbreviation per page; it now wraps every
129
+ occurrence (`a.abbreviation-term[data-abbr-key]`). A course-owned e2e test or
130
+ snapshot that expected exactly one such link per term per page, or CSS that
131
+ assumed a page's later occurrences were plain text, must be updated.
132
+ `AbbreviationLayer.astro` already resolved markers independently by
133
+ `data-abbr-key`, so no island change is needed.
134
+ - **The shipped baseline grew** (`base-abbreviations.mjs`): `AM`, `PM`, `A.M.`,
135
+ `P.M.`, and `e.t.c.` join `a.m.`, `p.m.`, `e.g.`, `i.e.`, and `etc.`. This only
136
+ loosens the gate. A now-redundant `abbreviations.allow` line or
137
+ `content/abbreviations/*.md` entry for one of them is harmless and can stay; a
138
+ course that wants one explained anyway lists it in `abbreviations.deny`.
139
+
140
+ ### 6. Rule N16 and rule N17: delimiter conventions and sizes
141
+
142
+ Rule N16 (alternate `()`, `[]`, `\{\}` for purely stylistic nested grouping,
143
+ never for conventional delimiters such as `f(x)`, `\mathbb{E}[X]`, or
144
+ intervals) is **review-only**: nothing checks it and nothing needs to change to
145
+ keep building. Rule N17 (every outer scalable pair in a nest grows strictly
146
+ larger than the pairs inside it: `\left…\right` → `\Bigl…\Bigr` → `\biggl…\biggr`
147
+ → `\Biggl…\Biggr`) is a **Tier-3 warning**, `delimiter-size-hierarchy`, reported
148
+ at the outer pair's line and column by the normal math gate for lesson math,
149
+ notation formulas, and assessment math. It never fails a build, so no course
150
+ needs to change to keep building. To clear the warnings, run
151
+ `pnpm content diagnostics warning`, and enlarge each named outer pair to the
152
+ next manual size without changing its delimiter shape. Rule N15's build-enforced
153
+ behavior is unchanged — its documentation now also accepts an N17 manual pair as
154
+ "sized". See [`notation.md#n16`](notation.md#n16) and
155
+ [`notation.md#n17`](notation.md#n17).
156
+
157
+ ### 7. Rule P1: "or" never introduces a second name for the same thing
158
+
159
+ Rule P1 opens the new `P` (prose) rule area and is **review-only**: nothing is
160
+ checked at build time and no course change is required. When editing lessons,
161
+ replace "the residual or error term" with "the residual (i.e., the error
162
+ term)"-style wording. See [`prose.md#p1`](prose.md#p1).
163
+
164
+ ### 8. Optional: click-to-copy equations, glossary equation numbers, and live lab LaTeX
165
+
166
+ Nothing to do for a course that wires `Footer` to `LessonFooter.astro`: a
167
+ click on any `[data-notation-formula]` box now copies its `data-latex`, and every
168
+ glossary formula gets a `G.<n>` number and copy button, automatically.
169
+
170
+ Only a course with an interactive lab island that renders math through
171
+ `renderLabMath()` can opt that equation into the gesture, and keep the copied
172
+ LaTeX in step with the sliders, with `plainLatexTemplate` and the new
173
+ `slotPlaceholder` export:
174
+
175
+ ```tsx
176
+ import {
177
+ slotPlaceholder,
178
+ type RenderedLabMath,
179
+ } from 'explico/reference/render-lab-math';
180
+
181
+ // On the math root element:
182
+ <div
183
+ className="lab-math"
184
+ data-notation-formula
185
+ data-latex={rendered.plainLatexTemplate}
186
+ /* … */
187
+ />
188
+
189
+ // Where the island writes each slot's live value (`values`: slot name → text):
190
+ let latex = rendered.plainLatexTemplate;
191
+ for (const [name, value] of Object.entries(values)) {
192
+ latex = latex.split(slotPlaceholder(name)).join(value);
193
+ }
194
+ root.dataset.latex = latex;
195
+ ```
196
+
197
+ Rebuild `latex` from the template on every update, not from the previous result.
198
+ A lab that does not set these attributes is unchanged. `RenderedLabMath` gained
199
+ the required `plainLatexTemplate` field; a course that constructs one by hand
200
+ (a test double, for instance) must add it.
201
+
202
+ ### 9. Only if you override `Footer` yourself: mount `AnchorScrollEnhancer`
203
+
204
+ The new smooth-scroll behavior for in-page anchor links ships in
205
+ `AnchorScrollEnhancer.astro`, which `LessonFooter.astro` mounts. A course whose
206
+ Starlight `components.Footer` is `explico/components/starlight/LessonFooter.astro`
207
+ gets it with no change. A course that swapped in its own footer component must
208
+ render it (`import AnchorScrollEnhancer from
209
+ 'explico/components/starlight/AnchorScrollEnhancer.astro'`, then
210
+ `<AnchorScrollEnhancer />`) or its anchor links keep the browser's native jump.
211
+
212
+ ### 10. Optional: wire `rehypeBaseLinks` for a course deployed under a subpath
213
+
214
+ Skip this for a course served from a domain root (`base` omitted or `'/'`); the
215
+ plugin is a no-op there. For a course under a subpath (a GitHub Pages *project*
216
+ site, `base: '/my-course'`), Starlight prefixes only its own navigation, so
217
+ every root-relative link the Markdown pipeline produces — prose links,
218
+ equation cross-references, citation links — 404s without this:
219
+
220
+ ```diff
221
+ + import rehypeBaseLinks from 'explico/reference/rehype-base-links.mjs';
222
+ …
223
+ rehypePlugins: [
224
+ [rehypeKatex, createNotationKatexOptions()],
225
+ rehypeFailKatexErrors,
226
+ + [rehypeBaseLinks, base],
227
+ ]
228
+ ```
229
+
230
+ `base` is the same value passed to `defineConfig({ base })`. **Undo any manual
231
+ workaround first:** the plugin prefixes every `href` beginning with a single
232
+ `/`, so a lesson link already hand-written as `/my-course/other/` would become
233
+ `/my-course/my-course/other/`. Write lesson links without the base. A lesson's
234
+ Starlight frontmatter (`hero.actions[].link`) never passes through this
235
+ pipeline; write those as paths relative to the current page.
236
+
237
+ ### 11. Optional: run logs for `pnpm validate:content`
238
+
239
+ Route the course's `scripts/compile-manifest.ts` through `createRunLog()` to
240
+ record every diagnostic to `build/logs/<timestamp>/{error,warning,info}.log`
241
+ (Python `logging` format, `build/logs/latest` symlinks the newest run):
242
+
243
+ ```diff
244
+ + import { createRunLog } from 'explico/compiler/logging';
245
+ …
246
+ + const log = createRunLog();
247
+ const manifest = await compileManifest(courseConfig);
248
+ …
249
+ - console.warn(`[${warning.code}] ${warning.message}`);
250
+ + console.warn(log.warning('notation', `[${warning.code}] ${warning.message}`));
251
+ …
252
+ + const summary = log.close();
253
+ + console.log(
254
+ + `Run log: ${summary.relativeDirectory} ` +
255
+ + `(${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s), ${summary.counts.INFO} info).`,
256
+ + );
257
+ ```
258
+
259
+ Each `log.error/warning/info(source, message)` call returns its message, so
260
+ printing and recording share one expression; `log.infoDiagnostics(manifest.diagnostics)`
261
+ records the severity-`info` findings the console never showed. Call
262
+ `log.close()` before any `throw` so a failing run's log is complete
263
+ (`playground/scripts/compile-manifest.ts` is a full worked example). Add
264
+ `build/logs/` to the course's `.gitignore` — a course commits
265
+ `build/resolution/*`, and nothing prunes old runs.
266
+
267
+ ### 12. Optional: the `content diagnostics` / `content warnings` commands and VS Code tasks
268
+
269
+ `content diagnostics [info|warning|error] [--json]` and `content warnings
270
+ [--json]` are new, and `content status` now reports an `info` count
271
+ (`report.diagnostics.infos`). The engine implements them in
272
+ `explico/compiler/cli`, but a course's own `scripts/content.ts` dispatches
273
+ commands, so it must route them:
274
+
275
+ ```diff
276
+ import {
277
+ ContentCliError,
278
+ contentCheck,
279
+ contentContext,
280
+ + contentDiagnostics,
281
+ contentStatus,
282
+ + contentWarnings,
283
+ …
284
+ } from 'explico/compiler/cli';
285
+ ```
286
+
287
+ Add a `case 'warnings'` (`contentWarnings(manifest)`) and a `case 'diagnostics'`
288
+ (validate the one positional as `info`/`warning`/`error`, default `info`, then
289
+ `contentDiagnostics(manifest, severity)`) beside `case 'status'`, printing
290
+ `file:line:column: severity code: message` lines — copy
291
+ `playground/scripts/content.ts`. To surface findings in VS Code's Problems
292
+ panel, copy the three **Explico: Show content diagnostics** tasks from the
293
+ engine repository's `.vscode/tasks.json`, replacing the `--filter` package name
294
+ and the problem matcher's `fileLocation` root with the course's own.
295
+
296
+ ### 13. No action: other changes in this release
297
+
298
+ - Every `<a>` back-link (`↩`) in a page's References section now also carries a
299
+ `title` tooltip; `remark-citation.mjs` does this itself.
300
+ - Standalone HTML export now inlines a `<link>` only when its `rel` is
301
+ `stylesheet` or an icon type, and leaves other `<link>`s (`canonical`,
302
+ `alternate`, `preload`, …) as authored.
303
+
304
+ ### Verification
305
+
306
+ `pnpm validate:content` (run it **twice** — the first run regenerates
307
+ `build/resolution/*`, item 1), then `pnpm content check --changed`, then
308
+ `astro check`, then `astro build`. `astro build` is the step that reports item 3.
309
+
310
+ ---
311
+
312
+ ## 0.5.0 → 0.6.0
313
+
314
+ ### 1. Rule S1: no heading or float title runs past 65 characters
315
+
316
+ Every Markdown heading (`##` through `######`), a lesson's own
317
+ `frontmatter.title`, and the `title`/`label` prop on `Figure`, `Diagram`,
318
+ `Memo`, and `ShortNote` is now capped at 65 characters; `Table` is exempt (it
319
+ has only a `caption`). No wiring is required — the check runs as part of
320
+ `pnpm validate:content` / `astro build` the same as N9/N13/N15. Audit for
321
+ existing violations and shorten them, moving the rest of the sentence into
322
+ the paragraph after a heading or into a float's `caption`. See
323
+ [`docs/authoring/structure-rules.md#s1`](structure-rules.md#s1).
324
+
325
+ ### 2. Removed markup: `.notation-pin`, `[data-notation-control]`, `.notation-layer-heading`
326
+
327
+ The "Page notation" panel's "Pin explanation" button (`.notation-pin`,
328
+ `[data-notation-control]`) and its separate `<h2>` (`.notation-layer-heading`)
329
+ are both gone from the rendered DOM. Pinning a card now works the same way an
330
+ inline prose symbol always has — click its glyph — and the panel's
331
+ `<summary>` is its only heading. Update any course-owned CSS or end-to-end
332
+ test that targets these selectors; no other action is required.
333
+
334
+ ### 3. Removed prop: `<Diagram description>`
335
+
336
+ `Diagram`'s `description` prop and its collapsed "Diagram description"
337
+ fallback block (`.diagram-fallback`) no longer exist. `astro check` fails on
338
+ an excess prop, so remove every `description="…"` passed to `<Diagram>`; fold
339
+ that text into the diagram's `label` (caption) instead.
340
+
341
+ ### 4. Optional: collapsible "Page notation" panel and References section
342
+
343
+ `NotationLayer` gains `defaultOpen`/`rememberState` props (mirroring
344
+ `Figure`'s own), and `remark-citation.mjs`'s generated References heading now
345
+ accepts a `defaultOpen` plugin option — both ride the same persisted,
346
+ deep-linkable disclosure machinery `Figure`/`CollapsibleSections` already use,
347
+ in place of the previous always-native-default `<details>`. No action is
348
+ required to keep building: both default to closed, matching prior behavior,
349
+ and the References option only takes effect on a course whose
350
+ `astro.config.mjs` already wires `rehypeHeadingAnchors` into its rehype
351
+ pipeline. To opt into a different default, set `CourseConfig.disclosureDefaults`:
352
+
353
+ ```ts
354
+ disclosureDefaults: {
355
+ notation: true, // "Page notation" panel starts open for a reader with nothing saved yet
356
+ references: true, // generated References section starts open
357
+ },
358
+ ```
359
+
360
+ ### 5. Manifest schema is now v14 (additive: v13 → v14)
361
+
362
+ `CourseConfig.disclosureDefaults` is new and additive — nothing existing
363
+ changed shape — but a client asserting on `MANIFEST_SCHEMA_VERSION` by exact
364
+ equality needs the bump.
365
+
366
+ ### 6. Re-run `pnpm validate:content` — the notation auto-annotator's silent-failure bugs are fixed
367
+
368
+ Several shapes that previously either dropped a symbol's tooltip silently (no
369
+ build error) or produced malformed output are now fixed at the resolver
370
+ (`resolveMathGlyphs`), not just documented as traps to avoid: a bare
371
+ `\mathrm{…}`-only symbol (`\mathrm{SSR}`, `\mathrm{SST}`) can now auto-resolve
372
+ at every later bare occurrence, a bare symbol immediately next to an
373
+ `\explain{…}{…}` call no longer has its span corrupted, and a partial
374
+ `\explain{key}{…}` wrap no longer produces unparseable doubled output. No
375
+ course-side wiring changes, but re-running `pnpm validate:content` may surface
376
+ two new classes of error that previously passed silently:
377
+
378
+ - **"Unresolved notation"** for a symbol that was relying on one of the fixed
379
+ bugs to appear resolved when it was not (rare — the bugs mostly hid symbols
380
+ entirely rather than falsely resolving them) or for the uncovered remainder
381
+ of a partial `\explain{key}{…}` wrap. Fix by explaining the symbol, or by
382
+ wrapping the key's *entire* declared `latex`, not just its base glyph.
383
+ - **"Page glyphs … have structurally identical LaTeX"** for two different
384
+ notation keys whose declared `latex` is the same `\operatorname{…}` or
385
+ `\mathrm{…}` form (e.g. two keys both `\operatorname{Var}`) — this check
386
+ previously silently skipped every such glyph and could not catch this. Give
387
+ the colliding keys distinct canonical forms, or reach for
388
+ `\explain{key}{…}` at every occurrence of whichever one keeps the shared
389
+ form.
390
+
36
391
  ## 0.4.x → 0.5.0
37
392
 
38
- ### 1. Rule N14: catalogue every abbreviation used in lesson prose
393
+ ### 1. Rule N9: wrap every plain-text quantity, unit, and operator in `$…$`
394
+
395
+ Every physical quantity, unit abbreviation, or math operator ever typed as
396
+ plain text — in lesson prose, a table cell, or (new in 0.5.0) an assessment
397
+ item's `prompt` / `explanation` string — must now be inside `$…$`. `45 m/s`,
398
+ `t (s)`, `vx`, `\|v\|`, `KE/m` are all the same mistake. This is build-enforced
399
+ with **no `lint-ignore.yml` escape hatch**: `pnpm validate:content` / `astro
400
+ build` fails on every existing violation. Fix per
401
+ [`docs/authoring/notation.md#n9`](notation.md#n9) — wrap the symbol in `$…$`
402
+ and fold its unit into the *same* span as `\text{}` (`$t\ \text{(s)}$`, not two
403
+ adjacent spans).
404
+
405
+ Also wire the accessible-table-header plugin this rule ships with — the build
406
+ works without it, but a table header that is now 100% KaTeX (a bare `$t$`
407
+ column, since its unit moved inside `$…$` too) renders with no
408
+ screen-reader-visible name until this is wired:
409
+
410
+ ```diff
411
+ + import rehypeTableHeaderLabels from 'explico/reference/rehype-table-header-labels.mjs';
412
+ …
413
+ rehypePlugins: [
414
+ [rehypeKatex, createNotationKatexOptions()],
415
+ rehypeFailKatexErrors,
416
+ + rehypeTableHeaderLabels,
417
+ ]
418
+ ```
419
+
420
+ It reads the `\explain{key}{…}` key(s) a header's LaTeX names and backfills an
421
+ `sr-only` label from that key's notation metadata — the one prerequisite is
422
+ that the header's symbol resolves to a key at all, so a `\mathrm{…}` compound
423
+ (`\mathrm{KE}/m`) needs each side explicitly `\explain{}`-wrapped.
424
+
425
+ ### 2. Rule N13: a nonlinear function always takes a parenthesized argument
426
+
427
+ `\sin x`, `\log t`, `\exp\theta` — every trig/inverse-trig/hyperbolic function
428
+ plus `exp`/`log`/`ln`/`lg` — is now a hard error unless its argument is
429
+ parenthesized: `\sin(x)` or `\sin\left(x\right)` is required. Same enforcement
430
+ shape as N9: build-enforced, no `lint-ignore.yml` escape. It fires on lesson
431
+ body math, `notation.formula`, assessment `prompt`/`explanation`, `Diagram`
432
+ formulas, and interactive lab math alike, since all of them resolve through
433
+ the same glyph walker. Fix every occurrence; see
434
+ [`docs/authoring/notation.md#n13`](notation.md#n13).
435
+
436
+ ### 3. Rule N15: size every level of a nested visible delimiter
437
+
438
+ Once a `(`, `[`, `\{`, or `|` sits inside another one of those four — same
439
+ type or mixed, e.g. `(a + [b])` — **every level of that nest** must switch to
440
+ `\left`/`\right` sizing: `\sin(2(90°-\theta))` is now a hard error,
441
+ `\sin\left(2\left(90°-\theta\right)\right)` is required. Same enforcement
442
+ shape as N9/N13: build-enforced, no escape hatch. A single, non-nested bare
443
+ delimiter (`(x)`) is untouched. Fix every nest; see
444
+ [`docs/authoring/notation.md#n15`](notation.md#n15).
445
+
446
+ ### 4. Rule N14: catalogue every abbreviation used in lesson prose
39
447
 
40
448
  Every acronym (`OLS`, `MSE`) or dotted abbreviation (`w.r.t.`, `i.i.d.`) used in
41
449
  plain prose must now be on the shipped baseline
@@ -69,14 +477,75 @@ every `abbreviation-explained` diagnostic by adding a
69
477
  `content/abbreviations/<key>.md` entry or an `allow` entry in
70
478
  `course.config.ts`.
71
479
 
72
- ### 2. Manifest schema is v13 (additive: v10 → v11 → v12 → v13)
480
+ The catalog check above runs regardless of Astro wiring, but the inline
481
+ hover/click card on a lesson page's first occurrence of a catalogued
482
+ abbreviation needs its remark plugin wired in too:
73
483
 
74
- `lessons[].lessonExports`-derived fields (v11), `manifest.abbreviations` (v12),
75
- and `manifest.diagnostics.rawFloats` (v13) are new. A client asserting on
76
- `MANIFEST_SCHEMA_VERSION` by exact equality needs the bump; nothing existing
77
- changed shape.
484
+ ```diff
485
+ + import remarkAbbreviations from 'explico/reference/remark-abbreviations.mjs';
486
+ …
487
+ remarkPlugins: [
488
+ …
489
+ [remarkCitation, { sources: manifest.sources }],
490
+ + [remarkAbbreviations, { abbreviations: manifest.abbreviations }],
491
+ ]
492
+ ```
78
493
 
79
- ### 3. Optional: enable standalone HTML / print lesson exports
494
+ Skipping this second half is now **build-enforced**, not just documented:
495
+ `explicoContentRefresh()`'s `astro:config:setup` hook checks
496
+ `markdown.remarkPlugins` against the manifest and throws if a catalogued
497
+ manifest table (`manifest.abbreviations`, `manifest.sources`, or
498
+ `manifest.notation.raw`) has no matching plugin (`remarkAbbreviations`,
499
+ `remarkCitation`, `remarkNotation` respectively) registered — so a
500
+ half-completed migration like this one fails `astro build`/`astro dev`
501
+ instead of silently rendering plain text with `pnpm validate:content` still
502
+ green.
503
+
504
+ ### 5. Optional: rule N10 warns on an unexplained `\mathrm{…}` label
505
+
506
+ A `\mathrm{…}` multi-letter label (`\mathrm{KE}`, `\mathrm{PV}`) not on the
507
+ shipped roman-label baseline (`base-roman-labels.mjs`) or `course.config.ts`'s
508
+ new `romanLabels.allow` now needs a notation entry the reader can hover — the
509
+ `\mathrm{…}` counterpart to N8. Unlike N9/N13/N14/N15, this is a **Tier-3
510
+ warning** (`roman-label-explained`), not a build failure: it never blocks
511
+ `astro build`, and an existing violation can be silenced with a reviewed
512
+ `lint-ignore.yml` line (`roman-label-explained:KE — <reason>`) while content is
513
+ cleaned up. `pnpm validate:content` surfaces it. See
514
+ [`docs/authoring/notation.md#n10`](notation.md#n10).
515
+
516
+ ### 6. Only if you render `AssessmentRunner` directly (bypassing `AssessmentSet.astro`)
517
+
518
+ `AssessmentRunner`'s `Item` props changed shape: `prompt` / `explanation`
519
+ (`string`) are now `promptContent` / `explanationContent`
520
+ (`MixedContentNode[]`), rendered with the new `RenderedContent` component,
521
+ because `AssessmentSet.astro` now pre-renders assessment math through the same
522
+ KaTeX pipeline as lesson body math instead of handing `AssessmentRunner` a raw
523
+ string. A course that only uses `<AssessmentSet>` needs no change here — this
524
+ only applies to a course that imports and renders `AssessmentRunner` itself.
525
+
526
+ ### 7. Manifest schema is v13 (additive: v7 → v8 → v9 → v10 → v11 → v12 → v13)
527
+
528
+ Every step is additive — nothing existing changed shape — but a client
529
+ asserting on `MANIFEST_SCHEMA_VERSION` by exact equality needs the final
530
+ number:
531
+
532
+ - **v8** — `assessmentContent`: every assessment item's `prompt` /
533
+ `explanation`, pre-split and resolved for `AssessmentSet.astro` (see item 6
534
+ above).
535
+ - **v9** — `diagrams`, `lessons[].diagrams`, `lessons[].numbering.diagrams`,
536
+ `diagnostics.diagrams`: `<Diagram>` becomes a first-class keyed float with
537
+ its own number stream and `[[dia-<id>]]` references.
538
+ - **v10** — `checks`, `lessons[].knowledgeChecks`, `lessons[].numbering.checks`,
539
+ `diagnostics.checks`: numbered, referenceable knowledge checks.
540
+ - **v11** — `lessons[].lessonExports`-derived fields: standalone HTML / print
541
+ export settings (item 8 below).
542
+ - **v12** — `manifest.abbreviations` (rule N14, item 4 above).
543
+ - **v13** — `manifest.diagnostics.rawFloats`: a raw Markdown/HTML table, an
544
+ unkeyed `<Diagram>`, or a `<Diagram>` nested outside
545
+ `<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` — none of these reach
546
+ keyed-float validation, so this diagnostic flags them separately.
547
+
548
+ ### 8. Optional: enable standalone HTML / print lesson exports
80
549
 
81
550
  No action required to keep building. To opt in, add to
82
551
  `content/course.config.ts`:
@@ -89,7 +558,7 @@ lessonExports: {
89
558
  canonicalBaseUrl: 'https://your-course.example.com',
90
559
  ```
91
560
 
92
- ### 4. Optional: turn on strict numbering cross-checks
561
+ ### 9. Optional: turn on strict numbering cross-checks
93
562
 
94
563
  `remarkNotation`'s new `strict: true` option — recommended via
95
564
  `createRemarkNotationOptions(manifest, { strict: true })` — fails the build on