explico 0.11.1 → 0.13.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 (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +15 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +3 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -168,6 +168,7 @@ const GATE_MATH: Emitter = { module: 'src/reference/gate-math.ts', fn: 'resolveI
168
168
  const GLYPHS: Emitter = { module: 'src/reference/math-glyphs.mjs', fn: 'resolveMathGlyphs' };
169
169
  const REGISTRY: Emitter = { module: 'src/reference/registry.ts', fn: 'buildNotationRegistry' };
170
170
  const KEYED_FLOATS: Emitter = { module: 'src/reference/keyed-float-validate.ts', fn: 'validateKeyedFloats' };
171
+ const INDICES: Emitter = { module: 'src/reference/gate-indices.ts', fn: 'gateIndices' };
171
172
 
172
173
  function consistency(fn: string): Emitter {
173
174
  return { module: 'src/reference/consistency.ts', fn };
@@ -203,6 +204,15 @@ function keyedFloatChecks(): RuleCheck[] {
203
204
  check(`${prefix}-duplicate-key`, 'error', false, emitter),
204
205
  check(`${prefix}-ref-resolves`, 'error', false, emitter),
205
206
  );
207
+ // A display block is one numbered equation: a second `\label` in it
208
+ // would name nothing.
209
+ if (prefix === 'eq') {
210
+ out.push(
211
+ check('eq-label-repeated', 'error', false, emitter),
212
+ // LaTeX's `\eqref{eq:key}` in prose, which MDX cannot parse.
213
+ check('eq-latex-ref', 'error', false, emitter),
214
+ );
215
+ }
206
216
  if (prefix === 'tab' || prefix === 'fig' || prefix === 'dia' || prefix === 'ex') {
207
217
  out.push(check(`${prefix}-key-unused`, 'info', false, emitter));
208
218
  // A prop that is gone: a name prop the caption pair (`caption` +
@@ -212,6 +222,20 @@ function keyedFloatChecks(): RuleCheck[] {
212
222
  // JSX float; a table never had one to remove.
213
223
  out.push(check(`${prefix}-removed-prop`, 'error', false, emitter));
214
224
  }
225
+ // A `short={…}` / `caption={…}` the manifest cannot read: a reference's
226
+ // card, a copied link and the float index name the float without it.
227
+ if (prefix !== 'eq' && prefix !== 'check') {
228
+ out.push(check(`${prefix}-unread-prop`, 'warning', false, emitter));
229
+ }
230
+ // A `<Table>` holding a second table, which every export would drop.
231
+ if (prefix === 'tab') {
232
+ out.push(
233
+ check('tab-several-tables', 'error', false, {
234
+ module: 'src/reference/tables-validate.ts',
235
+ fn: 'validateTables',
236
+ }),
237
+ );
238
+ }
215
239
  }
216
240
  // A worked-example set cannot do without its `id` (its anchor and
217
241
  // `[[ex-…]]`) or its `short` title (its heading after the "Example" badge).
@@ -221,13 +245,18 @@ function keyedFloatChecks(): RuleCheck[] {
221
245
  fn: 'validateExamples',
222
246
  }),
223
247
  );
224
- // A section's key is its heading's anchor, which the slugger makes unique,
225
- // so only the reference can go wrong.
248
+ // A section's key is its heading's anchor, which the slugger makes unique
249
+ // among headings; the reference can go wrong, and so can an anchor the
250
+ // engine already gives a float, a memo or the References section.
226
251
  out.push(
227
252
  check('sec-ref-resolves', 'error', true, {
228
253
  module: 'src/reference/sections-validate.ts',
229
254
  fn: 'validateSectionRefs',
230
255
  }),
256
+ check('heading-anchor-collision', 'error', true, {
257
+ module: 'src/reference/sections-validate.ts',
258
+ fn: 'validateHeadingAnchors',
259
+ }),
231
260
  );
232
261
  // A `<Fold>` is named by its `label`, or by the numbered floats it holds;
233
262
  // one with neither has an empty bar.
@@ -356,7 +385,7 @@ const RULES: readonly RuleEntry[] = [
356
385
  elements: ['prose', 'assessment', 'table'],
357
386
  doc: 'notation.md#n9',
358
387
  summary:
359
- 'Never write `45 m/s`, `t (s)`, `vx` or `KE/m` as plain text, and never spell a stated quantity\'s unit out: "4 seconds", "2 meters per second squared", "30 degrees", "$x = 3$ meters" are `$4\\ \\text{s}$`, `$2\\ \\text{m/s}^2$`, `$30^{\\circ}$`, `$x = 3\\ \\text{m}$`. A unit inside math is `\\text{}`-wrapped, short form, in the same span as its number or symbol. A unit named on its own ("measured in meters") stays a word; a sentence that defines a unit is waived. JSX prop strings (`caption="…"`) and assessment choice labels render no math, so N9 does not apply there.',
388
+ 'Never write `45 m/s`, `t (s)`, `vx` or `KE/m` as plain text, and never spell a stated quantity\'s unit out: "4 seconds", "2 meters per second squared", "30 degrees", "$x = 3$ meters" are `$4\\ \\text{s}$`, `$2\\ \\text{m/s}^2$`, `$30^{\\circ}$`, `$x = 3\\ \\text{m}$`. A unit inside math is `\\text{}`-wrapped, short form, in the same span as its number or symbol. A unit named on its own ("measured in meters") stays a word; a sentence that defines a unit is waived. JSX prop strings (`caption="…"`) render no math, so N9 does not apply there; nor does it apply to an assessment choice label, which may still hold math (`$30^{\\circ}$`).',
360
389
  checks: [
361
390
  check(
362
391
  'prose-notation',
@@ -530,7 +559,7 @@ const RULES: readonly RuleEntry[] = [
530
559
  elements: ['notation'],
531
560
  doc: 'notation.md#n19',
532
561
  summary:
533
- 'Every notation entry has `units:` from `units-vocab.ts` (`m/s`, `s`, `kg`, …) or `dimensionless: true`. A token outside the vocabulary is a warning.',
562
+ 'Every notation entry has `units:` from `units-vocab.ts` (`m/s`, `s`, `kg`, …) or `dimensionless: true`, except an index entry (`latex: "{}_0"`, N23), which takes neither. A token outside the vocabulary is a warning.',
534
563
  checks: [
535
564
  check('notation-units', 'warning', false, consistency('checkNotationUnits')),
536
565
  check('units-vocab', 'warning', false, consistency('checkNotationUnits')),
@@ -557,7 +586,7 @@ const RULES: readonly RuleEntry[] = [
557
586
  elements: ['notation'],
558
587
  doc: 'notation.md#n21',
559
588
  summary:
560
- 'A gloss `label` names the symbol in a few words (“launch angle”); a clause, sentence punctuation, or a definition belongs in a full notation entry instead.',
589
+ 'A gloss `name` names the symbol in a few words (“launch angle”); a clause, sentence punctuation, or a definition belongs in a full notation entry instead.',
561
590
  checks: [check('gloss-name-shape', 'warning', false, consistency('checkGlossNameShape'))],
562
591
  },
563
592
  {
@@ -572,6 +601,7 @@ const RULES: readonly RuleEntry[] = [
572
601
  'A decoration (`\\hat`, `\\bar`, `\\overline`, `\'`, `^*`, `^\\dagger`, …) is on the course’s `allow` list (shipped empty), explained by an operator entry (`latex: \\hat{}`), or the decorated form has its own entry (`latex: \\hat{\\beta}`), which wins for that form. A page may suspend a decoration in `notation.suspend`, with a `reason`; every decorated form of it there then needs its own entry.',
573
602
  checks: [
574
603
  check('accent-explained', 'warning', false, consistency('checkAccentExplained')),
604
+ check('accent-swallowed', 'warning', false, consistency('checkAccentExplained')),
575
605
  check('suspended-operator-unresolved', 'error', true, GATE_MATH),
576
606
  check('operator-suspension-reason', 'warning', false, consistency('checkOperatorSuspension')),
577
607
  check('operator-suspended', 'info', false, consistency('checkOperatorSuspension')),
@@ -579,6 +609,25 @@ const RULES: readonly RuleEntry[] = [
579
609
  ],
580
610
  settings: baselineEdits('Decorations'),
581
611
  },
612
+ {
613
+ kind: 'rule',
614
+ id: 'N23',
615
+ short: 'An index is part of the symbol: whole form, family or index entry',
616
+ title:
617
+ 'An index is part of the symbol: a subscript is explained by the indexed symbol’s entry, a family entry, or an index entry.',
618
+ elements: ['math', 'notation'],
619
+ doc: 'notation.md#n23',
620
+ summary:
621
+ 'A subscript on a symbol is explained by the indexed symbol’s own entry (`latex: v_0`), a family entry whose index runs (`latex: x_k` explains `x_1`), or an index entry for the subscript (`latex: "{}_0"`, which composes a derived card); a card on the base alone does not explain it, and arithmetic on an index letter with its own card does (`X_{t-1}`). A running index — a lowercase letter not marked `fixedIndex` — states its set, as `range` on its card or after a `\\quad` on the first display line that uses it. Never wrap an indexed symbol in `\\explain` with a key whose `latex` has no index.',
622
+ checks: [
623
+ check('index-explained', 'error', true, INDICES),
624
+ check('index-swallowed', 'error', true, INDICES),
625
+ check('index-range', 'error', true, INDICES),
626
+ check('derived-key-taken', 'error', true, INDICES),
627
+ ],
628
+ unchecked:
629
+ 'whether a `\\quad` clause states the set correctly; whether a lowercase index is really running; lab math whose glyph table is a copy of the one `labMathScope` returned.',
630
+ },
582
631
 
583
632
  // ---- V — visualizations -----------------------------------------------------
584
633
  {
@@ -613,8 +662,19 @@ const RULES: readonly RuleEntry[] = [
613
662
  elements: ['plot'],
614
663
  doc: 'visualizations.md#v3',
615
664
  summary:
616
- 'An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath`, and labels its controls; the figure’s caption is its name, so the lab writes no accessible name or text alternative of its own.',
617
- checks: [],
665
+ 'An interactive figure is an island under `content/labs/` inside a `<Figure>`, renders its equations through `renderLabMath` (`{ display: false }` for a legend, an axis title or a label; a `{ math }` run for the caption clause) and never KaTeX directly, and labels its controls; the figure’s caption is its name, so the lab writes no accessible name or text alternative of its own. Its default picture is drawn at build time too and handed to the island as children, so the built page has it.',
666
+ checks: [
667
+ check('lab-katex-direct', 'error', false, {
668
+ module: 'src/reference/lab-katex-validate.ts',
669
+ fn: 'validateLabKatexImports',
670
+ }),
671
+ check('lab-picture-undrawn', 'warning', false, {
672
+ module: 'src/reference/figure-pictures.ts',
673
+ fn: 'undrawnFigurePictures',
674
+ }),
675
+ ],
676
+ unchecked:
677
+ 'that a lab labels its controls and writes no name of its own, and math a lab draws without importing `katex` — the build’s rendered check (`figure-symbols.ts`) covers the page it renders.',
618
678
  },
619
679
  {
620
680
  kind: 'rule',
@@ -666,6 +726,57 @@ const RULES: readonly RuleEntry[] = [
666
726
  },
667
727
  ],
668
728
  },
729
+ {
730
+ kind: 'rule',
731
+ id: 'V7',
732
+ short: 'A figure’s picture fits on one screen',
733
+ title: 'A figure’s picture takes at most `maxPercent` of the screen height.',
734
+ elements: ['plot'],
735
+ doc: 'visualizations.md#v7',
736
+ summary:
737
+ 'The default slot of a `<Figure>` — plot, key and tables — stays within `maxPercent` of the viewport height, so the reader sees the picture and its caption line together. A square or tall plot gets a fixed width instead of the full column.',
738
+ checks: [
739
+ check('figure-too-tall', 'warning', false, {
740
+ module: 'src/reference/figure-fit.ts',
741
+ fn: 'checkFigureFit',
742
+ }),
743
+ ],
744
+ unchecked:
745
+ 'any page or viewport the course’s Playwright suite does not open — the check runs in the browser, not in the build.',
746
+ settings: [
747
+ {
748
+ key: 'maxPercent',
749
+ title: 'Largest picture height (% of screen)',
750
+ description: 'How much of the viewport height a figure’s picture may take.',
751
+ schema: { type: 'integer', minimum: 10, default: 80 },
752
+ },
753
+ ],
754
+ },
755
+ {
756
+ kind: 'rule',
757
+ id: 'V8',
758
+ short: 'A diagram fits the lesson column',
759
+ title: 'A diagram is no wider than the lesson column.',
760
+ elements: ['diagram'],
761
+ doc: 'visualizations.md#v8',
762
+ summary:
763
+ 'A `<Diagram>` lays out no wider than `columnWidth`, the lesson column at a 1366px-wide window, so the reader sees the whole graph without scrolling it. Narrow it with `direction="TB"`, a shorter label or a node `width` (its label wraps inside it); `warnAboveWidth={false}` marks a diagram meant to scroll.',
764
+ checks: [
765
+ check('dia-too-wide', 'warning', false, {
766
+ module: 'src/reference/diagram-fit.ts',
767
+ fn: 'oversizedDiagrams',
768
+ }),
769
+ ],
770
+ settings: [
771
+ {
772
+ key: 'columnWidth',
773
+ title: 'Column width (px)',
774
+ description:
775
+ 'The widest a diagram may lay out: the lesson column at the window width the course is read at most (707px at 1366px).',
776
+ schema: { type: 'integer', minimum: 200, default: 707 },
777
+ },
778
+ ],
779
+ },
669
780
 
670
781
  // ---- C — versioning ---------------------------------------------------------
671
782
  {
@@ -799,7 +910,7 @@ const RULES: readonly RuleEntry[] = [
799
910
  elements: ['table'],
800
911
  doc: 'structure-rules.md#s3',
801
912
  summary:
802
- 'Wrap every pipe table or `<table>` in `<Table id="…" caption="…">` so it gets a number and `[[tab-…]]` can reach it.',
913
+ 'Wrap every pipe table or `<table>` in `<Table id="…" caption="…">` so it gets a number and `[[tab-…]]` can reach it; a table in a worked example stays plain.',
803
914
  checks: [
804
915
  check('raw-table-markdown', 'warning', false, {
805
916
  module: 'src/reference/raw-floats-validate.ts',
@@ -831,24 +942,6 @@ const RULES: readonly RuleEntry[] = [
831
942
  }),
832
943
  ],
833
944
  },
834
- {
835
- kind: 'rule',
836
- id: 'S5',
837
- short: 'A sign / perspective convention is declared once and cited elsewhere',
838
- title: 'A sign or perspective convention is declared once and cited everywhere else.',
839
- elements: ['prose'],
840
- doc: 'structure-rules.md#s5',
841
- summary:
842
- 'Prose that states a sign / cash-flow-perspective convention resolves it to the single declaration — the `conventions.signConventionKey` notation entry (`[[key]]`) or the `conventions.cashFlowPerspectiveCompetency` competency — rather than restating it.',
843
- checks: [
844
- check(
845
- 'convention-single-definition',
846
- 'warning',
847
- false,
848
- consistency('checkConventionSingleDefinition'),
849
- ),
850
- ],
851
- },
852
945
  {
853
946
  kind: 'rule',
854
947
  id: 'S6',
@@ -864,6 +957,12 @@ const RULES: readonly RuleEntry[] = [
864
957
  module: 'src/reference/gate-heading-structure.ts',
865
958
  fn: 'gateHeadingStructure',
866
959
  }),
960
+ // A knowledge check or a worked-example set, which draws an `<h2>`,
961
+ // inside a memo or a short note.
962
+ check('heading-nested', 'error', true, {
963
+ module: 'src/reference/gate-heading-structure.ts',
964
+ fn: 'gateHeadingStructure',
965
+ }),
867
966
  ],
868
967
  },
869
968
  {
@@ -906,6 +1005,33 @@ const RULES: readonly RuleEntry[] = [
906
1005
  'Give each figure, table and diagram a `short` caption that names it: it opens the caption line in bold, and names the float in "Hidden here", a copied link and the "Figures and tables" page. The long `caption` goes on from that name to say what the float shows — its subject, its marks and symbols, the case shown — and at most the one thing to notice. The argument drawn from a figure is lesson text after it, and a lab’s instructions are the first line of its `tools`.',
907
1006
  checks: [],
908
1007
  },
1008
+ {
1009
+ kind: 'rule',
1010
+ id: 'S10',
1011
+ short: 'A page that is not a chapter explains its symbols but numbers nothing: it links a chapter’s float instead',
1012
+ title:
1013
+ 'A page that is not a chapter — the landing page, a part’s overview page — explains its symbols like a chapter, but carries no numbered float: it links the chapter’s float instead.',
1014
+ elements: ['part-page', 'plot', 'table', 'diagram', 'equation', 'notation'],
1015
+ doc: 'structure-rules.md#s10',
1016
+ summary:
1017
+ 'On `content/docs/index.mdx`, a `content/docs/<part>/index.mdx`, or any page outside a part: `[[key]]`, `\\explain`, math, citations and `[[chapter#tab-key]]` links work and open cards, and the math and prose gates apply. No `<Table>`, `<Figure>`, `<Diagram>`, `<CompactExamples>`, `<AssessmentSet>`, lab, `\\label{eq:…}` or `notation.local`/`notation.suspend`; no `$…$` or `[[…]]` in `hero.title`, `hero.tagline` or a `<PartOverview chain>` step.',
1018
+ checks: [
1019
+ check('non-chapter-float', 'error', true, {
1020
+ module: 'src/reference/gate-non-chapter-pages.ts',
1021
+ fn: 'gateNonChapterPages',
1022
+ }),
1023
+ check('non-chapter-local-notation', 'error', false, {
1024
+ module: 'src/reference/gate-non-chapter-pages.ts',
1025
+ fn: 'gateNonChapterPages',
1026
+ }),
1027
+ check('non-chapter-plain-text', 'error', false, {
1028
+ module: 'src/reference/gate-non-chapter-pages.ts',
1029
+ fn: 'gateNonChapterPages',
1030
+ }),
1031
+ ],
1032
+ locked:
1033
+ 'a float needs a chapter number, and such a page has none — turning the rule off would not render one.',
1034
+ },
909
1035
 
910
1036
  // ---- P — prose --------------------------------------------------------------
911
1037
  {
@@ -1077,6 +1203,13 @@ const RETIRED: readonly RetiredRule[] = [
1077
1203
  into: 'C3',
1078
1204
  why: 'The release procedure and the invariant it keeps are one rule, and `knowledge-changelog-missing` (owned by C3) was already the check that fired on a half-finished release.',
1079
1205
  },
1206
+ {
1207
+ id: 'S5',
1208
+ title: 'A sign or perspective convention is declared once and cited everywhere else.',
1209
+ date: '2026-10-07',
1210
+ into: 'N4',
1211
+ why: 'Its check knew one finance course’s phrasing and one key per course, and never fired in any course; the `conventions` setting it read is gone. A convention is a definition, so it is now N4’s: defined once on its notation entry, whose `meaning` names the other choice and whose body says how to convert.',
1212
+ },
1080
1213
  ];
1081
1214
 
1082
1215
  const INVARIANTS: readonly InvariantEntry[] = [
@@ -1087,7 +1220,7 @@ const INVARIANTS: readonly InvariantEntry[] = [
1087
1220
  elements: ['notation', 'math'],
1088
1221
  doc: 'components.md#reference-systems',
1089
1222
  summary:
1090
- 'A `[[key]]` or `\\explain{key}{…}` names an existing entry; a key is kebab-case; a shared entry is defined once; formula references never form a cycle.',
1223
+ 'A `[[key]]` or `\\explain{key}{…}` names an existing entry; a key is kebab-case; a shared entry is defined once; references in meanings and bodies never form a cycle (formulas may name each other).',
1091
1224
  checks: [
1092
1225
  check('undefined-reference', 'error', false, REGISTRY),
1093
1226
  check('invalid-key', 'error', false, REGISTRY, GLYPHS),
@@ -1103,6 +1236,10 @@ const INVARIANTS: readonly InvariantEntry[] = [
1103
1236
  }),
1104
1237
  check('malformed-explicit-binding', 'error', true, GLYPHS),
1105
1238
  check('unused-definition', 'info', false, REGISTRY),
1239
+ // A page-local entry under a shared key: two meanings for one key.
1240
+ check('local-shadows-shared', 'warning', false, REGISTRY),
1241
+ // A `[[` that never closes, before the render fails on it.
1242
+ check('malformed-reference', 'error', false, REGISTRY),
1106
1243
  ],
1107
1244
  },
1108
1245
  {
@@ -1123,6 +1260,11 @@ const INVARIANTS: readonly InvariantEntry[] = [
1123
1260
  check('ambiguous-base-fallback', 'error', true, GLYPHS),
1124
1261
  check('ambiguous-canonical-notation', 'error', false, GLYPHS),
1125
1262
  check('math-compile-error', 'error', true, GATE_MATH),
1263
+ // `$5 to $6`: money remark-math reads as one math span.
1264
+ check('currency-dollar', 'error', true, {
1265
+ module: 'src/reference/gate-math.ts',
1266
+ fn: 'gateContentMath',
1267
+ }),
1126
1268
  ],
1127
1269
  },
1128
1270
  {
@@ -1142,7 +1284,7 @@ const INVARIANTS: readonly InvariantEntry[] = [
1142
1284
  elements: ['changelog'],
1143
1285
  doc: 'README.md#amending',
1144
1286
  summary:
1145
- 'A `rules.<ID>` block names a live rule; a deprecated top-level key (`operators`, `romanLabels`, `abbreviations`) still works but warns until it is moved under `rules`.',
1287
+ 'A `rules.<ID>` block names a live rule; a deprecated top-level key (`operators`, `romanLabels`, `abbreviations`) still works but warns until it is moved under `rules`; `conventions` is ignored and warns until it is deleted.',
1146
1288
  checks: [
1147
1289
  check('config-deprecated-key', 'warning', false, {
1148
1290
  module: 'src/rules/settings.ts',
@@ -1189,8 +1331,10 @@ const INVARIANTS: readonly InvariantEntry[] = [
1189
1331
  'track-order',
1190
1332
  'assessment-coverage',
1191
1333
  'invalid-assessment',
1192
- 'review-state',
1334
+ 'missing-assumptions',
1193
1335
  'teaches-support',
1336
+ // `[text [@id]](url)`: a citation the page prints as typed.
1337
+ 'citation-in-link',
1194
1338
  ] as const
1195
1339
  ).map((code) =>
1196
1340
  check(code, 'error', false, {
@@ -1204,7 +1348,6 @@ const INVARIANTS: readonly InvariantEntry[] = [
1204
1348
  'alignment-unknown-lesson',
1205
1349
  'alignment-introduction',
1206
1350
  'alignment-unavailable',
1207
- 'alignment-review-state',
1208
1351
  ] as const
1209
1352
  ).map((code) =>
1210
1353
  check(code, 'error', false, {
@@ -1212,7 +1355,74 @@ const INVARIANTS: readonly InvariantEntry[] = [
1212
1355
  fn: 'validateNotationAlignment',
1213
1356
  }),
1214
1357
  ),
1215
- ] as [RuleCheck, ...RuleCheck[]],
1358
+ // A course's parts and the lessons in them (`compileManifest`): a
1359
+ // lesson in a directory no part lists, and a part with nothing in it yet.
1360
+ check('lesson-outside-parts', 'error', false, {
1361
+ module: 'src/compiler/manifest.ts',
1362
+ fn: 'compileManifest',
1363
+ }),
1364
+ check('part-empty', 'warning', false, {
1365
+ module: 'src/compiler/manifest.ts',
1366
+ fn: 'compileManifest',
1367
+ }),
1368
+ ] as unknown as [RuleCheck, ...RuleCheck[]],
1369
+ },
1370
+ {
1371
+ kind: 'invariant',
1372
+ id: 'content-schema',
1373
+ title: 'Every content file satisfies its collection’s schema.',
1374
+ elements: ['notation', 'assessment', 'source', 'changelog'],
1375
+ doc: 'structure.md#content-schemas',
1376
+ summary:
1377
+ 'A page’s explico fields, a notation entry, an abbreviation, a competency, an assessment, a source and a track match the schema `astro build` loads them with (`compiler/content-schemas.ts`); `validate:content` reports the field and its line.',
1378
+ checks: [
1379
+ check('content-schema', 'error', false, {
1380
+ module: 'src/compiler/content-schema.ts',
1381
+ fn: 'validateContentSchemas',
1382
+ }),
1383
+ ],
1384
+ },
1385
+ {
1386
+ kind: 'invariant',
1387
+ id: 'retired-keys',
1388
+ title: 'No content file sets a key the engine has retired or cannot honour.',
1389
+ elements: ['changelog'],
1390
+ doc: 'structure.md#retired-keys',
1391
+ summary:
1392
+ 'A frontmatter or JSON key explico no longer reads (`editorialStatus`) fails the build at every file that still sets it; delete the line. A lesson’s `draft: true` fails it too (the sidebar would name a page the build drops), and its `sidebar.order` warns (chapters follow the tracks).',
1393
+ checks: [
1394
+ check('retired-key', 'error', false, {
1395
+ module: 'src/reference/gate-retired-keys.ts',
1396
+ fn: 'findRetiredKeys',
1397
+ }),
1398
+ check('unsupported-key', 'error', false, {
1399
+ module: 'src/reference/gate-retired-keys.ts',
1400
+ fn: 'findUnsupportedKeys',
1401
+ }),
1402
+ check('ignored-key', 'warning', false, {
1403
+ module: 'src/reference/gate-retired-keys.ts',
1404
+ fn: 'findUnsupportedKeys',
1405
+ }),
1406
+ ],
1407
+ },
1408
+ {
1409
+ kind: 'invariant',
1410
+ id: 'prop-text',
1411
+ title: 'A component’s quoted prop holds plain text, and prose holds no `{…}` expression.',
1412
+ elements: ['math', 'plot', 'diagram', 'table'],
1413
+ doc: 'components.md#props-are-plain-text',
1414
+ summary:
1415
+ 'A quoted prop (`caption="…"`, `short="…"`, `title="…"`, `label="…"`) reaches the page as plain text: no KaTeX, reference or citation runs in it. `$…$`, `[[…]]` or `[@…]` in one fails the build, since it would print as typed; write the symbol in Unicode or words, and reference or cite from the prose next to the component. MDX reads `{…}` in prose as JavaScript (`{a}` fails the build, "a is not defined"): write `\\{a\\}`, or `$\\{a\\}$` for a set.',
1416
+ checks: [
1417
+ check('markup-in-prop', 'error', false, {
1418
+ module: 'src/reference/gate-prop-text.ts',
1419
+ fn: 'findMarkupInProps',
1420
+ }),
1421
+ check('mdx-expression', 'error', false, {
1422
+ module: 'src/reference/gate-mdx-expression.ts',
1423
+ fn: 'gateMdxExpressions',
1424
+ }),
1425
+ ],
1216
1426
  },
1217
1427
  ];
1218
1428
 
@@ -174,6 +174,16 @@ export function resolveRules(config: CourseConfig): ResolvedRules {
174
174
  });
175
175
  }
176
176
 
177
+ if (config.conventions !== undefined) {
178
+ diagnostics.push({
179
+ code: 'config-deprecated-key',
180
+ severity: 'warning',
181
+ message:
182
+ 'CourseConfig `conventions` is no longer read: rule S5, its only reader, was retired into rule N4. ' +
183
+ 'Delete the block. See docs/authoring/retired.md.',
184
+ });
185
+ }
186
+
177
187
  const enabled = new Map<string, boolean>();
178
188
  const settings = new Map<string, Readonly<Record<string, unknown>>>();
179
189
 
@@ -11,6 +11,8 @@
11
11
  * cookie-backed store) and nothing else changes. `createBrowserPreferenceStore`
12
12
  * is the "versioned local-storage adapter" the architecture doc anticipates —
13
13
  * kept deliberately tiny, and the only place `git grep localStorage` matches.
14
+ * `createSessionPreferenceStore` is its per-tab twin, for state one tab keeps
15
+ * of its own (the cards it carries, `card-carry.ts`).
14
16
  */
15
17
 
16
18
  export interface PreferenceStore {
@@ -54,6 +56,57 @@ export function createBrowserPreferenceStore(): PreferenceStore {
54
56
  };
55
57
  }
56
58
 
59
+ /**
60
+ * A `sessionStorage`-backed store: one tab's own state, which a reload, Back
61
+ * and Forward keep and another tab never sees. Wrapped like the local one.
62
+ */
63
+ export function createSessionPreferenceStore(): PreferenceStore {
64
+ return {
65
+ get(key) {
66
+ try {
67
+ return window.sessionStorage.getItem(key);
68
+ } catch {
69
+ return null;
70
+ }
71
+ },
72
+ set(key, value) {
73
+ try {
74
+ window.sessionStorage.setItem(key, value);
75
+ } catch {
76
+ // Storage unavailable or full — the tab still works, it just forgets.
77
+ }
78
+ },
79
+ remove(key) {
80
+ try {
81
+ window.sessionStorage.removeItem(key);
82
+ } catch {
83
+ // Storage unavailable — there is nothing stored to forget.
84
+ }
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The store's one inline twin: the source of a script that copies a stored
91
+ * preference onto the root element as `attribute` before the page first
92
+ * paints. Every other reader of a preference is a module script, which runs
93
+ * after that paint: fine for a panel's state, but a page drawn at one text
94
+ * size and then redrawn at another visibly jumps. Only a value in `values`
95
+ * is copied; anything else stored is ignored. It is built here so that
96
+ * browser storage is still touched in this file alone.
97
+ */
98
+ export function inlineRootAttributeScript(
99
+ key: string,
100
+ attribute: string,
101
+ values: readonly string[],
102
+ ): string {
103
+ return (
104
+ `try{var v=window.localStorage.getItem(${JSON.stringify(key)});` +
105
+ `if(${JSON.stringify(values)}.indexOf(v)>=0)` +
106
+ `document.documentElement.setAttribute(${JSON.stringify(attribute)},v)}catch(e){}`
107
+ );
108
+ }
109
+
57
110
  /** An in-memory store — the no-op-equivalent seam for SSR or tests. */
58
111
  export function createMemoryPreferenceStore(): PreferenceStore {
59
112
  const map = new Map<string, string>();