explico 0.12.0 → 0.13.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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -77,6 +77,8 @@ _How to apply._
77
77
  (thin space, then `\mathrm{d}x`), `\dfrac{\mathrm{d}f}{\mathrm{d}x}`,
78
78
  `\dfrac{\mathrm{d}^2 f}{\mathrm{d}x^2}`, `\mathrm{d}s`, `\mathrm{d}A`,
79
79
  `\mathrm{d}t`. An italic `d` is always a variable (a distance, a day count).
80
+ The resolver still reads a bare `d` as the differential (the base library,
81
+ [N4](#n4)), so an italic one does not fail the build: a reviewer catches it.
80
82
  This is the ISO 80000-2 convention, and it keeps the differential clear of a
81
83
  lesson that binds `d`. A partial derivative uses `\partial` bare —
82
84
  `\dfrac{\partial f}{\partial x}` — which is already distinct; do not wrap
@@ -122,8 +124,8 @@ _Rationale._ This is the core contract the completeness gate enforces
122
124
  corpus-wide; the rule here is just its author-facing statement. A reader hovers
123
125
  any symbol and gets exactly one card.
124
126
 
125
- _How to apply._ See the engine README's notation section and the `notation`
126
- collection schema. A letter that exists only so one entry's own `formula` can
127
+ _How to apply._ See the `notation` collection schema
128
+ (`src/compiler/content-schemas.ts`). A letter that exists only so one entry's own `formula` can
127
129
  be stated is a **gloss**, not a card — it creates no curriculum edge.
128
130
 
129
131
  Do not satisfy this rule by dumping every `[[key]]` a page uses into one
@@ -152,6 +154,12 @@ the body — otherwise the build fails with "Stale Explico manifest for
152
154
  […]." So every local entry still needs one `\explain{key}{…}` (or `[[key]]`)
153
155
  mention, even though the math gate alone wouldn't ask for it.
154
156
 
157
+ A local entry's key is its own: one that equals a shared entry's key wins on
158
+ its page, so `[[key]]` would mean one thing there and another in the
159
+ glossary and on every other page. `pnpm validate:content` warns
160
+ (`local-shadows-shared`); give the local entry a key of its own, or use the
161
+ shared one.
162
+
155
163
  The auto-annotator (`resolveMathGlyphs` in `math-glyphs.mjs`) re-derives every
156
164
  `\explain{…}` marker's insertion point from KaTeX's own parsed offsets, then
157
165
  re-applies those offsets to the raw source. Three shapes used to corrupt that
@@ -220,6 +228,42 @@ lesson and supplied component/formula context; a final Astro build check also
220
228
  rejects any `data-notation-key` in generated HTML that has no matching
221
229
  `data-notation-entry` source.
222
230
 
231
+ **Indexed symbols.** How the resolver binds a symbol with a subscript is part
232
+ of this rule and always on; [N23](#n23) builds on it. A whole form wins under
233
+ a power or a mark (`v_0^{2}` is `v_0` squared). An instance takes its
234
+ family's card ahead of a plain entry for its base (`x_1` opens the `x_k`
235
+ card), and only from a family whose index has the same shape. An indexed
236
+ entry lends its card to no bare symbol: a family (`\alpha_i`) explains its
237
+ own instances only, and an entry with a fixed index (`v_0`, `H_0`) only
238
+ itself. A bare `\alpha` that had a card only by borrowing one fails
239
+ `unresolved-glyph`, and the message names the indexed entry with that base:
240
+ give it its own entry, or `\explain{key}{\alpha}` where it really is the
241
+ family's shorthand. A function form still lends by its head (`V_0(1_t)`
242
+ explains `V_0(1_2)`), and an entry with only a superscript (`j^{(m)}`) still
243
+ lends to its base.
244
+
245
+ **A definition that is a convention.** Some definitions are one of several in
246
+ use: the sign on a moving-average coefficient, kurtosis with or without the
247
+ Gaussian 3 subtracted, information per observation or per sample, whom a cash
248
+ flow is positive for. The mathematics is the same and only the choice differs,
249
+ so a formula brought in from a source that chose the other way gives a wrong
250
+ answer and no error. Such an entry is defined once and cited with `[[key]]`
251
+ like any other, and it also says that it is a choice: its `meaning` names the
252
+ other choice in a clause, so the card warns where the symbol is used, and its
253
+ body says how to convert. A definition nobody writes differently (a residual
254
+ as observed minus fitted) is not a convention and needs neither. This part is
255
+ a guide; no check reads it.
256
+
257
+ ```md
258
+ ---
259
+ key: ma.coefficient
260
+ latex: \theta
261
+ meaning: Weight on the lagged innovation, added to the current one; Box–Jenkins and statsmodels subtract it instead.
262
+ ---
263
+ A formula from a source that subtracts the term holds for this course's
264
+ $\theta$ after replacing $\theta$ with $-\theta$.
265
+ ```
266
+
223
267
  _Enforcement._ Build-enforced. The completeness gate (`gate-math.ts`, `pnpm
224
268
  validate:content`; `astro build` re-runs the same resolver) fails on an
225
269
  unresolved glyph (`unresolved-glyph`) in every rendered-math context;
@@ -263,8 +307,17 @@ stripped before KaTeX like the label, heads the `[[eq-…]]` preview card and
263
307
  names the copy action ("Copy LaTeX — Range formula"). An equation takes no
264
308
  long caption ([components.md § The caption pair](components.md#the-caption-pair)).
265
309
 
310
+ A `$$…$$` block is one numbered equation, so it takes one label: to refer to
311
+ two rows, give each its own block. LaTeX's own numbering tools have no place
312
+ in it — the block already is the display, and the engine numbers it — so
313
+ `\begin{align}`, `\begin{equation}`, `\begin{gather}` and `\tag{…}` fail
314
+ with what to write instead (the rows of an `align` go in `\begin{aligned}`),
315
+ and so do `\eqref{…}`, `\ref{…}` and a `\label` without `eq:`: a reference
316
+ is `[[eq-key]]` in the prose.
317
+
266
318
  _Enforcement._ Partial. The converse is checked: `equations-validate.ts`
267
- reports `eq-key-unused` (info) for a labelled equation nothing references.
319
+ reports `eq-key-unused` (info) for a labelled equation nothing references,
320
+ and `eq-label-repeated` (an error) for a second `\label` in one block.
268
321
  An unlabelled-but-referenced equation builds; a reviewer catches the
269
322
  hand-numbering.
270
323
 
@@ -392,14 +445,14 @@ next to it was invisible to every check until this rule.
392
445
  _How to apply._
393
446
 
394
447
  - A table header cell that names a quantity uses the same bound symbol the
395
- lesson already introduced, `\explain{key}{…}`-wrapped if the column needs a
396
- component or a different style than the card's own notation (`v_x` for
397
- `[[motion.velocity]]`'s `v(t)`, exactly as `motion.acceleration`'s own
398
- `formula:` already does for `\explain{motion.velocity}{\mathbf{v}}`), with
399
- its unit folded into the same span as a `\text{}` suffix:
448
+ lesson already introduced, with its unit folded into the same span as a
449
+ `\text{}` suffix. A component takes its subscript the way [N23](#n23)
450
+ asks: an index entry `{}_x` makes `v_x` one derived card, "Horizontal
451
+ component of velocity". An `\explain{motion.velocity}{v_x}` wrap fails
452
+ `index-swallowed`, since that card describes the vector:
400
453
 
401
454
  ```text
402
- | $t\ \text{(s)}$ | $\explain{motion.velocity}{v_x}\ \text{(m/s)}$ |
455
+ | $t\ \text{(s)}$ | $v_x\ \text{(m/s)}$ |
403
456
  ```
404
457
 
405
458
  - An absolute value or norm is `\lvert…\rvert`, never a literal `|` — a raw
@@ -477,6 +530,12 @@ as a hard error with no Tier-3 warming-up period and no `lint-ignore.yml`
477
530
  entry, because each part landed in the same change that brought the whole
478
531
  corpus to zero — see the two files for the exact patterns.
479
532
 
533
+ A currency amount is prose too, its dollar escaped: `\$5 to \$6`. remark-math
534
+ pairs any two unescaped `$` on a line, so `$5 to $6` is one math span that
535
+ shows "5 to", dollars gone; `pnpm validate:content` fails such a pair
536
+ (`currency-dollar`, invariant `math-syntax`) — one whose content starts with
537
+ a digit and ends in a space or runs into a digit.
538
+
480
539
  _Settings._ `rules.N9.allow` in the course settings lists unit words the
481
540
  course lets prose spell out (`["hour", "minute"]`), singular or plural; a
482
541
  phrase is skipped only when every unit word in it is allowed.
@@ -522,11 +581,13 @@ _How to apply._
522
581
  itself a notation key stays a key.
523
582
 
524
583
  - A generic qualifier that functions like an operator's extremum subscript —
525
- `x_{\mathrm{opt}}`, an "optimal" reading with no quantity of its own — just
526
- works. The full baseline is the greppable set in `base-roman-labels.mjs`,
527
- kept deliberately small: unlike a trig function, an upright label is almost
528
- always a named quantity specific to one course, so it defaults to needing a
529
- card rather than defaulting to universal.
584
+ `x_{\mathrm{opt}}`, an "optimal" reading with no quantity of its own —
585
+ needs no card of its own under this rule. The symbol it indexes still does
586
+ ([N23](#n23)): a whole form `x_{\mathrm{opt}}`, or an index entry
587
+ `{}_{\mathrm{opt}}`. The full baseline is the greppable set in
588
+ `base-roman-labels.mjs`, kept deliberately small: unlike a trig function,
589
+ an upright label is almost always a named quantity specific to one course,
590
+ so it defaults to needing a card rather than defaulting to universal.
530
591
 
531
592
  - A named quantity gets a notation entry, exactly like a glyph ([N4](#n4)) —
532
593
  a shared card if the course teaches it broadly, otherwise `notation.local`,
@@ -694,7 +755,7 @@ resolves, table cells and component slots included — and
694
755
  `gateMathPunctuationInAssessments` every assessment `prompt` /
695
756
  `explanation`. It flags a `.`, `,`, `;` or `:` that is the last visible
696
757
  thing in the span, or in a row before a `\\`. It looks through what renders
697
- nothing there: spacing (`\,`, `\quad`), `\label{…}` / `\tag{…}`, a row
758
+ nothing there: spacing (`\,`, `\quad`), `\label{…}` / `\caption{…}`, a row
698
759
  break, `\end{…}`, the null delimiter `\right.`. It looks into a closing
699
760
  `\text{…}` / `\mathrm{…}`, so `5\ \text{m,}` is caught. A finding is located
700
761
  at the mark, so a per-place waiver can cover it.
@@ -1024,7 +1085,9 @@ or `a = b \le c` — write it as an `aligned` block. The first line carries the
1024
1085
  left-hand side. Every later line starts with `&` directly before its relation
1025
1086
  (`&=`, `&\le`, `&\approx`, `&\propto`), so the relation signs stand in one
1026
1087
  column. A step that needs a reason gets it at the end of its own line, after
1027
- `\qquad` (or `\quad` when the line is tight), inside `\text{…}`:
1088
+ `\qquad` (or `\quad` when the line is tight), inside `\text{…}`. A `\quad`
1089
+ clause that names an index and its set, `\quad i = 1, \dots, n`, is
1090
+ [N23](#n23)'s set statement, not a reason:
1028
1091
 
1029
1092
  ```text
1030
1093
  $$
@@ -1134,7 +1197,9 @@ _Enforcement._ Build-enforced (`consistency.ts` `checkNotationUnits`,
1134
1197
  and `units-vocab` (a token outside the vocabulary).
1135
1198
 
1136
1199
  _Exceptions._ A `lint-ignore.yml` entry (`- notation-units:<key> — <reason>`)
1137
- for a symbol whose unit is genuinely context-dependent.
1200
+ for a symbol whose unit is genuinely context-dependent. An index entry
1201
+ (`latex: "{}_0"`, [N23](#n23)) takes no units at all: a subscript has none,
1202
+ and the derived card it makes takes the indexed symbol's.
1138
1203
 
1139
1204
  ## N20
1140
1205
 
@@ -1168,33 +1233,33 @@ definition table) may cite the whole; say so in the locator (`"whole"`).
1168
1233
 
1169
1234
  **A gloss is named by a short noun phrase, not described by a sentence.**
1170
1235
 
1171
- A gloss binds a symbol on one page with a `label` shown in the hover card. The
1172
- label *names* the symbol; it does not explain it.
1236
+ A gloss binds a symbol on one page with a `name` shown in the hover card. The
1237
+ name *names* the symbol; it does not explain it.
1173
1238
 
1174
1239
  ```yaml
1175
1240
  # A name.
1176
- - key: launch-angle
1177
- label: launch angle
1241
+ - latex: \theta
1242
+ name: launch angle
1178
1243
 
1179
- # A description — belongs in a full notation entry's body, not a gloss label.
1180
- - key: launch-angle
1181
- label: the angle between the launch velocity and the horizontal, which we vary
1244
+ # A description — belongs in a full notation entry's body, not a gloss name.
1245
+ - latex: \theta
1246
+ name: the angle between the launch velocity and the horizontal, which we vary
1182
1247
  ```
1183
1248
 
1184
1249
  _Rationale._ A gloss is the lightweight form: a name and a glyph. Once the
1185
- label grows a clause or a sentence it is doing a notation entry's job without
1250
+ name grows a clause or a sentence it is doing a notation entry's job without
1186
1251
  that entry's units, sources, or shared identity — and the card shows a wall of
1187
1252
  text where every other card shows a name.
1188
1253
 
1189
- _How to apply._ Keep the label to a few words with no sentence punctuation or
1254
+ _How to apply._ Keep the name to a few words with no sentence punctuation or
1190
1255
  relative clause. If the symbol needs a description, promote the gloss to a
1191
1256
  notation entry (the `gloss-wants-promoting` hint under N4 says when).
1192
1257
 
1193
1258
  _Enforcement._ Build-enforced (`consistency.ts` `checkGlossNameShape`, `pnpm
1194
- validate:content`) — warning `gloss-name-shape` for a label over the word
1259
+ validate:content`) — warning `gloss-name-shape` for a name over the word
1195
1260
  limit or containing sentence punctuation / a relative clause.
1196
1261
 
1197
- _Exceptions._ None — a label that cannot be short is a notation entry.
1262
+ _Exceptions._ None — a name that cannot be short is a notation entry.
1198
1263
 
1199
1264
  ---
1200
1265
 
@@ -1295,6 +1360,7 @@ operator on the page, and why.
1295
1360
 
1296
1361
  | Code | Severity | When |
1297
1362
  | --- | --- | --- |
1363
+ | `accent-swallowed` | warning | `\explain{key}{\hat{\beta}}` names a key whose own `latex` has no `\hat`, so its card cannot explain the hat |
1298
1364
  | `suspended-operator-unresolved` | error | a decorated form of a suspended decoration has no whole form |
1299
1365
  | `operator-suspended` | info | a suspension with a `reason` — recorded, nothing to fix |
1300
1366
  | `operator-suspension-reason` | warning | a suspension without a `reason` |
@@ -1308,7 +1374,12 @@ _Enforcement._ Tier-3 warning `accent-explained` (`consistency.ts`
1308
1374
  `checkAccentExplained`) for a decoration that composes with no operator entry
1309
1375
  and is not on the allow list, one per decoration per lesson, surfaced by `pnpm
1310
1376
  validate:content` and silenced only by a reviewed line in `lint-ignore.yml`
1311
- (`accent-explained:\hat — <reason>`). Suspension is build-enforced: the
1377
+ (`accent-explained:\hat — <reason>`). The same function warns
1378
+ `accent-swallowed` for an `\explain{key}{…}` around a decorated form whose
1379
+ key's `latex` lacks that decoration (`\explain{beta}{\hat{\beta}}`): the wrap
1380
+ counts as the whole form, but its card says nothing about the hat. Wrap only
1381
+ the letter (`\hat{\explain{beta}{\beta}}`) and explain the hat, or name an entry
1382
+ for `\hat{\beta}`. Suspension is build-enforced: the
1312
1383
  completeness gate (`gate-math.ts`) and the render path (`remark-notation.mjs`)
1313
1384
  both fail `suspended-operator-unresolved`; `checkOperatorSuspension` emits the
1314
1385
  other three codes.
@@ -1316,3 +1387,184 @@ other three codes.
1316
1387
  _Exceptions._ Suspension (above) is the sanctioned way to give a decoration a
1317
1388
  different meaning on one page. Quoting a source that uses a decoration
1318
1389
  differently **and** showing what it means in the same passage.
1390
+
1391
+ ## N23
1392
+
1393
+ **An index is part of the symbol: a subscript is explained by the indexed
1394
+ symbol's entry, a family entry, or an index entry.**
1395
+
1396
+ `\mathbf{v}_0` is the initial velocity, not the velocity: hovering it must
1397
+ say so. A subscript on a symbol is explained in one of three ways, and a card
1398
+ on the base alone does not count. If more than one applies, the first wins,
1399
+ the same precedence [N22](#n22) uses for decorations:
1400
+
1401
+ 1. **Whole form** — the indexed symbol is a quantity of its own. Give it its
1402
+ own entry; that form gets that card, under a power too (`v_0^{2}` is `v_0`
1403
+ squared):
1404
+
1405
+ ```yaml
1406
+ key: initial-velocity
1407
+ latex: \mathbf{v}_0
1408
+ label: Initial velocity
1409
+ ```
1410
+
1411
+ 2. **Family** — the entry's index is a running letter: `latex: x_k`, "k-th
1412
+ iterate". It gives its card to every instance with the same shape — the
1413
+ same base and number of comma-separated slots: `x_1`, `x_{k+1}`, `x_N` —
1414
+ ahead of a plain `x` entry, and never to a bare `x`, which is a different
1415
+ object (it needs its own entry, or an `\explain{key}{x}` where it really
1416
+ is shorthand). A digit fills a running slot, but a `\mathrm{…}` or
1417
+ `\text{…}` label never does: `x_{\mathrm{e}}` is not an instance of
1418
+ `x_k`. Two families on one base that both fit an instance fail
1419
+ `ambiguous-base-fallback`: keep one per page, or `\explain` the instance.
1420
+ An entry whose index is fixed (`v_0`, `H_1`, `T_{\mathrm{opt}}`, or one
1421
+ marked `fixedIndex: true`) is a single symbol and lends its card to
1422
+ nothing. A function form still lends by its head: `V_0(1_t)` explains
1423
+ `V_0(1_2)`.
1424
+
1425
+ 3. **Index entry** — the subscript means the same thing on every symbol of
1426
+ the page. Give the subscript alone an entry: `latex` is the subscript on
1427
+ an empty group, quoted, because a bare `{}` starts a YAML mapping.
1428
+
1429
+ ```yaml
1430
+ key: motion.initial
1431
+ latex: "{}_0"
1432
+ label: Initial value
1433
+ meaning: The value of a quantity at the start of the scenario, the instant when time is zero.
1434
+ reads: "{base} evaluated at t = 0"
1435
+ formula: "{base}_0 = {base}\\,(\\explain{motion.time}{t} = 0)"
1436
+ ```
1437
+
1438
+ With the entry of the symbol it indexes, it makes a **derived card**: the
1439
+ whole `\mathbf{v}_0` is one target, titled "Velocity vector evaluated at
1440
+ t = 0". Its summary is the index entry's `meaning`, never the base's; its
1441
+ formula is the index entry's `formula` with `{base}` bound to the base
1442
+ symbol, so the reader opens the base's card from it; its units are the
1443
+ base's. `reads` defaults to "`<label>` of `{base}`" ("Horizontal component
1444
+ of velocity"); it names the symbol with `{base}` and is plain prose, with
1445
+ no `$`, `\` or backtick. An index entry is listed with a square for the
1446
+ symbol, `□₀`; a derived card is never listed. The loader rejects `units`,
1447
+ `dimensionless`, `range` or `fixedIndex` on an index entry ([N19](#n19)
1448
+ does not apply), and `reads` on any other entry. Two index entries for the
1449
+ same subscript on one page fail `ambiguous-canonical-notation`: an index
1450
+ has one meaning per page.
1451
+
1452
+ Arithmetic on an index letter with its own card is explained already:
1453
+ `X_{t-1}` is `X`'s card, `t`'s card (`time.index`) and an offset, and every
1454
+ comma-separated slot needs such a letter (`F_{0,T}` needs the `0`
1455
+ explained). A letter in a subscript is an index, so a base-library `i`, `e`
1456
+ or `d` there needs a card too.
1457
+
1458
+ A subscript on an operator or a bracket is not an index: `\sum_{i=1}`,
1459
+ `\lim_{t \to 0}`, `\log_{10}`, `\lVert x\rVert_2`, `\left.f\right\rvert_{x=0}`.
1460
+ A superscript is a power (`y^{(m)}` is out of scope). An entry whose `latex`
1461
+ has a superscript and no subscript (`j^{(m)}`) is a whole form for that
1462
+ symbol and, having no index, still lends its card to its bare base.
1463
+
1464
+ ### Running and fixed indices
1465
+
1466
+ A **running** index is a lowercase letter (`i`, `k`, `t`) that ranges over a
1467
+ set; the rule asks for that set, in one of two places:
1468
+
1469
+ - on the letter's card, or on the family entry that covers it, as `range`
1470
+ (LaTeX, shown on the card as "Runs over …"):
1471
+
1472
+ ```yaml
1473
+ key: time.index
1474
+ latex: t
1475
+ range: "1, \\dots, T"
1476
+ ```
1477
+
1478
+ - after a `\quad` on the first display line where the letter is an index,
1479
+ for a set that belongs to the page:
1480
+
1481
+ ```latex
1482
+ x_i \sim \mathcal{N}(\mu,\ \sigma^2), \quad i = 1, \dots, n
1483
+ ```
1484
+
1485
+ The clause names the letter and a relation (`=`, `\in`, `\le`, `\dots`, …).
1486
+ It is a different slot from [N18](#n18)'s `\qquad` reason.
1487
+
1488
+ A **fixed** index names one member: digits, a `\mathrm{…}` or `\text{…}`
1489
+ label, a subscript an index entry declares (`{}_x`), or a letter whose card
1490
+ says `fixedIndex: true` (a component `x`, a label). An uppercase or Greek
1491
+ letter in a subscript (`X_N`, `p_\theta`) is a size or a symbol, not a
1492
+ running index, and needs no set.
1493
+
1494
+ ### `\explain` around an indexed symbol
1495
+
1496
+ `\explain{key}{v_x}` makes the whole `v_x` one target with `key`'s card. If
1497
+ `key`'s own `latex` has no index (`motion.velocity` is `v(t)`), that card
1498
+ cannot explain the `x`, and the wrap hides the gap from the resolver: the
1499
+ rule reports it. Give the subscript an index entry (`{}_x`) and drop the wrap,
1500
+ or name an entry for the indexed symbol. An index inside a larger wrap
1501
+ (`\explain{survival-probability}{S(0,t_i)}`) is the wrap's, as every glyph
1502
+ inside one is.
1503
+
1504
+ | Code | Severity | When |
1505
+ | --- | --- | --- |
1506
+ | `index-explained` | error | a subscript on a symbol has no whole form, family or index entry, and a slot has no letter with its own card — a digit, a label, a base-library letter — or the base itself has no card |
1507
+ | `index-swallowed` | error | `\explain{key}{X_i}` names a key whose `latex` has no index |
1508
+ | `index-range` | error | a running index letter's card (and the family entry over it) has no `range`, and the first display line where it is an index has no `\quad` clause stating its set |
1509
+ | `derived-key-taken` | error | an authored entry's key equals a derived card's, `<base>.sub.<index>` |
1510
+
1511
+ In a lesson body each is reported once per indexed form (per letter, for
1512
+ `index-range`) per page, at its line; a `notation.local` formula counts with
1513
+ its page. An assessment counts on its own, and a shared entry's formula once
1514
+ per formula, with no line.
1515
+
1516
+ **Where the rule applies.** Everywhere math is rendered:
1517
+
1518
+ | Where | Derived cards | Checked by |
1519
+ | --- | --- | --- |
1520
+ | Lesson body (and a page that is not a chapter) | yes | `validate:content` (all four codes) |
1521
+ | Assessment prompt, explanation, option | yes | `validate:content` (`index-explained`, `index-swallowed`) |
1522
+ | Lab math (`renderLabMath` with a `labMathScope` table) | yes | `astro build`, as the lab renders |
1523
+ | `<Diagram>` node formula | yes | `astro build`, as the diagram renders |
1524
+ | Notation entry `formula:` | no | `validate:content` (`index-explained`, `index-swallowed`) |
1525
+
1526
+ A notation formula resolves against its own card's table, so an index entry
1527
+ does not compose there. Explain an indexed symbol in a formula with a whole
1528
+ form instead: `\explain{motion.launch-speed}{v_0}`, or a gloss on the entry
1529
+ (`glosses:` with `latex: v_0` and its `name`, as in [N21](#n21)). Or wrap the subscript in the index entry's key,
1530
+ `\mathbf{v}_{\explain{motion.initial}{0}}`, which gives the `0` that card.
1531
+ `index-range` reads lesson bodies only: a letter's set is stated once, on its
1532
+ card or on the page.
1533
+
1534
+ _Rationale._ The resolver binds a letter by its shape, so before this rule a
1535
+ subscript inherited whatever its base meant: `\mathbf{v}_0` opened "Velocity
1536
+ vector", with only the `v` highlighted and the `0` explained by nothing. An
1537
+ indexed entry also lent its card to symbols it is not: the family `\alpha_i`
1538
+ ("ARCH coefficient") opened on a bare `\alpha` meaning a learning rate, and
1539
+ `H_0` ("Null hypothesis") on `H_1`. A printed book's list of symbols makes
1540
+ the same split this rule does: `v_0` when it is a named quantity, and a
1541
+ "Subscripts" group that gives `0` once when it means the same thing on many
1542
+ symbols. And a running index without its set is a familiar omission: the
1543
+ reader cannot tell whether `i` runs to `n` or to `N`.
1544
+
1545
+ _How to apply._ For each reported form, pick the first that fits: a whole
1546
+ form for a named quantity, a family for a running index, an index entry for a
1547
+ subscript that recurs with one meaning. The same index on the same letter can
1548
+ mean different things on two pages (`x_0` as a starting point, a center, a
1549
+ first observation), so an index entry is often page-local
1550
+ (`notation.local`). Give each index letter's card a `range`, or state the set
1551
+ after a `\quad`.
1552
+
1553
+ _Enforcement._ Partial (`gate-indices.ts` `gateIndices`, `pnpm
1554
+ validate:content`) — the four errors above, over lesson bodies, notation
1555
+ formulas and assessments. A lesson-body finding is located, so `{/*
1556
+ explico-allow N23: <reason> */}` waives one place. Lab math
1557
+ (`render-lab-math.ts`) and `<Diagram>` formulas fail `astro build` with the
1558
+ same findings as they render (`indexFindings`). A reviewer checks the rest:
1559
+ that a `\quad` clause states the right set, and that a lowercase index the
1560
+ build treats as running really runs. A course switches the rule
1561
+ off with `rules.N23.enabled: false`, which stops all of it; the derived cards
1562
+ are built either way. The resolver behaviors the rule rests on — whole forms
1563
+ under a power, families by shape, no lending from a fixed label or to a bare
1564
+ symbol — are part of [N4](#n4) and always on: a bare symbol that has no card
1565
+ of its own fails `unresolved-glyph`, and its message names the indexed
1566
+ entry with that base.
1567
+
1568
+ _Exceptions._ A `\quad` clause the check misreads takes a waiver with its
1569
+ reason. A lab whose glyph table is not the one `labMathScope` returned (a
1570
+ copy) composes no derived card and is not checked.
@@ -31,5 +31,6 @@ fails when it is stale.
31
31
  | N7 | The differential `d` is upright `\mathrm{d}`. | 2026-09-26 | [N2](README.md#rule-index) | `\mathrm{d}` is a line in N2’s `reserved` table. |
32
32
  | N16 | In AI-generated mathematical content, alternate delimiter types for nested grouping when their shape is stylistic; preserve every conventional or semantic delimiter shape. | 2026-09-26 | [N17](README.md#rule-index) | Folded into N17 with the preference dropped: a delimiter’s shape carries its meaning, so depth is shown by size — which this engine handles automatically — not by alternating shapes. The rule also scoped itself to “AI-generated” content, which is not a property of the page. |
33
33
  | C4 | On a release, move `## [Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and bump `about.version` in the same commit. | 2026-09-26 | [C3](README.md#rule-index) | The release procedure and the invariant it keeps are one rule, and `knowledge-changelog-missing` (owned by C3) was already the check that fired on a half-finished release. |
34
+ | S5 | A sign or perspective convention is declared once and cited everywhere else. | 2026-10-07 | [N4](README.md#rule-index) | Its check knew one finance course’s phrasing and one key per course, and never fired in any course; the `conventions` setting it read is gone. A convention is a definition, so it is now N4’s: defined once on its notation entry, whose `meaning` names the other choice and whose body says how to convert. |
34
35
 
35
36
  <!-- RETIRED-RULES:END -->
@@ -2,8 +2,8 @@
2
2
 
3
3
  `S` rules govern the shape of a lesson's headings, the short captions on its
4
4
  keyed floats, the paragraphs that open a part page, what a page that is not
5
- a chapter may carry, and how a table, a
6
- diagram, or a convention is declared so the rest of the course can reach it —
5
+ a chapter may carry, and how a table or a
6
+ diagram is declared so the rest of the course can reach it —
7
7
  the parts of a page a reader scans, links to, or cites. This
8
8
  is not [structure.md](structure.md), which is the taxonomy these rules (and
9
9
  the rest of the engine) speak in, not a rule set itself.
@@ -28,7 +28,9 @@ tooltip — the `[short]` half of LaTeX's `\caption[short]{long}`. The long
28
28
  `caption` is never measured: it is the full sentence shown with the float's
29
29
  number (see [components.md § The caption pair](components.md#the-caption-pair)).
30
30
  A heading is measured as the reader sees it: its markers (`[collapsed]`,
31
- `[card]`, …) never render, so they never count.
31
+ `[card]`, …) never render, so they never count; a formula counts as its
32
+ glyphs (`$\explain{motion.range}{R}$` is one character) and a `[[key]]` term
33
+ as its label.
32
34
 
33
35
  A heading or short caption that runs long is usually doing a caption's job.
34
36
  Move the rest of the sentence into the paragraph right after a heading, or
@@ -230,43 +232,6 @@ fail the build.
230
232
  _Exceptions._ A purely decorative diagram that nothing will ever cite may stay
231
233
  unkeyed; say so in a comment next to it.
232
234
 
233
- ## S5
234
-
235
- **A sign or perspective convention is declared once and cited everywhere
236
- else.**
237
-
238
- A course names one notation entry as its sign convention
239
- (`conventions.signConventionKey`) and one competency as its cash-flow /
240
- perspective convention (`conventions.cashFlowPerspectiveCompetency`). A lesson
241
- whose prose states such a convention — "outflows are negative", "angles are
242
- measured counter-clockwise from the horizontal" — resolves it to that single
243
- declaration rather than restating it.
244
-
245
- ```mdx
246
- <!-- Restated: a second, drifting definition. -->
247
- In this chapter, an outflow is a negative cash flow.
248
-
249
- <!-- Resolved to the declaration. -->
250
- Cash flows follow the [[cash-flow.sign]] convention: an outflow is negative.
251
- ```
252
-
253
- _Rationale._ A convention restated in several lessons drifts — one says
254
- "negative", another "shown in parentheses" — and a reader who learned it in
255
- one place cannot tell whether the other place means the same thing. One
256
- declaration, cited, keeps the meaning in one file.
257
-
258
- _How to apply._ Cite the convention's notation entry with `[[key]]` in the
259
- sentence that invokes it, or list the convention's competency in the lesson's
260
- `teaches` / `requires`. Either anchors the prose to the declaration.
261
-
262
- _Enforcement._ Build-enforced (`consistency.ts`
263
- `checkConventionSingleDefinition`, `pnpm validate:content`) — a warning
264
- (`convention-single-definition`) for a lesson whose prose matches the
265
- convention pattern and is anchored by neither the term nor the competency.
266
-
267
- _Exceptions._ The lesson that *is* the declaration — the one that teaches the
268
- convention's competency — is anchored by definition.
269
-
270
235
  ## S6
271
236
 
272
237
  **Headings descend one level at a time: a lesson body starts at `##` and
@@ -307,7 +272,11 @@ _Enforcement._ Build-enforced (`gate-heading-structure.ts`, `pnpm
307
272
  validate:content`, `heading-skip`) over a lesson's outline — the top-level
308
273
  headings the section numbers count. A heading inside a component's children
309
274
  (a `<Memo>`, a `<CompactExample>`) belongs to that component and is not part
310
- of the outline, and a memo's own bar is not a heading of the lesson.
275
+ of the outline, and a memo's own bar is not a heading of the lesson. A
276
+ `<CompactExamples>` or an `<AssessmentSet>` draws an `<h2>` of its own, so
277
+ one inside a `<Memo>` or a `<ShortNote>` fails (`heading-nested`): its
278
+ heading would land under the memo's `<h6>`, and "On this page" would never
279
+ list it. Put it at the top level, after the memo.
311
280
 
312
281
  _Exceptions._ None in the rule; waive one place with
313
282
  `{/* explico-allow S6: reason */}` above the heading.
@@ -106,6 +106,13 @@ Part 2 Projectile motion
106
106
 
107
107
  ## Part pages
108
108
 
109
+ A part is listed in `course.settings.json` → `parts` as `{ "dir", "label" }`
110
+ and lives in `content/docs/<dir>/`. A part listed before anything is written
111
+ in it — no chapter and no overview page yet — is left out of the sidebar and
112
+ the numbering, with a `part-empty` warning, so the parts after it keep their
113
+ numbers until it has content. A lesson in a directory no part lists fails
114
+ `validate:content` (`lesson-outside-parts`).
115
+
109
116
  A part is a directory, so `/<part>/` is a URL a reader will eventually type,
110
117
  follow, or be handed by a search engine. Without a page it is a 404, and the
111
118
  part exists only as a sidebar heading that cannot be linked to.
@@ -309,7 +316,11 @@ punctuation dropped, so `## Two ways to name the same point` is
309
316
  `-1`, `-2`. A reference to a heading without a number (deeper than `depth`, or
310
317
  with `show: false`) prints the heading's title in quotes instead: a reader
311
318
  could not find the number. A reference that names no top-level heading fails
312
- `pnpm validate:content` (`sec-ref-resolves`) and the build.
319
+ `pnpm validate:content` (`sec-ref-resolves`) and the build. So does a heading
320
+ whose anchor the page already gives something else
321
+ (`heading-anchor-collision`): `## References` on a page that cites a source
322
+ (the References section is generated, at `#references`), or `## Tab totals`
323
+ beside `<Table id="totals">` (`#tab-totals`).
313
324
 
314
325
  ### Structure from the bottom up
315
326
 
@@ -347,6 +358,13 @@ worked-example set (`<CompactExamples>`) is numbered **`‹part›.‹chapter›
347
358
  - **Keyed or not, everything counts.** An equation with no `\label{eq:…}` still
348
359
  takes a number and a positional anchor (`#eq-2-1-4`); it just cannot be
349
360
  referenced by name.
361
+ - **The lesson's flow counts, a callout does not.** An equation or a float
362
+ in a list item or a `>` quote counts like one at the top level, and so
363
+ does one in a `<Memo>`, `<ShortNote>` or `<Fold>`. One in a `:::note`
364
+ aside (or `<Aside>`, or a worked example) is illustrative and takes no
365
+ number; a `<Table>` or `<Figure>` there fails the build. A display
366
+ equation is a `$$` block whose fences sit on lines of their own:
367
+ `$$x$$` inside a sentence, or on one line, is inline math.
350
368
  - **The number sits with the caption.** A figure or diagram prints it in the
351
369
  caption line below its picture, a table in the caption line above, and a
352
370
  display equation beside it. What names each
@@ -571,7 +589,9 @@ A block folds in one of two ways, and every default lives in one place, the
571
589
  `sections`, `subsections` and `knowledgeChecks` take `{ collapsible,
572
590
  defaultOpen }`; the others take `{ defaultOpen }`; `tables` takes
573
591
  `{ truncateAfter }` — a table with more rows shows that many, half from each
574
- end, and a "… N more rows …" line. There is no `disclosure.diagrams`: a
592
+ end, and a "… N more rows …" line, unless that would hide a single row (the
593
+ line is as tall as the row, so the table shows whole). There is no
594
+ `disclosure.diagrams`: a
575
595
  diagram does not fold. Every collapsible remembers the reader's own choice,
576
596
  per page, over the default (`rememberState={false}` on a memo, a fold, a
577
597
  figure's tools or an example set opts that one out). A short note's in-place
@@ -646,3 +666,34 @@ While collapsed, the surrounding sentence remains one paragraph and only the
646
666
  point in the reading flow, and exposes a “Show less” control that restores the
647
667
  original sentence. Short notes are for a locally optional explanation, not for
648
668
  material that needs a number or citations elsewhere; use a memo for that.
669
+
670
+ ## Retired keys
671
+
672
+ A key the engine has stopped reading fails the build wherever a content file
673
+ still sets it, so a course cannot keep maintaining a field that does nothing.
674
+ Each `retired-key` error names the file and the key's line; deleting the line
675
+ clears it.
676
+
677
+ | Key | Where it was set | Why it went |
678
+ | --- | --- | --- |
679
+ | `editorialStatus` | pages, notation entries, abbreviations, competencies, assessments, sources, tracks | Lessons are reviewed continuously, so no page ever earns a one-time "reviewed" stamp. The draft / in-review / reviewed labels it put on part pages, notation cards, glossary entries and citations are gone. |
680
+
681
+ Two of Starlight's own keys do not work in a course, because the manifest,
682
+ not Starlight, decides which chapters a course lists and in what order. A
683
+ lesson's `draft: true` fails the build as `unsupported-key`: Starlight would
684
+ leave the page out of a production build while the sidebar still names it.
685
+ Keep an unfinished lesson outside `content/docs/` until it is ready. A
686
+ chapter's `sidebar.order` warns as `ignored-key`: chapters follow the order of
687
+ the tracks (`content/tracks/*.json`).
688
+
689
+ ## Content schemas
690
+
691
+ Every content file is checked against its collection's schema — the same
692
+ one `astro build` loads it with (`src/compiler/content-schemas.ts`) — by
693
+ `pnpm validate:content` and `pnpm content check`, so a value the build would
694
+ reject fails there first, at the key's line, as a `content-schema` error:
695
+ a notation `meaning` with `$` or `\`, a source `type` outside `primary`,
696
+ `official-guidance`, `paper`, `book` and `secondary`, or a `studyMinutes`
697
+ range whose low end exceeds its high end. A lesson is checked for the fields
698
+ explico adds (`teaches`, `assumptions`, `studyMinutes`, `notation`);
699
+ Starlight checks its own (`title`, `description`, …) when the site builds.