explico 0.8.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +1239 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +2 -2
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1096 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1471 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -69,6 +69,12 @@ import {
69
69
  type DiagramDiagnostic,
70
70
  type DiagramLabelRecord,
71
71
  } from '../reference/diagrams-validate';
72
+ import {
73
+ validateExamples,
74
+ type ExampleDiagnostic,
75
+ type ExampleLabelRecord,
76
+ } from '../reference/examples-validate';
77
+ import { validateFolds, type FoldDiagnostic } from '../reference/folds-validate';
72
78
  import {
73
79
  validateKnowledgeChecks,
74
80
  type KnowledgeCheckDiagnostic,
@@ -87,8 +93,21 @@ import {
87
93
  gateProseNotation,
88
94
  gateProseNotationInAssessments,
89
95
  } from '../reference/gate-prose-notation';
96
+ import {
97
+ gateMathPunctuation,
98
+ gateMathPunctuationInAssessments,
99
+ } from '../reference/gate-math-punctuation';
90
100
  import { gateAbbreviations } from '../reference/gate-abbreviations';
101
+ import { gateCourseRules } from '../reference/gate-course-rules';
91
102
  import { gateHeadingLength } from '../reference/gate-heading-length';
103
+ import { gateHeadingStructure } from '../reference/gate-heading-structure';
104
+ import { gatePartPages } from '../reference/gate-part-pages';
105
+ import {
106
+ scanSectionHeadings,
107
+ scanTitledBlocks,
108
+ } from '../reference/sections.mjs';
109
+ import { knowledgeCheckAnchorId } from '../reference/knowledge-checks.mjs';
110
+ import { validateSectionRefs } from '../reference/sections-validate';
92
111
  import {
93
112
  splitMixedContent,
94
113
  type ContentSegment,
@@ -109,12 +128,30 @@ import type {
109
128
  } from '../reference/types';
110
129
  import {
111
130
  coursePartsOf,
131
+ normalizeBlockLabels,
112
132
  normalizeLessonExports,
113
- normalizeDisclosureDefaults,
133
+ normalizeDisclosure,
134
+ normalizeNumbering,
135
+ type BlockLabels,
114
136
  type CourseConfig,
115
137
  type NormalizedLessonExports,
116
- type NormalizedDisclosureDefaults,
138
+ type NormalizedDisclosure,
139
+ type NormalizedNumbering,
117
140
  } from './course-config';
141
+ import {
142
+ disabledCodes,
143
+ dropDisabled,
144
+ partPageLimits,
145
+ resolveRules,
146
+ rulesSnapshot,
147
+ type ConfigDiagnostic,
148
+ type ManifestRules,
149
+ } from '../rules/settings';
150
+ import {
151
+ applyWaivers,
152
+ collectWaivers,
153
+ type WaiverDiagnostic,
154
+ } from '../rules/waivers';
118
155
  import {
119
156
  resolveKnowledgeVersion,
120
157
  type KnowledgeVersionDiagnostic,
@@ -125,6 +162,7 @@ import {
125
162
  loadNotationDefinitions,
126
163
  loadNotationRegistryInput,
127
164
  loadNotationSpecs,
165
+ loadPartOverviews,
128
166
  loadSourceRecords,
129
167
  } from './collections';
130
168
  import {
@@ -190,8 +228,70 @@ import {
190
228
  * optional advice. Unused keys/definitions and restructuring candidates move
191
229
  * from `warning` to `info`; their diagnostic codes and container fields stay
192
230
  * unchanged.
231
+ * v17 (part pages, additive): `parts` — every part with its chapters in
232
+ * reading order and, where the course authored one, its overview page. A part
233
+ * with an overview page also gains a leading `Overview` item in its
234
+ * `sidebar` group, so `sidebar[].items` is now a mixed list of lesson slugs
235
+ * and `{ label, link, attrs? }` entries rather than one kind per group.
236
+ * Also `lessons[].studyMinutes` from the optional frontmatter field, and
237
+ * `parts[].studyMinutes` totalling it across a part whose chapters all
238
+ * state one.
239
+ *
240
+ * v18 (rule settings, additive): `rules` — the course's rule switches
241
+ * (`disabled`) and every rule's effective settings; `diagnostics.config` —
242
+ * findings about the course settings themselves (a deprecated top-level
243
+ * `operators` / `romanLabels` / `abbreviations` block, a block for a retired
244
+ * rule), from `resolveRules`. A diagnostic owned by a disabled rule is not
245
+ * published at all; one covered by a per-place waiver
246
+ * (`{/* explico-allow N9: reason *\/}`) is dropped too, and
247
+ * `diagnostics.waivers` reports the waivers themselves (malformed, invalid,
248
+ * unused). Every math-gate finding now carries a `code`
249
+ * (`prose-notation`, `abbreviation-unexplained`, `title-length`,
250
+ * `unresolved-glyph`, `math-compile-error`, or the resolver's own).
251
+ *
252
+ * v19 (course rules, additive): `rules.course` — the course's own `X` rules
253
+ * from `courseRules` in the settings, validated, in file order, so
254
+ * `build/rules-in-effect.md` and `content rules` list them beside the engine's.
255
+ * A finding from one is in `diagnostics.math` with the rule's ID as its `code`.
256
+ *
257
+ * v20 (outline numbering): `numbering` — the course's resolved
258
+ * `numbering` settings; `lessons[].headings` — every top-level heading with
259
+ * its anchor, text, level and, down to `numbering.sections.depth`, its
260
+ * `‹part›.‹chapter›.‹section›…` number; `lessons[].numbering.prefix` — the
261
+ * float-number prefix. Under the default `numbering.floats: "chapter"` that
262
+ * prefix is `‹part›.‹chapter›` and every per-kind offset is 0, so a float
263
+ * number is `2.2.1` where it used to be `2.1`. A `[[sec-…]]` that names no
264
+ * heading is a `sec-ref-resolves` error in `diagnostics.math`.
265
+ *
266
+ * v21 (one caption pair): `lessons[].equations` / `.tables` / `.figures` /
267
+ * `.diagrams` entries, and the `labels` of `equations` / `tables` / `figures`
268
+ * / `diagrams`, carry the float's `short` caption (an equation's
269
+ * `\caption{…}`, or the `short` prop) and, for a JSX float, its long
270
+ * `caption`, each when stated as a literal. `diagnostics.figures` /
271
+ * `diagnostics.diagrams` gain `fig-removed-prop` / `dia-removed-prop`.
272
+ *
273
+ * v22 (one collapse model): `disclosure` replaces `disclosureDefaults` — the
274
+ * course's whole `disclosure` settings block with the engine defaults applied
275
+ * (`NormalizedDisclosure`): `sections` / `subsections` / `knowledgeChecks`
276
+ * as `{ collapsible, defaultOpen }`, `notation` / `references` / `memos` /
277
+ * `figures` / `diagrams` / `workedExamples` as `{ defaultOpen }`, and
278
+ * `tables.truncateAfter`.
279
+ *
280
+ * v23 (kind badges): a worked-example set is a numbered kind of its own —
281
+ * `examples` (same shape as `figures`), `lessons[].examples`,
282
+ * `lessons[].numbering.examples` and `diagnostics.examples` (`ex-…` codes,
283
+ * `ex-missing-prop` among them). `lessons[].knowledgeChecks` entries carry
284
+ * the check's `title`; `lessons[].titledBlocks` lists every check and example
285
+ * set with the anchor and title of the `<h2>` it draws and the heading it
286
+ * follows; `blockLabels` is the course's resolved badge form.
287
+ *
288
+ * v24 (figures stop compacting; `<Fold>`): `disclosure` loses `diagrams` —
289
+ * a diagram no longer folds by itself — and gains `folds`, the first-visit
290
+ * state of a `<Fold>`; `disclosure.figures` now says whether a lab's tools
291
+ * start open; `diagnostics.folds` holds `fold-unnamed`
292
+ * (docs/plans/figures-and-short-captions.md, Parts 1–2).
193
293
  */
194
- export const MANIFEST_SCHEMA_VERSION = 16;
294
+ export const MANIFEST_SCHEMA_VERSION = 24;
195
295
 
196
296
  const REPOSITORY_ROOT = process.cwd();
197
297
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -275,74 +375,121 @@ export interface ManifestLessonNotation {
275
375
  readonly formulaScope: readonly GlyphScopeEntry[];
276
376
  }
277
377
 
378
+ /**
379
+ * One keyed float of a lesson (v21): its key and number, and its caption pair
380
+ * — `short` (an equation's `\caption{…}`, or the `short` prop) and, for a
381
+ * `<Table>` / `<Figure>` / `<Diagram>`, the long `caption`. Each is present
382
+ * only when the source states it as a literal.
383
+ */
384
+ export interface ManifestLessonFloat {
385
+ readonly key: string;
386
+ readonly number: string;
387
+ readonly short?: string;
388
+ readonly caption?: string;
389
+ }
390
+
391
+ /**
392
+ * A block that draws a heading of its own where its tag stands (v23): a
393
+ * knowledge check (`<AssessmentSet />`) or a worked-example set
394
+ * (`<CompactExamples>`). `anchor` is the heading's id, `title` the author's
395
+ * topical title, `after` the slug of the top-level heading it follows.
396
+ */
397
+ export interface ManifestTitledBlock {
398
+ readonly kind: 'check' | 'example';
399
+ readonly key: string;
400
+ readonly anchor: string;
401
+ readonly title: string;
402
+ readonly after?: string;
403
+ }
404
+
278
405
  export interface ManifestLesson {
279
406
  readonly id: string;
280
407
  /** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
281
408
  readonly slug: string;
282
409
  /**
283
410
  * The 1-based ordinal of the **part** this lesson sits in — its sidebar
284
- * group's position in `courseConfig.parts`. It is the number before the dot
285
- * in every equation, table, figure, diagram, and knowledge-check number on
286
- * the page: `(2.1)` is the first equation of part 2. See
287
- * `docs/authoring/structure.md`.
411
+ * group's position in `courseConfig.parts`. It is the first number of every
412
+ * section, equation, table, figure, diagram, and knowledge-check number on
413
+ * the page. See `docs/authoring/structure.md`.
288
414
  */
289
415
  readonly part: number;
290
416
  /**
291
417
  * The lesson's 1-based ordinal **within its part** (merged track order, then
292
- * any unranked lessons by slug) — its chapter number. This orders the
293
- * sidebar and the reading path; it does not appear in a float's number.
418
+ * any unranked lessons by slug) — its chapter number, the second number of
419
+ * every section number (`1.2.3`) and, under the default
420
+ * `numbering.floats: "chapter"`, of every float number (`(1.2.1)`).
294
421
  */
295
422
  readonly chapter: number;
296
423
  /**
297
- * How many floats of each kind the earlier lessons of this part already
298
- * used. The lesson's own floats continue from here, so a part's sequence
299
- * runs unbroken across its chapters instead of restarting on every page.
424
+ * The float numbers' `prefix` — `‹part›.‹chapter›` under
425
+ * `numbering.floats: "chapter"`, `‹part›` under `"part"` — and, per kind,
426
+ * how many floats of that kind precede this lesson in that scope: always 0
427
+ * for `chapter`, the earlier chapters' count for `part`, whose sequence
428
+ * runs unbroken across the part. A float is `‹prefix›.‹offset + n›`.
300
429
  */
301
430
  readonly numbering: {
431
+ readonly prefix: string;
302
432
  readonly equations: number;
303
433
  readonly tables: number;
304
434
  readonly figures: number;
305
435
  readonly diagrams: number;
306
436
  readonly checks: number;
437
+ readonly examples: number;
307
438
  };
439
+ /**
440
+ * Every top-level heading in document order (`sections.mjs`): its anchor
441
+ * (`slug`, which the page's heading is given, and which `[[sec-slug]]`
442
+ * names), its text and level, and — for a `##` to the course's
443
+ * `numbering.sections.depth` — its outline number, `‹part›.‹chapter›.‹s›…`.
444
+ */
445
+ readonly headings: readonly {
446
+ readonly slug: string;
447
+ readonly text: string;
448
+ readonly level: number;
449
+ readonly number?: string;
450
+ }[];
308
451
  readonly file: string;
309
452
  readonly title: string;
310
453
  readonly description: string;
454
+ /** Authored reader-facing estimate; `40` and `[30, 40]` both land here. */
455
+ readonly studyMinutes?: { readonly min: number; readonly max: number };
311
456
  readonly status: EditorialStatus;
312
457
  /** Ordered — the curriculum contract. */
313
458
  readonly teaches: readonly string[];
314
459
  readonly requires: readonly string[];
315
460
  readonly assumptions: readonly string[];
316
461
  readonly assessments: readonly string[];
317
- /** Numbered knowledge checks from `assessments`, in `.checks.yml` order. */
462
+ /**
463
+ * Numbered knowledge checks from `assessments`, in `.checks.yml` order,
464
+ * each with the `title` its assessment file gives it (v23) — the topic its
465
+ * heading shows after the engine's badge.
466
+ */
318
467
  readonly knowledgeChecks: readonly {
319
468
  readonly key: string;
320
469
  readonly number: string;
470
+ readonly title?: string;
321
471
  }[];
472
+ /**
473
+ * The blocks that draw an `<h2>` of their own (v23): each knowledge check
474
+ * and worked-example set, in document order, with that heading's `anchor`
475
+ * and `title` and `after`, the slug of the top-level heading it follows
476
+ * (absent before the first) — where "On this page" lists it.
477
+ */
478
+ readonly titledBlocks: readonly ManifestTitledBlock[];
322
479
  readonly sources: readonly string[];
323
480
  readonly citations: readonly ManifestCitation[];
324
481
  /** Competency prerequisite edges this lesson introduces. */
325
482
  readonly prereqEdges: readonly PrereqEdge[];
326
483
  /** Labelled display equations in appearance order (D7). */
327
- readonly equations: readonly {
328
- readonly key: string;
329
- readonly number: string;
330
- }[];
484
+ readonly equations: readonly ManifestLessonFloat[];
331
485
  /** Keyed `<Table>` elements in appearance order. */
332
- readonly tables: readonly {
333
- readonly key: string;
334
- readonly number: string;
335
- }[];
486
+ readonly tables: readonly ManifestLessonFloat[];
336
487
  /** Keyed `<Figure>` elements in appearance order. */
337
- readonly figures: readonly {
338
- readonly key: string;
339
- readonly number: string;
340
- }[];
488
+ readonly figures: readonly ManifestLessonFloat[];
341
489
  /** Keyed `<Diagram>` elements in appearance order. */
342
- readonly diagrams: readonly {
343
- readonly key: string;
344
- readonly number: string;
345
- }[];
490
+ readonly diagrams: readonly ManifestLessonFloat[];
491
+ /** Keyed `<CompactExamples>` sets in appearance order; `short` is the set's title (v23). */
492
+ readonly examples: readonly ManifestLessonFloat[];
346
493
  readonly notation: {
347
494
  readonly bindings: readonly {
348
495
  readonly key: string;
@@ -391,12 +538,46 @@ export interface ManifestAssessmentItemContent {
391
538
  readonly explanationSegments: readonly ContentSegment[];
392
539
  }
393
540
 
541
+ /**
542
+ * One **part** (v17) — a sidebar group and the directory its chapters live in,
543
+ * with its chapters in reading order and its overview page when the course
544
+ * authored one at `content/docs/<dir>/index.mdx`.
545
+ *
546
+ * `link` is where the directory URL resolves; without an `overview` that URL
547
+ * has no page behind it, which is exactly the 404 a part-level page removes.
548
+ */
549
+ export interface ManifestPart {
550
+ readonly dir: string;
551
+ readonly label: string;
552
+ /** 1-based position in `courseConfig.parts` — the float-number prefix. */
553
+ readonly ordinal: number;
554
+ /** Base-relative href of the part's own page, `/<dir>/`. */
555
+ readonly link: string;
556
+ /** Lesson ids in chapter order. */
557
+ readonly lessons: readonly string[];
558
+ /**
559
+ * The part's total study time — present only when **every** chapter in it
560
+ * states one, so a half-annotated part reports the chapters it knows and no
561
+ * misleading sum.
562
+ */
563
+ readonly studyMinutes?: { readonly min: number; readonly max: number };
564
+ readonly overview?: {
565
+ readonly title: string;
566
+ readonly file: string;
567
+ readonly description?: string;
568
+ };
569
+ }
570
+
394
571
  export interface Manifest {
395
572
  readonly schemaVersion: number;
396
573
  /** Normalized build/UI settings for opt-in per-lesson exports. */
397
574
  readonly lessonExports: NormalizedLessonExports;
398
- /** Initial open/closed state for "Page notation" and "References". */
399
- readonly disclosureDefaults: NormalizedDisclosureDefaults;
575
+ /** Every collapse default, engine defaults applied (`CourseConfig.disclosure`). */
576
+ readonly disclosure: NormalizedDisclosure;
577
+ /** Outline and float numbering, defaults applied (`CourseConfig.numbering`). */
578
+ readonly numbering: NormalizedNumbering;
579
+ /** How a check's and an example set's heading names its kind (`CourseConfig.blockLabels`). */
580
+ readonly blockLabels: BlockLabels;
400
581
  /** Reader-facing identity + versions (the `[?]` About panel). */
401
582
  readonly about: ManifestAbout;
402
583
  readonly notation: {
@@ -413,6 +594,8 @@ export interface Manifest {
413
594
  /** Abbreviations table (N14), for `remarkAbbreviations`. Ordered by key. */
414
595
  readonly abbreviations: readonly Record<string, unknown>[];
415
596
  readonly sidebar: readonly SidebarGroup[];
597
+ /** The course's parts in config order, with their chapters and overview pages. */
598
+ readonly parts: readonly ManifestPart[];
416
599
  readonly lessons: readonly ManifestLesson[];
417
600
  /** Full competency prerequisite graph. */
418
601
  readonly prereqEdges: readonly PrereqEdge[];
@@ -442,6 +625,11 @@ export interface Manifest {
442
625
  readonly labels: readonly KnowledgeCheckLabelRecord[];
443
626
  readonly numbersBySlug: Record<string, Record<string, string>>;
444
627
  };
628
+ /** Keyed-float identity for `<CompactExamples>` — same shape as `figures` (v23). */
629
+ readonly examples: {
630
+ readonly labels: readonly ExampleLabelRecord[];
631
+ readonly numbersBySlug: Record<string, Record<string, string>>;
632
+ };
445
633
  /**
446
634
  * Every assessment item's `prompt` / `explanation`, pre-split into
447
635
  * text/math segments (v8). `AssessmentSet.astro` filters this by
@@ -452,6 +640,13 @@ export interface Manifest {
452
640
  * lesson's scope can resolve its glyphs differently.
453
641
  */
454
642
  readonly assessmentContent: readonly ManifestAssessmentItemContent[];
643
+ /**
644
+ * The course's rule switches and settings (`CourseConfig.rules`, resolved
645
+ * against `src/rules/catalog.ts`). `disabled` names the rules whose
646
+ * diagnostics are withheld below; `settings` is every rule's effective
647
+ * values, defaults applied.
648
+ */
649
+ readonly rules: ManifestRules;
455
650
  readonly diagnostics: {
456
651
  readonly curriculum: readonly CurriculumIssue[];
457
652
  readonly notation: readonly NotationDiagnostic[];
@@ -471,12 +666,20 @@ export interface Manifest {
471
666
  readonly diagrams: readonly DiagramDiagnostic[];
472
667
  /** Knowledge-check identity and `[[check-…]]` reference checks. */
473
668
  readonly checks: readonly KnowledgeCheckDiagnostic[];
669
+ /** `<CompactExamples>` identity checks — the keyed-float codes, `ex-` prefixed, plus `ex-missing-prop`. */
670
+ readonly examples: readonly ExampleDiagnostic[];
671
+ /** `<Fold>` checks: `fold-unnamed`, a fold with no `label` and no numbered float to name its bar by. */
672
+ readonly folds: readonly FoldDiagnostic[];
474
673
  /**
475
674
  * A raw Markdown table, a raw HTML `<table>`, or an Explico `<Diagram>`
476
675
  * that is unkeyed or nested outside `<Table>`/`<Figure>` — invisible to
477
676
  * keyed-float validation above. Warnings only.
478
677
  */
479
678
  readonly rawFloats: readonly RawFloatDiagnostic[];
679
+ /** Findings about `CourseConfig.rules` and the deprecated blocks it replaces. Warnings only. */
680
+ readonly config: readonly ConfigDiagnostic[];
681
+ /** Per-place waivers that are malformed, name an unwaivable rule, or silenced nothing. */
682
+ readonly waivers: readonly WaiverDiagnostic[];
480
683
  };
481
684
  readonly hashes: {
482
685
  /** `sha256` of every content source file, keyed by repo-relative path. */
@@ -617,13 +820,45 @@ function lessonNotationBundle(
617
820
  };
618
821
  }
619
822
 
620
- /** `title` / `description` from a lesson's raw docs frontmatter, keyed by id. */
823
+ /**
824
+ * `studyMinutes: 40` and `studyMinutes: [30, 40]` are the same estimate; the
825
+ * manifest carries one shape so a total is a sum of two numbers.
826
+ */
827
+ function studyMinutesRange(
828
+ value: unknown,
829
+ ): { min: number; max: number } | undefined {
830
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
831
+ return { min: value, max: value };
832
+ }
833
+ if (
834
+ Array.isArray(value) &&
835
+ value.length === 2 &&
836
+ value.every((entry) => typeof entry === 'number' && entry > 0)
837
+ ) {
838
+ const [min, max] = value as [number, number];
839
+ return min <= max ? { min, max } : { min: max, max: min };
840
+ }
841
+ return undefined;
842
+ }
843
+
844
+ /** `title` / `description` / `studyMinutes` from a lesson's raw docs frontmatter. */
621
845
  function lessonFrontmatterById(): Map<
622
846
  string,
623
- { title: string; description: string }
847
+ {
848
+ title: string;
849
+ description: string;
850
+ studyMinutes?: { min: number; max: number };
851
+ }
624
852
  > {
625
853
  const base = join(CONTENT_ROOT, 'docs');
626
- const result = new Map<string, { title: string; description: string }>();
854
+ const result = new Map<
855
+ string,
856
+ {
857
+ title: string;
858
+ description: string;
859
+ studyMinutes?: { min: number; max: number };
860
+ }
861
+ >();
627
862
  const walk = (directory: string): void => {
628
863
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
629
864
  const path = join(directory, entry.name);
@@ -637,10 +872,12 @@ function lessonFrontmatterById(): Map<
637
872
  const id = slug.replace(/\.(md|mdx)$/, '').replace(/\//g, '.');
638
873
  const data = (parseFrontmatter(readFileSync(path, 'utf8')).frontmatter ??
639
874
  {}) as Record<string, unknown>;
875
+ const studyMinutes = studyMinutesRange(data.studyMinutes);
640
876
  result.set(id, {
641
877
  title: typeof data.title === 'string' ? data.title : id,
642
878
  description:
643
879
  typeof data.description === 'string' ? data.description : '',
880
+ ...(studyMinutes === undefined ? {} : { studyMinutes }),
644
881
  });
645
882
  }
646
883
  };
@@ -672,6 +909,16 @@ export async function compileManifest(
672
909
  );
673
910
  }
674
911
 
912
+ // Every rule switch and setting, validated against the catalog. Throws on
913
+ // a wrong ID, key, or value — before any gate runs with it.
914
+ const rules = resolveRules(courseConfig);
915
+ // Codes a disabled rule owns — filtered out of every diagnostics group
916
+ // below; the whole-rule gates are skipped at their call site as well.
917
+ const off = disabledCodes(rules);
918
+ // Throws on a bad `numbering` or `blockLabels` value, like `resolveRules` above.
919
+ const numberingSettings = normalizeNumbering(courseConfig);
920
+ const blockLabels = normalizeBlockLabels(courseConfig);
921
+
675
922
  const [catalog, notationInput, abbreviationsInput] = await Promise.all([
676
923
  loadCurriculumCatalog(),
677
924
  loadNotationRegistryInput(),
@@ -697,11 +944,16 @@ export async function compileManifest(
697
944
 
698
945
  const assessmentsInput = catalog.assessments as unknown as {
699
946
  id: string;
947
+ title?: string;
700
948
  items?: { id?: string; prompt?: string; explanation?: string }[];
701
949
  }[];
702
950
 
703
951
  const curriculumIssues = validateCurriculum(catalog);
704
952
  const alignmentIssues = validateNotationAlignment(registry, catalog);
953
+ const mathEndAbbreviations = {
954
+ allow: rules.strings('N12', 'allow'),
955
+ deny: rules.strings('N12', 'deny'),
956
+ };
705
957
  const mathIssues = [
706
958
  ...gateContentMath({
707
959
  notationInput,
@@ -711,14 +963,42 @@ export async function compileManifest(
711
963
  assessmentIds: lesson.assessments,
712
964
  })),
713
965
  }),
714
- ...gateProseNotation(notationInput.lessons),
715
- ...gateProseNotationInAssessments(assessmentsInput),
716
- ...gateAbbreviations({
717
- notationInput,
718
- abbreviations: abbreviationsInput,
719
- courseAbbreviations: courseConfig.abbreviations,
966
+ ...(rules.isEnabled('N9')
967
+ ? [
968
+ ...gateProseNotation(notationInput.lessons, {
969
+ allowSpelledUnits: rules.strings('N9', 'allow'),
970
+ }),
971
+ ...gateProseNotationInAssessments(assessmentsInput, {
972
+ allowSpelledUnits: rules.strings('N9', 'allow'),
973
+ }),
974
+ ]
975
+ : []),
976
+ ...(rules.isEnabled('N12')
977
+ ? [
978
+ ...gateMathPunctuation(notationInput.lessons, mathEndAbbreviations),
979
+ ...gateMathPunctuationInAssessments(assessmentsInput, mathEndAbbreviations),
980
+ ]
981
+ : []),
982
+ ...(rules.isEnabled('N14')
983
+ ? gateAbbreviations({
984
+ notationInput,
985
+ abbreviations: abbreviationsInput,
986
+ courseAbbreviations: rules.settings('N14'),
987
+ })
988
+ : []),
989
+ ...(rules.isEnabled('S1')
990
+ ? gateHeadingLength(notationInput.lessons, {
991
+ maxLength: rules.number('S1', 'maxLength'),
992
+ })
993
+ : []),
994
+ ...gateHeadingStructure(notationInput.lessons, {
995
+ levels: rules.isEnabled('S6'),
996
+ adjacency: rules.isEnabled('S7'),
720
997
  }),
721
- ...gateHeadingLength(notationInput.lessons),
998
+ // The course's own rules (`courseRules`): one pass for every enabled `X`
999
+ // rule that carries a `forbid` pattern. A disabled one, and a guide rule
1000
+ // with no pattern, do no work here.
1001
+ ...gateCourseRules(notationInput.lessons, rules.course),
722
1002
  ];
723
1003
 
724
1004
  // Pre-render every assessment `prompt` / `explanation` into text/math
@@ -780,20 +1060,60 @@ export async function compileManifest(
780
1060
  const lessonSlugs = [...notationInput.lessons]
781
1061
  .map((lesson) => lesson.lessonId.replace(/\./g, '/'))
782
1062
  .sort();
1063
+ const configuredParts = coursePartsOf(courseConfig);
1064
+ const overviewPages = loadPartOverviews(configuredParts);
1065
+ const overviewByDir = new Map(overviewPages.map((page) => [page.dir, page]));
1066
+ // S2: every authored part page carries its bridge and scope paragraphs.
1067
+ if (rules.isEnabled('S2')) {
1068
+ mathIssues.push(
1069
+ ...gatePartPages(
1070
+ overviewPages.map((page) => ({
1071
+ ...page,
1072
+ ordinal: configuredParts.findIndex((part) => part.dir === page.dir) + 1,
1073
+ })),
1074
+ partPageLimits(rules),
1075
+ ),
1076
+ );
1077
+ }
1078
+ // Per-place waivers: drop the located findings each one covers, and
1079
+ // 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.
1081
+ const waiverDiagnostics: WaiverDiagnostic[] = [];
1082
+ const waiverRules = { courseRules: rules.course };
1083
+ const waivers = notationInput.lessons.flatMap((lesson) => {
1084
+ const collected = collectWaivers(lesson, waiverRules);
1085
+ waiverDiagnostics.push(...collected.diagnostics);
1086
+ return collected.waivers;
1087
+ });
1088
+ const waived = applyWaivers(mathIssues, waivers, waiverRules);
1089
+ mathIssues.splice(0, mathIssues.length, ...waived.kept);
1090
+ waiverDiagnostics.push(...waived.unused);
1091
+
783
1092
  const sidebar = buildSidebar(
784
1093
  catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
785
1094
  lessonSlugs,
786
- coursePartsOf(courseConfig),
1095
+ configuredParts.map((part) => ({
1096
+ ...part,
1097
+ ...(overviewByDir.has(part.dir)
1098
+ ? { overviewLink: `/${part.dir}/` }
1099
+ : {}),
1100
+ })),
1101
+ {
1102
+ floatIndex: ['.mdx', '.md'].some((extension) =>
1103
+ existsSync(join(CONTENT_ROOT, 'docs', `figures-and-tables${extension}`)),
1104
+ ),
1105
+ },
787
1106
  );
788
1107
 
789
1108
  // Part and chapter, read straight off the sidebar so there is one
790
1109
  // lesson-ordering source and not two: a part is a sidebar group (parts in
791
1110
  // config order), a chapter is a lesson's position inside its part (merged
792
- // track order, then slug). The part is what prefixes every numbered item —
793
- // `(2.1)` is the first item of one independent kind in part 2 — while `n`
794
- // continues across the part's chapters rather than restarting on each page,
795
- // so every lesson also needs the running count that precedes it.
796
- // See `docs/authoring/structure.md`.
1111
+ // track order, then slug). Both prefix every section number (`1.2.3`).
1112
+ // A float number is `‹prefix›.‹n›`, each kind counting on its own: under
1113
+ // `numbering.floats: "chapter"` the prefix is `‹part›.‹chapter›` and `n`
1114
+ // restarts in every chapter; under `"part"` the prefix is the part alone
1115
+ // and `n` runs on across its chapters, so every lesson also needs the
1116
+ // running count that precedes it. See `docs/authoring/structure.md`.
797
1117
  const bodyByLessonId = new Map(
798
1118
  notationInput.lessons.map((lesson) => [lesson.lessonId, lesson.body]),
799
1119
  );
@@ -802,15 +1122,10 @@ export async function compileManifest(
802
1122
  {
803
1123
  part: number;
804
1124
  chapter: number;
805
- numbering: {
806
- equations: number;
807
- tables: number;
808
- figures: number;
809
- diagrams: number;
810
- checks: number;
811
- };
1125
+ numbering: ManifestLesson['numbering'];
812
1126
  }
813
1127
  >();
1128
+ const chapterScoped = numberingSettings.floats === 'chapter';
814
1129
  let partCursor = 0;
815
1130
  for (const group of sidebar) {
816
1131
  const lessonIds = group.items
@@ -818,32 +1133,122 @@ export async function compileManifest(
818
1133
  .map(lessonIdFromSlug);
819
1134
  if (lessonIds.length === 0) continue; // the static Reference group
820
1135
  partCursor += 1;
821
- // Running totals for this part, advanced one chapter at a time.
1136
+ // Running totals for this part, advanced one chapter at a time; they
1137
+ // only reach a number under the part scope.
822
1138
  const used = {
823
1139
  equations: 0,
824
1140
  tables: 0,
825
1141
  figures: 0,
826
1142
  diagrams: 0,
827
1143
  checks: 0,
1144
+ examples: 0,
828
1145
  };
829
1146
  let chapterCursor = 0;
830
1147
  for (const lessonId of lessonIds) {
831
1148
  const body = bodyByLessonId.get(lessonId) ?? '';
1149
+ const chapter = ++chapterCursor;
832
1150
  placementByLessonId.set(lessonId, {
833
1151
  part: partCursor,
834
- chapter: ++chapterCursor,
835
- numbering: { ...used },
1152
+ chapter,
1153
+ numbering: chapterScoped
1154
+ ? {
1155
+ prefix: `${partCursor}.${chapter}`,
1156
+ equations: 0,
1157
+ tables: 0,
1158
+ figures: 0,
1159
+ diagrams: 0,
1160
+ checks: 0,
1161
+ examples: 0,
1162
+ }
1163
+ : { prefix: String(partCursor), ...used },
836
1164
  });
837
1165
  used.equations += countDisplayEquations(body);
838
1166
  used.tables += countKeyedJsxFloats(body, { tagName: 'Table' });
839
1167
  used.figures += countKeyedJsxFloats(body, { tagName: 'Figure' });
840
1168
  used.diagrams += countKeyedJsxFloats(body, { tagName: 'Diagram' });
841
1169
  used.checks += lessonMetaById.get(lessonId)?.assessments.length ?? 0;
1170
+ used.examples += countKeyedJsxFloats(body, { tagName: 'CompactExamples' });
842
1171
  }
843
1172
  }
844
1173
 
1174
+ // The outline: every lesson's top-level headings with their anchors and,
1175
+ // for a placed lesson, their `‹part›.‹chapter›.‹s›…` numbers. A `[[sec-…]]`
1176
+ // must name one of them.
1177
+ const headingsByLessonId = new Map(
1178
+ notationInput.lessons.map((lesson) => {
1179
+ const placement = placementByLessonId.get(lesson.lessonId);
1180
+ return [
1181
+ lesson.lessonId,
1182
+ scanSectionHeadings(lesson.body, {
1183
+ ...(placement
1184
+ ? { prefix: `${placement.part}.${placement.chapter}` }
1185
+ : {}),
1186
+ depth: numberingSettings.sections.depth,
1187
+ }) as ManifestLesson['headings'],
1188
+ ];
1189
+ }),
1190
+ );
1191
+ mathIssues.push(
1192
+ ...validateSectionRefs(
1193
+ notationInput.lessons.map((lesson) => ({
1194
+ slug: lesson.lessonId.replace(/\./g, '/'),
1195
+ file: lesson.source.file,
1196
+ body: lesson.body,
1197
+ ...(lesson.bodyLine === undefined ? {} : { bodyLine: lesson.bodyLine }),
1198
+ headingSlugs: (headingsByLessonId.get(lesson.lessonId) ?? []).map(
1199
+ (heading) => heading.slug,
1200
+ ),
1201
+ })),
1202
+ ),
1203
+ );
1204
+
1205
+ // The parts themselves, in config order, with their chapters in reading
1206
+ // order — the one place a part-level page or a breadcrumb reads, so neither
1207
+ // has to re-derive a part from lesson slugs.
1208
+ const manifestParts: ManifestPart[] = configuredParts.map((part, index) => {
1209
+ const overview = overviewByDir.get(part.dir);
1210
+ const lessonIds = [...placementByLessonId]
1211
+ .filter(([lessonId]) => lessonId.startsWith(`${part.dir}.`))
1212
+ .sort(([, a], [, b]) => a.chapter - b.chapter)
1213
+ .map(([lessonId]) => lessonId);
1214
+ const chapterMinutes = lessonIds.map(
1215
+ (lessonId) => frontmatterById.get(lessonId)?.studyMinutes,
1216
+ );
1217
+ // A total only means something when no chapter is missing from it.
1218
+ const studyMinutes =
1219
+ chapterMinutes.length > 0 &&
1220
+ chapterMinutes.every((range) => range !== undefined)
1221
+ ? chapterMinutes.reduce(
1222
+ (total, range) => ({
1223
+ min: total.min + range!.min,
1224
+ max: total.max + range!.max,
1225
+ }),
1226
+ { min: 0, max: 0 },
1227
+ )
1228
+ : undefined;
1229
+ return {
1230
+ dir: part.dir,
1231
+ label: part.label,
1232
+ ordinal: index + 1,
1233
+ link: `/${part.dir}/`,
1234
+ lessons: lessonIds,
1235
+ ...(studyMinutes === undefined ? {} : { studyMinutes }),
1236
+ ...(overview
1237
+ ? {
1238
+ overview: {
1239
+ title: overview.title,
1240
+ file: overview.file,
1241
+ ...(overview.description === undefined
1242
+ ? {}
1243
+ : { description: overview.description }),
1244
+ },
1245
+ }
1246
+ : {}),
1247
+ };
1248
+ });
1249
+
845
1250
  const floatInputs = (
846
- kind: 'equations' | 'tables' | 'figures' | 'diagrams',
1251
+ kind: 'equations' | 'tables' | 'figures' | 'diagrams' | 'examples',
847
1252
  ) =>
848
1253
  notationInput.lessons.map((lesson) => {
849
1254
  const placement = placementByLessonId.get(lesson.lessonId);
@@ -855,41 +1260,49 @@ export async function compileManifest(
855
1260
  ...(placement
856
1261
  ? {
857
1262
  numbering: {
858
- prefix: placement.part,
1263
+ prefix: placement.numbering.prefix,
859
1264
  offset: placement.numbering[kind],
860
1265
  },
861
1266
  }
862
1267
  : {}),
863
1268
  };
864
1269
  });
1270
+ // `lessons[].equations` / `.tables` / `.figures` / `.diagrams` /
1271
+ // `.examples`: each keyed float of the lesson in appearance order, with its
1272
+ // caption pair (v21) — an example set's `short` is its title.
1273
+ const floatsBySlug = (
1274
+ labels: readonly {
1275
+ readonly slug: string;
1276
+ readonly key: string;
1277
+ readonly number: string;
1278
+ readonly short?: string;
1279
+ readonly caption?: string;
1280
+ }[],
1281
+ ): Map<string, ManifestLessonFloat[]> => {
1282
+ const bySlug = new Map<string, ManifestLessonFloat[]>();
1283
+ for (const { slug, key, number, short, caption } of labels) {
1284
+ const list = bySlug.get(slug) ?? [];
1285
+ list.push({
1286
+ key,
1287
+ number,
1288
+ ...(short === undefined ? {} : { short }),
1289
+ ...(caption === undefined ? {} : { caption }),
1290
+ });
1291
+ bySlug.set(slug, list);
1292
+ }
1293
+ return bySlug;
1294
+ };
865
1295
  const equationValidation = validateEquations(floatInputs('equations'));
866
- const equationsBySlug = new Map(
867
- Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
868
- slug,
869
- Object.entries(table).map(([key, number]) => ({ key, number })),
870
- ]),
871
- );
1296
+ const equationsBySlug = floatsBySlug(equationValidation.labels);
872
1297
  const tableValidation = validateTables(floatInputs('tables'));
873
- const tablesBySlug = new Map(
874
- Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
875
- slug,
876
- Object.entries(table).map(([key, number]) => ({ key, number })),
877
- ]),
878
- );
1298
+ const tablesBySlug = floatsBySlug(tableValidation.labels);
879
1299
  const figureValidation = validateFigures(floatInputs('figures'));
880
- const figuresBySlug = new Map(
881
- Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
882
- slug,
883
- Object.entries(table).map(([key, number]) => ({ key, number })),
884
- ]),
885
- );
1300
+ const figuresBySlug = floatsBySlug(figureValidation.labels);
886
1301
  const diagramValidation = validateDiagrams(floatInputs('diagrams'));
887
- const diagramsBySlug = new Map(
888
- Object.entries(diagramValidation.numbersBySlug).map(([slug, table]) => [
889
- slug,
890
- Object.entries(table).map(([key, number]) => ({ key, number })),
891
- ]),
892
- );
1302
+ const diagramsBySlug = floatsBySlug(diagramValidation.labels);
1303
+ const exampleValidation = validateExamples(floatInputs('examples'));
1304
+ const examplesBySlug = floatsBySlug(exampleValidation.labels);
1305
+ const foldValidation = validateFolds(floatInputs('figures'));
893
1306
  const checkValidation = validateKnowledgeChecks(
894
1307
  notationInput.lessons.map((lesson) => {
895
1308
  const placement = placementByLessonId.get(lesson.lessonId);
@@ -903,7 +1316,7 @@ export async function compileManifest(
903
1316
  ...(placement
904
1317
  ? {
905
1318
  numbering: {
906
- prefix: placement.part,
1319
+ prefix: placement.numbering.prefix,
907
1320
  offset: placement.numbering.checks,
908
1321
  },
909
1322
  }
@@ -911,12 +1324,51 @@ export async function compileManifest(
911
1324
  };
912
1325
  }),
913
1326
  );
1327
+ // Each check with the title its heading shows after the badge (v23).
1328
+ const checkTitleById = new Map(
1329
+ assessmentsInput.map((assessment) => [assessment.id, assessment.title]),
1330
+ );
914
1331
  const checksBySlug = new Map(
915
1332
  Object.entries(checkValidation.numbersBySlug).map(([slug, table]) => [
916
1333
  slug,
917
- Object.entries(table).map(([key, number]) => ({ key, number })),
1334
+ Object.entries(table).map(([key, number]) => {
1335
+ const title = checkTitleById.get(key);
1336
+ return { key, number, ...(title === undefined ? {} : { title }) };
1337
+ }),
918
1338
  ]),
919
1339
  );
1340
+ // The blocks that draw an `<h2>` where their tag stands, placed after the
1341
+ // outline heading they follow — "On this page" lists them there (v23).
1342
+ // `<AssessmentSet />` draws every check of the lesson, or the one its `id`
1343
+ // names; `<CompactExamples>` draws its own set.
1344
+ const titledBlocksFor = (lessonId: string): ManifestTitledBlock[] => {
1345
+ const slug = lessonId.replace(/\./g, '/');
1346
+ const body = bodyByLessonId.get(lessonId) ?? '';
1347
+ const headings = headingsByLessonId.get(lessonId) ?? [];
1348
+ const checks = checksBySlug.get(slug) ?? [];
1349
+ const examples = examplesBySlug.get(slug) ?? [];
1350
+ return scanTitledBlocks(body, ['AssessmentSet', 'CompactExamples']).flatMap(
1351
+ (block: { name: string; heading: number; id?: string }): ManifestTitledBlock[] => {
1352
+ const after = headings[block.heading]?.slug;
1353
+ const place = after === undefined ? {} : { after };
1354
+ if (block.name === 'CompactExamples') {
1355
+ const set = examples.find((example) => example.key === block.id);
1356
+ return set?.short
1357
+ ? [{ kind: 'example' as const, key: set.key, anchor: `ex-${set.key}`, title: set.short, ...place }]
1358
+ : [];
1359
+ }
1360
+ return checks
1361
+ .filter((check) => block.id === undefined || check.key === block.id)
1362
+ .map((check) => ({
1363
+ kind: 'check' as const,
1364
+ key: check.key,
1365
+ anchor: knowledgeCheckAnchorId(check.key),
1366
+ title: check.title ?? check.key,
1367
+ ...place,
1368
+ }));
1369
+ },
1370
+ );
1371
+ };
920
1372
  const rawFloatDiagnostics = validateRawFloats(
921
1373
  notationInput.lessons.map((lesson) => ({
922
1374
  lessonId: lesson.lessonId,
@@ -945,27 +1397,35 @@ export async function compileManifest(
945
1397
  part: placementByLessonId.get(id)?.part ?? 0,
946
1398
  chapter: placementByLessonId.get(id)?.chapter ?? 0,
947
1399
  numbering: placementByLessonId.get(id)?.numbering ?? {
1400
+ prefix: '',
948
1401
  equations: 0,
949
1402
  tables: 0,
950
1403
  figures: 0,
951
1404
  diagrams: 0,
952
1405
  checks: 0,
1406
+ examples: 0,
953
1407
  },
1408
+ headings: headingsByLessonId.get(id) ?? [],
954
1409
  file: notationLesson.source.file,
955
1410
  title: frontmatter.title,
956
1411
  description: frontmatter.description,
1412
+ ...(frontmatter.studyMinutes === undefined
1413
+ ? {}
1414
+ : { studyMinutes: frontmatter.studyMinutes }),
957
1415
  status: meta.status,
958
1416
  teaches: [...teaches],
959
1417
  requires: [...meta.requires],
960
1418
  assumptions: [...meta.assumptions],
961
1419
  assessments: [...meta.assessments],
962
1420
  knowledgeChecks: checksBySlug.get(id.replace(/\./g, '/')) ?? [],
1421
+ titledBlocks: titledBlocksFor(id),
963
1422
  sources: deriveSources(notationLesson.body),
964
1423
  citations: lessonCitations(notationLesson.body),
965
1424
  equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
966
1425
  tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
967
1426
  figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
968
1427
  diagrams: diagramsBySlug.get(id.replace(/\./g, '/')) ?? [],
1428
+ examples: examplesBySlug.get(id.replace(/\./g, '/')) ?? [],
969
1429
  prereqEdges: sortEdges(
970
1430
  teaches.flatMap((to) =>
971
1431
  (prerequisitesById.get(to) ?? [])
@@ -994,12 +1454,9 @@ export async function compileManifest(
994
1454
  catalog,
995
1455
  lintIgnore: loadLintIgnore(),
996
1456
  conventions: courseConfig.conventions,
997
- ...(courseConfig.operators === undefined
998
- ? {}
999
- : { operators: courseConfig.operators }),
1000
- ...(courseConfig.romanLabels === undefined
1001
- ? {}
1002
- : { romanLabels: courseConfig.romanLabels }),
1457
+ operators: rules.settings('N8'),
1458
+ romanLabels: rules.settings('N10'),
1459
+ accents: rules.settings('N22'),
1003
1460
  });
1004
1461
 
1005
1462
  const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
@@ -1068,7 +1525,9 @@ export async function compileManifest(
1068
1525
  return {
1069
1526
  schemaVersion: MANIFEST_SCHEMA_VERSION,
1070
1527
  lessonExports: normalizeLessonExports(courseConfig),
1071
- disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
1528
+ disclosure: normalizeDisclosure(courseConfig),
1529
+ numbering: numberingSettings,
1530
+ blockLabels,
1072
1531
  about,
1073
1532
  notation: {
1074
1533
  definitions: registry.definitions,
@@ -1082,6 +1541,7 @@ export async function compileManifest(
1082
1541
  tracks,
1083
1542
  abbreviations,
1084
1543
  sidebar,
1544
+ parts: manifestParts,
1085
1545
  lessons,
1086
1546
  prereqEdges,
1087
1547
  equations: {
@@ -1104,20 +1564,29 @@ export async function compileManifest(
1104
1564
  labels: checkValidation.labels,
1105
1565
  numbersBySlug: checkValidation.numbersBySlug,
1106
1566
  },
1567
+ examples: {
1568
+ labels: exampleValidation.labels,
1569
+ numbersBySlug: exampleValidation.numbersBySlug,
1570
+ },
1107
1571
  assessmentContent,
1572
+ rules: rulesSnapshot(rules),
1108
1573
  diagnostics: {
1109
- curriculum: curriculumIssues,
1110
- notation: registry.diagnostics,
1111
- alignment: alignmentIssues,
1112
- math: mathIssues,
1113
- consistency: consistencyIssues,
1114
- equations: equationValidation.diagnostics,
1115
- tables: tableValidation.diagnostics,
1116
- figures: figureValidation.diagnostics,
1117
- diagrams: diagramValidation.diagnostics,
1118
- checks: checkValidation.diagnostics,
1119
- knowledge: knowledge.diagnostics,
1120
- rawFloats: rawFloatDiagnostics,
1574
+ curriculum: dropDisabled(curriculumIssues, off),
1575
+ notation: dropDisabled(registry.diagnostics, off),
1576
+ alignment: dropDisabled(alignmentIssues, off),
1577
+ math: dropDisabled(mathIssues, off),
1578
+ consistency: dropDisabled(consistencyIssues, off),
1579
+ equations: dropDisabled(equationValidation.diagnostics, off),
1580
+ tables: dropDisabled(tableValidation.diagnostics, off),
1581
+ figures: dropDisabled(figureValidation.diagnostics, off),
1582
+ diagrams: dropDisabled(diagramValidation.diagnostics, off),
1583
+ checks: dropDisabled(checkValidation.diagnostics, off),
1584
+ examples: dropDisabled(exampleValidation.diagnostics, off),
1585
+ folds: dropDisabled(foldValidation.diagnostics, off),
1586
+ knowledge: dropDisabled(knowledge.diagnostics, off),
1587
+ rawFloats: dropDisabled(rawFloatDiagnostics, off),
1588
+ config: rules.diagnostics,
1589
+ waivers: waiverDiagnostics,
1121
1590
  },
1122
1591
  hashes,
1123
1592
  };