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
@@ -84,11 +84,18 @@ import {
84
84
  validateRawFloats,
85
85
  type RawFloatDiagnostic,
86
86
  } from '../reference/raw-floats-validate';
87
+ import {
88
+ validateLabKatexImports,
89
+ type LabKatexDiagnostic,
90
+ } from '../reference/lab-katex-validate';
87
91
  import {
88
92
  gateContentMath,
89
93
  pageScope,
94
+ scopeEntry,
90
95
  type MathGateDiagnostic,
91
96
  } from '../reference/gate-math';
97
+ import { gateIndices, type DerivedUse } from '../reference/gate-indices';
98
+ import { derivedNotationEntries } from '../reference/derived-entries';
92
99
  import {
93
100
  gateProseNotation,
94
101
  gateProseNotationInAssessments,
@@ -102,12 +109,18 @@ import { gateCourseRules } from '../reference/gate-course-rules';
102
109
  import { gateHeadingLength } from '../reference/gate-heading-length';
103
110
  import { gateHeadingStructure } from '../reference/gate-heading-structure';
104
111
  import { gatePartPages } from '../reference/gate-part-pages';
112
+ import { gateNonChapterPages } from '../reference/gate-non-chapter-pages';
113
+ import { findRetiredKeys, findUnsupportedKeys } from '../reference/gate-retired-keys';
114
+ import { findMarkupInProps } from '../reference/gate-prop-text';
115
+ import { gateMdxExpressions } from '../reference/gate-mdx-expression';
116
+ import { CITATION_BRACKET, bracketCitations, withoutLinkCitations } from '../reference/citation-format.mjs';
117
+ import { validateContentSchemas } from './content-schema';
105
118
  import {
106
119
  scanSectionHeadings,
107
120
  scanTitledBlocks,
108
121
  } from '../reference/sections.mjs';
109
122
  import { knowledgeCheckAnchorId } from '../reference/knowledge-checks.mjs';
110
- import { validateSectionRefs } from '../reference/sections-validate';
123
+ import { validateHeadingAnchors, validateSectionRefs } from '../reference/sections-validate';
111
124
  import {
112
125
  splitMixedContent,
113
126
  type ContentSegment,
@@ -119,7 +132,6 @@ import type { GlyphScopeEntry } from '../reference/gloss';
119
132
  import { resolveReferenceId } from '../reference/references';
120
133
  import type { NotationGlossEntry } from '../reference/types';
121
134
  import type {
122
- EditorialStatus,
123
135
  NotationBacklink,
124
136
  NotationDefinitionRecord,
125
137
  NotationDiagnostic,
@@ -129,11 +141,13 @@ import type {
129
141
  import {
130
142
  coursePartsOf,
131
143
  normalizeBlockLabels,
144
+ normalizeCards,
132
145
  normalizeLessonExports,
133
146
  normalizeDisclosure,
134
147
  normalizeNumbering,
135
148
  type BlockLabels,
136
149
  type CourseConfig,
150
+ type NormalizedCards,
137
151
  type NormalizedLessonExports,
138
152
  type NormalizedDisclosure,
139
153
  type NormalizedNumbering,
@@ -158,9 +172,11 @@ import {
158
172
  } from './knowledge-version';
159
173
  import {
160
174
  loadAbbreviationRegistryInput,
175
+ loadContentRecords,
161
176
  loadCurriculumCatalog,
162
177
  loadNotationDefinitions,
163
178
  loadNotationRegistryInput,
179
+ loadNonChapterPages,
164
180
  loadNotationSpecs,
165
181
  loadPartOverviews,
166
182
  loadSourceRecords,
@@ -290,8 +306,48 @@ import {
290
306
  * state of a `<Fold>`; `disclosure.figures` now says whether a lab's tools
291
307
  * start open; `diagnostics.folds` holds `fold-unnamed`
292
308
  * (docs/plans/figures-and-short-captions.md, Parts 1–2).
309
+ *
310
+ * v25 (cards on non-chapter pages, rule S10): `pages` — every docs page that
311
+ * is not a chapter (the landing page, a part's overview page, …) with its
312
+ * `id`, `slug`, `file` and a `notation` bundle of the lesson shape, so the
313
+ * footer renders the cards its `[[key]]` terms and math open.
314
+ * `notation.bundles` and `notation.backlinks` now carry those pages' ids too.
315
+ *
316
+ * v26 (carried cards, additive): `cards` — the course's `cards` settings with
317
+ * the engine defaults applied: whether "Carry cards" starts on (rule K1).
318
+ *
319
+ * v27 (LaTeX export): `lessonExports.latex` and `lessonExports.latexPaper`;
320
+ * every `assessmentContent` item carries its `type`, a choice item's
321
+ * `options` (plain-text labels) and `correctOptionId`, and a
322
+ * numeric item's `answer`, so an answer key can be printed without re-reading
323
+ * `content/assessments/`.
324
+ *
325
+ * v28 (Markdown export): `lessonExports.markdown`.
326
+ *
327
+ * v29 (no editorial status): the draft / in-review / reviewed flag is gone —
328
+ * `lessons[].status`, `editorialStatus` on the notation definitions of
329
+ * `lessons[]` and `pages[]`, and `status` on `notation.definitions[]` and
330
+ * `notation.bundles[]`. `diagnostics.curriculum` carries `retired-key`, with
331
+ * `file` and `line`, for a content file that still sets the key, and the
332
+ * lesson-assumptions check reports `missing-assumptions`, not `review-state`.
333
+ *
334
+ * v30 (headings with math, additive): `lessons[].headings[].text` is what a
335
+ * reader sees — inline math as it reads (`$\explain{k}{R}$` is `R`, not its
336
+ * LaTeX) and a `[[key]]` term as its label — and `math: true` marks a heading
337
+ * with inline math. A heading's `slug` is unchanged. Each choice option of
338
+ * `assessmentContent` carries `labelSegments`, its label split into text and
339
+ * math like `promptSegments`, so a label's math renders.
340
+ *
341
+ * v31 (indexed symbols, rule N23): the `notation` bundle of `lessons[]` and
342
+ * `pages[]` gains `derived` — the derived cards its body math uses, each
343
+ * composed from a base entry and an index entry (`\mathbf{v}_0` from
344
+ * `\mathbf{v}` and `{}_0`), keyed `<base>.sub.<index>`. A notation
345
+ * definition carries `reads`, `range`, `fixedIndex` and an index entry's
346
+ * `formulaTemplate` when its entry sets them, and a bundle definition its
347
+ * `range`. `diagnostics.math` carries `index-explained`, `index-swallowed`,
348
+ * `index-range` and `derived-key-taken`.
293
349
  */
294
- export const MANIFEST_SCHEMA_VERSION = 24;
350
+ export const MANIFEST_SCHEMA_VERSION = 31;
295
351
 
296
352
  const REPOSITORY_ROOT = process.cwd();
297
353
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -361,10 +417,11 @@ export interface ManifestLessonNotation {
361
417
  readonly notation: string;
362
418
  readonly label: string;
363
419
  readonly summary: string;
364
- readonly editorialStatus: EditorialStatus;
365
420
  readonly seeAlso: readonly string[];
366
421
  readonly units?: string;
367
422
  readonly formula?: string;
423
+ /** Rule N23: the set a running index letter runs over, shown on its card (v31). */
424
+ readonly range?: string;
368
425
  /** Symbols the formula names that have no card of their own (D15). */
369
426
  readonly glosses: readonly NotationGlossEntry[];
370
427
  /**
@@ -375,6 +432,27 @@ export interface ManifestLessonNotation {
375
432
  readonly formulaScope: readonly GlyphScopeEntry[];
376
433
  }
377
434
 
435
+ /**
436
+ * A derived card (rule N23, v31): the card of a symbol with a subscript that
437
+ * an index entry explains, composed from the `base` entry and the `index`
438
+ * entry by `derived-notation.mjs`. Its title and summary are about the
439
+ * indexed symbol; its formula names the base, whose card is one level deeper.
440
+ * It is listed nowhere — not in the Page notation panel, the glossary or an
441
+ * export's notation section — and opens only from the symbol.
442
+ */
443
+ export interface ManifestDerivedNotation {
444
+ readonly key: string;
445
+ readonly base: string;
446
+ readonly index: string;
447
+ readonly notation: string;
448
+ readonly label: string;
449
+ readonly summary: string;
450
+ readonly units?: string;
451
+ readonly formula?: string;
452
+ readonly glosses: readonly NotationGlossEntry[];
453
+ readonly formulaScope: readonly GlyphScopeEntry[];
454
+ }
455
+
378
456
  /**
379
457
  * One keyed float of a lesson (v21): its key and number, and its caption pair
380
458
  * — `short` (an equation's `\caption{…}`, or the `short` prop) and, for a
@@ -444,16 +522,18 @@ export interface ManifestLesson {
444
522
  */
445
523
  readonly headings: readonly {
446
524
  readonly slug: string;
525
+ /** What a reader sees: math as it reads, a `[[key]]` term as its label (v30). */
447
526
  readonly text: string;
448
527
  readonly level: number;
449
528
  readonly number?: string;
529
+ /** The heading has inline math (v30). */
530
+ readonly math?: true;
450
531
  }[];
451
532
  readonly file: string;
452
533
  readonly title: string;
453
534
  readonly description: string;
454
535
  /** Authored reader-facing estimate; `40` and `[30, 40]` both land here. */
455
536
  readonly studyMinutes?: { readonly min: number; readonly max: number };
456
- readonly status: EditorialStatus;
457
537
  /** Ordered — the curriculum contract. */
458
538
  readonly teaches: readonly string[];
459
539
  readonly requires: readonly string[];
@@ -497,11 +577,27 @@ export interface ManifestLesson {
497
577
  }[];
498
578
  readonly definitionIds: readonly string[];
499
579
  readonly definitions: readonly ManifestLessonNotation[];
580
+ /** The derived cards (rule N23) the page's body math uses (v31). */
581
+ readonly derived: readonly ManifestDerivedNotation[];
500
582
  /** Every `[[key]]` / `\explain{key}` occurrence, resolved and located. */
501
583
  readonly resolution: readonly ManifestNotationResolution[];
502
584
  };
503
585
  }
504
586
 
587
+ /**
588
+ * A docs page that is not a chapter (v25, rule S10) — the landing page, a
589
+ * part's overview page, any page outside a part directory. It numbers
590
+ * nothing, so it carries only what its cards need: the notation bundle its
591
+ * `[[key]]` terms and math reach, shared entries only.
592
+ */
593
+ export interface ManifestPage {
594
+ /** `index`, `motion.index`, `glossary` — `lessonIdFromSlug` of the slug; never a lesson id. */
595
+ readonly id: string;
596
+ readonly slug: string;
597
+ readonly file: string;
598
+ readonly notation: ManifestLesson['notation'];
599
+ }
600
+
505
601
  /** The reader-facing "about this material" bundle — the `[?]` panel's data. */
506
602
  export interface ManifestAbout {
507
603
  /** `explico`'s own identity, from its `package.json` (the version's golden source). */
@@ -536,6 +632,22 @@ export interface ManifestAssessmentItemContent {
536
632
  readonly itemId: string;
537
633
  readonly promptSegments: readonly ContentSegment[];
538
634
  readonly explanationSegments: readonly ContentSegment[];
635
+ /** What kind of answer the item takes (v27). */
636
+ readonly type: 'single-choice' | 'numeric';
637
+ /**
638
+ * A choice item's options in authored order: the label as authored, and
639
+ * split into text and math like the prompt (`labelSegments`, v30), so a
640
+ * label's math renders.
641
+ */
642
+ readonly options?: readonly {
643
+ readonly id: string;
644
+ readonly label: string;
645
+ readonly labelSegments: readonly ContentSegment[];
646
+ }[];
647
+ /** A choice item's correct option. */
648
+ readonly correctOptionId?: string;
649
+ /** A numeric item's accepted value and tolerance. */
650
+ readonly answer?: { readonly value: number; readonly tolerance: number };
539
651
  }
540
652
 
541
653
  /**
@@ -578,6 +690,8 @@ export interface Manifest {
578
690
  readonly numbering: NormalizedNumbering;
579
691
  /** How a check's and an example set's heading names its kind (`CourseConfig.blockLabels`). */
580
692
  readonly blockLabels: BlockLabels;
693
+ /** How the explanation cards start for a reader, defaults applied (`CourseConfig.cards`). */
694
+ readonly cards: NormalizedCards;
581
695
  /** Reader-facing identity + versions (the `[?]` About panel). */
582
696
  readonly about: ManifestAbout;
583
697
  readonly notation: {
@@ -597,6 +711,8 @@ export interface Manifest {
597
711
  /** The course's parts in config order, with their chapters and overview pages. */
598
712
  readonly parts: readonly ManifestPart[];
599
713
  readonly lessons: readonly ManifestLesson[];
714
+ /** Every docs page that is not a chapter, by id (v25). */
715
+ readonly pages: readonly ManifestPage[];
600
716
  /** Full competency prerequisite graph. */
601
717
  readonly prereqEdges: readonly PrereqEdge[];
602
718
  /** Equation identity (D7): every labelled display equation + the render-path lookup. */
@@ -660,8 +776,12 @@ export interface Manifest {
660
776
  readonly equations: readonly EquationDiagnostic[];
661
777
  /** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
662
778
  readonly tables: readonly TableDiagnostic[];
663
- /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
664
- readonly figures: readonly FigureDiagnostic[];
779
+ /**
780
+ * `<Figure>` identity checks (F-tables-figures) — same codes, `fig-`
781
+ * prefixed — and rule V3's `lab-katex-direct`: a file a lesson imports
782
+ * that calls KaTeX itself.
783
+ */
784
+ readonly figures: readonly (FigureDiagnostic | LabKatexDiagnostic)[];
665
785
  /** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
666
786
  readonly diagrams: readonly DiagramDiagnostic[];
667
787
  /** Knowledge-check identity and `[[check-…]]` reference checks. */
@@ -691,7 +811,6 @@ export interface Manifest {
691
811
 
692
812
  // --- compile ---------------------------------------------------------
693
813
 
694
- const CITE_PATTERN = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
695
814
 
696
815
  /** Distinct `[@id; locator]` occurrences in a body, in first-appearance order. */
697
816
  function lessonCitations(body: string): ManifestCitation[] {
@@ -700,13 +819,16 @@ function lessonCitations(body: string): ManifestCitation[] {
700
819
  .replace(/`[^`\n]*`/g, '');
701
820
  const seen = new Set<string>();
702
821
  const citations: ManifestCitation[] = [];
703
- for (const match of withoutCode.matchAll(CITE_PATTERN)) {
704
- const id = (match[1] ?? '').trim();
705
- const locator = (match[2] ?? '').replace(/\s+/g, ' ').trim() || undefined;
706
- const dedupeKey = `${id} ${locator ?? ''}`;
707
- if (seen.has(dedupeKey)) continue;
708
- seen.add(dedupeKey);
709
- citations.push(locator === undefined ? { id } : { id, locator });
822
+ // A citation in a link's text renders as typed (`citation-in-link`).
823
+ for (const match of withoutLinkCitations(withoutCode).matchAll(CITATION_BRACKET)) {
824
+ for (const citation of bracketCitations(match[1] ?? '', match[2])) {
825
+ const id = citation.id;
826
+ const locator = citation.locator || undefined;
827
+ const dedupeKey = `${id} ${locator ?? ''}`;
828
+ if (seen.has(dedupeKey)) continue;
829
+ seen.add(dedupeKey);
830
+ citations.push(locator === undefined ? { id } : { id, locator });
831
+ }
710
832
  }
711
833
  return citations;
712
834
  }
@@ -770,7 +892,9 @@ function lessonNotationResolution(
770
892
  : 'local'
771
893
  : 'unresolved',
772
894
  definitionId: definitionId ?? null,
773
- kind: reference.kind,
895
+ // A page's own references are prose or math; a definition's formula
896
+ // reference is reported as the definition reference it always was.
897
+ kind: reference.kind === 'formula' ? 'definition' : reference.kind,
774
898
  line: reference.source.line ?? null,
775
899
  column: reference.source.column ?? null,
776
900
  };
@@ -782,11 +906,14 @@ function lessonNotationBundle(
782
906
  definitionsById: ReadonlyMap<string, NotationDefinitionRecord>,
783
907
  resolution: readonly ManifestNotationResolution[],
784
908
  cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
909
+ derivedUses: readonly DerivedUse[] = [],
785
910
  ): ManifestLesson['notation'] {
786
911
  const definitions: ManifestLessonNotation[] = [];
912
+ const records: NotationDefinitionRecord[] = [];
787
913
  for (const definitionId of bundle?.definitionIds ?? []) {
788
914
  const definition = definitionsById.get(definitionId);
789
915
  if (!definition) continue;
916
+ records.push(definition);
790
917
  definitions.push({
791
918
  id: definition.id,
792
919
  key: definition.key,
@@ -794,7 +921,6 @@ function lessonNotationBundle(
794
921
  notation: definition.notation,
795
922
  label: definition.label,
796
923
  summary: definition.summary,
797
- editorialStatus: definition.status,
798
924
  seeAlso: [...definition.seeAlso],
799
925
  glosses: [...definition.glosses],
800
926
  formulaScope: formulaGlyphScope({
@@ -810,12 +936,18 @@ function lessonNotationBundle(
810
936
  ...(definition.formula === undefined
811
937
  ? {}
812
938
  : { formula: definition.formula }),
939
+ ...(definition.range === undefined ? {} : { range: definition.range }),
813
940
  });
814
941
  }
815
942
  return {
816
943
  bindings: [...(bundle?.bindings ?? [])].map((binding) => ({ ...binding })),
817
944
  definitionIds: [...(bundle?.definitionIds ?? [])],
818
945
  definitions,
946
+ derived: derivedNotationEntries(
947
+ derivedUses,
948
+ new Map(records.map((record) => [record.key, record])),
949
+ cardsByKey,
950
+ ),
819
951
  resolution: [...resolution],
820
952
  };
821
953
  }
@@ -925,13 +1057,25 @@ export async function compileManifest(
925
1057
  loadAbbreviationRegistryInput(),
926
1058
  ]);
927
1059
 
928
- const registry = buildNotationRegistry(notationInput);
1060
+ // Rule S10: a page that is not a chapter explains its symbols like a
1061
+ // chapter does, so its references resolve through the same registry and its
1062
+ // text and math pass the same gates. It stays out of `notationInput.lessons`,
1063
+ // which is the chapter list every numbering and curriculum pass reads.
1064
+ const nonChapterPages = loadNonChapterPages();
1065
+ const withPages: typeof notationInput = {
1066
+ ...notationInput,
1067
+ lessons: [
1068
+ ...notationInput.lessons,
1069
+ ...nonChapterPages.map((page) => page.notation),
1070
+ ],
1071
+ };
1072
+ const registry = buildNotationRegistry(withPages);
929
1073
  // Cards a `formula` may name with `\explain{key}` (D15). Shared entries
930
1074
  // only: a page-local symbol is not addressable from another entry.
931
1075
  const cardsByKey = new Map<string, GlyphScopeEntry>(
932
1076
  notationInput.sharedDefinitions.map((definition) => [
933
1077
  definition.key,
934
- { key: definition.key, notation: definition.notation },
1078
+ scopeEntry(definition),
935
1079
  ]),
936
1080
  );
937
1081
  const definitionsById = new Map(
@@ -945,27 +1089,74 @@ export async function compileManifest(
945
1089
  const assessmentsInput = catalog.assessments as unknown as {
946
1090
  id: string;
947
1091
  title?: string;
948
- items?: { id?: string; prompt?: string; explanation?: string }[];
1092
+ items?: {
1093
+ id?: string;
1094
+ type?: string;
1095
+ prompt?: string;
1096
+ explanation?: string;
1097
+ options?: { id?: string; label?: string }[];
1098
+ correctOptionId?: string;
1099
+ answer?: { value?: number; tolerance?: number };
1100
+ }[];
949
1101
  }[];
950
1102
 
951
- const curriculumIssues = validateCurriculum(catalog);
1103
+ const contentRecords = loadContentRecords();
1104
+ const curriculumIssues = [
1105
+ ...validateCurriculum(catalog),
1106
+ ...validateContentSchemas(contentRecords),
1107
+ ...findRetiredKeys(contentRecords),
1108
+ ...findUnsupportedKeys(contentRecords),
1109
+ ...findMarkupInProps(contentRecords),
1110
+ ];
952
1111
  const alignmentIssues = validateNotationAlignment(registry, catalog);
953
1112
  const mathEndAbbreviations = {
954
1113
  allow: rules.strings('N12', 'allow'),
955
1114
  deny: rules.strings('N12', 'deny'),
956
1115
  };
1116
+ // A heading's text is what the reader sees: a `[[key]]` term in it reads as
1117
+ // its label, the page's own symbol first (rule S1 measures it, and the
1118
+ // outline records it).
1119
+ const sharedLabels = new Map(
1120
+ notationInput.sharedDefinitions.map((definition) => [definition.key, definition.label]),
1121
+ );
1122
+ const localLabelsByLesson = new Map(
1123
+ notationInput.lessons.map((lesson) => [
1124
+ lesson.lessonId,
1125
+ new Map(lesson.localDefinitions.map((definition) => [definition.key, definition.label])),
1126
+ ]),
1127
+ );
1128
+ const termLabel = (lessonId: string, key: string): string | undefined =>
1129
+ localLabelsByLesson.get(lessonId)?.get(key) ?? sharedLabels.get(key);
1130
+ // Rule N23 and the derived cards a page uses: one resolver pass over every
1131
+ // body. The cards are built whatever the rule's switch says; its findings
1132
+ // drop with the rule (`dropDisabled`) and yield to a waiver.
1133
+ const indexGate = gateIndices(
1134
+ withPages.lessons,
1135
+ new Map(
1136
+ notationInput.sharedDefinitions.map((definition) => [definition.key, definition]),
1137
+ ),
1138
+ {
1139
+ assessments: assessmentsInput,
1140
+ lessonAssessments: catalog.lessons.map((lesson) => ({
1141
+ lessonId: lesson.id,
1142
+ assessmentIds: lesson.assessments,
1143
+ })),
1144
+ },
1145
+ );
957
1146
  const mathIssues = [
1147
+ ...indexGate.diagnostics,
958
1148
  ...gateContentMath({
959
- notationInput,
1149
+ notationInput: withPages,
960
1150
  assessments: assessmentsInput,
961
1151
  lessonAssessments: catalog.lessons.map((lesson) => ({
962
1152
  lessonId: lesson.id,
963
1153
  assessmentIds: lesson.assessments,
964
1154
  })),
965
1155
  }),
1156
+ ...gateMdxExpressions(withPages.lessons),
966
1157
  ...(rules.isEnabled('N9')
967
1158
  ? [
968
- ...gateProseNotation(notationInput.lessons, {
1159
+ ...gateProseNotation(withPages.lessons, {
969
1160
  allowSpelledUnits: rules.strings('N9', 'allow'),
970
1161
  }),
971
1162
  ...gateProseNotationInAssessments(assessmentsInput, {
@@ -975,13 +1166,13 @@ export async function compileManifest(
975
1166
  : []),
976
1167
  ...(rules.isEnabled('N12')
977
1168
  ? [
978
- ...gateMathPunctuation(notationInput.lessons, mathEndAbbreviations),
1169
+ ...gateMathPunctuation(withPages.lessons, mathEndAbbreviations),
979
1170
  ...gateMathPunctuationInAssessments(assessmentsInput, mathEndAbbreviations),
980
1171
  ]
981
1172
  : []),
982
1173
  ...(rules.isEnabled('N14')
983
1174
  ? gateAbbreviations({
984
- notationInput,
1175
+ notationInput: withPages,
985
1176
  abbreviations: abbreviationsInput,
986
1177
  courseAbbreviations: rules.settings('N14'),
987
1178
  })
@@ -989,6 +1180,7 @@ export async function compileManifest(
989
1180
  ...(rules.isEnabled('S1')
990
1181
  ? gateHeadingLength(notationInput.lessons, {
991
1182
  maxLength: rules.number('S1', 'maxLength'),
1183
+ labelFor: termLabel,
992
1184
  })
993
1185
  : []),
994
1186
  ...gateHeadingStructure(notationInput.lessons, {
@@ -998,7 +1190,9 @@ export async function compileManifest(
998
1190
  // The course's own rules (`courseRules`): one pass for every enabled `X`
999
1191
  // rule that carries a `forbid` pattern. A disabled one, and a guide rule
1000
1192
  // with no pattern, do no work here.
1001
- ...gateCourseRules(notationInput.lessons, rules.course),
1193
+ ...gateCourseRules(withPages.lessons, rules.course),
1194
+ // S10 is locked: a float on a non-chapter page has no number to render.
1195
+ ...gateNonChapterPages(nonChapterPages),
1002
1196
  ];
1003
1197
 
1004
1198
  // Pre-render every assessment `prompt` / `explanation` into text/math
@@ -1015,22 +1209,57 @@ export async function compileManifest(
1015
1209
  assessmentsInput.map((assessment) => [assessment.id, assessment]),
1016
1210
  );
1017
1211
  const assessmentContent: ManifestAssessmentItemContent[] = [];
1212
+ // An assessment is page math: its indexed symbols take derived cards
1213
+ // (rule N23), which the lesson then ships with its body's.
1214
+ const assessmentDerived = new Map<string, Map<string, DerivedUse>>();
1018
1215
  for (const lesson of catalog.lessons) {
1019
1216
  if (lesson.assessments.length === 0) continue;
1020
1217
  const lessonInput = notationLessonById.get(lesson.id);
1021
1218
  if (!lessonInput) continue;
1022
1219
  const scope = pageScope(lessonInput, cardsByKey);
1220
+ const derived = new Map<string, DerivedUse>();
1221
+ assessmentDerived.set(lesson.id, derived);
1222
+ const page = { derive: true, derived };
1023
1223
  for (const assessmentId of lesson.assessments) {
1024
1224
  const assessment = assessmentInputById.get(assessmentId);
1025
1225
  if (!assessment?.items) continue;
1026
1226
  for (const item of assessment.items) {
1027
1227
  if (!item.id) continue;
1228
+ const numeric =
1229
+ item.type === 'numeric' &&
1230
+ typeof item.answer?.value === 'number' &&
1231
+ typeof item.answer.tolerance === 'number';
1028
1232
  assessmentContent.push({
1029
1233
  lessonId: lesson.id,
1030
1234
  assessmentId,
1031
1235
  itemId: item.id,
1032
- promptSegments: splitMixedContent(item.prompt ?? '', scope),
1033
- explanationSegments: splitMixedContent(item.explanation ?? '', scope),
1236
+ promptSegments: splitMixedContent(item.prompt ?? '', scope, page),
1237
+ explanationSegments: splitMixedContent(item.explanation ?? '', scope, page),
1238
+ type: item.type === 'numeric' ? 'numeric' : 'single-choice',
1239
+ ...(item.options
1240
+ ? {
1241
+ options: item.options.flatMap((option) =>
1242
+ option.id
1243
+ ? [
1244
+ {
1245
+ id: option.id,
1246
+ label: option.label ?? '',
1247
+ labelSegments: splitMixedContent(option.label ?? '', scope, page),
1248
+ },
1249
+ ]
1250
+ : [],
1251
+ ),
1252
+ }
1253
+ : {}),
1254
+ ...(item.correctOptionId ? { correctOptionId: item.correctOptionId } : {}),
1255
+ ...(numeric
1256
+ ? {
1257
+ answer: {
1258
+ value: item.answer!.value!,
1259
+ tolerance: item.answer!.tolerance!,
1260
+ },
1261
+ }
1262
+ : {}),
1034
1263
  });
1035
1264
  }
1036
1265
  }
@@ -1060,9 +1289,44 @@ export async function compileManifest(
1060
1289
  const lessonSlugs = [...notationInput.lessons]
1061
1290
  .map((lesson) => lesson.lessonId.replace(/\./g, '/'))
1062
1291
  .sort();
1063
- const configuredParts = coursePartsOf(courseConfig);
1064
- const overviewPages = loadPartOverviews(configuredParts);
1292
+ // A part takes a sidebar group and an ordinal once it has a chapter or an
1293
+ // overview page; one with neither yet (planned, its directory not written)
1294
+ // is left out of both, with a warning, so every part number — a lesson's,
1295
+ // `parts[]`, the LaTeX export's — counts the same parts.
1296
+ const listedParts = coursePartsOf(courseConfig);
1297
+ const overviewPages = loadPartOverviews(listedParts);
1065
1298
  const overviewByDir = new Map(overviewPages.map((page) => [page.dir, page]));
1299
+ const hasChapter = (dir: string) => lessonSlugs.some((slug) => slug.startsWith(`${dir}/`));
1300
+ const configuredParts = listedParts.filter(
1301
+ (part) => hasChapter(part.dir) || overviewByDir.has(part.dir),
1302
+ );
1303
+ for (const part of listedParts) {
1304
+ if (configuredParts.includes(part)) continue;
1305
+ curriculumIssues.push({
1306
+ kind: 'part-empty',
1307
+ severity: 'warning',
1308
+ message:
1309
+ `course.settings.json \`parts\` lists "${part.dir}" (${JSON.stringify(part.label)}), but ` +
1310
+ `content/docs/${part.dir}/ has no chapter and no overview page yet, so the part ` +
1311
+ 'is left out of the sidebar and the numbering until it has one.',
1312
+ });
1313
+ }
1314
+ // A lesson whose directory no part lists has no part, no chapter number and
1315
+ // no place in the sidebar.
1316
+ const partDirs = new Set(listedParts.map((part) => part.dir));
1317
+ for (const lesson of notationInput.lessons) {
1318
+ const dir = lesson.lessonId.split('.')[0] ?? '';
1319
+ if (partDirs.has(dir)) continue;
1320
+ curriculumIssues.push({
1321
+ kind: 'lesson-outside-parts',
1322
+ message:
1323
+ `${lesson.source.file} is in content/docs/${dir}/, which no part in ` +
1324
+ `course.settings.json \`parts\` lists, so it has no part, no chapter number and no ` +
1325
+ `place in the sidebar. Add { "dir": "${dir}", "label": "…" } to \`parts\`, or move the ` +
1326
+ "lesson into a part's directory.",
1327
+ file: lesson.source.file,
1328
+ });
1329
+ }
1066
1330
  // S2: every authored part page carries its bridge and scope paragraphs.
1067
1331
  if (rules.isEnabled('S2')) {
1068
1332
  mathIssues.push(
@@ -1077,10 +1341,12 @@ export async function compileManifest(
1077
1341
  }
1078
1342
  // Per-place waivers: drop the located findings each one covers, and
1079
1343
  // report the waivers themselves — a stale one is a warning, a bad one an
1080
- // error. Only lesson bodies are scanned; a part page has no waivable rule.
1344
+ // error. Chapters and non-chapter pages are scanned alike, since the text
1345
+ // and math gates run on both; S2 and S10 are per-file or locked, so no
1346
+ // waiver reaches them.
1081
1347
  const waiverDiagnostics: WaiverDiagnostic[] = [];
1082
1348
  const waiverRules = { courseRules: rules.course };
1083
- const waivers = notationInput.lessons.flatMap((lesson) => {
1349
+ const waivers = withPages.lessons.flatMap((lesson) => {
1084
1350
  const collected = collectWaivers(lesson, waiverRules);
1085
1351
  waiverDiagnostics.push(...collected.diagnostics);
1086
1352
  return collected.waivers;
@@ -1127,12 +1393,14 @@ export async function compileManifest(
1127
1393
  >();
1128
1394
  const chapterScoped = numberingSettings.floats === 'chapter';
1129
1395
  let partCursor = 0;
1130
- for (const group of sidebar) {
1396
+ // The part groups, in `configuredParts` order; the static Reference group
1397
+ // after them is no part. An overview-only part counts, as `parts[]` does.
1398
+ for (const group of sidebar.slice(0, configuredParts.length)) {
1399
+ partCursor += 1;
1131
1400
  const lessonIds = group.items
1132
1401
  .filter((item): item is string => typeof item === 'string')
1133
1402
  .map(lessonIdFromSlug);
1134
- if (lessonIds.length === 0) continue; // the static Reference group
1135
- partCursor += 1;
1403
+ if (lessonIds.length === 0) continue;
1136
1404
  // Running totals for this part, advanced one chapter at a time; they
1137
1405
  // only reach a number under the part scope.
1138
1406
  const used = {
@@ -1184,6 +1452,7 @@ export async function compileManifest(
1184
1452
  ? { prefix: `${placement.part}.${placement.chapter}` }
1185
1453
  : {}),
1186
1454
  depth: numberingSettings.sections.depth,
1455
+ labelFor: (key: string) => termLabel(lesson.lessonId, key),
1187
1456
  }) as ManifestLesson['headings'],
1188
1457
  ];
1189
1458
  }),
@@ -1199,6 +1468,7 @@ export async function compileManifest(
1199
1468
  (heading) => heading.slug,
1200
1469
  ),
1201
1470
  })),
1471
+ nonChapterPages.map((page) => page.slug),
1202
1472
  ),
1203
1473
  );
1204
1474
 
@@ -1337,6 +1607,52 @@ export async function compileManifest(
1337
1607
  }),
1338
1608
  ]),
1339
1609
  );
1610
+ // A heading's anchor must not be one the engine gives something else on
1611
+ // the page (`heading-anchor-collision`, keyed-floats invariant).
1612
+ const engineAnchorsFor = (lessonId: string, body: string): Map<string, string> => {
1613
+ const slug = lessonId.replace(/\./g, '/');
1614
+ const anchors = new Map<string, string>();
1615
+ const keyed = (
1616
+ floats: readonly { readonly key: string }[],
1617
+ prefix: string,
1618
+ owner: (key: string) => string,
1619
+ ) => {
1620
+ for (const float of floats) anchors.set(`${prefix}-${float.key}`, owner(float.key));
1621
+ };
1622
+ keyed(equationsBySlug.get(slug) ?? [], 'eq', (key) => `the equation labelled eq:${key}`);
1623
+ keyed(tablesBySlug.get(slug) ?? [], 'tab', (key) => `<Table id="${key}">`);
1624
+ keyed(figuresBySlug.get(slug) ?? [], 'fig', (key) => `<Figure id="${key}">`);
1625
+ keyed(diagramsBySlug.get(slug) ?? [], 'dia', (key) => `<Diagram id="${key}">`);
1626
+ keyed(examplesBySlug.get(slug) ?? [], 'ex', (key) => `<CompactExamples id="${key}">`);
1627
+ for (const check of checksBySlug.get(slug) ?? []) {
1628
+ anchors.set(knowledgeCheckAnchorId(check.key), `the knowledge check ${check.key}`);
1629
+ }
1630
+ for (const [name, prefix] of [
1631
+ ['Memo', 'memo'],
1632
+ ['ShortNote', 'short-note'],
1633
+ ] as const) {
1634
+ const tag = new RegExp(`<${name}\\b[^>]*?\\bid=["']([^"']+)["']`, 'g');
1635
+ for (const match of body.matchAll(tag)) {
1636
+ anchors.set(`${prefix}-${match[1]}`, `<${name} id="${match[1]}">`);
1637
+ }
1638
+ }
1639
+ if (deriveSources(body).length > 0) {
1640
+ anchors.set('references', 'the References section generated from the page’s citations');
1641
+ }
1642
+ return anchors;
1643
+ };
1644
+ mathIssues.push(
1645
+ ...validateHeadingAnchors(
1646
+ notationInput.lessons.map((lesson) => ({
1647
+ file: lesson.source.file,
1648
+ body: lesson.body,
1649
+ ...(lesson.bodyLine === undefined ? {} : { bodyLine: lesson.bodyLine }),
1650
+ headings: headingsByLessonId.get(lesson.lessonId) ?? [],
1651
+ anchors: engineAnchorsFor(lesson.lessonId, lesson.body),
1652
+ })),
1653
+ ),
1654
+ );
1655
+
1340
1656
  // The blocks that draw an `<h2>` where their tag stands, placed after the
1341
1657
  // outline heading they follow — "On this page" lists them there (v23).
1342
1658
  // `<AssessmentSet />` draws every check of the lesson, or the one its `id`
@@ -1369,6 +1685,16 @@ export async function compileManifest(
1369
1685
  },
1370
1686
  );
1371
1687
  };
1688
+ // Rule V3: the course files a lesson's imports reach draw math through
1689
+ // `renderLabMath`, never KaTeX itself.
1690
+ const labKatexDiagnostics = validateLabKatexImports(
1691
+ notationInput.lessons.map((lesson) => ({
1692
+ lessonId: lesson.lessonId,
1693
+ file: lesson.source.file,
1694
+ body: lesson.body,
1695
+ })),
1696
+ REPOSITORY_ROOT,
1697
+ );
1372
1698
  const rawFloatDiagnostics = validateRawFloats(
1373
1699
  notationInput.lessons.map((lesson) => ({
1374
1700
  lessonId: lesson.lessonId,
@@ -1412,7 +1738,6 @@ export async function compileManifest(
1412
1738
  ...(frontmatter.studyMinutes === undefined
1413
1739
  ? {}
1414
1740
  : { studyMinutes: frontmatter.studyMinutes }),
1415
- status: meta.status,
1416
1741
  teaches: [...teaches],
1417
1742
  requires: [...meta.requires],
1418
1743
  assumptions: [...meta.assumptions],
@@ -1442,18 +1767,38 @@ export async function compileManifest(
1442
1767
  availableDefinitionIds,
1443
1768
  ),
1444
1769
  cardsByKey,
1770
+ [
1771
+ ...(indexGate.derivedByLesson.get(id) ?? []),
1772
+ ...(assessmentDerived.get(id)?.values() ?? []),
1773
+ ],
1445
1774
  ),
1446
1775
  });
1447
1776
  }
1448
1777
  lessons.sort((a, b) => a.id.localeCompare(b.id));
1778
+ const pages: ManifestPage[] = nonChapterPages
1779
+ .map((page) => ({
1780
+ id: page.id,
1781
+ slug: page.slug,
1782
+ file: page.file,
1783
+ notation: lessonNotationBundle(
1784
+ bundlesByLesson.get(page.id),
1785
+ definitionsById,
1786
+ lessonNotationResolution(
1787
+ page.notation,
1788
+ definitionsById,
1789
+ availableDefinitionIds,
1790
+ ),
1791
+ cardsByKey,
1792
+ indexGate.derivedByLesson.get(page.id),
1793
+ ),
1794
+ }))
1795
+ .sort((a, b) => a.id.localeCompare(b.id));
1449
1796
 
1450
1797
  const consistencyIssues = runConsistencyChecks({
1451
1798
  registry,
1452
- notationInput,
1799
+ notationInput: withPages,
1453
1800
  specs: loadNotationSpecs(),
1454
- catalog,
1455
1801
  lintIgnore: loadLintIgnore(),
1456
- conventions: courseConfig.conventions,
1457
1802
  operators: rules.settings('N8'),
1458
1803
  romanLabels: rules.settings('N10'),
1459
1804
  accents: rules.settings('N22'),
@@ -1528,6 +1873,7 @@ export async function compileManifest(
1528
1873
  disclosure: normalizeDisclosure(courseConfig),
1529
1874
  numbering: numberingSettings,
1530
1875
  blockLabels,
1876
+ cards: normalizeCards(courseConfig),
1531
1877
  about,
1532
1878
  notation: {
1533
1879
  definitions: registry.definitions,
@@ -1543,6 +1889,7 @@ export async function compileManifest(
1543
1889
  sidebar,
1544
1890
  parts: manifestParts,
1545
1891
  lessons,
1892
+ pages,
1546
1893
  prereqEdges,
1547
1894
  equations: {
1548
1895
  labels: equationValidation.labels,
@@ -1578,7 +1925,7 @@ export async function compileManifest(
1578
1925
  consistency: dropDisabled(consistencyIssues, off),
1579
1926
  equations: dropDisabled(equationValidation.diagnostics, off),
1580
1927
  tables: dropDisabled(tableValidation.diagnostics, off),
1581
- figures: dropDisabled(figureValidation.diagnostics, off),
1928
+ figures: dropDisabled([...figureValidation.diagnostics, ...labKatexDiagnostics], off),
1582
1929
  diagrams: dropDisabled(diagramValidation.diagnostics, off),
1583
1930
  checks: dropDisabled(checkValidation.diagnostics, off),
1584
1931
  examples: dropDisabled(exampleValidation.diagnostics, off),