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
package/CHANGELOG.md CHANGED
@@ -14,6 +14,993 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.15.0] - 2026-10-12
18
+
19
+ ### Added
20
+
21
+ - **Rule D11: a deck is flagged when its chapter changed after it did.**
22
+ `deck-stale`, an info diagnostic, names a lesson's own deck whose chapter's
23
+ rule C5 version is newer than the deck's — two files mean the chapter and
24
+ the deck can drift apart with nothing else to notice it. Scoped to a
25
+ lesson's own deck; a part's deck covers more than one chapter, so it has
26
+ none to compare against and is never flagged. `page-versions.ts` gains
27
+ `deckStaleDiagnostics`; the diagnostic joins `diagnostics.math` beside
28
+ D1–D10's frame codes. No manifest shape change.
29
+ - **`content help <verb> [--json]`: each verb's contract, machine-readable.**
30
+ `content help symbol` prints the verb's usage, what it is for, its flags,
31
+ what it writes, when it exits 1, and the shape of its `--json` as a tree of
32
+ named types; `--json` prints the same with a self-contained JSON Schema
33
+ (draft 2020-12). `content help --json` prints the whole reference. The verb
34
+ table is one source, `src/compiler/cli-verbs.ts` (`CONTENT_VERBS`, from which
35
+ `CONTENT_USAGE` is now derived, every line ending in `[--json]`);
36
+ `scripts/agent/gen-cli.mjs` reads the TypeScript report types with the
37
+ compiler API and writes `docs/authoring/cli-reference.json` and
38
+ `docs/authoring/cli.md`, both shipped, `--check` in verify and CI. New
39
+ module `explico/compiler/cli-reference` (`loadCliReference`, `verbHelp`,
40
+ `schemaOf`, `formatVerbHelp`, `cliReferenceMarkdown`, and
41
+ `jsonShapeErrors`, which checks a value against one of the schemas).
42
+ `cli-main` exports the named `--json` types its verbs print beside the
43
+ reports: `RulesJson`, `EditPlanJson`, `NewJson`, `ErrorJson`.
44
+ - **`content verify [--e2e] [--force]`: a course's one gate before handing
45
+ work back.** `validate:content` → `content check --changed` → `astro check`
46
+ (skipped, by name, when `@astrojs/check` is not installed) → the course's
47
+ `test` script → `astro build`, and with `--e2e` its `test:e2e` script (or
48
+ `playwright test`). Each step is its own process with its whole output in
49
+ `node_modules/.cache/explico/verify/<step>.log`; the run prints one line per
50
+ step, the tail of each failed step's log, and exits 1 on a failure. A pass
51
+ records the tree it left (git HEAD, the diff against it, the untracked
52
+ files), and a rerun on the same tree that asks for no more answers from the
53
+ record without running anything. New module `explico/compiler/cli-verify`
54
+ (`runVerify`, `VerifyReport`).
55
+ - `runContentCli` takes an optional fifth argument, a manifest already
56
+ compiled for the course, so a test running many verbs compiles once.
57
+ - **`content release [major|minor|patch] [--write]`: a content release in
58
+ every course.** `## [Unreleased]` in `content/CHANGELOG.md` becomes a dated
59
+ `## [x.y.z]` heading and `about.version` becomes `x.y.z` (rule C3), shown as
60
+ a diff and written with `--write`, like `move`, `rename` and `fix`; the bump
61
+ defaults to what the section's headings imply (rule C2), and the date is the
62
+ local one. It was the playground's own `scripts/release-content.ts`, which
63
+ the shipped `release-content` skill called as `pnpm release:content` in
64
+ courses that never had it. The playground's `release:content` script is now
65
+ `content release --write`. New module `explico/compiler/content-release`
66
+ (`releaseEdit`, `planContentRelease`); new error code `no-changelog`.
67
+
68
+ ### Changed
69
+
70
+ - `content init-agent` says `new` or `differs from the engine's copy` for each
71
+ file, where a dry run used to say `installed`; after `--write` every edit
72
+ verb prints what to run next.
73
+ - `content status` lists at most six of the pages with unreleased changes and
74
+ counts the rest; `content context` says how many of a lesson's diagnostics
75
+ sit in its own file and how many in the shared cards it shows (the first is
76
+ what `diagnostics --lesson` lists).
77
+ - The shipped procedures (`docs/course-agent/`) survive a course's prettier:
78
+ no inline code span crosses a line break, which prettier re-indented out of
79
+ its list item. `gen-map --check` fails on one.
80
+ - `migrating.md`, 0.13.1 → 0.14.0, corrected where upgrading
81
+ credit-and-derivatives found it wrong: the patch goes before `pnpm add`
82
+ (pnpm refuses with `ERR_PNPM_UNUSED_PATCH`) and its entry may sit in
83
+ `pnpm-workspace.yaml`; pnpm 11's `minimumReleaseAgeExclude`; a course that
84
+ commits `build/resolution/` fails its first run after the upgrade; stale
85
+ `lint-ignore.yml` lines and a course's own tier guidance (step 20); the 404
86
+ page without a raw `<div>` (step 24); tests comparing sidebar and page
87
+ colours (step 25). Upgrading time-series added two more: pnpm can instead
88
+ stop with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` (step 1), and the first
89
+ `content/versions.json` goes in before the upgrade's own changelog bullet,
90
+ or every page reads "not yet released" (step 10).
91
+ - `content fix` takes several lessons, or `--all`, as one plan: the lessons
92
+ with nothing to fix are counted, not listed. A course upgrading to 0.14.0
93
+ can have a thousand redundant `\explain` wraps (data-science-I: 1193).
94
+ - `content check` lists a lesson's errors and warnings line by line and
95
+ counts its info by code, with the command that lists them; `--json` is
96
+ unchanged.
97
+ - `definition-repeated` on a local entry whose glyph a shared card already
98
+ owns no longer says "move one copy to `content/notation/`": promoting it
99
+ would put two shared cards on one glyph. It says the entry is a reused
100
+ letter (rule N4) and how to acknowledge the copies; `content move` prints a
101
+ caution when the glyph is taken (time-series: 9 of its 34).
102
+ - `dia-too-wide` on `<CurriculumMap>`'s competency graph names the map's own
103
+ props (`graph="per-part"`, `direction`, `warnAboveWidth`) instead of a node
104
+ `width` and a shorter label, which the author does not write there.
105
+ - The rules-in-effect page says the course's `rules` and `courseRules` live
106
+ in `course.settings.json` or `course.config.ts`, not only the first.
107
+
108
+ ### Fixed
109
+
110
+ - `content move` wrote the new card a placeholder body ("Extend this body
111
+ with what a glossary reader needs."), which the glossary would publish, and
112
+ `domain: general` for a key without a prefix. The card now has no body, its
113
+ domain is the key's prefix when the course has that domain, else the one
114
+ most of the lesson's cards have; the plan says that the body is the
115
+ author's to write, and when the entry had no `label`.
116
+ - `validate:content`'s run log recorded each consistency warning
117
+ (`definition-repeated`, `glyph-unique-in-corpus`, …) at INFO and their
118
+ one-line digest as the only WARNING, so its `Run log: (… warning(s))` count
119
+ disagreed with `content status`. Each finding is recorded at WARNING now,
120
+ the digest at INFO.
121
+ - `ContextReport`'s `equations`, `tables`, `figures` and `diagrams` are typed
122
+ `ManifestLessonFloat[]`: `content context --json` always printed each
123
+ float's `short` and `caption`, which the type left out. The playground's
124
+ new `--json` contract test runs every read-only verb against the schemas and
125
+ caught it.
126
+
127
+ ## [0.14.0] - 2026-10-11
128
+
129
+ ### Breaking
130
+
131
+ - **A symbol no entry binds in a notation entry's body fails `validate:content`
132
+ (rule N4), and the build fails on an entry whose body did not render.** The
133
+ Markdown body of a `content/notation/*.md` entry was the one rendered-math
134
+ context no gate read. `remarkNotation` rejected an unbound `$w_3$` there, but
135
+ Astro's content layer only logs a render error (`[ERROR] [glob-loader]`) and
136
+ keeps the entry without its body, so `astro build` ended green and the
137
+ glossary showed the entry with no text. `gateContentMath` now resolves every
138
+ `$…$` / `$$…$$` and currency `$` in an entry's body (`unresolved-glyph`,
139
+ `invalid-latex`, `currency-dollar`, …, context `body`) at the file's own
140
+ line, against the table the render uses: every shared entry, built by the new
141
+ `registry-scope.mjs` for both. `NotationGlossary.astro` throws, naming the
142
+ files, when an entry with a body has none rendered, whatever the cause. No
143
+ diagnostic code is new. Measured on the three downstream courses and the
144
+ playground (109 body spans), nothing fails.
145
+ - **Each chapter and part page carries the knowledge version its content
146
+ last changed in (rule C5), kept in `content/versions.json`, which
147
+ `validate:content` writes and the course commits.** `MANIFEST_SCHEMA_VERSION`
148
+ is now v32. The course's version moves at every release, whatever the
149
+ release touched, so stamped on a chapter it said only "built after 0.4.0",
150
+ and in every PDF six times over, so a content release changed every LaTeX
151
+ cache key. Now a chapter's PDF, its source zip's README, its Markdown and
152
+ the `[?]` panel (a new "This page" row, `data-page-since`) say "unchanged
153
+ since version 0.2.0", the release its content last changed in, or "with
154
+ changes after version 0.4.0, not yet released" while `## [Unreleased]`
155
+ holds a change; a part's and the course's PDF and Markdown carry the newest
156
+ among their pages. A document unchanged since an earlier release keeps
157
+ that release's date (`SOURCE_DATE_EPOCH`, its figures' dates) and its
158
+ trailer `/ID`, so with `EXPLICO_LATEX_CACHE` a release recompiles only the
159
+ documents it changed. What counts as the page: its file, its `.checks.yml`
160
+ and assessments, the shared notation entries its text uses, the sources it
161
+ cites and its numbers. `content/versions.json` is a record like a lockfile:
162
+ `compileManifest` reads the committed one and works the next out
163
+ (`page-versions.ts`: a release takes every pending change; an unchanged
164
+ page keeps its version; a change waits while `## [Unreleased]` has
165
+ bullets), `validate:content` writes it on a developer's machine
166
+ (`syncPageVersions`) and in CI (`CI` set) fails with `page-versions-stale`
167
+ until the committed file is the one it would write — **so a course's first
168
+ build after upgrading needs one `pnpm validate:content` and a commit of
169
+ `content/versions.json`**. A file that is not such a record warns
170
+ `page-versions-invalid` and is rebuilt. The manifest gains
171
+ `lessons[].since`, `parts[].overview.since`, `about.knowledge.releases`
172
+ (every released entry with its date) and `about.knowledge.unreleased`, and
173
+ `pageVersions`; `content status` reports `pageVersions` (`tracked`,
174
+ `unreleased`), `content context` a lesson's `since`.
175
+ `explico.cls`: `\ExplicoSource`'s second argument and `\ExplicoTitlePage`'s
176
+ third are the phrase, printed as given ("From *Course*, unchanged since
177
+ version 0.2.0."; "Content unchanged since version 0.2.0"), where they were
178
+ a bare version. Rule off (`rules.C5.enabled: false`): nothing is read or
179
+ written, and every page carries the course's version as before.
180
+
181
+ - **The course asks each reader before it keeps their reading in their
182
+ browser, and keeps to the answer.** A small, non-blocking question at the
183
+ bottom of the window ("Remember your reading?", `ReadingMemoryNotice.astro`,
184
+ rendered by the header on every page) offers "Remember" and "Don't
185
+ remember", the same size and weight. Reading memory is what records what a
186
+ reader did across visits: the chapters they opened (the sidebar marks, and
187
+ the note under a changed page's title), the cards they keep open on a page
188
+ (`card-memory.ts`) and the course's last set of carried cards
189
+ (`card-carry.ts`). Until the reader says yes, and after a no, none of it
190
+ outlives the tab: no visit is recorded and no mark shown, and kept and
191
+ carried cards live in the tab's session storage, so Back, a reload and a
192
+ link still bring them back until the tab closes. **Kept and carried cards
193
+ therefore no longer come back after a browser restart, or in a new tab,
194
+ unless the reader said yes**; before, they always did. Settings a reader
195
+ chooses (theme, text size, panel layout, a folded section or part, the
196
+ "Carry cards" switch) are kept as before and need no answer. The answer is
197
+ kept per course (`explico-reading-memory:<base path>`) for 13 months, then
198
+ asked again; a no deletes the course's reading memory at once. The `[?]`
199
+ panel gains an "In this browser" section: a "Remember my reading" switch,
200
+ every copy of the panel in step, and "Forget what this course stored",
201
+ which deletes every key of the course (and the settings explico courses on
202
+ the site share, and Starlight's theme) and asks again. Nothing explico
203
+ stores is a cookie, and nothing leaves the browser. The new
204
+ `readingMemory` setting (`"ask"`, default, or `"off"`: never ask, never
205
+ keep reading memory, no marks) is in the manifest as `readingMemory`
206
+ (v32). `session/reading-memory.ts` holds the logic; `PreferenceStore`
207
+ gains an optional `keys()`.
208
+
209
+ ### Added
210
+
211
+ - **The docs point at two downloadable courses.** The README, the shipped
212
+ agent guide, the rule index and the course `AGENTS.md` baseline link
213
+ https://danieltuzes.github.io/explico/start/, where the playground (at the
214
+ engine's latest release, standalone) and credit-and-derivatives are
215
+ downloadable at their newest version, with a machine-readable
216
+ `sources.json`; a `playground/…` path in the docs is a file of that
217
+ download.
218
+ - **The agent procedures ship to courses, and `content init-agent` installs
219
+ them.** `docs/course-agent/` (in the package) holds the `draft-lesson`,
220
+ `migrate-course` and `release-content` skills, the `rules-reviewer`
221
+ sub-agent and a baseline course `AGENTS.md` that points at the engine's
222
+ guide; `pnpm content init-agent [--write]` copies the skills and the agent
223
+ into the course's `.claude/` and writes the baseline only where the course
224
+ has no `AGENTS.md`. `content status` says whether the course's `AGENTS.md`
225
+ points at `docs/authoring/agents.md` and whether the procedures are
226
+ installed (`agentGuide` in the JSON). `docs/authoring/agents.md` replaces its
227
+ paragraph of verbs with a table: what you want to know, the verb, what it
228
+ prints.
229
+ - **The `content` CLI and `validate:content` ship in the engine.**
230
+ `runContentCli(courseConfig, argv?, io?)` (`explico/compiler/cli-main`) is
231
+ the whole router — every verb, the usage text (`CONTENT_USAGE`, printed by
232
+ `pnpm content help`), the flag parsing (`parseContentArgs`), the text output
233
+ and the exit code — and `runValidateContent(courseConfig)`
234
+ (`explico/compiler/validate-content`) the whole validate run. A course's
235
+ `scripts/content.ts` and `scripts/compile-manifest.ts` become three-line
236
+ shims, so a verb added to the engine reaches every course with the package;
237
+ the playground's two scripts are those shims.
238
+ - **The `appearance` setting: how the page tells its parts apart, and how a
239
+ deck's slides sit on it.** `zones` is `tonal` (the default: both sidebars
240
+ one step of Starlight's neutral grey, the header a step further, the
241
+ reading column unchanged), `accent-header` (the header a deep band of the
242
+ course accent in both themes, drawn on dark-theme colours, and the
243
+ dark-theme logo on it; its menus and the search dialog keep the page's
244
+ colours), `paper` (header and sidebars a desk, the reading column a sheet
245
+ on it whose edge casts a soft shadow, a step lighter than the desk in the
246
+ dark theme) or `none` (Starlight's colours). `slides` is `lifted` (the
247
+ default: an edge and a drop shadow, and in the dark theme a sheet a step
248
+ lighter than the page), `edge` (the edge and shadow on the page's colour)
249
+ or `sorter` (the deck on a tinted well). `LayoutHeader.astro` writes the
250
+ choice on `[data-explico-zones]` / `[data-explico-slides]`, and the new
251
+ `src/styles/zones.css` and `deck.css` select it with `:root:has(…)`, so it
252
+ is right in the first painted frame; each style is a set of `--ex-zone-*`
253
+ / `--deck-stage-*` tokens per theme that a course can retune. Screen only:
254
+ print, Present mode and a figure captured for the exports are unchanged.
255
+ `normalizeAppearance` validates it; the manifest carries it as
256
+ `appearance`, and the manifest schema is now v37.
257
+ - **Released courses now receive copy-ready 404-page instructions** in the
258
+ README, the structure guide and the current migration section: add
259
+ `content/docs/404.mdx` as a splash page, use Astro's `BASE_URL` to get home
260
+ from any missing path, and set Astro's `site` origin so Starlight can emit its
261
+ sitemap without a warning. The playground turns the missing URL into a
262
+ trajectory that misses its plotted page and exercises an actually missing
263
+ route.
264
+ - **The contents list and a part's page show each lesson's slides**, when the
265
+ course has a deck. A slides column ends every row of `<CourseContents>` and
266
+ of `<PartOverview>`'s reading list: a lesson's own deck is a tile on its
267
+ title line, and a part's deck is one bar along the chapters it covers, from
268
+ the first covered title to the last, each cell a link to that chapter's
269
+ section of the deck, named and described as the sidebar names it ("Slides:
270
+ Velocity Under Constant Acceleration" / "Slides 17–25 of the part's
271
+ slides"). A part's deck that skips a chapter breaks into runs; one that
272
+ covers a chapter with its own deck runs in a second column. Each title
273
+ starts with the glyph of what it opens (a chapter's sheet, a slides-only
274
+ lesson's frame), a part's heading with Overview's two sheets
275
+ (`--explico-part-glyph`, `global.css`), and the contents list's counts gain
276
+ the decks ("4 slide decks") and a key to the glyphs. A slides-only lesson's
277
+ tile is out of the tab order and hidden from assistive technology: its
278
+ title opens the same page. A course with no deck renders both lists as
279
+ before; `slides={false}` on either component turns it all off. The cells
280
+ come from `deck-lane.ts` (`deckLanes`, `laneLink`).
281
+ - **The glossary lists page-bound meanings.** Under its index, "On one page
282
+ only" lists every `notation.local` entry (linking to its lesson) and every
283
+ gloss (linking to the card whose formula names it), so a reader who looks a
284
+ letter up always finds it and sees which meanings are course-wide and which
285
+ are page-bound; the cards themselves stay the shared entries.
286
+ - **`content move`, `content rename` and `content fix`: the refactors as
287
+ verbs, dry-run by default.** `move <lesson>:<key> [--to <key>]` turns a
288
+ `notation.local` entry into a shared card written from the entry's own
289
+ fields, removes the copy from every lesson that declares the key (reporting
290
+ a copy that differed) and, with a new key, rewrites every `[[key]]` and
291
+ `\explain{key}`; `rename <old> <new>` renames a shared card and everything
292
+ that names it, `seeAlso` and `lint-ignore.yml` included; `fix <lesson>`
293
+ unwraps every `\explain` the gate reports as redundant. Each prints its
294
+ plan as a unified diff and changes nothing without `--write`. `planMove`,
295
+ `planRename`, `planFix`, `renderEditPlan`, `applyEditPlan`,
296
+ `removeLocalItem`, `rewriteKeyReferences` and `sharedCardFromLocal` are
297
+ exported from `explico/compiler/cli`.
298
+ - **`definition-repeated` (warning), `meaning-repeated` (info) and
299
+ `explain-redundant` (info), under rule N4.** One local key declared in two
300
+ lessons, saying whether the copies agree and that moving one copy to
301
+ `content/notation/<key>.md` keeps every `[[key]]` working; one symbol and
302
+ name defined at two sites in any tier (a local card and a gloss, two
303
+ glosses, a local card beside the shared card that already says it); and an
304
+ `\explain{key}{…}` in page math whose bare symbol already resolves to the
305
+ same card with nothing left over. The first two come from `consistency.ts`
306
+ (`checkDefinitionRepeated`, `checkMeaningRepeated`), the third from the
307
+ completeness gate, located at the span; `explainCalls` and
308
+ `redundantExplainWraps` are exported from `explico/reference/gate-math`.
309
+ - **`content equation "<latex>" [--lesson <id>]` resolves one expression the
310
+ way the gate will.** Against the lesson's scope (its `notation.local` and
311
+ the shared cards it introduces, with its suspended decorations) or, with no
312
+ lesson, against every shared card as a notation entry's body is; prints
313
+ each glyph's card, redundant `\explain` wraps, unresolved glyphs with where
314
+ they are defined, and exits 1 when the build would refuse the span. A few
315
+ lines, while drafting. `contentEquation` and `formatEquationReport` are
316
+ exported from `explico/compiler/cli`.
317
+ - **`content migrate [--from <v>] [--to <v>]` prints the migration an
318
+ upgrade needs.** The guide's `## <from> → <to>` sections from the version
319
+ `package.json` pins at HEAD to the one installed (either end overridable),
320
+ oldest first, each section whole; a `0.4.x` range on the left matches its
321
+ patch versions, `Unreleased` is allowed on the right, and a hole in the
322
+ chain is an error that names the version no section continues from. One
323
+ or two sections instead of the forty-thousand-token file. `content status`
324
+ sizes that migration among the agent surfaces when the pin and the install
325
+ differ. `contentMigrate`, `parseMigrationSections`, `selectMigrationPath`,
326
+ `installedEngineVersion`, `pinnedEngineVersion` and `formatMigrateReport`
327
+ are exported from `explico/compiler/cli`.
328
+ - **`content bindings <lesson>` shows what a lesson's math binds before a
329
+ build.** Span by span: each glyph and its card with how it resolved
330
+ (`canonical`, `family`, `derived`, `base`, `operator`, or an explicit
331
+ wrap), every `\explain{key}{…}` whose bare symbol resolves to the same card
332
+ (a redundant wrap), and every unresolved glyph with where `content symbol`
333
+ finds it defined; the counts first. The same resolver and scope the
334
+ completeness gate uses. `contentBindings`, `spanBindings` and
335
+ `formatBindingsReport` are exported from `explico/compiler/cli`.
336
+ - **`content rule <ID>` prints one rule whole.** The catalog entry as this
337
+ course has it (kind, elements, enforcement, editions, on or off, waivable,
338
+ codes, settings in effect, summary) and the rule page's own section sliced
339
+ by heading, example included, about half a thousand tokens where opening
340
+ the page costs twenty thousand; an invariant by its slug, a retired rule
341
+ with its date and successor, an unknown ID lists the rules in effect and
342
+ exits 1. `contentRule`, `formatRuleReport`, `ruleSection` and
343
+ `authoringDocsDir` are exported from `explico/compiler/cli`.
344
+ - **A lesson can have a deck: its slide edition, `<lesson>.deck.mdx`**
345
+ (`docs/authoring/deck.md`). A deck sits beside its chapter, served at
346
+ `/<part>/<lesson>/slides/` and listed under the chapter in the sidebar, or
347
+ alone, as a slides-first lesson at the lesson's own URL. `##` opens a section
348
+ (a divider frame), `###` is a frame whose heading is its title,
349
+ `<Pause />` is a step, `<Notes>` holds the speaker notes and
350
+ `<Recall ref="eq-…" />` (`fig-`, `tab-`, `dia-`) shows the chapter's float with
351
+ the chapter's number; none needs an `import`. The engine writes the title
352
+ frame, the outcomes frame (`deck.outcomesFrame`) and the references frame, and
353
+ a legend line on each frame naming the symbols it introduces. A deck shows its
354
+ lesson's symbols: its math binds in the chapter's scope, its notation bundle
355
+ holds the chapter's, and a lab recalled onto it reads the lesson's scope
356
+ (`labMathScope`). Its own figures, tables and diagrams are unnumbered and an
357
+ equation it labels numbers (1), (2) within it; `Figure`, `Table` and
358
+ `Diagram` read that from the manifest (`components/deck/deck-context.ts`), so a
359
+ course's wrapper forwards nothing new. Area **D** joins the rule set: rule D1
360
+ (sections of frames, locked), rule D2 (a frame fits its canvas), rule D3 (a
361
+ frame makes one claim), rule D4 (notes on a frame with text), rule D5 (a frame
362
+ title fits one line), rule D6 (at most `maxSteps` steps, each adding), rule D7
363
+ (one picture per frame), rule D8 (recall, never retype), rule D9 (at most
364
+ `maxNewSymbols` new symbols) and rule D10 (generated title and outcomes
365
+ frames), with the new invariant `decks` (`deck-frontmatter`,
366
+ `deck-outside-part`, `recall-unresolved`, `pause-invalid`). A catalog entry
367
+ now says which edition of a lesson it is about (`editions`): V7, V8, S1, S6 and
368
+ S7 are about a chapter's page, and a `D` rule says the same of a frame, so a
369
+ course's own `X` rule may name its edition too. `build/rules-in-effect.md` is
370
+ the chapter's page, as before; a course with a deck also gets
371
+ `build/rules-in-effect-deck.md` (`content rules --edition deck`).
372
+ `content new deck <lesson>` drafts a deck from the chapter. The course setting
373
+ `deck` sets the canvas (`aspectRatio`: `16:9`, `16:10` or `4:3`), the legend
374
+ line, who sees the notes, the outcomes frame and the handout PDF.
375
+ `MANIFEST_SCHEMA_VERSION` is now v33 (additive): `decks`, `deck` and
376
+ `lessons[].editions`. `explico` depends on `github-slugger` 2.0.0 (the id
377
+ Astro gives a deck's page). On a chapter, Next and Previous still step
378
+ from chapter to chapter, past a chapter's slides row; on a deck's page they
379
+ step from deck to deck, a part's and a slides-first lesson's included, each
380
+ named "Slides: *title*" (`deckNeighbours` in `deck-context.ts`). The
381
+ glossary names a chapter once, not its deck as a second page, and a deck's
382
+ "On this page" names each frame by its title as it reads, math included.
383
+ - **A deck's page presents.** It reads as a study page, every frame on a
384
+ canvas of fixed size (`deck.aspectRatio`: 800×450, 720×450 or 600×450 CSS
385
+ pixels) scaled to the column, each in its final step with its notes under it.
386
+ Its Present button shows one frame at a time, full screen: → ↓ Space PageDown
387
+ step, ← ↑ PageUp Backspace step back, Home and End, a number and Enter jump,
388
+ `F` full screen, `N` the notes beside the frame, `P` a presenter window (the
389
+ frame, the next, the notes, a clock, kept in step through a
390
+ `BroadcastChannel`), `B` a black screen, Esc leaves; the address is
391
+ `#slide-N`. A step can drive a lab (`<Pause control="Launch angle" from="15"
392
+ to="75" seconds="5" />` moves its control), light a symbol up
393
+ (`highlight="key"`), and a frame titled `### … [morph]` morphs from the one
394
+ before through View Transitions. A figure on a frame scales its tools down
395
+ and narrows its picture to fit, to no less than 60 %. After the build, each deck page is
396
+ laid out in the course's Chromium and a frame that still runs past its canvas
397
+ fails the build: `frame-overflow` (rule D2; a warning without a browser,
398
+ an error in CI). The deck offers "Continue at slide N", for the tab and,
399
+ with the reader's yes to reading memory, on later visits
400
+ (`explico:decks:<base>`, `docs/privacy.md`). Print gives one frame per
401
+ page.
402
+ - **A deck downloads as slides, a handout, LaTeX, offline HTML and Marp
403
+ Markdown.** With `lessonExports.latex`, each deck compiles in beamer with
404
+ explico's theme: `downloads/<lesson>-slides.pdf`, one page per step, every
405
+ symbol on a frame linked to its entry on the notation frames that close the
406
+ deck; `downloads/<lesson>-slides-notes.pdf`, each frame in its final state
407
+ followed by its notes and its check's answers (with `deck.handoutPdf`, and
408
+ `deck.notes` not `off`); and their source, `downloads/<lesson>-slides-latex.zip`
409
+ (`main.tex`, `handout.tex`, `deck.tex`, `beamerthemeexplico.sty`). The build
410
+ fails when the PDF numbers a frame differently from the page, and warns when
411
+ a frame runs over its slide on paper. `explico.cls` now loads
412
+ `explico-common.sty` (fonts, colours, figures, diagrams, notation markers),
413
+ which the theme shares; a chapter's and a book's LaTeX folder carry it.
414
+ Beamer cannot write a tagged PDF, so a deck's PDFs are untagged. With
415
+ `standaloneHtml`, `downloads/<lesson>-slides.html` presents offline; with
416
+ `markdown`, `downloads/<lesson>-slides-markdown.zip` holds the deck in Marp's
417
+ dialect. The Download menu has a row for the deck on its page and its
418
+ chapter's (`deck` scope, a `handout` column). A part's and the course's book
419
+ keep a slides-first lesson's place with a short chapter (its outcomes and a
420
+ link to its slides), so later chapters keep their numbers. `decks[].since`
421
+ dates a deck (rule C5): its PDFs' date, title frame and cache key. The CI
422
+ package list (`texlive-packages.txt`) adds `beamer` and `translator`.
423
+ - **Every diagnostic line names its rule and page.** Text output reads
424
+ `info consistency:weak-local [N4 · notation.md#n4]: …`, and the JSON carries
425
+ `doc` next to `rule`, so finding what a code answers to needs no second
426
+ step. `formatCliDiagnostic` and `ownerTag` are exported from
427
+ `explico/compiler/cli`; the playground wrapper prints through them.
428
+ - **`content status` sizes the agent surfaces.** One line with the estimated
429
+ tokens of `AGENTS.md` (the course's, and the repository's one level up) and
430
+ of the rules-in-effect page (`agentSurfaces` in the JSON): a budget to
431
+ watch.
432
+ - **A page that changed since the reader last opened it says so under its
433
+ title**: "This page changed since you last opened it.", with a "What
434
+ changed" link to the course's `changelogHref` when it has one, and the
435
+ sidebar's accent dot beside it. It shows once, on the visit that finds the
436
+ change, only with the reader's yes to reading memory, and not with
437
+ `sidebarMarks.changed` off. `LayoutPageTitle.astro` renders it hidden on
438
+ every chapter and part page; the sidebar's script shows it.
439
+ - **[`docs/privacy.md`](docs/privacy.md)**, shipped in the package: every key
440
+ an explico site keeps in a reader's browser, when it is written, which of
441
+ them need the reader's yes, and what a course's own privacy notice should
442
+ add (its host, anything it loads from another site).
443
+ - **`content symbol <latex | key>` says where a symbol is already defined.**
444
+ The shared card, every `notation.local` entry with its lesson and every
445
+ gloss with its card, matched on the latex as written, a shared card's
446
+ `aliases` or the key, then the entries whose notation contains it as a
447
+ glyph (`v` in `v_0`), each with label, units, the first sentence of its
448
+ meaning and, for a shared card, the lessons that bind it. One call before
449
+ declaring a symbol, so an author references what exists instead of copying
450
+ it. Read-only; `--json` for the full report. `contentSymbol` and
451
+ `formatSymbolReport` are exported from `explico/compiler/cli`; a course
452
+ adds the verb to its `scripts/content.ts`.
453
+ - **`<CurriculumMap>` takes `graph`, `direction` and `warnAboveWidth`.** A
454
+ course with many competencies got a `dia-too-wide` warning (rule V8) on the
455
+ map's one competency graph and could not act on it. `graph="per-part"` draws
456
+ one diagram per part (`competency-graph-<part dir>`), each with the
457
+ competencies its lessons teach first and the earlier ones they build on as
458
+ boxes labelled with their part, and a last group for competencies no lesson
459
+ teaches; `direction` and `warnAboveWidth` reach every competency diagram.
460
+ The default, `graph="whole"`, renders exactly what it did.
461
+ - **The sidebar marks, for each reader, the chapters and part pages they
462
+ have not opened yet and those that changed since they last opened them.**
463
+ A quiet ring for "not opened yet", an accent dot for "changed since you
464
+ last opened it". A labelled row carries its mark in the panel's margin just
465
+ past the row's end, so a title keeps its width; a compact part's tile
466
+ carries it on its corner, and the tile's tooltip names it under the title;
467
+ a closed part (the phone's menu) carries its strongest mark after its
468
+ label; and with the panel folded away, its restore control carries a
469
+ change. A line under the chapter list says what the marks mean and offers
470
+ "Mark all as seen". "Changed" compares the page's rule C5 hash (the
471
+ current one, `pending ?? source`, cut to 12 hex digits) with the one the
472
+ reader saw, so an unreleased change on the live site counts and a release
473
+ that only stamps a version on a page does not. What a reader has seen is
474
+ view state kept in their browser through the `PreferenceStore`, under
475
+ `explico-page-marks:<base path>`, so courses that share an origin keep
476
+ their own; it is reading memory (above), so it is recorded, and marks
477
+ shown, only after the reader's yes; without storage nothing is marked. Each marked link names its
478
+ mark as its description (`aria-describedby`). The new `sidebarMarks`
479
+ setting (`unopened`, `changed`, both on by default) turns either off; the
480
+ manifest carries it as `sidebarMarks` (v32). `page-marks.ts` holds the
481
+ logic, `LayoutSidebar.astro` the wiring, `global.css` the marks.
482
+ - **`withNoneSeen` and `DECK_LESSON_CLASS`.** `page-marks.ts` names both
483
+ resets of a reader's record: `withNoneSeen()`, the record with no page
484
+ seen ("Mark all as unseen", below under Changed), beside `withAllSeen`.
485
+ `lesson-derivation.ts` exports `DECK_LESSON_CLASS` (`explico-deck-lesson`),
486
+ the class of a slides-first lesson's sidebar item, beside `DECK_ROW_CLASS`,
487
+ and `sidebarLessonSlug(item)`, the lesson a sidebar item lists (a chapter's
488
+ slug, or a slides-first lesson's item), which the manifest's chapter
489
+ numbering reads. `buildSidebar` takes `slidesFirst`, the lessons whose only
490
+ edition is a deck.
491
+ - **`sidebarEditions`, and two controls in the sidebar's foot that switch
492
+ it.** A lesson with a chapter and a deck is one sidebar row (below, under
493
+ Changed); the setting says which edition the row opens, `lead: "chapter"`
494
+ (default) or `"slides"`, and whether it lists the other, `other: "listed"`
495
+ (default) or `"hidden"` (`normalizeSidebarEditions`,
496
+ `SIDEBAR_EDITIONS_DEFAULTS` in `course-config.ts`; any other value fails
497
+ the build, naming the field). Each reader switches both under the chapter
498
+ list: "Lessons open as" Chapter or Slides, a pair of radios drawn as one
499
+ control, and "List the other edition too", a checkbox. Their choice is a
500
+ setting, like a compact part, kept in their browser whatever they answered
501
+ about reading memory, under `explico-sidebar-editions` (one key for the
502
+ courses on a site, listed in `docs/privacy.md`, reset by "Forget what this
503
+ course stored" with the other shared settings), and kept field by field, so
504
+ a field they never set follows the course's default. A course without a
505
+ deck shows no controls. The manifest carries the setting as
506
+ `sidebarEditions`, and `MANIFEST_SCHEMA_VERSION` is now v34.
507
+ `sidebar-editions.ts` holds the logic, `LayoutSidebar.astro` the wiring.
508
+ - **A deck's page shows which slides Present mode has shown the reader, and
509
+ presents from any of them** (`docs/plans/deck-navigation/`, phase 3, and
510
+ the "Seen slides and glyphs" decisions). Each frame Present mode shows, at
511
+ any step, in the room's window or the presenter's, is kept as it was. A
512
+ line above each frame, outside its canvas and in the page's own type
513
+ (`.deck-frame__line`), says "Slide N" and "Shown", "Not shown yet" (the
514
+ sidebar's ring) or "Changed since shown" (its dot), and ends on a button:
515
+ "Continue here", filled and always shown, on the frame to continue at;
516
+ "Present from here" on every other (`button.deck-frame__present`, named
517
+ "Present from here, slide N"), shown while the pointer is on the frame or
518
+ the keyboard's focus is in it, with an accent ring round the frame, and
519
+ always, as its icon, on a screen without hover. Under the title, the slide
520
+ map (`nav.deck-map`) shows every frame as a numbered square linking to it:
521
+ filled once shown, outlined before, dotted when changed since, the frame to
522
+ continue at ringed and the one in view underlined (`aria-current`). It
523
+ stays under the header while the deck scrolls (`data-stuck`), and a frame
524
+ a link lands on keeps its line clear of it (`--deck-map-height`). A deck
525
+ too long for the map's line keeps its first and last frames, the frames
526
+ before and after the one to continue at and round the one in view, as many
527
+ as the line has room for, with a gap for each run left out that links to
528
+ the frame in its middle (`components/deck/deck-map.ts`: `slideMap`,
529
+ `slideMapKeeping`). "Not shown yet" and "Changed since shown" are the
530
+ sidebar's two marks, which `sidebarMarks` turns off here too; with
531
+ `unopened` off, no frame says "Shown" either. Each frame names its state as
532
+ its description (`aria-describedby`). A line under Present counts them
533
+ ("14 slides not shown yet", "2 slides changed since shown") and offers
534
+ "Mark all as seen" and "Mark all as unseen" for the deck, with a
535
+ `role="status"` line that says what a button did. Every frame of
536
+ `decks[].frames` carries `id`, 12 hex digits of a SHA-256 over what it
537
+ shows (an authored frame's source, notes included, and the chapter's
538
+ number for each float it recalls; the title frame's title, course and
539
+ authors, not its version; the outcomes frame's outcomes; the references
540
+ frame's sources), stamped on the frame as `data-frame-id`, so a frame moved
541
+ by one added before it is still the frame the reader saw.
542
+ `MANIFEST_SCHEMA_VERSION` is now v35. `buildDecks` takes `course` (the
543
+ title frame's course and authors) and each lesson's `outcomes`. The record
544
+ under `explico:decks:<base>` is now `{ "v": 2, "decks": { slug: { at,
545
+ step, seen } } }`: the frame and the step Present mode stopped at, and the
546
+ frames it showed; a version 1 record (`{ slug: frame }`) reads as one with
547
+ no frame seen. "Continue at slide N" presents from the frame the reader
548
+ stopped at, unless they were shown its every step, then from the frame
549
+ after it, following that frame to its number now when frames were added
550
+ before it. Before the reader answers the reading-memory question, the
551
+ frames shown are kept, and marked, for the tab alone, as kept cards are;
552
+ "Remember" keeps them for later visits (`mergeDeckMemory`), and "Don't
553
+ remember" clears them, keeps only where each deck stopped for the tab, and
554
+ records no frame after. With `readingMemory: "off"` no frame is recorded and
555
+ nothing is marked. The question itself now names the slides it keeps. The
556
+ resume button, the lines, the map and the marks' line are the reader's own
557
+ (`data-explico-export-chrome`): Present mode, print and a downloaded deck
558
+ show none of them. `session/deck-memory.ts` holds the logic
559
+ (`decodeDeckMemory`, `encodeDeckMemory`, `withPosition`, `withShown`,
560
+ `withAllSeen`, `withNoneSeen`, `withEveryDeckSeen`, `withEveryDeckUnseen`,
561
+ `mergeDeckMemory`, `frameMark`, `resumeAt`, `DECK_MEMORY_EVENT`),
562
+ `deck-controller.ts` the wiring.
563
+ - **A part can have a deck: `<part>/index.deck.mdx`, served at
564
+ `/<part>/slides/`** (`docs/authoring/deck.md`, "A deck for a part";
565
+ `docs/plans/deck-navigation/`, phase 4). It sits beside the part's page,
566
+ sets only `title` and `description`, and each `##` section may name the
567
+ chapter it covers, `## Title [chapter: <name>]`, the marker stripped from
568
+ the divider. A chapter may run over several sections in a row, each naming
569
+ it, as a chapter's own deck does; the first one's divider is the chapter's
570
+ address in the deck (`#slide-N`). Its symbols bind in the scope of the part's chapters, a key
571
+ two of them define binding to the first in reading order; `<Recall ref>`
572
+ shows a covered chapter's float with that chapter's number, linked to it,
573
+ the section's own chapter first; `[[eq-…]]`, `[[fig-…]]` and `[[sec-…]]`
574
+ fall back to the one covered chapter that has the key; `<AssessmentSet />`
575
+ asks the checks of its section's chapter (the engine passes `lesson`, a new
576
+ prop); the outcomes frame lists every covered chapter's competencies; the
577
+ title frame falls back to the part's label. In the sidebar it is a row of
578
+ its own right after the part's Overview, `explico-deck-row
579
+ explico-part-deck`, the deck's glyph before "Slides" and a tile of its own in a
580
+ compact part; a chapter it covers that has no deck of its own carries
581
+ `data-deck-href`, and the sidebar's script makes that address the
582
+ chapter's other edition, "Slides: *title*", unmarked (`data-deck-section`):
583
+ a lesson's own deck wins. "Slides for this chapter" goes to that section
584
+ when the chapter has no deck; the part's page offers "Slides for this
585
+ part" and the deck's page "Open the part". It downloads as every deck does,
586
+ named after its part (`downloads/motion-slides.pdf`, `-notes.pdf`,
587
+ `-latex.zip`, `-markdown.zip`, `.html`), on its page, on its part's and,
588
+ as "Its slides", on each chapter it covers that has no deck of its own;
589
+ `deckFileStem` takes the owner's slug, and `deckOwnerSlug`, `deckSaveName`
590
+ (`exports/export-pages.ts`) and `deckCheckItems` (`latex/document.ts`) name
591
+ and fill it. Its page has a version of its own (rule C5), which counts the
592
+ number, captions and equation each recalled float has in its chapter. The
593
+ `decks` invariant gains `deck-chapter-unknown`, `deck-chapter-repeated` (a
594
+ chapter named again after a section of another chapter or of the deck's
595
+ own), `deck-chapter-outside-part`, `deck-marker-misplaced`,
596
+ `deck-check-no-chapter` and `deck-route-taken`. `content new deck <part>`
597
+ drafts it from the part's chapters (a section per chapter in track order, a
598
+ frame per section, a `<Recall>` per numbered float), and `content check`
599
+ takes a deck's page, `<part>/slides` or `<part>/<lesson>/slides`
600
+ (`findCheckTarget`). `MANIFEST_SCHEMA_VERSION` is now v36: `decks[]` gains
601
+ `part`, `covers` (every deck; empty on a lesson's), `recalledFrom` and
602
+ `frames[].chapter`, and `lessonId` is absent on a part's deck. In the
603
+ engine, `DeckFile` is `LessonDeckFile | PartDeckFile` (`scope`, `partDir`),
604
+ `NotationLessonInput.scopeLessonIds` lists the lessons a deck's page binds
605
+ in (`resolvePageReferenceId`, `pageScopeLessonIds` in
606
+ `reference/references.ts`; `scopeLessonsOf` in `page-notation.ts`), and
607
+ `lesson-derivation.ts` gains `isPartDeckSlug`, `partDeckDir`,
608
+ `partDeckRouteSlug`, `partDeckPageId` and `PART_DECK_CLASS`. A course with
609
+ no part's deck sees no change but the manifest's shape.
610
+ - **`EXPLICO_LATEX_CACHE=<directory>` reuses a compiled PDF whose source did
611
+ not change.** The LaTeX export recompiled every document on every build:
612
+ 43 LuaLaTeX compiles, 685 s on a two-core CI runner, for a course of 37
613
+ chapters. With the variable set, `latex/cache.ts` keys each document by a
614
+ SHA-256 over every file of its folder, the engine, the TeX installation
615
+ (`texFingerprint`: the engine's and latexmk's banners, the LaTeX kernel's
616
+ date), the epoch and `EXPLICO_LATEX_CACHE_SALT`, and keeps its PDF, `.aux`
617
+ and log. A reused document is checked like a compiled one: its numbers
618
+ against the website's, its log for characters no font set. A failed
619
+ compile stores nothing, an entry that does not read back whole is a miss,
620
+ and entries unused for a week are deleted. The summary line says
621
+ "7 compiled, 36 reused from …". Unset, the build is unchanged. With rule
622
+ C5 (above) a document's version line and date are its own, so a content
623
+ release, too, recompiles only the documents it changed.
624
+ - **`pnpm check:dist-imports`, run by `prepack`.** It parses every module in
625
+ `dist/` and fails on a relative import whose target the package does not
626
+ ship. The repository never loads `dist/`, so such a gap (below) passes the
627
+ build, the playground and the tests.
628
+
629
+ - **[`docs/diagram-scaling.md`](docs/diagram-scaling.md)**, shipped in the
630
+ package like `docs/katex-bracket-scaling.md`: how a diagram follows the
631
+ phone's text size (below, under Changed), why it is scaled through the HTML
632
+ canvas and never the SVG, the rules for changing it, and what to check on a
633
+ real iPhone, which the end-to-end suite (Chromium) cannot.
634
+
635
+ ### Changed
636
+
637
+ - **The header, the two sidebars and the reading column are told apart by
638
+ colour, and every slide on a deck's page has a visible edge and a drop
639
+ shadow** (`appearance`, above, at its defaults). Before, the light theme
640
+ drew all three columns white, and the dark theme gave the header and the
641
+ course sidebar one colour and the contents sidebar the article's. A dark
642
+ slide had no visible edge: its border was `hairline-shade`, which in the
643
+ dark theme is the page's own colour, and its shadow was black on
644
+ near-black. The part holding the open page is tinted with the sidebar's
645
+ own accent tint (`--ex-zone-rail-accent`), a step darker than
646
+ `accent-low`, so it differs from the grey by lightness as well as hue.
647
+ The course's title in the header is its accent a step toward the text
648
+ colour (`--ex-zone-title`), which keeps it at 4.5:1 on the tinted header.
649
+ `"zones": "none"` keeps the old frame.
650
+ - **A part's deck shares the part's row with its Overview.** It was a row of
651
+ its own under the Overview, listed whatever the sidebar's two controls
652
+ said, with a chapter's deck glyph and the tooltip "Slides". The sidebar's
653
+ script now makes the two one row, as a lesson's chapter and deck are, and
654
+ it follows "Lessons open as" and "List the other edition too" the same way,
655
+ Overview standing for the chapter: "Overview" with the deck as the icon at
656
+ its end, or "Slides for this part" with Overview's two sheets there, or
657
+ one of them alone (an unlisted one still shows while it is the open page).
658
+ In a part with no Overview the deck keeps a row of its own, unlisted only
659
+ while chapters lead with the other edition unlisted. Both links keep their
660
+ glyph in every state, and a compact part joins their tiles, both in the
661
+ accent. The deck's glyph is two frames, its own in front with an outlined
662
+ frame behind (`--explico-part-deck-glyph`, `deck.css`). Its tooltip names
663
+ the part, then on a quieter second line its chapters and slides ("Slides
664
+ for this part: Motion in the plane" / "Chapters 1–2 · 26 slides"), and a
665
+ chapter's section of it names its slides ("Slides 17–25 of the part's
666
+ slides"); the second line is the link's `aria-description` and a
667
+ `small.layout-edition-detail` in its name's span, shown only where the
668
+ span is a tooltip. `buildSidebar` adds `data-part` and `data-part-edition`
669
+ to the Overview's item and the deck's, `data-title` and `data-chapters`
670
+ (the covered chapters' ordinals, `1–2`, `1, 3`) to the deck's, and the new
671
+ `withPartDeckFrames` adds, once `buildDecks` has run, `data-frames` to the
672
+ deck's item and `data-deck-range` to each chapter that links into its
673
+ section (its frames, `2–16`). `sidebar-editions.ts` gains `partRow`,
674
+ `partEditionLabel`, `partDeckDetail`, `sectionDetail`,
675
+ `PART_EDITION_ATTRIBUTE`, `PART_ROW_CLASSES` and `EDITION_DETAIL_CLASS`;
676
+ `lesson-derivation.ts` gains `numberList`. Without scripts the sidebar is as
677
+ before. No schema bump: the attributes are additive.
678
+ - **A formula's "Symbols" list names every symbol the formula names.** The
679
+ cards an entry's `formula` wraps with `\explain{key}{…}` are listed with its
680
+ glosses, in formula order, each card linked to its own entry on the
681
+ glossary page (`cards` on `<NotationFormula>`; `data-notation-card` marks
682
+ one); the list used to show the glosses alone, so a formula read as
683
+ half-explained without hover and in the exports.
684
+ - **Rule N4's lints follow the tier model: a letter is declared once.**
685
+ `glyph-unique-in-corpus` now warns only when two *shared* cards claim one
686
+ glyph; a `notation.local` entry or a gloss that reuses a letter is the
687
+ declared exception and is never reported, so the acknowledgements courses
688
+ wrote for every reused index letter can go. N4's summary and its page say
689
+ which tier a symbol takes in one question — is this letter already
690
+ defined? — and `content symbol` answers it.
691
+ - **`content diagnostics` counts by default and lists on request.** The text
692
+ output is a severity line, then counts by code (with each code's rule and
693
+ page) and by file, most frequent first and the file list capped at 25;
694
+ `--all` lists every line as before, and `--code <code>`, `--lesson <id>` or
695
+ `--changed` list a part. The JSON report is the same shape it was, filtered
696
+ when a flag is given. `summarizeDiagnostics`, `filterDiagnostics` and
697
+ `formatDiagnosticsSummary` are exported from `explico/compiler/cli`.
698
+ - **`validate:content` passes on its first run.** It still rewrites
699
+ `build/resolution/*`, but it fails only on a report git tracks that it had
700
+ to rewrite (the committed-report contract a course that tracks `build/`
701
+ relies on) and lists the rest as refreshed (`refreshed` on
702
+ `syncResolutionReports`' result). A course whose `build/` is git-ignored no
703
+ longer fails and asks for a commit of files git ignores; a "run it twice"
704
+ instruction can go. `scripts/agent/verify` runs it once.
705
+ - **The LaTeX export starts the longest documents first**: the course, then
706
+ the parts, then the chapters, the largest folder first within each. The
707
+ course took longest and started last, so it ran alone at the end while the
708
+ other cores had nothing left to do. Output is unchanged.
709
+ - **The README and the wiring errors show `markdown.processor: unified({ … })`.**
710
+ Astro 7.3 deprecates the flat `markdown.remarkPlugins` / `rehypePlugins`
711
+ lists. `assertRemarkPluginsWired` still reads both surfaces; only its
712
+ messages and the README sketch name the processor form.
713
+ `docs/authoring/migrating.md` has the step to move a course's lists.
714
+ - **The Download button's tooltip names what its menu offers.** It said
715
+ "Download this page" everywhere, but the menu lists the scopes that contain
716
+ the page: this chapter, this part and the whole course on a chapter; this
717
+ part and the whole course on a part's page; the whole course on any other
718
+ page, which has no download of its own. The `title` now reads "Download this
719
+ chapter, this part or the whole course", "Download this part or the whole
720
+ course" or "Download the whole course", in the menu's own words. The
721
+ accessible name is still "Download this page", so a browser test that finds
722
+ the button by role and name keeps working.
723
+
724
+ - **The text size on touch screens runs from 50% to 100% in five geometric
725
+ steps**: 50, 59, 71, 84 and 100%, each 2^(1/4) (about 1.19) times the one
726
+ below, listed smallest first. It was 100, 90, 80 and 70%. `global.css` has
727
+ one `:root[data-text-size='…']` rule per step and
728
+ `TextSizeControl.astro` builds the list. A size the earlier release saved
729
+ (90, 80 or 70) is ignored: the page opens at 100% until the reader picks a
730
+ new one. `<html>` now also carries `--explico-text-scale`, the choice as a
731
+ number from 0.5 to 1, for what is drawn in px.
732
+
733
+ - **A diagram follows the text size as a whole**, boxes and text together, so
734
+ a wide diagram can be made to fit a phone: the phone menu's 50% takes a
735
+ 687 px diagram to 344 px. It is scaled the way print already scales it:
736
+ the SVG stays at its build-time size (WebKit does not scale a
737
+ `foreignObject`'s HTML with the `viewBox`) and the HTML canvas around it
738
+ takes a `transform`. `Diagram.astro` therefore has a new
739
+ `div.diagram-frame` between `.diagram-scroll` and `.diagram-canvas`; it
740
+ holds `--diagram-width` and `--diagram-height` (the canvas inherits them)
741
+ and takes the scaled size, which is what the scroller measures, so the
742
+ widen button ↔ appears or goes with the size
743
+ (`DiagramEnhancer.astro` watches the frame). Course CSS or tests that
744
+ select `.diagram-scroll > .diagram-canvas` as a child must use a
745
+ descendant selector. Only a diagram in the lesson (also when expanded)
746
+ scales; one in a preview card keeps its size and the card its 12 px text.
747
+ With no size chosen, and on a screen with hover, nothing changes.
748
+
749
+ - **The sidebar draws a chapter, a part and a deck differently.** A compact
750
+ part's chapter tile is one sheet with lines of text on it (it was two
751
+ plain sheets, as the part's Overview tile still is). A deck is a filled
752
+ slide frame with a play mark cut out of it, on its "Slides" row and its
753
+ tile; a slides-first lesson's row carries it before its title, as the
754
+ Overview row carries its sheets, and its tile shows it in place of a
755
+ sheet. The manifest's `sidebar` lists a slides-first lesson as
756
+ `{ slug, attrs: { class: "explico-deck-lesson" } }` rather than its bare
757
+ slug, so `deck.css` can find it; it keeps its place and its chapter
758
+ number. The chapter's and the deck's glyphs are one SVG mask each over
759
+ the link's own colour (`--explico-chapter-glyph` in `global.css`,
760
+ `--explico-deck-glyph` in `deck.css`), the part's two sheets a border and
761
+ a shadow, so each takes the link's colour hovered, open or covered; the
762
+ Overview's second sheet now follows the tile's fill in those states.
763
+ - **A chapter's tile and its deck's tile touch**, one pair in a compact
764
+ part: square where they meet, with one line between them that lights with
765
+ whichever tile is hovered, and accent around the open page's tile. The
766
+ sidebar's script makes the two one row (below), and `li[data-editions="pair"]`
767
+ joins them; without the script, `:has()` does, and a browser without it
768
+ keeps the gap.
769
+ - **A lesson is one row in the sidebar.** A lesson with a chapter and a deck
770
+ was two rows, the chapter and a "Slides" row under it. It is one row now:
771
+ the row opens the lead edition under the lesson's title, and the other
772
+ edition is an icon at the row's end, as `»` ends a part's header, or, in a
773
+ compact part, the second of two joined tiles, the lead first
774
+ (`docs/plans/deck-navigation/`, phase 2). The icon's accessible name,
775
+ and its tooltip, is "Slides: *title*" or "Chapter: *title*", so a browser
776
+ test that finds a sidebar link by a lesson's title without `exact: true`
777
+ now finds two. A lesson with one edition carries its glyph before its title
778
+ only when that edition does not lead: a slides-first lesson while chapters
779
+ lead, as before, and a chapter-only lesson while slides lead
780
+ (`explico-chapter-lesson`, a sheet). A hidden edition is listed while it is
781
+ the open page. The row's own link carries its mark at the row's end, as
782
+ before; the icon carries its own on its corner
783
+ (`a.layout-edition-other[data-page-mark]`, drawn by `i.layout-edition-mark`),
784
+ and the progress rail counts rows, the row holding the open page being the
785
+ reader's place. The two controls (above, under Added) and the marks' line
786
+ share the panel's foot, `.explico-sidebar-foot`, which draws the separator
787
+ the line drew. Without scripts the sidebar is what the compiler emits: each
788
+ chapter, then its "Slides" row. In the manifest's `sidebar`, every item of
789
+ a lesson names its edition, lesson and title (`data-edition`,
790
+ `data-lesson`, `data-title` in `attrs`), so a chapter is `{ slug, attrs }`
791
+ where it was a bare slug; `sidebarLessonSlug` reads an item's lesson as its
792
+ `data-lesson` when that is its own `slug`, and `buildSidebar` takes
793
+ `titleOf`, the lesson titles.
794
+ - **Pointing at a part's `»` lights its Overview row**, or its compact tile,
795
+ as a pointer on the row would: both open the part's page. Keyboard focus
796
+ on `»` counts.
797
+ - **The marks' line stays, and offers "Mark all as unseen" beside "Mark all
798
+ as seen".** The line under the chapter list is there whenever the reader's
799
+ record is kept, with a mark on screen or none, so either reset can be
800
+ undone; "Mark all as unseen" forgets every page but the open one, which is
801
+ open. What a button did is said in a `role="status"` line inside it
802
+ ("Every page is marked as seen." / "… as not opened yet."), where
803
+ "Mark all as seen" used to replace the line with that sentence and leave
804
+ no way back. A button with nothing to change is `aria-disabled`: it keeps
805
+ its place, and focus.
806
+ - **The sidebar's "Mark all as seen" and "Mark all as unseen" cover every
807
+ deck's frames too** (D9 of `docs/plans/deck-navigation/`), so a deck's
808
+ page shows them all seen, or none, at once and on its next load. In a
809
+ course with a deck, the status they leave says so: "Every page and slide
810
+ is marked as seen." and "Every page is marked as not opened yet, and every
811
+ slide as not shown yet." The marks' payload (`PageMarksPayload.decks`)
812
+ carries each deck's frame ids, and a button is in use while a deck's frame
813
+ has something to change, as a page does. A deck's page now holds a
814
+ "Present from here, slide N" button in every frame's line, so a browser
815
+ test that finds the deck's Present button by name adds `exact: true`.
816
+
817
+ ### Fixed
818
+
819
+ - **A hovered row in either sidebar shows its fill in the dark theme.** The
820
+ fill was `--sl-color-gray-6`, which is the dark course sidebar's own
821
+ colour, so a hovered chapter or part there changed only its text colour.
822
+ The fill is now each sidebar's colour with 10 % ink mixed in
823
+ (`--ex-row-hover`), whatever colour the sidebar is.
824
+ - **A single-language course no longer warns that Starlight's `i18n`
825
+ collection is missing or empty.** `explico/content-config` now exports the
826
+ collection with Starlight's schema and an empty English in-memory fallback;
827
+ authored JSON or YAML translations under `content/i18n/` replace it. The
828
+ playground also declares its canonical Astro `site`, clearing the sitemap
829
+ warning alongside its new `404.mdx`.
830
+ - **With a part's deck open, the chapter on screen lights its slides in the
831
+ sidebar, and the rail follows it.** Opened at a chapter's section
832
+ (`/motion/slides/#slide-17`) only the part's deck was lit, not the
833
+ chapter's slides icon that opened it, and the rail stopped at the part's
834
+ row. Each frame of a covered section now carries its chapter
835
+ (`data-chapter`, from `frameChapters` in `file.data.explicoDeck`), and the
836
+ deck's page tells the sidebar the chapter of the frame in view on the
837
+ study page, or shown in Present mode and the presenter
838
+ (`DECK_FRAME_EVENT`, `explico:deck-frame`, with `{ deck, chapter }`, and
839
+ `data-deck-chapter` on `<html>`). The sidebar lights that chapter's section
840
+ link with `aria-current="location"`, as the open page lights its own, and
841
+ the rail covers every row before it; the title and outcomes frames, which
842
+ belong to no chapter, light none.
843
+ - **"Continue at slide N" is offered whenever a reader left a deck past its
844
+ first frame, and presents from there.** Present mode writes `#slide-N`
845
+ into the address so a slide can be shared, and the offer was made only on
846
+ a page opened without one, once: after Esc, and after a reload of that
847
+ address, it was missing. `refreshResume` in `deck-controller.ts` now
848
+ renders it as the page opens and again whenever Present mode ends,
849
+ whatever the address says: a button (`data-deck-resume`) beside Present,
850
+ where it was a link to the frame, that presents from the frame to continue
851
+ at (above). Once the reader was shown all of the last frame it reads
852
+ "Present again" and presents from the first.
853
+ It is the reader's own (`data-explico-export-chrome`), so a deck's
854
+ standalone HTML and Markdown never carry it. A link to `#slide-N` still
855
+ opens the study page at that frame.
856
+ - **Opening a page no longer writes its sections' state to the browser.**
857
+ `wirePersistedDetails` meant to save only a section the reader folded or
858
+ opened, but restoring a section's default state fired a `toggle` of its
859
+ own, which it saved too: every page with collapsible sections left one
860
+ `disclosure:<path>:<section>` entry per section on a mere visit, a list of
861
+ the pages a reader opened. Only a state the reader moved a section to is
862
+ saved now.
863
+ - **A notation entry's body honours `fixedIndex` (rule N23).** Its glyph table
864
+ dropped the flag that a lesson's keeps, so in the glossary `r_2` took the
865
+ card of the family `r_m` although `m` was marked `fixedIndex: true`. The
866
+ entry's body and a lesson now agree; the error names the card that fixed the
867
+ index. The render's message for an entry's body no longer suggests
868
+ `notation.local`, which an entry does not have.
869
+ - **`astro build` reports a notation entry's error at the file's own line.**
870
+ Astro's glob loader renders an entry from its body alone, so the line in
871
+ `entry.md:1:43` (the message, and the loader's `Failed to parse Markdown file`
872
+ header) counted from the end of the frontmatter. `remark-notation.mjs` now
873
+ finds where the body sits in the file (`source-lines.mjs`, read only while a
874
+ failure or warning is reported) and moves the location, so the build and
875
+ `validate:content` agree. A lesson is compiled whole and was already right.
876
+ - **An unresolved-glyph error quotes the math it is in** (`… "w", in $w_3$.`),
877
+ in `astro build` and in `validate:content`, so a symbol can be searched for
878
+ whatever the line says. The sentence before `Introduce the symbol …` gains
879
+ `, in $…$`; a test matching the text up to the token is unaffected.
880
+ - **The published package loads again.** 0.13.0 and 0.13.1 shipped
881
+ `dist/compiler/exports/{latex,markdown}/serialize.js` importing
882
+ `dist/components/float-name.js`, which no one built, so `astro build`,
883
+ `astro dev` and `astro check` stopped with "Unable to load your Astro
884
+ config". `float-name.ts` (no Astro, no DOM beyond the `Element` that
885
+ `floatOf` takes) moved from `src/components/` to `src/reference/`, which
886
+ tsup compiles. It was never a documented author-facing export; a course
887
+ that patched the module in (`patches/explico@0.13.1.patch`) deletes the
888
+ patch.
889
+ - **`migrating.md`'s 0.13.0 → 0.13.1 section said the manifest schema stays
890
+ v29.** It is v31.
891
+ - **A figure no longer fails on `e`, `\pi`, `i` or `d`.** `renderLabMath`
892
+ leaves a plain base-library atom without a card, as a lesson's math does, but
893
+ `assertFigureSymbolsBound` / `unboundFigureSymbols` (`figure-symbols.ts`)
894
+ then failed the build on it ("figure symbols without a notation card"). The
895
+ check now exempts the same library (`isBaseLibraryGlyph`,
896
+ `base-library.mjs`), in a power too (`e^{i\pi}`). An index (`e_i`, rule N23),
897
+ a decorated (`\hat{e}`, rule N22) or a styled (`\mathbf{e}`) letter is
898
+ another symbol and is still reported.
899
+ - **The build reports every page with a figure symbol that has no card, or a
900
+ notation trigger with no popup entry, at once.** `astro:build:done` threw on
901
+ the first page and never reached the later ones or the exports. It now checks
902
+ all pages and throws once with the whole list (`assertNoUnboundFigureSymbols`
903
+ is new in `figure-symbols.ts`; `assertFigureSymbolsBound` is unchanged). The
904
+ standalone, part, course and LaTeX exports still wait for a clean check.
905
+ - **`lab-katex-direct` is reported per lesson.** A helper that imports `katex`
906
+ was reported once, against the first lesson that reached it, so
907
+ `content check <lesson>` could not tell whether another lesson was clean. It
908
+ is now reported once for each lesson whose imports reach it, with that
909
+ lesson's own chain, and a lesson that reaches none is clean. The code and
910
+ severity are unchanged. `validate:content` prints one error per lesson while
911
+ several share a helper.
912
+ - **A card only a lab template uses now reaches the page.** The package serves
913
+ `explico/reference/*` from `dist/`, but `LessonFooter.astro` imports the
914
+ TypeScript source of `page-notation`, so the two copies had separate
915
+ registries. A key bound by `labMathScope` or `renderLabMath` in a template
916
+ (and a derived N23 card it registered) was never in the card payload, and the
917
+ hover opened nothing. The registries now live on `globalThis` under
918
+ `Symbol.for('explico.reference.page-notation')`, one per process.
919
+ - **Rule N23 no longer asks for the set of a letter that is not an index.**
920
+ `index-range` named `b` in `\nabla_{b_n}` (the base of the carded symbol
921
+ `b_n`, whose own index `n` is checked in its own right) and `g` in
922
+ `\mathbb{E}_{Z \sim g}` (a density in a condition), and asked for a `range`
923
+ on the card of `b_n` as "the set of the running index b". A letter that sits
924
+ inside a larger symbol with a card of its own, and every letter of a
925
+ subscript slot that holds a relation (`\sim`, `=`, `\in`, …), is no longer a
926
+ running index. A letter with a card of its own, and one inside an author's
927
+ `\explain`, is checked as before.
928
+ - **A family whose index is juxtaposed letters now reaches its instances.** The
929
+ entry `x_{ij}` (`K_{ij}`, `r_{ik}`) gave its card to the exact form only, so
930
+ `x_{i1}`, `x_{12}` and `K_{ji}` failed rule N4 as unresolved and the message
931
+ said the entry "explains its own instances only". A slot of two or more
932
+ lowercase letters is now a double index: the family matches a run of as many
933
+ letters and digits (`x_{i1}`, `x_{12}`, `x_{ji}`), as `x_{i,j}` always did,
934
+ and never `x_i`, `x_{ijk}` or `x_{i+1}`. A page that has both `x_{ij}` and
935
+ `x_k` as entries now fails `ambiguous-base-fallback` on `x_{12}`, which both
936
+ fit; wrap that instance in `\explain`.
937
+ - **An `\explain` inside a family instance's index no longer unbinds the
938
+ family.** `\beta_{\explain{count}{K}}` and `x_{\explain{iter}{k}}` left the
939
+ base unresolved, because the match was dropped for enclosing the author's
940
+ wrap. A symbol that holds an `\explain` in its own subscript now keeps its
941
+ card, and the two wraps nest: `\explain{beta-j}{\beta_{\explain{count}{K}}}`.
942
+ A match that only straddles a wrap's braces is still dropped.
943
+ - **A wrapped family instance is not re-bound inside.** On a page with the
944
+ family `\sigma_k` and a plain `\sigma`, `\explain{component-sd}{\sigma_1}`
945
+ became `\explain{component-sd}{\explain{population-sd}{\sigma}_1}`, so hovering
946
+ the σ opened the population standard deviation. The wrap names the indexed
947
+ symbol, so its base takes no other entry's card; this holds under a power
948
+ (`\sigma_1^2`), for a two-letter family and for a font-styled base. A wrap
949
+ around a symbol whose key has another base still binds the glyphs inside.
950
+ - **An active prose link no longer falls below AA contrast.** 0.13.1 blended
951
+ the highlight of every active notation target (`mix-blend-mode: multiply`
952
+ in the light theme, `screen` in the dark one), but the blend exists for a
953
+ symbol in math, whose KaTeX boxes paint over the glyph to their left. On a
954
+ prose term link (`a.notation-term`), `multiply` darkened the tint over a
955
+ glossary row's panel and the engine's own link colour dropped from 4.75:1
956
+ to 4.4:1, which axe reports as `color-contrast`. The blend is now scoped to
957
+ `.katex-html`; a prose link keeps the opaque tint it had before 0.13.1. A
958
+ course that added `a.notation-term.is-notation-active { color: … }` to pass
959
+ axe deletes it.
960
+ - **`unresolved-glyph` names the card that fixed a family's index.** Marking
961
+ the card of `m` `fixedIndex: true` turns the family `r_m` into a single
962
+ symbol (rule N23), so `r_2` stops being its instance. The error then blamed
963
+ "a bare r" and offered `\explain{periodic-rate}{r}`, which is the wrong
964
+ repair. For a subscripted symbol that a family would have explained, it now
965
+ says "`r_2` would be an instance of `periodic-rate`, but `m` is marked
966
+ `fixedIndex` on `compounding-frequency`", and to give that card a `range`
967
+ if `m` runs over a set. The same holds for an entry that carries
968
+ `fixedIndex` itself, and for an index entry that fixes the subscript. A
969
+ truly bare `r` keeps its message. The error `astro build` throws for the
970
+ same symbol says it too. `index-range`'s offer of `fixedIndex: true` now
971
+ says that a family indexed by that letter then explains none of its
972
+ instances. `resolveMathGlyphs` returns `instance` and `fixedLenders` on such
973
+ an unresolved item, and `math-glyphs.mjs` exports `fixedLenderMessage`.
974
+
975
+ - **The `[?]` About panel opens on screen in the phone menu.** The menu's
976
+ copy of the butto
977
+ ### Removed
978
+
979
+ - **`gloss-wants-promoting`.** It fired on three identical glosses and never
980
+ did in any course; `meaning-repeated` reports the same copy at two. A
981
+ `lint-ignore.yml` line for it can be deleted.
982
+ n starts its row, and the panel hung from the button's end,
983
+ so it ran off the screen's start edge and past the end of the menu. Below
984
+ 50rem it is now a sheet fixed under the header, centred, at most 21rem
985
+ wide.
986
+
987
+ - **The `[?]` button is round in the phone menu.** The sidebar's rules for a
988
+ part's header (`.layout-navigation-content summary`) are unlayered and
989
+ reached it, making it a rounded square with the `?` at its start; they now
990
+ skip `.about-toggle`.
991
+
992
+ - **A hover peek no longer overwrites a collapsed section's saved state.** A
993
+ collapsed section peeks (its content floats open) when the pointer is on its
994
+ title, and a peek has to set `details.open = true` to render, which fired the
995
+ native `toggle` that `wirePersistedDetails` saves as "open". A reader who
996
+ collapsed a section and reloaded with the pointer still on its title (the
997
+ browser sends the pointer's `pointerenter` after the reload) found it open
998
+ from then on: `open` was stored over `closed`. A peek now carries
999
+ `data-explico-temporary-open`, which `persisted-disclosure.ts` already skips,
1000
+ from its first `open = true` until the `toggle` its closing fires; a click
1001
+ that commits the peek removes it. `CollapsibleSections.astro`'s module
1002
+ comment already said a peek "never persists anything".
1003
+
17
1004
  ## [0.13.1] - 2026-10-07
18
1005
 
19
1006
  ### Fixed
@@ -3446,7 +4433,9 @@ under **Breaking** below; a PATCH never breaks.
3446
4433
  system with its completeness gate, the curriculum-graph validator, the
3447
4434
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
3448
4435
 
3449
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.13.1...HEAD
4436
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.15.0...HEAD
4437
+ [0.15.0]: https://github.com/danieltuzes/explico/compare/v0.14.0...v0.15.0
4438
+ [0.14.0]: https://github.com/danieltuzes/explico/compare/v0.13.1...v0.14.0
3450
4439
  [0.13.1]: https://github.com/danieltuzes/explico/compare/v0.13.0...v0.13.1
3451
4440
  [0.13.0]: https://github.com/danieltuzes/explico/compare/v0.12.0...v0.13.0
3452
4441
  [0.12.0]: https://github.com/danieltuzes/explico/compare/v0.11.1...v0.12.0