explico 0.8.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +1257 -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 +5 -3
  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 +1152 -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 +1467 -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
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="playground/src/assets/explico-mark-dark.svg">
3
+ <img src="playground/src/assets/explico-mark.svg" alt="" width="88" align="right">
4
+ </picture>
5
+
1
6
  # explico
2
7
 
3
8
  A build-time engine for **prerequisite-aware, notation-rigorous course
@@ -23,6 +28,13 @@ What it gives a course:
23
28
  - **Equation identity** — `\label{eq:key}` gives a display equation a stable
24
29
  key, an appearance-order number, an anchor, and cross-lesson `[[eq-key]]`
25
30
  references.
31
+ - **Outline numbering** — sections number `part.chapter.section` like a book
32
+ (`§ 1.1.2`), floats `part.chapter.n` (`Figure 2.2.1`); `[[sec-…]]` cites a
33
+ section, a heading's chain icon copies its link, and a typed number form
34
+ (`#sec-1.1.2`) lands on the named anchor. A knowledge check and a set of
35
+ worked examples are numbered sections of their own: the author writes the
36
+ topic, the engine the kind's badge and the number (`[[ex-…]]`,
37
+ `[[check-…]]`).
26
38
  - **A KaTeX trust boundary** — build-time HTML + MathML; the trust callback
27
39
  accepts exactly one validated `data-notation-key`. No runtime math, no CDN.
28
40
  - **A deterministic JSON manifest** — the MCP/LMS contract: lessons, notation,
@@ -78,8 +90,10 @@ import remarkNotation, {
78
90
  } from 'explico/reference/remark-notation.mjs';
79
91
  import remarkShortNote from 'explico/reference/remark-short-note.mjs';
80
92
  import remarkCitation from 'explico/reference/remark-citation.mjs';
93
+ import remarkAsides from 'explico/reference/remark-asides.mjs';
81
94
  import { createNotationKatexOptions } from 'explico/reference/katex-options.mjs';
82
95
  import rehypeFailKatexErrors from 'explico/reference/rehype-fail-katex-errors.mjs';
96
+ import rehypeHeadingAnchors from 'explico/reference/rehype-heading-anchors.mjs';
83
97
  import { courseConfig } from './content/course.config.ts';
84
98
 
85
99
  const manifest = await loadManifest(courseConfig);
@@ -93,14 +107,26 @@ const manifest = await loadManifest(courseConfig);
93
107
  // // the build when a catalogued lesson has equations/tables/figures but no
94
108
  // // numbering or cross-page entry, instead of silently numbering it by
95
109
  // // `##` section while references to it read the part scheme.
96
- // [remarkNotation, createRemarkNotationOptions(manifest, { strict: true })],
97
- // [remarkCitation, { sources: manifest.sources }],
110
+ // // `base` is the Astro `base` of a subpath deployment (e.g. a GitHub
111
+ // // Pages project site); the links these plugins write 404 without it.
112
+ // [remarkNotation, createRemarkNotationOptions(manifest, { strict: true, base })],
113
+ // // The generated References section's first state, from the course's
114
+ // // `disclosure` settings block (`CourseConfig.disclosure`).
115
+ // [remarkCitation, {
116
+ // sources: manifest.sources,
117
+ // defaultOpen: courseConfig.disclosure?.references?.defaultOpen === true,
118
+ // }],
119
+ // // Last: renders `:::note` / `:::tip` / `:::caution` / `:::danger` as
120
+ // // Starlight's `<Aside>` (Starlight's own pass skips `content/docs/`).
121
+ // remarkAsides,
98
122
  // ]
99
123
  // … integrations: [explicoContentRefresh(courseConfig, manifest),
100
124
  // starlight({ customCss: ['explico/styles/global.css'],
101
125
  // components: { Footer: 'explico/components/starlight/LessonFooter.astro',
102
126
  // SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro', … } })
103
- // … rehypePlugins: [[rehypeKatex, createNotationKatexOptions()], rehypeFailKatexErrors]
127
+ // … rehypePlugins: [[rehypeKatex, createNotationKatexOptions()], rehypeFailKatexErrors,
128
+ // // Heading collapse defaults: the same `disclosure` block.
129
+ // [rehypeHeadingAnchors, { defaultCollapsible: courseConfig.disclosure }]]
104
130
  ```
105
131
 
106
132
  `loadManifest(courseConfig)` always recompiles current content before Astro
@@ -109,18 +135,18 @@ and restarts the development server when any file under `content/` is added,
109
135
  changed, or removed, keeping Markdown transforms and UI components on the same
110
136
  model without an editor-specific task.
111
137
 
112
- A memo is a substantial optional derivation: it behaves like a collapsible,
113
- numbered subsection without adding a heading to the page outline. Cite it from
114
- earlier prose with `[[memo-id]]`, then place the block wherever it best fits.
115
- The citation, the block's own number, and its chain icon link to the same URL
116
- fragment:
138
+ A memo is a substantial optional derivation: it folds like a `######`
139
+ section without adding a heading to the page outline. Cite it from earlier
140
+ prose with `[[memo-id]]` — a superscript letter — then place the block
141
+ wherever it best fits. The citation and the chain icon on its bar link to the
142
+ same URL fragment:
117
143
 
118
144
  ```mdx
119
145
  The omitted calculation is recorded in [[memo-full-check]].
120
146
 
121
147
  Several paragraphs may intervene.
122
148
 
123
- <Memo id="full-check" title="Full check" description="The omitted algebra and evidence.">
149
+ <Memo id="full-check" short="Full check" caption="The omitted algebra and evidence">
124
150
 
125
151
  The longer derivation can contain paragraphs, equations, tables, and figures.
126
152
 
@@ -140,9 +166,24 @@ Differentiate the polynomial, set the result to zero, and solve.
140
166
  </ShortNote>. But symmetry is quicker.
141
167
  ```
142
168
 
143
- Both components require literal `id`, `title`, and `description` strings.
144
- Memos start collapsed and persist reader state unless `rememberState={false}`;
145
- short notes always start collapsed.
169
+ A memo requires literal `id` and `caption` strings (`short` is optional); a
170
+ short note requires `id`, `title` and `description`. Memos start as the
171
+ course's `disclosure.memos` says (closed by default) and persist reader state
172
+ unless `rememberState={false}`; short notes always start collapsed.
173
+
174
+ A figure or a diagram never folds by itself. To let a reader fold one away
175
+ where it stands — or any stretch of content — import
176
+ `explico/components/fold/Fold.astro` and wrap it in `<Fold>`: no heading,
177
+ number or letter, only a bar with the section chevron. The bar is its `label`
178
+ or, without one, the numbers and short captions of the floats inside
179
+ ("Diagram 2.1.1 Steps of the derivation"). Folds start as the course's
180
+ `disclosure.folds` says (closed by default), or as `defaultOpen` says:
181
+
182
+ ```mdx
183
+ <Fold>
184
+ <Diagram id="steps" caption="…" short="Steps of the derivation" nodes={…} edges={…} />
185
+ </Fold>
186
+ ```
146
187
 
147
188
  ### Lesson exports
148
189
 
@@ -176,9 +217,13 @@ viewer’s system color scheme.
176
217
 
177
218
  The **Print / save as PDF** action calls the browser's native print dialog; it
178
219
  does not download or build a `.pdf` file itself. The print layout removes site
179
- chrome, expands disclosures and complete data surfaces without changing saved
180
- reader state, hides controls that have no paper equivalent while retaining their
181
- current visual result, and keeps linked text clickable with canonical destinations.
220
+ chrome and prints every disclosure as the reader left it on screen: an open
221
+ section, figure, memo, or question prints in full, and a closed one prints its
222
+ title and a "(content collapsed — full text: …)" note with its permalink.
223
+ Complete data surfaces (every table row, every worked example of an open set)
224
+ still print in full. It does all this without changing saved reader state,
225
+ hides controls that have no paper equivalent while retaining their current
226
+ visual result, and keeps linked text clickable with canonical destinations.
182
227
  Paper size, margins, browser-added headers/footers, final filename, and the
183
228
  choice to save as PDF remain browser and reader settings.
184
229
 
@@ -1,6 +1,8 @@
1
1
  import { AstroConfig, AstroIntegration } from 'astro';
2
- import { CourseConfig } from './course-config.js';
2
+ import { C as CourseConfig } from '../course-config-Dsnri9Tk.js';
3
3
  import { Manifest } from './manifest.js';
4
+ import '../rules/catalog.js';
5
+ import '../rules/course-rules.js';
4
6
  import '../curriculum/validation.js';
5
7
  import '../reference/consistency.js';
6
8
  import './collections.js';
@@ -11,11 +13,14 @@ import '../reference/tables-validate.js';
11
13
  import '../reference/keyed-float-validate.js';
12
14
  import '../reference/figures-validate.js';
13
15
  import '../reference/diagrams-validate.js';
16
+ import '../reference/examples-validate.js';
17
+ import '../reference/folds-validate.js';
14
18
  import '../reference/knowledge-checks-validate.js';
15
19
  import '../reference/raw-floats-validate.js';
16
20
  import '../reference/gate-math.js';
17
21
  import '../reference/mixed-content.js';
18
22
  import '../reference/gloss.js';
23
+ import '../rules/waivers.js';
19
24
  import './knowledge-version.js';
20
25
  import './lesson-derivation.js';
21
26
 
@@ -1,9 +1,12 @@
1
1
  import { DiagnosticSeverity } from '../reference/types.js';
2
2
  import { EquationLink } from '../reference/gate-math.js';
3
3
  import { Manifest, ManifestLesson } from './manifest.js';
4
+ import { RulesFilter, RuleInEffect } from '../rules/in-effect.js';
4
5
  import '../curriculum/validation.js';
5
6
  import '../reference/consistency.js';
6
- import './course-config.js';
7
+ import '../course-config-Dsnri9Tk.js';
8
+ import '../rules/catalog.js';
9
+ import '../rules/course-rules.js';
7
10
  import './collections.js';
8
11
  import '../reference/lint-ignore.js';
9
12
  import '../reference/equations-validate.js';
@@ -11,10 +14,13 @@ import '../reference/tables-validate.js';
11
14
  import '../reference/keyed-float-validate.js';
12
15
  import '../reference/figures-validate.js';
13
16
  import '../reference/diagrams-validate.js';
17
+ import '../reference/examples-validate.js';
18
+ import '../reference/folds-validate.js';
14
19
  import '../reference/knowledge-checks-validate.js';
15
20
  import '../reference/raw-floats-validate.js';
16
21
  import '../reference/mixed-content.js';
17
22
  import '../reference/gloss.js';
23
+ import '../rules/waivers.js';
18
24
  import './knowledge-version.js';
19
25
  import './lesson-derivation.js';
20
26
 
@@ -56,6 +62,12 @@ declare class ContentCliError extends Error {
56
62
  /** One normalized diagnostic. `code` is `<source>:<kind>` and always stable. */
57
63
  interface CliDiagnostic {
58
64
  readonly code: string;
65
+ /**
66
+ * The catalog entry that owns the code — a rule ID (`N9`) or an invariant
67
+ * slug (`keyed-floats`). Absent only for a code the catalog does not know,
68
+ * which `tests/rules/catalog.test.ts` treats as a bug.
69
+ */
70
+ readonly rule?: string;
59
71
  readonly severity: DiagnosticSeverity;
60
72
  readonly message: string;
61
73
  readonly file?: string;
@@ -123,6 +135,7 @@ interface StatusReport {
123
135
  readonly keyedTables: number;
124
136
  readonly keyedFigures: number;
125
137
  readonly keyedDiagrams: number;
138
+ readonly keyedExamples: number;
126
139
  };
127
140
  readonly draftDebt: {
128
141
  readonly lessons: readonly string[];
@@ -150,6 +163,23 @@ interface WarningsReport {
150
163
  declare function contentDiagnostics(manifest: Manifest, minimumSeverity?: DiagnosticThreshold): DiagnosticsReport;
151
164
  /** Every warning exactly; errors and informational advice are excluded. */
152
165
  declare function contentWarnings(manifest: Manifest): WarningsReport;
166
+ interface RulesReport {
167
+ readonly filter: RulesFilter;
168
+ readonly count: number;
169
+ readonly rules: readonly RuleInEffect[];
170
+ /** The same selection as the page `validate:content` writes. */
171
+ readonly markdown: string;
172
+ }
173
+ /**
174
+ * Parse `--element a,b` / `--enforcement script` into a filter, throwing a
175
+ * usage error on a value outside the vocabulary.
176
+ */
177
+ declare function parseRulesFilter(input: {
178
+ readonly element?: string;
179
+ readonly enforcement?: string;
180
+ }): RulesFilter;
181
+ /** The rules this course has in effect, optionally narrowed. */
182
+ declare function contentRules(manifest: Manifest, filter?: RulesFilter): RulesReport;
153
183
  interface ContextReport {
154
184
  readonly lesson: {
155
185
  readonly id: string;
@@ -196,9 +226,23 @@ interface ContextReport {
196
226
  key: string;
197
227
  number: string;
198
228
  }[];
229
+ /** Worked-example sets: `short` is the title `[[ex-key]]` names. */
230
+ readonly examples: readonly {
231
+ key: string;
232
+ number: string;
233
+ short?: string;
234
+ }[];
199
235
  readonly knowledgeChecks: readonly {
200
236
  key: string;
201
237
  number: string;
238
+ title?: string;
239
+ }[];
240
+ /** The top-level headings: `slug` is what `[[sec-slug]]` names. */
241
+ readonly sections: readonly {
242
+ slug: string;
243
+ text: string;
244
+ level: number;
245
+ number?: string;
202
246
  }[];
203
247
  readonly sources: readonly {
204
248
  id: string;
@@ -242,4 +286,4 @@ interface Scaffold {
242
286
  declare function scaffoldLesson(rawId: string): Scaffold;
243
287
  declare function scaffoldTerm(rawKey: string): Scaffold;
244
288
 
245
- export { type CheckLessonResult, type CheckReport, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DomainFileSignatures, type RenderResult, type Scaffold, type StatusReport, type TokenCounts, type WarningsReport, changedLessonIds, contentCheck, contentContext, contentDiagnostics, contentStatus, contentWarnings, estimateTokens, findLesson, lessonDiagnostics, normalizeLessonId, renderLesson, scaffoldLesson, scaffoldTerm };
289
+ export { type CheckLessonResult, type CheckReport, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DomainFileSignatures, type RenderResult, type RulesReport, type Scaffold, type StatusReport, type TokenCounts, type WarningsReport, changedLessonIds, contentCheck, contentContext, contentDiagnostics, contentRules, contentStatus, contentWarnings, estimateTokens, findLesson, lessonDiagnostics, normalizeLessonId, parseRulesFilter, renderLesson, scaffoldLesson, scaffoldTerm };
@@ -18,6 +18,11 @@ import remarkNotation, {
18
18
  } from "../reference/remark-notation.mjs";
19
19
  import remarkShortNote from "../reference/remark-short-note.mjs";
20
20
  import { isLessonSlug, lessonIdFromSlug } from "./lesson-derivation.js";
21
+ import { isCourseRuleId, ownerOf, ELEMENTS } from "../rules/catalog.js";
22
+ import {
23
+ renderRulesInEffect,
24
+ rulesInEffect
25
+ } from "../rules/in-effect.js";
21
26
  const REPOSITORY_ROOT = process.cwd();
22
27
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
23
28
  const DOCS_ROOT = join(CONTENT_ROOT, "docs");
@@ -33,6 +38,15 @@ class ContentCliError extends Error {
33
38
  function diagnosticSeverity(value) {
34
39
  return value === "info" || value === "warning" ? value : "error";
35
40
  }
41
+ function ownerFields(code) {
42
+ const bare = code.slice(code.indexOf(":") + 1);
43
+ const owner = ownerOf(bare);
44
+ if (owner !== void 0) return { rule: owner.id };
45
+ return isCourseRuleId(bare) ? { rule: bare } : {};
46
+ }
47
+ function withOwner(diagnostic) {
48
+ return { ...diagnostic, ...ownerFields(diagnostic.code) };
49
+ }
36
50
  function mentions(message, lessonId, file) {
37
51
  return message.includes(lessonId) || message.includes(file) || message.includes(file.replace(/^content\/docs\//, ""));
38
52
  }
@@ -71,7 +85,7 @@ function lessonDiagnostics(manifest, lesson) {
71
85
  );
72
86
  if (!forLesson) continue;
73
87
  out.push({
74
- code: `math:${diagnostic.code ?? diagnostic.context}`,
88
+ code: `math:${diagnostic.code}`,
75
89
  severity: diagnostic.severity,
76
90
  message: diagnostic.message,
77
91
  file: diagnostic.file,
@@ -96,6 +110,8 @@ function lessonDiagnostics(manifest, lesson) {
96
110
  ...manifest.diagnostics.figures,
97
111
  ...manifest.diagnostics.diagrams,
98
112
  ...manifest.diagnostics.checks,
113
+ ...manifest.diagnostics.examples,
114
+ ...manifest.diagnostics.folds,
99
115
  ...manifest.diagnostics.rawFloats
100
116
  ]) {
101
117
  if (diagnostic.lessonId !== id && diagnostic.file !== file && !mentions(diagnostic.message, id, file)) {
@@ -117,7 +133,7 @@ function lessonDiagnostics(manifest, lesson) {
117
133
  ...withFile(diagnostic.file)
118
134
  });
119
135
  }
120
- return out.sort(
136
+ return out.map(withOwner).sort(
121
137
  (a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message)
122
138
  );
123
139
  }
@@ -306,6 +322,7 @@ function contentStatus(manifest) {
306
322
  const draftNotation = manifest.notation.definitions.filter((definition) => definition.status === "draft").map((definition) => definition.id).sort();
307
323
  const orphans = manifest.notation.diagnostics.filter((diagnostic) => diagnostic.code === "unused-definition").map((diagnostic) => ({
308
324
  code: `notation:${diagnostic.code}`,
325
+ ...ownerFields(diagnostic.code),
309
326
  severity: diagnosticSeverity(diagnostic.severity),
310
327
  message: diagnostic.message,
311
328
  ...diagnostic.source?.file === void 0 ? {} : { file: diagnostic.source.file }
@@ -320,9 +337,13 @@ function contentStatus(manifest) {
320
337
  ...manifest.diagnostics.figures.map((d) => d.severity),
321
338
  ...manifest.diagnostics.diagrams.map((d) => d.severity),
322
339
  ...manifest.diagnostics.checks.map((d) => d.severity),
340
+ ...manifest.diagnostics.examples.map((d) => d.severity),
341
+ ...manifest.diagnostics.folds.map((d) => d.severity),
323
342
  ...manifest.diagnostics.rawFloats.map((d) => d.severity),
324
343
  ...manifest.diagnostics.consistency.map((d) => d.severity),
325
- ...manifest.diagnostics.knowledge.map((d) => d.severity)
344
+ ...manifest.diagnostics.knowledge.map((d) => d.severity),
345
+ ...manifest.diagnostics.config.map((d) => d.severity),
346
+ ...manifest.diagnostics.waivers.map((d) => d.severity)
326
347
  ];
327
348
  const keyedEquations = manifest.lessons.reduce(
328
349
  (count, lesson) => count + lesson.equations.length,
@@ -340,6 +361,10 @@ function contentStatus(manifest) {
340
361
  (count, lesson) => count + lesson.diagrams.length,
341
362
  0
342
363
  );
364
+ const keyedExamples = manifest.lessons.reduce(
365
+ (count, lesson) => count + lesson.examples.length,
366
+ 0
367
+ );
343
368
  const assessments = new Set(
344
369
  manifest.lessons.flatMap((lesson) => lesson.assessments)
345
370
  ).size;
@@ -355,7 +380,8 @@ function contentStatus(manifest) {
355
380
  keyedEquations,
356
381
  keyedTables,
357
382
  keyedFigures,
358
- keyedDiagrams
383
+ keyedDiagrams,
384
+ keyedExamples
359
385
  },
360
386
  draftDebt: { lessons: draftLessons, notation: draftNotation },
361
387
  orphans,
@@ -403,6 +429,7 @@ function contentDiagnostics(manifest, minimumSeverity = "info") {
403
429
  const equation = equationLink(diagnostic.equation);
404
430
  diagnostics.push({
405
431
  code: `${group}:${detail}`,
432
+ ...ownerFields(detail),
406
433
  severity,
407
434
  message: String(diagnostic.message ?? detail),
408
435
  ...file === void 0 ? {} : { file },
@@ -427,6 +454,41 @@ function contentWarnings(manifest) {
427
454
  );
428
455
  return { count: warnings.length, warnings };
429
456
  }
457
+ function parseRulesFilter(input) {
458
+ const filter = {};
459
+ if (input.element !== void 0) {
460
+ const elements = input.element.split(",").map((e) => e.trim()).filter(Boolean);
461
+ for (const element of elements) {
462
+ if (!ELEMENTS.includes(element)) {
463
+ throw new ContentCliError(
464
+ "usage",
465
+ `--element ${element}: not an element. One of: ${ELEMENTS.join(", ")}.`
466
+ );
467
+ }
468
+ }
469
+ filter.elements = elements;
470
+ }
471
+ if (input.enforcement !== void 0) {
472
+ const kinds = ["script", "partial", "guide", "locked"];
473
+ if (!kinds.includes(input.enforcement)) {
474
+ throw new ContentCliError(
475
+ "usage",
476
+ `--enforcement ${input.enforcement}: one of ${kinds.join(", ")}.`
477
+ );
478
+ }
479
+ filter.enforcement = input.enforcement;
480
+ }
481
+ return filter;
482
+ }
483
+ function contentRules(manifest, filter = {}) {
484
+ const rules = rulesInEffect(manifest, filter);
485
+ return {
486
+ filter,
487
+ count: rules.length,
488
+ rules,
489
+ markdown: renderRulesInEffect(manifest, filter)
490
+ };
491
+ }
430
492
  function contentContext(manifest, rawId) {
431
493
  const lesson = findLesson(manifest, rawId);
432
494
  const { frontmatter, body } = lessonRaw(lesson);
@@ -474,7 +536,9 @@ function contentContext(manifest, rawId) {
474
536
  tables: lesson.tables.map((table) => ({ ...table })),
475
537
  figures: lesson.figures.map((figure) => ({ ...figure })),
476
538
  diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
539
+ examples: lesson.examples.map((example) => ({ ...example })),
477
540
  knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
541
+ sections: lesson.headings.map((heading) => ({ ...heading })),
478
542
  sources: lesson.sources.map((id) => {
479
543
  const record = sourceById.get(id);
480
544
  return {
@@ -667,12 +731,14 @@ export {
667
731
  contentCheck,
668
732
  contentContext,
669
733
  contentDiagnostics,
734
+ contentRules,
670
735
  contentStatus,
671
736
  contentWarnings,
672
737
  estimateTokens,
673
738
  findLesson,
674
739
  lessonDiagnostics,
675
740
  normalizeLessonId,
741
+ parseRulesFilter,
676
742
  renderLesson,
677
743
  scaffoldLesson,
678
744
  scaffoldTerm
@@ -1,5 +1,5 @@
1
1
  import { CurriculumCatalog } from '../curriculum/validation.js';
2
- import { NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryInput } from '../reference/types.js';
2
+ import { NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryInput, LocalNotationDefinitionInput, NotationSuspension } from '../reference/types.js';
3
3
 
4
4
  /**
5
5
  * The single path from content files to typed data.
@@ -19,8 +19,41 @@ import { NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryIn
19
19
  * ignoring code fences and inline code, sorted by position.
20
20
  */
21
21
  declare function extractNotationReferences(text: string, file: string, kind: NotationReferenceInput['kind'] | undefined): NotationReferenceInput[];
22
+ /**
23
+ * One part's **overview page** — `content/docs/<dir>/index.mdx`, the page the
24
+ * part's own directory URL resolves to. `isLessonSlug` already excludes any
25
+ * `<dir>/index`, so this page is not a chapter: it carries no competencies, no
26
+ * knowledge check, and no float numbers, and nothing in the curriculum graph
27
+ * changes when a course adds one.
28
+ */
29
+ interface PartOverviewPage {
30
+ readonly dir: string;
31
+ readonly slug: string;
32
+ readonly file: string;
33
+ readonly title: string;
34
+ readonly description?: string;
35
+ /** MDX after the frontmatter — what the S2 gate (`gate-part-pages.ts`) reads. */
36
+ readonly body: string;
37
+ /** 1-based file line `body` starts on, for diagnostic locations. */
38
+ readonly bodyLine: number;
39
+ }
40
+ /**
41
+ * The overview page of every part that has one, keyed by the part's directory.
42
+ * A part without one is simply absent — overview pages are opt-in, one part at
43
+ * a time.
44
+ */
45
+ declare function loadPartOverviews(parts: readonly {
46
+ readonly dir: string;
47
+ }[]): PartOverviewPage[];
22
48
  declare function loadCurriculumCatalog(): Promise<CurriculumCatalog>;
23
49
  declare function loadAbbreviationRegistryInput(): Promise<readonly AbbreviationDefinitionInput[]>;
50
+ /**
51
+ * A lesson's `notation.suspend` (rule N22): decorations that are not
52
+ * operators on this page. A name outside `DECORATION_NAMES`, a decoration
53
+ * suspended twice, and a `notation.local` operator entry for a suspended
54
+ * decoration (the page would contradict itself) each fail the load.
55
+ */
56
+ declare function notationSuspensions(raw: unknown, localDefinitions: readonly LocalNotationDefinitionInput[], file: string): NotationSuspension[];
24
57
  declare function loadNotationRegistryInput(): Promise<NotationRegistryInput>;
25
58
  /**
26
59
  * Notation frontmatter objects for `remark-notation`'s `definitions` option.
@@ -68,4 +101,4 @@ declare function notationSourceLocatorGaps(): {
68
101
  total: number;
69
102
  };
70
103
 
71
- export { type NotationSpec, extractNotationReferences, loadAbbreviationRegistryInput, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadSourceRecords, notationSourceLocatorGaps };
104
+ export { type NotationSpec, type PartOverviewPage, extractNotationReferences, loadAbbreviationRegistryInput, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadPartOverviews, loadSourceRecords, notationSourceLocatorGaps, notationSuspensions };
@@ -3,9 +3,14 @@ import { basename, join, relative } from "node:path";
3
3
  import { parseFrontmatter } from "@astrojs/markdown-remark";
4
4
  import { isSubstantiveMeaning } from "../reference/prose.js";
5
5
  import { parseEquationRef } from "../reference/equations.mjs";
6
+ import {
7
+ DECORATION_NAMES,
8
+ decorationOfEntry
9
+ } from "../reference/math-glyphs.mjs";
6
10
  import { parseKeyedFloatRef } from "../reference/keyed-jsx-floats.mjs";
7
11
  import { parseKnowledgeCheckRef } from "../reference/knowledge-checks.mjs";
8
12
  import { parseMemoRef } from "../reference/remark-memo.mjs";
13
+ import { parseSectionRef } from "../reference/sections.mjs";
9
14
  import { resolveLabel } from "../reference/label.js";
10
15
  import { resolveGlosses } from "../reference/gloss.js";
11
16
  import {
@@ -151,7 +156,7 @@ function extractNotationReferences(text, file, kind) {
151
156
  for (const [pattern, referenceKind] of patterns) {
152
157
  for (const match of searchable.matchAll(pattern)) {
153
158
  const key = (match[1] ?? "").trim();
154
- if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" }) || parseKeyedFloatRef(key, { prefix: "dia" }) || parseKnowledgeCheckRef(key) || parseMemoRef(key)) {
159
+ if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" }) || parseKeyedFloatRef(key, { prefix: "dia" }) || parseKeyedFloatRef(key, { prefix: "ex" }) || parseKnowledgeCheckRef(key) || parseSectionRef(key) || parseMemoRef(key)) {
155
160
  continue;
156
161
  }
157
162
  references.push({
@@ -184,6 +189,29 @@ function lessonCheckIds(lessonPath) {
184
189
  const checksPath = lessonPath.replace(/\.mdx?$/, ".checks.yml");
185
190
  return existsSync(checksPath) ? parseCheckList(readFileSync(checksPath, "utf8")) : [];
186
191
  }
192
+ function loadPartOverviews(parts) {
193
+ const pages = [];
194
+ for (const { dir } of parts) {
195
+ for (const extension of [".mdx", ".md"]) {
196
+ const path = join(CONTENT_ROOT, "docs", dir, `index${extension}`);
197
+ if (!existsSync(path)) continue;
198
+ const { data, file, body, bodyLine } = parseDocument(path);
199
+ const title = typeof data.title === "string" ? data.title : dir;
200
+ const description = typeof data.description === "string" ? data.description : void 0;
201
+ pages.push({
202
+ dir,
203
+ slug: `${dir}/index`,
204
+ file,
205
+ title,
206
+ ...description === void 0 ? {} : { description },
207
+ body,
208
+ bodyLine
209
+ });
210
+ break;
211
+ }
212
+ }
213
+ return pages;
214
+ }
187
215
  function lessonEntries(prerequisitesById) {
188
216
  const lessons = [];
189
217
  for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
@@ -329,6 +357,37 @@ function abbreviationEntries() {
329
357
  async function loadAbbreviationRegistryInput() {
330
358
  return abbreviationEntries();
331
359
  }
360
+ function notationSuspensions(raw, localDefinitions, file) {
361
+ if (raw === void 0) return [];
362
+ if (!Array.isArray(raw)) {
363
+ throw new TypeError(`${file} notation.suspend must be an array`);
364
+ }
365
+ const seen = /* @__PURE__ */ new Set();
366
+ return raw.map((item, index) => {
367
+ const at = `${file} notation.suspend[${index}]`;
368
+ const data = record(item, at);
369
+ const operator = string(data.operator, `${at}.operator`).trim();
370
+ if (!DECORATION_NAMES.has(operator)) {
371
+ throw new Error(
372
+ `${at}.operator ${JSON.stringify(operator)} is not a decoration; use one of ${[...DECORATION_NAMES].join(", ")}`
373
+ );
374
+ }
375
+ if (seen.has(operator)) {
376
+ throw new Error(`${at} suspends ${JSON.stringify(operator)} a second time`);
377
+ }
378
+ seen.add(operator);
379
+ const local = localDefinitions.find(
380
+ (definition) => decorationOfEntry(definition.notation) === operator
381
+ );
382
+ if (local) {
383
+ throw new Error(
384
+ `${at} suspends ${JSON.stringify(operator)}, but notation.local entry ${local.key} explains it as an operator (latex ${JSON.stringify(local.notation)}); drop one of them`
385
+ );
386
+ }
387
+ const reason = optionalString(data.reason, `${at}.reason`);
388
+ return reason === void 0 ? { operator } : { operator, reason: reason.trim() };
389
+ });
390
+ }
332
391
  function notationLessonEntries() {
333
392
  const lessons = [];
334
393
  for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
@@ -337,12 +396,14 @@ function notationLessonEntries() {
337
396
  const { file, data, body, bodyLine } = parseDocument(path);
338
397
  const notation = record(data.notation ?? {}, `${file} notation`);
339
398
  const local = Array.isArray(notation.local) ? notation.local : [];
399
+ const localDefinitions = local.map(
400
+ (entry, index) => localDefinition(entry, file, index)
401
+ );
340
402
  lessons.push({
341
403
  lessonId: lessonIdFromSlug(slug),
342
404
  status: status(data.editorialStatus, `${file} editorialStatus`),
343
- localDefinitions: local.map(
344
- (entry, index) => localDefinition(entry, file, index)
345
- ),
405
+ localDefinitions,
406
+ suspended: notationSuspensions(notation.suspend, localDefinitions, file),
346
407
  references: extractNotationReferences(body, file, void 0),
347
408
  body,
348
409
  bodyLine,
@@ -448,6 +509,8 @@ export {
448
509
  loadNotationDefinitions,
449
510
  loadNotationRegistryInput,
450
511
  loadNotationSpecs,
512
+ loadPartOverviews,
451
513
  loadSourceRecords,
452
- notationSourceLocatorGaps
514
+ notationSourceLocatorGaps,
515
+ notationSuspensions
453
516
  };