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
@@ -11,105 +11,100 @@ and "Review-only" mean here, and how to amend a rule.
11
11
 
12
12
  ---
13
13
 
14
- ## N1
15
-
16
- **The normal distribution is `\mathcal{N}`.**
17
-
18
- Write a Gaussian as `\mathcal{N}(\mu, \sigma^2)` — calligraphic `N`,
19
- parameterised by **mean and variance**. Name any other named distribution the
20
- same way: a calligraphic letter through `\mathcal{…}`.
21
-
22
- Do not use `N(\mu, \sigma^2)`, `\mathrm{N}`, `Norm(...)`, or `\mathbb{N}` — the
23
- last is the natural numbers ([N2](#n2)) and a build-reserved token.
14
+ ## N2
24
15
 
25
- _Rationale._ One corpus-wide spelling makes the symbol searchable, lets it bind
26
- to a single notation card, and keeps it clear of `N` used as a count and
27
- `\mathbb{N}` used as a set. Mean/variance is the form the standard-normal CDF
28
- `\Phi` and PDF `\varphi` compose with cleanly.
16
+ **A reserved spelling means only what it is reserved for, and every other styled
17
+ letter is a symbol of its own.**
18
+
19
+ A course keeps a short table of spellings that are fixed to one meaning: nothing
20
+ else may use them. Whether a reserved spelling *also* needs a notation entry is a
21
+ separate question, and rule [N4](#n4)'s gate answers it — reserving a spelling is
22
+ a convention, not an exemption. The shipped table (`rules.N2.reserved` in
23
+ `course.settings.json`) is:
24
+
25
+ | Spelling | Means | Notation entry |
26
+ | --- | --- | --- |
27
+ | `\mathbb{N}`, `\mathbb{Z}`, `\mathbb{R}`, `\mathbb{C}` | the naturals, integers, reals, complexes | not needed — the only four tokens the resolver treats as universal (`IGNORED_MATH_TOKENS`) |
28
+ | `\mathbb{P}` | probability | yes, like any other identifier |
29
+ | `\mathcal{N}(\mu, \sigma^2)` | the normal distribution, by mean and variance | yes |
30
+ | `\mathrm{d}` | the differential | not needed — a `\mathrm{…}` body is skipped whole ([N10](#n10)) |
31
+
32
+ Every _other_ styled letter — `\mathbb{E}`, `\mathbb{Q}`, `\mathbb{1}`,
33
+ `\mathcal{L}`, `\mathbf{v}`, an italic `d` used as a variable — is an ordinary
34
+ symbol that resolves to a notation entry of its own, even when the plain letter
35
+ already has one ([N4](#n4)).
36
+
37
+ _Rationale._ Two facts, one rule. A reserved spelling is genuinely universal, so
38
+ a per-page definition of `\mathbb{R}` would be noise — and reserving it stops a
39
+ second meaning ("`\mathbb{N}` for the sample size") from colliding with the
40
+ first. Everything outside the table is domain-specific and earns a card: a
41
+ styled letter is a different glyph from its plain letter, and a reader who sees
42
+ `\mathbb{E}` hovers it expecting the expectation, not an energy `E`.
29
43
 
30
44
  _How to apply._
31
45
 
32
- - Define it once. Add a notation entry — a shared card if the course teaches
33
- probability, otherwise `notation.local` on the first lesson that needs it:
46
+ - **The number systems** need nothing: write `\mathbb{R}^n`, `x \in \mathbb{Z}`.
47
+ Need the rationals as a set on one lesson? `\mathbb{Q}` is not reserved, so
48
+ give it an entry (or `\explain{...}{\mathbb{Q}}`) and note it in the domain
49
+ vocabulary.
50
+
51
+ - **Probability** is `\mathbb{P}(A)` — never a bare `P` (a price, a power, a
52
+ point), never `\Pr`, never `\text{Prob}`. Conditional probability is
53
+ `\mathbb{P}(A \mid B)`, with `\mid`, not a bare `|` ([N15](#n15)).
54
+ Expectation is the same family, `\mathbb{E}[X]`, and — being outside the
55
+ table — takes an entry:
34
56
 
35
57
  ```yaml
36
- key: prob.normal
37
- latex: \mathcal{N}
38
- label: Normal distribution
39
- meaning: Gaussian distribution with mean μ and variance σ².
58
+ key: prob.p
59
+ latex: \mathbb{P}
60
+ label: Probability
61
+ meaning: Probability of an event.
40
62
  ```
41
63
 
42
- Then introduce it in prose with `[[prob.normal]]` before its first appearance
43
- in math, exactly like any other shared glyph ([N4](#n4)).
44
-
45
- - In a one-off context where a full entry is overkill, use the escape hatch:
46
- `\explain{prob.normal}{\mathcal{N}}(\mu, \sigma^2)`. Prefer the entry.
47
-
48
- - If a course standardises on standard deviation rather than variance, state
49
- that once in the domain notes, then write `\mathcal{N}(\mu, \sigma)`
50
- consistently. Never mix the two forms in one corpus.
51
-
52
- - Compose consistently: `X \sim \mathcal{N}(\mu, \sigma^2)`,
53
- `\Phi` for its CDF, `\varphi` for its PDF. Pick the CDF/PDF glyphs once per
54
- course.
55
-
56
- _Enforcement._ Build-enforced that the symbol resolves to a key — `\mathcal{N}`
57
- is a bindable identifier atom to the glyph resolver, so an undefined one fails
58
- the completeness gate at `file:line:token`. The choice of `\mathcal{N}` over
59
- `N` / `\mathrm{N}` / `Norm` is convention, checked in review.
64
+ Introduce it in prose with `[[prob.p]]` before its first appearance in math
65
+ ([N4](#n4)). A course doing measure theory may add `\mathbb{Q}` as the
66
+ risk-neutral measure to its own `reserved` list, and state the split once in
67
+ the domain notes.
68
+
69
+ - **The normal distribution** is `\mathcal{N}(\mu, \sigma^2)` — calligraphic
70
+ `N`, parameterised by mean and variance; not `N(\mu, \sigma^2)`,
71
+ `\mathrm{N}`, `Norm(...)`, or `\mathbb{N}`. Name any other distribution the
72
+ same way, with a calligraphic letter. `\mathcal{N}` needs an entry of its own
73
+ even on a page that defines a plain `N` as a count, and it composes with `\Phi`
74
+ for its CDF and `\varphi` for its PDF — pick those two once per course.
75
+
76
+ - **The differential** is upright `\mathrm{d}`: `\int_a^b f(x)\,\mathrm{d}x`
77
+ (thin space, then `\mathrm{d}x`), `\dfrac{\mathrm{d}f}{\mathrm{d}x}`,
78
+ `\dfrac{\mathrm{d}^2 f}{\mathrm{d}x^2}`, `\mathrm{d}s`, `\mathrm{d}A`,
79
+ `\mathrm{d}t`. An italic `d` is always a variable (a distance, a day count).
80
+ This is the ISO 80000-2 convention, and it keeps the differential clear of a
81
+ lesson that binds `d`. A partial derivative uses `\partial` bare —
82
+ `\dfrac{\partial f}{\partial x}` — which is already distinct; do not wrap
83
+ it. `\mathrm{d}` is a typographic label, not a bindable atom, so it needs no
84
+ key of its own ([N10](#n10)); only the variable of integration still has to
85
+ resolve.
86
+
87
+ - **Changing the table** is a course decision, made in one place:
88
+ `rules.N2.reserved` in `course.settings.json`, which the settings form editor
89
+ renders as a list. A course with no probability drops those two lines; a
90
+ course that parameterises the normal by standard deviation writes
91
+ `\mathcal{N}(\mu, \sigma)` there and then uses that form everywhere. The
92
+ effective table is printed on `build/rules-in-effect.md`, so an agent reads it
93
+ without opening this page.
94
+
95
+ _Enforcement._ Review-only. The completeness gate ([N4](#n4)) forces every
96
+ styled letter but the four number systems to resolve — to its own entry, since a
97
+ styled letter never falls back to the plain letter's card and the base library
98
+ has no card for it — and an unresolved one fails at `file:line:token`. But
99
+ *which* spelling was chosen for a meaning, and whether a reserved one was reused
100
+ for something else, is convention, caught in review. The two lists are
101
+ deliberately different things: `IGNORED_MATH_TOKENS`
102
+ (`src/reference/math-glyphs.mjs`) is what needs no key, `rules.N2.reserved` is
103
+ what may not mean anything else.
60
104
 
61
105
  _Exceptions._ Quoting a source that uses a different glyph **and** showing the
62
- mapping to `\mathcal{N}` in the same passage.
63
-
64
- ---
65
-
66
- ## N2
67
-
68
- **`\mathbb{N}`, `\mathbb{Z}`, `\mathbb{R}`, `\mathbb{C}` are reserved for the
69
- number systems.**
70
-
71
- These four blackboard-bold tokens mean the naturals, integers, reals, and
72
- complexes, always, and need no notation key. Every _other_ `\mathbb{…}` —
73
- `\mathbb{E}`, `\mathbb{P}`, `\mathbb{Q}`, `\mathbb{1}` — is an ordinary
74
- identifier that must resolve to a semantic key.
75
-
76
- _Rationale._ The universal sets are genuinely universal; forcing a per-page
77
- definition of `\mathbb{R}` would be noise. Everything else in blackboard bold
78
- is domain-specific (an expectation, a probability measure, an indicator) and
79
- earns a card.
80
-
81
- _How to apply._ Need the rationals as a set on one lesson? That collides with a
82
- reserved token — reach for `\explain{...}{\mathbb{Q}}` or a differently-styled
83
- glyph, and note it in the domain vocabulary.
84
-
85
- _Enforcement._ Partial. The gate forces every non-reserved `\mathbb{…}` to
86
- resolve; keeping `N/Z/R/C` for the number systems (not, say, `\mathbb{N}` for a
87
- sample size) is convention.
88
-
89
- _Exceptions._ None for the four reserved tokens.
90
-
91
- ---
92
-
93
- ## N3
94
-
95
- **Multi-letter roman labels use `\mathrm{…}`.**
96
-
97
- An upright multi-letter name — `\mathrm{PV}`, `\mathrm{opt}`, `\mathrm{max}`
98
- where no built-in operator exists — is wrapped in `\mathrm{}`. Never write it as
99
- a run of bare italic letters (`PV`), which reads as a product `P \cdot V`.
100
-
101
- _Rationale._ `\mathrm{}` is a typographic label, not a bindable atom — the glyph
102
- resolver skips its body whole. Bare letter runs are parsed as identifier atoms,
103
- so `PV` asks the gate to resolve `P` and `V` separately: a build failure at
104
- best, a wrong binding at worst.
105
-
106
- _How to apply._ Subscripts too: `v_{\mathrm{max}}`, not `v_{max}`.
107
-
108
- _Enforcement._ Build-enforced indirectly — bare multi-letter runs hit the
109
- completeness gate as unresolved identifiers.
110
-
111
- _Exceptions._ Genuine products, and single-letter subscripts that are
112
- themselves notation keys.
106
+ mapping to the reserved spelling in the same passage. None for the four number
107
+ systems.
113
108
 
114
109
  ---
115
110
 
@@ -184,7 +179,22 @@ notation," at `pnpm validate:content` time. Wrap the whole declared form —
184
179
  `\explain{z-statistic}{Z_N}` — instead; bare `Z_N` elsewhere on the page then
185
180
  resolves automatically. The same applies to accent-wrapped forms
186
181
  (`latex: \widehat{\theta}`): wrap the accent and its argument together, not
187
- just the inner letter.
182
+ just the inner letter. A whole-form entry like that is one of three ways to
183
+ satisfy [N22](#n22), which treats every accent or mark as an operator.
184
+
185
+ A **font-styled letter is its own glyph.** `\mathbf{x}`, `\boldsymbol{\beta}`,
186
+ `\bm{x}`, `\mathcal{N}`, `\mathscr{L}`, `\mathfrak{g}`, `\mathbb{E}`,
187
+ `\mathsf{x}` and `\mathtt{x}` never borrow the card of the plain letter on the
188
+ same page: a bold vector is not the scalar, and an expectation is not an
189
+ energy. Each needs its own entry (`latex: \mathbf{x}`) or an
190
+ `\explain{key}{\mathbf{x}}` wrap, or it fails as `unresolved-glyph`, with a
191
+ hint that it is a different symbol. A styled base-library letter
192
+ (`\mathbf{e}`, `\boldsymbol{\pi}`) is not Euler's number or π either. The
193
+ exceptions are `\mathit{x}` and `\mathnormal{x}`, which render the plain
194
+ italic letter and so share its card, and the four reserved number systems
195
+ ([N2](#n2)). The leak ran one way only: a `\mathcal{N}` entry never bound a
196
+ plain `N`. This applies to every rendered-math context — lesson body,
197
+ formulas, assessments and labs.
188
198
 
189
199
  Math authored inside a nested Astro component is outside the lesson MDX scan,
190
200
  so the component must declare the formula text at render time. `Diagram` and
@@ -210,8 +220,14 @@ lesson and supplied component/formula context; a final Astro build check also
210
220
  rejects any `data-notation-key` in generated HTML that has no matching
211
221
  `data-notation-entry` source.
212
222
 
213
- _Enforcement._ Build-enforced (completeness gate + one-meaning-per-glyph
214
- consistency check + final rendered-trigger/payload parity check).
223
+ _Enforcement._ Build-enforced. The completeness gate (`gate-math.ts`, `pnpm
224
+ validate:content`; `astro build` re-runs the same resolver) fails on an
225
+ unresolved glyph (`unresolved-glyph`) in every rendered-math context;
226
+ `consistency.ts` adds the corpus-level lints `glyph-unique-in-corpus`
227
+ (warning), `weak-local`, `gloss-wants-promoting`, `card-wants-demoting`
228
+ (info); a final rendered-trigger/payload parity check backs it at render time.
229
+ This rule is **locked** — the notation layer is this rule, so a course cannot
230
+ turn it off.
215
231
 
216
232
  _Exceptions._ None. Genuinely different meanings need different glyphs or
217
233
  different pages.
@@ -226,98 +242,33 @@ If prose points back at a display equation — this page or another — the equa
226
242
  has `\label{eq:key}` and the reference is `[[eq-key]]`. Do not hand-number
227
243
  equations in the text.
228
244
 
229
- _Rationale._ The engine numbers every display equation `‹part›.‹n›` — the part
230
- is the sidebar group's position in `courseConfig.parts`, `n` counts the display
231
- equations across that whole part in appearance order, continuing from one
232
- chapter into the next (`##` and `###` headings do not affect it; see
233
- [`structure.md`](structure.md)) — and derives the anchor and any cross-lesson
234
- link from the label. A hand-typed "(3)" goes stale the moment an equation is
235
- inserted above it, or the chapter moves within its part. `<Table>` and
236
- `<Figure>` are numbered the same way, each with its own counter.
245
+ _Rationale._ The engine numbers every display equation
246
+ `‹part›.‹chapter›.‹n›` — the part is the sidebar group's position in
247
+ `courseConfig.parts`, the chapter the lesson's position in its part, and `n`
248
+ counts the chapter's display equations in appearance order (`##` and `###`
249
+ headings do not affect it; a course may count across the whole part instead,
250
+ see [`structure.md` § Numbering](structure.md#numbering)) — and derives the
251
+ anchor and any cross-lesson link from the label. A hand-typed "(3)" goes stale
252
+ the moment an equation is inserted above it, or the chapter moves within its
253
+ part. `<Table>` and `<Figure>` are numbered the same way, each with its own
254
+ counter.
237
255
 
238
256
  _How to apply._ Only label equations you actually reference; an unlabelled
239
- display equation is still fine.
257
+ display equation is still fine. An equation may also carry a short caption,
258
+ `\caption{Range formula}`, next to its label inside the `$$…$$`: it is
259
+ stripped before KaTeX like the label, heads the `[[eq-…]]` preview card and
260
+ names the copy action ("Copy LaTeX — Range formula"). An equation takes no
261
+ long caption ([components.md § The caption pair](components.md#the-caption-pair)).
240
262
 
241
- _Enforcement._ Review-only — an unlabelled-but-referenced equation builds; a
242
- reviewer catches the hand-numbering.
263
+ _Enforcement._ Partial. The converse is checked: `equations-validate.ts`
264
+ reports `eq-key-unused` (info) for a labelled equation nothing references.
265
+ An unlabelled-but-referenced equation builds; a reviewer catches the
266
+ hand-numbering.
243
267
 
244
268
  _Exceptions._ None worth stating.
245
269
 
246
270
  ---
247
271
 
248
- ## N6
249
-
250
- **Probability is `\mathbb{P}` — double-struck `P`.**
251
-
252
- Write a probability as `\mathbb{P}(A)` — blackboard-bold `P`. Not `P(A)` (a bare
253
- `P` is a variable — a price, a power, a point), not `\Pr`, not `\text{Prob}`.
254
- Expectation is the same family: `\mathbb{E}[X]`.
255
-
256
- _Rationale._ `\mathbb{P}` is unmistakably the probability functional, never a
257
- quantity. It reads next to `\mathbb{E}` and `\mathbb{Q}` as one notation family
258
- and never collides with a `P` the lesson uses for something physical.
259
-
260
- _How to apply._
261
-
262
- - Define it once, like [N1](#n1):
263
-
264
- ```yaml
265
- key: prob.p
266
- latex: \mathbb{P}
267
- label: Probability
268
- meaning: Probability of an event.
269
- ```
270
-
271
- Introduce it in prose with `[[prob.p]]` before its first appearance in math.
272
-
273
- - Conditional probability is `\mathbb{P}(A \mid B)` — `\mid`, not a bare `|`.
274
-
275
- - A course doing measure theory may define `\mathbb{P}` as the real-world
276
- measure and `\mathbb{Q}` as a risk-neutral one, consistent with the reserved
277
- reading of `\mathbb{Q}` ([N2](#n2)). State that split once in the domain
278
- notes.
279
-
280
- _Enforcement._ Build-enforced that the symbol resolves to a key — `\mathbb{P}`
281
- is a non-reserved `\mathbb{…}` and the completeness gate demands a binding
282
- ([N2](#n2)). The choice of `\mathbb{P}` over `P` / `\Pr` is convention.
283
-
284
- _Exceptions._ Quoting a source that writes `\Pr` or `P` **and** showing the
285
- mapping to `\mathbb{P}` in the same passage.
286
-
287
- ---
288
-
289
- ## N7
290
-
291
- **The differential `d` is upright `\mathrm{d}`.**
292
-
293
- The `d` of an integrand and the `d` of a derivative are the same operator and
294
- get the same upright form — distinct from an italic `d`, which is always a
295
- variable (a distance, a day count, a dimension):
296
-
297
- - `\int_a^b f(x)\,\mathrm{d}x` — thin space `\,`, then `\mathrm{d}x`.
298
- - `\dfrac{\mathrm{d}f}{\mathrm{d}x}`, `\dfrac{\mathrm{d}^2 f}{\mathrm{d}x^2}`.
299
- - `\mathrm{d}s`, `\mathrm{d}A`, `\mathrm{d}V`, `\mathrm{d}t` — same rule for
300
- every differential.
301
-
302
- A partial derivative uses `\partial` bare — `\dfrac{\partial f}{\partial x}` —
303
- which is already distinct; do not wrap it in anything.
304
-
305
- _Rationale._ This is the ISO 80000-2 convention. Upright `\mathrm{d}` cannot be
306
- read as a factor `d \cdot x`, and it stays clear of a notation key `d` on a
307
- lesson that happens to have one.
308
-
309
- _How to apply._ `\mathrm{d}` is a typographic operator, not a bindable atom —
310
- the glyph resolver skips its body like any `\mathrm{…}` label ([N3](#n3)), so it
311
- needs no notation key. Only the variable of integration (`x`, `t`) still has to
312
- resolve.
313
-
314
- _Enforcement._ Build-enforced indirectly — a bare `\int f(x) dx` puts an
315
- unresolved identifier `d` in front of the completeness gate ([N3](#n3)).
316
-
317
- _Exceptions._ A verbatim quotation of a source in a blockquote.
318
-
319
- ---
320
-
321
272
  ## N8
322
273
 
323
274
  **A named operator is universal, or it is explained.**
@@ -325,12 +276,12 @@ _Exceptions._ A verbatim quotation of a source in a blockquote.
325
276
  Every named operator a lesson uses — `\exp`, `\int`, `\sin`, … as built-ins,
326
277
  `\operatorname{Var}`, `\operatorname{Cov}`, `\operatorname{E}` as declared ones
327
278
  — is either on the shipped operator baseline (`src/reference/base-operators.mjs`
328
- plus this course's `operators.allow` in `course.config.ts`) or it carries a
279
+ plus this course's `rules.N8.allow` in `course.settings.json`) or it carries a
329
280
  notation entry the reader can hover.
330
281
 
331
282
  _Rationale._ The completeness gate has no opinion about operators: KaTeX parses
332
283
  `\exp` as an operator node and `\operatorname{Var}` as an `operatorname` node,
333
- and the glyph resolver skips both whole ([N3](#n3)). So `\operatorname{Var}`
284
+ and the glyph resolver skips both whole ([N10](#n10)). So `\operatorname{Var}`
334
285
  renders fine while the reader is never told what "Var" means. `\exp` and `\int`
335
286
  need no such help — everyone knows them — but a domain operator does, and the
336
287
  line between the two is a course decision, made once, not per lesson.
@@ -353,7 +304,7 @@ _How to apply._
353
304
 
354
305
  Then introduce it in prose with `[[stats.variance]]` before its first
355
306
  appearance in math, and write it `\operatorname{Var}(X)` — never `Var(X)`
356
- (bare letters, [N3](#n3)) and never `\mathrm{Var}` (a label, not an operator).
307
+ (bare letters, [N10](#n10)) and never `\mathrm{Var}` (a label, not an operator).
357
308
 
358
309
  - A course whose domain treats an operator as common — `\operatorname{Var}`,
359
310
  `\operatorname{Cov}`, `\operatorname{Corr}`, `\operatorname{sgn}` in a
@@ -364,13 +315,17 @@ _How to apply._
364
315
  ```
365
316
 
366
317
  Names are the `\operatorname{…}` body, or the command without its backslash
367
- for a built-in. `operators.deny` forces a baseline operator to be explained
318
+ for a built-in. `rules.N8.deny` forces a baseline operator to be explained
368
319
  anyway.
369
320
 
370
- _Enforcement._ Tier-3 warning (`operator-explained`), surfaced by
371
- `pnpm validate:content`, recorded in the manifest, and silenced only by a
372
- reviewed line in `lint-ignore.yml` (`operator-explained:Var — <reason>`). Like
373
- `formula` before it (D15), the tier can be promoted once a corpus is clean.
321
+ - Decorations (`\hat`, `'`, `^*`) are operators too, with a rule of their
322
+ own: [N22](#n22).
323
+
324
+ _Enforcement._ Tier-3 warning (`operator-explained`, `consistency.ts`
325
+ `checkOperatorExplained`), surfaced by `pnpm validate:content`, recorded in
326
+ the manifest, and silenced only by a reviewed line in `lint-ignore.yml`
327
+ (`operator-explained:Var — <reason>`). Like `formula` before it (D15), the
328
+ tier can be promoted once a corpus is clean.
374
329
 
375
330
  _Exceptions._ Quoting a source that uses an operator glyph **and** showing what
376
331
  it means in the same passage.
@@ -397,18 +352,31 @@ the unit portion in `\text{}` and fold it into the _same_ math span as the
397
352
  symbol it modifies (`$t\ \text{(s)}$`, not two adjacent spans `$t$
398
353
  $\text{(s)}$`) — and prefer the short symbol over the spelled-out word even
399
354
  inside `\text{}` (`\text{(s)}`, not `\text{(seconds)}`), the same way a
400
- variable is `v_0` and not "initial velocity" once it's introduced. This does
401
- **not** cover a fully spelled-out unit used as an ordinary English noun in a
402
- sentence — "the ball leaves at 10 m/s, 30 **degrees** above the horizontal"
403
- keeps "degrees" as a word; that carve-out is about flowing prose, not a
404
- compact data label like a table header or a computed answer.
355
+ variable is `v_0` and not "initial velocity" once it's introduced.
356
+
357
+ That holds in flowing prose too: a unit that quantifies something is a symbol,
358
+ not a word. "A time of 4 seconds" is `a time of $4\ \text{s}$`; "2 meters per
359
+ second squared" is `$2\ \text{m/s}^2$`; "30 degrees above the horizontal" is
360
+ `$30^{\circ}$ above the horizontal`; "$x = 3$ meters" is `$x = 3\ \text{m}$` —
361
+ the number and its unit share one span. A unit **named** without a quantity
362
+ stays a word ("a position measured in meters", "what is its range, in
363
+ meters?"), and so does a non-unit use of the same word ("the second
364
+ derivative", "three degrees of freedom"). Where a sentence _defines_ a unit
365
+ ("one meter per second is the speed that covers one meter each second"),
366
+ waive that block rather than contort it:
367
+
368
+ ```mdx
369
+ {/* explico-allow N9: this sentence defines the unit */}
370
+ One meter per second is the speed that covers one meter each second.
371
+ ```
405
372
 
406
373
  This also does not apply inside a JSX component prop (`<Figure caption="…">`,
407
374
  `<Table caption="…">`, `title="…"`) — a prop string is handed to the DOM as
408
375
  plain text with no math rendering at all, so `$…$` there would show up as
409
376
  broken literal dollar signs instead of rendering; write the unit as a word or
410
377
  the symbol as plain Unicode there instead ("peaks at 45°" in a caption is
411
- fine).
378
+ fine). The same goes for an assessment's choice `label`, which renders as
379
+ plain text too.
412
380
 
413
381
  _Rationale._ A reader who can hover `$v_0$` for its notation card gets nothing
414
382
  from the same quantity spelled `v0` in a table header — the two look
@@ -438,7 +406,7 @@ _How to apply._
438
406
 
439
407
  - A specific (per-unit) quantity like kinetic energy per unit mass is
440
408
  `\mathrm{KE}/m`, not `KE/m` — multi-letter roman labels still take
441
- `\mathrm{}` ([N3](#n3)) inside the math span.
409
+ `\mathrm{}` ([N10](#n10)) inside the math span.
442
410
 
443
411
  - A unit exponent is a real LaTeX superscript (`\text{m/s}^2`), never the
444
412
  Unicode "²"/"³" character — KaTeX's strict mode rejects those as unknown
@@ -474,8 +442,7 @@ _How to apply._
474
442
  to work with. A bare `$t$`/`$x$` already does, via the ordinary `[[key]]`
475
443
  intro + auto-resolution every other bound glyph on the page uses; the one
476
444
  case that doesn't is a `\mathrm{…}` compound like `\mathrm{KE}/m`, because
477
- the glyph resolver never looks inside a bare `\mathrm{}` body ([N3](#n3),
478
- [N10](#n10)) — explicitly `\explain{}`-wrap each side there
445
+ the glyph resolver never looks inside a bare `\mathrm{}` body ([N10](#n10)) — explicitly `\explain{}`-wrap each side there
479
446
  (`\explain{kinetic-energy}{\mathrm{KE}}/\explain{mass}{m}`) so the plugin
480
447
  has something to read; a header with more than one bound key composes them
481
448
  joined by `" / "`.
@@ -492,14 +459,29 @@ just as invisible to a hover card as one in a lesson. Both look for: an
492
459
  escaped `\|`; a numeral glued to an abbreviated unit (`45 m/s`); a bare unit
493
460
  in parens (`(s)`, `(m/s²)`); a hand-typed `_`/`^` between alphanumerics; and a
494
461
  `/` between two short alphabetic tokens that isn't on a small reviewed idiom
495
- allowlist (`and/or`, `his/her`, …) in the check's own source. Unlike every
496
- other build-enforced rule here, N9 ships as a hard error
497
- with no Tier-3 warming-up period and no `lint-ignore.yml` entry, because it
498
- landed in the same change that brought the whole corpus to zero — see the
499
- check's file for the exact patterns and the allowlist.
500
-
501
- _Exceptions._ None acknowledged through `lint-ignore.yml` — that registry is
502
- for corpus-level warnings a reviewer accepts, and N9 has no warning tier to
462
+ allowlist (`and/or`, `his/her`, …) in the check's own source — all reported
463
+ as `prose-notation`. A unit written as a word is reported as `spelled-unit`
464
+ (`unit-words.ts`, called from both entry points): an SI-style unit word
465
+ (meter, second, minute, hour, gram, newton, joule, watt, hertz, pascal,
466
+ volt, ampere, kelvin, mole, liter, radian, degree, and their common
467
+ prefixed forms) after a number in digits or words (`4 seconds`, `two-meter`)
468
+ or after a typeset number (`$x = 3$ meters`), before or after `per`, or with
469
+ `square` / `cubic` / `squared` / `cubed`. The message spells out the math to
470
+ write instead. "Degrees of freedom", a hyphenated compound (`second-order`)
471
+ and a unit word standing alone are not flagged; calendar time and currency
472
+ are not in the list. Unlike every other build-enforced rule here, N9 ships
473
+ as a hard error with no Tier-3 warming-up period and no `lint-ignore.yml`
474
+ entry, because each part landed in the same change that brought the whole
475
+ corpus to zero — see the two files for the exact patterns.
476
+
477
+ _Settings._ `rules.N9.allow` in the course settings lists unit words the
478
+ course lets prose spell out (`["hour", "minute"]`), singular or plural; a
479
+ phrase is skipped only when every unit word in it is allowed.
480
+
481
+ _Exceptions._ One place is excused with a waiver on the line above its block,
482
+ `{/* explico-allow N9: reason */}` — a sentence that defines a unit, or quotes
483
+ a source verbatim. None acknowledged through `lint-ignore.yml`: that registry
484
+ is for corpus-level warnings a reviewer accepts, and N9 has no warning tier to
503
485
  accept into. A genuinely non-mathematical slash idiom (`input/output`, …) is
504
486
  added to the allowlist in `gate-prose-notation.ts` itself, reviewed like any
505
487
  other change to this file.
@@ -508,25 +490,34 @@ other change to this file.
508
490
 
509
491
  ## N10
510
492
 
511
- **A `\mathrm{…}` multi-letter label is universal, or it is explained.**
493
+ **A multi-letter roman label is written `\mathrm{…}`, and it is universal or it
494
+ is explained.**
512
495
 
513
- Every multi-letter roman label a lesson uses in math — `\mathrm{KE}`,
514
- `\mathrm{PV}`, `\mathrm{opt}` ([N3](#n3)) — is either on the shipped
515
- roman-label baseline (`src/reference/base-roman-labels.mjs` plus this
516
- course's `romanLabels.allow` in `course.config.ts`) or it carries a notation
517
- entry the reader can hover.
496
+ A word-like name inside math — `KE`, `PV`, `opt`, `max` where no built-in
497
+ operator exists — is wrapped in `\mathrm{}`: `\mathrm{KE}`,
498
+ `v_{\mathrm{max}}`, never a bare run of italic letters. And the label itself is
499
+ either on the shipped roman-label baseline
500
+ (`src/reference/base-roman-labels.mjs` plus this course's `rules.N10.allow` in
501
+ `course.settings.json`) or it carries a notation entry the reader can hover.
518
502
 
519
- _Rationale._ N3 fixes the _format_ (`\mathrm{}`, never bare letters) but is
520
- silent on whether the label is ever explained, because — same as an
503
+ _Rationale._ Two halves of one mistake. `\mathrm{}` is a typographic label, not
504
+ a bindable atom: a bare `PV` is parsed as identifier atoms, so it asks the gate
505
+ to resolve `P` and `V` separately — a build failure at best, a wrong binding at
506
+ worst. Wrapping it fixes the parse but not the reader, because — same as an
521
507
  `\operatorname{}` body ([N8](#n8)) — the glyph resolver skips a `\mathrm{}`
522
- body whole; nothing about satisfying N3 requires a definition to exist. N9's
508
+ body whole, so nothing about the wrap requires a definition to exist. N9's
523
509
  enforcement fixed one direction of this exact page (`KE`/`PE` typed as plain
524
- text); this rule closes the other: `KE`/`PE` typed correctly as `\mathrm{}`
525
- but with no card at all, which N9 cannot see because the notation is already
526
- inside `$…$`.
510
+ text); this rule closes the other two: `KE` typed as bare italics, and
511
+ `\mathrm{KE}` typed correctly but with no card at all, which N9 cannot see
512
+ because the notation is already inside `$…$`.
527
513
 
528
514
  _How to apply._
529
515
 
516
+ - **Write the label upright.** `\mathrm{PV}`, `\mathrm{opt}`,
517
+ `v_{\mathrm{max}}` — subscripts too, `v_{\mathrm{max}}` and never `v_{max}`.
518
+ A genuine product stays bare letters, and a single-letter subscript that is
519
+ itself a notation key stays a key.
520
+
530
521
  - A generic qualifier that functions like an operator's extremum subscript —
531
522
  `x_{\mathrm{opt}}`, an "optimal" reading with no quantity of its own — just
532
523
  works. The full baseline is the greppable set in `base-roman-labels.mjs`,
@@ -555,18 +546,22 @@ _How to apply._
555
546
  romanLabels: { allow: ['PV', 'NPV'] }
556
547
  ```
557
548
 
558
- Names are the `\mathrm{…}` body. `romanLabels.deny` forces a baseline label
549
+ Names are the `\mathrm{…}` body. `rules.N10.deny` forces a baseline label
559
550
  to be explained anyway.
560
551
 
561
- _Enforcement._ Tier-3 warning (`roman-label-explained`), surfaced by
562
- `pnpm validate:content`, recorded in the manifest, and silenced only by a
563
- reviewed line in `lint-ignore.yml` (`roman-label-explained:KE — <reason>`).
552
+ _Enforcement._ Tier-3 warning (`roman-label-explained`, `consistency.ts`
553
+ `checkRomanLabelExplained`), surfaced by `pnpm validate:content`, recorded in
554
+ the manifest, and silenced only by a reviewed line in `lint-ignore.yml`
555
+ (`roman-label-explained:KE — <reason>`).
564
556
  Same rollout shape as [N8](#n8): the tier can be promoted once a corpus is
565
- clean.
557
+ clean. Partial, because the first half has no check of its own: a bare
558
+ multi-letter run is reported by [N4](#n4)'s completeness gate as unresolved
559
+ one-letter symbols, which is a different message for the same mistake, and a
560
+ run whose letters all happen to be bound slips through.
566
561
 
567
562
  _Exceptions._ Quoting a source that uses a roman-label glyph **and** showing
568
563
  what it means in the same passage. A single-letter `\mathrm{…}` body (`d`,
569
- [N7](#n7)) is out of scope entirely — that is a fixed typographic operator,
564
+ the differential, [N2](#n2)) is out of scope entirely — that is a fixed typographic operator,
570
565
  not a named quantity.
571
566
 
572
567
  ---
@@ -662,6 +657,16 @@ notation — a comma separating two side-by-side equations in one display block
662
657
  (`x = \ldots, \qquad y = \ldots`) is a list separator inside the math, not
663
658
  prose leaking in, and is unaffected by this rule.
664
659
 
660
+ The same holds for each row of a multi-line block: a row of an `aligned` or
661
+ `cases` block never ends in a comma or full stop before its `\\`. The comma
662
+ between a `cases` value and its condition (`1, & x > 0`) is notation and
663
+ stays.
664
+
665
+ Two endings that look like notation are not allowed either. A trailing
666
+ decimal point (`\hat{\beta} = 5.`) is sometimes used to say "exactly this
667
+ precision", but a reader cannot tell it from the full stop. Write `5`, or
668
+ `5.0` when the precision matters. A typed `...` is `\ldots`.
669
+
665
670
  _Rationale._ A trailing period or comma inside `$…$` becomes part of the
666
671
  LaTeX source: KaTeX renders it in math italic/spacing rather than as
667
672
  punctuation, it is included in whatever `\text{}` it landed inside (turning a
@@ -678,16 +683,26 @@ this corpus (see [N5](#n5)'s `\label{eq:…}` pattern) already ends bare. For
678
683
  inline math (`$x = 5$` mid-sentence), put the period or comma after the
679
684
  closing `$`, where it has always belonged.
680
685
 
681
- _Enforcement._ Review-only. A gate could, in principle, flag `.` or `,` as
682
- the last character before a closing `$`/`$$`, but it cannot tell a stray
683
- sentence-ending mark from a legitimate mathematical use of the same
684
- character (a decimal point, a `\ldots`, a matrix row separator) without
685
- false positives outweighing the catches — left to review, the same as
686
- [N5](#n5).
687
-
688
- _Exceptions._ A comma or period that is itself notation — a decimal point in
689
- a number, `\ldots`, a matrix/vector component separator — is not this rule's
690
- target and stays exactly where the math needs it.
686
+ _Enforcement._ Build-enforced (`gate-math-punctuation.ts`, wired into the
687
+ same `pnpm validate:content` pass as [N9](#n9), code
688
+ `math-trailing-punctuation`, an error). `gateMathPunctuation` reads every
689
+ `$…$` / `$$…$$` span in a lesson body — the same spans the completeness gate
690
+ resolves, table cells and component slots included — and
691
+ `gateMathPunctuationInAssessments` every assessment `prompt` /
692
+ `explanation`. It flags a `.`, `,`, `;` or `:` that is the last visible
693
+ thing in the span, or in a row before a `\\`. It looks through what renders
694
+ nothing there: spacing (`\,`, `\quad`), `\label{…}` / `\tag{…}`, a row
695
+ break, `\end{…}`, the null delimiter `\right.`. It looks into a closing
696
+ `\text{…}` / `\mathrm{…}`, so `5\ \text{m,}` is caught. A finding is located
697
+ at the mark, so a per-place waiver can cover it.
698
+
699
+ _Exceptions._ An upright dotted abbreviation that is itself notation may end
700
+ the math: `+ \text{h.c.}`, `+ \text{c.c.}`, `X_n \to X\ \text{a.s.}`. The
701
+ shipped list is `a.e.`, `a.s.`, `c.c.`, `const.`, `h.c.`, `i.i.d.`; a course
702
+ adds one with `rules.N12.allow` and drops one with `rules.N12.deny` in
703
+ `course.settings.json`. Set in math italic (`h.c.` without `\text{}`), it is
704
+ not on the list. Anything else is waived in place with
705
+ `{/* explico-allow N12: reason */}`.
691
706
 
692
707
  ---
693
708
 
@@ -742,7 +757,7 @@ _How to apply._
742
757
  _Enforcement._ Build-enforced, with no partial-corpus warm-up tier — like
743
758
  [N9](#n9), it ships as a hard error. `resolveMathGlyphs`
744
759
  (`src/reference/math-glyphs.mjs`) walks the parsed KaTeX tree for every
745
- rendered-math context this engine has — the same single resolver [N1](#n1)
760
+ rendered-math context this engine has — the same single resolver [N2](#n2)
746
761
  through [N10](#n10) already rely on — and fails the moment a
747
762
  `PARENTHESIZED_FUNCTIONS` name is not immediately followed by a visible
748
763
  opening delimiter. Because every context (lesson body, `notation.formula`,
@@ -769,7 +784,7 @@ does not require reproducing it.
769
784
  Every all-caps acronym (`OLS`, `MSE`, `SSR`) or dotted abbreviation (`w.r.t.`,
770
785
  `i.i.d.`) a lesson uses in ordinary prose — outside any `$…$` — is either on
771
786
  the shipped abbreviation baseline (`src/reference/base-abbreviations.mjs`
772
- plus this course's `abbreviations.allow` in `course.config.ts`), carries an
787
+ plus this course's `rules.N14.allow` in `course.settings.json`), carries an
773
788
  entry in the `abbreviations` content collection
774
789
  (`content/abbreviations/*.md`), or is already explained as a math quantity on
775
790
  the same page ([N8](#n8) / [N10](#n10)). A term resolved by the table is not
@@ -815,7 +830,7 @@ _How to apply._
815
830
  abbreviations: { allow: ['GDP', 'CPU'] }
816
831
  ```
817
832
 
818
- `abbreviations.deny` forces a shipped baseline entry (`e.g.`, `i.e.`, …) to
833
+ `rules.N14.deny` forces a shipped baseline entry (`e.g.`, `i.e.`, …) to
819
834
  be explained anyway.
820
835
 
821
836
  _Enforcement._ Build-enforced (`gate-abbreviations.ts`), wired into the same
@@ -829,7 +844,7 @@ was added.
829
844
 
830
845
  _Exceptions._ None acknowledged through `lint-ignore.yml`. A genuinely
831
846
  universal abbreviation this course's readers cannot be expected to look up is
832
- added to `abbreviations.allow` in `course.config.ts`, reviewed like any other
847
+ added to `rules.N14.allow` in `course.settings.json`, reviewed like any other
833
848
  change to that file.
834
849
 
835
850
  ---
@@ -885,7 +900,7 @@ _How to apply._
885
900
  - A `|` has no distinct open/close glyph, so a lone pair — `\left|x\right|`,
886
901
  or bare `|x|` when it is not nested in anything — reads as one absolute
887
902
  value/norm, matched left to right. Conditional probability still prefers
888
- `\mid` over a bare `|` regardless of nesting ([N6](#n6)).
903
+ `\mid` over a bare `|` regardless of nesting ([N2](#n2)).
889
904
  - A delimiter inside `\text{…}` is display text, not a sizing mark — a unit
890
905
  written `\text{(s)}` ([N9](#n9)) is untouched by this rule even when the
891
906
  math around it also uses `(`.
@@ -914,91 +929,14 @@ notation.
914
929
 
915
930
  ---
916
931
 
917
- ## N16
918
-
919
- **In AI-generated mathematical content, alternate delimiter types for nested
920
- grouping when their shape is stylistic; preserve every conventional or
921
- semantic delimiter shape.**
922
-
923
- For a nest whose delimiters do nothing except group an expression, prefer this
924
- cycle from the inside out:
925
-
926
- 1. `( ... )`
927
- 2. `[ ... ]`
928
- 3. `\{ ... \}`
929
- 4. repeat the cycle if another grouping level is genuinely necessary.
930
-
931
- This is a preference for generated notation, not a mechanical rewrite. Do not
932
- change or alternate a delimiter whose type is part of the mathematical
933
- notation. Preserve, among other conventional forms:
934
-
935
- - function application such as `f(x)`;
936
- - expectation notation such as `\mathbb{E}[X]`;
937
- - intervals such as `[a,b]` and `(a,b]`;
938
- - sets and set-builder notation;
939
- - tuples and coordinates;
940
- - commutators and probability notation;
941
- - matrices, vectors, indexing, norms, and absolute values.
942
-
943
- When there is doubt about whether the delimiter is semantic, keep the
944
- conventional notation. Do not alternate merely to complete the cycle.
945
-
946
- _Rationale._ Repeating the same shape at every purely stylistic grouping level
947
- makes the boundaries harder to scan. Alternation can expose the grouping
948
- structure, but only mathematical context can distinguish a disposable grouping
949
- mark from notation whose shape carries meaning. That judgment belongs to the
950
- authoring agent or reviewer, not a syntax linter.
951
-
952
- _How to apply._ Decide whether each pair is stylistic before choosing its
953
- shape. Alternate only the pairs that pass that test. Sizes are not your
954
- concern: [N17](#n17) hands every nested pair to the engine, which grows each
955
- level automatically. Shape is an authoring decision; size is not.
956
-
957
- ```text
958
- \left[
959
- a+
960
- \left(
961
- b+
962
- \left[
963
- c+
964
- \left(d+e\right)
965
- \right]
966
- \right)
967
- \right] ✓
968
- ```
969
-
970
- Semantic notation stays intact even when stylistic grouping surrounds it:
971
-
972
- ```text
973
- \left[
974
- \mathbb{E}\!\left[
975
- f\!\left(X\right)
976
- \right]
977
- +
978
- \left(
979
- a+b
980
- \right)
981
- \right] ✓
982
- ```
983
-
984
- In particular, never turn `\mathbb{E}[X]` into another delimiter type merely
985
- to alternate shapes.
986
-
987
- _Enforcement._ Review-only. Delimiter semantics cannot be inferred reliably
988
- from LaTeX syntax, so no automated check enforces alternation. The N17 linter
989
- checks sizing syntax only and never recommends changing a delimiter type.
990
-
991
- _Exceptions._ Every conventional or semantically meaningful delimiter is
992
- outside the alternation preference. Preserving established mathematical
993
- notation takes precedence over the stylistic cycle.
994
-
995
- ---
996
-
997
932
  ## N17
998
933
 
999
- **A nest of scalable `()`, `[]`, or `\{\}` delimiters is sized by the engine:
1000
- use `\left ... \right` at every level, and never hand-size a pair inside a nest
1001
- with `\bigl`, `\Bigl`, `\biggl`, or `\Biggl`.**
934
+ **A delimiter's shape carries its meaning and never changes to show nesting: a
935
+ nest is sized by the engine — `\left ... \right` at every level, never
936
+ `\bigl`, `\Bigl`, `\biggl`, or `\Biggl` inside it.**
937
+
938
+ Two decisions, one rule, and neither is the author's to improvise. *Shape* is
939
+ fixed by what the delimiter means; *size* belongs to the engine.
1002
940
 
1003
941
  ```text
1004
942
  \left[a+\left(b+\left[c+\left(d+e\right)\right]\right)\right] ✓
@@ -1030,9 +968,17 @@ _How to apply._
1030
968
  - Do not "help" the engine with a manual size. If a nest still looks wrong,
1031
969
  the expression is too deep to read — restructure it, or pull an inner group
1032
970
  into its own display, rather than hand-sizing it.
1033
- - This rule is independent of [N16](#n16): shapes still alternate where they
1034
- are stylistic, and semantic shapes are still preserved. Only the *size*
1035
- decision moved to the engine.
971
+ - **Keep every shape as its meaning requires it.** Function application
972
+ `f(x)`, expectation `\mathbb{E}[X]`, an interval `[a, b)`, a set or
973
+ set-builder `\{…\}`, a tuple, a commutator, a matrix, an index, a norm
974
+ `\|·\|`, an absolute value — the shape is part of the notation and never
975
+ changes. A plain parenthesis is the shape for grouping.
976
+ - **Never alternate shapes to mark depth.** `( [ \{ … \} ] )` for a nest whose
977
+ delimiters only group is not this house's style: with the sizing below, depth
978
+ is already visible, and a reader who meets `[` inside a grouping nest has to
979
+ rule out an interval, an expectation and a matrix first. When in doubt about
980
+ whether a pair is semantic, keep the conventional notation — which is the
981
+ same answer as "do not alternate".
1036
982
  - A *lone* hand-sized pair that is not part of a nest is untouched — a tuple
1037
983
  written `\bigl(x(t),\ y(t)\bigr)` has no hierarchy to get wrong and stays a
1038
984
  deliberate typographic choice.
@@ -1053,9 +999,317 @@ suggests the `\left`/`\right` replacement, repeating the exact opening and
1053
999
  closing delimiter shapes it found. Malformed pairs remain KaTeX syntax errors;
1054
1000
  the linter does not guess at their structure.
1055
1001
 
1056
- The automated check deliberately does **not** infer mathematical semantics or
1057
- lint delimiter-type alternation.
1002
+ The automated check deliberately does **not** infer mathematical semantics, so
1003
+ the shape half of this rule is review-only: `delimiter-manual-size` parses
1004
+ sizing syntax and never decides whether a bracket is semantic or suggests
1005
+ turning one shape into another. A bare nested delimiter is [N15](#n15)'s error
1006
+ rather than this rule's warning.
1058
1007
 
1059
1008
  _Exceptions._ None inside a nest. Bare, non-nested semantic notation such as
1060
1009
  `f(x)` or `\mathbb{E}[X]` is outside this linter and remains governed by the
1061
1010
  mathematical convention and [N15](#n15)'s separate bare-nesting rule.
1011
+
1012
+ ---
1013
+
1014
+ ## N18
1015
+
1016
+ **Write a multi-step derivation as one chain: one step per line, aligned at
1017
+ the relation sign, with any justification on the same line after `\qquad`.**
1018
+
1019
+ When a display takes an expression through two or more steps — `a = b = c`,
1020
+ or `a = b \le c` — write it as an `aligned` block. The first line carries the
1021
+ left-hand side. Every later line starts with `&` directly before its relation
1022
+ (`&=`, `&\le`, `&\approx`, `&\propto`), so the relation signs stand in one
1023
+ column. A step that needs a reason gets it at the end of its own line, after
1024
+ `\qquad` (or `\quad` when the line is tight), inside `\text{…}`:
1025
+
1026
+ ```text
1027
+ $$
1028
+ \begin{aligned}
1029
+ \operatorname{Var}(Y)
1030
+ &=\mathbb{E}\left[\left(Y-\mathbb{E}[Y]\right)^2\right]
1031
+ \qquad\text{definition of the variance}\\
1032
+ &=\mathbb{E}\left[Y^2\right]-\mathbb{E}[Y]^2
1033
+ \qquad\text{linearity of }\mathbb{E}
1034
+ \end{aligned}
1035
+ \label{eq:variance-shortcut}
1036
+ $$ ✓
1037
+ ```
1038
+
1039
+ ```text
1040
+ $$
1041
+ \operatorname{Var}(Y)=\mathbb{E}\left[\left(Y-\mathbb{E}[Y]\right)^2\right]=\mathbb{E}\left[Y^2\right]-\mathbb{E}[Y]^2
1042
+ $$ ✗ one line: grows sideways, no room for reasons
1043
+
1044
+ $$\operatorname{Var}(Y)=\mathbb{E}\left[\left(Y-\mathbb{E}[Y]\right)^2\right]$$
1045
+ which, by linearity of the expectation, equals
1046
+ $$\mathbb{E}\left[Y^2\right]-\mathbb{E}[Y]^2$$ ✗ one chain split by prose
1047
+ ```
1048
+
1049
+ _Rationale._ A chain written on one line grows sideways with every step and
1050
+ soon runs past the reading column, where it scrolls or spills; an aligned
1051
+ chain grows downward and is only as wide as its widest step. Relation signs
1052
+ in one column let a reader compare consecutive right-hand sides and check
1053
+ each step on its own. A reason on the step's own line sits next to what it
1054
+ justifies, where a reason in the following paragraph ("the second equality
1055
+ holds because …") has to be matched back to a position by counting. And the
1056
+ whole derivation stays one display equation: one number ([N5](#n5)), one
1057
+ link target, one copy-LaTeX unit.
1058
+
1059
+ _How to apply._
1060
+
1061
+ - Use `aligned` inside `$$…$$`. End every line but the last with `\\`.
1062
+ - Put `&` directly before the relation, never after it, so the relation signs
1063
+ line up; a continuation line starts with `&=` (or its own relation).
1064
+ - A justification goes after the step's math and before its `\\`, separated
1065
+ by `\qquad`, as `\text{…}`. Math inside a justification leaves the text
1066
+ group: `\text{linearity of }\mathbb{E}`.
1067
+ - A justification names the fact it uses — a definition, a property, an
1068
+ assumption, a named result ("independence of the errors", "Jensen's
1069
+ inequality"). Never type another equation's number into the math:
1070
+ [N5](#n5) forbids hand numbering, and the engine cannot link from inside
1071
+ math. When a step rests on another display equation, link that equation
1072
+ with `[[eq-key]]` in the sentence that introduces the chain ("Substituting
1073
+ [[eq-mean]] gives"), and name it in the step if the reader needs the
1074
+ reminder there.
1075
+ - Label the chain once, if prose refers to it ([N5](#n5)). It gets one
1076
+ number. If later prose must cite a single line of it, end the chain before
1077
+ that line and give the rest its own display and label.
1078
+ - A step whose explanation needs more than a short phrase belongs in prose:
1079
+ end the chain, explain, and continue with a new chain.
1080
+ - Keep the sentence's own punctuation out of the chain ([N12](#n12)).
1081
+
1082
+ _Enforcement._ Review-only. It could be surfaced as an info-level
1083
+ diagnostic — a display with two or more top-level relation signs outside an
1084
+ `aligned` block — but no such check exists yet.
1085
+
1086
+ _Exceptions._
1087
+
1088
+ - A chain of short relations that fits comfortably on one line —
1089
+ `0 \le p \le 1`, `a = b = 0` — may stay on one line.
1090
+ - Side-by-side results that are not steps of one derivation
1091
+ (`x = \ldots, \qquad y = \ldots`) are a list, not a chain.
1092
+ - Quoting a source whose layout carries meaning, with the passage saying so.
1093
+
1094
+ ## N19
1095
+
1096
+ **A notation entry states its `units`, or declares itself `dimensionless`,
1097
+ using the units vocabulary.**
1098
+
1099
+ Every entry in `content/notation/` and every `notation.local` definition
1100
+ carries either a `units:` string built from the tokens in
1101
+ `src/reference/units-vocab.ts` (`m`, `s`, `kg`, `m/s`, `m/s^2`, `rad`, `1`,
1102
+ …) or `dimensionless: true`.
1103
+
1104
+ ```yaml
1105
+ # content/notation/motion.velocity.md
1106
+ key: motion.velocity
1107
+ label: velocity
1108
+ units: m/s
1109
+
1110
+ # content/notation/motion.launch-angle.md
1111
+ key: motion.launch-angle
1112
+ label: launch angle
1113
+ units: rad
1114
+
1115
+ # a pure count or ratio
1116
+ dimensionless: true
1117
+ ```
1118
+
1119
+ _Rationale._ The hover card, the table-header label
1120
+ (`rehype-table-header-labels.mjs`), and a worked example's dimensional check
1121
+ (N11) all read `units` from the entry. An entry without it renders a card with
1122
+ no unit, and a unit spelled outside the vocabulary (`meters/sec`) cannot be
1123
+ matched against the one in the prose.
1124
+
1125
+ _How to apply._ Add `units:` from the vocabulary, or `dimensionless: true`.
1126
+ Compose with `/` and `^`: `kg·m/s^2` is `kg*m/s^2`. If the vocabulary lacks a
1127
+ unit the course needs, add it to `units-vocab.ts` in the engine first.
1128
+
1129
+ _Enforcement._ Build-enforced (`consistency.ts` `checkNotationUnits`,
1130
+ `pnpm validate:content`) — warnings `notation-units` (neither field present)
1131
+ and `units-vocab` (a token outside the vocabulary).
1132
+
1133
+ _Exceptions._ A `lint-ignore.yml` entry (`- notation-units:<key> — <reason>`)
1134
+ for a symbol whose unit is genuinely context-dependent.
1135
+
1136
+ ## N20
1137
+
1138
+ **A notation entry's source citation carries a locator.**
1139
+
1140
+ Each item under an entry's `sources:` names *where* in the source the symbol is
1141
+ defined — a section, a page, an equation number — not just which source.
1142
+
1143
+ ```yaml
1144
+ sources:
1145
+ - id: halliday-fundamentals
1146
+ locator: "§4.3, eq. (4-12)"
1147
+ ```
1148
+
1149
+ _Rationale._ A bare source id says "this book somewhere". The hover card shows
1150
+ the locator, so a reader can open the source at the right page; a reviewer can
1151
+ check the definition against it without re-reading the chapter.
1152
+
1153
+ _How to apply._ Fill `locator:` with the smallest unit the source has:
1154
+ `"§3.2"`, `"p. 41"`, `"eq. (2.7)"`, `"Table 4"`. Never leave it empty or
1155
+ omit it.
1156
+
1157
+ _Enforcement._ Build-enforced (`consistency.ts` `checkNotationSourceLocator`,
1158
+ `pnpm validate:content`) — warning `notation-source-locator` per source item
1159
+ without a non-empty locator.
1160
+
1161
+ _Exceptions._ A source that is one page long (a datasheet, a standard's
1162
+ definition table) may cite the whole; say so in the locator (`"whole"`).
1163
+
1164
+ ## N21
1165
+
1166
+ **A gloss is named by a short noun phrase, not described by a sentence.**
1167
+
1168
+ A gloss binds a symbol on one page with a `label` shown in the hover card. The
1169
+ label *names* the symbol; it does not explain it.
1170
+
1171
+ ```yaml
1172
+ # A name.
1173
+ - key: launch-angle
1174
+ label: launch angle
1175
+
1176
+ # A description — belongs in a full notation entry's body, not a gloss label.
1177
+ - key: launch-angle
1178
+ label: the angle between the launch velocity and the horizontal, which we vary
1179
+ ```
1180
+
1181
+ _Rationale._ A gloss is the lightweight form: a name and a glyph. Once the
1182
+ label grows a clause or a sentence it is doing a notation entry's job without
1183
+ that entry's units, sources, or shared identity — and the card shows a wall of
1184
+ text where every other card shows a name.
1185
+
1186
+ _How to apply._ Keep the label to a few words with no sentence punctuation or
1187
+ relative clause. If the symbol needs a description, promote the gloss to a
1188
+ notation entry (the `gloss-wants-promoting` hint under N4 says when).
1189
+
1190
+ _Enforcement._ Build-enforced (`consistency.ts` `checkGlossNameShape`, `pnpm
1191
+ validate:content`) — warning `gloss-name-shape` for a label over the word
1192
+ limit or containing sentence punctuation / a relative clause.
1193
+
1194
+ _Exceptions._ None — a label that cannot be short is a notation entry.
1195
+
1196
+ ---
1197
+
1198
+ ## N22
1199
+
1200
+ **An accent or mark on a symbol is an operator: universal, explained, or part
1201
+ of the symbol's own entry.**
1202
+
1203
+ `\hat{\beta}` reads as "hat applied to β". β keeps its own card, and the hat is
1204
+ either universal for this course (`rules.N22.allow`; the shipped list is
1205
+ empty), explained by an operator entry (`latex: \hat{}`, meaning "estimate
1206
+ of"), or the decorated form has its own entry (`latex: \hat{\beta}`), which
1207
+ wins for that exact form. Decorations are accents (`\hat`, `\widehat`, `\bar`,
1208
+ `\tilde`, `\dot`, `\vec`, …), `\overline` / `\underline`, and the superscript
1209
+ marks `'` (any run of primes), `*` (or `\ast`) and `\dagger`. `\widehat` and
1210
+ `\hat` are different operators. `^\top`, `^{-1}`, powers, subscripts and fonts
1211
+ are not decorations; a font makes a different symbol ([N4](#n4)).
1212
+
1213
+ _Rationale._ The glyph resolver binds a letter by its own shape, so before this
1214
+ rule it walked straight through a hat: `\hat{\beta}` got β's card, and the hat
1215
+ was never explained. That is the same gap [N8](#n8) closes for named
1216
+ operators — `\operatorname{Var}(X)` gives `Var` and `X` a card each. A
1217
+ decoration means different things in different fields: `\hat` is an
1218
+ estimator, a unit vector or a Fourier transform; `\bar` a mean, a conjugate or
1219
+ a closure; `'` a derivative or "another one". So none is universal by default.
1220
+
1221
+ _How to apply._ Pick one of three, in this order of precedence:
1222
+
1223
+ 1. **Whole form** — the decorated symbol is a quantity in its own right, with
1224
+ its own formula or distribution. Give it its own entry; that exact form gets
1225
+ that card, hat included:
1226
+
1227
+ ```yaml
1228
+ key: coef-estimate
1229
+ latex: \hat{\beta}
1230
+ label: Estimated coefficient
1231
+ ```
1232
+
1233
+ 2. **Operator entry** — the decoration means the same thing wherever it
1234
+ appears on the page. Give the decoration over an empty group its own
1235
+ entry — `\hat{}`, `\overline{}`, `{}'`, `{}^{*}`, `{}^{\dagger}` — as a
1236
+ shared card introduced with `[[key]]` or a `notation.local` entry, like an
1237
+ N8 operator. The reader then hovers the hat for its card, and the letter
1238
+ under it for the letter's:
1239
+
1240
+ ```yaml
1241
+ key: stats.estimate
1242
+ latex: \hat{}
1243
+ label: Estimate
1244
+ meaning: The value of a quantity computed from a sample, as opposed to its true value.
1245
+ ```
1246
+
1247
+ 3. **Universal for the course** — a course that uses a decoration one way
1248
+ throughout lists it once in `course.settings.json`:
1249
+
1250
+ ```json
1251
+ "rules": { "N22": { "allow": ["\\dot", "\\vec"] } }
1252
+ ```
1253
+
1254
+ `rules.N22.deny` exists for symmetry with N8; the shipped list is empty.
1255
+
1256
+ A letter under a decoration still needs its own card ([N4](#n4)); the
1257
+ decoration adds a card, it never replaces one.
1258
+
1259
+ ### Suspending an operator on one page
1260
+
1261
+ Sometimes a decoration is not an operator on one page: `b'`, `b''`, `b'''` are
1262
+ successive versions of `b`, not derivatives. Say so in the lesson's
1263
+ frontmatter, with the reason:
1264
+
1265
+ ```yaml
1266
+ notation:
1267
+ suspend:
1268
+ - operator: "'"
1269
+ reason: marks successive versions of b, not derivatives
1270
+ local:
1271
+ - key: b-v1
1272
+ latex: b'
1273
+ # …
1274
+ ```
1275
+
1276
+ On that page the decoration never composes: every decorated symbol that uses
1277
+ it must be a whole form — its own entry, or an `\explain{key}{b''}` wrap around
1278
+ the whole of it — or the build fails. That catches a forgotten `b''''`, and an
1279
+ `f'(x)` that still means a derivative. An `\explain{key}{b}''` around the letter
1280
+ alone is not a whole form. Other pages keep the operator reading.
1281
+
1282
+ `operator` takes the same names as `rules.N22.allow` (`"'"`, `\hat`, `*`, …).
1283
+ A name that is not a decoration, a decoration suspended twice, and a
1284
+ `notation.local` operator entry for a decoration the same page suspends each
1285
+ fail the load. A shared operator card may still be cited on a suspending page
1286
+ with `[[key]]`, to explain the difference.
1287
+
1288
+ Suspension covers lesson-body math only — the same scope as `notation.local`.
1289
+ A notation formula, an assessment prompt and a lab keep the operator reading.
1290
+ The Page notation panel tells the reader that the decoration is not an
1291
+ operator on the page, and why.
1292
+
1293
+ | Code | Severity | When |
1294
+ | --- | --- | --- |
1295
+ | `suspended-operator-unresolved` | error | a decorated form of a suspended decoration has no whole form |
1296
+ | `operator-suspended` | info | a suspension with a `reason` — recorded, nothing to fix |
1297
+ | `operator-suspension-reason` | warning | a suspension without a `reason` |
1298
+ | `operator-shadowed` | info | a whole-form entry decorates with an operator that is explained or universal on its page, and the page does not suspend it |
1299
+
1300
+ `operator-shadowed` is a note, not a fault: "β̂ is the OLS estimate" under a
1301
+ course-wide `\hat` = "estimate of" card is a legitimate refinement. It
1302
+ suggests suspension in case the decoration never means the operator there.
1303
+
1304
+ _Enforcement._ Tier-3 warning `accent-explained` (`consistency.ts`
1305
+ `checkAccentExplained`) for a decoration that composes with no operator entry
1306
+ and is not on the allow list, one per decoration per lesson, surfaced by `pnpm
1307
+ validate:content` and silenced only by a reviewed line in `lint-ignore.yml`
1308
+ (`accent-explained:\hat — <reason>`). Suspension is build-enforced: the
1309
+ completeness gate (`gate-math.ts`) and the render path (`remark-notation.mjs`)
1310
+ both fail `suspended-operator-unresolved`; `checkOperatorSuspension` emits the
1311
+ other three codes.
1312
+
1313
+ _Exceptions._ Suspension (above) is the sanctioned way to give a decoration a
1314
+ different meaning on one page. Quoting a source that uses a decoration
1315
+ differently **and** showing what it means in the same passage.