explico 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  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 +14 -1
  15. package/dist/compiler/logging.js +6 -2
  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 +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -2,8 +2,8 @@
2
2
 
3
3
  `V` rules govern anything a lesson draws: diagrams, static charts, and hydrated
4
4
  interactive figures. The engine assumes no single plotting approach — these
5
- rules make the choice per figure predictable and keep every figure accessible
6
- and deterministic.
5
+ rules make the choice per figure predictable, keep every figure's math
6
+ gate-checked, and keep its drawing deterministic.
7
7
 
8
8
  See [README.md](README.md) for what "strong recommendation", "Build-enforced",
9
9
  and "Review-only" mean here, and how to amend a rule.
@@ -47,15 +47,21 @@ an independent diagram number, a `#dia-<id>` anchor, and references of the form
47
47
  `[[dia-<id>]]` or `[[lesson/slug#dia-<id>]]`. Diagram numbers do not advance the
48
48
  equation, table, or figure sequences.
49
49
 
50
+ A diagram is named like every float: a long `caption`, shown below the graph
51
+ in the same caption line a figure has (`Diagram 2.1.1`, a chain icon that
52
+ copies the diagram's link, the caption), and an optional `short` caption for
53
+ the `[[dia-…]]` preview card
54
+ ([components.md § The caption pair](components.md#the-caption-pair)).
55
+
50
56
  The component preserves the build-time canvas size and scrolls horizontally
51
- when the work area is narrower. A compact control on the title line can expand
52
- the original diagram toward the full in-page region and shrink it back; it is
53
- not modal, follows the diagram's document position while the page scrolls, and
54
- adds internal vertical scrolling when the viewport is too short. The shrink
55
- control or Escape restores its original width. Clicking the title minimizes/
56
- restores the diagram; this state is remembered per page by default
57
- (`defaultOpen` / `rememberState` configure it).
58
- The title also carries the same chain-link permalink treatment as headings.
57
+ when the work area is narrower. Then a widen button ↔ at the end of the
58
+ caption line — the boxed button a table's caption has — expands the original
59
+ diagram toward the full in-page region and shrinks it back; it is not modal,
60
+ follows the diagram's document position while the page scrolls, and adds
61
+ internal vertical scrolling when the viewport is too short. The button again,
62
+ or Escape, restores its original width. A diagram never folds by itself: it
63
+ is its graph and then its caption line, in every state. To let a reader fold
64
+ it away, put it in a collapsible section.
59
65
 
60
66
  By default a laid-out width above `800px` emits a compile-time warning. Treat
61
67
  that warning as a prompt to try
@@ -103,8 +109,8 @@ _Exceptions._ None — this rule _is_ the exception process.
103
109
 
104
110
  ## V3
105
111
 
106
- **Interactive figures: a hydrated island, the `renderLabMath` seam for
107
- equations, and a text alternative.**
112
+ **Interactive figures: a hydrated island and the `renderLabMath` seam for
113
+ equations.**
108
114
 
109
115
  An interactive figure is a React island, hydrated `client:visible` (or
110
116
  `client:idle`). It carries:
@@ -119,18 +125,60 @@ An interactive figure is a React island, hydrated `client:visible` (or
119
125
  in this page's popup payload. See
120
126
  `playground/content/labs/ProjectileRangeExplorer.tsx` +
121
127
  `ProjectileRangeMath.astro`.
122
- - **An accessible name and description** — `ariaLabel` / `ariaDescription` on
123
- the plot, marks that are decorative set `ariaHidden`.
124
- - **A text alternative.** A `<details>` block with the model's assumptions and a
125
- small table of representative values, so the figure is usable with no JS and
126
- by a screen reader.
127
- - **Labelled controls.** Every control is a labelled `<input>`; where precision
128
+ - **Labelled controls.** Every control is a labelled `<input>` — the visible
129
+ label is how any reader knows what a slider sets; where precision
128
130
  matters, pair a `range` slider with a `number` field (see `RangeField` in
129
131
  `ProjectileRangeExplorer.tsx`).
132
+ - **No title or caption of its own.** The island sits in a `<Figure>`, and the
133
+ figure's `caption` (with an optional `short`) names the whole lab. The lab
134
+ adds no heading, no inner `<figure>` / `<figcaption>`, and no
135
+ `aria-labelledby` pointing at one: one object has one name. A sentence
136
+ that describes the plot ("the dot marks the landing point") belongs in the
137
+ figure's caption.
138
+ - **Picture, then caption, then tools.** A lab is laid out as a LaTeX figure
139
+ with its controls after it (docs/plans/figures-and-short-captions.md,
140
+ decision 3). The plot goes in the `<Figure>`'s default slot — its
141
+ picture — and the controls in its `tools` slot: an instruction line, the
142
+ sliders and fields, the live equation. The two are separate islands that
143
+ share their values. The tools render after the caption line, in a tray of
144
+ their own, and are the only part that folds: the fold button ⌃ / ⌄ ends the
145
+ caption line, and folded tools leave a "Hidden here: 2 sliders, 1
146
+ equation" line that unfolds them. Whether they start open is the course's
147
+ `disclosure.figures`, or `defaultOpen` on the one figure; the reader's
148
+ choice is remembered per page. Print and export show the picture and the
149
+ caption; the tools never print.
150
+ - **A live end to the caption.** The caption ends with a clause stating the
151
+ case shown — "Shown for v₀ = 20 m/s and θ = 30°: range 35.3 m, time of
152
+ flight 2.04 s." The lab writes it with `setFigureState(element, runs)`
153
+ (`explico/components/figure/figure-state.ts`), `element` being any element
154
+ of its own inside the figure and `runs` plain text, `{ text, italic: true }`
155
+ or `{ sub }` — never HTML. The clause prints with the caption; a reader
156
+ of the long caption elsewhere (a preview card, a fold's bar) sees the
157
+ caption alone.
158
+ - **Which text goes where.** The caption names the figure and says what it
159
+ shows — its subject, its marks and symbols, the case shown — and may state
160
+ the one thing to notice. The argument drawn from the figure is lesson
161
+ text, after the figure. Instructions for the controls belong to the
162
+ controls: the tools' first line, not the caption.
163
+ - **A preview shows the plot.** A same-page `[[fig-…]]` reference to a lab
164
+ previews a still copy of its plot as drawn; the tools are never copied. A
165
+ plot not drawn yet — or one on another page, drawn only in the browser —
166
+ leaves the card to the caption and number, so a lab no longer needs
167
+ `previewable={false}`.
168
+
169
+ The figure itself never hides; a lab that should fold away whole, with the
170
+ prose around it, goes in a `<Fold>` or a collapsible section.
171
+
172
+ The lab writes no accessible name, description or text alternative: the
173
+ `<Figure>` renders a real `<figure>` and `<figcaption>`, so its caption is
174
+ already the figure's accessible name
175
+ ([Accessibility](README.md#accessibility)). A plot option such as
176
+ `ariaLabel` / `ariaDescription` / `ariaHidden` may stay where it is written;
177
+ nothing asks for one.
130
178
 
131
179
  _Rationale._ The seam is the only reason interactive math is not a silent
132
- exception to the completeness gate. The text alternative and aria wiring are the
133
- difference between a figure and an inaccessible one.
180
+ exception to the completeness gate. One name per object means the reader, a
181
+ `[[fig-…]]` reference and a screen reader all call the lab the same thing.
134
182
 
135
183
  _How to apply._ Keep the island's job to "patch numbers and redraw". Domain
136
184
  math lives in a plain module (`playground/content/domain/motion.ts`), not in the
@@ -226,3 +274,77 @@ _Enforcement._ Review-only.
226
274
  _Exceptions._ A figure where the meaningful range is a known physical bound
227
275
  (a probability on `[0, 1]`, an angle on `[0°, 360°]`) uses that bound instead —
228
276
  state it in a comment.
277
+
278
+ ---
279
+
280
+ ## V6
281
+
282
+ **Every quantitative axis of a chart shows at least three labelled values.**
283
+
284
+ A chart that draws an x axis and a y axis — a hand-drawn SVG, an Observable
285
+ Plot, an interactive island, a printed pgfplots figure — labels at least three
286
+ tick values on each quantitative axis, as numbers at their positions. An axis
287
+ _name_ (`feature x`, `density`) does not count, and neither does an annotation
288
+ of one special point (`\mu`, `\bar{x}`, a threshold).
289
+
290
+ _Rationale._ Values are what make a chart readable rather than decorative.
291
+ One value pins a single point. Two values give a scale only if the reader
292
+ already knows the axis is linear, and two labels on a log axis look exactly
293
+ like two on a linear one. Three values show where the axis starts, how big a
294
+ unit is, and whether the spacing is linear or logarithmic. Only then can a
295
+ reader read a value off the curve, compare two panels, or check the figure
296
+ against a number in the prose beside it.
297
+
298
+ _How to apply._
299
+
300
+ - Use round values: steps of 1, 2, 2.5, or 5 times a power of ten, inside the
301
+ axis domain. Let the library choose them where it can. Observable Plot's
302
+ automatic ticks satisfy the rule at any ordinary chart size; `ticks: 5`
303
+ pins a count. In pgfplots the automatic ticks satisfy it; never set
304
+ `xtick=\empty` or `ytick=\empty` on a quantitative axis.
305
+ - A hand-drawn SVG computes its values from the domain and falls back to a
306
+ finer step until at least three fit:
307
+
308
+ ```ts
309
+ const NICE_STEPS = [1, 2, 2.5, 5];
310
+
311
+ function axisTicks([lo, hi]: readonly [number, number], target = 5): number[] {
312
+ let exponent = Math.floor(Math.log10((hi - lo) / target));
313
+ let index = NICE_STEPS.findIndex((n) => n * 10 ** exponent >= (hi - lo) / target);
314
+ if (index < 0) [index, exponent] = [0, exponent + 1];
315
+ for (;;) {
316
+ const step = NICE_STEPS[index] * 10 ** exponent;
317
+ const values: number[] = [];
318
+ for (let v = Math.ceil(lo / step - 1e-9) * step; v <= hi + step * 1e-9; v += step) {
319
+ values.push(Number(v.toFixed(10)));
320
+ }
321
+ if (values.length >= 3) return values;
322
+ if (--index < 0) [index, exponent] = [NICE_STEPS.length - 1, exponent - 1];
323
+ }
324
+ }
325
+ ```
326
+
327
+ Draw a short tick mark at each value and the number beside it.
328
+ - The values follow the domain. When a control changes an axis range
329
+ ([V5](#v5)), recompute the values from the new range in the same step.
330
+ - A density or likelihood axis gets values too. "The vertical scale does not
331
+ matter here" is a reason to say so in the caption, not to hide the scale.
332
+ - A log axis labels its decades (`10`, `100`, `1000`).
333
+ - Special-point annotations stay, next to the values rather than instead of
334
+ them.
335
+ - Stacked panels that share an axis, aligned and said to be shared, may
336
+ label it once, on the panel nearest the axis. Every other axis of every
337
+ panel carries its own values.
338
+
339
+ _Enforcement._ Review-only. The engine cannot tell a chart from any other
340
+ drawing. A course can make it build-enforced in its own browser tests by
341
+ marking each axis's value group and counting the values in it.
342
+
343
+ _Exceptions._
344
+
345
+ - A figure without axes has nothing to label: a node-edge diagram, a
346
+ bullseye target, an illustration.
347
+ - A categorical axis shows its categories instead of numbers, even if there
348
+ are only two.
349
+ - A sketch that draws axes is a chart. If the axes are worth drawing, their
350
+ scale is worth labelling.
@@ -0,0 +1,191 @@
1
+ # Auto-scaling brackets in KaTeX (and MathJax)
2
+
3
+ > **Status in this repo.** Solution 2 is implemented, but as *code* rather than
4
+ > as a `patches/` file: [`src/reference/katex-delimiters.mjs`](../src/reference/katex-delimiters.mjs)
5
+ > applies the same one-constant change at import time, so it ships inside the
6
+ > published `explico` package and a downstream course inherits it with no
7
+ > install step. Rule [N17](authoring/notation.md#n17) depends on it. The
8
+ > sections below are the derivation; see
9
+ > [How this repo applies it](#how-this-repo-applies-it).
10
+
11
+ ## The problem
12
+
13
+ In LaTeX, `\left[ ... \right]` sizes a bracket to fit its content — but nested brackets often come out **the same size**, because the sizing rule only requires the delimiter to be *close enough* to the content's height, not strictly bigger.
14
+
15
+ Example:
16
+
17
+ ```latex
18
+ $$\left[ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 \right]$$
19
+ ```
20
+
21
+ The outer `[` and inner `[` render at the same size, even though the outer one wraps strictly more content. The alternative — manually choosing `\bigl`, `\Bigl`, `\biggl`, `\Biggl` — fixes the sizing but abandons `\left`/`\right`'s automatic "match the height of everything inside" behavior, since those are fixed-size delimiters that don't adapt to content.
22
+
23
+ So there are two properties you want simultaneously, and the two standard tools each give you only one:
24
+
25
+ 1. **Strict growth outward** — each nesting level's bracket must be visibly bigger than the one inside it.
26
+ 2. **Full coverage** — every bracket must span the full height of its content, with no gap.
27
+
28
+ `\bigl`/`\Bigl`/`\biggl`/`\Biggl` give you (1) but not (2) (fixed sizes don't track arbitrary content). `\left`/`\right` give you (2) but not (1) (the sizing rule allows equal-looking sizes at different nesting depths).
29
+
30
+ ## Why it happens: the underlying rule
31
+
32
+ TeX sizes a `\left`/`\right` pair using this rule (TeXbook, Appendix G, rule 19):
33
+
34
+ ```
35
+ s_min = max( 2·δ₀ · f / 1000 , 2·δ₀ − l )
36
+ ```
37
+
38
+ TeX then picks the smallest available delimiter glyph whose total size `s` satisfies `s ≥ s_min`.
39
+
40
+ | Symbol | Meaning |
41
+ |---|---|
42
+ | `h` | height of the content: how far it reaches above the baseline |
43
+ | `d` | depth of the content: how far it reaches below the baseline |
44
+ | `a` | axis height — the line fraction bars and minus signs sit on; delimiters are centered on this line |
45
+ | `δ₀` | `max(h − a, d + a)` — how far the content reaches from the axis, on whichever side is farther |
46
+ | `f` | `\delimiterfactor`, default **901** |
47
+ | `l` | `\delimitershortfall`, default **5pt** |
48
+ | `s_min` | the smallest total bracket size TeX will accept |
49
+ | `s` | the size of the delimiter TeX actually picks |
50
+
51
+ With the default `l = 5pt`, `s_min` can be *smaller* than the content's own height (since `901/1000 ≈ 0.9`), so a delimiter that already matches the inner bracket's height passes the test for the outer bracket too — no forced growth. The same slack also means a delimiter can legally fall up to 5pt short of the content, i.e. requirement (2) isn't fully guaranteed by default either.
52
+
53
+ **The fix in real LaTeX:** set `\delimitershortfall` to a *negative* value (e.g. `-1pt`). Then `s_min = 2δ₀ + 1pt`, which is always larger than the content's own extent — forcing outward growth at every level, automatically, with no per-formula work. This is a document-wide setting, not something you write per equation:
54
+
55
+ ```latex
56
+ \delimitershortfall=-1pt
57
+ \left[ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 \right]
58
+ ```
59
+
60
+ Verified by rendering with `pdflatex`: default nesting produces equal-sized brackets; the same source with `\delimitershortfall=-1pt` produces strictly increasing sizes at every depth, and small content (`(x)`, `((x))`, `(((x)))`) is unaffected since the correction is tiny relative to normal delimiter jumps.
61
+
62
+ ## Why it doesn't carry over to the web
63
+
64
+ `\delimitershortfall` is a TeX engine primitive. **Neither KaTeX nor MathJax expose it or honor it** — I tested both directly:
65
+
66
+ - KaTeX: `\delimitershortfall=-1pt` throws `ParseError: Expected group after '\delimitershortfall'` (undefined control sequence).
67
+ - MathJax v3: the command is silently dropped/renders as an error span; no effect on sizing.
68
+
69
+ So the web renderers need a different fix.
70
+
71
+ ## Solution 1: a `\grow` macro (portable, works in *any* KaTeX/MathJax page)
72
+
73
+ Since neither engine's *rule* can be changed from user-level TeX, the workaround changes the **content** being measured instead. Wrap everything inside a bracket pair (except the innermost level) in an invisible padding macro that makes the content measure as slightly taller and deeper than it visually is. Then the normal `\left`/`\right` sizing — which *does* work correctly in both engines — is forced to pick a bigger glyph at each level, because the content it measures really is bigger.
74
+
75
+ **Mechanism:** for content `#1`, stack a raised zero-width phantom copy 0.5em above it and a lowered zero-width phantom copy 0.5em below it. `\vphantom{#1}` takes zero horizontal space and is invisible, but claims the same height/depth as `#1`. Raising and lowering copies of it extends the *measured* bounding box by 0.5em on each side — 1em of forced growth per wrap — without changing what's visually drawn.
76
+
77
+ **KaTeX** (verified — `\raisebox` only accepts a text-mode argument, so the phantom must be re-entered as math and forced to `\displaystyle`; without that it silently shrinks to inline size and the trick does nothing):
78
+
79
+ ```latex
80
+ \newcommand{\grow}[1]{#1\raisebox{0.5em}{$\displaystyle\vphantom{#1}$}\raisebox{-0.5em}{$\displaystyle\vphantom{#1}$}}
81
+ ```
82
+
83
+ **MathJax** (verified — `\raisebox` doesn't exist here; plain-TeX `\raise`/`\lower` take a math argument directly and preserve the current style):
84
+
85
+ ```latex
86
+ \newcommand{\grow}[1]{#1\raise{0.5em}{\vphantom{#1}}\lower{0.5em}{\vphantom{#1}}}
87
+ ```
88
+
89
+ **Usage** — wrap the content of every bracket pair except the innermost:
90
+
91
+ ```latex
92
+ \left[ \grow{ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 } \right]
93
+ ```
94
+
95
+ Verified by rendering with real KaTeX 0.18.7 and MathJax 3.2.2 (headless Chromium screenshots): both show strictly increasing bracket sizes across 2–3 nesting levels, matching the pdfLaTeX reference.
96
+
97
+ **Trade-offs:**
98
+ - Manual — you place `\grow{}` yourself at each level; it isn't automatic like the LaTeX-engine setting.
99
+ - The two spellings aren't interchangeable: `\raisebox` errors out in MathJax, `\raise`/`\lower` don't exist in KaTeX. Pick the one matching your renderer.
100
+ - Can be defined once (in MathJax's `tex.macros` config, or KaTeX's `macros` render option) so you only write `\grow{...}` per formula, not the full definition each time.
101
+
102
+ ## Solution 2: patch KaTeX directly (better fit for a site you build/host yourself)
103
+
104
+ Since you control the build (an Astro site rendering KaTeX server-side), you don't need the macro workaround at all — you can restore the actual `\delimitershortfall` behavior by editing the one place KaTeX hard-codes it.
105
+
106
+ **The code.** KaTeX's delimiter-sizing function (`makeLeftRightDelim`, in the compiled bundle since no per-file TS source ships in the npm package) contains:
107
+
108
+ ```js
109
+ const delimiterFactor = 901; // TeX's \delimiterfactor
110
+ const delimiterExtend = 5.0 / options.fontMetrics().ptPerEm; // TeX's \delimitershortfall = 5pt
111
+ ```
112
+
113
+ This is the exact same formula from the TeXbook rule above, with the same two constants. Changing `5.0` to `-1.0` is precisely `\delimitershortfall=-1pt` — `delimiterFactor` doesn't need to change, since a negative shortfall always makes the second term of the `max(...)` dominate anyway.
114
+
115
+ **Verified:** I applied this one-line edit to the installed `katex` package and rendered plain, unmodified `\left[...\right]` source (no `\grow` macro anywhere) through the patched build. Result: strict growth across 4 nesting levels, small content (`(x)`, `((x))`, `(((x)))`) unaffected, and — critically — the LaTeX source embedded in KaTeX's output for copy/paste (`<annotation encoding="application/x-tex">`) stays exactly what you typed, since only the *sizing*, not the *content*, is touched.
116
+
117
+ **Where the line lives** (npm package, KaTeX 0.16.47):
118
+
119
+ | File | Used by | Patched here |
120
+ |---|---|---|
121
+ | `src/delimiter.ts` | the readable source the bundles are built from | yes |
122
+ | `dist/katex.mjs` | ESM import — what an Astro/Vite build resolves to | yes |
123
+ | `dist/katex.js` | CJS `require` | yes |
124
+ | `dist/katex.min.js` | only if KaTeX is also loaded client-side via `<script>` | **no** — see below |
125
+
126
+ `dist/katex.min.js` is left alone on purpose. explico renders every formula at
127
+ build time and ships only `katex.min.css` plus fonts to the browser, so nothing
128
+ here loads that bundle — and because it is a single ~270 KB line, including it
129
+ turns a 2 KB patch into a 550 KB one that no reviewer can read and no KaTeX
130
+ upgrade can rebase. A course that *does* render KaTeX in the browser has to
131
+ patch it too; the minified form of the same line is
132
+ `l=5/n.fontMetrics().ptPerEm` inside `makeLeftRightDelim`.
133
+
134
+ ## How this repo applies it
135
+
136
+ A `pnpm patch` would work, and is what the section above describes, but it is
137
+ **project-local**: pnpm records it in the consuming repo's
138
+ `pnpm-workspace.yaml`, so it cannot travel inside the published `explico`
139
+ package. Every downstream course would have to reapply it, and a course that
140
+ forgot would render flat nests while its linter told authors the engine handled
141
+ sizing. Documenting that in a migration guide only narrows the window for the
142
+ mistake; it does not close it.
143
+
144
+ So the engine does the same edit in code, at import time, in
145
+ [`src/reference/katex-delimiters.mjs`](../src/reference/katex-delimiters.mjs):
146
+
147
+ 1. read KaTeX's own ESM bundle off disk (`katex/dist/katex.mjs`);
148
+ 2. rewrite that one constant, throwing if it is not found verbatim;
149
+ 3. `import()` the rewritten source as a `data:` URL — the bundle has no imports
150
+ of its own, so it needs no base to resolve against;
151
+ 4. move `render`, `renderToString`, `__renderToDomTree` and
152
+ `__renderToHTMLTree` onto the shared `katex` object every consumer in the
153
+ process already imported;
154
+ 5. render a known nest and throw unless the sizes actually grew.
155
+
156
+ Step 4 is what makes it reach lesson-body math. `rehype-katex` does its own
157
+ `import katex from 'katex'`, but it calls `katex.renderToString(...)` — a
158
+ property read at call time — and KaTeX's default export is a plain mutable
159
+ object. Replacing the function on that object is therefore enough; the plugin
160
+ does not have to be forked or aliased.
161
+
162
+ **A course inherits this with no changes.** `astro.config.mjs` already imports
163
+ `createNotationKatexOptions` from `explico/reference/katex-options.mjs`, and
164
+ that module imports the installer for its side effect, so the sizing is in
165
+ place before Astro renders anything. The engine's own non-Astro paths
166
+ (`math-glyphs.mjs`, `render-lab-math.ts`, and therefore `validate:content`)
167
+ import it directly.
168
+
169
+ **It cannot fail quietly.** Steps 2 and 5 throw. A KaTeX upgrade that moves the
170
+ constant, or anything that renders before the installer runs, stops the build
171
+ with an explicit message instead of silently reverting to stock spacing — which
172
+ is the failure mode a patch file plus a migration note could not rule out.
173
+
174
+ **Trade-offs.** It is a monkey-patch of a third-party module object, so it
175
+ rests on three upstream properties: the constant is a literal in the shipped
176
+ bundle, the bundle is self-contained, and the default export is mutable. All
177
+ three are asserted at install time rather than assumed. It also holds a second
178
+ copy of the KaTeX module in memory (~600 KB of source, ~50 ms to load) for the
179
+ duration of a build, which is not worth optimising away.
180
+
181
+ ## Why the patch is the better fit here
182
+
183
+ Your original idea — keep writing `\left[` everywhere, run a build-time preprocessing step that inserts `\grow{}` at each nesting level, then swap in the original source for a "copy" button — would work, but has two costs the patch avoids:
184
+
185
+ 1. **The preprocessor needs a real TeX parser**, not a regex. It has to track bracket nesting depth correctly across `\text{...}`, comments, `\right.` (null right delimiters), and — critically — macros. If you ever define something like `\norm{...}` that *expands* to `\left\lVert...\right\rVert`, a source-level preprocessor can't see the nesting hidden inside the macro and will miscount levels. The patch operates after macro expansion, inside KaTeX's own layout engine, so it can't miss anything.
186
+ 2. **The copy-button problem disappears.** KaTeX already embeds your literal input in the hidden MathML `<annotation>` tag — that's what the standard `copy-tex` extension reads. With preprocessing, that annotation would contain the `\grow`-polluted string, so you'd have to intercept the copy handler and substitute the original text yourself. With the patch, the *displayed* math and the *embedded source* never diverge in the first place, because nothing was ever inserted into the source.
187
+
188
+ ## Caveats
189
+
190
+ - If your pipeline actually renders to **SVG** rather than HTML+CSS, you're using MathJax's `tex-svg` output, not KaTeX (KaTeX renders to HTML spans, not SVG). MathJax's stretchy-delimiter logic lives in the output stage rather than one constants block, so the equivalent patch would be far more invasive there — the `\grow` macro is the practical option for MathJax.
191
+ - On an Astro build, KaTeX runs at build time (via `remark-math`/`rehype-katex` or similar) and only sends finished HTML + `katex.css` + fonts to the browser — the patched JS never ships to visitors, it only needs to be present in your build environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explico",
3
- "version": "0.7.0",
3
+ "version": "0.11.0",
4
4
  "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -30,6 +30,7 @@
30
30
  "README.md",
31
31
  "CHANGELOG.md",
32
32
  "docs/VERSIONING.md",
33
+ "docs/katex-bracket-scaling.md",
33
34
  "docs/authoring"
34
35
  ],
35
36
  "dependencies": {
@@ -63,7 +64,7 @@
63
64
  "vitest": "4.1.11"
64
65
  },
65
66
  "scripts": {
66
- "build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json",
67
+ "build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json && node scripts/gen-rules-schema.mjs",
67
68
  "check:release-migration": "node scripts/check-release-migration.mjs",
68
69
  "test": "vitest run",
69
70
  "test:watch": "vitest"