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
@@ -1,3 +1,6 @@
1
+ import type { CourseRulesConfig } from '../rules/settings';
2
+ import type { CourseRuleDefinitions } from '../rules/course-rules';
3
+
1
4
  /**
2
5
  * The shape `content/course.config.ts` must satisfy (Phase F).
3
6
  *
@@ -10,14 +13,21 @@
10
13
  /**
11
14
  * One **part** — a sidebar group, holding the chapters (lessons) whose slugs
12
15
  * start with `dir`. A part's 1-based position in `CourseConfig.parts` is the
13
- * number before the dot in every equation, table, and figure number inside it.
14
- * See `docs/authoring/structure.md` for the full level taxonomy.
16
+ * first number in every section, equation, table, and figure number inside
17
+ * it. See `docs/authoring/structure.md` for the full level taxonomy.
15
18
  */
16
19
  export interface CoursePart {
17
20
  /** Lesson-slug prefix, e.g. `bonds` for `bonds/yield-to-maturity`. */
18
21
  readonly dir: string;
19
22
  /** Sidebar group heading. */
20
23
  readonly label: string;
24
+ /**
25
+ * The sidebar row that opens this part's **overview page**, when
26
+ * `content/docs/<dir>/index.mdx` exists. Defaults to `Overview`; the page's
27
+ * own title stays the part's name, which would only repeat the group
28
+ * heading one row below it.
29
+ */
30
+ readonly overviewLabel?: string;
21
31
  }
22
32
 
23
33
  /**
@@ -65,6 +75,21 @@ export interface CourseRomanLabels {
65
75
  readonly deny?: readonly string[];
66
76
  }
67
77
 
78
+ /**
79
+ * A course's edits to the shipped accent baseline (`BASE_ACCENTS`, empty),
80
+ * consumed by the `accent-explained` consistency check (rule N22). A
81
+ * decoration a lesson uses (`\hat`, `\bar`, `'`, `*`, …) is fine when it is on
82
+ * the baseline plus `allow` minus `deny`, explained by an operator entry
83
+ * (`latex: \hat{}`), or part of a whole-form entry (`latex: \hat{\beta}`).
84
+ * Same shape as {@link CourseOperators}; set under `rules.N22` only.
85
+ */
86
+ export interface CourseAccents {
87
+ /** Decorations this course treats as universal (`\dot`, `\vec`, …). */
88
+ readonly allow?: readonly string[];
89
+ /** Baseline decorations this course wants explained anyway. */
90
+ readonly deny?: readonly string[];
91
+ }
92
+
68
93
  /**
69
94
  * A course's edits to the shipped abbreviation baseline (`BASE_ABBREVIATIONS`),
70
95
  * consumed by the `abbreviation-explained` gate (rule N14). An abbreviation-
@@ -116,43 +141,235 @@ export interface CourseAbout {
116
141
  readonly changelogHref?: string;
117
142
  }
118
143
 
144
+ /** A heading-like block's collapse default: may it fold, and does it start open? */
145
+ export interface CourseHeadingDisclosure {
146
+ /** Wrap it in a collapsible section. */
147
+ readonly collapsible?: boolean;
148
+ /** Its state for a reader with nothing saved yet; ignored when not collapsible. */
149
+ readonly defaultOpen?: boolean;
150
+ }
151
+
152
+ /** A block that always folds; only its first-visit state is a course choice. */
153
+ export interface CourseOpenDefault {
154
+ readonly defaultOpen?: boolean;
155
+ }
156
+
119
157
  /**
120
- * A course's chosen initial open/closed state for the engine's own built-in
121
- * collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
122
- * and the generated "References" section (`remark-citation.mjs`). Each is
123
- * independent of `defaultCollapsible.sections`/`.subsections` (an author's
124
- * own `##` headings): both of these are engine-authored, not written by a
125
- * lesson, so they need their own default rather than inheriting the site's
126
- * general heading default.
158
+ * Every collapse default a course sets — the `disclosure` block of the course
159
+ * settings, in its own shape (docs/plans/titles-and-containers.md Part 4).
160
+ * Each kind names its default here:
127
161
  *
128
- * `true` starts open, `false` (or omitted) starts closed — the engine's own
129
- * default is closed either way, matching a long panel a reader usually
130
- * doesn't need open on arrival.
162
+ * - **Full collapse**, the section wrapper (`CollapsibleSections.astro`):
163
+ * `sections` (`##`), `subsections` (`###`–`######`), `knowledgeChecks`, the
164
+ * generated `references` section, `memos` (a memo is a `######`-rank
165
+ * section of its own), `folds` (a `<Fold>`, the same without a memo's
166
+ * marker, letter or frame) and `workedExamples` (a set is a `##`-rank
167
+ * section of its own, Part 6). The notation panel (`notation`) folds the
168
+ * same way.
169
+ * - **A float's own fold**, which keeps its picture and caption line:
170
+ * `figures` sets whether a lab's tools start open
171
+ * (docs/plans/figures-and-short-captions.md, decision 5); a table is
172
+ * minimized by hand, and starts truncated when it has more than
173
+ * `tables.truncateAfter` rows. A figure or a diagram never folds by
174
+ * itself (decision 7).
175
+ *
176
+ * A heading's own marker (`[collapsible]`, `[collapsed]`, `[no-collapsible]`)
177
+ * and a component's `defaultOpen` prop override the kind's default at one
178
+ * place. Every collapsible remembers the reader's choice per page.
179
+ * {@link normalizeDisclosure} fills in the engine defaults — the values each
180
+ * kind had before they became settings.
131
181
  */
132
- export interface CourseDisclosureDefaults {
133
- /** Initial state of the "Page notation" panel. Default: closed. */
134
- readonly notation?: boolean;
135
- /** Initial state of the generated "References" section. Default: closed. */
136
- readonly references?: boolean;
182
+ export interface CourseDisclosure {
183
+ readonly sections?: CourseHeadingDisclosure;
184
+ readonly subsections?: CourseHeadingDisclosure;
185
+ readonly knowledgeChecks?: CourseHeadingDisclosure;
186
+ readonly notation?: CourseOpenDefault;
187
+ readonly references?: CourseOpenDefault;
188
+ readonly memos?: CourseOpenDefault;
189
+ /** Whether a `<Fold>` starts open. */
190
+ readonly folds?: CourseOpenDefault;
191
+ /** Whether a lab's tools start open. */
192
+ readonly figures?: CourseOpenDefault;
193
+ readonly workedExamples?: CourseOpenDefault;
194
+ readonly tables?: {
195
+ /** A table with more rows than this starts truncated. Default `6`. */
196
+ readonly truncateAfter?: number;
197
+ };
137
198
  }
138
199
 
139
- /** The stable, non-optional form `NormalizedLessonExports` mirrors. */
140
- export interface NormalizedDisclosureDefaults {
141
- readonly notation: boolean;
142
- readonly references: boolean;
200
+ /** {@link CourseDisclosure} with every default applied. */
201
+ export interface NormalizedDisclosure {
202
+ readonly sections: Required<CourseHeadingDisclosure>;
203
+ readonly subsections: Required<CourseHeadingDisclosure>;
204
+ readonly knowledgeChecks: Required<CourseHeadingDisclosure>;
205
+ readonly notation: Required<CourseOpenDefault>;
206
+ readonly references: Required<CourseOpenDefault>;
207
+ readonly memos: Required<CourseOpenDefault>;
208
+ readonly folds: Required<CourseOpenDefault>;
209
+ readonly figures: Required<CourseOpenDefault>;
210
+ readonly workedExamples: Required<CourseOpenDefault>;
211
+ readonly tables: { readonly truncateAfter: number };
143
212
  }
144
213
 
145
- /** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
146
- * an absent sub-field) resolves to closed, the engine's own default. */
147
- export function normalizeDisclosureDefaults(
148
- config: CourseConfig,
149
- ): NormalizedDisclosureDefaults {
214
+ /**
215
+ * The engine's defaults. Headings do not fold unless the course says so;
216
+ * the notation panel, References, memos, folds and worked examples start
217
+ * closed; a knowledge check folds and starts open; a lab's tools start open;
218
+ * a table over 6 rows starts truncated.
219
+ */
220
+ export const DISCLOSURE_DEFAULTS: NormalizedDisclosure = {
221
+ sections: { collapsible: false, defaultOpen: true },
222
+ subsections: { collapsible: false, defaultOpen: true },
223
+ knowledgeChecks: { collapsible: true, defaultOpen: true },
224
+ notation: { defaultOpen: false },
225
+ references: { defaultOpen: false },
226
+ memos: { defaultOpen: false },
227
+ folds: { defaultOpen: false },
228
+ figures: { defaultOpen: true },
229
+ workedExamples: { defaultOpen: false },
230
+ tables: { truncateAfter: 6 },
231
+ };
232
+
233
+ /** Validate `CourseConfig.disclosure` and apply its defaults. Throws on a bad value. */
234
+ export function normalizeDisclosure(config: CourseConfig): NormalizedDisclosure {
235
+ const disclosure = config.disclosure ?? {};
236
+ if ('diagrams' in disclosure) {
237
+ throw new Error(
238
+ 'CourseConfig disclosure.diagrams is gone: a diagram no longer folds by ' +
239
+ 'itself. Delete the key; to let a reader fold a diagram away, wrap it ' +
240
+ 'in a <Fold> or put it in a collapsible section.',
241
+ );
242
+ }
243
+ const heading = (
244
+ key: 'sections' | 'subsections' | 'knowledgeChecks',
245
+ ): Required<CourseHeadingDisclosure> => ({
246
+ collapsible: disclosure[key]?.collapsible ?? DISCLOSURE_DEFAULTS[key].collapsible,
247
+ defaultOpen: disclosure[key]?.defaultOpen ?? DISCLOSURE_DEFAULTS[key].defaultOpen,
248
+ });
249
+ const open = (
250
+ key: 'notation' | 'references' | 'memos' | 'folds' | 'figures' | 'workedExamples',
251
+ ): Required<CourseOpenDefault> => ({
252
+ defaultOpen: disclosure[key]?.defaultOpen ?? DISCLOSURE_DEFAULTS[key].defaultOpen,
253
+ });
254
+ const truncateAfter =
255
+ disclosure.tables?.truncateAfter ?? DISCLOSURE_DEFAULTS.tables.truncateAfter;
256
+ if (!Number.isInteger(truncateAfter) || truncateAfter < 2) {
257
+ throw new Error(
258
+ `CourseConfig disclosure.tables.truncateAfter must be a whole number of ` +
259
+ `rows, at least 2; received ${JSON.stringify(truncateAfter)}.`,
260
+ );
261
+ }
150
262
  return {
151
- notation: config.disclosureDefaults?.notation === true,
152
- references: config.disclosureDefaults?.references === true,
263
+ sections: heading('sections'),
264
+ subsections: heading('subsections'),
265
+ knowledgeChecks: heading('knowledgeChecks'),
266
+ notation: open('notation'),
267
+ references: open('references'),
268
+ memos: open('memos'),
269
+ folds: open('folds'),
270
+ figures: open('figures'),
271
+ workedExamples: open('workedExamples'),
272
+ tables: { truncateAfter },
153
273
  };
154
274
  }
155
275
 
276
+ /**
277
+ * How the course numbers its outline and its floats (`numbering` in the
278
+ * course settings; docs/authoring/structure.md § Numbering). Every field is
279
+ * optional; {@link normalizeNumbering} fills in the defaults.
280
+ */
281
+ export interface CourseNumbering {
282
+ readonly sections?: {
283
+ /**
284
+ * Show section numbers: on wide screens, in print, and on a collapsed
285
+ * section's bar. Default `true`. Off, `[[sec-…]]` prints the title.
286
+ */
287
+ readonly show?: boolean;
288
+ /** The deepest heading level numbered, `2` (`##`) to `6`. Default `4`. */
289
+ readonly depth?: number;
290
+ };
291
+ /**
292
+ * The scope a float's counter runs over. `chapter` (default) restarts every
293
+ * kind in each chapter and prints `‹part›.‹chapter›.‹n›`; `part` runs on
294
+ * across the part's chapters and prints `‹part›.‹n›`. A plain `string`
295
+ * because the settings arrive as JSON; {@link normalizeNumbering} rejects
296
+ * any other value.
297
+ */
298
+ readonly floats?: string;
299
+ /**
300
+ * Resolve a number-form fragment a reader types (`#sec-1.1.2`,
301
+ * `#fig-2.2.1`) to the named anchor. Default `true`. The site itself only
302
+ * ever links and copies the named anchor.
303
+ */
304
+ readonly aliases?: boolean;
305
+ }
306
+
307
+ /** {@link CourseNumbering} with every default applied. */
308
+ export interface NormalizedNumbering {
309
+ readonly sections: { readonly show: boolean; readonly depth: number };
310
+ readonly floats: 'chapter' | 'part';
311
+ readonly aliases: boolean;
312
+ }
313
+
314
+ /** Validate `CourseConfig.numbering` and apply its defaults. Throws on a bad value. */
315
+ export function normalizeNumbering(config: CourseConfig): NormalizedNumbering {
316
+ const numbering = config.numbering ?? {};
317
+ const depth = numbering.sections?.depth ?? 4;
318
+ if (!Number.isInteger(depth) || depth < 2 || depth > 6) {
319
+ throw new Error(
320
+ `CourseConfig numbering.sections.depth must be a heading level from 2 ` +
321
+ `(##) to 6 (######); received ${JSON.stringify(depth)}.`,
322
+ );
323
+ }
324
+ const floats = numbering.floats ?? 'chapter';
325
+ if (floats !== 'chapter' && floats !== 'part') {
326
+ throw new Error(
327
+ `CourseConfig numbering.floats must be "chapter" or "part"; received ` +
328
+ `${JSON.stringify(floats)}.`,
329
+ );
330
+ }
331
+ return {
332
+ sections: { show: numbering.sections?.show !== false, depth },
333
+ floats,
334
+ aliases: numbering.aliases !== false,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * How a knowledge check's and a worked-example set's heading names its kind
340
+ * (`blockLabels` in the course settings; docs/plans/titles-and-containers.md
341
+ * Q11). The engine draws the kind before the author's title: `icon-word`
342
+ * (the default) shows the kind's icon and its word, "Knowledge check" or
343
+ * "Example"; `icon` shows the icon alone and keeps the word for a screen
344
+ * reader.
345
+ */
346
+ export type BlockLabels = 'icon' | 'icon-word';
347
+
348
+ /** The engine default for {@link CourseConfig.blockLabels}. */
349
+ export const BLOCK_LABELS_DEFAULT: BlockLabels = 'icon-word';
350
+
351
+ /** Validate `CourseConfig.blockLabels` and apply its default. Throws on a bad value. */
352
+ export function normalizeBlockLabels(config: CourseConfig): BlockLabels {
353
+ const labels = config.blockLabels ?? BLOCK_LABELS_DEFAULT;
354
+ if (labels !== 'icon' && labels !== 'icon-word') {
355
+ throw new Error(
356
+ `CourseConfig blockLabels must be "icon" or "icon-word"; received ` +
357
+ `${JSON.stringify(labels)}.`,
358
+ );
359
+ }
360
+ return labels;
361
+ }
362
+
363
+ /**
364
+ * Rule S2's word ranges (`rules.S2.bridgeMinWords` … in the course settings),
365
+ * in the shape `gate-part-pages.ts` takes. Built by `partPageLimits` in
366
+ * `src/rules/settings.ts`.
367
+ */
368
+ export interface NormalizedPartPages {
369
+ readonly bridge: { readonly minWords: number; readonly maxWords: number };
370
+ readonly scope: { readonly minWords: number; readonly maxWords: number };
371
+ }
372
+
156
373
  /** Optional, build-time lesson artifacts exposed to readers. */
157
374
  export interface LessonExportsConfig {
158
375
  /** Emit one self-contained, offline-capable HTML file per lesson. */
@@ -191,17 +408,39 @@ export interface CourseConfig {
191
408
  readonly conventions: CourseConventions;
192
409
  /** Reader-facing identity + knowledge version (the `[?]` About panel). */
193
410
  readonly about: CourseAbout;
194
- /** Edits to the shipped named-operator baseline (rule N8). Optional. */
411
+ /**
412
+ * Per-rule switches and settings, flat by rule ID — only what differs from
413
+ * the defaults (`{ "P5": { "enabled": false }, "S1": { "maxLength": 72 } }`).
414
+ * Resolved and validated by `resolveRules` (`src/rules/settings.ts`); the
415
+ * closed list of IDs and settings is `src/rules/catalog.ts`.
416
+ */
417
+ readonly rules?: CourseRulesConfig;
418
+ /**
419
+ * The course's *own* rules, on top of the engine's — `X1`…`X99`, each with
420
+ * its own text, elements, severity and optional `forbid` patterns. A list,
421
+ * not a map, so the settings form can add and remove one. Resolved and
422
+ * validated by `resolveCourseRules` (`src/rules/course-rules.ts`).
423
+ */
424
+ readonly courseRules?: CourseRuleDefinitions;
425
+ /** @deprecated Moved to `rules.N8` (same shape). Honoured for now; removed in a later MINOR. */
195
426
  readonly operators?: CourseOperators;
196
- /** Edits to the shipped roman-label baseline (rule N10). Optional. */
427
+ /** @deprecated Moved to `rules.N10` (same shape). Honoured for now; removed in a later MINOR. */
197
428
  readonly romanLabels?: CourseRomanLabels;
198
- /** Edits to the shipped abbreviation baseline (rule N14). Optional. */
429
+ /** @deprecated Moved to `rules.N14` (same shape). Honoured for now; removed in a later MINOR. */
199
430
  readonly abbreviations?: CourseAbbreviations;
200
431
  /** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
201
432
  readonly lessonExports?: LessonExportsConfig;
202
- /** Initial open/closed state for the engine's own built-in collapsible
203
- * sections ("Page notation", "References"). Closed when omitted. */
204
- readonly disclosureDefaults?: CourseDisclosureDefaults;
433
+ /** Every collapse default: headings, checks, memos, floats, tables. */
434
+ readonly disclosure?: CourseDisclosure;
435
+ /** Outline and float numbering. The defaults apply when omitted. */
436
+ readonly numbering?: CourseNumbering;
437
+ /**
438
+ * How a check's and an example set's heading names its kind: `icon-word`
439
+ * (default) or `icon` ({@link BlockLabels}). A plain `string` because the
440
+ * settings arrive as JSON; {@link normalizeBlockLabels} rejects any other
441
+ * value.
442
+ */
443
+ readonly blockLabels?: string;
205
444
  }
206
445
 
207
446
  /** Validate and canonicalize the build-facing lesson-export configuration. */
@@ -141,7 +141,7 @@ export function resolveKnowledgeVersion(options: {
141
141
  if (!latest) {
142
142
  warn(
143
143
  'knowledge-changelog-missing',
144
- `${file} has no released \`## [x.y.z]\` entry — only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C4).`,
144
+ `${file} has no released \`## [x.y.z]\` entry — only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C3).`,
145
145
  );
146
146
  return { version: declaredVersion, diagnostics };
147
147
  }
@@ -136,27 +136,48 @@ export function mergeTrackOrder(tracks: readonly TrackLike[]): string[] {
136
136
  export interface SidebarSection {
137
137
  readonly dir: string;
138
138
  readonly label: string;
139
+ readonly overviewLabel?: string;
140
+ /**
141
+ * Href of this part's overview page (`/<dir>/`), set only when the part has
142
+ * one. It becomes the group's first item, so the page a part directory
143
+ * resolves to is reachable from the navigation that names the part.
144
+ */
145
+ readonly overviewLink?: string;
139
146
  }
140
147
 
141
- export type SidebarGroup =
142
- | { readonly label: string; readonly items: readonly string[] }
148
+ /**
149
+ * One sidebar entry: a lesson slug, or an explicit link. `attrs` is Starlight's
150
+ * own passthrough onto the rendered `<a>` — the engine uses it to mark a part's
151
+ * overview row so CSS can give it its own icon.
152
+ */
153
+ export type SidebarItem =
154
+ | string
143
155
  | {
144
156
  readonly label: string;
145
- readonly items: readonly {
146
- readonly label: string;
147
- readonly link: string;
148
- }[];
157
+ readonly link: string;
158
+ readonly attrs?: Readonly<Record<string, string>>;
149
159
  };
150
160
 
161
+ export interface SidebarGroup {
162
+ readonly label: string;
163
+ readonly items: readonly SidebarItem[];
164
+ }
165
+
166
+ /** Marks the overview row so `global.css` can give it its own icon. */
167
+ export const PART_OVERVIEW_CLASS = 'explico-part-overview';
168
+
151
169
  /**
152
170
  * The Starlight `sidebar` config: one group per section, lessons ordered by the
153
171
  * merged track order (ties broken by slug), then the static Reference group.
154
172
  * Replaces seven `autogenerate` blocks that read an authored `sidebar.order`.
173
+ * `floatIndex`: the course has a top-level "Figures and tables" page
174
+ * (`figures-and-tables.mdx`, `FloatIndex.astro`), listed in Reference.
155
175
  */
156
176
  export function buildSidebar(
157
177
  tracks: readonly TrackLike[],
158
178
  lessonSlugs: readonly string[],
159
179
  parts: readonly SidebarSection[],
180
+ { floatIndex = false }: { readonly floatIndex?: boolean } = {},
160
181
  ): SidebarGroup[] {
161
182
  const rank = new Map(
162
183
  mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index]),
@@ -164,21 +185,39 @@ export function buildSidebar(
164
185
  const positionOf = (slug: string) =>
165
186
  rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
166
187
 
167
- const groups: SidebarGroup[] = parts.map(({ dir, label }) => ({
168
- label,
169
- items: lessonSlugs
170
- .filter((slug) => slug.startsWith(`${dir}/`))
171
- .sort(
172
- (left, right) =>
173
- positionOf(left) - positionOf(right) || left.localeCompare(right),
174
- ),
175
- }));
188
+ const groups: SidebarGroup[] = parts.map(
189
+ ({ dir, label, overviewLabel, overviewLink }) => {
190
+ const chapters = lessonSlugs
191
+ .filter((slug) => slug.startsWith(`${dir}/`))
192
+ .sort(
193
+ (left, right) =>
194
+ positionOf(left) - positionOf(right) || left.localeCompare(right),
195
+ );
196
+ // The overview leads the group: it is the part's own page, so it reads
197
+ // as the chapter list's heading rather than an appendix to it, and in
198
+ // compact mode it is the first icon instead of the last.
199
+ return {
200
+ label,
201
+ items: overviewLink
202
+ ? [
203
+ {
204
+ label: overviewLabel ?? 'Overview',
205
+ link: overviewLink,
206
+ attrs: { class: PART_OVERVIEW_CLASS },
207
+ },
208
+ ...chapters,
209
+ ]
210
+ : chapters,
211
+ };
212
+ },
213
+ );
176
214
 
177
215
  groups.push({
178
216
  label: 'Reference',
179
217
  items: [
180
218
  { label: 'Curriculum map', link: '/curriculum-map/' },
181
219
  { label: 'Notation glossary', link: '/glossary/' },
220
+ ...(floatIndex ? [{ label: 'Figures and tables', link: '/figures-and-tables/' }] : []),
182
221
  ],
183
222
  });
184
223
 
@@ -42,6 +42,7 @@ import {
42
42
  writeFileSync,
43
43
  } from 'node:fs';
44
44
  import { join, relative } from 'node:path';
45
+ import { ruleIdOf } from '../rules/catalog';
45
46
 
46
47
  const REPOSITORY_ROOT = process.cwd();
47
48
  /** Sits beside `build/manifest.json` and `build/resolution/` (Phase F). */
@@ -163,14 +164,19 @@ export interface RunLog {
163
164
  * `[code] file:line:column: message`, matching what the console has always
164
165
  * printed, with ` (eq:<key> -> <href>)` appended when the finding belongs to a
165
166
  * keyed equation — so a size-only finding inside a long `aligned` block names
166
- * the result it is about, not just its coordinates.
167
+ * the result it is about, not just its coordinates. A code owned by a lettered
168
+ * rule is prefixed with that rule's ID (`[N9 prose-notation]`), so the line
169
+ * names the rule page to read; an invariant's code stands alone, and a course
170
+ * rule's code *is* its ID (`[X1]`), so it is not repeated.
167
171
  */
168
172
  export function diagnosticLine(diagnostic: AnyDiagnostic): string {
169
173
  const code = diagnostic.code ?? diagnostic.kind;
170
174
  const at = [diagnostic.file, diagnostic.line, diagnostic.column]
171
175
  .filter((part) => part !== undefined)
172
176
  .join(':');
173
- const prefix = code === undefined ? '' : `[${code}] `;
177
+ const owner = code === undefined ? undefined : ruleIdOf(code);
178
+ const rule = owner === undefined || owner === code ? '' : `${owner} `;
179
+ const prefix = code === undefined ? '' : `[${rule}${code}] `;
174
180
  const equation =
175
181
  diagnostic.equation === undefined
176
182
  ? ''