explico 0.6.0 → 0.8.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 (95) hide show
  1. package/CHANGELOG.md +350 -1
  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 +30 -4
  6. package/dist/compiler/cli.js +79 -10
  7. package/dist/compiler/collections.js +16 -3
  8. package/dist/compiler/exports/standalone-html.js +13 -0
  9. package/dist/compiler/logging.d.ts +119 -0
  10. package/dist/compiler/logging.js +108 -0
  11. package/dist/compiler/manifest.d.ts +7 -3
  12. package/dist/compiler/manifest.js +1 -1
  13. package/dist/reference/base-abbreviations.mjs +8 -2
  14. package/dist/reference/consistency.d.ts +8 -10
  15. package/dist/reference/consistency.js +3 -47
  16. package/dist/reference/delimiter-size.d.ts +48 -0
  17. package/dist/reference/delimiter-size.js +153 -0
  18. package/dist/reference/diagrams-validate.d.ts +1 -1
  19. package/dist/reference/equations-validate.d.ts +2 -2
  20. package/dist/reference/equations-validate.js +1 -1
  21. package/dist/reference/equations.d.mts +9 -3
  22. package/dist/reference/equations.mjs +7 -1
  23. package/dist/reference/figures-validate.d.ts +1 -1
  24. package/dist/reference/gate-abbreviations.js +2 -2
  25. package/dist/reference/gate-heading-length.js +2 -2
  26. package/dist/reference/gate-math.d.ts +25 -1
  27. package/dist/reference/gate-math.js +50 -9
  28. package/dist/reference/gate-prose-notation.d.ts +12 -1
  29. package/dist/reference/gate-prose-notation.js +5 -1
  30. package/dist/reference/katex-delimiters.d.mts +15 -0
  31. package/dist/reference/katex-delimiters.mjs +125 -0
  32. package/dist/reference/katex-options.mjs +4 -0
  33. package/dist/reference/keyed-float-validate.d.ts +2 -2
  34. package/dist/reference/keyed-float-validate.js +1 -1
  35. package/dist/reference/math-glyphs.mjs +16 -2
  36. package/dist/reference/references.js +2 -1
  37. package/dist/reference/registry.js +1 -1
  38. package/dist/reference/rehype-base-links.d.mts +23 -0
  39. package/dist/reference/rehype-base-links.mjs +40 -0
  40. package/dist/reference/remark-abbreviations.mjs +10 -12
  41. package/dist/reference/remark-citation.mjs +0 -0
  42. package/dist/reference/render-lab-math.d.ts +18 -1
  43. package/dist/reference/render-lab-math.js +11 -4
  44. package/dist/reference/tables-validate.d.ts +1 -1
  45. package/dist/reference/types.d.ts +8 -1
  46. package/docs/VERSIONING.md +2 -2
  47. package/docs/authoring/README.md +12 -2
  48. package/docs/authoring/agents.md +7 -3
  49. package/docs/authoring/migrating.md +514 -0
  50. package/docs/authoring/notation.md +157 -7
  51. package/docs/authoring/prose.md +64 -0
  52. package/docs/katex-bracket-scaling.md +191 -0
  53. package/package.json +2 -1
  54. package/src/compiler/astro.ts +89 -0
  55. package/src/compiler/cli.ts +167 -12
  56. package/src/compiler/collections.ts +33 -3
  57. package/src/compiler/exports/standalone-html.ts +15 -0
  58. package/src/compiler/logging.ts +259 -0
  59. package/src/compiler/manifest.ts +7 -3
  60. package/src/components/memo/Memo.astro +2 -5
  61. package/src/components/memo/ShortNote.astro +2 -5
  62. package/src/components/notation/NotationFormula.astro +199 -10
  63. package/src/components/notation/NotationGlossary.astro +100 -58
  64. package/src/components/notation/NotationLayer.astro +83 -81
  65. package/src/components/notation/render-notation-math.ts +111 -10
  66. package/src/components/scroll-animation.ts +160 -0
  67. package/src/components/starlight/AboutPanel.astro +17 -2
  68. package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
  69. package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
  70. package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
  71. package/src/components/starlight/LessonFooter.astro +2 -0
  72. package/src/reference/base-abbreviations.mjs +8 -2
  73. package/src/reference/consistency.ts +10 -66
  74. package/src/reference/delimiter-size.ts +279 -0
  75. package/src/reference/diagrams-validate.ts +1 -1
  76. package/src/reference/equations-validate.ts +3 -3
  77. package/src/reference/equations.mjs +7 -1
  78. package/src/reference/figures-validate.ts +1 -1
  79. package/src/reference/gate-abbreviations.ts +2 -2
  80. package/src/reference/gate-heading-length.ts +2 -2
  81. package/src/reference/gate-math.ts +91 -8
  82. package/src/reference/gate-prose-notation.ts +15 -1
  83. package/src/reference/katex-delimiters.mjs +125 -0
  84. package/src/reference/katex-options.mjs +4 -0
  85. package/src/reference/keyed-float-validate.ts +3 -3
  86. package/src/reference/math-glyphs.mjs +16 -2
  87. package/src/reference/references.ts +2 -1
  88. package/src/reference/registry.ts +1 -1
  89. package/src/reference/rehype-base-links.mjs +40 -0
  90. package/src/reference/remark-abbreviations.mjs +10 -12
  91. package/src/reference/remark-citation.mjs +0 -0
  92. package/src/reference/render-lab-math.ts +25 -2
  93. package/src/reference/tables-validate.ts +1 -1
  94. package/src/reference/types.ts +8 -1
  95. package/src/styles/global.css +119 -14
@@ -33,6 +33,510 @@ required config block throws with the section of this file to read.
33
33
 
34
34
  ---
35
35
 
36
+ ## 0.7.0 → 0.8.0
37
+
38
+ ### 1. Rule N17 is inverted: delimiters are sized by the engine (`\left`/`\right` everywhere)
39
+
40
+ The engine now installs its own KaTeX delimiter sizing, so a `\left … \right`
41
+ pair is always strictly taller than its content and a nest shows its depth
42
+ without help. Hand-picking `\bigl` → `\Bigl` → `\biggl` → `\Biggl` outward —
43
+ which rule N17 previously *required* — is now the thing the linter warns about.
44
+
45
+ **There is no install step.** `astro.config.mjs` already imports
46
+ `explico/reference/katex-options.mjs`, and that module now imports
47
+ `explico/reference/katex-delimiters.mjs` for its side effect. Confirm the import
48
+ is still there; a course that inlined its own KaTeX options object instead must
49
+ add, as the **first** import in `astro.config.mjs`:
50
+
51
+ ```js
52
+ import 'explico/reference/katex-delimiters.mjs';
53
+ ```
54
+
55
+ Order matters: the installer moves KaTeX's render entry points onto the shared
56
+ `katex` object, so anything that renders math before the import would keep stock
57
+ spacing. It refuses to fail quietly — it renders a known nest at install time and
58
+ throws if the sizes did not grow.
59
+
60
+ Two course-side conditions make the build throw, both with an explicit message:
61
+
62
+ - **A `patches/katex@*.patch` file that already applies a negative
63
+ `\delimitershortfall`.** Delete the patch and its `pnpm.patchedDependencies`
64
+ entry; the engine does this in code now, and a patch is project-local anyway
65
+ so it never reached a consumer.
66
+ - **A `katex` whose bundle no longer holds the constant where the engine looks
67
+ for it.** The rewrite targets one line in KaTeX's ESM bundle, so keep `katex`
68
+ on the declared peer version (`0.16.47`). An upgrade that moves or reformats
69
+ that line fails the build asking you to re-derive the change against
70
+ [`../katex-bracket-scaling.md`](../katex-bracket-scaling.md), which is
71
+ shipped in the package. The failure is loud and immediate, not a silent
72
+ return to stock spacing.
73
+
74
+ ### 2. Diagnostic renamed: `delimiter-size-hierarchy` → `delimiter-manual-size`
75
+
76
+ The old code string no longer exists. Update every place that names it:
77
+
78
+ - `lint-ignore.yml` — a stale line is inert and silently stops suppressing.
79
+ - Course scripts, tests, and type narrowings over the diagnostic union;
80
+ `astro check` flags a comparison against a non-member.
81
+
82
+ Its payload changed shape too. A consumer reading the fields, rather than just
83
+ the `code` and `message`, needs this edit:
84
+
85
+ ```diff
86
+ -// outerSize / nestedSize described the mismatch between two levels
87
+ -const found = diagnostic.outerSize;
88
+ -const suggested = diagnostic.suggestion?.size;
89
+ +// `size` is the hand-picked size found; `suggestion` is always present and is
90
+ +// always a \left/\right pair, so it no longer carries a `size` of its own.
91
+ +const found = diagnostic.size;
92
+ +const suggested = diagnostic.suggestion.latex;
93
+ ```
94
+
95
+ `DelimiterSizeSuggestion` keeps `open`, `close`, and `latex`, and drops `size`.
96
+ `DelimiterSizeDiagnostic` drops `outerSize` and `nestedSize`, gains a required
97
+ `size`, and `suggestion` is no longer optional.
98
+
99
+ ### 3. Content: hand-sized nests now warn
100
+
101
+ Math that was N17-compliant under 0.7.0 reports one `delimiter-manual-size`
102
+ finding per hand-sized level. This is a Tier-3 warning — it does not fail a
103
+ build — so it can be worked through at your own pace, but the findings are real
104
+ and the repair improves the rendering.
105
+
106
+ Find them with:
107
+
108
+ ```
109
+ pnpm content warnings --json
110
+ ```
111
+
112
+ The repair is mechanical: replace the hand-picked pair with `\left`/`\right`,
113
+ keeping the delimiter shapes.
114
+
115
+ ```diff
116
+ -\Biggl[a+\biggl(b+\Bigl[c+\left(d+e\right)\Bigr]\biggr)\Biggr]
117
+ +\left[a+\left(b+\left[c+\left(d+e\right)\right]\right)\right]
118
+ ```
119
+
120
+ A **lone** hand-sized pair that is not part of a nest — `\bigl(x,\ y\bigr)`
121
+ around a tuple, for instance — is a single-level typographic choice with no
122
+ hierarchy to get wrong, and is deliberately left alone. Do not convert those.
123
+
124
+ Expect a small visual change everywhere a nest is rendered: each level is now
125
+ strictly larger than the one inside it, so outer delimiters grow by roughly one
126
+ size step. A course holding screenshot or bounding-box baselines over math
127
+ should re-record them.
128
+
129
+ ### 4. Manifest schema is now v16: a math diagnostic can name its equation
130
+
131
+ `MANIFEST_SCHEMA_VERSION` is 16 (v15 → v16). The bump is **additive** — nothing
132
+ existing changed shape — so the only required edit is to a client that asserts
133
+ the number by exact equality, which now expects 16.
134
+
135
+ What was added: a finding the math gate reports from a `$$…$$` block carrying a
136
+ `\label{eq:…}` gains an optional `equation` field, `{ key, href }`. `href` is
137
+ the rendered page's anchor for that equation
138
+ (`/l1-ar/ar1-basics/#eq-ar1-variance`). `CliDiagnostic` carries the same
139
+ optional field, so a course's own `scripts/content.ts` picks it up with no
140
+ change.
141
+
142
+ `diagnosticLine()` now appends it, so console output, the run log, and
143
+ `pnpm content diagnostics` read:
144
+
145
+ ```
146
+ [code] file:line:column: message (eq:<key> -> <href>)
147
+ ```
148
+
149
+ A course script that parses that line with an anchored regex must tolerate the
150
+ optional trailing ` (eq:… -> …)`. One that just prints the string needs nothing.
151
+
152
+ ### 5. Diagnostic line numbers are corrected — re-record anything that stored them
153
+
154
+ Every gate scans the lesson body, which Astro hands over with the frontmatter
155
+ already stripped, but findings are reported against the lesson *file* — so every
156
+ `file:line:column` the engine has emitted for a lesson was short by the height
157
+ of that lesson's frontmatter. `gate-math` (every `delimiter-manual-size`
158
+ column), `gate-prose-notation` (rule N9), `gate-abbreviations` (rule N14), and
159
+ `gate-heading-length` (S1) are all corrected at once.
160
+
161
+ Nothing to edit in content, but line numbers move, so:
162
+
163
+ - Re-check any suppression, snapshot, or problem matcher keyed to a line number.
164
+ Line numbers were wrong before; they now point at the real line.
165
+ - `pnpm validate:content` (twice) regenerates `build/resolution/*` with the
166
+ corrected positions. Commit the result.
167
+
168
+ ### 6. Malformed `\explain` / `\slot` keys now fail the build
169
+
170
+ Previously a key that did not match the notation key pattern —
171
+ `\explain{Not A Key}{\Omega}` — was not a KaTeX parse error: KaTeX rendered the
172
+ command name in its error color and carried on, so the page shipped red
173
+ `\htmlData` text with a green build. `renderNotationMath` now checks both marker
174
+ macros before KaTeX runs and throws, naming the offending key.
175
+
176
+ **This can newly fail a build that passed on 0.7.0.** If it does, the page was
177
+ already broken for readers. Fix the key to match the notation key pattern
178
+ (lower-case, hyphen-separated) and make sure it resolves to a real entry.
179
+ `renderNotationFormula` still degrades to inert-but-correct math rather than
180
+ costing a reader the equation.
181
+
182
+ ### 7. Glossary entry heading markup: the anchor is now a sibling of the `<h2>`
183
+
184
+ The per-entry anchor link moved out of the `<h2>` and into a new
185
+ `.glossary-heading` flex row wrapping both — the same shape
186
+ `rehype-heading-anchors` gives a lesson heading. This restores the heading's
187
+ accessible name (a screen reader was announcing "Velocity Link to Velocity").
188
+
189
+ Nothing visual changed, but update course-owned CSS and end-to-end tests that
190
+ assumed the nesting:
191
+
192
+ ```diff
193
+ -h2:hover .glossary-anchor,
194
+ -h2:focus-within .glossary-anchor {
195
+ +.glossary-heading:hover .glossary-anchor,
196
+ +.glossary-heading:focus-within .glossary-anchor {
197
+ ```
198
+
199
+ A test locating the anchor as a descendant of the heading must target
200
+ `.glossary-heading .glossary-anchor` instead. A check for the heading *by
201
+ accessible name* now matches the term alone, which is the point of the fix — if
202
+ such a test was written against the broken name ("Velocity Link to Velocity"),
203
+ it needs the term.
204
+
205
+ ### 8. The About panel publishes its versions as data attributes
206
+
207
+ Additive; no course edit is required. `details[data-about-panel]` now carries
208
+ `data-knowledge-version`, `data-knowledge-date` (omitted when the content
209
+ changelog has no date), and `data-engine-version`, and the rendered version line
210
+ is marked `data-knowledge-line`.
211
+
212
+ This exists so a build check does not have to match prose. A course asserting
213
+ the panel's contents by text is pinned to one version string and turns every
214
+ routine bump into a test edit — this repo's own `about.spec.ts` was asserting
215
+ `0.4.0` against a playground long since past it. Prefer:
216
+
217
+ ```diff
218
+ -await expect(panel).toContainText('0.6.0');
219
+ +await expect(panel).toHaveAttribute('data-knowledge-version', knowledgeVersion);
220
+ ```
221
+
222
+ reading the expected value from `content/course.config.ts` rather than hard-coding
223
+ it. A bare version substring is not scoped to the panel either — the engine
224
+ version printed one row below can satisfy it.
225
+
226
+ ### 9. Fixes that need no action
227
+
228
+ These remove build failures; they never require a content edit. Formulas you
229
+ previously worked around may now be written directly.
230
+
231
+ - **A notation `formula` can use a manual delimiter size, an accent, a box, or
232
+ a color.** `render-notation-math.ts`'s attribute allowlist is now KaTeX's
233
+ whole inert presentation surface, so `\Bigl`/`\biggl`/`\Biggl`,
234
+ `\xrightarrow`, `\boxed`, and `\colorbox` no longer throw
235
+ `Unexpected KaTeX property`. `alt`, `href`, `src`, and `title` are explicitly
236
+ denied; no attribute that can carry a URL is allowed through.
237
+ - **`\vec` and every other accent renders.** The style sanitizer no longer
238
+ requires a trailing `;` on the last CSS declaration. `\mathbf` remains the
239
+ house style for a vector; `\vec` simply works now.
240
+ - **A macro in a formula no longer invents a source span.** `\vdots`,
241
+ `\varSigma`, `\argmax`, `\bmod`, `\R`/`\N`/`\Z` and 79 of KaTeX's 282 macros
242
+ produce nodes whose `loc` indexes the macro body, which the glyph resolver
243
+ read as a span over unrelated text — failing with `Unresolved notation ""`,
244
+ or worse, silently splitting a formula mid-command. Fixed. An existing
245
+ explicit `\explain{key}{\varSigma}` annotation still works and is still the
246
+ way to bind a macro-expanded glyph, since it has no authored span for
247
+ canonical matching to see.
248
+ - **`equationAnchorId({ key })` type-checks without a `number`.** Its JSDoc
249
+ required a property the function ignores when a `key` is given.
250
+
251
+ ### 10. Verify
252
+
253
+ ```
254
+ pnpm add -D explico@0.8.0
255
+ pnpm validate:content # twice — the first run regenerates build/resolution/*
256
+ pnpm content check --changed
257
+ pnpm content warnings # expect new delimiter-manual-size findings (item 3)
258
+ astro check
259
+ astro build
260
+ ```
261
+
262
+ Commit the regenerated `build/resolution/*` (items 4 and 5 both change it).
263
+
264
+ ## 0.6.0 → 0.7.0
265
+
266
+ ### 1. Manifest schema is now v15: diagnostic severity has three levels
267
+
268
+ `MANIFEST_SCHEMA_VERSION` is 15 (v14 → v15). Public diagnostic severity unions
269
+ now include `info`, so a client that treated "not an error" as "a warning"
270
+ needs three edits:
271
+
272
+ 1. A client asserting `MANIFEST_SCHEMA_VERSION` by exact equality expects 15.
273
+ 2. Widen any hand-written `'error' | 'warning'` type over manifest diagnostics
274
+ to `'error' | 'warning' | 'info'`. `ConsistencyDiagnostic.severity` is now
275
+ `'warning' | 'info'`.
276
+ 3. In `scripts/compile-manifest.ts` (and any other consumer), filter on
277
+ `severity === 'warning'` before printing or counting warnings. A script that
278
+ counts all of `manifest.diagnostics.consistency` as "Tier-3 findings" now
279
+ over-reports:
280
+
281
+ ```diff
282
+ -const consistencyByCode = consistencyCounts(consistency);
283
+ -const consistencyTotal = consistency.length;
284
+ +const consistencyWarnings = consistency.filter(
285
+ + (diagnostic) => diagnostic.severity === 'warning',
286
+ +);
287
+ +const consistencyByCode = consistencyCounts(consistencyWarnings);
288
+ +const consistencyTotal = consistencyWarnings.length;
289
+ ```
290
+
291
+ These codes moved from `warning` to `info`: `eq-key-unused`, every keyed-float
292
+ `*-key-unused`, `unused-definition`, `weak-local`, `gloss-wants-promoting`, and
293
+ `card-wants-demoting`. Their code strings and container fields did not change,
294
+ the `lint-ignore.yml` lines that already worked for `weak-local`,
295
+ `gloss-wants-promoting`, and `card-wants-demoting` still match, and nothing
296
+ blocks the build. They no longer appear in `pnpm content warnings`; read them
297
+ with `pnpm content diagnostics info`.
298
+
299
+ Run `pnpm validate:content` twice. The committed `build/resolution/*` files
300
+ (including `consistency-report.json`) change because those findings now carry
301
+ `info` severity and `numerals-tagged` findings are gone (item 2); commit them.
302
+
303
+ ### 2. Retired diagnostic: `numerals-tagged`
304
+
305
+ `ConsistencyCode` no longer contains `numerals-tagged`, and the engine never
306
+ emits it. Delete every `numerals-tagged` line from `lint-ignore.yml` (a stale
307
+ line is inert but misleads the next reader) and every course script, test, or
308
+ type narrowing that names the code — `tsc`/`astro check` flags the latter as a
309
+ comparison against a non-member of the union.
310
+
311
+ ### 3. `explicoContentRefresh()` fails the build on a missing remark plugin (rule N14 and its siblings)
312
+
313
+ This is the enforcement half of rule N14's Astro wiring (see item 4 of the
314
+ 0.4.x → 0.5.0 section). Its `astro:config:setup` hook now compares the manifest
315
+ against the configured remark plugins and throws, naming each gap, when a
316
+ non-empty table has no matching plugin registered:
317
+
318
+ | Manifest table is non-empty | Plugin that must be registered |
319
+ | --- | --- |
320
+ | `manifest.abbreviations` | `[remarkAbbreviations, { abbreviations: manifest.abbreviations }]` from `explico/reference/remark-abbreviations.mjs` |
321
+ | `manifest.sources` | `[remarkCitation, { sources: manifest.sources }]` from `explico/reference/remark-citation.mjs` |
322
+ | `manifest.notation.raw` | `[remarkNotation, createRemarkNotationOptions(manifest)]` from `explico/reference/remark-notation.mjs` |
323
+
324
+ Check `astro.config.mjs` against that table. The check reads both wiring
325
+ surfaces Astro offers — the flat `markdown.remarkPlugins` array and a
326
+ `markdown.processor` such as `unified({ remarkPlugins: [...] })` — and accepts
327
+ either the bare plugin or a `[plugin, options]` tuple. It compares by identity
328
+ with the module the `explico/reference/*.mjs` specifier resolves to, so register
329
+ the imported function itself rather than a local wrapper around it. A course
330
+ that was already fully wired needs no edit; one that skipped a plugin was
331
+ silently rendering triggers as plain text and now fails `astro build` /
332
+ `astro dev` until it adds the missing registration.
333
+
334
+ ### 4. Removed markup: popup "Copy LaTeX", glossary `#` copy-link, formula scroll region
335
+
336
+ None of these has a replacement selector; update course-owned CSS and
337
+ end-to-end tests that target them:
338
+
339
+ - `.notation-popup-copy` / `[data-popup-copy]` — the button in a notation
340
+ popup. Copying is now a click on the equation itself (item 8).
341
+ - `[data-glossary-anchor]` and its "Link copied" flash — a glossary entry's `#`
342
+ button that wrote its URL to the clipboard. The entry title is now a link to
343
+ `#notation-<key>` (`.glossary-term-link`), and a hover-revealed link-icon
344
+ anchor (`.glossary-anchor`) simply navigates; nothing is copied.
345
+ - `NotationFormula`'s `tabindex="0"` scroll region moved from
346
+ `.notation-formula-math` to a nested `.notation-formula-scroll`. A test that
347
+ focuses or scrolls `.notation-formula-math` must target the inner element.
348
+
349
+ No source or config change is needed beyond selectors.
350
+
351
+ ### 5. Rule N14: every occurrence of a catalogued abbreviation is now wrapped
352
+
353
+ Two changes ride under rule N14, neither of which needs a config edit:
354
+
355
+ - **Rendered output changed.** `remarkAbbreviations` used to wrap only the first
356
+ occurrence of each catalogued abbreviation per page; it now wraps every
357
+ occurrence (`a.abbreviation-term[data-abbr-key]`). A course-owned e2e test or
358
+ snapshot that expected exactly one such link per term per page, or CSS that
359
+ assumed a page's later occurrences were plain text, must be updated.
360
+ `AbbreviationLayer.astro` already resolved markers independently by
361
+ `data-abbr-key`, so no island change is needed.
362
+ - **The shipped baseline grew** (`base-abbreviations.mjs`): `AM`, `PM`, `A.M.`,
363
+ `P.M.`, and `e.t.c.` join `a.m.`, `p.m.`, `e.g.`, `i.e.`, and `etc.`. This only
364
+ loosens the gate. A now-redundant `abbreviations.allow` line or
365
+ `content/abbreviations/*.md` entry for one of them is harmless and can stay; a
366
+ course that wants one explained anyway lists it in `abbreviations.deny`.
367
+
368
+ ### 6. Rule N16 and rule N17: delimiter conventions and sizes
369
+
370
+ Rule N16 (alternate `()`, `[]`, `\{\}` for purely stylistic nested grouping,
371
+ never for conventional delimiters such as `f(x)`, `\mathbb{E}[X]`, or
372
+ intervals) is **review-only**: nothing checks it and nothing needs to change to
373
+ keep building. Rule N17 (every outer scalable pair in a nest grows strictly
374
+ larger than the pairs inside it: `\left…\right` → `\Bigl…\Bigr` → `\biggl…\biggr`
375
+ → `\Biggl…\Biggr`) is a **Tier-3 warning**, `delimiter-size-hierarchy`, reported
376
+ at the outer pair's line and column by the normal math gate for lesson math,
377
+ notation formulas, and assessment math. It never fails a build, so no course
378
+ needs to change to keep building. To clear the warnings, run
379
+ `pnpm content diagnostics warning`, and enlarge each named outer pair to the
380
+ next manual size without changing its delimiter shape. Rule N15's build-enforced
381
+ behavior is unchanged — its documentation now also accepts an N17 manual pair as
382
+ "sized". See [`notation.md#n16`](notation.md#n16) and
383
+ [`notation.md#n17`](notation.md#n17).
384
+
385
+ ### 7. Rule P1: "or" never introduces a second name for the same thing
386
+
387
+ Rule P1 opens the new `P` (prose) rule area and is **review-only**: nothing is
388
+ checked at build time and no course change is required. When editing lessons,
389
+ replace "the residual or error term" with "the residual (i.e., the error
390
+ term)"-style wording. See [`prose.md#p1`](prose.md#p1).
391
+
392
+ ### 8. Optional: click-to-copy equations, glossary equation numbers, and live lab LaTeX
393
+
394
+ Nothing to do for a course that wires `Footer` to `LessonFooter.astro`: a
395
+ click on any `[data-notation-formula]` box now copies its `data-latex`, and every
396
+ glossary formula gets a `G.<n>` number and copy button, automatically.
397
+
398
+ Only a course with an interactive lab island that renders math through
399
+ `renderLabMath()` can opt that equation into the gesture, and keep the copied
400
+ LaTeX in step with the sliders, with `plainLatexTemplate` and the new
401
+ `slotPlaceholder` export:
402
+
403
+ ```tsx
404
+ import {
405
+ slotPlaceholder,
406
+ type RenderedLabMath,
407
+ } from 'explico/reference/render-lab-math';
408
+
409
+ // On the math root element:
410
+ <div
411
+ className="lab-math"
412
+ data-notation-formula
413
+ data-latex={rendered.plainLatexTemplate}
414
+ /* … */
415
+ />
416
+
417
+ // Where the island writes each slot's live value (`values`: slot name → text):
418
+ let latex = rendered.plainLatexTemplate;
419
+ for (const [name, value] of Object.entries(values)) {
420
+ latex = latex.split(slotPlaceholder(name)).join(value);
421
+ }
422
+ root.dataset.latex = latex;
423
+ ```
424
+
425
+ Rebuild `latex` from the template on every update, not from the previous result.
426
+ A lab that does not set these attributes is unchanged. `RenderedLabMath` gained
427
+ the required `plainLatexTemplate` field; a course that constructs one by hand
428
+ (a test double, for instance) must add it.
429
+
430
+ ### 9. Only if you override `Footer` yourself: mount `AnchorScrollEnhancer`
431
+
432
+ The new smooth-scroll behavior for in-page anchor links ships in
433
+ `AnchorScrollEnhancer.astro`, which `LessonFooter.astro` mounts. A course whose
434
+ Starlight `components.Footer` is `explico/components/starlight/LessonFooter.astro`
435
+ gets it with no change. A course that swapped in its own footer component must
436
+ render it (`import AnchorScrollEnhancer from
437
+ 'explico/components/starlight/AnchorScrollEnhancer.astro'`, then
438
+ `<AnchorScrollEnhancer />`) or its anchor links keep the browser's native jump.
439
+
440
+ ### 10. Optional: wire `rehypeBaseLinks` for a course deployed under a subpath
441
+
442
+ Skip this for a course served from a domain root (`base` omitted or `'/'`); the
443
+ plugin is a no-op there. For a course under a subpath (a GitHub Pages *project*
444
+ site, `base: '/my-course'`), Starlight prefixes only its own navigation, so
445
+ every root-relative link the Markdown pipeline produces — prose links,
446
+ equation cross-references, citation links — 404s without this:
447
+
448
+ ```diff
449
+ + import rehypeBaseLinks from 'explico/reference/rehype-base-links.mjs';
450
+ …
451
+ rehypePlugins: [
452
+ [rehypeKatex, createNotationKatexOptions()],
453
+ rehypeFailKatexErrors,
454
+ + [rehypeBaseLinks, base],
455
+ ]
456
+ ```
457
+
458
+ `base` is the same value passed to `defineConfig({ base })`. **Undo any manual
459
+ workaround first:** the plugin prefixes every `href` beginning with a single
460
+ `/`, so a lesson link already hand-written as `/my-course/other/` would become
461
+ `/my-course/my-course/other/`. Write lesson links without the base. A lesson's
462
+ Starlight frontmatter (`hero.actions[].link`) never passes through this
463
+ pipeline; write those as paths relative to the current page.
464
+
465
+ ### 11. Optional: run logs for `pnpm validate:content`
466
+
467
+ Route the course's `scripts/compile-manifest.ts` through `createRunLog()` to
468
+ record every diagnostic to `build/logs/<timestamp>/{error,warning,info}.log`
469
+ (Python `logging` format, `build/logs/latest` symlinks the newest run):
470
+
471
+ ```diff
472
+ + import { createRunLog } from 'explico/compiler/logging';
473
+ …
474
+ + const log = createRunLog();
475
+ const manifest = await compileManifest(courseConfig);
476
+ …
477
+ - console.warn(`[${warning.code}] ${warning.message}`);
478
+ + console.warn(log.warning('notation', `[${warning.code}] ${warning.message}`));
479
+ …
480
+ + const summary = log.close();
481
+ + console.log(
482
+ + `Run log: ${summary.relativeDirectory} ` +
483
+ + `(${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s), ${summary.counts.INFO} info).`,
484
+ + );
485
+ ```
486
+
487
+ Each `log.error/warning/info(source, message)` call returns its message, so
488
+ printing and recording share one expression; `log.infoDiagnostics(manifest.diagnostics)`
489
+ records the severity-`info` findings the console never showed. Call
490
+ `log.close()` before any `throw` so a failing run's log is complete
491
+ (`playground/scripts/compile-manifest.ts` is a full worked example). Add
492
+ `build/logs/` to the course's `.gitignore` — a course commits
493
+ `build/resolution/*`, and nothing prunes old runs.
494
+
495
+ ### 12. Optional: the `content diagnostics` / `content warnings` commands and VS Code tasks
496
+
497
+ `content diagnostics [info|warning|error] [--json]` and `content warnings
498
+ [--json]` are new, and `content status` now reports an `info` count
499
+ (`report.diagnostics.infos`). The engine implements them in
500
+ `explico/compiler/cli`, but a course's own `scripts/content.ts` dispatches
501
+ commands, so it must route them:
502
+
503
+ ```diff
504
+ import {
505
+ ContentCliError,
506
+ contentCheck,
507
+ contentContext,
508
+ + contentDiagnostics,
509
+ contentStatus,
510
+ + contentWarnings,
511
+ …
512
+ } from 'explico/compiler/cli';
513
+ ```
514
+
515
+ Add a `case 'warnings'` (`contentWarnings(manifest)`) and a `case 'diagnostics'`
516
+ (validate the one positional as `info`/`warning`/`error`, default `info`, then
517
+ `contentDiagnostics(manifest, severity)`) beside `case 'status'`, printing
518
+ `file:line:column: severity code: message` lines — copy
519
+ `playground/scripts/content.ts`. To surface findings in VS Code's Problems
520
+ panel, copy the three **Explico: Show content diagnostics** tasks from the
521
+ engine repository's `.vscode/tasks.json`, replacing the `--filter` package name
522
+ and the problem matcher's `fileLocation` root with the course's own.
523
+
524
+ ### 13. No action: other changes in this release
525
+
526
+ - Every `<a>` back-link (`↩`) in a page's References section now also carries a
527
+ `title` tooltip; `remark-citation.mjs` does this itself.
528
+ - Standalone HTML export now inlines a `<link>` only when its `rel` is
529
+ `stylesheet` or an icon type, and leaves other `<link>`s (`canonical`,
530
+ `alternate`, `preload`, …) as authored.
531
+
532
+ ### Verification
533
+
534
+ `pnpm validate:content` (run it **twice** — the first run regenerates
535
+ `build/resolution/*`, item 1), then `pnpm content check --changed`, then
536
+ `astro check`, then `astro build`. `astro build` is the step that reports item 3.
537
+
538
+ ---
539
+
36
540
  ## 0.5.0 → 0.6.0
37
541
 
38
542
  ### 1. Rule S1: no heading or float title runs past 65 characters
@@ -215,6 +719,16 @@ abbreviation needs its remark plugin wired in too:
215
719
  ]
216
720
  ```
217
721
 
722
+ Skipping this second half is now **build-enforced**, not just documented:
723
+ `explicoContentRefresh()`'s `astro:config:setup` hook checks
724
+ `markdown.remarkPlugins` against the manifest and throws if a catalogued
725
+ manifest table (`manifest.abbreviations`, `manifest.sources`, or
726
+ `manifest.notation.raw`) has no matching plugin (`remarkAbbreviations`,
727
+ `remarkCitation`, `remarkNotation` respectively) registered — so a
728
+ half-completed migration like this one fails `astro build`/`astro dev`
729
+ instead of silently rendering plain text with `pnpm validate:content` still
730
+ green.
731
+
218
732
  ### 5. Optional: rule N10 warns on an unexplained `\mathrm{…}` label
219
733
 
220
734
  A `\mathrm{…}` multi-letter label (`\mathrm{KE}`, `\mathrm{PV}`) not on the