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