explico 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -91,8 +91,11 @@ import {
91
91
  import {
92
92
  gateContentMath,
93
93
  pageScope,
94
+ scopeEntry,
94
95
  type MathGateDiagnostic,
95
96
  } from '../reference/gate-math';
97
+ import { gateIndices, type DerivedUse } from '../reference/gate-indices';
98
+ import { derivedNotationEntries } from '../reference/derived-entries';
96
99
  import {
97
100
  gateProseNotation,
98
101
  gateProseNotationInAssessments,
@@ -107,12 +110,17 @@ import { gateHeadingLength } from '../reference/gate-heading-length';
107
110
  import { gateHeadingStructure } from '../reference/gate-heading-structure';
108
111
  import { gatePartPages } from '../reference/gate-part-pages';
109
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';
110
118
  import {
111
119
  scanSectionHeadings,
112
120
  scanTitledBlocks,
113
121
  } from '../reference/sections.mjs';
114
122
  import { knowledgeCheckAnchorId } from '../reference/knowledge-checks.mjs';
115
- import { validateSectionRefs } from '../reference/sections-validate';
123
+ import { validateHeadingAnchors, validateSectionRefs } from '../reference/sections-validate';
116
124
  import {
117
125
  splitMixedContent,
118
126
  type ContentSegment,
@@ -124,7 +132,6 @@ import type { GlyphScopeEntry } from '../reference/gloss';
124
132
  import { resolveReferenceId } from '../reference/references';
125
133
  import type { NotationGlossEntry } from '../reference/types';
126
134
  import type {
127
- EditorialStatus,
128
135
  NotationBacklink,
129
136
  NotationDefinitionRecord,
130
137
  NotationDiagnostic,
@@ -134,11 +141,13 @@ import type {
134
141
  import {
135
142
  coursePartsOf,
136
143
  normalizeBlockLabels,
144
+ normalizeCards,
137
145
  normalizeLessonExports,
138
146
  normalizeDisclosure,
139
147
  normalizeNumbering,
140
148
  type BlockLabels,
141
149
  type CourseConfig,
150
+ type NormalizedCards,
142
151
  type NormalizedLessonExports,
143
152
  type NormalizedDisclosure,
144
153
  type NormalizedNumbering,
@@ -163,6 +172,7 @@ import {
163
172
  } from './knowledge-version';
164
173
  import {
165
174
  loadAbbreviationRegistryInput,
175
+ loadContentRecords,
166
176
  loadCurriculumCatalog,
167
177
  loadNotationDefinitions,
168
178
  loadNotationRegistryInput,
@@ -302,8 +312,42 @@ import {
302
312
  * `id`, `slug`, `file` and a `notation` bundle of the lesson shape, so the
303
313
  * footer renders the cards its `[[key]]` terms and math open.
304
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`.
305
349
  */
306
- export const MANIFEST_SCHEMA_VERSION = 25;
350
+ export const MANIFEST_SCHEMA_VERSION = 31;
307
351
 
308
352
  const REPOSITORY_ROOT = process.cwd();
309
353
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -373,10 +417,11 @@ export interface ManifestLessonNotation {
373
417
  readonly notation: string;
374
418
  readonly label: string;
375
419
  readonly summary: string;
376
- readonly editorialStatus: EditorialStatus;
377
420
  readonly seeAlso: readonly string[];
378
421
  readonly units?: string;
379
422
  readonly formula?: string;
423
+ /** Rule N23: the set a running index letter runs over, shown on its card (v31). */
424
+ readonly range?: string;
380
425
  /** Symbols the formula names that have no card of their own (D15). */
381
426
  readonly glosses: readonly NotationGlossEntry[];
382
427
  /**
@@ -387,6 +432,27 @@ export interface ManifestLessonNotation {
387
432
  readonly formulaScope: readonly GlyphScopeEntry[];
388
433
  }
389
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
+
390
456
  /**
391
457
  * One keyed float of a lesson (v21): its key and number, and its caption pair
392
458
  * — `short` (an equation's `\caption{…}`, or the `short` prop) and, for a
@@ -456,16 +522,18 @@ export interface ManifestLesson {
456
522
  */
457
523
  readonly headings: readonly {
458
524
  readonly slug: string;
525
+ /** What a reader sees: math as it reads, a `[[key]]` term as its label (v30). */
459
526
  readonly text: string;
460
527
  readonly level: number;
461
528
  readonly number?: string;
529
+ /** The heading has inline math (v30). */
530
+ readonly math?: true;
462
531
  }[];
463
532
  readonly file: string;
464
533
  readonly title: string;
465
534
  readonly description: string;
466
535
  /** Authored reader-facing estimate; `40` and `[30, 40]` both land here. */
467
536
  readonly studyMinutes?: { readonly min: number; readonly max: number };
468
- readonly status: EditorialStatus;
469
537
  /** Ordered — the curriculum contract. */
470
538
  readonly teaches: readonly string[];
471
539
  readonly requires: readonly string[];
@@ -509,6 +577,8 @@ export interface ManifestLesson {
509
577
  }[];
510
578
  readonly definitionIds: readonly string[];
511
579
  readonly definitions: readonly ManifestLessonNotation[];
580
+ /** The derived cards (rule N23) the page's body math uses (v31). */
581
+ readonly derived: readonly ManifestDerivedNotation[];
512
582
  /** Every `[[key]]` / `\explain{key}` occurrence, resolved and located. */
513
583
  readonly resolution: readonly ManifestNotationResolution[];
514
584
  };
@@ -562,6 +632,22 @@ export interface ManifestAssessmentItemContent {
562
632
  readonly itemId: string;
563
633
  readonly promptSegments: readonly ContentSegment[];
564
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 };
565
651
  }
566
652
 
567
653
  /**
@@ -604,6 +690,8 @@ export interface Manifest {
604
690
  readonly numbering: NormalizedNumbering;
605
691
  /** How a check's and an example set's heading names its kind (`CourseConfig.blockLabels`). */
606
692
  readonly blockLabels: BlockLabels;
693
+ /** How the explanation cards start for a reader, defaults applied (`CourseConfig.cards`). */
694
+ readonly cards: NormalizedCards;
607
695
  /** Reader-facing identity + versions (the `[?]` About panel). */
608
696
  readonly about: ManifestAbout;
609
697
  readonly notation: {
@@ -723,7 +811,6 @@ export interface Manifest {
723
811
 
724
812
  // --- compile ---------------------------------------------------------
725
813
 
726
- const CITE_PATTERN = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
727
814
 
728
815
  /** Distinct `[@id; locator]` occurrences in a body, in first-appearance order. */
729
816
  function lessonCitations(body: string): ManifestCitation[] {
@@ -732,13 +819,16 @@ function lessonCitations(body: string): ManifestCitation[] {
732
819
  .replace(/`[^`\n]*`/g, '');
733
820
  const seen = new Set<string>();
734
821
  const citations: ManifestCitation[] = [];
735
- for (const match of withoutCode.matchAll(CITE_PATTERN)) {
736
- const id = (match[1] ?? '').trim();
737
- const locator = (match[2] ?? '').replace(/\s+/g, ' ').trim() || undefined;
738
- const dedupeKey = `${id} ${locator ?? ''}`;
739
- if (seen.has(dedupeKey)) continue;
740
- seen.add(dedupeKey);
741
- 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
+ }
742
832
  }
743
833
  return citations;
744
834
  }
@@ -802,7 +892,9 @@ function lessonNotationResolution(
802
892
  : 'local'
803
893
  : 'unresolved',
804
894
  definitionId: definitionId ?? null,
805
- 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,
806
898
  line: reference.source.line ?? null,
807
899
  column: reference.source.column ?? null,
808
900
  };
@@ -814,11 +906,14 @@ function lessonNotationBundle(
814
906
  definitionsById: ReadonlyMap<string, NotationDefinitionRecord>,
815
907
  resolution: readonly ManifestNotationResolution[],
816
908
  cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
909
+ derivedUses: readonly DerivedUse[] = [],
817
910
  ): ManifestLesson['notation'] {
818
911
  const definitions: ManifestLessonNotation[] = [];
912
+ const records: NotationDefinitionRecord[] = [];
819
913
  for (const definitionId of bundle?.definitionIds ?? []) {
820
914
  const definition = definitionsById.get(definitionId);
821
915
  if (!definition) continue;
916
+ records.push(definition);
822
917
  definitions.push({
823
918
  id: definition.id,
824
919
  key: definition.key,
@@ -826,7 +921,6 @@ function lessonNotationBundle(
826
921
  notation: definition.notation,
827
922
  label: definition.label,
828
923
  summary: definition.summary,
829
- editorialStatus: definition.status,
830
924
  seeAlso: [...definition.seeAlso],
831
925
  glosses: [...definition.glosses],
832
926
  formulaScope: formulaGlyphScope({
@@ -842,12 +936,18 @@ function lessonNotationBundle(
842
936
  ...(definition.formula === undefined
843
937
  ? {}
844
938
  : { formula: definition.formula }),
939
+ ...(definition.range === undefined ? {} : { range: definition.range }),
845
940
  });
846
941
  }
847
942
  return {
848
943
  bindings: [...(bundle?.bindings ?? [])].map((binding) => ({ ...binding })),
849
944
  definitionIds: [...(bundle?.definitionIds ?? [])],
850
945
  definitions,
946
+ derived: derivedNotationEntries(
947
+ derivedUses,
948
+ new Map(records.map((record) => [record.key, record])),
949
+ cardsByKey,
950
+ ),
851
951
  resolution: [...resolution],
852
952
  };
853
953
  }
@@ -975,7 +1075,7 @@ export async function compileManifest(
975
1075
  const cardsByKey = new Map<string, GlyphScopeEntry>(
976
1076
  notationInput.sharedDefinitions.map((definition) => [
977
1077
  definition.key,
978
- { key: definition.key, notation: definition.notation },
1078
+ scopeEntry(definition),
979
1079
  ]),
980
1080
  );
981
1081
  const definitionsById = new Map(
@@ -989,16 +1089,62 @@ export async function compileManifest(
989
1089
  const assessmentsInput = catalog.assessments as unknown as {
990
1090
  id: string;
991
1091
  title?: string;
992
- 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
+ }[];
993
1101
  }[];
994
1102
 
995
- 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
+ ];
996
1111
  const alignmentIssues = validateNotationAlignment(registry, catalog);
997
1112
  const mathEndAbbreviations = {
998
1113
  allow: rules.strings('N12', 'allow'),
999
1114
  deny: rules.strings('N12', 'deny'),
1000
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
+ );
1001
1146
  const mathIssues = [
1147
+ ...indexGate.diagnostics,
1002
1148
  ...gateContentMath({
1003
1149
  notationInput: withPages,
1004
1150
  assessments: assessmentsInput,
@@ -1007,6 +1153,7 @@ export async function compileManifest(
1007
1153
  assessmentIds: lesson.assessments,
1008
1154
  })),
1009
1155
  }),
1156
+ ...gateMdxExpressions(withPages.lessons),
1010
1157
  ...(rules.isEnabled('N9')
1011
1158
  ? [
1012
1159
  ...gateProseNotation(withPages.lessons, {
@@ -1033,6 +1180,7 @@ export async function compileManifest(
1033
1180
  ...(rules.isEnabled('S1')
1034
1181
  ? gateHeadingLength(notationInput.lessons, {
1035
1182
  maxLength: rules.number('S1', 'maxLength'),
1183
+ labelFor: termLabel,
1036
1184
  })
1037
1185
  : []),
1038
1186
  ...gateHeadingStructure(notationInput.lessons, {
@@ -1061,22 +1209,57 @@ export async function compileManifest(
1061
1209
  assessmentsInput.map((assessment) => [assessment.id, assessment]),
1062
1210
  );
1063
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>>();
1064
1215
  for (const lesson of catalog.lessons) {
1065
1216
  if (lesson.assessments.length === 0) continue;
1066
1217
  const lessonInput = notationLessonById.get(lesson.id);
1067
1218
  if (!lessonInput) continue;
1068
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 };
1069
1223
  for (const assessmentId of lesson.assessments) {
1070
1224
  const assessment = assessmentInputById.get(assessmentId);
1071
1225
  if (!assessment?.items) continue;
1072
1226
  for (const item of assessment.items) {
1073
1227
  if (!item.id) continue;
1228
+ const numeric =
1229
+ item.type === 'numeric' &&
1230
+ typeof item.answer?.value === 'number' &&
1231
+ typeof item.answer.tolerance === 'number';
1074
1232
  assessmentContent.push({
1075
1233
  lessonId: lesson.id,
1076
1234
  assessmentId,
1077
1235
  itemId: item.id,
1078
- promptSegments: splitMixedContent(item.prompt ?? '', scope),
1079
- 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
+ : {}),
1080
1263
  });
1081
1264
  }
1082
1265
  }
@@ -1106,9 +1289,44 @@ export async function compileManifest(
1106
1289
  const lessonSlugs = [...notationInput.lessons]
1107
1290
  .map((lesson) => lesson.lessonId.replace(/\./g, '/'))
1108
1291
  .sort();
1109
- const configuredParts = coursePartsOf(courseConfig);
1110
- 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);
1111
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
+ }
1112
1330
  // S2: every authored part page carries its bridge and scope paragraphs.
1113
1331
  if (rules.isEnabled('S2')) {
1114
1332
  mathIssues.push(
@@ -1175,12 +1393,14 @@ export async function compileManifest(
1175
1393
  >();
1176
1394
  const chapterScoped = numberingSettings.floats === 'chapter';
1177
1395
  let partCursor = 0;
1178
- 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;
1179
1400
  const lessonIds = group.items
1180
1401
  .filter((item): item is string => typeof item === 'string')
1181
1402
  .map(lessonIdFromSlug);
1182
- if (lessonIds.length === 0) continue; // the static Reference group
1183
- partCursor += 1;
1403
+ if (lessonIds.length === 0) continue;
1184
1404
  // Running totals for this part, advanced one chapter at a time; they
1185
1405
  // only reach a number under the part scope.
1186
1406
  const used = {
@@ -1232,6 +1452,7 @@ export async function compileManifest(
1232
1452
  ? { prefix: `${placement.part}.${placement.chapter}` }
1233
1453
  : {}),
1234
1454
  depth: numberingSettings.sections.depth,
1455
+ labelFor: (key: string) => termLabel(lesson.lessonId, key),
1235
1456
  }) as ManifestLesson['headings'],
1236
1457
  ];
1237
1458
  }),
@@ -1247,6 +1468,7 @@ export async function compileManifest(
1247
1468
  (heading) => heading.slug,
1248
1469
  ),
1249
1470
  })),
1471
+ nonChapterPages.map((page) => page.slug),
1250
1472
  ),
1251
1473
  );
1252
1474
 
@@ -1385,6 +1607,52 @@ export async function compileManifest(
1385
1607
  }),
1386
1608
  ]),
1387
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
+
1388
1656
  // The blocks that draw an `<h2>` where their tag stands, placed after the
1389
1657
  // outline heading they follow — "On this page" lists them there (v23).
1390
1658
  // `<AssessmentSet />` draws every check of the lesson, or the one its `id`
@@ -1470,7 +1738,6 @@ export async function compileManifest(
1470
1738
  ...(frontmatter.studyMinutes === undefined
1471
1739
  ? {}
1472
1740
  : { studyMinutes: frontmatter.studyMinutes }),
1473
- status: meta.status,
1474
1741
  teaches: [...teaches],
1475
1742
  requires: [...meta.requires],
1476
1743
  assumptions: [...meta.assumptions],
@@ -1500,6 +1767,10 @@ export async function compileManifest(
1500
1767
  availableDefinitionIds,
1501
1768
  ),
1502
1769
  cardsByKey,
1770
+ [
1771
+ ...(indexGate.derivedByLesson.get(id) ?? []),
1772
+ ...(assessmentDerived.get(id)?.values() ?? []),
1773
+ ],
1503
1774
  ),
1504
1775
  });
1505
1776
  }
@@ -1518,6 +1789,7 @@ export async function compileManifest(
1518
1789
  availableDefinitionIds,
1519
1790
  ),
1520
1791
  cardsByKey,
1792
+ indexGate.derivedByLesson.get(page.id),
1521
1793
  ),
1522
1794
  }))
1523
1795
  .sort((a, b) => a.id.localeCompare(b.id));
@@ -1526,9 +1798,7 @@ export async function compileManifest(
1526
1798
  registry,
1527
1799
  notationInput: withPages,
1528
1800
  specs: loadNotationSpecs(),
1529
- catalog,
1530
1801
  lintIgnore: loadLintIgnore(),
1531
- conventions: courseConfig.conventions,
1532
1802
  operators: rules.settings('N8'),
1533
1803
  romanLabels: rules.settings('N10'),
1534
1804
  accents: rules.settings('N22'),
@@ -1603,6 +1873,7 @@ export async function compileManifest(
1603
1873
  disclosure: normalizeDisclosure(courseConfig),
1604
1874
  numbering: numberingSettings,
1605
1875
  blockLabels,
1876
+ cards: normalizeCards(courseConfig),
1606
1877
  about,
1607
1878
  notation: {
1608
1879
  definitions: registry.definitions,