explico 0.7.0 → 0.11.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 (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -33,6 +33,1328 @@ required config block throws with the section of this file to read.
33
33
 
34
34
  ---
35
35
 
36
+ ## 0.10.0 → 0.11.0
37
+
38
+ **Course changes are required.** Figures and diagrams no longer compact.
39
+ `disclosure.diagrams`, `<Diagram defaultOpen>` and `<Diagram rememberState>`
40
+ are removed, and each one fails the build until you delete it (steps 2–3).
41
+ The rest is additive or affects only readers: `<Fold>`, a lab's `tools`
42
+ slot and live caption end, the short caption shown in the caption line (rule
43
+ S9, a guide), the "Figures and tables" page, unnumbered `[card=meta]`
44
+ sections, and explanation cards that pin, dock, drag and are remembered.
45
+ Nothing in `astro.config.mjs` changes. The manifest schema moves from v23 to
46
+ v24.
47
+
48
+ **Coming from 0.8.0, the last version on npm before this one.** 0.9.0 and
49
+ 0.10.0 were tagged but never published to npm, so a course that installs
50
+ from npm goes from 0.8.0 straight to 0.11.0. Install 0.11.0 once (step 1).
51
+ Then apply the course changes of "0.8.0 → 0.9.0" and "0.9.0 → 0.10.0" below,
52
+ oldest first, and skip their "Update the dependency" steps. Finish with this
53
+ section. Three steps of the 0.10.0 section read differently at 0.11.0. Apply
54
+ them in this form:
55
+
56
+ - Its step 3 puts `diagrams: { defaultOpen: true }` in `disclosure`, and its
57
+ step 4 adds `diagrams` to the settings schema. Leave `diagrams` out: it now
58
+ fails the build (step 2 below). `disclosure.figures` now sets whether a
59
+ lab's tools start open, not whether a figure starts compacted.
60
+ - Its step 14 describes the compaction markup of a figure and a diagram
61
+ (`.float-caption__toggle`, "a compacted float keeps the caption line").
62
+ That markup is gone (step 9 below).
63
+ - Its step 5 lists the diagnostic groups for `compile-manifest.ts`. Add
64
+ `folds` to them as well (step 4 below).
65
+
66
+ Work in this order: steps 1–5 fix the wiring, so that `pnpm validate:content`
67
+ runs; steps 6–8 are content, and steps 7–8 are conditional; steps 9–11 apply
68
+ only to a course whose own CSS, tests, code or stored keys reach into the
69
+ engine; step 12 is optional; step 13 records the content changes; step 15 is
70
+ the check.
71
+
72
+ ### 1. Update the dependency
73
+
74
+ ```bash
75
+ pnpm add -D explico@0.11.0
76
+ ```
77
+
78
+ ### 2. Delete `disclosure.diagrams` (the build fails until you do)
79
+
80
+ A diagram no longer folds by itself, so it has no default. `normalizeDisclosure`
81
+ throws on the key and names the edit to make, which stops both
82
+ `pnpm validate:content` and `astro build`. Delete `diagrams` from the
83
+ `disclosure` block of `content/course.settings.json`, or from
84
+ `CourseConfig.disclosure` in `content/course.config.ts`, wherever the course
85
+ keeps it.
86
+
87
+ `disclosure.figures` stays, but it now means something else: whether a
88
+ **lab's tools** start open (the `tools` slot, step 12). A figure's picture
89
+ and caption always show. A course that set `figures: { defaultOpen: false }`
90
+ to start its figures compacted now starts every lab's tools folded instead.
91
+ Delete the setting unless that is what you want. To let a reader fold a
92
+ figure away, put it in a `<Fold>` (step 12) or a collapsible section.
93
+
94
+ New and optional: `disclosure.folds: { defaultOpen }`, whether a `<Fold>`
95
+ starts open. The default is closed.
96
+
97
+ **Only if the course describes its settings in a JSON Schema** (the
98
+ playground's `course.settings.schema.json`, where `disclosure` has
99
+ `additionalProperties: false`), remove `diagrams`, add `folds` and
100
+ re-describe `figures`:
101
+
102
+ ```json
103
+ "folds": {
104
+ "title": "Folds",
105
+ "description": "A `<Fold>` folds like a memo, with no marker, letter or frame: a stretch of content — most often a figure or diagram — a reader can fold away. Default: closed.",
106
+ "$ref": "#/definitions/initialState"
107
+ },
108
+ "figures": {
109
+ "title": "A lab's tools",
110
+ "description": "Whether a lab's tools (the sliders, fields and live equation after a `<Figure>`'s caption) start open. The figure's picture and caption always show. Default: open.",
111
+ "$ref": "#/definitions/initialState"
112
+ }
113
+ ```
114
+
115
+ The `rules` and `courseRules` properties `$ref` the schemas in
116
+ `node_modules/explico/dist/rules/`, so rule S9 (step 6) needs no edit there.
117
+
118
+ ### 3. Delete `defaultOpen` / `rememberState` from every `<Diagram>`
119
+
120
+ ```bash
121
+ rg -n -U '<Diagram\b[^>]*\b(defaultOpen|rememberState)\b' content src
122
+ ```
123
+
124
+ Both props are removed. In a lesson, each one fails `pnpm validate:content`
125
+ (`dia-removed-prop`). A `<Diagram>` rendered by a wrapper component throws
126
+ when its page builds, and the manifest scan cannot see inside the wrapper.
127
+ Check every wrapper `.astro` file, including one that passes its own props
128
+ through to `<Diagram>`. To let a reader fold a diagram away, put it in a
129
+ `<Fold>` (step 12) or a collapsible section.
130
+
131
+ `<Figure defaultOpen>` and `<Figure rememberState>` stay. They now set that
132
+ figure's lab tools (step 12), and they do nothing on a figure without a
133
+ `tools` slot.
134
+
135
+ ### 4. `scripts/compile-manifest.ts`: add the `folds` group
136
+
137
+ The manifest has a new diagnostics group, `diagnostics.folds`
138
+ (`fold-unnamed`, step 12). Add it to both the warnings loop and the errors
139
+ loop, next to `examples`:
140
+
141
+ ```ts
142
+ const { /* … */ examples, folds, knowledge, rawFloats, config, waivers } =
143
+ manifest.diagnostics;
144
+
145
+ // In BOTH loops over [['equations', equations], …]:
146
+ ['examples', examples],
147
+ ['folds', folds],
148
+ ['rawFloats', rawFloats],
149
+ ```
150
+
151
+ Without it, a `<Fold>` with nothing to name its bar does not fail
152
+ `validate:content`, though the page build still fails on it. `content check`,
153
+ `content diagnostics` and `content status` read the group already.
154
+
155
+ ### 5. The manifest schema is now v24
156
+
157
+ It was v23 in 0.10.0. `pnpm validate:content` rewrites `build/manifest.json`,
158
+ so this step matters only for code that reads the manifest or pins its
159
+ version:
160
+
161
+ - **v24**: `disclosure` loses `diagrams` and gains `folds`, and
162
+ `diagnostics.folds` is new. When the course has a top-level
163
+ `content/docs/figures-and-tables.mdx`, the Reference group in `sidebar` also
164
+ lists "Figures and tables". The item has the same shape as the others.
165
+
166
+ ### 6. Captions: rule S9, and the short caption that now shows
167
+
168
+ A float's `short` caption now opens its caption line as a bold title:
169
+ "**Sampled flight data.** Position, velocity, …". It also names the float in
170
+ a folded section's "Hidden here" line, in a copied link, and on the "Figures
171
+ and tables" page. Nothing fails, but two kinds of float now read badly:
172
+
173
+ 1. **A long `caption` that restates the `short` one reads twice**, for
174
+ example "**Sampled flight data.** Sampled flight data: position, …". Rule
175
+ S9 (`structure-rules.md#s9`) says the long caption continues from the
176
+ short caption's name. It says what the float shows — its subject, its
177
+ marks and symbols, the case shown — and at most the one thing to notice.
178
+ 2. **A figure, table or diagram without `short`** has no title, and the
179
+ "Figures and tables" page lists it by its long caption, greyed out. Rule
180
+ S9 and `components.md` now ask for a `short` on every one. Find them:
181
+
182
+ ```bash
183
+ rg -n -U --pcre2 '<(Figure|Table|Diagram)\b(?![^>]*\bshort=)[^>]*>' content/docs
184
+ ```
185
+
186
+ Rule S9 also moves text out of figures. A figure holds no running text. The
187
+ argument drawn from a figure is lesson text after it, and a lab's
188
+ instructions are the first line of its tools (step 12). Rule S9 is a guide:
189
+ review-only, on `build/rules-in-effect.md` and `pnpm content rules`. Switch it
190
+ off with `rules.S9.enabled: false` if it does not fit the course.
191
+
192
+ ### 7. Only if a lesson has a `[card=meta]` section: section numbers change
193
+
194
+ ```bash
195
+ rg -n '^#{2,6} .*\[card=meta\]' content/docs
196
+ ```
197
+
198
+ A `[card=meta]` section is now unnumbered, like References, and so is every
199
+ heading inside it. The sections after it no longer count it: in a chapter
200
+ that opens with one, § 1.2.2 becomes § 1.2.1. Float, check and example
201
+ numbers do not change.
202
+
203
+ - `[[sec-…]]` references print from the manifest and need no edit. A
204
+ reference to the meta section itself now prints its title in quotes
205
+ ("What this chapter uses"), since the section has no number.
206
+ - Prose, captions and tests that type a section number, and number-form
207
+ fragments (`#sec-1.2.2`) past the meta section, now point at a different
208
+ section. Cite the section by key, or update the number.
209
+ - If the section should keep its number, drop the `=meta` (a plain `[card]`
210
+ accent card is numbered) or make it an aside (`:::note[Title]`).
211
+
212
+ Record the renumbering in `content/CHANGELOG.md` (step 13), because readers
213
+ see new numbers.
214
+
215
+ ### 8. Only if the course has its own `Fold` component or a `figures-and-tables` page
216
+
217
+ ```bash
218
+ rg -n '<Fold\b|import Fold\b' content src
219
+ ls content/docs/figures-and-tables.* 2>/dev/null
220
+ ```
221
+
222
+ - **`<Fold>` is now an engine name.** A hit that imports
223
+ `explico/components/fold/Fold.astro` is the engine's own component; any
224
+ other `Fold` is the course's. The manifest reads every `<Fold>` in a
225
+ lesson as the engine's, whatever it is imported from. The equations and
226
+ floats inside it are numbered in the lesson's sequence, and a `<Fold>` with
227
+ no `label` and no numbered float inside fails `fold-unnamed`. To keep the
228
+ old behaviour, import the course's own component under another name
229
+ (`import CourseFold from …`, `<CourseFold>`).
230
+ - **A top-level `content/docs/figures-and-tables.mdx`** (or `.md`) now adds
231
+ "Figures and tables" to the sidebar's Reference group. If the course
232
+ already has a page at that path for something else, rename it, or make it
233
+ the float index (step 12).
234
+
235
+ ### 9. Only if your course CSS, browser tests or scripts reach into engine markup
236
+
237
+ ```bash
238
+ rg -n 'float-caption__toggle|keyed-figure__details|diagram__details|float-compact|--float-toggle-|diagram-toolbar|diagram-(expand|shrink)-(button|icon)|data-diagram-(details|body|toolbar)|block-badge__icon|table-overflow-expand-btn|keyed-table__caption--paginated|data-panel-pin|citation-panel-pin|notation-popup-close|[0-9] questions|Link copied|Hidden here' src content tests
239
+ ```
240
+
241
+ - **Figure and diagram.** Neither one compacts. `.float-caption__toggle`,
242
+ `.keyed-figure__details`, `.diagram__details`, `.float-compact`,
243
+ `.float-compact__summary` and the `--float-toggle-size`,
244
+ `--float-toggle-gutter` and `--float-toggle-room` tokens are gone. A
245
+ figure's picture is `.keyed-figure > .keyed-figure__body`. The caption line
246
+ can open with `b.float-caption__title` (the short caption) and end with a
247
+ `.float-tools` row of boxed `.float-tool` buttons. A `<Figure>`'s caption
248
+ line ends with `span.float-caption__state[data-figure-state]`, outside
249
+ `.float-caption__text`. Print shows a figure's picture and caption, never
250
+ a lab's tools, and never a "compacted" note.
251
+ - **A diagram's widen button** is now
252
+ `figcaption .float-tools > [data-diagram-expand]`, and shows only while the
253
+ graph is wider than the column. `.diagram-toolbar`, `.diagram-expand-button`,
254
+ `.diagram-expand-icon` and `.diagram-shrink-icon` are gone.
255
+ - **Table.** The widen button ↔ and the fold button sit together in the
256
+ caption line's `.float-tools`. The fold toggle reads ⌃ / ⌄ (it read ▴ / ▾),
257
+ keeps `.keyed-table__pagination-toggle`, and is no longer a
258
+ `.table-overflow-expand-btn`. `.keyed-table__caption--paginated` is gone.
259
+ The "… N more rows …" label is centred on the part of the table on screen.
260
+ - **Knowledge check and example set.** The icon is `.block-heading__icon`,
261
+ before `.block-heading__title` (it was `.block-badge__icon`, inside the
262
+ badge). The badge (`.block-badge`: the word and `.block-heading__number`)
263
+ sits in the card's top-right corner. A check's score is empty until the
264
+ first answer, then reads "2 of 3 correct" under the questions. The
265
+ "3 questions" line is gone.
266
+ - **Explanation cards** (notation, `[[key]]` terms, citations,
267
+ abbreviations, and equation, table, figure, diagram and section
268
+ references) are all clones of `HoverPanel.astro`'s `<template>`, run by one
269
+ card manager. The header row holds the grip `[data-card-grip]` ("Move
270
+ card"), Side `[data-card-side]`, Pin `[data-card-pin]` (`aria-pressed`) and
271
+ × `[data-card-close]`. A note line `[data-card-note]` sits under it. A
272
+ dragged card has `data-card-mode="moved"`. `[data-panel-pin]`,
273
+ `.citation-panel-pin` and `.notation-popup-close` are gone.
274
+ `.notation-popup[data-popup-key="…"]` still finds a notation card. For
275
+ behaviour tests:
276
+ - A click or tap on a trigger pins its card and never navigates. The
277
+ card's "Go to …" link navigates, and ends in ↑ / ↓ (same page), → (another
278
+ page) or ↗ (a citation's source); every one used to end in ↗. A reference
279
+ whose target is already fully on screen still highlights it and
280
+ navigates.
281
+ - A click inside a card never closes it. A hovered card no longer closes a
282
+ pinned one, and several cards can be open at once. Escape closes the
283
+ newest card first. A `[[key]]` term shows no browser tooltip.
284
+ - A page restores the cards a reader kept on it, from local storage. A
285
+ fresh browser context starts empty, but a test that pins a card and then
286
+ reloads or goes back sees the card again.
287
+ - **Copy link and "Hidden here".** The confirmation names what was copied:
288
+ "Link copied · Figure 2.1.1 · Projectile trajectory explorer", or a
289
+ section's, check's or example set's title. Where the browser has
290
+ `ClipboardItem`, a `text/html` link goes on the clipboard next to the URL. A
291
+ folded section's line names its floats first: "Hidden here: **Figure
292
+ 2.1.1** Projectile trajectory explorer, 2 paragraphs". A heading's chain
293
+ icon has the title "Copy link" where a click copies.
294
+ - **Layout.** The header is 3rem tall at every width (`--sl-nav-height`), with
295
+ a 2rem search box. Check course header content sized for 3.5rem or 4rem.
296
+ Headings are smaller: `global.css` sets `--sl-text-h1`…`--sl-text-h5`, and
297
+ course CSS that sets them wins. The navigation and "On this page" rail
298
+ cover a card that reaches them. Accent and meta cards have less padding, a
299
+ lesson's first block adds no top margin, and Previous / Next stay side by
300
+ side down to a 26rem column. The notation panel's help text is inside the
301
+ panel. Re-record screenshots.
302
+ - **A figure's preview card** shows a still copy of its picture, a lab's
303
+ plot included (controls disabled, ids prefixed). A lab no longer needs
304
+ `previewable={false}`.
305
+
306
+ ### 10. Only if course code imports engine internals
307
+
308
+ - `components/hover-panel.ts` (`createHoverPanel`, `claimActiveSurface`,
309
+ `releaseActiveSurface`), `components/popup-stack.ts` and
310
+ `components/disclosure/float-compaction.ts` are gone. A layer of the
311
+ course's own registers a `CardSource` with `registerCardSource`. An overlay
312
+ that must clear the hovered cards calls `closeTransientCards()`. Both come
313
+ from `explico/components/card-manager.ts`.
314
+ - `countHiddenContent` in `components/starlight/section-summary.ts` is gone.
315
+ Use `collectHiddenContent`, then `hiddenContentParts`.
316
+ - `PreferenceStore` gains an optional `remove(key)`. A course's own store
317
+ still fits the interface. Implement `remove` so that closing a page's last
318
+ card forgets the page.
319
+
320
+ ### 11. Reader-side state
321
+
322
+ A figure's and a diagram's remembered compaction (`figure:<id>`,
323
+ `diagram:<id>`) is no longer read, so a returning reader sees every figure
324
+ and diagram open. The new keys are `figure-tools:<figure id>` (a lab's
325
+ tools), `section:fold-…` (a `<Fold>`), `explico:cards:<page path>` (the cards
326
+ kept on a page) and `explico:layout:keep-article-left`. You need to add them
327
+ only if the course clears preference keys by name.
328
+
329
+ ### 12. Optional: features to adopt
330
+
331
+ - **`<Fold>`** (`explico/components/fold/Fold.astro`) lets a reader fold away
332
+ a stretch of the lesson where it stands, most often one figure, diagram or
333
+ table. It has no heading, number, marker or frame:
334
+
335
+ ```mdx
336
+ import Fold from 'explico/components/fold/Fold.astro';
337
+
338
+ <Fold>
339
+ <Diagram id="launch-angle-dependency" short="Inputs to the launch-angle search" caption="…" />
340
+ </Fold>
341
+ ```
342
+
343
+ The bar is the `label`, or else the numbers and short captions of the
344
+ numbered floats inside. A fold with neither fails `fold-unnamed`. A float
345
+ the bar names drops its own bold caption title inside the fold. The fold's
346
+ first state comes from `defaultOpen`, or else `disclosure.folds` (closed);
347
+ `rememberState={false}` keeps the default on every visit. The equations
348
+ and floats inside keep their numbers.
349
+ - **A lab's tools.** In a `<Figure>`, the default slot is the picture. Give the
350
+ controls island `slot="tools"`, or wrap the controls in
351
+ `<Fragment slot="tools">`. The tools render after the caption line in a
352
+ tray with a fold button. They start as `defaultOpen` says, or else as
353
+ `disclosure.figures` says, and they never print. The picture and the tools
354
+ are separate islands, so they share their values through the course's own
355
+ store (the playground's `content/labs/useStoredState.ts`). `global.css`
356
+ styles `.lab-tools`, `.lab-instruction` (the tools' first line, saying
357
+ what to do) and a `.lab-shell` in the tray. A figure without the slot
358
+ renders as before.
359
+
360
+ ```mdx
361
+ <Figure id="launch-angle-explorer" short="Range vs. launch angle explorer" caption="…">
362
+ <LaunchAnglePlot client:visible />
363
+ <LaunchAngleTools slot="tools" client:visible />
364
+ </Figure>
365
+ ```
366
+
367
+ - **The caption's live end.** `setFigureState(element, runs)` from
368
+ `explico/components/figure/figure-state` writes the case the plot shows
369
+ at the end of the caption line. `element` is any element of the lab's
370
+ inside the figure. `runs` holds strings, `{ text, italic: true }` and
371
+ `{ sub }` items, never HTML:
372
+ `setFigureState(host, ['Shown for ', { text: 'v', italic: true }, { sub: '0' }, ' = 20 m/s'])`.
373
+ The clause prints with the caption.
374
+ - **Drop `previewable={false}`** from labs. The preview card now copies the
375
+ plot as drawn, and falls back to the caption and number when the plot is
376
+ not drawn yet.
377
+ - **The "Figures and tables" page** is LaTeX's `\listoffigures` and
378
+ `\listoftables` in one. Create `content/docs/figures-and-tables.mdx`:
379
+
380
+ ```mdx
381
+ ---
382
+ title: Figures and tables
383
+ description: Every numbered figure, table and diagram of the course, by part and chapter.
384
+ ---
385
+
386
+ import FloatIndex from 'explico/components/FloatIndex.astro';
387
+
388
+ <FloatIndex />
389
+ ```
390
+
391
+ It lists every numbered figure, table and diagram that has an `id`. A
392
+ diagram drawn by a wrapper component is listed when the lesson imports the
393
+ wrapper as `Diagram` and passes it `id`, `short` and `caption`
394
+ (`<Diagram id="…" short="…" caption="…" />`). That is also how the diagram
395
+ gets a number and a `[[dia-…]]` key.
396
+ - **Cards beside the article, dragged cards and remembered cards** need no
397
+ wiring.
398
+
399
+ ### 13. Record the content changes (rules C1–C3)
400
+
401
+ Steps 2, 6, 7 and 12 edit `content/`. A settings edit is a content change
402
+ too. Add bullets under `## [Unreleased]` in `content/CHANGELOG.md` (rule C1):
403
+ the dropped `disclosure.diagrams`, new and rewritten captions, renumbered
404
+ sections, and new folds or pages. Bump `about.version` per rule C2 when you
405
+ release, and keep it equal to the newest heading (rule C3).
406
+
407
+ ### 14. Fixes that need no action
408
+
409
+ A section, memo or fold that the browser opens itself, for find-in-page or
410
+ a linked fragment, now opens properly and stays open. A reference into a
411
+ closed memo or folded section opens its preview card and marks the fold's
412
+ bar. A figure's preview card shows an SVG sized only by its `viewBox`. A
413
+ touch screen no longer draws a badge icon over its word. A worked example's
414
+ closing display equation loses its stray bottom margin.
415
+
416
+ ### 15. Verify
417
+
418
+ ```bash
419
+ pnpm validate:content # run twice: the first run regenerates build/resolution/*
420
+ pnpm content rules # rule S9 is on the list now
421
+ pnpm content warnings
422
+ pnpm content check --changed
423
+ astro check
424
+ astro build
425
+ ```
426
+
427
+ Then run the course's browser tests, if it has any, and update the selectors
428
+ and texts from step 9. A course with its own gate script that runs these
429
+ stages (explico's is `scripts/agent/verify`) runs that instead.
430
+
431
+ ## 0.9.0 → 0.10.0
432
+
433
+ **Course changes are required.** This release carries four lines of work: the
434
+ rule catalog (per-rule settings and switches, waivers, the rules-in-effect
435
+ page, a course's own rules), the rule restructuring (six rules folded into
436
+ four), accents and styled letters as symbols of their own, and titles and
437
+ containers (outline numbering, one caption pair per float, one collapse
438
+ model, heading discipline, kind badges). Several props are removed with no
439
+ alias period, and several rules now fail the build where they did not.
440
+
441
+ Work in this order: steps 1–6 fix the wiring, so that `pnpm validate:content`
442
+ runs and reports; steps 7–13 clear what it reports; steps 14–16 are
443
+ conditional or optional; step 17 is the check. Where a diagnostic code leads
444
+ to a step, the step names it. The manifest schema moves from v16 to v23 in
445
+ seven bumps (step 6).
446
+
447
+ ### 1. Update the dependency
448
+
449
+ ```bash
450
+ pnpm add -D explico@0.10.0
451
+ ```
452
+
453
+ 0.10.0 was never published to npm. Coming from npm's 0.8.0, install 0.11.0
454
+ instead, and read the "Coming from 0.8.0" note of "0.10.0 → 0.11.0" first.
455
+
456
+ ### 2. `astro.config.mjs`
457
+
458
+ Make these edits; the result below shows all of them together.
459
+
460
+ 1. **Add `remarkAsides` last in `markdown.remarkPlugins`.** Starlight turns
461
+ `:::note` / `:::tip` / `:::caution` / `:::danger` into asides only under its
462
+ own `src/content/docs/`, so in a course's `content/docs/` they rendered as a
463
+ bare `<div>`. `remarkAsides` hands each one to Starlight's `<Aside>`. No
464
+ check fails without it; the asides just stay unstyled. Do **not** add
465
+ `content/docs/` to Starlight's `markdown.processedDirs` instead: every
466
+ heading would get two anchors.
467
+ 2. **Drop the `disclosureDefaults` mapping.** `CourseConfig.disclosureDefaults`
468
+ is gone (step 3). If the file builds a `resolvedCourseConfig = { ...courseConfig,
469
+ disclosureDefaults: { notation, references } }`, delete it and pass
470
+ `courseConfig` itself to both `loadManifest` and `explicoContentRefresh`.
471
+ 3. **Read the heading and References defaults from `courseConfig.disclosure`.**
472
+ `rehypeHeadingAnchors` takes `{ defaultCollapsible: courseConfig.disclosure }`
473
+ (it reads `sections` and `subsections`), and `remarkCitation` takes
474
+ `defaultOpen: courseConfig.disclosure?.references?.defaultOpen === true`.
475
+ A local `defaultCollapsible` object moves into the `disclosure` block of
476
+ step 3.
477
+ 4. **Wire `LayoutPageTitle.astro` as Starlight's `PageTitle` override.** It puts
478
+ the part's name, linked, above every chapter title. Optional, but it is the
479
+ one route to a part page (step 15) that does not depend on the sidebar.
480
+ 5. **Check that `PageSidebar` is `LayoutPageSidebar.astro`.** "On this page"
481
+ lists knowledge checks and worked-example sets only through it.
482
+ 6. **Only for a course deployed under a subpath** (an Astro `base`, such as a
483
+ GitHub Pages project site): pass `base` to `createRemarkNotationOptions` and
484
+ to `remarkAbbreviations`. Without it, the links these plugins write (a
485
+ symbol's glossary card, a cross-lesson reference, an abbreviation) are
486
+ root-relative and 404.
487
+
488
+ ```js
489
+ import remarkAsides from 'explico/reference/remark-asides.mjs';
490
+ import rehypeHeadingAnchors from 'explico/reference/rehype-heading-anchors.mjs';
491
+ import { courseConfig } from './content/course.config.ts';
492
+
493
+ const disclosure = courseConfig.disclosure ?? {};
494
+ const manifest = await loadManifest(courseConfig);
495
+
496
+ // … integrations:
497
+ explicoContentRefresh(courseConfig, manifest),
498
+ starlight({
499
+ // …
500
+ components: {
501
+ Footer: 'explico/components/starlight/LessonFooter.astro',
502
+ PageSidebar: 'explico/components/starlight/LayoutPageSidebar.astro',
503
+ PageTitle: 'explico/components/starlight/LayoutPageTitle.astro',
504
+ // … Header, Sidebar, SocialIcons as before
505
+ },
506
+ }),
507
+
508
+ // … markdown.remarkPlugins:
509
+ [remarkNotation, createRemarkNotationOptions(manifest, { strict: true, base })],
510
+ [remarkCitation, {
511
+ sources: manifest.sources,
512
+ defaultOpen: disclosure.references?.defaultOpen === true,
513
+ }],
514
+ [remarkAbbreviations, { abbreviations: manifest.abbreviations, base }],
515
+ remarkAsides, // last
516
+
517
+ // … markdown.rehypePlugins:
518
+ [rehypeHeadingAnchors, { defaultCollapsible: disclosure }],
519
+ ```
520
+
521
+ **Only if your course overrides `Footer` with its own component** instead of
522
+ `LessonFooter.astro`: read the notation panel's first state from
523
+ `manifest.disclosure.notation.defaultOpen` (it was
524
+ `manifest.disclosureDefaults.notation`), pass `KeyedFloatEnhancer` its new
525
+ `aliases` prop (`manifest.numbering.aliases`, step 11) and `NotationLayer` its
526
+ new `suspended` prop (the page's `notation.suspend`, step 10), as
527
+ `LessonFooter.astro` does.
528
+
529
+ ### 3. `CourseConfig` (`content/course.config.ts`)
530
+
531
+ 1. **`disclosureDefaults` → `disclosure`.** Every collapse default is now one
532
+ block, in the shape of the course settings' `disclosure` block. Move the
533
+ old `disclosureDefaults.notation` / `.references` booleans and the
534
+ `sections` / `subsections` objects from `astro.config.mjs` into it. Every
535
+ key is optional; the values below are the defaults, which match what each
536
+ kind had hardcoded before:
537
+
538
+ ```ts
539
+ disclosure: {
540
+ sections: { collapsible: false, defaultOpen: true }, // ## headings
541
+ subsections: { collapsible: false, defaultOpen: true }, // ### and deeper
542
+ notation: { defaultOpen: false }, // was disclosureDefaults.notation
543
+ references: { defaultOpen: false }, // was disclosureDefaults.references
544
+ knowledgeChecks: { collapsible: true, defaultOpen: true }, // new
545
+ memos: { defaultOpen: false }, // new
546
+ figures: { defaultOpen: true }, // new
547
+ diagrams: { defaultOpen: true }, // new
548
+ workedExamples: { defaultOpen: false }, // new
549
+ tables: { truncateAfter: 6 }, // new: rows shown, half from each end
550
+ },
551
+ ```
552
+
553
+ If the course keeps its settings in a JSON file and spreads it into
554
+ `CourseConfig`, stop splitting `disclosure` off: pass the block through as
555
+ it is. A course that calls `normalizeDisclosureDefaults` switches to
556
+ `normalizeDisclosure` (with `DISCLOSURE_DEFAULTS`); both are exported from
557
+ `explico/compiler/course-config`.
558
+ 2. **Move the deprecated top-level blocks into `rules`.** `operators`,
559
+ `romanLabels` and `abbreviations` still work with a `config-deprecated-key`
560
+ warning; move each `{ allow, deny }` block unchanged to `rules.N8`,
561
+ `rules.N10` and `rules.N14` (rule N8, rule N10, rule N14). Setting both
562
+ the old key and its replacement is an error. The old keys go away in a later
563
+ MINOR.
564
+ 3. **Only if you took `partPages` from `main` before this release:** it is
565
+ `rules.S2` now (`bridgeMinWords`, `bridgeMaxWords`, `scopeMinWords`,
566
+ `scopeMaxWords`), with no alias.
567
+ 4. **Optional new fields**, each with the default that applies when it is left
568
+ out:
569
+ - `numbering`: `{ sections: { show: true, depth: 4 }, floats: "chapter",
570
+ aliases: true }` (step 11).
571
+ - `blockLabels`: `"icon-word"`, or `"icon"` for a knowledge check's and an
572
+ example set's badge without its word (the word stays for a screen
573
+ reader).
574
+ - `rules`: per-rule switches and settings (step 12).
575
+ - `courseRules`: the course's own `X` rules (step 15).
576
+ - `CoursePart.overviewLabel`: renames a part's `Overview` sidebar row.
577
+
578
+ ### 4. Course settings schema (only if your course publishes one)
579
+
580
+ A course that describes its settings in a JSON Schema (the playground's
581
+ `course.settings.schema.json`) points two properties at the schemas
582
+ `pnpm build` now ships, so the rule blocks never need a copy:
583
+
584
+ ```json
585
+ "rules": { "$ref": "../node_modules/explico/dist/rules/rules.schema.json" },
586
+ "courseRules": { "$ref": "../node_modules/explico/dist/rules/course-rules.schema.json" }
587
+ ```
588
+
589
+ Add `numbering`, `blockLabels`, and the new `disclosure` keys
590
+ (`knowledgeChecks`, `memos`, `figures`, `diagrams`, `workedExamples`,
591
+ `tables.truncateAfter`) with the defaults in step 3; the playground's
592
+ [`course.settings.schema.json`](https://github.com/danieltuzes/explico/blob/v0.10.0/playground/content/course.settings.schema.json)
593
+ has each with a title and a description. If you use explico's settings form
594
+ editor (`tools/vscode-settings-editor/` in the explico repository), reinstall
595
+ it from a v0.10.0 checkout (`node tools/vscode-settings-editor/install.mjs`):
596
+ an older build shows `rules` and `courseRules` as text fields reading
597
+ `[object Object]`.
598
+
599
+ ### 5. Course scripts
600
+
601
+ 1. **`scripts/compile-manifest.ts`** — three new diagnostics groups, and the
602
+ rules-in-effect page. Without `examples` in the group lists, an `ex-…`
603
+ error does not fail `validate:content` (the page build still fails on it).
604
+
605
+ ```ts
606
+ import { createRunLog, diagnosticLine } from 'explico/compiler/logging';
607
+ import { writeRulesInEffect } from 'explico/rules/in-effect';
608
+
609
+ const { /* … */ checks, examples, knowledge, rawFloats, config, waivers } =
610
+ manifest.diagnostics;
611
+
612
+ // Add ['examples', examples] to BOTH the warnings loop and the errors loop
613
+ // over [['equations', equations], …, ['checks', checks], …].
614
+
615
+ // The course settings themselves: a deprecated block, a retired rule.
616
+ for (const warning of config) {
617
+ console.warn(log.warning('config', `[${warning.code}] ${warning.message}`));
618
+ }
619
+ // Per-place waivers: a stale one warns; a malformed or invalid one fails.
620
+ for (const warning of waivers.filter((d) => d.severity === 'warning')) {
621
+ console.warn(log.warning('waivers', diagnosticLine(warning)));
622
+ }
623
+ // … and next to the other failures:
624
+ for (const issue of waivers.filter((d) => d.severity === 'error')) {
625
+ fail('waivers', diagnosticLine(issue));
626
+ }
627
+
628
+ // Optional: format math findings with the owning rule's ID,
629
+ // `[N9 prose-notation] file:line: …`.
630
+ const formatMathIssue = (issue: (typeof math)[number]) => diagnosticLine(issue);
631
+
632
+ // The page an agent reads before drafting (step 12).
633
+ writeRulesInEffect(manifest);
634
+ ```
635
+
636
+ 2. **`scripts/content.ts`** — add the `rules` verb: `contentRules(manifest,
637
+ parseRulesFilter({ element, enforcement }))` from `explico/compiler/cli`,
638
+ taking `--element <e>[,<e>]`, `--enforcement script|partial|guide|locked`
639
+ and `--json`. `content status` can print the new `keyedExamples` count.
640
+ 3. **`.gitignore`** — `build/rules-in-effect.md` is regenerated on every run.
641
+ Ignore it unless `build/` is ignored already.
642
+ 4. **Tools that read `content … --json`.** Each diagnostic now carries its
643
+ owner as `rule`. A math finding's `code` is `math:<code>` for every finding
644
+ (`math:prose-notation`, `math:unresolved-glyph`, …), where it used to be
645
+ `math:prose` or `math:structure` for code-less ones.
646
+ `MathGateDiagnostic.code` is required in the type.
647
+ 5. **Code that imports engine internals.** `RuleEntry.retired` is gone
648
+ (`retiredRules()` / `retiredRule(id)` in `explico/rules/catalog`);
649
+ `proseOnly` takes `{ keepMath }`; `gateProseNotation` /
650
+ `gateProseNotationInAssessments` take `{ allowSpelledUnits }`;
651
+ `resolveMathGlyphs` takes `{ suspended }` as a third argument, and an
652
+ `unresolved` item may carry `suspended` or `font`. A lab island imports
653
+ `slotPlaceholder` / `RenderedLabMath` from `explico/reference/lab-math-slot`
654
+ (`render-lab-math` still re-exports both, but pulls the server-only KaTeX
655
+ chain into the browser graph and makes Vite warn about `node:fs`).
656
+
657
+ ### 6. The manifest schema is now v23
658
+
659
+ It was v16 in 0.9.0. `pnpm validate:content` rewrites `build/manifest.json`,
660
+ so this step is only for code that reads the manifest itself:
661
+
662
+ - **v17** (additive): `parts` — every part with its chapters in reading order
663
+ and its overview page; `sidebar[].items` is one mixed list of lesson slugs
664
+ and `{ label, link, attrs? }` entries (`SidebarItem`), and `SidebarGroup` is
665
+ one interface instead of a union; `lessons[].studyMinutes`,
666
+ `parts[].studyMinutes`.
667
+ - **v18** (additive): `rules` (`disabled`, each rule's effective settings);
668
+ `diagnostics.config`; `diagnostics.waivers`; every math-gate finding carries
669
+ a `code`.
670
+ - **v19** (additive): `rules.course`, the course's own rules;
671
+ `MathGateDiagnostic.context` gains `'course'`.
672
+ - **v20**: `numbering`; `lessons[].headings` (`slug`, `text`, `level`,
673
+ `number?`); `lessons[].numbering.prefix`. Under the default chapter scope,
674
+ the per-kind counts in `lessons[].numbering`, which used to carry a part's
675
+ running total, are all 0.
676
+ - **v21**: `lessons[].equations` / `.tables` / `.figures` / `.diagrams` and the
677
+ kinds' `labels` carry `short` and, for a JSX float, `caption`
678
+ (`ManifestLessonFloat`).
679
+ - **v22**: `disclosure` (every default applied) replaces
680
+ `disclosureDefaults`.
681
+ - **v23**: `examples`, `lessons[].examples`, `lessons[].numbering.examples`,
682
+ `diagnostics.examples`; `lessons[].knowledgeChecks[].title`;
683
+ `lessons[].titledBlocks`; `blockLabels`.
684
+
685
+ ### 7. Rename the removed float props (the build fails until you do)
686
+
687
+ Every float is named by one caption pair: a long `caption` and an optional
688
+ `short` caption, LaTeX's `\caption[short]{long}`. The other name props are
689
+ removed outright, and each fails with the edit to make. Find them:
690
+
691
+ ```bash
692
+ rg -n -U '<(Figure|Diagram|Memo|CompactExamples)\b[^>]*\b(title|label|description|summary)=' content src
693
+ ```
694
+
695
+ 1. `<Figure title="X" caption="Y">` → `<Figure short="X" caption="Y">`, or
696
+ drop `title` when X only repeats the caption (`fig-removed-prop`).
697
+ 2. `<Diagram label="X">` → `<Diagram caption="X">`, plus `short` for a long
698
+ caption; `<Diagram title>` fails too (`dia-removed-prop`). Check wrapper
699
+ `.astro` components as well: the manifest scan cannot see inside them, but
700
+ the component throws when it renders.
701
+ 3. `<Memo title="X" description="Y">` → `<Memo short="X" caption="…">`.
702
+ `caption` is required and is now the memo's bar, so write it as bar text;
703
+ `short` is optional (the marker's tooltip). `remarkMemo` fails the old props.
704
+ 4. `<CompactExamples summary="X">` → `<CompactExamples id="…" short="X">`.
705
+ Both are required now (`ex-missing-prop`; `summary` is `ex-removed-prop`):
706
+ `id` is lowercase kebab-case and anchors the set as `#ex-‹id›`; `short` names
707
+ the set's topic ("Speeding up and braking"), since the engine draws the
708
+ "Example" badge and the number before it. `short` no longer defaults to
709
+ "Worked examples", and `hint` has no default.
710
+
711
+ Rule S1 now measures the `short` caption (of a `Figure`, `Diagram`, `Table`,
712
+ `Memo` and `CompactExamples`), a display equation's `\caption{…}` and a
713
+ `ShortNote`'s `title`, and never a long `caption`; a heading's `[collapsed]`
714
+ / `[card]` markers no longer count toward it. A `Figure title` that passed rule
715
+ S1 passes the same way as `short`.
716
+
717
+ ### 8. Assessment titles and labs
718
+
719
+ 1. **Assessment titles name the topic alone.** A knowledge check's heading is
720
+ now the engine's badge ("Knowledge check"), its number, then the assessment
721
+ file's `title`, so a title that repeats the kind reads twice ("Knowledge
722
+ check 1.1.1 Velocity check"). In each `content/assessments/*.json`, drop
723
+ "check" / "Knowledge check" from `title`.
724
+ 2. **A lab carries no title or caption of its own**; the `<Figure>` around it
725
+ names the whole. In a lab inside a `<Figure>`, delete its own heading and
726
+ the `aria-labelledby` that names it, and turn an inner
727
+ `<figure><figcaption>` into a plain element, moving the sentence into the
728
+ `<Figure caption>`. The engine's `.lab-chart-figure` styles are gone; draw
729
+ the chart in a plain `.lab-chart`.
730
+ 3. **Rule V3 is narrowed.** It keeps the hydrated island, the `labMathScope` /
731
+ `renderLabMath` / `\slot{}` seam and labelled controls, and no longer asks
732
+ for an accessible name or description (`ariaLabel` / `ariaDescription` /
733
+ `ariaHidden`) or a `<details>` text alternative: the `<Figure>` caption is
734
+ the name. Nothing in a build changes; keep or drop what you wrote for the
735
+ old rule V3.
736
+
737
+ ### 9. Headings: rule S6 and rule S7 (errors)
738
+
739
+ Both read a lesson's outline, the same top-level headings the section numbers
740
+ count. A heading inside a component and a memo's bar are not in it; a
741
+ `<AssessmentSet />` and a `<CompactExamples>` are, as `##` headings.
742
+
743
+ - **Rule S6** (`heading-skip`): headings descend one level at a time. A lesson
744
+ body starts at `##` (the frontmatter `title` is the page's one `#`, so a `#`
745
+ in the body fails), and `##` → `####` fails; going back up is free. A `###`
746
+ → example set → `####` is a skip too. Fix it by making the heading one level
747
+ deeper than the one before it.
748
+ - **Rule S7** (`heading-adjacent`): a heading is followed by body content,
749
+ never directly by another heading, and a check or an example set counts as a
750
+ heading. Write a sentence between the two, or delete a heading that only
751
+ announces the block under it (`## Check yourself` above `<AssessmentSet />`,
752
+ `## Try it` above `<CompactExamples>`).
753
+
754
+ Otherwise waive one place with `{/* explico-allow S6: reason */}` on the line
755
+ above the heading, or switch a rule off with `rules.S6.enabled: false` /
756
+ `rules.S7.enabled: false`. For parallel items deep in the outline,
757
+ `docs/authoring/structure.md` § Structure from the bottom up recommends run-in
758
+ labels or a description list instead of deeper headings.
759
+
760
+ ### 10. Math and prose: what now fails or warns
761
+
762
+ Run `pnpm validate:content` and work through these by code.
763
+
764
+ 1. **Rule N9, `spelled-unit` (error).** A unit written as a word after a
765
+ number, around `per`, or with `square` / `cubic` / `squared` / `cubed`
766
+ fails, in lesson bodies and assessment `prompt` / `explanation`: "a time
767
+ of 4 seconds" → `$4\ \text{s}$`, "30 degrees above the horizontal" →
768
+ `$30^{\circ}$`, "$x = 3$ meters" → `$x = 3\ \text{m}$`. The message gives
769
+ the math. A unit named on its own ("measured in meters") passes, and rule
770
+ N9's old carve-out for a spelled-out unit in flowing prose is gone. Rewrite
771
+ it, waive a defining sentence with `{/* explico-allow N9: reason */}`, or
772
+ list unit words the course keeps in `rules.N9.allow`.
773
+ 2. **Rule N12, `math-trailing-punctuation` (error).** A `.`, `,`, `;` or `:` is
774
+ never the last visible thing in a `$…$` / `$$…$$` span, or in a row before
775
+ `\\`; move it out after the closing `$`. A trailing decimal point (`5.`) and
776
+ a typed `...` fail too; the decimal-point exception is gone. Dotted notation
777
+ abbreviations (`a.e.`, `i.i.d.`, …) pass; edit that list with
778
+ `rules.N12.allow` / `rules.N12.deny`, waive a place, or switch rule N12 off.
779
+ 3. **Rule N4, `unresolved-glyph` (error): a font-styled letter is its own
780
+ symbol.** `\mathbf{x}`, `\boldsymbol{\beta}`, `\bm{x}`, `\mathcal{N}`,
781
+ `\mathscr{L}`, `\mathfrak{g}`, `\mathbb{E}`, `\mathsf{x}` and `\mathtt{x}`
782
+ no longer borrow the plain letter's card, and `\mathbf{e}` /
783
+ `\boldsymbol{\pi}` no longer pass as Euler's number or π. Give each its own
784
+ entry (`latex: \mathbf{x}`) or an `\explain{key}{\mathbf{x}}` wrap — in
785
+ lessons, notation formulas, assessments and labs alike. `\mathit` /
786
+ `\mathnormal` still share the plain letter's card, and `\mathbb{N,Z,R,C}`
787
+ stay reserved.
788
+ 4. **Rule N22, `accent-explained` (warning): an accent or mark is an
789
+ operator.** A decoration (`\hat`, `\bar`, `\tilde`, `\dot`, `\vec`,
790
+ `\overline`, a superscript `'`, `^*`, `^\dagger`, …) needs one of: an entry
791
+ on `rules.N22.allow` (empty by default), an operator notation entry
792
+ (`latex: \hat{}`, `{}'`, `{}^{*}`), or a whole-form entry
793
+ (`latex: \hat{\beta}`). A page where a mark is not an operator declares it
794
+ in its frontmatter (`notation.suspend: [{ operator: "'", reason: … }]`);
795
+ every decorated form of it on that page then needs its own entry or a
796
+ whole-form `\explain`, or the build fails with
797
+ `suspended-operator-unresolved`. Two consequences for existing notation:
798
+ - A marked entry no longer lends its card to the bare letter: with only
799
+ `latex: b'` (or `\theta^{*}`), a bare `b` elsewhere on the page now needs
800
+ its own entry, or rule N4's gate fails with `unresolved-glyph`.
801
+ - A whole-form match now wraps the decoration too
802
+ (`\explain{k}{\hat{\beta}}`, not `\hat{\explain{k}{\beta}}`), and the wrap
803
+ for `\boldsymbol`, `\bm`, `\bold`, `\Bbb`, `\frak`, `\mathsf`, `\mathtt`
804
+ and `\mathnormal` lands around the command. Only snapshot tests of
805
+ rendered HTML notice.
806
+ 5. **Rule N8 and rule N10 now read labelled display equations**
807
+ (`operator-explained`, `roman-label-explained`, both warnings). A
808
+ `\label{eq:…}` used to hide the whole equation from both scans, so new
809
+ findings may appear. Explain the operator or label, or list it under
810
+ `rules.N8.allow` / `rules.N10.allow`.
811
+ 6. **Rule N5: hand-typed numbers are now wrong numbers.** Every float number
812
+ changed (step 11). Prose that types "Equation 2.4" instead of citing
813
+ `[[eq-…]]` now names the wrong equation; cite it by key.
814
+ 7. `x^\prime` no longer fails as an unresolved `\prime`.
815
+
816
+ ### 11. Numbering: every number a reader sees changed
817
+
818
+ 1. **Float numbers are chapter-scoped.** A display equation, table, figure,
819
+ diagram, knowledge check and example set is numbered
820
+ `‹part›.‹chapter›.‹n›`, and each kind restarts in every chapter:
821
+ `Figure 2.1` is now `Figure 2.2.1`. `[[…]]` references need no edit, since
822
+ they print from the manifest. `numbering.floats: "part"` keeps the old
823
+ `‹part›.‹n›` scheme.
824
+ 2. **The positional anchor of an unkeyed float changed** with its number
825
+ (`#eq-2-4` → `#eq-2-2-4`). Update links that point at one, or give the
826
+ float a key (`\label{eq:…}`, an `id`) so its anchor names its content.
827
+ 3. **Top-level headings are numbered** `1.1.2`, down to `####` by default
828
+ (`numbering.sections.depth`, `2`–`6`), on a wide screen, in print, and on a
829
+ folded section's bar. `numbering.sections.show: false` hides them, and a
830
+ knowledge check's and example set's numbers with them. The number is
831
+ generated content: the heading's text and anchor do not change.
832
+ 4. **A heading with inline math gets its anchor from its source**
833
+ (`## The range $R$` → `#the-range-r`), no longer from KaTeX's output.
834
+ Update links to such headings: `rg -n '^#{2,6} .*\$' content/docs`.
835
+ 5. **`sec-` and `ex-` are reserved reference prefixes.** `[[sec-…]]` cites a
836
+ section and `[[ex-…]]` an example set. Rename a notation key that starts
837
+ with either and is cited as `[[sec-…]]` or `[[ex-…]]`.
838
+ 6. **Number aliases** (`numbering.aliases`, on by default): a fragment a reader
839
+ types in number form (`#sec-1.1.2`, `#fig-2.2.1`, `#ex-1.1.1`) lands on the
840
+ element that shows that number. The site itself links and copies only
841
+ named anchors.
842
+ 7. Update prose, captions and tests that quote a number, and record the
843
+ renumbering in `content/CHANGELOG.md` (rule C1): readers see new numbers.
844
+
845
+ ### 12. Rules: retired IDs, settings, switches, waivers
846
+
847
+ 1. **Retired rules.** Rule N1 (the normal distribution is `\mathcal{N}`), rule
848
+ N6 (probability is `\mathbb{P}`) and rule N7 (the differential is
849
+ `\mathrm{d}`) are now lines of **rule N2**, whose table is the setting
850
+ `rules.N2.reserved`: by default the four number systems, `\mathbb{P}`,
851
+ `\mathcal{N}(\mu, \sigma^2)` and `\mathrm{d}`, one
852
+ `"<latex> — <meaning>"` string each. Replace the list to state the course's
853
+ own conventions: drop the probability lines in a course without
854
+ probability, or write `\mathcal{N}(\mu, \sigma)` for one that parameterises
855
+ the normal by standard deviation. Rule N3 (write a multi-letter label as
856
+ `\mathrm{…}`) is folded into **rule N10**, now `partial`. Rule N16
857
+ (alternate delimiter shapes to show nesting) is folded into **rule N17**,
858
+ and **reversed**: a delimiter's shape carries its meaning, and the engine
859
+ sizes a nest. Content that alternated shapes still builds, but the rule no
860
+ longer asks for it, and a `[` inside a grouping nest reads as an interval
861
+ first. Rule C4 (a release moves `[Unreleased]` and bumps `about.version`
862
+ together) is folded into **rule C3**, now `partial`. A `rules.N1` /
863
+ `rules.N3` / `rules.N6` / `rules.N7` / `rules.N16` / `rules.C4` block warns
864
+ `config-retired-rule`, naming the successor: delete it (only rule N2 takes
865
+ settings). A waiver naming a retired ID is `waiver-invalid`; name the
866
+ successor. `docs/authoring/retired.md` lists every retired rule.
867
+ 2. **Per-rule settings and switches: `rules`.** One block per rule ID, holding
868
+ only what differs from the defaults: rule S1's `maxLength` (65), rule
869
+ S2's `bridgeMinWords` / `bridgeMaxWords` / `scopeMinWords` /
870
+ `scopeMaxWords` (30 / 150), rule V5's `seeds` (10), rule V6's
871
+ `minLabels` (3), the `allow` / `deny` lists of rule N8, rule N10, rule
872
+ N12, rule N14 and rule N22, rule N9's `allow`, and rule N2's `reserved`.
873
+ With `rules.<ID>.enabled: false` a rule stops running and its diagnostics
874
+ are withheld. Switchable: N5, N8, N9, N10, N14, N17, N19, N20, N21, S1–S7, C3,
875
+ and every guide rule. The build fails on an unknown rule or key, a value
876
+ outside its spec, `enabled: false` on a locked rule (rule N4, rule N13,
877
+ rule N15), and `rules.X1` (course rules live in `courseRules`).
878
+ 3. **Per-place waivers.** `{/* explico-allow <ID>[, <ID>]: <reason> */}` on
879
+ its own line waives the named rules for the next block. A malformed waiver,
880
+ or one naming a locked rule, an invariant or an unknown ID, is an error
881
+ (`waiver-malformed`, `waiver-invalid`); one that silenced nothing warns
882
+ (`waiver-unused`). Record the reason: it is what a reviewer reads.
883
+ 4. **Read the rules in effect, not the rule set.** `pnpm validate:content`
884
+ writes `build/rules-in-effect.md`, and `pnpm content rules [--element …]
885
+ [--enforcement …]` prints it filtered: the rules this course has on, by
886
+ element, with codes and settings. An agent drafting for the course reads
887
+ that page; `docs/authoring/agents.md` now says so.
888
+ 5. **Rules that only gained a name.** Rule S3 (`raw-table-markdown`,
889
+ `raw-table-html`), rule S4 (`diagram-unkeyed`, `diagram-nested`), rule S5
890
+ (`convention-single-definition`), rule N19 (`notation-units`,
891
+ `units-vocab`), rule N20 (`notation-source-locator`) and rule N21
892
+ (`gloss-name-shape`) name checks that already ran. Nothing new fires; the
893
+ console line now carries the rule, `[S3 raw-table-markdown] …`.
894
+ 6. **New guide rules** — review-only, so nothing fails; switch one off with
895
+ `rules.<ID>.enabled: false` if it does not fit the course:
896
+ - rule N18: a multi-step derivation is one `aligned` chain, `&` before each
897
+ relation, a justification after `\qquad` as `\text{…}`.
898
+ - rule V6: every quantitative axis shows at least three labelled values.
899
+ - rule P2 (one canonical term per concept), rule P3 (one meaning per term),
900
+ rule P4 (the established term, defined where first needed), rule P5
901
+ (literal main text; analogies in an aside), rule P6 (industry jargon only
902
+ in an "Industry jargon" aside), rule P7 (one claim per sentence, links
903
+ stated), rule P8 (relation words match the math), rule P9 (a display
904
+ equation's lead-in says what it gives and where it comes from).
905
+ - rule S8: prefer `<Memo>` over a Starlight aside when the content needs an
906
+ anchor, a caption or collapsing.
907
+ 7. **Rule S2 applies only to a part page** (step 15). A course without
908
+ `content/docs/<dir>/index.mdx` is unaffected. With one, each needs a
909
+ `<PartScope>` paragraph and, for every part after the first, a
910
+ `<PartBridge>` paragraph before it, each within the `rules.S2` word range
911
+ (`part-page-paragraph`).
912
+
913
+ ### 13. Record the content changes (rules C1–C3)
914
+
915
+ The steps above edit `content/`. Add bullets under `## [Unreleased]` in
916
+ `content/CHANGELOG.md` (rule C1): renumbered floats and sections, retitled
917
+ checks and example sets, renamed sections whose anchors changed. Bump
918
+ `about.version` per rule C2 when you release (`pnpm release:content` in the
919
+ playground's shape), keeping it equal to the newest heading (rule C3).
920
+
921
+ ### 14. Only if your course CSS, browser tests or scripts reach into engine markup
922
+
923
+ ```bash
924
+ rg -n 'keyed-figure__(titlebar|summary|title-anchor)|diagram__(titlebar|summary|title-anchor)|lab-chart-figure|memo-block__summary|memo-preview__description|data-memo-(self|anchor)-link|compact-examples__(summary|title)|details\.compact-examples|assessment-heading-number' src content tests
925
+ ```
926
+
927
+ - **Figure and diagram.** The number moved into a caption line below the body:
928
+ `figcaption.float-caption` with `.float-caption__toggle`,
929
+ `.float-caption__number`, `.float-caption__anchor` and
930
+ `.float-caption__text`. `.keyed-figure__titlebar`, `.keyed-figure__summary`,
931
+ `.keyed-figure__title-anchor`, `.diagram__titlebar`, `.diagram__summary` and
932
+ `.diagram__title-anchor` are gone; `.diagram__number` is a link. A compacted
933
+ float keeps the caption line and hides only its body. The `<details>` keeps a
934
+ native `<summary>` shown only under `(scripting: none)`.
935
+ - **Table.** The caption line is the same `.float-caption` (caption size, a
936
+ chain icon after the number, no " — ").
937
+ - **Memo.** `.memo-block` is the frame; the `<details>`,
938
+ `.memo-block__summary`, `[data-memo-self-link]`, `[data-memo-anchor-link]`
939
+ and `.memo-preview__description` are gone. Its bar is an `<h6 role="none">`
940
+ with the `#memo-…` anchor, folded by `CollapsibleSections`. The marker is a
941
+ superscript italic letter (`a`, `b`, …), not `[1]`; `data-memo-number`
942
+ carries the letter.
943
+ - **Knowledge check.** The heading is `BlockHeading.astro`'s
944
+ `<h2 id="check-‹id›" class="block-heading">` (badge, number, title in
945
+ `.block-heading__title`), carrying `data-check-number`; its wrapper carries
946
+ `data-card="accent"`. `.assessment-heading-number` is gone, and
947
+ `.assessment-shell` draws no box.
948
+ - **Example set.** `details.compact-examples`, `.compact-examples__summary`
949
+ and `.compact-examples__title` are gone; `<compact-examples
950
+ class="compact-examples">` is the card, `#ex-‹id›` its heading.
951
+ - **Container tiers.** Every container is restyled from the tokens in
952
+ `explico/styles/surfaces.css` (`--ex-surface-*`, `--ex-edge-*`,
953
+ `--ex-depth-*`, `--ex-radius-*`; `.ex-tier-meta`, `.ex-tier-accent`,
954
+ `.ex-tier-aside`), which `global.css` imports — `customCss` needs no second
955
+ entry. A `<Figure>` and a `.lab-shell` lose their card; checks, example sets
956
+ and `[card]` sections are accent cards; the notation panel, part-page rail,
957
+ glossary entries, curriculum-map cards, About popover and a Starlight
958
+ `<Card>` are meta; a memo, an open `<ShortNote>` and Starlight asides are
959
+ asides. No container has a top rule. Override the tokens rather than the
960
+ old per-container rules.
961
+ - **Screenshots and layout assertions.** Printed and displayed numbers
962
+ (step 11); a folded section's "Hidden here: …" line and, on a narrow screen,
963
+ its in-line chevron and "(tap to expand)"; the chain icon's "Link copied ·
964
+ § 1.1.2" confirmation; 0.25em more block padding on each display equation
965
+ (about 8px); the sidebar's leading part caret, filled hover rows, reading
966
+ progress, and `Overview` rows; print as shown (a closed disclosure prints
967
+ its title and a link, where print used to expand everything).
968
+ - **Reader state.** A memo's open state moved from `memo:‹id›` to
969
+ `section:memo-‹id›`, so a returning reader's choice resets once. An example
970
+ set remembers its state under `section:ex-‹id›` (new). A course that clears
971
+ preference keys by name adds both.
972
+
973
+ ### 15. Optional: features to adopt
974
+
975
+ - **Captions.** `\caption{…}` inside a display equation's `$$…$$` (next to its
976
+ `\label`) gives it a short caption: the `[[eq-…]]` preview card's heading
977
+ and the copy tooltip ("Copy LaTeX — Range formula"). `short` on a
978
+ `<Table>`, `<Figure>` or `<Diagram>` heads its preview card.
979
+ - **References.** `[[sec-slug]]` prints `§ 1.1.2` with a preview card;
980
+ `[[ex-id]]` prints `Example 1.1.1`; `[[check-…]]` now previews the check.
981
+ `content context <lesson> --json` lists a lesson's `sections` and
982
+ `examples`, so a drafter need not derive a slug.
983
+ - **Cards and asides.** `[card]` / `[card=meta]` after a heading
984
+ (`### Explore the range [collapsed] [card]`) cards its whole section.
985
+ `:::note` / `:::tip` / `:::caution` / `:::danger` render as asides (step 2);
986
+ `{icon="heart"}` names another Starlight icon and `icon=""` drops it, on a
987
+ directive or an `<Aside>`.
988
+ - **Part pages.** `content/docs/<dir>/index.mdx` is a part's own page: render
989
+ it with `<PartOverview>` and state its argument with `<PartScope>` /
990
+ `<PartBridge>` (rule S2). `<CourseContents>` lists every part and chapter for
991
+ a landing page. `studyMinutes: 40` (or `[30, 40]`) in a lesson's frontmatter
992
+ feeds the per-part totals.
993
+ - **Disclosure per instance.** `defaultOpen` is new on `<Memo>` and
994
+ `<CompactExamples>`, and `rememberState` on `<CompactExamples>`.
995
+ - **Course rules.** `courseRules` in the settings is a list of the course's
996
+ own rules, `X1`…`X99` (the `X` area is never used by the engine), each
997
+ `{ id, title, summary?, elements?, enabled?, severity?, forbid?, unchecked?,
998
+ doc? }`. With `forbid` patterns a rule is enforced at its `severity`
999
+ (`error` or `warning`), and each finding's code is the rule's ID; without
1000
+ one it is a guide. A bad definition fails the build with the setting's path.
1001
+ See `docs/authoring/README.md` § Rules of your own.
1002
+ - **Accessibility** is the engine's job (`docs/authoring/README.md`
1003
+ § Accessibility): nothing asks an author for an accessible name or a text
1004
+ alternative.
1005
+
1006
+ ### 16. Fixes that need no action
1007
+
1008
+ Grey boxes get a background in the dark theme; an equation's copy button keeps
1009
+ its "Copy LaTeX" tooltip; a collapsed section's chevron is on screen on a
1010
+ phone; the copy-link icon shows on touch screens; labs hydrate under
1011
+ `astro dev` again; a printed `\sqrt` keeps its radical sign; `<Diagram>`
1012
+ formulas render in Safari and iOS, on screen and in print; display equations
1013
+ are no longer clipped at the bottom; pointing at the letter under an accent
1014
+ shows the letter's card.
1015
+
1016
+ ### 17. Verify
1017
+
1018
+ ```bash
1019
+ pnpm validate:content # run twice: the first run regenerates build/resolution/*
1020
+ pnpm content rules # the rules this course now has on; read before drafting
1021
+ pnpm content warnings # new warnings: rule N22, rule N8 / rule N10 in labelled equations
1022
+ pnpm content check --changed
1023
+ astro check
1024
+ astro build
1025
+ ```
1026
+
1027
+ Then run the course's browser tests, if it has any, and re-record the numbers
1028
+ and selectors of step 11 and step 14. A course with a gate script of its own
1029
+ that runs these stages (explico's is `scripts/agent/verify`) runs that
1030
+ instead of the list above.
1031
+
1032
+ ## 0.8.0 → 0.9.0
1033
+
1034
+ **No course changes are required** beyond the dependency bump, unless your
1035
+ course's own CSS reaches into the layout chrome (step 2). Everything in this
1036
+ release is engine-side UI: the panel collapse controls, reader-set panel
1037
+ widths, and the compact-chapter tooltip. No authoring rule, manifest field,
1038
+ CLI command, `CourseConfig` field, or Astro wiring moved, and the manifest
1039
+ schema version is unchanged.
1040
+
1041
+ ### 1. Update the dependency
1042
+
1043
+ ```bash
1044
+ pnpm add -D explico@0.9.0
1045
+ ```
1046
+
1047
+ 0.9.0 was never published to npm. Coming from npm's 0.8.0, install 0.11.0
1048
+ instead, and read the "Coming from 0.8.0" note of "0.10.0 → 0.11.0" first.
1049
+
1050
+ Or set `"explico": "0.9.0"` in `package.json` and run `pnpm install`. Nothing
1051
+ in `astro.config.mjs`, `content/course.config.ts`, `content/`, or your course
1052
+ scripts needs to change for this release.
1053
+
1054
+ ### 2. Only if your course CSS targets the collapse control
1055
+
1056
+ Two names in the layout chrome changed. Check your own stylesheets and
1057
+ components:
1058
+
1059
+ ```bash
1060
+ rg -n 'layout-edge-icon|--layout-edge-toggle-size' src content
1061
+ ```
1062
+
1063
+ - `--layout-edge-toggle-size` no longer exists. It was one square; the control
1064
+ is now a rectangle, so set `--layout-edge-toggle-width` and
1065
+ `--layout-edge-toggle-height` instead.
1066
+ - `svg.layout-edge-icon` / `.layout-edge-icon-hide` / `.layout-edge-icon-restore`
1067
+ are now `span.layout-edge-glyph` / `.layout-edge-glyph-hide` /
1068
+ `.layout-edge-glyph-restore`.
1069
+
1070
+ No hit means nothing to do.
1071
+
1072
+ ### 3. Optional: retune the docked control
1073
+
1074
+ The control docks into the corner the header and the panel's separator line
1075
+ make. One gap sets both of those edges, and the radius applies to the one
1076
+ corner free of them (bottom-left on the navigation panel, bottom-right on the
1077
+ contents panel, mirrored in RTL). Override any of these in your course CSS:
1078
+
1079
+ ```css
1080
+ :root {
1081
+ --layout-edge-toggle-gap: 2px; /* under the header, and in from the line */
1082
+ --layout-edge-toggle-width: 1.875rem;
1083
+ --layout-edge-toggle-height: 1.125rem;
1084
+ --layout-edge-toggle-radius: 0.4375rem;
1085
+ --layout-peek-delay: 40ms; /* rest this long on a rail before it peeks */
1086
+ }
1087
+ ```
1088
+
1089
+ `--layout-peek-delay` is read by the script, not only by the stylesheet, so it
1090
+ accepts any CSS `<time>` (`40ms`, `0.04s`) and falls back to 40ms if it cannot
1091
+ be parsed. Set it to `0ms` for the previous behaviour, where crossing a
1092
+ collapsed rail opened it immediately.
1093
+
1094
+ Widening or heightening the control also widens the strip each panel keeps
1095
+ clear at its top (`--layout-edge-toggle-reserve`), so the first chapter row
1096
+ stays out from under it; you do not set that one yourself.
1097
+
1098
+ ### 4. Reader-side state
1099
+
1100
+ Layout preferences moved from `credit-playground:layout:*` to
1101
+ `explico:layout:*`, and two new keys join them
1102
+ (`explico:layout:navigation-width`, `explico:layout:contents-width`). The old
1103
+ keys are read once as a fallback, so a returning reader keeps the layout they
1104
+ left and there is nothing to migrate. Only if your course clears preference
1105
+ keys by name — few do — add the new names to that list.
1106
+
1107
+ ### 5. Only if your course has browser tests over the layout chrome
1108
+
1109
+ A test that asserts the collapse control's size, shape, or position, or that
1110
+ clicks it by an `svg` selector, needs updating: it is a rectangle roughly
1111
+ 30 × 18 px docked 2 px under the header and 2 px in from the separator line,
1112
+ both panels' controls share one block start, and its glyphs are text. While a
1113
+ collapsed panel peeks open on hover, its control also *grows* towards the
1114
+ separator — its outer edge stays put — so a test that hovers a rail sees a
1115
+ much wider control than the resting one. A peek also no longer opens the
1116
+ instant a rail is hovered: a test that hovers one and asserts immediately must
1117
+ poll, or set `--layout-peek-delay: 0ms` for the duration of that test. Panels also now carry
1118
+ `.layout-edge-resizer` handles, and a resized panel stamps
1119
+ `data-navigation-resized` / `data-contents-resized` on `<html>`.
1120
+
1121
+ ### 6. Verify
1122
+
1123
+ ```bash
1124
+ pnpm validate:content # run twice: the first run regenerates build/resolution/*
1125
+ pnpm content check --changed
1126
+ astro check
1127
+ astro build
1128
+ ```
1129
+
1130
+ ## 0.7.0 → 0.8.0
1131
+
1132
+ ### 1. Rule N17 is inverted: delimiters are sized by the engine (`\left`/`\right` everywhere)
1133
+
1134
+ The engine now installs its own KaTeX delimiter sizing, so a `\left … \right`
1135
+ pair is always strictly taller than its content and a nest shows its depth
1136
+ without help. Hand-picking `\bigl` → `\Bigl` → `\biggl` → `\Biggl` outward —
1137
+ which rule N17 previously *required* — is now the thing the linter warns about.
1138
+
1139
+ **There is no install step.** `astro.config.mjs` already imports
1140
+ `explico/reference/katex-options.mjs`, and that module now imports
1141
+ `explico/reference/katex-delimiters.mjs` for its side effect. Confirm the import
1142
+ is still there; a course that inlined its own KaTeX options object instead must
1143
+ add, as the **first** import in `astro.config.mjs`:
1144
+
1145
+ ```js
1146
+ import 'explico/reference/katex-delimiters.mjs';
1147
+ ```
1148
+
1149
+ Order matters: the installer moves KaTeX's render entry points onto the shared
1150
+ `katex` object, so anything that renders math before the import would keep stock
1151
+ spacing. It refuses to fail quietly — it renders a known nest at install time and
1152
+ throws if the sizes did not grow.
1153
+
1154
+ Two course-side conditions make the build throw, both with an explicit message:
1155
+
1156
+ - **A `patches/katex@*.patch` file that already applies a negative
1157
+ `\delimitershortfall`.** Delete the patch and its `pnpm.patchedDependencies`
1158
+ entry; the engine does this in code now, and a patch is project-local anyway
1159
+ so it never reached a consumer.
1160
+ - **A `katex` whose bundle no longer holds the constant where the engine looks
1161
+ for it.** The rewrite targets one line in KaTeX's ESM bundle, so keep `katex`
1162
+ on the declared peer version (`0.16.47`). An upgrade that moves or reformats
1163
+ that line fails the build asking you to re-derive the change against
1164
+ [`../katex-bracket-scaling.md`](../katex-bracket-scaling.md), which is
1165
+ shipped in the package. The failure is loud and immediate, not a silent
1166
+ return to stock spacing.
1167
+
1168
+ ### 2. Diagnostic renamed: `delimiter-size-hierarchy` → `delimiter-manual-size`
1169
+
1170
+ The old code string no longer exists. Update every place that names it:
1171
+
1172
+ - `lint-ignore.yml` — a stale line is inert and silently stops suppressing.
1173
+ - Course scripts, tests, and type narrowings over the diagnostic union;
1174
+ `astro check` flags a comparison against a non-member.
1175
+
1176
+ Its payload changed shape too. A consumer reading the fields, rather than just
1177
+ the `code` and `message`, needs this edit:
1178
+
1179
+ ```diff
1180
+ -// outerSize / nestedSize described the mismatch between two levels
1181
+ -const found = diagnostic.outerSize;
1182
+ -const suggested = diagnostic.suggestion?.size;
1183
+ +// `size` is the hand-picked size found; `suggestion` is always present and is
1184
+ +// always a \left/\right pair, so it no longer carries a `size` of its own.
1185
+ +const found = diagnostic.size;
1186
+ +const suggested = diagnostic.suggestion.latex;
1187
+ ```
1188
+
1189
+ `DelimiterSizeSuggestion` keeps `open`, `close`, and `latex`, and drops `size`.
1190
+ `DelimiterSizeDiagnostic` drops `outerSize` and `nestedSize`, gains a required
1191
+ `size`, and `suggestion` is no longer optional.
1192
+
1193
+ ### 3. Content: hand-sized nests now warn
1194
+
1195
+ Math that was N17-compliant under 0.7.0 reports one `delimiter-manual-size`
1196
+ finding per hand-sized level. This is a Tier-3 warning — it does not fail a
1197
+ build — so it can be worked through at your own pace, but the findings are real
1198
+ and the repair improves the rendering.
1199
+
1200
+ Find them with:
1201
+
1202
+ ```
1203
+ pnpm content warnings --json
1204
+ ```
1205
+
1206
+ The repair is mechanical: replace the hand-picked pair with `\left`/`\right`,
1207
+ keeping the delimiter shapes.
1208
+
1209
+ ```diff
1210
+ -\Biggl[a+\biggl(b+\Bigl[c+\left(d+e\right)\Bigr]\biggr)\Biggr]
1211
+ +\left[a+\left(b+\left[c+\left(d+e\right)\right]\right)\right]
1212
+ ```
1213
+
1214
+ A **lone** hand-sized pair that is not part of a nest — `\bigl(x,\ y\bigr)`
1215
+ around a tuple, for instance — is a single-level typographic choice with no
1216
+ hierarchy to get wrong, and is deliberately left alone. Do not convert those.
1217
+
1218
+ Expect a small visual change everywhere a nest is rendered: each level is now
1219
+ strictly larger than the one inside it, so outer delimiters grow by roughly one
1220
+ size step. A course holding screenshot or bounding-box baselines over math
1221
+ should re-record them.
1222
+
1223
+ ### 4. Manifest schema is now v16: a math diagnostic can name its equation
1224
+
1225
+ `MANIFEST_SCHEMA_VERSION` is 16 (v15 → v16). The bump is **additive** — nothing
1226
+ existing changed shape — so the only required edit is to a client that asserts
1227
+ the number by exact equality, which now expects 16.
1228
+
1229
+ What was added: a finding the math gate reports from a `$$…$$` block carrying a
1230
+ `\label{eq:…}` gains an optional `equation` field, `{ key, href }`. `href` is
1231
+ the rendered page's anchor for that equation
1232
+ (`/l1-ar/ar1-basics/#eq-ar1-variance`). `CliDiagnostic` carries the same
1233
+ optional field, so a course's own `scripts/content.ts` picks it up with no
1234
+ change.
1235
+
1236
+ `diagnosticLine()` now appends it, so console output, the run log, and
1237
+ `pnpm content diagnostics` read:
1238
+
1239
+ ```
1240
+ [code] file:line:column: message (eq:<key> -> <href>)
1241
+ ```
1242
+
1243
+ A course script that parses that line with an anchored regex must tolerate the
1244
+ optional trailing ` (eq:… -> …)`. One that just prints the string needs nothing.
1245
+
1246
+ ### 5. Diagnostic line numbers are corrected — re-record anything that stored them
1247
+
1248
+ Every gate scans the lesson body, which Astro hands over with the frontmatter
1249
+ already stripped, but findings are reported against the lesson *file* — so every
1250
+ `file:line:column` the engine has emitted for a lesson was short by the height
1251
+ of that lesson's frontmatter. `gate-math` (every `delimiter-manual-size`
1252
+ column), `gate-prose-notation` (rule N9), `gate-abbreviations` (rule N14), and
1253
+ `gate-heading-length` (S1) are all corrected at once.
1254
+
1255
+ Nothing to edit in content, but line numbers move, so:
1256
+
1257
+ - Re-check any suppression, snapshot, or problem matcher keyed to a line number.
1258
+ Line numbers were wrong before; they now point at the real line.
1259
+ - `pnpm validate:content` (twice) regenerates `build/resolution/*` with the
1260
+ corrected positions. Commit the result.
1261
+
1262
+ ### 6. Malformed `\explain` / `\slot` keys now fail the build
1263
+
1264
+ Previously a key that did not match the notation key pattern —
1265
+ `\explain{Not A Key}{\Omega}` — was not a KaTeX parse error: KaTeX rendered the
1266
+ command name in its error color and carried on, so the page shipped red
1267
+ `\htmlData` text with a green build. `renderNotationMath` now checks both marker
1268
+ macros before KaTeX runs and throws, naming the offending key.
1269
+
1270
+ **This can newly fail a build that passed on 0.7.0.** If it does, the page was
1271
+ already broken for readers. Fix the key to match the notation key pattern
1272
+ (lower-case, hyphen-separated) and make sure it resolves to a real entry.
1273
+ `renderNotationFormula` still degrades to inert-but-correct math rather than
1274
+ costing a reader the equation.
1275
+
1276
+ ### 7. Glossary entry heading markup: the anchor is now a sibling of the `<h2>`
1277
+
1278
+ The per-entry anchor link moved out of the `<h2>` and into a new
1279
+ `.glossary-heading` flex row wrapping both — the same shape
1280
+ `rehype-heading-anchors` gives a lesson heading. This restores the heading's
1281
+ accessible name (a screen reader was announcing "Velocity Link to Velocity").
1282
+
1283
+ Nothing visual changed, but update course-owned CSS and end-to-end tests that
1284
+ assumed the nesting:
1285
+
1286
+ ```diff
1287
+ -h2:hover .glossary-anchor,
1288
+ -h2:focus-within .glossary-anchor {
1289
+ +.glossary-heading:hover .glossary-anchor,
1290
+ +.glossary-heading:focus-within .glossary-anchor {
1291
+ ```
1292
+
1293
+ A test locating the anchor as a descendant of the heading must target
1294
+ `.glossary-heading .glossary-anchor` instead. A check for the heading *by
1295
+ accessible name* now matches the term alone, which is the point of the fix — if
1296
+ such a test was written against the broken name ("Velocity Link to Velocity"),
1297
+ it needs the term.
1298
+
1299
+ ### 8. The About panel publishes its versions as data attributes
1300
+
1301
+ Additive; no course edit is required. `details[data-about-panel]` now carries
1302
+ `data-knowledge-version`, `data-knowledge-date` (omitted when the content
1303
+ changelog has no date), and `data-engine-version`, and the rendered version line
1304
+ is marked `data-knowledge-line`.
1305
+
1306
+ This exists so a build check does not have to match prose. A course asserting
1307
+ the panel's contents by text is pinned to one version string and turns every
1308
+ routine bump into a test edit — this repo's own `about.spec.ts` was asserting
1309
+ `0.4.0` against a playground long since past it. Prefer:
1310
+
1311
+ ```diff
1312
+ -await expect(panel).toContainText('0.6.0');
1313
+ +await expect(panel).toHaveAttribute('data-knowledge-version', knowledgeVersion);
1314
+ ```
1315
+
1316
+ reading the expected value from `content/course.config.ts` rather than hard-coding
1317
+ it. A bare version substring is not scoped to the panel either — the engine
1318
+ version printed one row below can satisfy it.
1319
+
1320
+ ### 9. Fixes that need no action
1321
+
1322
+ These remove build failures; they never require a content edit. Formulas you
1323
+ previously worked around may now be written directly.
1324
+
1325
+ - **A notation `formula` can use a manual delimiter size, an accent, a box, or
1326
+ a color.** `render-notation-math.ts`'s attribute allowlist is now KaTeX's
1327
+ whole inert presentation surface, so `\Bigl`/`\biggl`/`\Biggl`,
1328
+ `\xrightarrow`, `\boxed`, and `\colorbox` no longer throw
1329
+ `Unexpected KaTeX property`. `alt`, `href`, `src`, and `title` are explicitly
1330
+ denied; no attribute that can carry a URL is allowed through.
1331
+ - **`\vec` and every other accent renders.** The style sanitizer no longer
1332
+ requires a trailing `;` on the last CSS declaration. `\mathbf` remains the
1333
+ house style for a vector; `\vec` simply works now.
1334
+ - **A macro in a formula no longer invents a source span.** `\vdots`,
1335
+ `\varSigma`, `\argmax`, `\bmod`, `\R`/`\N`/`\Z` and 79 of KaTeX's 282 macros
1336
+ produce nodes whose `loc` indexes the macro body, which the glyph resolver
1337
+ read as a span over unrelated text — failing with `Unresolved notation ""`,
1338
+ or worse, silently splitting a formula mid-command. Fixed. An existing
1339
+ explicit `\explain{key}{\varSigma}` annotation still works and is still the
1340
+ way to bind a macro-expanded glyph, since it has no authored span for
1341
+ canonical matching to see.
1342
+ - **`equationAnchorId({ key })` type-checks without a `number`.** Its JSDoc
1343
+ required a property the function ignores when a `key` is given.
1344
+
1345
+ ### 10. Verify
1346
+
1347
+ ```
1348
+ pnpm add -D explico@0.8.0
1349
+ pnpm validate:content # twice — the first run regenerates build/resolution/*
1350
+ pnpm content check --changed
1351
+ pnpm content warnings # expect new delimiter-manual-size findings (item 3)
1352
+ astro check
1353
+ astro build
1354
+ ```
1355
+
1356
+ Commit the regenerated `build/resolution/*` (items 4 and 5 both change it).
1357
+
36
1358
  ## 0.6.0 → 0.7.0
37
1359
 
38
1360
  ### 1. Manifest schema is now v15: diagnostic severity has three levels
@@ -151,8 +1473,8 @@ needs to change to keep building. To clear the warnings, run
151
1473
  `pnpm content diagnostics warning`, and enlarge each named outer pair to the
152
1474
  next manual size without changing its delimiter shape. Rule N15's build-enforced
153
1475
  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).
1476
+ "sized". See [`notation.md#n17`](notation.md#n17) — and, for rule N16, which
1477
+ was later folded into N17, [`retired.md`](retired.md).
156
1478
 
157
1479
  ### 7. Rule P1: "or" never introduces a second name for the same thing
158
1480