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