explico 0.8.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +1257 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +5 -3
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1152 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1467 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -75,63 +75,315 @@ naming collision both stayed invisible.
75
75
 
76
76
  ```
77
77
  Course Explico Playground
78
- ├── Part 1 Motion in the plane content/docs/motion/
79
- │ ├── Ch 1 Position and Coordinate Systems position-and-coordinates.mdx
80
- │ │ ├── § Locating a point in the plane ##
81
- │ │ ├── § Two ways to name the same point ##
82
- │ │ │ ├── §§ From Cartesian to polar ###
83
- │ │ │ └── §§ From polar to Cartesian ###
84
- │ │ ├── § Velocity and acceleration as derivatives ##
85
- │ │ └── § The special case: constant acceleration ##
86
- │ │ ├── §§ Integrating once: velocity ###
87
- │ │ └── §§ Integrating twice: position ###
88
- │ └── Ch 2 Velocity Under Constant Acceleration constant-acceleration.mdx
89
- └── Part 2 Projectile motion content/docs/projectiles/
90
- ├── Ch 1 Projectile Range projectile-range.mdx
91
- └── Ch 2 Optimal Launch Angle optimal-launch-angle.mdx
78
+ ├── Part 1 Motion in the plane content/docs/motion/
79
+ │ ├── Ch 1 Position and Coordinate Systems position-and-coordinates.mdx
80
+ │ │ ├── § 1.1.1 Locating a point in the plane ##
81
+ │ │ ├── § 1.1.2 Two ways to name the same point ##
82
+ │ │ │ ├── § 1.1.2.1 From Cartesian to polar ###
83
+ │ │ │ └── § 1.1.2.2 From polar to Cartesian ###
84
+ │ │ ├── § 1.1.3 Velocity and acceleration as derivatives ##
85
+ │ │ ├── § 1.1.4 The special case: constant acceleration ##
86
+ │ │ │ ├── § 1.1.4.1 Integrating once: velocity ###
87
+ │ │ │ └── § 1.1.4.2 Integrating twice: position ###
88
+ │ │ └── § 1.1.5 Try it ##
89
+ │ └── Ch 2 Velocity Under Constant Acceleration constant-acceleration.mdx
90
+ └── Part 2 Projectile motion content/docs/projectiles/
91
+ ├── Ch 1 Projectile Range projectile-range.mdx
92
+ └── Ch 2 Optimal Launch Angle optimal-launch-angle.mdx
92
93
  ```
93
94
 
94
- Which numbers every float like this — the part supplies the prefix, and the
95
- index carries on across the part's chapters:
95
+ Which numbers every float like this — the part and the chapter supply the
96
+ prefix, and each chapter counts from 1:
96
97
 
97
98
  ```
98
99
  Part 1 Motion in the plane
99
- Ch 1 Position and Coordinate Systems (1.1) (1.2) (1.3) (1.4) (1.5) (1.6) (1.7)
100
- Ch 2 Velocity Under Constant Accel. (1.8)
100
+ Ch 1 Position and Coordinate Systems (1.1.1) (1.1.2) … (1.1.7)
101
+ Ch 2 Velocity Under Constant Accel. (1.2.1)
101
102
  Part 2 Projectile motion
102
- Ch 1 Projectile Range (2.1) (2.2)
103
- Ch 2 Optimal Launch Angle (2.3) (2.4) (2.5) (2.6)
103
+ Ch 1 Projectile Range (2.1.1) (2.1.2) (2.1.3)
104
+ Ch 2 Optimal Launch Angle (2.2.1) (2.2.2) (2.2.3) (2.2.4)
104
105
  ```
105
106
 
107
+ ## Part pages
108
+
109
+ A part is a directory, so `/<part>/` is a URL a reader will eventually type,
110
+ follow, or be handed by a search engine. Without a page it is a 404, and the
111
+ part exists only as a sidebar heading that cannot be linked to.
112
+
113
+ Give a part a page by authoring **`content/docs/<dir>/index.mdx`**:
114
+
115
+ ```mdx
116
+ ---
117
+ title: Motion in the plane
118
+ description: Locate a moving point, then predict where it goes next.
119
+ ---
120
+
121
+ import PartBridge from 'explico/components/PartBridge.astro';
122
+ import PartOverview from 'explico/components/PartOverview.astro';
123
+ import PartScope from 'explico/components/PartScope.astro';
124
+
125
+ <PartBridge>
126
+
127
+ How the earlier parts lead into this one — omitted on the first part.
128
+
129
+ </PartBridge>
130
+
131
+ <PartScope>
132
+
133
+ What this part covers, in the course's own voice.
134
+
135
+ </PartScope>
136
+
137
+ <PartOverview
138
+ chain={[
139
+ 'A point in the plane gets a name: Cartesian, or polar',
140
+ 'Velocity and acceleration are that name differentiated',
141
+ ]}
142
+ />
143
+ ```
144
+
145
+ **The two paragraphs are required** ([S2](structure-rules.md#s2)): every part
146
+ page after the first opens with a `<PartBridge>` paragraph that links the
147
+ earlier parts to this one, and every part page carries a `<PartScope>`
148
+ paragraph on its own content. `pnpm validate:content` fails when one is
149
+ missing, holds more than one paragraph, or falls outside the course's
150
+ `rules.S2` word range (30–150 words unless the course sets its own).
151
+
152
+ **An overview page is not a chapter.** `isLessonSlug` has always excluded
153
+ `<dir>/index`, so the page teaches no competency, carries no knowledge check,
154
+ takes no float numbers, and never appears in `manifest.lessons` or a track. A
155
+ course adds one part at a time; a part without one behaves exactly as before.
156
+
157
+ `<PartOverview>` reads the manifest for everything it lists — chapters in
158
+ reading order with their descriptions, the competencies the part teaches, the
159
+ parts it builds on and leads to — so the page cannot fall out of date with the
160
+ content. Only the two paragraphs and the optional `chain` (the part's argument,
161
+ one step per line, which no manifest field knows) are authored. `rail={false}` drops the
162
+ side rail and keeps the reading list.
163
+
164
+ ### Study time
165
+
166
+ A chapter may state how long it takes, and only the chapter can know:
167
+
168
+ ```yaml
169
+ studyMinutes: 40 # or a range
170
+ studyMinutes: [30, 40]
171
+ ```
172
+
173
+ `<PartOverview>` prints it beside each chapter and totals the part;
174
+ `<CourseContents>` totals the course in hours. **A total appears only when
175
+ every chapter in that part states one**, so a half-annotated course shows the
176
+ chapters it knows about and no misleading sum. Nothing derives the field and
177
+ nothing requires it — it is the reader-facing half of the old
178
+ `estimatedMinutes`, which was dropped when there was nowhere to add it up.
179
+
180
+ ### Reaching the page
181
+
182
+ Three routes lead to it, and a course gets all three at once:
183
+
184
+ | Route | Where it comes from | Works without JavaScript |
185
+ | --- | --- | --- |
186
+ | The `Overview` row leading the part's sidebar group | the compiled `sidebar` | yes |
187
+ | The `»` control in the sidebar group's header | `LayoutSidebar.astro`'s script | no |
188
+ | The part's name above every chapter title | `LayoutPageTitle.astro` | yes |
189
+
190
+ The part label in the sidebar keeps collapsing the group when clicked — the
191
+ control sits beside the caret rather than taking either gesture. The breadcrumb
192
+ needs one line of wiring in the course's `astro.config.mjs`:
193
+
194
+ ```js
195
+ components: {
196
+ PageTitle: 'explico/components/starlight/LayoutPageTitle.astro',
197
+ }
198
+ ```
199
+
200
+ Name the sidebar row with `overviewLabel` on the part when `Overview` is wrong
201
+ for a course: `{ dir: 'bonds', label: 'Bonds', overviewLabel: 'All bonds' }`.
202
+
203
+ ## The course's own contents
204
+
205
+ A landing page that links only the first chapter hides the rest of the course.
206
+ `<CourseContents>` lists every part with its chapters, in reading order, each
207
+ part heading linking its overview page:
208
+
209
+ ```mdx
210
+ import CourseContents from 'explico/components/CourseContents.astro';
211
+
212
+ <CourseContents />
213
+ ```
214
+
215
+ It reads the same manifest the sidebar does, so a renamed chapter or a new part
216
+ cannot leave a stale copy behind on the front page. `descriptions={false}`
217
+ drops the one-line summaries; `headingLevel={3}` fits it under an existing
218
+ `##`.
219
+
220
+ ## Where the reader is
221
+
222
+ Two marks, both derived, neither authored:
223
+
224
+ - **The navigation rail** colours the segment beside every chapter already
225
+ passed, so the boundary between accent and hairline is the reader's position
226
+ in the course. A part whose chapters are all behind them is marked whole.
227
+ Reading order is the sidebar's own order, which is the merged track order.
228
+ - **The contents rail** does the same down one page, following Starlight's
229
+ scroll-spy as the reader travels.
230
+
231
+ Both are progressive enhancements: without JavaScript the rails are simply
232
+ uncoloured.
233
+
234
+ The part header carries the third mark, and that one is pure CSS, so it is
235
+ right in the first painted frame: a standing tint while the open page belongs
236
+ to that part. Together the three never read as the same thing — a solid accent
237
+ row is the chapter you are on, a tint is the part holding it, a neutral fill
238
+ comes and goes with the pointer.
239
+
106
240
  ## Numbering
107
241
 
108
- A display equation, `<Table>`, `<Figure>`, `<Diagram>`, or knowledge check is
109
- numbered **`‹part›.‹n›`**:
242
+ Every number follows the book's outline: part, then chapter, then the thing
243
+ numbered. A course sets the details in `course.settings.json` → `numbering`;
244
+ every key is optional, and these are the defaults:
110
245
 
111
- - **`part`** is the part's 1-based position in `courseConfig.parts`.
112
- - **`n`** counts that kind of numbered item across the *whole part*, in document
113
- order, running unbroken from one chapter into the next. Each kind counts on
114
- its own, so a `<Table>` never advances the `<Figure>`, `<Diagram>`, or
115
- knowledge-check sequence.
246
+ ```json
247
+ "numbering": {
248
+ "sections": { "show": true, "depth": 4 },
249
+ "floats": "chapter",
250
+ "aliases": true
251
+ }
252
+ ```
253
+
254
+ A number is for a reader to see and say. It is never the anchor: a heading's
255
+ anchor is its slug (`#two-ways-to-name-the-same-point`) and a float's is its
256
+ `id` (`#tab-kinematics-summary`), so a link survives a section inserted above
257
+ its target.
258
+
259
+ ### Section numbers
260
+
261
+ A lesson's top-level `##` to `######` headings are numbered
262
+ **`‹part›.‹chapter›.‹section›[.‹subsection›…]`**:
263
+
264
+ - **`part`** is the part's 1-based position in `courseConfig.parts`, and
265
+ **`chapter`** the lesson's position in its part, in sidebar order.
266
+ - `##` counts within the chapter, `###` within its `##`, and so on; a deeper
267
+ counter restarts under every shallower heading. Headings descend one level
268
+ at a time (rule S6), so no level is skipped; where one is anyway (S6 off or
269
+ waived), it counts as 0 (`##` then `####` is `1.1.1.0.1`), as in LaTeX.
270
+ - A heading deeper than `numbering.sections.depth` (default `4`, `####`) is
271
+ unnumbered. A heading inside a component — a `<Memo>`, a `<CompactExample>` —
272
+ belongs to that component, not to the outline, so it is neither numbered nor
273
+ counted. The generated "References" section and a knowledge check's own
274
+ heading are not numbered either.
275
+
276
+ Where a section number shows:
277
+
278
+ | | Wide screen | Narrow screen (under 50em) | Print |
279
+ | --- | :-: | :-: | :-: |
280
+ | An open section's heading | ✓ | — | ✓ |
281
+ | A collapsed section's bar | ✓ | ✓ | ✓ |
282
+ | The chain icon's "Link copied · § 1.1.2" | ✓ | ✓ | — |
283
+
284
+ `numbering.sections.show: false` shows no section numbers anywhere.
285
+
286
+ **The chain icon copies the link.** Clicking (or tapping) a heading's chain
287
+ icon puts the page's URL with the heading's named anchor on the clipboard,
288
+ sets the address bar to it, highlights the section, and says "Link copied ·
289
+ § 1.1.2" beside the icon. On a narrow screen, where an open heading shows no
290
+ number, that confirmation is where the number surfaces, and the icon is always
291
+ visible there. If the browser refuses the clipboard, the click still navigates
292
+ and highlights. Tapping a heading's title only folds or unfolds a collapsible
293
+ section; it never changes the URL.
294
+
295
+ **Citing a section.** `[[sec-‹anchor›]]` on the same page, or
296
+ `[[lesson/slug#sec-‹anchor›]]` from another lesson, prints `§ 1.1.2` as a link
297
+ to the section, with a hover card showing its title and first paragraph. The
298
+ anchor is the heading's slug: its text in lowercase, spaces as hyphens,
299
+ punctuation dropped, so `## Two ways to name the same point` is
300
+ `[[sec-two-ways-to-name-the-same-point]]`, and a title that repeats takes
301
+ `-1`, `-2`. A reference to a heading without a number (deeper than `depth`, or
302
+ with `show: false`) prints the heading's title in quotes instead: a reader
303
+ could not find the number. A reference that names no top-level heading fails
304
+ `pnpm validate:content` (`sec-ref-resolves`) and the build.
305
+
306
+ ### Structure from the bottom up
307
+
308
+ A heading's level says where it sits in the outline: rule S6 lets it go one
309
+ level deeper than the heading before it, and rule S7 puts body content
310
+ between two headings. Neither is a way to get structure where the outline
311
+ has run out. When a section near the bottom of the outline holds two parallel
312
+ enumerations — the assumptions of a model and its conclusions, the steps of
313
+ two methods — do not open `#####` headings for them, and do not climb back
314
+ to `##` for the look of it. Structure them from the bottom up, the way LaTeX
315
+ does with `\paragraph` and its `description` environment:
316
+
317
+ - a **run-in label** — a bold phrase that opens its paragraph (`**Level
318
+ ground.** The launch and landing heights are equal …`), for a few items
319
+ that each take a paragraph;
320
+ - a **description list** — one line per item, the label in bold and its
321
+ explanation after it — for many short items;
322
+ - a `<Table>`, when the items share the same attributes.
323
+
324
+ A run-in label is not in the outline, "On this page" or the numbering, and it
325
+ needs none of that: it names a paragraph for the reader who is already
326
+ reading the section, not a place to jump to. Promote it to a heading only
327
+ when it deserves a link, a number or folding of its own.
328
+
329
+ ### Float numbers
330
+
331
+ A display equation, `<Table>`, `<Figure>`, `<Diagram>`, knowledge check or
332
+ worked-example set (`<CompactExamples>`) is numbered **`‹part›.‹chapter›.‹n›`**:
333
+
334
+ - **`n`** counts that kind of numbered item in the chapter, in document order.
335
+ Each kind counts on its own, so a `<Table>` never advances the `<Figure>`,
336
+ `<Diagram>`, knowledge-check or example sequence.
116
337
  - **Sections and subsections do not participate.** Neither `##` nor `###`
117
338
  resets `n` or appears in the number.
118
339
  - **Keyed or not, everything counts.** An equation with no `\label{eq:…}` still
119
- takes a number and a positional anchor (`#eq-2-4`); it just cannot be
340
+ takes a number and a positional anchor (`#eq-2-1-4`); it just cannot be
120
341
  referenced by name.
121
-
122
- In the playground: part 1 runs `1.1` … `1.7` through chapter 1 and continues
123
- `1.8` in chapter 2; part 2 opens at `2.1`.
124
-
125
- `manifest.lessons[]` carries what the render path needs to reproduce this:
126
- `part`, `chapter`, and `numbering.{equations,tables,figures,diagrams,checks}` —
127
- the running count each chapter starts from.
342
+ - **The number sits with the caption.** A figure or diagram prints it in the
343
+ caption line below its picture, a table in the caption line above, and a
344
+ display equation beside it. What names each
345
+ float — its long `caption` and optional `short` caption, an equation's
346
+ `\caption{…}` — is [components.md § The caption pair](components.md#the-caption-pair).
347
+ - **A check's and an example set's number shows like a section's.** Both are
348
+ sections of the page with a heading of their own, so their number follows
349
+ [§ Section numbers](#section-numbers) — wide screen, print, folded bar, and
350
+ not at all with `numbering.sections.show` off — while a float's stays in its
351
+ caption at every width, because prose names a float by its number.
352
+
353
+ In the playground: part 1 runs `1.1.1` … `1.1.7` through chapter 1, and
354
+ chapter 2 starts again at `1.2.1`; part 2 opens at `2.1.1`.
355
+
356
+ `numbering.floats: "part"` keeps the older scheme, **`‹part›.‹n›`**, with `n`
357
+ running on from one chapter of the part into the next — the first equation of
358
+ a chapter that follows one with seven is `1.8`.
359
+
360
+ `manifest.lessons[]` carries what the render path needs to reproduce all of
361
+ this: `part`, `chapter`, `numbering.prefix` and the per-kind count each chapter
362
+ starts from (`numbering.{equations,tables,figures,diagrams,checks,examples}`,
363
+ all 0 under the chapter scope), and `headings`, every top-level heading with
364
+ its anchor and number.
365
+
366
+ ### Number aliases
367
+
368
+ A number is not an anchor, but a reader may still type one. With
369
+ `numbering.aliases` on (the default), a fragment in number form — `#sec-1.1.2`,
370
+ `#fig-2.2.1`, `#eq-1.1.4`, `#tab-…`, `#dia-…`, `#check-…`, `#ex-…` — lands on the
371
+ section or float that shows that number on the page, and the address bar
372
+ switches to its named anchor. This works inbound only: nothing the site links
373
+ or copies is in number form. An alias is only as stable as the content: once a
374
+ section is inserted above it, `§ 1.1.2` names a different section, so a number
375
+ quoted outside the course belongs next to the knowledge version the About
376
+ `[?]` panel shows.
128
377
 
129
378
  ### The invariant, and why it is build-enforced
130
379
 
131
380
  The number a page prints beside an equation and the number a cross-page
132
381
  `[[slug#eq-key]]` prints for it are computed by two code paths from the same
133
382
  content. `remarkNotation` cross-checks them for every keyed float and fails the
134
- build on any disagreement, naming the key and both numbers.
383
+ build on any disagreement, naming the key and both numbers. Sections get the
384
+ same guard: a page takes its headings' anchors and numbers from the manifest,
385
+ and the build fails if the page's top-level headings are not the ones the
386
+ manifest numbered.
135
387
 
136
388
  This exists because the disagreement is otherwise silent and shipped: a course
137
389
  config that passed the manifest number tables without the placement map
@@ -156,8 +408,8 @@ could still silently fall back to `##`-section numbering with no error at all.
156
408
  `strict: true` closes that: it fails the build when a catalogued lesson
157
409
  contains any equation, `<Table>`, or `<Figure>` — keyed or not — but has no
158
410
  `numbering` placement or no cross-page number table entry for its slug.
159
- `<Diagram>` is exempt, since `Diagram.astro` already renders one without a
160
- number by design when it has none. `strict` defaults to `false`, so existing
411
+ `<Diagram>` and `<CompactExamples>` are exempt, since both components already
412
+ render one without a number by design when it has none. `strict` defaults to `false`, so existing
161
413
  non-strict wiring keeps building unchanged; turn it on once a course's
162
414
  manifest covers every catalogued lesson.
163
415
 
@@ -182,14 +434,135 @@ inside `<Memo>`/`<ShortNote>`), or wrap it in `<Figure>`.
182
434
 
183
435
  ### Knowledge checks
184
436
 
185
- An assessment title is authored once, in `content/assessments/<id>.json`. Put
186
- that id in the lesson's colocated `.checks.yml`, then render `<AssessmentSet />`;
187
- do not add a separate “Check yourself” heading. The component renders
188
- `Knowledge check ‹number› — ‹title›` as a permalinked, collapsible section at
189
- `#check-<id>`. Cite it on the same page with `[[check-<id>]]`, or from another
190
- lesson with `[[lesson/slug#check-<id>]]`. Single-choice options are shuffled
191
- when the browser views the check; option ids, correctness, and authored content
192
- remain unchanged.
437
+ An assessment title is authored once, in `content/assessments/<id>.json`, and
438
+ names the topic alone ("Position and coordinates"). Put that id in the lesson's
439
+ colocated `.checks.yml`, then render `<AssessmentSet />`; do not add a
440
+ separate “Check yourself” heading. The component draws the check's `<h2>` —
441
+ the check-mark icon and the title, with "Knowledge check" and the number as a
442
+ label in the card's corner
443
+ ([components.md § Knowledge checks and worked
444
+ examples](components.md#knowledge-checks-and-worked-examples)) — as a
445
+ permalinked section at `#check-<id>`, collapsible and open unless the course's
446
+ `disclosure.knowledgeChecks` says otherwise ([§ Collapsing](#collapsing-two-mechanisms)),
447
+ and "On this page" lists it. Cite it on the same page with `[[check-<id>]]`, or
448
+ from another lesson with `[[lesson/slug#check-<id>]]`. Single-choice options are
449
+ shuffled when the browser views the check; option ids, correctness, and
450
+ authored content remain unchanged.
451
+
452
+ ### Worked examples
453
+
454
+ A `<CompactExamples id="…" short="…">` is a numbered set of worked examples,
455
+ `Example ‹part›.‹chapter›.‹n›`, one `<CompactExample label="…">` tab each. Both
456
+ props are required: the `id` anchors it at `#ex-<id>` and is what
457
+ `[[ex-<id>]]` (or `[[lesson/slug#ex-<id>]]`) cites; `short` is its topic. The
458
+ component draws the set's `<h2>` — the "Example" badge, the number, the topic
459
+ — so no heading goes above it. The set folds like a section, closed on a first
460
+ visit unless `disclosure.workedExamples` says otherwise, and "On this page"
461
+ lists it.
462
+
463
+ ## Containers: plain, meta, accent, aside
464
+
465
+ Every box the engine draws belongs to one of four tiers, and the tier says
466
+ what the content *is* — the reader learns one visual language, not one per
467
+ component:
468
+
469
+ | Tier | Looks like | Means | Used by |
470
+ | --- | --- | --- | --- |
471
+ | **plain** | no card | material a book would print in its main flow | sections, figures, tables, equations, diagrams, References |
472
+ | **meta** | grey surface, thin border | logistics, or generated for the reader | the notation panel, a part page's rail, the About panel, the glossary and curriculum-map cards, a Starlight `<Card>` |
473
+ | **accent** | accent tint, thin border, shadow | enrichment the author marked | knowledge checks, worked examples, a `[card]` section |
474
+ | **aside** | a left rule (a memo is also tinted) | an annotation beside the flow | `<Memo>`, an opened `<ShortNote>`, a Starlight aside |
475
+
476
+ A tier is fixed per kind; a course has no setting to change it. No container
477
+ has a top rule, and the shadow never grows with heading depth: hierarchy is in
478
+ type size and numbers. An interactive figure has no card of its own either —
479
+ its lab is plain figure material.
480
+
481
+ **Heading markers.** A heading takes up to two markers at the end of its
482
+ line, one of each kind, in either order. They never render, never reach the
483
+ anchor (`### Explore the range [card]` is `#explore-the-range`), and rule S1
484
+ does not count them.
485
+
486
+ | Marker | Effect |
487
+ | --- | --- |
488
+ | `[collapsible]` | collapsible, starts open, whatever the `disclosure` default |
489
+ | `[collapsed]` | collapsible, starts closed |
490
+ | `[no-collapsible]` | never collapsible |
491
+ | `[card]` | the section — heading and body, down to the next heading of the same or a higher rank — becomes one **accent** card |
492
+ | `[card=meta]` | the same, in the **meta** tier, and unnumbered: logistics — what a chapter builds on, how to read it — is outside the outline, like References, and so is every heading inside it |
493
+
494
+ ```mdx
495
+ ### Explore the range [collapsed] [card]
496
+
497
+ Move the sliders; the section folds and unfolds as one card.
498
+ ```
499
+
500
+ Use `[card]` to set a stretch of the lesson apart as something to try or
501
+ explore — a lab with the prose that frames it, say. It is not for emphasis:
502
+ a section that matters more is a better heading, not a box.
503
+
504
+ **Starlight asides and cards.** `:::note`, `:::tip`, `:::caution` and
505
+ `:::danger` (and `<Aside type="…">`) render in the aside tier with
506
+ Starlight's own icon per type; `note` and `tip` take the course accent,
507
+ `caution` and `danger` keep a warning and a danger hue. Name any Starlight
508
+ built-in icon to replace it (`:::note{icon="heart"}`), or leave it empty to
509
+ drop it (`:::note{icon=""}`, `<Aside icon="">`). The `:::` form needs
510
+ `remarkAsides` (`explico/reference/remark-asides.mjs`) last in the course's
511
+ `remarkPlugins`: Starlight renders directives only under its own
512
+ `src/content/docs/`, and this pass hands them to Starlight's `<Aside>`. In a
513
+ `.md` lesson (no JSX) an aside has no icon. A `<Card>` / `<CardGrid>` renders
514
+ in the meta tier. Rule S8 says when a `<Memo>` is the better choice than an
515
+ aside.
516
+
517
+ ## Collapsing: two mechanisms
518
+
519
+ A block folds in one of two ways, and every default lives in one place, the
520
+ `disclosure` block of `course.settings.json`
521
+ (`CourseConfig.disclosure`, published as `manifest.disclosure`):
522
+
523
+ - **A section folds away whole.** Its heading stays as a bar with a chevron;
524
+ its body hides behind a "Hidden here: 2 paragraphs, 1 equation" line and
525
+ peeks on hover. `##` … `######` sections, a knowledge check, a set of worked
526
+ examples, the generated References, a `<Memo>` (a `######`-rank section
527
+ of its own) and a `<Fold>` (the same, with no marker, letter or frame, and
528
+ a bar that names the floats it holds — [components.md](components.md#folding-content-away-fold))
529
+ fold this way; the notation panel folds the same way.
530
+ - **A float folds what follows its caption line, never the line.** A table
531
+ folds to its caption line and header row, and starts truncated when it is
532
+ long. A table's fold button is at the top right of its caption line, next
533
+ to a widen button ↔ when the table is wider than the column; the column at
534
+ its right edge widens it too. A figure or a diagram does not fold: it is
535
+ its picture and then its caption line — number, link, caption
536
+ ([components.md § The caption pair](components.md#the-caption-pair)) — in
537
+ every state, as LaTeX lays a figure out; a diagram's line ends with a widen
538
+ button ↔ while the graph is wider than the column. What a figure can fold
539
+ is what follows its caption line: a lab's tools, with the fold button ⌃ /
540
+ ⌄ at the line's end and a "Hidden here: 2 sliders, 1 equation" line while
541
+ folded. To fold a float away
542
+ entirely, wrap it in a `<Fold>` or put it in a collapsible section.
543
+
544
+ | Block | Mechanism | Setting | Engine default | One place overrides with |
545
+ | --- | --- | --- | --- | --- |
546
+ | `##` section | section | `disclosure.sections` | not collapsible | a heading marker |
547
+ | `###`–`######` section | section | `disclosure.subsections` | not collapsible | a heading marker |
548
+ | Knowledge check | section | `disclosure.knowledgeChecks` | collapsible, open | — |
549
+ | References | section | `disclosure.references` | closed | — |
550
+ | Notation panel | section | `disclosure.notation` | closed | — |
551
+ | `<Memo>` | section, `######` rank | `disclosure.memos` | closed | `defaultOpen` |
552
+ | `<Fold>` | section, `######` rank | `disclosure.folds` | closed | `defaultOpen` |
553
+ | A lab's tools (`<Figure>`'s `tools` slot) | a float's fold, after its caption line | `disclosure.figures` | open | `defaultOpen` |
554
+ | `<CompactExamples>` | section, `##` rank | `disclosure.workedExamples` | closed | `defaultOpen` |
555
+ | `<Table>` | minimize + truncation | `disclosure.tables.truncateAfter` | truncated above 6 rows | — |
556
+
557
+ `sections`, `subsections` and `knowledgeChecks` take `{ collapsible,
558
+ defaultOpen }`; the others take `{ defaultOpen }`; `tables` takes
559
+ `{ truncateAfter }` — a table with more rows shows that many, half from each
560
+ end, and a "… N more rows …" line. There is no `disclosure.diagrams`: a
561
+ diagram does not fold. Every collapsible remembers the reader's own choice,
562
+ per page, over the default (`rememberState={false}` on a memo, a fold, a
563
+ figure's tools or an example set opts that one out). A short note's in-place
564
+ reveal and a question's show/hide button are inline affordances, not
565
+ containers, and are not settings.
193
566
 
194
567
  ## Detailed memos and inline short notes
195
568
 
@@ -197,8 +570,11 @@ Headings define the page's conceptual hierarchy: they belong in the outline and
197
570
  should help every reader navigate the main argument. A `<Memo>` instead holds
198
571
  optional depth — a long derivation, proof, robustness check, edge case, or
199
572
  alternative method — without pretending that material is another section of
200
- the lecture. It keeps the subsection-like card, background, and border, but is
201
- collapsed and independently numbered.
573
+ the lecture. It sits in the aside tier (a left rule and the accent tint), is
574
+ independently lettered, and folds like a `######` section — the same chevron,
575
+ peek and "Hidden here" line — starting closed unless the course's
576
+ `disclosure.memos` says otherwise. Its bar is not a heading of the lesson: it
577
+ is not in the outline, "On this page" or the heading rules.
202
578
 
203
579
  Register `remarkShortNote` and then `remarkMemo` after `remarkMath` in
204
580
  `astro.config.mjs`. Import `explico/components/memo/Memo.astro`, cite a memo with
@@ -209,7 +585,7 @@ The full calculation is available in [[memo-supporting-check]].
209
585
 
210
586
  Several paragraphs may intervene before the detailed material.
211
587
 
212
- <Memo id="supporting-check" title="Supporting check" description="The detailed calculation behind the claim.">
588
+ <Memo id="supporting-check" short="Supporting check" caption="The detailed calculation behind the claim">
213
589
 
214
590
  The body can contain ordinary Markdown, display equations, `<Table>`, and
215
591
  `<Figure>` components.
@@ -217,19 +593,26 @@ The body can contain ordinary Markdown, display equations, `<Table>`, and
217
593
  </Memo>
218
594
  ```
219
595
 
220
- `[[memo-supporting-check]]` renders as `[1]`. Clicking it opens and scrolls to
221
- the block and updates the URL to `#memo-supporting-check`; the `[1]` shown on
222
- the memo itself remains a numbered permalink, and the chain icon beside its
223
- title provides the same explicit anchor affordance used by headings and
224
- figures. `id`, `title`, and `description` are required literal strings. The
225
- block is collapsed on a reader's first visit and remembers later open/closed
226
- choices unless `rememberState={false}` is set. Memo numbering is independent
227
- of equation, table, figure, and diagram numbering. Those numbered items remain
228
- in their normal page/part streams inside a memo.
229
-
230
- For print and PDF export, the memo opens at its reading-order position. It does
231
- not move to the bottom of the page: browser support for CSS footnote floats is
232
- not reliable enough to make that layout portable.
596
+ `[[memo-supporting-check]]` renders as a superscript letter, *ᵃ* — the footnote
597
+ convention, so it never reads as a citation's `[1]`; memos are lettered `a`,
598
+ `b`, … per page. Clicking it opens and scrolls to the block and updates the
599
+ URL to `#memo-supporting-check`; clicking it again folds the memo. The memo's
600
+ bar shows the same letter, and the chain icon beside it copies the memo's
601
+ link, as a heading's does. A memo is named like every float
602
+ ([components.md § The caption pair](components.md#the-caption-pair)): `id` and
603
+ `caption` are required literal strings — the caption is the memo's bar and
604
+ heads its marker's preview — and an optional `short` caption is the marker's
605
+ tooltip. The block starts as `disclosure.memos` says (closed by default;
606
+ `defaultOpen` overrides it at one memo) and remembers later open/closed
607
+ choices unless `rememberState={false}` is set. Memo lettering is
608
+ independent of equation, table, figure, and diagram numbering. Those numbered
609
+ items remain in their normal page/part streams inside a memo.
610
+
611
+ For print and PDF export, the memo prints as the reader left it on screen. An
612
+ open memo prints at its reading-order position. It does not move to the bottom
613
+ of the page: browser support for CSS footnote floats is not reliable enough to
614
+ make that layout portable. A closed memo prints its caption and a "(content
615
+ collapsed — full text: …)" note that links its permalink.
233
616
 
234
617
  Use `<ShortNote>` for the smaller, genuinely inline interaction. Import
235
618
  `explico/components/memo/ShortNote.astro` and put it exactly where its collapsed
@@ -63,7 +63,7 @@ reader, a course that forks this one) needs the same "did this break me?" signal
63
63
  from the knowledge version that a library gets from a package version.
64
64
 
65
65
  _How to apply._ Decide the bump when you move `## [Unreleased]` to a dated
66
- heading ([C4](#c4)); set `about.version` to match.
66
+ heading ([C3](#c3)); set `about.version` to match.
67
67
 
68
68
  _Enforcement._ Review-only.
69
69
 
@@ -74,36 +74,18 @@ _Exceptions._ None. If a change does not fit a tier, it is PATCH.
74
74
  ## C3
75
75
 
76
76
  **`about.version` equals the newest `## [x.y.z]` heading in
77
- `content/CHANGELOG.md`.**
77
+ `content/CHANGELOG.md`, and a release moves `## [Unreleased]` and bumps
78
+ `about.version` in one commit.**
78
79
 
79
80
  _Rationale._ Two sources of the same fact must not drift. The manifest — and so
80
81
  the `[?]` panel — reads `about.version`; a reader chasing "what changed" reads
81
- the changelog. If they disagree, one is lying.
82
+ the changelog. If they disagree, one is lying. A release is the atomic moment
83
+ they agree again; splitting it across commits leaves a window where the build
84
+ warns and the panel is wrong.
82
85
 
83
- _How to apply._ On a release, edit both in the same commit ([C4](#c4)). Between
84
- releases, `about.version` stays at the last released value while
85
- `## [Unreleased]` accumulates bullets.
86
-
87
- _Enforcement._ Build-enforced (Tier-3). `pnpm validate:content` emits
88
- `knowledge-version-mismatch` when they differ, `knowledge-changelog-missing`
89
- when there is no released heading, and `knowledge-version-invalid` when
90
- `about.version` is not `x.y.z`. All are warnings — they never fail the build, but
91
- CI may choose to.
92
-
93
- _Exceptions._ None.
94
-
95
- ---
96
-
97
- ## C4
98
-
99
- **On a release, move `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and bump
100
- `about.version` in the same commit.**
101
-
102
- _Rationale._ A release is the atomic moment the two records agree again
103
- ([C3](#c3)). Splitting it across commits leaves a window where the build warns
104
- and the panel is wrong.
105
-
106
- _How to apply._
86
+ _How to apply._ Between releases, `about.version` stays at the last released
87
+ value while `## [Unreleased]` accumulates bullets ([C1](#c1)). On a release, in
88
+ one commit:
107
89
 
108
90
  1. Choose `x.y.z` from the accumulated `## [Unreleased]` bullets and [C2](#c2).
109
91
  2. Rename the heading to `## [x.y.z] - <today>`; add a fresh empty
@@ -111,6 +93,16 @@ _How to apply._
111
93
  3. Set `about.version` in `content/course.config.ts` to `x.y.z`.
112
94
  4. `pnpm validate:content` — no `knowledge-*` warning.
113
95
 
114
- _Enforcement._ Review-only (the end state is checked by [C3](#c3)).
96
+ `pnpm release:content` does steps 2 and 3 for you.
97
+
98
+ _Enforcement._ Build-enforced (Tier-3, `knowledge-version.ts`
99
+ `resolveKnowledgeVersion`). `pnpm validate:content` emits
100
+ `knowledge-version-mismatch` when they differ, `knowledge-changelog-missing`
101
+ when there is no released heading — which is also what a half-finished release
102
+ looks like — and `knowledge-version-invalid` when `about.version` is not
103
+ `x.y.z`. All are warnings; they never fail the build, but CI may choose to.
104
+ Partial: the build only ever sees the end state, so it cannot tell that the two
105
+ edits landed in one commit — it warns until the second half lands.
115
106
 
116
107
  _Exceptions._ None.
108
+