explico 0.13.1 → 0.14.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 (239) hide show
  1. package/CHANGELOG.md +879 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1045 -16
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +15 -7
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -16,16 +16,24 @@ The pattern every time:
16
16
 
17
17
  1. Record the currently installed version before changing it:
18
18
  `node -p "require('explico/package.json').version"`.
19
- 2. `pnpm add -D explico@<version>`.
20
- 3. Read this file from the newly installed package and apply **every** section
19
+ 2. `pnpm add explico@<version>`, without `-D`: pnpm then updates the block that
20
+ already lists `explico`, `dependencies` or `devDependencies`. `-D` would move
21
+ the entry into `devDependencies`.
22
+ 3. `pnpm content migrate` prints exactly those sections: from the version
23
+ `package.json` pinned at HEAD to the one now installed, oldest first
24
+ (`--from` and `--to` override either end), and refuses when a section is
25
+ missing. Apply **every** section it prints. Reading this file whole instead
26
+ costs forty thousand tokens for the one or two sections an upgrade needs.
27
+ Without the verb, apply every section
21
28
  after the old version through the target version, oldest first. Do not skip a
22
29
  section merely because its changelog entry is not labelled **Breaking**.
23
30
  4. Make every required course-side change named by those sections, including
24
31
  changes under `content/`, Astro/source configuration, and course scripts; do
25
32
  not stop after changing the dependency declaration.
26
- 5. `pnpm validate:content` (run it **twice** — the first run regenerates
27
- `build/resolution/*` and reports it stale; the second is clean), then
28
- `pnpm content check --changed`, then `astro check`, then `astro build`.
33
+ 5. `pnpm validate:content` (it passes on its first run: it rewrites
34
+ `build/resolution/*` and fails only on a report git tracks that it had to
35
+ rewrite), then `pnpm content check --changed`, then `astro check`, then
36
+ `astro build`.
29
37
  Each stage names what it doesn't like; fix and re-run.
30
38
 
31
39
  The engine never fails silently on a migration it can detect: a missing
@@ -33,18 +41,1011 @@ required config block throws with the section of this file to read.
33
41
 
34
42
  ---
35
43
 
44
+ ## 0.13.1 → 0.14.0
45
+
46
+ **One course change is required, two are recommended, and a rare page needs
47
+ one.** Required: run `pnpm validate:content` once and commit the
48
+ `content/versions.json` it writes, the record of each page's own version
49
+ (rule C5, step 10); in CI the build fails until it is committed. Recommended:
50
+ `astro.config.mjs` keeps working as it is, but Astro 7.3 deprecates the flat
51
+ `markdown.remarkPlugins`, `markdown.rehypePlugins` and `markdown.remarkRehype`
52
+ (every `astro` command prints "Pass them to `unified({...})` from
53
+ `@astrojs/markdown-remark` directly instead.", and `astro check` reports a
54
+ `ts(6385)` hint wherever course code reads them), and its default processor is
55
+ now Sätteri, which runs neither list. Until the lists are removed Astro moves
56
+ them onto a `unified()` processor for the course. The manifest schema is now
57
+ v32 (step 10). **One new error can appear:** rule N4 now reads the math in a notation
58
+ entry's body, and the build fails on an entry whose body did not render
59
+ (step 9); no known course has one. The rest is optional or rare: a package
60
+ that now loads, a prop on `<CurriculumMap>`, four glyph-resolver fixes (step 4), one of which can turn
61
+ an ambiguous `x_{12}` into an `ambiguous-base-fallback` error, lab fixes
62
+ (step 5), and, only for a course whose CSS or browser tests reach into a
63
+ diagram's markup, the phone's text size or the `[?]` panel, a diagram that now
64
+ shrinks with the phone's text size inside a new frame element (step 6), a
65
+ contrast fix for an active term link (step 7), and a new tooltip on the
66
+ Download button (step 8). A course with the LaTeX export can turn on a
67
+ compile cache (step 13); `scripts/content.ts` and `scripts/compile-manifest.ts`
68
+ become three-line shims over the engine, which brings every new `content` verb
69
+ and rule C5 to a course that skipped it (step 14); `validate:content` now passes on its
70
+ first run and every diagnostic names its rule (step 15); rule N4's lints now
71
+ follow the tier model, which retires the acknowledgements for reused letters
72
+ and adds a warning and two infos to clear (step 20). The glossary page now
73
+ lists page-bound meanings under its index and a formula's symbol list names
74
+ every symbol (step 21): nothing to do unless course CSS or tests read them. A lesson can now have
75
+ a deck, its slide edition; nothing changes until a course writes one (step 16),
76
+ and the sidebar then lists its lesson once, with a setting and a reader's
77
+ switch for which edition leads; the manifest schema is now v34 (step 17). A
78
+ deck's page shows which slides Present mode has shown each reader, above each
79
+ frame and on a slide map, and presents from any frame; the manifest schema is
80
+ now v35 (step 18). A part can have a deck of its
81
+ own, the lecture of the whole part; nothing changes until a course writes one,
82
+ and the manifest schema is now v36 (step 19); in the sidebar it then shares
83
+ its Overview's row (step 22). The sidebar now marks the pages a
84
+ reader has not opened yet and those changed since (step 11), and every
85
+ course now asks each reader before it keeps their reading in the browser
86
+ (step 12); nothing to do unless a course wants either off, but a course's
87
+ browser tests that expect kept cards to survive a restart need a setup line.
88
+ Add a course-owned 404 page and an Astro `site` value to replace the fallback
89
+ and clear its two build warnings (step 24). The header, the sidebars and a
90
+ deck's slides now have colours and edges of their own, from a new
91
+ `appearance` setting; nothing to do unless screenshot tests or course CSS
92
+ read them, and the manifest schema is now v37 (step 25). The engine now supplies an empty
93
+ English `i18n` entry when a course has no translations, so the old empty-`i18n`
94
+ warning needs no course workaround.
95
+
96
+ ### 1. Update the dependency
97
+
98
+ ```bash
99
+ pnpm add explico@<this version>
100
+ ```
101
+
102
+ **A course on 0.13.0 or 0.13.1** also stops needing the downstream patch that
103
+ added `dist/components/float-name.js` to the installed package
104
+ (`patches/explico@0.13.1.patch`, or a `pnpm.patchedDependencies` entry): this
105
+ release compiles the module as `dist/reference/float-name.js`, which the
106
+ export serializers import. Delete the patch and its `patchedDependencies`
107
+ entry, reinstall, and delete any test that pins the old path.
108
+
109
+ **Coming from 0.12.0 or earlier:** apply "0.13.0 → 0.13.1" and "0.12.0 →
110
+ 0.13.0" below first.
111
+
112
+ ### 2. Move the plugin lists into `markdown.processor`
113
+
114
+ Only when `astro.config.mjs` still has `markdown.remarkPlugins`,
115
+ `markdown.rehypePlugins` or `markdown.remarkRehype`:
116
+
117
+ ```bash
118
+ rg -n 'remarkPlugins|rehypePlugins|remarkRehype' astro.config.mjs src scripts
119
+ ```
120
+
121
+ Import `unified` and hand it the same lists:
122
+
123
+ ```js
124
+ import { unified } from '@astrojs/markdown-remark';
125
+
126
+ export default defineConfig({
127
+ // …
128
+ markdown: {
129
+ processor: unified({
130
+ remarkPlugins: [/* the entries markdown.remarkPlugins had, in order */],
131
+ rehypePlugins: [/* the entries markdown.rehypePlugins had, in order */],
132
+ // remarkRehype: { … } // only when you set one
133
+ }),
134
+ },
135
+ });
136
+ ```
137
+
138
+ Keep the order: `remarkAsides` stays last in `remarkPlugins`. Nothing in
139
+ explico needs the flat form: `explicoContentRefresh()` checks the
140
+ manifest-driven plugins (`remarkAbbreviations`, `remarkCitation`,
141
+ `remarkNotation`) on both surfaces, MDX extends `markdown.processor`, and
142
+ Starlight 0.42 appends its own plugins to it. Course code that reads
143
+ `markdown.remarkPlugins` (a test that asserts the wiring, a script) reads
144
+ `markdown.processor.options.remarkPlugins` instead. The playground's
145
+ `astro.config.mjs` is a worked example; the time-series course made this move
146
+ and all 141 of its built pages came out byte-identical.
147
+
148
+ ### 3. Only if a competency graph warns `dia-too-wide`
149
+
150
+ A course with many competencies gets `[dia-too-wide] curriculum-map/index.html:
151
+ Diagram "competency-graph" …` (rule V8) on the curriculum map, because the
152
+ graph is one diagram. `<CurriculumMap>` now takes three optional props, all
153
+ forwarded to the competency diagram(s); with none of them the page is
154
+ unchanged:
155
+
156
+ ```mdx
157
+ <CurriculumMap graph="per-part" />
158
+ ```
159
+
160
+ - `graph="per-part"` draws one diagram per part, in the order of
161
+ `course.settings.json` `parts`, under an `<h3>` with the part's overview
162
+ title (its label when it has none). A diagram holds the competencies the
163
+ part's lessons teach first and one box per earlier competency they build on,
164
+ labelled `<part>: <competency>`; competencies no lesson teaches form a last
165
+ group. The ids are `competency-graph-<part dir>` and
166
+ `competency-graph-untaught`. The default is `graph="whole"`.
167
+ - `direction="LR"` lays the diagrams out left to right (`TB` is the default).
168
+ - `warnAboveWidth={false}` (or a width in px) replaces rule V8's column width
169
+ for these diagrams, for a whole-course graph that is meant to scroll.
170
+
171
+ ### 4. Rule N23 and the glyph resolver: findings that go away, one that can appear
172
+
173
+ Four resolver fixes change what `validate:content` reports, and one message is
174
+ clearer; none needs an edit to pass. Look for the workarounds they make
175
+ unnecessary, and for one new error:
176
+
177
+ - **`index-range` for a letter that is not an index.** `b` in `\nabla_{b_n}`
178
+ (the base of a carded symbol in a subscript) and the letters of a subscript
179
+ with a relation (`\mathbb{E}_{Z \sim g}`) are no longer running indices. A
180
+ `range` you gave the card of `b_n` only to silence `b`, and a
181
+ `fixedIndex: true` on a density `g` that only appeared in a condition, can
182
+ be deleted. `n` of `b_n` still needs its set, once per page.
183
+ - **A family of juxtaposed letters now reaches its instances.** `x_{ij}`,
184
+ `K_{ij}`, `r_{ik}` give their card to `x_{i1}`, `x_{12}`, `K_{ji}` (which
185
+ failed rule N4 as unresolved): a run of as many letters and digits. `\explain{responsibility}{r_{i1}}` wraps that you
186
+ added so an instance found its card are harmless and can stay or go. **A page
187
+ that has both `x_{ij}` and `x_k` as entries now fails
188
+ `ambiguous-base-fallback` on `x_{12}`**, which both fit: wrap that instance
189
+ in `\explain{key}{x_{12}}` for the one you mean.
190
+ - **An `\explain` in an instance's subscript** (`\beta_{\explain{count}{K}}`)
191
+ keeps the family's card on the instance. The workaround `\explain{beta-j}{\beta_{\explain{count}{K}}}`
192
+ resolves to the same markup, so nothing needs to change.
193
+ - **A wrapped instance is not re-bound inside.** `\explain{component-sd}{\sigma_1}`
194
+ no longer wraps the `\sigma` in a plain `\sigma` entry's card, so a page that
195
+ kept the wrap off to avoid that can add it back. Rendered pages that had the
196
+ inner card change: re-record screenshots of a hovered σ.
197
+ - **A clearer `unresolved-glyph` for an instance of a family whose index a card
198
+ fixed.** Marking the card of `m` `fixedIndex: true` makes the family `r_m` a
199
+ single symbol, so `r_2` is no longer its instance. The error used to say
200
+ "never a bare r" and suggest `\explain{periodic-rate}{r}`; it now names the
201
+ card that fixed `m` and suggests a `range` on it. The same error, only its
202
+ text and two new fields on the unresolved item (`instance`, `fixedLenders`
203
+ of `resolveMathGlyphs`) change, so a test that matched the old sentence for
204
+ such a symbol moves. A course that cleared an `index-range` error with
205
+ `fixedIndex: true` on a letter that also indexes a family (`m` under `r_m`)
206
+ and then wrapped the instances can give that card a `range` instead and
207
+ delete the wraps.
208
+
209
+ ### 5. Lab math: workarounds you can drop
210
+
211
+ Three fixes touch what a lab template and the build do with a figure. None
212
+ needs an edit, and each makes an earlier workaround unnecessary:
213
+
214
+ - **`e`, `\pi`, `i` and `d` in a lab template** no longer fail the figure check
215
+ ("figure symbols without a notation card"), so the notation cards a course
216
+ added only to bind Euler's number or the circle constant in a template can go
217
+ if nothing else uses them. An index (`e_i`, rule N23), a decorated letter
218
+ (rule N22) or a styled one still needs one.
219
+ - **A key only a lab template binds** now reaches the page's card payload. A
220
+ course that also named every such key in the lesson's prose so the card
221
+ would ship can stop doing so.
222
+ - **`lab-katex-direct` is one error per lesson that reaches a KaTeX-importing
223
+ helper**, not one per helper. A script that tallies the diagnostics by file
224
+ sees more of them, and `pnpm content check <lesson>` now answers for that
225
+ lesson alone. `astro build` reports every page with an unbound figure symbol
226
+ in one failure, not the first.
227
+
228
+ ### 6. Only if your course CSS or browser tests reach into a diagram, the text size or the `[?]` panel
229
+
230
+ Nothing in `content/` or `course.config.ts` changes, and the manifest schema
231
+ stays v31. These are changes to markup and behaviour that course CSS and
232
+ browser tests can depend on:
233
+
234
+ - **A diagram has a new frame element.** The markup is now
235
+ `.diagram-scroll > .diagram-frame > .diagram-canvas > svg`. A selector that
236
+ takes `.diagram-canvas` as a *child* of `.diagram-scroll`
237
+ (`.diagram-scroll > .diagram-canvas`) must become a descendant selector
238
+ (`.diagram-scroll .diagram-canvas`). `--diagram-width` and `--diagram-height`
239
+ moved from the canvas to the frame; the canvas inherits them, so
240
+ `getComputedStyle(canvas).getPropertyValue(…)` still reads them, but
241
+ `canvas.style.getPropertyValue(…)` no longer does.
242
+ - **The phone's text size is 50, 59, 71, 84 and 100%** (it was 100, 90, 80 and
243
+ 70%), smallest first. A reader's saved 90, 80 or 70 is ignored and the page
244
+ opens at 100% until they choose again; no action. A browser test that picks
245
+ a size or asserts the root font size must use the new values:
246
+ `data-text-size="84"` is 13.44 px, `71` is 11.36 px, `59` is 9.44 px, `50` is
247
+ 8 px.
248
+ - **A diagram shrinks with the text size as a whole**, boxes and text,
249
+ through `transform: scale(var(--explico-text-scale))` on `.diagram-canvas`
250
+ and a scaled `width`/`height` on `.diagram-frame`, on a touch screen with a
251
+ size chosen. A test that reads a node's `getBoundingClientRect()` then sees
252
+ scaled numbers, and `.diagram-canvas`'s `offsetWidth` stays the build-time
253
+ size (it ignores the transform): measure the frame or the scroller's
254
+ `scrollWidth`. Course CSS must not set `transform`, `width` or `height` on
255
+ `.diagram-canvas` or `.diagram-frame`. A diagram inside a preview card is not
256
+ scaled. Print is unchanged. `node_modules/explico/docs/diagram-scaling.md`
257
+ has the reasons and the rules for changing it.
258
+ - **The `[?]` panel on a phone** is a sheet fixed under the header below 50rem
259
+ (`.about-body`), no longer an absolute box hanging from the button. Delete a
260
+ course override of `.about-body`'s position or width that worked around the
261
+ old placement, and any rule that restyled the `[?]` button's `summary`
262
+ inside `.layout-navigation-content`: the button is round there now.
263
+ - **A hover peek no longer overwrites a collapsed section's saved state.** A
264
+ reader whose choice was already overwritten stays open until they collapse it
265
+ again. A browser test that collapses a section and reloads should move the
266
+ pointer away first (`page.mouse.move(0, 0)`): with the pointer left on the
267
+ heading, the section peeks (`data-state="peeking"`) after the reload, as it
268
+ should, and asserting `closed` at once races that.
269
+
270
+ ### 7. Only if your course CSS lifts the contrast of an active term link
271
+
272
+ 0.13.1 blended the highlight of every active notation target, a prose term
273
+ link (`a.notation-term`) included, and in the light theme that took the
274
+ engine's own link colour below 4.5:1 over a tinted panel (axe `color-contrast`
275
+ in a glossary entry). This release blends a symbol inside `.katex-html` only;
276
+ a prose link keeps the opaque `--sl-color-accent-low` tint it had on 0.13.0.
277
+
278
+ ```bash
279
+ rg -n 'notation-term.*is-notation-active|is-notation-active.*notation-term' src content tests
280
+ ```
281
+
282
+ A hit is the workaround for that fault, such as
283
+ `a.notation-term.is-notation-active { color: var(--sl-color-accent-high); }`.
284
+ Delete it; the link text is 4.75:1 on the tint without it. Run the course's
285
+ axe checks over a glossary page with a hovered symbol to confirm.
286
+
287
+ ### 8. Only if a browser test reads the Download button's tooltip
288
+
289
+ The header's Download button (`button.lesson-export-actions__download`) had
290
+ `title="Download this page"` on every page. Its `title` now names the scopes
291
+ the menu offers there: "Download this chapter, this part or the whole course"
292
+ on a chapter, "Download this part or the whole course" on a part's page and
293
+ "Download the whole course" on any other page. **Its accessible name is
294
+ unchanged**, so `getByRole('button', { name: 'Download this page' })` still
295
+ finds it.
296
+
297
+ ```bash
298
+ rg -n "Download this page" tests src
299
+ ```
300
+
301
+ A hit that selects the button by role and name needs nothing. One that matches
302
+ `title`, `getByTitle` or `[title="Download this page"]` takes the new text, or
303
+ better selects by role and name.
304
+
305
+ ### 9. Rule N4 now reads a notation entry's body
306
+
307
+ `validate:content` resolves every `$…$` / `$$…$$` in the Markdown body of a
308
+ `content/notation/*.md` entry, against every shared entry, as the glossary's
309
+ render already did. Before, a symbol no entry binds there passed
310
+ `validate:content`, `astro build` logged one `[ERROR] [glob-loader]` line and
311
+ ended green, and the glossary showed the entry without its body. Now:
312
+
313
+ - `validate:content` reports `[N4 unresolved-glyph] content/notation/<key>.md:<line>`
314
+ (or `invalid-latex`, `currency-dollar`, … for the other faults) at the
315
+ file's own line. Fix it in the entry: give the symbol an entry of its own in
316
+ `content/notation/`, or wrap it in `\explain{key}{latex}` with the entry it
317
+ stands for. `[[key]]` adds nothing here, since every shared entry is already
318
+ in scope, and an entry has no `notation.local`.
319
+ - `astro build` fails at the glossary with "Notation entries whose body did not
320
+ render: …" for an entry whose body Astro could not render, whatever the
321
+ cause. The `[glob-loader]` error above it in the log says why.
322
+ - `astro build` reports a notation entry's failure at the file's own line, and
323
+ an unresolved-glyph error ends its token list with `, in $…$`. A script or
324
+ test that parses `entry.md:<line>` from the build log no longer needs to add
325
+ the frontmatter's height.
326
+ - An entry's body now honours `fixedIndex` (rule N23): `r_2` no longer takes
327
+ the card of the family `r_m` when `m` is marked `fixedIndex: true`. Give the
328
+ instance an entry or an `\explain` wrap, or the letter a `range` if it runs.
329
+
330
+ A course test that resolves the entry bodies itself (credit-and-derivatives'
331
+ `tests/notation/notation-entry-body-math.test.ts`) can be deleted once
332
+ `validate:content` passes: the gate covers it, against the same table.
333
+
334
+ ### 10. Commit `content/versions.json`, each page's own version (rule C5)
335
+
336
+ Each chapter and part page now carries the knowledge version its content last
337
+ changed in, under the title of its PDF, in its source zip's README, under the
338
+ title of its Markdown, and in the `[?]` panel's new "This page" row:
339
+ "unchanged since version 0.2.0", or "with changes after version 0.4.0, not yet
340
+ released" while `## [Unreleased]` holds a change. A part's and the course's
341
+ PDF and Markdown carry the newest among their pages. The record is
342
+ `content/versions.json`, which `validate:content` writes and the course
343
+ commits like a lockfile; never edit it by hand. The manifest schema is now
344
+ v32 (`lessons[].since`, `parts[].overview.since`, `about.knowledge.releases`
345
+ and `unreleased`, `pageVersions`).
346
+
347
+ ```bash
348
+ pnpm validate:content # prints "content/versions.json written — commit it with the change (rule C5)"
349
+ git add content/versions.json
350
+ ```
351
+
352
+ Commit it with the upgrade. From then on `validate:content` rewrites the file
353
+ after a content change and after a release (`pnpm release:content`, then
354
+ `pnpm validate:content`), and the file goes in the same commit. In CI (`CI`
355
+ set) `validate:content` does not write; a record that would change fails with
356
+ `[C5 page-versions-stale] content/versions.json is out of date (N page(s): …)`
357
+ until the commit carries the file. The first record has no history to draw
358
+ on: while `## [Unreleased]` holds bullets, every page reads "with changes after
359
+ version `x.y.z`, not yet released", `x.y.z` being `about.version`, until the
360
+ next release stamps them all; with an empty `## [Unreleased]`, every page reads
361
+ "unchanged since version `x.y.z`". Earlier releases are not reconstructed.
362
+
363
+ What counts as a page's content: its file, its `.checks.yml` and the
364
+ assessments it lists, the shared notation entries its text uses, the sources
365
+ it cites, and its numbers. A lab, `content/domain/`, a part's label or the
366
+ engine change the PDF but not the stamp.
367
+
368
+ What else moves:
369
+
370
+ - The downloads' version line. `From Course, version 0.4.0.` under a
371
+ chapter's title is now `From Course, unchanged since version 0.2.0.`; a
372
+ book's title page says `Content unchanged since version 0.2.0` where it
373
+ said `Content version 0.4.0`; the zips' READMEs `(unchanged since version
374
+ 0.2.0)` where they said `(content version 0.4.0)`; Markdown the same. A
375
+ test that reads these lines reads the new ones.
376
+ - A document unchanged since an earlier release keeps that release's date in
377
+ its PDF and figure PDFs (`SOURCE_DATE_EPOCH`) and its trailer `/ID`. With
378
+ `EXPLICO_LATEX_CACHE` (step 13) a release therefore recompiles only the
379
+ documents it changed.
380
+ - A browser test that reads the `[?]` panel finds a `This page` row with
381
+ `[data-page-since="0.2.0"]` (`"unreleased"` for a change not yet released)
382
+ on chapters and part pages, none elsewhere.
383
+ - `pnpm content status --json` reports `pageVersions: { tracked, unreleased }`
384
+ and `content context` a lesson's `since`. Optional: print them in the
385
+ course's `scripts/content.ts`, and end `scripts/release-content.ts` with a
386
+ reminder to run `validate:content`; the playground's copies show both.
387
+
388
+ To opt out, turn the rule off in `course.settings.json`:
389
+ `"rules": { "C5": { "enabled": false } }`. Every page then carries the
390
+ course's version as before, and no file is read or written.
391
+
392
+ ### 11. The sidebar's marks: nothing to do, unless you want them off
393
+
394
+ The sidebar now marks, for each reader, the chapters and part pages they
395
+ have not opened yet (a quiet ring) and those whose content changed since they
396
+ last opened them (an accent dot), with a line under the chapter list that
397
+ says what the marks mean and offers "Mark all as seen" and "Mark all as
398
+ unseen". "Changed" uses the
399
+ page hashes of `content/versions.json` (step 10), so it needs nothing new
400
+ from the course. Both marks are on by default.
401
+
402
+ To turn either off, add the block to `content/course.settings.json`:
403
+
404
+ ```json
405
+ "sidebarMarks": { "unopened": false, "changed": true }
406
+ ```
407
+
408
+ and, when the course keeps a settings schema with
409
+ `"additionalProperties": false` (the playground's
410
+ `content/course.settings.schema.json` does), copy the playground's
411
+ `sidebarMarks` property into it, or the settings form flags the block.
412
+
413
+ Only if a course's CSS or browser tests reach into the sidebar:
414
+
415
+ - A marked row's `<li>` carries `data-page-mark="unopened"` or `"changed"`,
416
+ and draws the mark with its `::after`; a `<details>` part carries
417
+ `data-page-marks`, and so does `.layout-navigation-panel`. Course CSS that
418
+ sets `li::after` in the sidebar competes with the mark.
419
+ - A marked link gains `aria-describedby`, naming the mark ("Not opened yet",
420
+ "Changed since you last opened it"); its accessible name is unchanged.
421
+ - The legend (`.explico-page-marks-legend`) follows the sidebar's list
422
+ whenever the reader's record is kept, with a mark on screen or none. It
423
+ holds the two buttons (`.explico-page-marks-legend__action`, `aria-disabled`
424
+ while one has nothing to change) and a `role="status"` paragraph,
425
+ empty until a button is used. A test that finds `getByRole('status')` on
426
+ the whole page sees one more wherever the legend is.
427
+ - A compact part's chapter tile is one sheet, where it was two (its
428
+ `::before` has no `box-shadow`); the Overview tile keeps its two sheets.
429
+ Pointing at a part's `»`, or focusing it, lights the part's Overview row or
430
+ tile as a pointer on it would. No course change is required.
431
+ - The marks are reading memory (step 12): a fresh browser profile shows none
432
+ until the reader says yes to the course remembering their reading. After
433
+ the yes, every chapter but the open one is "not opened yet".
434
+
435
+ ### 12. The reading-memory question: nothing to do, unless your tests or CSS need it
436
+
437
+ Every course now asks each reader, once, whether it may remember their
438
+ reading in this browser: a small card at the bottom of the window ("Remember
439
+ your reading?", "Remember" / "Don't remember"), on every page until they
440
+ answer. Reading memory is the chapters they opened (the sidebar marks, and
441
+ the new line under a changed page's title) and the explanation cards they keep
442
+ open or carry, across visits. Until a yes, and after a no, none of it
443
+ outlives the tab; kept and carried cards still work within the tab, through
444
+ its session storage. Settings the reader chooses (theme, text size, panel
445
+ layout, folded sections, the "Carry cards" switch) are kept as before. The
446
+ answer stands 13 months; the `[?]` panel's new "In this browser" section
447
+ changes it or forgets everything. The site sets no cookies, and nothing leaves
448
+ the browser.
449
+
450
+ To never ask, and never keep reading memory (the sidebar marks are then off,
451
+ and cards last as long as the tab), set it in `content/course.settings.json`:
452
+
453
+ ```json
454
+ "readingMemory": "off"
455
+ ```
456
+
457
+ and, with a settings schema that sets `"additionalProperties": false`, copy
458
+ the playground's `readingMemory` property into it.
459
+
460
+ Only if the course has browser tests, or CSS that reaches into the page:
461
+
462
+ - **A test that expects kept or carried cards to come back after a browser
463
+ restart, or in a new tab**, starts from a browser without the answer, so the
464
+ cards stay in the old tab. Start such specs from a yes: in
465
+ `playwright.config.ts`, `use.storageState` with the localStorage entry
466
+ `explico-reading-memory:<base path>` set to
467
+ `{"v":1,"remember":true,"at":"<today, YYYY-MM-DD>"}`. The playground's
468
+ `tests/e2e/reading-memory-state.ts` builds it (`rememberReadingState(origin,
469
+ base)`), and its `playwright.config.ts` uses it. A context a spec opens itself
470
+ with `browser.newContext()` does not inherit `use`; pass the same
471
+ `storageState`, or copy the first context's (`await
472
+ context.storageState()`).
473
+ - **A screenshot of a page from an empty browser** now shows the question
474
+ along the bottom of the window. The same `storageState` keeps it out.
475
+ - **A test that counts `getByRole('status')`** finds one more per copy of the
476
+ `[?]` panel (its "Forgotten" line, empty until used).
477
+ - **Course CSS**: the question is `.explico-reading-notice` (moved to the end
478
+ of `<body>` by its script), the panel's section `.about-browser`, the line
479
+ under a changed page's title `.page-changed`.
480
+ - **Opening a page no longer writes its sections' open/closed state** to the
481
+ browser; a test that read `disclosure:<path>:<section>` keys right after a
482
+ page loaded finds none until the reader folds or opens a section.
483
+
484
+ ### 13. Optional: reuse the PDFs a build already compiled
485
+
486
+ Only for a course with `lessonExports.latex: true`. Nothing changes until
487
+ `EXPLICO_LATEX_CACHE` is set; then the LaTeX export compiles only the
488
+ documents whose folder changed (a chapter, its part and the course, for a
489
+ push that touches one chapter) and reuses the rest from that directory. Set
490
+ it in the CI job that builds, and keep the directory between runs:
491
+
492
+ ```yaml
493
+ - uses: actions/cache@v4
494
+ with:
495
+ path: .cache/explico-latex
496
+ key: explico-latex-${{ github.sha }}
497
+ restore-keys: explico-latex-
498
+ - run: pnpm build
499
+ env:
500
+ EXPLICO_LATEX_CACHE: .cache/explico-latex
501
+ ```
502
+
503
+ Add the directory to `.gitignore`. The build's `LaTeX export:` line now says
504
+ how many PDFs it compiled and reused. After a `tlmgr update` that the cache
505
+ cannot see (a font or a package, not a TeX Live release), set
506
+ `EXPLICO_LATEX_CACHE_SALT` to any new value once. With step 10 a document's
507
+ version line and date are its own, so a content release (rule C3), too,
508
+ recompiles only the documents whose pages it changed, their parts and the
509
+ course.
510
+
511
+ ### 14. Replace `scripts/content.ts` and `scripts/compile-manifest.ts` with the engine's shims
512
+
513
+ The `content` CLI and `validate:content` now live in the engine
514
+ (`explico/compiler/cli-main`, `explico/compiler/validate-content`), so every
515
+ verb reaches a course with the package instead of being ported by hand. Replace
516
+ both files whole:
517
+
518
+ ```ts
519
+ // scripts/content.ts
520
+ import { runContentCli } from 'explico/compiler/cli-main';
521
+ import { courseConfig } from '../content/course.config';
522
+
523
+ process.exitCode = await runContentCli(courseConfig);
524
+ ```
525
+
526
+ ```ts
527
+ // scripts/compile-manifest.ts
528
+ import { runValidateContent } from 'explico/compiler/validate-content';
529
+ import { courseConfig } from '../content/course.config';
530
+
531
+ process.exitCode = await runValidateContent(courseConfig);
532
+ ```
533
+
534
+ `package.json` keeps `"content": "tsx scripts/content.ts"` and
535
+ `"validate:content": "tsx scripts/compile-manifest.ts"`. `pnpm content help`
536
+ then lists every verb: `status`, `diagnostics`, `warnings`, `context`,
537
+ `equation`, `bindings`, `symbol`, `check`, `rule`, `migrate`, `rules`, `move`,
538
+ `rename`, `fix`, `new lesson | deck | term`.
539
+
540
+ What a course gains or changes by the swap:
541
+
542
+ - **New verbs:** `symbol` (where a symbol is already defined), `rule <ID>`
543
+ (one rule whole), `bindings <lesson>` and `equation "<latex>"` (what math
544
+ binds, before a build), `migrate` (this guide's sections between two
545
+ versions), and the write verbs `move`, `rename`, `fix` (a diff, applied only
546
+ with `--write`; `move` is how a `definition-repeated` warning, step 20, is
547
+ cleared in one command).
548
+ - **`validate:content` refreshes `content/versions.json` (rule C5, step 10)
549
+ and the resolution reports.** A course whose script did not call
550
+ `syncPageVersions` or `syncResolutionReports` (data-science-I and
551
+ time-series did not) starts writing both: commit `content/versions.json` with
552
+ the swap. The run fails only on a resolution report git tracks that it had to
553
+ rewrite, so a course whose `build/` is git-ignored passes on its first run.
554
+ - **Every diagnostic group gates.** Errors in curriculum, notation,
555
+ alignment, math, waivers, equations, tables, figures, diagrams, knowledge
556
+ checks, examples, folds and raw floats fail the run; warnings print; infos go
557
+ to `build/logs/`. A course script that added a check of its own keeps it by
558
+ running it after `runValidateContent` returns 0.
559
+ - **The output is the engine's.** A course test that matched the old
560
+ wrapper's text should match `pnpm content help` and the `--json` shapes
561
+ instead; the JSON reports did not change.
562
+
563
+ A course that keeps its own scripts loses only the new verbs; nothing else
564
+ depends on them.
565
+
566
+ Then, optionally, install the agent procedures that ship with the engine:
567
+ `pnpm content init-agent` shows what it would write (the `draft-lesson`,
568
+ `migrate-course` and `release-content` skills under `.claude/skills/`, the
569
+ `rules-reviewer` agent under `.claude/agents/`, and a baseline `AGENTS.md`
570
+ only when the course has none); `--write` writes it. An existing `AGENTS.md` is
571
+ never edited: `pnpm content status` says when it does not point at
572
+ `node_modules/explico/docs/authoring/agents.md`, and the course's file can then
573
+ shrink to what is particular to the course — the engine's guide covers the CLI,
574
+ the validate sequence and the notation rules.
575
+
576
+ ### 15. `validate:content` passes on its first run; diagnostics name their rule
577
+
578
+ Nothing to do. `validate:content` still rewrites `build/resolution/*`, but it
579
+ now fails only on a report git tracks that it had to rewrite, so a course whose
580
+ `build/` is git-ignored passes on the first run; delete any "run it twice"
581
+ instruction from the course's own AGENTS.md. A course that commits
582
+ `build/resolution/` (credit-and-derivatives) keeps committing the rewritten
583
+ files with the change, as before.
584
+
585
+ Optional, in `scripts/content.ts`: print diagnostics through
586
+ `formatCliDiagnostic` (and `ownerTag` in the `check` case), both exported from
587
+ `explico/compiler/cli`, so every line names its rule and page
588
+ (`[N4 · notation.md#n4]`), and add `report.agentSurfaces` to the `status`
589
+ output. The JSON already carries `doc` and `agentSurfaces` with no wrapper
590
+ change; the playground's wrapper is the reference.
591
+
592
+ Also optional: make `content diagnostics` count by default. Pass the report
593
+ through `summarizeDiagnostics` and print `formatDiagnosticsSummary` unless
594
+ `--all`, `--code <code>`, `--lesson <id>` or `--changed` is given, in which
595
+ case `filterDiagnostics` narrows it and the lines print as before (the
596
+ playground's `diagnostics` case has the ten lines). On a course with hundreds
597
+ of diagnostics of two codes this turns a 20 000-token dump into a 300-token
598
+ table.
599
+
600
+ ### 16. Decks: nothing to do, unless the course writes one
601
+
602
+ A lesson can now have a deck, its slide edition, in
603
+ `content/docs/<part>/<lesson>.deck.mdx` (`docs/authoring/deck.md`). A course
604
+ without one sees no change: the manifest schema is now v33, but only adds
605
+ `decks`, `deck` and `lessons[].editions`; `build/rules-in-effect.md` lists the
606
+ same rules as before, since the new area `D` is about decks only. Rules V7, V8,
607
+ S1, S6 and S7 now say they are about a chapter's page.
608
+
609
+ To write a deck:
610
+
611
+ 1. Add the verb to the course's `scripts/content.ts`, as the playground's does:
612
+ `new deck` beside `new lesson` (`scaffoldDeck` from `explico/compiler/cli`),
613
+ and the `--edition` flag on `rules` (passed to `parseRulesFilter`).
614
+ 2. If the course's `course.settings.schema.json` lists its properties with
615
+ `additionalProperties: false`, add the `deck` block (copy it from the
616
+ playground's schema) before setting `deck` in `course.settings.json`. The
617
+ defaults need no setting: a 16:9 canvas, a legend of first uses, notes shown
618
+ to readers, an outcomes frame and a handout PDF.
619
+ 3. `pnpm content new deck <part>/<lesson>` drafts the deck from the chapter.
620
+ Rewrite it to the deck rules before committing: rule D1 (sections of frames),
621
+ rule D2 (a frame fits its canvas), rule D3 (a frame makes one claim), rule D4
622
+ (notes on a frame with text), rule D5 (a frame title fits one line), rule D6
623
+ (at most `maxSteps` steps), rule D7 (one picture per frame), rule D8 (recall
624
+ the chapter's result, never retype it), rule D9 (few new symbols per frame)
625
+ and rule D10 (the engine writes the title and outcomes frames). Read
626
+ `pnpm content rules --edition deck` before drafting.
627
+ 4. `pnpm validate:content`, and commit `content/versions.json`: a deck beside
628
+ its chapter has a page version of its own (rule C5).
629
+
630
+ A course wrapper around `<Figure>`, `<Table>` or `<Diagram>` and a lab that
631
+ calls `labMathScope(…, { locals: Astro.locals, … })` need no change to be
632
+ recalled onto a deck.
633
+
634
+ In the sidebar, a deck is a play mark in a 16:9 frame, on its icon or tile and
635
+ on a slides-first lesson's own row and tile (`a.explico-deck-lesson`); a
636
+ lesson with a chapter and a deck is one row (step 17). The glyph is the link's
637
+ `::before` and `::after`. On a deck's page, a reader who left Present mode
638
+ past the first frame finds a "Continue at slide N" button
639
+ (`[data-deck-resume]`) beside Present. No course change is required; only
640
+ course CSS that styles the sidebar's links, or a browser test that counts the
641
+ deck page's buttons, can notice.
642
+
643
+ Once the course has a deck, its build needs a few more things:
644
+
645
+ - **A browser, in CI.** After `astro build`, every deck page is laid out in
646
+ the course's Playwright Chromium, and a frame that runs past its canvas fails
647
+ the build (`frame-overflow`, rule D2). Without Chromium the build warns and
648
+ measures nothing, except under `CI` or `EXPLICO_LATEX_STRICT=1`, where it
649
+ fails. A course whose CI already captures figures for the LaTeX export has
650
+ the browser. Otherwise, install it in CI before the build:
651
+ `pnpm exec playwright install --with-deps chromium`.
652
+ - **Beamer, with `lessonExports.latex`.** A deck compiles to slides and a
653
+ handout in beamer. A CI that installs TeX Live from
654
+ `node_modules/explico/src/compiler/exports/latex/texlive-packages.txt` gets
655
+ `beamer` and `translator` from the updated list. A CI with its own package
656
+ list adds those two.
657
+ - **Tests of the Download menu.** A chapter with a deck has one more row,
658
+ "Its slides", and the deck's page has "These slides" and "Its chapter". The
659
+ Download button's tooltip names "the slides" too. Update a test that counts
660
+ the rows or reads the tooltip on such a page.
661
+
662
+ ### 17. A lesson is one row in the sidebar: nothing to do, unless your tests or CSS read it
663
+
664
+ A lesson with a chapter and a deck is one sidebar row: it opens the **lead**
665
+ edition under the lesson's title, and the other edition is an icon at the
666
+ row's end (`a.layout-edition-other`, named "Slides: *title*" or "Chapter:
667
+ *title*"), the second of two joined tiles in a compact part. A lesson with one
668
+ edition is a row as before. Without scripts the sidebar lists each chapter,
669
+ then its "Slides" row. The manifest schema is now v34: it adds
670
+ `sidebarEditions`, and every item of a lesson in `sidebar` carries
671
+ `data-edition`, `data-lesson` and `data-title` in `attrs`, so a chapter is
672
+ `{ slug, attrs }` where it was a bare slug.
673
+
674
+ No course change is required. The default leads with the chapter and lists
675
+ the slides, and each reader can switch both in the sidebar's foot; their
676
+ choice is a setting kept in their browser, `explico-sidebar-editions`, so a
677
+ course privacy notice that copies `docs/privacy.md` gains that row. A course
678
+ without a deck sees no change but the manifest's shape.
679
+
680
+ - **To open lessons at their slides, or to leave the other edition out of the
681
+ sidebar**, set `sidebarEditions` in `course.settings.json`. If the course's
682
+ `course.settings.schema.json` lists its properties with
683
+ `additionalProperties: false`, copy the `sidebarEditions` block from the
684
+ playground's schema first.
685
+
686
+ ```json
687
+ "sidebarEditions": { "lead": "slides", "other": "listed" }
688
+ ```
689
+
690
+ `lead` is `chapter` (default) or `slides`; `other` is `listed` (default) or
691
+ `hidden`. Any other value fails the build, naming the field.
692
+ - **A test that reads `manifest.sidebar`** and expects a chapter as a bare
693
+ slug reads the item's `slug`, or `sidebarLessonSlug(item)` from
694
+ `explico/compiler/lesson-derivation`, which gives a lesson's slug for its
695
+ own item and `undefined` for a slides row, an overview or a link.
696
+ - **A browser test** that finds a sidebar link by a lesson's title adds
697
+ `exact: true`: the icon beside it is named "Slides: *title*". One that
698
+ counts the sidebar's `li` items counts one per lesson; one that clicks the
699
+ "Slides" row clicks the deck's `href` or `a.layout-edition-other`. A row's
700
+ mark is on its `li`, as before; the icon's is on the link,
701
+ `a.layout-edition-other[data-page-mark]`.
702
+ - **Course CSS** that styles `a.explico-deck-row` in the sidebar: once the
703
+ script pairs the editions, the deck's link is `a.layout-edition-other` at
704
+ its chapter's row end, or carries no class while it leads; a compact tile
705
+ draws a deck's glyph from `a[data-edition="slides"]`. The marks' line no
706
+ longer draws the separator above it: the panel's foot,
707
+ `.explico-sidebar-foot`, holds the line and the two controls.
708
+ - **Course CSS that redraws the sidebar's chapter or deck glyph** (the
709
+ `::before` or `::after` of a sidebar link or a compact tile): each glyph is
710
+ one SVG mask over the link's own colour, the chapter a sheet with lines of
711
+ text (`--explico-chapter-glyph`), the deck a filled slide frame with the
712
+ play mark cut out (`--explico-deck-glyph`). A border on `::before` no longer
713
+ shapes it, and `::after` draws nothing. To draw another glyph, set the
714
+ custom property on `:root` to another `url("data:image/svg+xml,…")`, black
715
+ where the glyph is.
716
+
717
+ ### 18. The slides a reader has seen: nothing to do, unless your tests or CSS read a deck's page
718
+
719
+ A deck's page keeps each frame Present mode showed the reader and shows it
720
+ frame by frame. A line above each frame says "Slide N" and "Shown", "Not shown
721
+ yet" or "Changed since shown", and ends on a button: "Continue here" on the
722
+ frame to continue at, "Present from here" on every other. Under the title, the
723
+ slide map shows every frame as a numbered square, pinned under the header
724
+ while the deck scrolls; a long deck's map leaves runs out, with a gap for
725
+ each. A line under Present counts the marks and offers "Mark all as seen" and
726
+ "Mark all as unseen"; the sidebar's two buttons cover every deck's frames too.
727
+ "Continue at slide N" presents from the frame the reader stopped at, or from
728
+ the one after it once they were shown its every step. Before the reader
729
+ answers the reading-memory question the frames shown are kept, and marked,
730
+ for the tab; "Remember" keeps them for later visits and "Don't remember"
731
+ clears them. The manifest schema is now v35: every frame of `decks[].frames`
732
+ carries `id`, 12 hex digits over what it shows, stamped on the frame as
733
+ `data-frame-id`.
734
+
735
+ No course change is required. The marks follow `sidebarMarks`, so a course
736
+ that turned a sidebar mark off has it off on its decks too, and one with
737
+ `readingMemory: "off"` shows none. A reader's record under
738
+ `explico:decks:<base>` moves to its new shape on its own. The reading-memory
739
+ question now names the slides it keeps. A course privacy notice that copies
740
+ `docs/privacy.md` gains that row: where each deck stopped, and the frames
741
+ Present mode showed, kept for the tab before an answer.
742
+
743
+ - **A browser test** that finds the deck's Present button by name adds
744
+ `exact: true`: every frame's line has a button named "Present from here,
745
+ slide N", or "Continue here, slide N".
746
+
747
+ ```ts
748
+ await page.getByRole('button', { name: 'Present', exact: true }).click();
749
+ ```
750
+
751
+ - **A browser test that reads the sidebar's status** after "Mark all as seen"
752
+ or "Mark all as unseen", in a course with a deck, expects "Every page and
753
+ slide is marked as seen." or "Every page is marked as not opened yet, and
754
+ every slide as not shown yet."
755
+ - **A browser test that runs before an answer** and expected a deck's page
756
+ to mark nothing: it marks the frames Present mode showed in that tab, and
757
+ the tab's session storage holds them under `explico:decks:<base>`.
758
+ - **A browser test that counts a deck page's links or buttons**: the slide
759
+ map (`nav.deck-map`) adds a link for each square and each gap, and each
760
+ frame's line (`.deck-frame__line`) a button.
761
+ - **A browser test that seeds `explico:decks:<base>`**: the old shape,
762
+ `{ "<deck slug>": 3 }`, still reads, as a deck stopped at frame 3 with no
763
+ frame seen. To seed frames seen, write
764
+ `{ "v": 2, "decks": { "<deck slug>": { "at": 3, "step": 1, "seen": { "1": "<id>" } } } }`
765
+ with the ids of the frames' `data-frame-id`. `step` is the step of frame
766
+ `at` Present mode stood on: at that frame's last step, Continue goes on to
767
+ the next frame; without `step`, it stays at `at`.
768
+ - **Course CSS** that styles a frame or the deck's actions row: a frame's
769
+ first child is now its line, `.deck-frame__line`, before `.deck-stage`; the
770
+ deck's first child is the slide map, `nav.deck-map`; "Present from here"
771
+ is in the line, not on the stage; the frame's number holds no mark; and the
772
+ actions row ends with the marks' line, `.deck-actions__marks`.
773
+
774
+ ### 19. A deck for a part: nothing to do, unless the course writes one
775
+
776
+ A part can now have a deck, the lecture of the whole part:
777
+ `content/docs/<part>/index.deck.mdx`, beside the part's page, served at
778
+ `/<part>/slides/` (`docs/authoring/deck.md`, "A deck for a part"). A course
779
+ without one sees no change but the manifest's shape: the schema is now v36,
780
+ and every entry of `decks` gains `covers` (empty on a lesson's deck), while a
781
+ part's deck carries `part` and `recalledFrom` and no `lessonId`. The `decks`
782
+ invariant gains six codes, which only a part's deck or a deck's `[chapter: …]`
783
+ or `[morph]` marker can raise: `deck-chapter-unknown`,
784
+ `deck-chapter-repeated`, `deck-chapter-outside-part`, `deck-marker-misplaced`,
785
+ `deck-check-no-chapter` and `deck-route-taken`.
786
+
787
+ - **A course test or script that reads `manifest.decks`** and takes
788
+ `deck.lessonId` for granted skips or handles a part's deck: test
789
+ `deck.part !== undefined`.
790
+
791
+ To write a part's deck:
792
+
793
+ 1. Update the course's `scripts/content.ts` usage line, as the playground's
794
+ does: `new deck <lesson | part>` (`scaffoldDeck` takes either), and
795
+ `check <lesson | deck>…` (a deck by its page, `<part>/slides`).
796
+ 2. `pnpm content new deck <part>` drafts it from the part's chapters: a
797
+ `## <chapter title> [chapter: <name>]` section per chapter in the order the
798
+ tracks read them, a frame per section of the chapter, a `<Recall>` for each
799
+ float it numbers. Keep the sections that cover a chapter; a section without
800
+ the marker is the deck's own. Rewrite it to the deck rules, as a lesson's
801
+ deck (step 16).
802
+ - **Or merge the chapters' own decks into it**, when one lecture covers
803
+ several chapters: move each chapter's deck's sections into the part's
804
+ deck in reading order, give every one of its `##` headings the chapter's
805
+ marker (`## Two coordinate systems [chapter: position-and-coordinates]`:
806
+ a chapter may run over several sections in a row), keep one `import`
807
+ line, and delete the chapter's `<lesson>.deck.mdx`. The chapter's address
808
+ in the part's deck is its first section's divider.
809
+ 3. A lesson's own deck wins over its part's: a chapter with a deck keeps its
810
+ sidebar icon and its "Slides for this chapter" link. Only a chapter the
811
+ part's deck covers and that has no deck of its own links into it, at its
812
+ first section's divider, and offers the part's deck as "Its slides" in its
813
+ Download menu. A chapter's deck merged into its part's (above) is gone from
814
+ the sidebar, from its chapter's title line and from `downloads/`: a link
815
+ to `/<part>/<lesson>/slides/` breaks, so point it at the part's deck.
816
+ 4. `pnpm validate:content` and `pnpm content check <part>/slides`, then commit
817
+ `content/versions.json`: the part's deck has a page version of its own
818
+ (rule C5).
819
+
820
+ - **A browser test that counts a part's sidebar items or compact tiles** sees
821
+ the part's deck in its Overview's row (`a.explico-part-deck`, step 22), and
822
+ without scripts one more item, right after the Overview; one that lists
823
+ `downloads/` sees the part's deck's files, `<part>-slides.html`, `.pdf`,
824
+ `-notes.pdf`, `-latex.zip` and `-markdown.zip`.
825
+
826
+ ### 20. Rule N4's lints: delete acknowledgements, move the repeated local entries
827
+
828
+ `validate:content` now reports these, all owned by rule N4:
829
+
830
+ - `glyph-unique-in-corpus` fires only when two **shared** cards claim one
831
+ glyph. A `notation.local` entry or a gloss that reuses a letter is the
832
+ declared exception and is no longer reported, so every
833
+ `glyph-unique-in-corpus:<glyph> — …` line in `lint-ignore.yml` that
834
+ acknowledged a page-local reuse can be deleted (they are harmless if kept).
835
+ - `definition-repeated` (warning): one `notation.local` key declared in two
836
+ or more chapters (a deck beside its chapter never counts). Run
837
+ `pnpm content diagnostics warning --code definition-repeated`; each line
838
+ says whether the copies agree. Move one copy to `content/notation/<key>.md`
839
+ (`pnpm content new term <key>` scaffolds it; paste the block's fields) and
840
+ delete the other blocks: every `[[key]]` and `\explain{key}` keeps working.
841
+ Where the copies differ, keep the wording you want, or give the second
842
+ meaning its own key. A course with many such keys can clear them one lesson
843
+ at a time.
844
+ - `meaning-repeated` (info): one symbol and name defined at two sites in any
845
+ tier. Promote it to a shared card, or bind the shared card the message names
846
+ instead of redeclaring it.
847
+ - `explain-redundant` (info): an `\explain{key}{…}` in page math whose bare
848
+ symbol already resolves to the same card. Write the symbol bare; the wrap
849
+ is for a compound or a colliding glyph. Optional to clear.
850
+ - `gloss-wants-promoting` is gone; delete any `lint-ignore.yml` line for it.
851
+
852
+ ### 21. The glossary: nothing to do, unless your CSS or tests read it
853
+
854
+ The glossary page gains a section "On one page only" (`.glossary-page-meanings`)
855
+ under its index, listing every `notation.local` entry with a link to its lesson
856
+ and every gloss with a link to its card, and a formula's "Symbols" list
857
+ (`.notation-gloss-list`) now holds the cards the formula names as well as its
858
+ glosses, in formula order, a card's name linked (`a.notation-gloss-name`) and
859
+ its row marked `data-notation-card`. A course that styles or asserts on either
860
+ list adapts the selector; nothing else changes.
861
+
862
+ ### 22. A part's deck shares the Overview's row: nothing to do, unless your tests or CSS read it
863
+
864
+ A part's deck is its Overview's other edition, as a lesson's deck is its
865
+ chapter's (step 17): one row, the part's, that follows "Lessons open as" and
866
+ "List the other edition too". With chapters leading the row is "Overview"
867
+ and the deck is the icon at its end, `a.layout-edition-other`, named "Slides
868
+ for this part: *part*"; with slides leading the row is "Slides for this
869
+ part" and Overview is the icon, named "Overview: *part*". Unlisted, the other
870
+ link is absent, unless it is the open page. In a compact part the two are
871
+ one `li[data-editions="pair"]`. A part with no Overview keeps its deck as a
872
+ row of its own. Without scripts the sidebar is as before: the Overview, then
873
+ the deck's "Slides" row. A course with no part's deck sees no change.
874
+
875
+ No course change is required. What moved, for a course whose tests or CSS
876
+ read it:
877
+
878
+ - **The deck's link** can be `a.layout-edition-other` inside the Overview's
879
+ `li[data-editions="pair"]`, or absent, and the script takes
880
+ `explico-deck-row` off it (it keeps `explico-part-deck`). A browser test
881
+ that found its row by `li:has(> a[href="/<part>/slides/"])` finds the part's
882
+ row by the Overview's `href`, and its mark on the link,
883
+ `a.explico-part-deck[data-page-mark]`. One that finds the part page's
884
+ "Slides for this part" link by name adds `exact: true`: the sidebar's icon
885
+ is named "Slides for this part: *part*".
886
+ - **New attributes** in `manifest.sidebar` and on the links:
887
+ `data-part-edition` (`overview` or `slides`) and `data-part` on both,
888
+ `data-title`, `data-chapters` and `data-frames` on the deck's,
889
+ `data-deck-range` on each chapter that links into its section. A test that
890
+ compares those items whole adds them.
891
+ - **A tooltip's second line**: the part's deck and a chapter's section carry
892
+ an `aria-description` ("Chapters 1–2 · 26 slides", "Slides 17–25 of the
893
+ part's slides"), drawn as `small.layout-edition-detail` inside the link's
894
+ `<span>`. A test that reads a tooltip's whole text (`toHaveText`) reads its
895
+ accessible name instead, or expects the second line after it.
896
+ - **The lit section**: with the part's deck open, the section link of the
897
+ chapter on screen carries `aria-current="location"`, and the rail
898
+ (`li[data-covered]`) runs to its row. The part deck's frames carry
899
+ `data-chapter`, and `<html>` carries `data-deck-chapter`.
900
+ - **Course CSS that redraws the part deck's glyph** sets
901
+ `--explico-part-deck-glyph` on `:root`, as the other glyphs (step 17).
902
+
903
+ ### 23. The contents list shows each lesson's slides: nothing to do, unless your tests or CSS read it
904
+
905
+ `<CourseContents>` and `<PartOverview>` draw a slides column at each row's
906
+ end, a glyph before each title and, on the contents list, a deck count and a
907
+ key, when the course has a deck. **A course with no deck sees no change.**
908
+
909
+ No course change is required. What moved, for a course with a deck:
910
+
911
+ - **Each row gains links.** A deck's cell is an `a.deck-lane` inside the
912
+ row's `li`, after the title's link, so a browser test that counts or reads
913
+ `li > a` (`.course-contents li > a`, `.part-chapters > li > a`) narrows it
914
+ to the titles with `li > a:not(.deck-lane)`. A test that finds a title's
915
+ link by its name is unaffected: the glyph is an `aria-hidden` span with no
916
+ text.
917
+ - **To keep the old lists**, pass `slides={false}`:
918
+ `<CourseContents slides={false} />` and `<PartOverview slides={false} />`.
919
+ - **Course CSS that redraws the glyphs** sets `--explico-chapter-glyph`,
920
+ `--explico-deck-glyph`, `--explico-part-deck-glyph` and the new
921
+ `--explico-part-glyph` on `:root`, as in steps 17 and 22.
922
+
923
+ ### 24. Recommended: give missing paths a course page
924
+
925
+ Starlight asks Astro for the docs entry `404` during every build. Its fallback
926
+ works, but Astro warns `Entry docs → 404 was not found`. Add
927
+ `content/docs/404.mdx`; adapt the title and tagline to the course:
928
+
929
+ ```mdx
930
+ ---
931
+ title: "404: Page outside the syllabus"
932
+ description: The requested course page could not be found.
933
+ template: splash
934
+ hero:
935
+ title: "404"
936
+ tagline: We followed this URL as far as the course map goes. It does not land on a page.
937
+ pagefind: false
938
+ sidebar:
939
+ hidden: true
940
+ editUrl: false
941
+ ---
942
+
943
+ import { LinkButton } from '@astrojs/starlight/components';
944
+
945
+ <div style="display: flex; justify-content: center;">
946
+ <LinkButton href={import.meta.env.BASE_URL} icon="left-arrow" iconPlacement="start">
947
+ Return to the course
948
+ </LinkButton>
949
+ </div>
950
+ ```
951
+
952
+ `import.meta.env.BASE_URL` points home from the originally requested URL and
953
+ survives a subpath deployment. Let the docs entry own
954
+ `/404.html` rather than colliding with Starlight's injected route:
955
+
956
+ ```js
957
+ starlight({
958
+ disable404Route: process.env.NODE_ENV === 'production',
959
+ // …
960
+ })
961
+ ```
962
+
963
+ The special route catches arbitrary missing URLs in development. The authored
964
+ docs entry owns `/404.html` in a static build, without a duplicate route.
965
+
966
+ Also give Astro the published
967
+ origin so Starlight can build its sitemap without its `site` warning; leave the
968
+ subpath in `base`:
969
+
970
+ ```js
971
+ export default defineConfig({
972
+ site: 'https://example.edu',
973
+ base: '/course/',
974
+ // …
975
+ });
976
+ ```
977
+
978
+ No placeholder `content/i18n/en.json` is needed. The `i18n` collection exported
979
+ by `explico/content-config` supplies an empty English entry only when the course
980
+ has no authored translations; real files under `content/i18n/` replace it.
981
+
982
+ ### 25. The page's zones and slide edges: nothing to do, unless your CSS or screenshots read them
983
+
984
+ Every page now tells its parts apart by colour: both sidebars take one step
985
+ of a neutral grey and the header a step further (`appearance.zones:
986
+ "tonal"`), and each slide on a deck's page has a visible edge and a drop
987
+ shadow, a step lighter than the page in the dark theme (`appearance.slides:
988
+ "lifted"`). A hovered row in either sidebar now fills in the dark theme too
989
+ (it had no fill there), and the part holding the open page is tinted a step
990
+ darker than before, so it stays visible on the grey. The manifest schema is
991
+ now v37, with `appearance`.
992
+
993
+ No course change is required. To choose another look, add the block to
994
+ `content/course.settings.json`:
995
+
996
+ ```json
997
+ "appearance": { "zones": "paper", "slides": "edge" }
998
+ ```
999
+
1000
+ `zones`: `tonal` (default), `accent-header` (the header a deep band of the
1001
+ course accent; on it, a course with a mark for each theme shows its
1002
+ dark-theme mark), `paper` (header and sidebars a desk, the reading column a
1003
+ sheet on it) or `none` (Starlight's colours, as before). `slides`: `lifted`
1004
+ (default), `edge` (edge and shadow on the page's colour) or `sorter` (the
1005
+ deck on a tinted well). When the course keeps a settings schema with
1006
+ `"additionalProperties": false`, copy the playground's `appearance` property
1007
+ into it, or the settings form flags the block.
1008
+
1009
+ Only if a course's CSS or browser tests read these:
1010
+
1011
+ - **Screenshot tests** of any page see the new header and sidebar colours,
1012
+ and of a deck's page the new slide edges: refresh their baselines, or set
1013
+ `"zones": "none"` to keep the old frame.
1014
+ - **Course CSS that set `--sl-color-bg-sidebar` or the header's background**
1015
+ now competes with `src/styles/zones.css`, which sets them from the
1016
+ `--ex-zone-*` tokens. Set the tokens instead (`--ex-zone-header`,
1017
+ `--ex-zone-rail`, `--ex-zone-toc`, their `-edge`s), on `:root` and
1018
+ `:root[data-theme='light']`.
1019
+ - **Course CSS for a slide's border or shadow** sets `--deck-stage-edge`
1020
+ and `--deck-stage-shadow`.
1021
+
1022
+ ### 26. Verify
1023
+
1024
+ ```bash
1025
+ pnpm validate:content # passes on its first run; a tracked report it rewrote is the one failure
1026
+ pnpm content check --changed
1027
+ astro check
1028
+ astro build
1029
+ ```
1030
+
1031
+ `astro build` prints no deprecation line for the markdown plugin lists after
1032
+ step 2 and none of the missing-404, empty-`i18n`, or sitemap notices after step
1033
+ 24. `validate:content` writes `content/versions.json` (step 10) on its
1034
+ first run and passes; commit the file. With step 13, a second `astro build` reports every
1035
+ PDF as reused.
1036
+
36
1037
  ## 0.13.0 → 0.13.1
37
1038
 
38
1039
  **No course changes required.** This release has one fix, in the engine's
39
1040
  stylesheet: hovering a symbol in an equation, or in a card's formula, no longer
40
1041
  paints its highlight over the glyph to its left. No authoring rule,
41
1042
  `CourseConfig` field, Astro wiring, course script or CLI command changes. The
42
- manifest schema stays v29.
1043
+ manifest schema stays v31.
43
1044
 
44
1045
  ### 1. Update the dependency
45
1046
 
46
1047
  ```bash
47
- pnpm add -D explico@0.13.1
1048
+ pnpm add explico@0.13.1
48
1049
  ```
49
1050
 
50
1051
  **Coming from 0.12.0 or earlier:** apply "0.12.0 → 0.13.0" below first, and
@@ -561,7 +1562,23 @@ it (a MAJOR bump under rule C2, step 20).
561
1562
  - **`resolveMathGlyphs`** returns `indices` and `derived` and takes a
562
1563
  `derive` option. A course test that compares its whole result with
563
1564
  `toEqual` sees an unresolved item carry `lenders` and a decoration carry
564
- `wholeFormExplicit`.
1565
+ `wholeFormExplicit`. A bare symbol that two entries share only through
1566
+ forms with a fixed label no longer throws `GlyphResolutionError`
1567
+ ("is the base glyph of more than one page symbol"): with the entries `P_0`
1568
+ and `P(y)`, `P` resolves to the card of `P(y)`. A test that expected the
1569
+ throw moves.
1570
+ - **A display equation is a `$$` on lines of its own.** The helpers that read
1571
+ a lesson's text (`scanEquationLabels`, `scanDisplayEquations`,
1572
+ `countDisplayEquations`, `validateEquations`; `explico/reference/equations.mjs`
1573
+ and `…/equations-validate`) follow remark-math. `$$ a=b \label{eq:x} $$` on
1574
+ one line is inline math there: 0.12.0 counted it, this release returns
1575
+ nothing for it. A course test whose fixture writes a display equation on one
1576
+ line gets empty results; put `$$` on a line, the equation on the next, and
1577
+ `$$` on a third, as `docs/authoring/structure.md` writes it.
1578
+ - **A test that compiles the whole course** with `compileManifest` renders
1579
+ every lesson's math, so on a large course it can pass vitest's 5 s default
1580
+ timeout. Give it an explicit one: `it('…', { timeout: 30_000 }, async () => {
1581
+ … })`.
565
1582
  - **The `content` CLI's JSON** loses `draftDebt` and `lesson.status` (step
566
1583
  3). Its lists now include the last build's render findings (from
567
1584
  `build/render-diagnostics.json`) under a `render:` source, such as
@@ -618,8 +1635,11 @@ of its width in the article, and diagram boxes are measured (step 13).
618
1635
  Rule V7 holds a `<Figure>`'s picture (plot, key and tables) to 80% of the
619
1636
  viewport height, or `rules.V7.maxPercent`, so the reader sees it with its
620
1637
  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:
1638
+ the course's Playwright suite. **Skip this step if no lesson has a `<Figure>`**
1639
+ (a lab that is not inside one is not checked): `lessonsWithFigures(manifest)` is
1640
+ then empty and the file defines no test, so running it alone, as
1641
+ `playwright test tests/e2e/figure-fit.spec.ts` does, fails with "No tests
1642
+ found". Otherwise add it there, at each viewport the course supports:
623
1643
 
624
1644
  ```ts
625
1645
  import { readFileSync } from 'node:fs';
@@ -759,7 +1779,7 @@ or removed. The manifest schema is now v25 (step 8).
759
1779
  ### 1. Update the dependency
760
1780
 
761
1781
  ```bash
762
- pnpm add -D explico@0.12.0
1782
+ pnpm add explico@0.12.0
763
1783
  ```
764
1784
 
765
1785
  ### 2. A lab never calls KaTeX itself (rule V3, rule N4)
@@ -931,6 +1951,15 @@ helpers directly:
931
1951
  a lesson id or a `manifest.pages[].id`. Its errors, and the local-notation
932
1952
  parity error, are reworded ("on page …", "notation mismatch"). Update a
933
1953
  test that matched the old text.
1954
+ - `remarkNotation` over a page that is not a chapter binds its math (step 5)
1955
+ instead of leaving it alone. A test that fed it `src/content/docs/about.md`
1956
+ and expected the text back unchanged now gets `Unresolved notation in page
1957
+ math: … (rule S10)` for each symbol with no shared entry. Give the fixture a
1958
+ shared entry for the symbol, or remove the math from it.
1959
+ - A page's own `notation.local` entries seed its bundle (step 7), so
1960
+ `registry.bundles[n].definitionIds` and `manifest.notation.bundles` now list
1961
+ them as `page:<lessonId>:<key>` next to the shared ids. A test that compares
1962
+ a bundle's ids with `toEqual` adds those.
934
1963
  - `.lab-math` no longer scrolls or pads; its `.katex-display` does. A
935
1964
  float's caption line is at least `1.5rem` tall while it shows a tool
936
1965
  button.
@@ -967,7 +1996,7 @@ v24.
967
1996
  ### 1. Update the dependency
968
1997
 
969
1998
  ```bash
970
- pnpm add -D explico@0.11.1
1999
+ pnpm add explico@0.11.1
971
2000
  ```
972
2001
 
973
2002
  **Coming from 0.8.0:** follow the "Coming from 0.8.0" note of "0.10.0 →
@@ -1050,7 +2079,7 @@ the check.
1050
2079
  ### 1. Update the dependency
1051
2080
 
1052
2081
  ```bash
1053
- pnpm add -D explico@0.11.0
2082
+ pnpm add explico@0.11.0
1054
2083
  ```
1055
2084
 
1056
2085
  ### 2. Delete `disclosure.diagrams` (the build fails until you do)
@@ -1425,7 +2454,7 @@ seven bumps (step 6).
1425
2454
  ### 1. Update the dependency
1426
2455
 
1427
2456
  ```bash
1428
- pnpm add -D explico@0.10.0
2457
+ pnpm add explico@0.10.0
1429
2458
  ```
1430
2459
 
1431
2460
  0.10.0 was never published to npm. Coming from npm's 0.8.0, install 0.11.0
@@ -2019,7 +3048,7 @@ schema version is unchanged.
2019
3048
  ### 1. Update the dependency
2020
3049
 
2021
3050
  ```bash
2022
- pnpm add -D explico@0.9.0
3051
+ pnpm add explico@0.9.0
2023
3052
  ```
2024
3053
 
2025
3054
  0.9.0 was never published to npm. Coming from npm's 0.8.0, install 0.11.0
@@ -2323,7 +3352,7 @@ previously worked around may now be written directly.
2323
3352
  ### 10. Verify
2324
3353
 
2325
3354
  ```
2326
- pnpm add -D explico@0.8.0
3355
+ pnpm add explico@0.8.0
2327
3356
  pnpm validate:content # twice — the first run regenerates build/resolution/*
2328
3357
  pnpm content check --changed
2329
3358
  pnpm content warnings # expect new delimiter-manual-size findings (item 3)