explico 0.12.0 → 0.13.1

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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -33,6 +33,713 @@ required config block throws with the section of this file to read.
33
33
 
34
34
  ---
35
35
 
36
+ ## 0.13.0 → 0.13.1
37
+
38
+ **No course changes required.** This release has one fix, in the engine's
39
+ stylesheet: hovering a symbol in an equation, or in a card's formula, no longer
40
+ paints its highlight over the glyph to its left. No authoring rule,
41
+ `CourseConfig` field, Astro wiring, course script or CLI command changes. The
42
+ manifest schema stays v29.
43
+
44
+ ### 1. Update the dependency
45
+
46
+ ```bash
47
+ pnpm add -D explico@0.13.1
48
+ ```
49
+
50
+ **Coming from 0.12.0 or earlier:** apply "0.12.0 → 0.13.0" below first, and
51
+ install 0.13.1 wherever it says to install 0.13.0.
52
+
53
+ ### 2. Only if your course CSS or browser tests reach into the symbol highlight
54
+
55
+ ```bash
56
+ rg -n 'data-notation-key|is-notation-active|mix-blend-mode' src content tests
57
+ ```
58
+
59
+ No hit means nothing to do. Otherwise:
60
+
61
+ - **The highlight** of a hovered or active `[data-notation-key]` inside
62
+ `.katex-html`, and of one inside `.notation-panel-body [data-slot-formula]`,
63
+ has `mix-blend-mode: screen` (`multiply` under `:root[data-theme='light']`).
64
+ The background colour is still `--sl-color-accent-low`. A course rule that
65
+ overrides that background keeps working; one that also relies on the
66
+ highlight covering the glyph beside it no longer does.
67
+ - **Screenshots** of a hovered symbol need re-recording.
68
+
69
+ ### 3. Verify
70
+
71
+ ```bash
72
+ pnpm validate:content # run twice: the first run regenerates build/resolution/*
73
+ pnpm content check --changed
74
+ astro check
75
+ astro build
76
+ ```
77
+
78
+ Then run the course's browser tests, if it has any.
79
+
80
+ ## 0.12.0 → 0.13.0
81
+
82
+ **Course changes are required.** A course still on 0.11.1 applies
83
+ "0.11.1 → 0.12.0" below first. Each of these can fail a build that passed
84
+ on 0.12.0:
85
+
86
+ - the Astro, Starlight and `@astrojs/react` versions, which move with
87
+ explico (step 1);
88
+ - `editorialStatus` in any content file, now `retired-key` (step 2), and a
89
+ course's `scripts/content.ts` that prints the draft counts it fed (step 3);
90
+ - a course test that still passes rule S5's `conventions` (step 4);
91
+ - `lessonExports.standaloneHtml` without `canonicalBaseUrl` (step 5);
92
+ - faults that used to pass in silence and are now `validate:content`
93
+ errors, rule S6 among them (step 7);
94
+ - rule N23, an index is part of the symbol, and rule N4, an indexed entry
95
+ lends its card to nothing (steps 8–14);
96
+ - code a course owns that reads what was removed (step 15).
97
+
98
+ The manifest schema is now v31, by way of v26, v27, v28, v29 and v30 (step
99
+ 15). Fix every finding in the same change as the upgrade. The steps are one
100
+ pass, ordered so that no fix has to be redone. `rules.N23.enabled: false`
101
+ exists, but a course that ships with it off shows its readers the cards this
102
+ rule exists to fix, and the N4 failures of step 8 cannot be switched off.
103
+
104
+ ### 1. Update the dependencies together
105
+
106
+ explico 0.13.0 pins a newer Astro stack in its `peerDependencies`. Set these
107
+ five in the course's `package.json`, each in the block that declares it now,
108
+ then run `pnpm install`:
109
+
110
+ | Package | from | to |
111
+ | --- | --- | --- |
112
+ | `explico` | 0.12.0 | 0.13.0 |
113
+ | `astro` | 7.2.7 | 7.3.5 |
114
+ | `@astrojs/starlight` | 0.41.9 | 0.42.4 |
115
+ | `@astrojs/react` | 6.0.4 | 7.0.0 |
116
+ | `@astrojs/markdown-remark` | 7.2.4 | 7.3.1 |
117
+
118
+ Move the four Astro packages together: Starlight 0.42 requires `astro`
119
+ 7.2.10 and `@astrojs/markdown-remark` 7.3.0 or later. `astro.config.mjs`
120
+ needs no change unless it uses one of these:
121
+
122
+ - `react({ babel: … })`. `@astrojs/react` 7 removes the option: move those
123
+ plugins to `@rolldown/plugin-babel` in `vite.plugins`.
124
+ - Starlight's top-level `tagline`. It is removed, so delete it. A splash
125
+ page's `hero.tagline` is unaffected.
126
+
127
+ Starlight's mobile menu is now a `#starlight__sidebar` popover, opened by a
128
+ plain `button.sl-menu-button`. The `<starlight-menu-button>` element, its
129
+ `aria-expanded` and `body[data-mobile-menu-expanded]` are gone. Course CSS,
130
+ scripts or browser tests that target them match nothing; use
131
+ `.sl-menu-button` and `body:has(#starlight__sidebar:popover-open)`:
132
+
133
+ ```bash
134
+ rg -n 'starlight-menu-button|data-mobile-menu-expanded' src content tests
135
+ ```
136
+
137
+ Starlight 0.42 supports Chrome 116, Safari 17 and Firefox 125 or later.
138
+ `astro build` now prints `[WARN] [content] The collection "i18n" does not
139
+ exist or is empty` twice. The warning is harmless.
140
+
141
+ ### 2. Delete `editorialStatus` from every content file
142
+
143
+ The draft / in-review / reviewed flag is retired, and nothing reads or shows
144
+ it. A page, notation entry, abbreviation, competency, assessment, source or
145
+ track that still sets it fails `validate:content` with `retired-key`, once
146
+ per file. Run this from the course root. It deletes the key from Markdown
147
+ frontmatter and from JSON records, along with the comma left behind where
148
+ the key was a record's last:
149
+
150
+ ```bash
151
+ node --input-type=module <<'EOF'
152
+ import { readFileSync, writeFileSync } from 'node:fs';
153
+ import { execFileSync } from 'node:child_process';
154
+ const files = execFileSync('grep', ['-rl', 'editorialStatus', 'content'], { encoding: 'utf8' })
155
+ .split('\n').filter(Boolean);
156
+ for (const file of files) {
157
+ const out = [];
158
+ let fence = file.endsWith('.json') ? null : 0; // Markdown: only inside the frontmatter
159
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
160
+ if (fence !== null && line.trim() === '---') fence += 1;
161
+ const json = fence === null && /^\s*"editorialStatus"\s*:\s*"[^"]*"\s*,?\s*$/.test(line);
162
+ const yaml = fence === 1 && /^\s*editorialStatus\s*:/.test(line);
163
+ if (!json && !yaml) { out.push(line); continue; }
164
+ if (json && !line.trimEnd().endsWith(',')) {
165
+ // The key was the record's last: drop the comma the key before it left.
166
+ const i = out.findLastIndex((l) => l.trim() !== '');
167
+ out[i] = out[i].replace(/,(\s*)$/, '$1');
168
+ }
169
+ }
170
+ writeFileSync(file, out.join('\n'));
171
+ if (file.endsWith('.json')) JSON.parse(out.join('\n'));
172
+ }
173
+ console.log(`editorialStatus removed from ${files.length} files`);
174
+ EOF
175
+ grep -rn editorialStatus content
176
+ ```
177
+
178
+ The final `grep` must print nothing. In `git diff`, each file loses one line,
179
+ and a JSON record whose last key it was also loses one comma. A course test
180
+ that builds notation, lesson or source inputs drops their `status` /
181
+ `editorialStatus` too (step 15).
182
+
183
+ ### 3. The course's `scripts/content.ts`
184
+
185
+ `tsx` does not type-check, so a field the engine no longer returns throws at
186
+ run time. In the course's own `content` wrapper:
187
+
188
+ - `content status`: drop the draft counts. `report.draftDebt` is gone, so
189
+ `report.draftDebt.lessons.length` and `report.draftDebt.notation.length`
190
+ throw.
191
+ - `content context`: drop `report.lesson.status`.
192
+ - `content check`: pass every id, `contentCheck(manifest, { lessons:
193
+ args.positionals, changed: args.changed })`. `lesson` still works, but it
194
+ checks only the first id: `content check a b` exits 0 having never
195
+ looked at `b`.
196
+
197
+ Optionally, as explico's playground now does, fail on an argument a command
198
+ does not take (`status`, `rules` and `warnings` take none; `context` and
199
+ `new` take one).
200
+
201
+ ### 4. Rule S5 is retired: delete `conventions`
202
+
203
+ Rule S5 (`convention-single-definition`) is gone, and so is the
204
+ `conventions` setting it read (`signConventionKey`,
205
+ `cashFlowPerspectiveCompetency`). Its check knew one finance course's wording
206
+ and never fired in any course. A convention is a definition, so rule N4 now
207
+ covers it: the convention's notation entry names the other choice in its
208
+ `meaning` and says how to convert in its body (notation.md, rule N4, "A
209
+ definition that is a convention").
210
+
211
+ - Delete the `conventions` block from `content/course.config.ts`, or from
212
+ `course.settings.json` and its schema where the course keeps its settings
213
+ there. Until then it does nothing, and `validate:content` warns
214
+ `config-deprecated-key`. The type keeps it, as a deprecated `unknown`,
215
+ only until the next MINOR.
216
+ - Delete a `rules.S5` block (it warns `config-retired-rule`) and any
217
+ `lint-ignore.yml` line for `convention-single-definition`, which now
218
+ silences nothing.
219
+ - A course test that calls `runConsistencyChecks` drops `catalog` and
220
+ `conventions` from its input. The check that read them is gone, and an
221
+ object literal that still passes them is a type error.
222
+ - Optional: on the entry that was the course's `signConventionKey`, and on
223
+ any other entry that is a convention, add the other choice to `meaning`
224
+ and the conversion to the body. That is a notation edit, so it gets a
225
+ `content/CHANGELOG.md` bullet like any other (rule C1).
226
+
227
+ ### 5. `lessonExports.canonicalBaseUrl` with standalone HTML
228
+
229
+ With `lessonExports.standaloneHtml` on, each part and the whole course now
230
+ also download as one offline HTML file: `downloads/part/<dir>.html` (the
231
+ part's page, then its chapters) and `downloads/course.html` (the landing
232
+ page, the curriculum map, the figures and tables, every part, the glossary).
233
+ An exported page that links elsewhere in the course fails the build without
234
+ a base URL, as a chapter always did, and a splash hero's actions always
235
+ link. Such a course sets its deployed root:
236
+
237
+ ```json
238
+ "lessonExports": {
239
+ "standaloneHtml": true,
240
+ "print": true,
241
+ "canonicalBaseUrl": "https://example.org/my-course/"
242
+ }
243
+ ```
244
+
245
+ A page that is not a chapter has no download of its own. Its content is in
246
+ its part's file and the course's. A course link into `downloads/` points at
247
+ `<slug>.html` for a chapter, `part/<dir>.html` or `course.html`.
248
+
249
+ ### 6. List the work
250
+
251
+ ```bash
252
+ pnpm validate:content; pnpm validate:content
253
+ pnpm content diagnostics error
254
+ pnpm content warnings
255
+ ```
256
+
257
+ Read the second `validate:content`; the first regenerates
258
+ `build/resolution/`. Step 7 covers the new errors that are not about
259
+ indices. Every N23 finding names its rule, the form, and the fix:
260
+ `index-explained`, `index-swallowed`, `index-range`, `derived-key-taken`
261
+ (errors), `accent-swallowed` (a rule N22 warning), and `unresolved-glyph`
262
+ lines whose message names "an indexed entry". Lab and diagram math only
263
+ exists as the page renders, so `astro build` reports it (step 13). Run that
264
+ once the content checks pass. To see the list before upgrading, run the
265
+ engine repository's dry run from its `playground/` directory in a checkout:
266
+ `node_modules/.bin/tsx ../scripts/agent/index-dry-run.ts <course-dir>`. It
267
+ leaves the course untouched (its report goes under the engine's
268
+ `.agent/index-dry-run/`). It covers lesson bodies, notation formulas,
269
+ assessments and `unresolved-glyph`, but not `accent-swallowed`, labs or
270
+ diagrams.
271
+
272
+ ### 7. Faults that now fail `validate:content`
273
+
274
+ Each of these used to pass and then show readers something wrong, or fail
275
+ `astro build` with no file or line. Each error names its file and line. Most
276
+ courses have few of them, or none:
277
+
278
+ | Code | What fails | Fix |
279
+ | --- | --- | --- |
280
+ | `markup-in-prop` | `$…$`, `[[…]]` or `[@…]` in a quoted prop (`short="Speed $v$"`) | write the symbol in Unicode or words; reference or cite from the prose beside the component |
281
+ | `mdx-expression` | `{a}` in prose, which MDX runs as JavaScript | `\{a\}`, or `$\{a\}$` for a set |
282
+ | `eq-label-repeated` | a second `\label{eq:…}` in one `$$…$$` block | give each equation you refer to its own block |
283
+ | `eq-latex-ref` | `\begin{align}`, `\begin{equation}`, `\tag`, an unprefixed `\label`, `\eqref` or `\ref` | `aligned` / `gathered` / `alignedat` inside the block, `\label{eq:key}`, `[[eq-key]]` |
284
+ | `invalid-key` | a notation key starting `eq-`, `tab-`, `fig-`, `dia-`, `ex-`, `check-`, `sec-` or `memo-` | rename it: a MAJOR bump under rule C2 |
285
+ | `heading-nested` | a worked-example set or a knowledge check inside a `<Memo>` or `<ShortNote>` (rule S6) | move it out, after the memo |
286
+ | `tab-several-tables` | one `<Table>` holding two tables | one `<Table>` each |
287
+ | `citation-in-link` | `[as measured in [@id]](https://…)` | cite after the link |
288
+ | `heading-anchor-collision` | a heading whose anchor something else holds: `## References` on a page that cites, `## Tab totals` beside `<Table id="totals">` | reword the heading |
289
+ | `currency-dollar` | `pay $5 ~ $6`, which remark-math reads as math | `\$5 to \$6` |
290
+ | `malformed-reference` | an unclosed `[[` | close it |
291
+ | `invalid-assessment` | a `[[…]]` or `[@…]` in a knowledge check's prompt, explanation or option, which reached students as typed | say it in words: a check renders text and math, not references or citations |
292
+ | `unsupported-key` | `draft: true` on a lesson | keep an unfinished lesson outside `content/docs/` |
293
+
294
+ Two more fail `astro build`: a `<Table>` or `<Figure>` inside a `:::`
295
+ aside (the message names the aside; move the float out), and a `<Diagram>`
296
+ edge from a node to itself (the message says how to draw the repetition).
297
+
298
+ A knowledge check's option label now renders its math, so rule N4 checks
299
+ it: a symbol in `"$30^{\circ}$"` needs a card, as in the prompt. Rule N9
300
+ still does not apply to a label ("30 degrees" stays valid). A declared
301
+ operator now binds only its own card (rule N8). A `\operatorname{Cov}` that
302
+ silently opened the page's `\operatorname{Var}` card has no card now: give
303
+ it one, or add it to `rules.N8.allow`. A name a course put on
304
+ `rules.N8.allow` only because its card was refused can come off the list,
305
+ with the card restored.
306
+
307
+ New warnings, worth clearing in the same pass:
308
+
309
+ - `local-shadows-shared`: a `notation.local` key equal to a shared one.
310
+ Rename the local key.
311
+ - `fig-unread-prop` (and the `tab-`, `dia-` and `ex-` variants): a caption
312
+ prop written as an expression, `short={"…"}`. Quote it, `short="…"`.
313
+ - `ignored-key`: a chapter's `sidebar.order`, which does nothing. Delete it.
314
+ - `part-empty`: a part listed in `parts` before its directory exists. Fine
315
+ while the part is planned.
316
+ - `lab-picture-undrawn` and `dia-too-wide` (step 13), and
317
+ `accent-swallowed` (step 14).
318
+
319
+ ### 8. A bare symbol that borrowed an indexed entry's card (rule N4)
320
+
321
+ An entry with a subscript lends its card to no bare symbol now. A family
322
+ (`\alpha_i`, "ARCH coefficient") gives it to its own instances only
323
+ (`\alpha_1`, `\alpha_q`); a fixed label (`H_0`, `v_0`, `T_{\mathrm{opt}}`)
324
+ to nothing. The bare symbol fails `unresolved-glyph`, and the message names
325
+ the entry it shared its base with. For each (page, symbol):
326
+
327
+ - **A different object** — the usual case: a learning rate `\alpha` beside
328
+ ARCH coefficients `\alpha_i`, an AR(1) coefficient `\beta` beside `\beta_i`,
329
+ a response `Y` beside observations `Y_i`. Give it its own entry, page-local
330
+ when only that page uses it:
331
+
332
+ ```yaml
333
+ notation:
334
+ local:
335
+ - key: learning-rate
336
+ latex: \alpha
337
+ label: Learning rate
338
+ meaning: The weight the newest observation gets in the exponentially weighted average.
339
+ dimensionless: true
340
+ ```
341
+
342
+ - **Shorthand for the family**, the indexed symbol with the index left off:
343
+ wrap it, `\explain{arch-coefficient}{\alpha}`.
344
+
345
+ - **Another member of a fixed-label set**: `H_1` borrowed the `H_0` card
346
+ ("Null hypothesis"). Give it its own entry, or introduce the one the course
347
+ has (`[[alternative-hypothesis]]`).
348
+
349
+ A function form still lends by its head: `D(0,t)` explains a bare `D`, and
350
+ `V_0(1_t)` explains `V_0(1_2)`, but no longer a bare `V`.
351
+
352
+ ### 9. `index-swallowed`: an `\explain` around an indexed symbol
353
+
354
+ `\explain{motion.velocity}{v_x}` made the whole `v_x` one target with the
355
+ velocity's card, which says nothing about `x`. Find them all:
356
+
357
+ ```bash
358
+ rg -n '\\explain\{[^}]+\}\{\\?[A-Za-z]+(\{[^}]*\})?_' content
359
+ ```
360
+
361
+ For each reported wrap, either drop it and explain the subscript the way
362
+ step 10 does (an index entry `{}_x` turns `v_x` into one derived card,
363
+ "Horizontal component of velocity"), or point the wrap at an entry for the
364
+ indexed symbol itself. A wrap whose key's `latex` has an index
365
+ (`\explain{launch-speed}{v_0}`) is fine.
366
+
367
+ ### 10. `index-explained`: whole form, family or index entry
368
+
369
+ A subscript only the base's card covered — a digit, a `\mathrm{}` label, a
370
+ base-library `i`, `e` or `d` used as an index — needs one of three, in this
371
+ order of precedence:
372
+
373
+ 1. **Whole form**, for a named quantity: an entry with `latex:
374
+ \mathbf{v}_0`, "Initial velocity". It also wins under a power
375
+ (`\mathbf{v}_0^{2}`), so an `\explain{key}{v_0}^{2}` workaround can go.
376
+ 2. **Family**, for an index that runs: `latex: x_k`, "k-th component". It
377
+ explains every instance with the same number of comma-separated slots
378
+ (`x_1`, `x_{k+1}`, `x_N`), ahead of a plain `x` entry, but not a fixed
379
+ label (`x_{\mathrm{e}}`). Two families on one base that both fit an
380
+ instance fail `ambiguous-base-fallback`: keep one per page, or
381
+ `\explain` the instance. An entry like `v_x` whose index is a label is no
382
+ family: mark it `fixedIndex: true`.
383
+ 3. **Index entry**, for a subscript with one meaning on the page — usually
384
+ page-local, since `x_0` may be a starting point on one page and a centre
385
+ on another:
386
+
387
+ ```yaml
388
+ notation:
389
+ local:
390
+ - key: initial
391
+ latex: "{}_0" # quoted: a bare {} starts a YAML mapping
392
+ label: Initial value
393
+ meaning: The value of a quantity at the start, when time is zero.
394
+ reads: "{base} evaluated at t = 0" # optional
395
+ formula: "{base}_0 = {base}\\,(\\explain{time}{t} = 0)" # optional
396
+ ```
397
+
398
+ Every symbol of the page that has a card of its own then shows `x_0` as
399
+ one derived card, titled from `reads` ("Velocity vector evaluated at
400
+ t = 0"; the default is "Initial value of velocity vector"), with the
401
+ index entry's meaning and the base's units. An index entry takes no
402
+ `units`, `dimensionless`, `range` or `fixedIndex` (the loader fails on
403
+ them), and rule N19 skips it. `reads` belongs to an index entry only; it
404
+ names the symbol with `{base}` and is plain prose, with no `$`, `\` or
405
+ backtick.
406
+
407
+ An index letter with its own card explains its slot, arithmetic included
408
+ (`X_{t-1}`); a base-library letter does not, so `x_i` needs an `i` card (an
409
+ index letter, with `range`, step 11) or a family. A digit beside a carded
410
+ letter in another slot (`F_{0,T}`) needs a family (`F_{t,T}`) or an index
411
+ entry. When the message says the base has no card either (an indicator
412
+ `\mathbf{1}_{\{k=n\}}`), give the base an entry.
413
+
414
+ ### 11. `index-range`: the set a running index runs over
415
+
416
+ A running index is a lowercase letter in a subscript (`i`, `k`, `t`) that
417
+ the page does not declare fixed. State its set once, in one of three ways:
418
+
419
+ - `range` on the letter's card, or on the family entry over it — one edit
420
+ for every page that card is on:
421
+
422
+ ```yaml
423
+ key: time.index
424
+ latex: t
425
+ range: "1, \\dots, T"
426
+ ```
427
+
428
+ - after a `\quad` on the first display line where the letter is an index,
429
+ for a set that belongs to the page: `x_i \sim \mathcal{N}(\mu, \sigma^2),
430
+ \quad i = 1, \dots, n` (not `\qquad`, which is rule N18's reason slot);
431
+ - `fixedIndex: true` on the letter's card when the letter is a label there
432
+ (a component `x`, a group `g`), not a position in a range.
433
+
434
+ Uppercase and Greek letters in a subscript (`X_N`, `p_\theta`) are sizes and
435
+ parameters: they need a card, never a range.
436
+
437
+ ### 12. Notation formulas and assessments
438
+
439
+ `validate:content` reports the same `index-explained` and `index-swallowed`
440
+ in a `formula:` of `content/notation/*.md` or `notation.local`, and in an
441
+ assessment's prompt, explanation or option. In an assessment, fix it as in
442
+ a lesson: the assessment renders with its lesson's notation, derived cards
443
+ included. A formula resolves against its own card's table, where an index
444
+ entry does not compose; explain the indexed symbol there with a whole form —
445
+ `\explain{launch-speed}{v_0}`, or a gloss on the entry:
446
+
447
+ ```yaml
448
+ glosses:
449
+ - latex: y_{\mathrm{new}}
450
+ name: yield after the shift
451
+ ```
452
+
453
+ — or wrap the subscript in the index entry's key,
454
+ `\mathbf{v}_{\explain{initial}{0}}`, which gives the `0` that card.
455
+
456
+ ### 13. Labs and diagrams: index math, a drawn picture, a width
457
+
458
+ **Rule N23 at render.** `astro build` fails with "Lab math has an index no
459
+ card explains (rule N23, <context>)" or "Diagram … has an index no card
460
+ explains (rule N23)", naming the form and the template. Fix the page's
461
+ notation as in steps 8–11. An index entry on the page gives the lab's and
462
+ the diagram's symbols their derived cards too, with no code change, as long
463
+ as the lab passes the table `labMathScope` returned straight to
464
+ `renderLabMath`. A wrapper that copies it (`[...scope]`) gets neither the
465
+ cards nor the check, so pass it unchanged.
466
+
467
+ **A lab's picture is in the built page (rule V3).** `astro build` warns
468
+ `lab-picture-undrawn` for a figure whose picture is an island that draws
469
+ nothing until the browser runs it. A card from another page, print without
470
+ JavaScript, and the HTML, LaTeX and Markdown exports all show an empty box
471
+ there. Draw the lab's default picture at build time in its `.astro` wrapper
472
+ and pass it to the island as children. The island shows them until it has
473
+ drawn into `.lab-chart-live`, and explico's stylesheet hides them from then
474
+ on. With Observable Plot, add `linkedom` as a course devDependency and draw
475
+ with the island's own options at its defaults:
476
+
477
+ ```ts
478
+ // content/labs/still-plot.ts — import it only from .astro frontmatter,
479
+ // so linkedom stays out of the island's bundle
480
+ import * as Plot from '@observablehq/plot';
481
+ import { parseHTML } from 'linkedom';
482
+
483
+ export function stillPlot(options: Plot.PlotOptions): string {
484
+ const { document } = parseHTML('<!doctype html><html><head></head><body></body></html>');
485
+ return (Plot.plot({ ...options, document }) as unknown as Element).outerHTML;
486
+ }
487
+ ```
488
+
489
+ ```astro
490
+ ---
491
+ const still = stillPlot(rangePlotOptions(DEFAULTS.speed, DEFAULTS.angle));
492
+ ---
493
+ <RangePlot client:visible><Fragment set:html={still} /></RangePlot>
494
+ ```
495
+
496
+ ```tsx
497
+ // the island: show the build's copy until the live plot is in
498
+ <div className="lab-chart">
499
+ {drawn ? null : children}
500
+ <div className="lab-chart-live" ref={chartRef} />
501
+ </div>
502
+ ```
503
+
504
+ Set `drawn` in the effect that puts the plot into `chartRef`.
505
+
506
+ **A diagram's width (rule V8).** Diagram boxes are now sized from their
507
+ label and formula, so a diagram can change size; re-shoot any screenshot of
508
+ one. A diagram wider than the lesson column, 707px unless the course sets
509
+ `rules.V8.columnWidth`, warns `dia-too-wide`. It used to be an untagged
510
+ console line, above 800px. Shorten its labels, split it, or lay it out
511
+ vertically. `warnAboveWidth={false}` still opts one diagram out, and a
512
+ number there sets that diagram's own limit. A node's `width` alone now takes
513
+ effect, with the height following, so a `height` that was there only to
514
+ make `width` work can go.
515
+
516
+ ### 14. `accent-swallowed` and `derived-key-taken`
517
+
518
+ `accent-swallowed` (rule N22, a warning) is the hat's twin of step 9:
519
+ `\explain{beta}{\hat{\beta}}` names a card that says nothing about the hat.
520
+ Wrap only the letter, `\hat{\explain{beta}{\beta}}`, and explain the hat
521
+ (an operator entry `\hat{}`, or `rules.N22.allow`), or name an entry for
522
+ `\hat{\beta}`. `derived-key-taken` means an authored key has the form
523
+ `<entry>.sub.<index entry>`, which the build uses for derived cards: rename
524
+ it (a MAJOR bump under rule C2, step 20).
525
+
526
+ ### 15. Code a course owns
527
+
528
+ - **Manifest v31.** `MANIFEST_SCHEMA_VERSION` goes from 25 to 31, so a
529
+ course test that pins it by number moves to 31. Along the way:
530
+ - v26 adds `cards`, the course's card settings with defaults applied;
531
+ - v27 adds `lessonExports.latex` and `lessonExports.latexPaper`, and gives
532
+ every `assessmentContent` item its `type`, a choice item its `options`
533
+ and `correctOptionId`, and a numeric item its `answer`;
534
+ - v28 adds `lessonExports.markdown`;
535
+ - v29 removes `lessons[].status`, the `editorialStatus` of the notation
536
+ definitions under `lessons[]` and `pages[]`, and the `status` of
537
+ `notation.definitions[]` and `notation.bundles[]`;
538
+ - v30 makes `lessons[].headings[].text` the text a reader sees (a formula
539
+ as it reads, a `[[key]]` term as its label), marks a heading with math
540
+ `math: true`, and gives each choice option `labelSegments`;
541
+ - v31 adds `derived`, the page's derived cards, to `lessons[].notation`
542
+ and `pages[].notation`. A notation definition carries `reads`, `range`,
543
+ `fixedIndex` and `formulaTemplate` when set, and a bundle definition
544
+ carries `range`.
545
+ - **Removed exports.** `EditorialStatus` and the `status` field of
546
+ `SharedNotationDefinitionInput`, `NotationLessonInput`, the notation
547
+ definition records and `NotationPageBundle` (`explico/reference/types`);
548
+ `LessonDefinition.status` and `SourceDefinition.editorialStatus`
549
+ (`explico/curriculum/validation`); `NotationLayer`'s per-definition
550
+ `editorialStatus`; `CourseConventions`. A course test that builds these
551
+ inputs drops `status`. `estimateBoxSize` is gone from
552
+ `explico/reference/diagram-layout`; size a node with
553
+ `nodeBoxSize({ text, formula: measureFormula(latex, fontPx) })`
554
+ (`measureFormula` from `explico/reference/katex-measure`).
555
+ - **`runConsistencyChecks`** no longer takes `catalog` or `conventions`
556
+ (step 4).
557
+ - **Diagnostic codes.** `review-state` and `alignment-review-state` are gone.
558
+ A lesson that `teaches` but declares no `assumptions` now reports
559
+ `missing-assumptions`. Rename either code where a course test or
560
+ `lint-ignore.yml` names it.
561
+ - **`resolveMathGlyphs`** returns `indices` and `derived` and takes a
562
+ `derive` option. A course test that compares its whole result with
563
+ `toEqual` sees an unresolved item carry `lenders` and a decoration carry
564
+ `wholeFormExplicit`.
565
+ - **The `content` CLI's JSON** loses `draftDebt` and `lesson.status` (step
566
+ 3). Its lists now include the last build's render findings (from
567
+ `build/render-diagnostics.json`) under a `render:` source, such as
568
+ `render:dia-too-wide`.
569
+ - **A card layer of the course's own** (rare) implements the new
570
+ `CardSource.build(item, page)` and `CardSource.awayItem(item, page)` from
571
+ `card-manager.ts`, which carried cards need.
572
+
573
+ ### 16. Course CSS and browser tests
574
+
575
+ Selectors and accessible names that moved:
576
+
577
+ ```bash
578
+ rg -n 'float-caption__anchor|keyed-equation__(controls|copy)|float-ref-panel-jump|explico-keep-left|(notation|glossary|citation|citation-panel)-status|Keep the article left|this lesson|Lesson exports|Go to (figure|equation|table|diagram) |data-card-key="notation:' src tests
579
+ ```
580
+
581
+ - **A float's chain icon is gone**: `a.float-caption__anchor` no longer
582
+ exists on figures, diagrams or tables. Hover or focus the float's number,
583
+ then use `a.float-toolbar__link`, which carries the same
584
+ `data-copy-link`. An equation's `button.keyed-equation__copy` is now a
585
+ child of `.keyed-equation`, not of `.keyed-equation__controls`.
586
+ - **A page-local symbol's card key is scoped to its page**:
587
+ `[data-card-key="notation:theta-star@projectiles/optimal-launch-angle"]`,
588
+ not `notation:theta-star`. A shared symbol's key is unchanged.
589
+ - **A reference card names its float in the title**: "Equation (1.1.3)",
590
+ "Figure 2.2.1 · …". `[data-panel-number]` moved into the title; an
591
+ equation's now reads "Equation (1.1.3)" rather than "(1.1.3)". The
592
+ "Go to …" link drops the number ("Go to the equation in Position and
593
+ Coordinate Systems", "Go to the table") and sits in its own row,
594
+ `p.float-ref-panel-links`. CSS that placed `.float-ref-panel-jump` as a
595
+ grid item of `.float-ref-panel-body` targets the row instead.
596
+ - **The side column's switches** share `.explico-side-switch` (knob
597
+ `.explico-side-switch-knob`) in place of `.explico-keep-left` and
598
+ `.explico-keep-left-switch`, inside a new row, `.explico-side-controls`,
599
+ which now holds the fixed position. "Keep the article left" reads
600
+ "Article left" (its tooltip is unchanged). `[data-keep-article-left]` is
601
+ unchanged, and "Carry cards" is `[data-carry-cards]`.
602
+ - **Page exports.** The group "Lesson exports" is "Page exports", and the
603
+ print button is "Print or save this page as PDF". The download link
604
+ "Download this lesson as standalone HTML" is now a "Download this page"
605
+ button that opens a menu. Its links are named by scope and format,
606
+ "Download this chapter as standalone HTML" ("… this part …", "… the whole
607
+ course …"), with the file's size appended in parentheses.
608
+ - **Status hooks are gone**: `.notation-status`, `.glossary-status`,
609
+ `.citation-status` and `.citation-panel-status`. A glossary entry's header
610
+ has two columns, not three.
611
+ - **Starlight's mobile menu**: see step 1.
612
+
613
+ Visual baselines move too: card text is 14px, a figure card's picture is 80%
614
+ of its width in the article, and diagram boxes are measured (step 13).
615
+
616
+ ### 17. Rule V7: a test that each figure fits on one screen
617
+
618
+ Rule V7 holds a `<Figure>`'s picture (plot, key and tables) to 80% of the
619
+ viewport height, or `rules.V7.maxPercent`, so the reader sees it with its
620
+ caption line. Only a laid-out page knows the height, so the check runs in
621
+ the course's Playwright suite. Add it there, at each viewport the course
622
+ supports:
623
+
624
+ ```ts
625
+ import { readFileSync } from 'node:fs';
626
+ import { expect, test } from '@playwright/test';
627
+ import { checkFigureFit, lessonsWithFigures } from 'explico/reference/figure-fit';
628
+
629
+ const manifest = JSON.parse(readFileSync('build/manifest.json', 'utf8'));
630
+
631
+ for (const path of lessonsWithFigures(manifest)) {
632
+ test(`${path} figures fit on one screen (rule V7)`, async ({ page }) => {
633
+ await page.setViewportSize({ width: 1280, height: 800 });
634
+ await page.goto(path);
635
+ const findings = await checkFigureFit(page);
636
+ expect(findings.map((finding) => finding.message)).toEqual([]);
637
+ });
638
+ }
639
+ ```
640
+
641
+ Pass `{ maxPercent }` when the course sets `rules.V7.maxPercent`, and
642
+ `{ allow: { 'fig-…': '<reason>' } }` for a picture meant to be scrolled.
643
+ Shorten a figure that fails, or give a square or tall plot a fixed width
644
+ (visualizations.md, rule V7).
645
+
646
+ ### 18. Optional: what a course can turn on
647
+
648
+ - **LaTeX source and PDF** of every chapter, each part and the course:
649
+ `lessonExports.latex: true`, and `lessonExports.latexPaper: "a4"` for A4
650
+ (the default is `letter`). This needs TeX Live 2025 or later with
651
+ `latexmk` (LuaLaTeX by default; `EXPLICO_LATEX_ENGINE=pdflatex` switches
652
+ engines) and Playwright's Chromium for the figures a browser draws
653
+ (`pnpm exec playwright install chromium`). The TeX Live package list,
654
+ which includes `float` and an optional `cm-unicode`, ships at
655
+ `node_modules/explico/src/compiler/exports/latex/texlive-packages.txt`.
656
+ When `CI` is set, or with `EXPLICO_LATEX_STRICT=1`, a missing engine or
657
+ browser fails the build. Elsewhere the build warns, still writes the
658
+ source zips, and leaves placeholders for uncaptured figures.
659
+ - **Markdown** of every chapter, each part and the course:
660
+ `lessonExports.markdown: true`.
661
+ - **Carry cards from the start**: `cards.carry: true` starts every reader
662
+ with the switch on. A reader's own choice still wins.
663
+
664
+ A course that keeps its settings in `course.settings.json` against a schema
665
+ of its own with `additionalProperties: false` adds `cards`,
666
+ `lessonExports.latex`, `lessonExports.latexPaper` and
667
+ `lessonExports.markdown` to that schema first, each with a `title` and a
668
+ `description`. `rules.V7.maxPercent` and `rules.V8.columnWidth` are in the
669
+ engine's generated rules schema already.
670
+
671
+ ### 19. What readers see without a content edit
672
+
673
+ None of this needs a course change, but the course's changelog says what
674
+ readers now see (step 20):
675
+
676
+ - **Cards.** Each float's number has a toolbar: Pin, Dock, Copy link. A
677
+ docked card can be widened, and a width menu sizes them all. "Carry cards"
678
+ keeps kept cards on screen and brings them to every page, each with an
679
+ "Opened at" link back. A symbol in a card from another lesson opens that
680
+ lesson's card. A reference card is titled by its number, a figure card's
681
+ picture is 80% of its width in the article, every card's text is 14px, and
682
+ footer links are outline buttons.
683
+ - **Touch screens** get a text size control (100% to 70%), rendered by
684
+ `LayoutSocialIcons.astro`, which the course already wires as its
685
+ `SocialIcons`.
686
+ - **Every page** has the print button and the Download menu, and Ctrl+P
687
+ loads the labs first.
688
+ - **Heading anchors with math read as the heading**:
689
+ `#the-range-explainmotionranger-in-a-heading` is now
690
+ `#the-range-r-in-a-heading`. A heading whose math has no markup keeps its
691
+ anchor. `validate:content` names a `[[sec-…]]` that used the old one; a
692
+ link from outside the course needs updating by hand.
693
+ - **Numbering.** An equation or a float in a list item or a `>` quote now
694
+ counts on the page as in the manifest, and one in a `:::` aside never
695
+ counts, so a later number can shift. `[@a; @b]` cites both sources, so a
696
+ reference list can gain an entry and renumber. A table one row over
697
+ `truncateAfter` shows whole.
698
+ - **The rules read what a reader sees.** Rule S1 measures a heading's
699
+ readable text, so `\explain{…}{…}` no longer counts against the limit and
700
+ an `explico-allow S1` waiver kept for that reason can go. Rule N9 and rule
701
+ N14 no longer read a link's address, and rule N9 quotes a hand-typed
702
+ sub/superscript whole. A course's own rule (`courseRules`) now matches a
703
+ link's text rather than its address, and matches the text of a
704
+ `caption`, `short`, `title` or `label` prop as a paragraph of its own: a
705
+ `forbid` pattern can newly fail on a caption. A section reference to a
706
+ part's page says why it cannot resolve (rule S10).
707
+ - **Notation.** Two notation formulas may name each other, local or shared;
708
+ a workaround for a local `reference-cycle` can go. A whole form survives a
709
+ power (`v_0^{2}`, step 10).
710
+ - **The LaTeX book and PDF** grow nested `\left … \right` brackets outward
711
+ as the website does (rule N17), link a formula's notation markers to the
712
+ notation appendix, number citations as the page does, and set Greek,
713
+ Cyrillic, CJK and emoji prose from fallback fonts.
714
+ - **Smaller fixes**: Copy LaTeX keeps a space after a control word, search
715
+ indexes a formula once, a wide equation fades at its cut edge, the splash
716
+ hero prints its light image, and a pinned card waits while its section is
717
+ folded.
718
+
719
+ ### 20. Record and check
720
+
721
+ Cards changed without a content edit (an instance now opens its family's
722
+ card, a power no longer breaks a whole form, a bare symbol lost a borrowed
723
+ card), and so did what step 19 lists. Add a `content/CHANGELOG.md` bullet
724
+ (rule C1) that says what readers now see, and bump `about.version` (rule C2:
725
+ MINOR for new entries, MAJOR if a notation key was renamed, as in steps 7
726
+ and 14). Then:
727
+
728
+ ```bash
729
+ pnpm validate:content; pnpm validate:content
730
+ pnpm content check --changed
731
+ pnpm exec astro check
732
+ pnpm exec astro build
733
+ pnpm test # unit tests (step 15)
734
+ pnpm test:e2e # browser tests, if the course has them (steps 16–17)
735
+ ```
736
+
737
+ `validate:content` must report no `retired-key`, `index-*`,
738
+ `derived-key-taken` or `unresolved-glyph` error, and no
739
+ `config-deprecated-key` warning. `astro build` must report no lab or diagram
740
+ N23 finding. Each `lab-picture-undrawn` and `dia-too-wide` it prints is
741
+ either fixed or deliberate.
742
+
36
743
  ## 0.11.1 → 0.12.0
37
744
 
38
745
  **Course changes may be required.** Four changes can fail a build that