explico 0.5.0 → 0.6.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.
@@ -33,9 +33,141 @@ required config block throws with the section of this file to read.
33
33
 
34
34
  ---
35
35
 
36
+ ## 0.5.0 → 0.6.0
37
+
38
+ ### 1. Rule S1: no heading or float title runs past 65 characters
39
+
40
+ Every Markdown heading (`##` through `######`), a lesson's own
41
+ `frontmatter.title`, and the `title`/`label` prop on `Figure`, `Diagram`,
42
+ `Memo`, and `ShortNote` is now capped at 65 characters; `Table` is exempt (it
43
+ has only a `caption`). No wiring is required — the check runs as part of
44
+ `pnpm validate:content` / `astro build` the same as N9/N13/N15. Audit for
45
+ existing violations and shorten them, moving the rest of the sentence into
46
+ the paragraph after a heading or into a float's `caption`. See
47
+ [`docs/authoring/structure-rules.md#s1`](structure-rules.md#s1).
48
+
49
+ ### 2. Removed markup: `.notation-pin`, `[data-notation-control]`, `.notation-layer-heading`
50
+
51
+ The "Page notation" panel's "Pin explanation" button (`.notation-pin`,
52
+ `[data-notation-control]`) and its separate `<h2>` (`.notation-layer-heading`)
53
+ are both gone from the rendered DOM. Pinning a card now works the same way an
54
+ inline prose symbol always has — click its glyph — and the panel's
55
+ `<summary>` is its only heading. Update any course-owned CSS or end-to-end
56
+ test that targets these selectors; no other action is required.
57
+
58
+ ### 3. Removed prop: `<Diagram description>`
59
+
60
+ `Diagram`'s `description` prop and its collapsed "Diagram description"
61
+ fallback block (`.diagram-fallback`) no longer exist. `astro check` fails on
62
+ an excess prop, so remove every `description="…"` passed to `<Diagram>`; fold
63
+ that text into the diagram's `label` (caption) instead.
64
+
65
+ ### 4. Optional: collapsible "Page notation" panel and References section
66
+
67
+ `NotationLayer` gains `defaultOpen`/`rememberState` props (mirroring
68
+ `Figure`'s own), and `remark-citation.mjs`'s generated References heading now
69
+ accepts a `defaultOpen` plugin option — both ride the same persisted,
70
+ deep-linkable disclosure machinery `Figure`/`CollapsibleSections` already use,
71
+ in place of the previous always-native-default `<details>`. No action is
72
+ required to keep building: both default to closed, matching prior behavior,
73
+ and the References option only takes effect on a course whose
74
+ `astro.config.mjs` already wires `rehypeHeadingAnchors` into its rehype
75
+ pipeline. To opt into a different default, set `CourseConfig.disclosureDefaults`:
76
+
77
+ ```ts
78
+ disclosureDefaults: {
79
+ notation: true, // "Page notation" panel starts open for a reader with nothing saved yet
80
+ references: true, // generated References section starts open
81
+ },
82
+ ```
83
+
84
+ ### 5. Manifest schema is now v14 (additive: v13 → v14)
85
+
86
+ `CourseConfig.disclosureDefaults` is new and additive — nothing existing
87
+ changed shape — but a client asserting on `MANIFEST_SCHEMA_VERSION` by exact
88
+ equality needs the bump.
89
+
90
+ ### 6. Re-run `pnpm validate:content` — the notation auto-annotator's silent-failure bugs are fixed
91
+
92
+ Several shapes that previously either dropped a symbol's tooltip silently (no
93
+ build error) or produced malformed output are now fixed at the resolver
94
+ (`resolveMathGlyphs`), not just documented as traps to avoid: a bare
95
+ `\mathrm{…}`-only symbol (`\mathrm{SSR}`, `\mathrm{SST}`) can now auto-resolve
96
+ at every later bare occurrence, a bare symbol immediately next to an
97
+ `\explain{…}{…}` call no longer has its span corrupted, and a partial
98
+ `\explain{key}{…}` wrap no longer produces unparseable doubled output. No
99
+ course-side wiring changes, but re-running `pnpm validate:content` may surface
100
+ two new classes of error that previously passed silently:
101
+
102
+ - **"Unresolved notation"** for a symbol that was relying on one of the fixed
103
+ bugs to appear resolved when it was not (rare — the bugs mostly hid symbols
104
+ entirely rather than falsely resolving them) or for the uncovered remainder
105
+ of a partial `\explain{key}{…}` wrap. Fix by explaining the symbol, or by
106
+ wrapping the key's *entire* declared `latex`, not just its base glyph.
107
+ - **"Page glyphs … have structurally identical LaTeX"** for two different
108
+ notation keys whose declared `latex` is the same `\operatorname{…}` or
109
+ `\mathrm{…}` form (e.g. two keys both `\operatorname{Var}`) — this check
110
+ previously silently skipped every such glyph and could not catch this. Give
111
+ the colliding keys distinct canonical forms, or reach for
112
+ `\explain{key}{…}` at every occurrence of whichever one keeps the shared
113
+ form.
114
+
36
115
  ## 0.4.x → 0.5.0
37
116
 
38
- ### 1. Rule N14: catalogue every abbreviation used in lesson prose
117
+ ### 1. Rule N9: wrap every plain-text quantity, unit, and operator in `$…$`
118
+
119
+ Every physical quantity, unit abbreviation, or math operator ever typed as
120
+ plain text — in lesson prose, a table cell, or (new in 0.5.0) an assessment
121
+ item's `prompt` / `explanation` string — must now be inside `$…$`. `45 m/s`,
122
+ `t (s)`, `vx`, `\|v\|`, `KE/m` are all the same mistake. This is build-enforced
123
+ with **no `lint-ignore.yml` escape hatch**: `pnpm validate:content` / `astro
124
+ build` fails on every existing violation. Fix per
125
+ [`docs/authoring/notation.md#n9`](notation.md#n9) — wrap the symbol in `$…$`
126
+ and fold its unit into the *same* span as `\text{}` (`$t\ \text{(s)}$`, not two
127
+ adjacent spans).
128
+
129
+ Also wire the accessible-table-header plugin this rule ships with — the build
130
+ works without it, but a table header that is now 100% KaTeX (a bare `$t$`
131
+ column, since its unit moved inside `$…$` too) renders with no
132
+ screen-reader-visible name until this is wired:
133
+
134
+ ```diff
135
+ + import rehypeTableHeaderLabels from 'explico/reference/rehype-table-header-labels.mjs';
136
+ …
137
+ rehypePlugins: [
138
+ [rehypeKatex, createNotationKatexOptions()],
139
+ rehypeFailKatexErrors,
140
+ + rehypeTableHeaderLabels,
141
+ ]
142
+ ```
143
+
144
+ It reads the `\explain{key}{…}` key(s) a header's LaTeX names and backfills an
145
+ `sr-only` label from that key's notation metadata — the one prerequisite is
146
+ that the header's symbol resolves to a key at all, so a `\mathrm{…}` compound
147
+ (`\mathrm{KE}/m`) needs each side explicitly `\explain{}`-wrapped.
148
+
149
+ ### 2. Rule N13: a nonlinear function always takes a parenthesized argument
150
+
151
+ `\sin x`, `\log t`, `\exp\theta` — every trig/inverse-trig/hyperbolic function
152
+ plus `exp`/`log`/`ln`/`lg` — is now a hard error unless its argument is
153
+ parenthesized: `\sin(x)` or `\sin\left(x\right)` is required. Same enforcement
154
+ shape as N9: build-enforced, no `lint-ignore.yml` escape. It fires on lesson
155
+ body math, `notation.formula`, assessment `prompt`/`explanation`, `Diagram`
156
+ formulas, and interactive lab math alike, since all of them resolve through
157
+ the same glyph walker. Fix every occurrence; see
158
+ [`docs/authoring/notation.md#n13`](notation.md#n13).
159
+
160
+ ### 3. Rule N15: size every level of a nested visible delimiter
161
+
162
+ Once a `(`, `[`, `\{`, or `|` sits inside another one of those four — same
163
+ type or mixed, e.g. `(a + [b])` — **every level of that nest** must switch to
164
+ `\left`/`\right` sizing: `\sin(2(90°-\theta))` is now a hard error,
165
+ `\sin\left(2\left(90°-\theta\right)\right)` is required. Same enforcement
166
+ shape as N9/N13: build-enforced, no escape hatch. A single, non-nested bare
167
+ delimiter (`(x)`) is untouched. Fix every nest; see
168
+ [`docs/authoring/notation.md#n15`](notation.md#n15).
169
+
170
+ ### 4. Rule N14: catalogue every abbreviation used in lesson prose
39
171
 
40
172
  Every acronym (`OLS`, `MSE`) or dotted abbreviation (`w.r.t.`, `i.i.d.`) used in
41
173
  plain prose must now be on the shipped baseline
@@ -69,14 +201,65 @@ every `abbreviation-explained` diagnostic by adding a
69
201
  `content/abbreviations/<key>.md` entry or an `allow` entry in
70
202
  `course.config.ts`.
71
203
 
72
- ### 2. Manifest schema is v13 (additive: v10 → v11 → v12 → v13)
204
+ The catalog check above runs regardless of Astro wiring, but the inline
205
+ hover/click card on a lesson page's first occurrence of a catalogued
206
+ abbreviation needs its remark plugin wired in too:
73
207
 
74
- `lessons[].lessonExports`-derived fields (v11), `manifest.abbreviations` (v12),
75
- and `manifest.diagnostics.rawFloats` (v13) are new. A client asserting on
76
- `MANIFEST_SCHEMA_VERSION` by exact equality needs the bump; nothing existing
77
- changed shape.
208
+ ```diff
209
+ + import remarkAbbreviations from 'explico/reference/remark-abbreviations.mjs';
210
+ …
211
+ remarkPlugins: [
212
+ …
213
+ [remarkCitation, { sources: manifest.sources }],
214
+ + [remarkAbbreviations, { abbreviations: manifest.abbreviations }],
215
+ ]
216
+ ```
78
217
 
79
- ### 3. Optional: enable standalone HTML / print lesson exports
218
+ ### 5. Optional: rule N10 warns on an unexplained `\mathrm{…}` label
219
+
220
+ A `\mathrm{…}` multi-letter label (`\mathrm{KE}`, `\mathrm{PV}`) not on the
221
+ shipped roman-label baseline (`base-roman-labels.mjs`) or `course.config.ts`'s
222
+ new `romanLabels.allow` now needs a notation entry the reader can hover — the
223
+ `\mathrm{…}` counterpart to N8. Unlike N9/N13/N14/N15, this is a **Tier-3
224
+ warning** (`roman-label-explained`), not a build failure: it never blocks
225
+ `astro build`, and an existing violation can be silenced with a reviewed
226
+ `lint-ignore.yml` line (`roman-label-explained:KE — <reason>`) while content is
227
+ cleaned up. `pnpm validate:content` surfaces it. See
228
+ [`docs/authoring/notation.md#n10`](notation.md#n10).
229
+
230
+ ### 6. Only if you render `AssessmentRunner` directly (bypassing `AssessmentSet.astro`)
231
+
232
+ `AssessmentRunner`'s `Item` props changed shape: `prompt` / `explanation`
233
+ (`string`) are now `promptContent` / `explanationContent`
234
+ (`MixedContentNode[]`), rendered with the new `RenderedContent` component,
235
+ because `AssessmentSet.astro` now pre-renders assessment math through the same
236
+ KaTeX pipeline as lesson body math instead of handing `AssessmentRunner` a raw
237
+ string. A course that only uses `<AssessmentSet>` needs no change here — this
238
+ only applies to a course that imports and renders `AssessmentRunner` itself.
239
+
240
+ ### 7. Manifest schema is v13 (additive: v7 → v8 → v9 → v10 → v11 → v12 → v13)
241
+
242
+ Every step is additive — nothing existing changed shape — but a client
243
+ asserting on `MANIFEST_SCHEMA_VERSION` by exact equality needs the final
244
+ number:
245
+
246
+ - **v8** — `assessmentContent`: every assessment item's `prompt` /
247
+ `explanation`, pre-split and resolved for `AssessmentSet.astro` (see item 6
248
+ above).
249
+ - **v9** — `diagrams`, `lessons[].diagrams`, `lessons[].numbering.diagrams`,
250
+ `diagnostics.diagrams`: `<Diagram>` becomes a first-class keyed float with
251
+ its own number stream and `[[dia-<id>]]` references.
252
+ - **v10** — `checks`, `lessons[].knowledgeChecks`, `lessons[].numbering.checks`,
253
+ `diagnostics.checks`: numbered, referenceable knowledge checks.
254
+ - **v11** — `lessons[].lessonExports`-derived fields: standalone HTML / print
255
+ export settings (item 8 below).
256
+ - **v12** — `manifest.abbreviations` (rule N14, item 4 above).
257
+ - **v13** — `manifest.diagnostics.rawFloats`: a raw Markdown/HTML table, an
258
+ unkeyed `<Diagram>`, or a `<Diagram>` nested outside
259
+ `<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` — none of these reach
260
+ keyed-float validation, so this diagnostic flags them separately.
261
+
262
+ ### 8. Optional: enable standalone HTML / print lesson exports
80
263
 
81
264
  No action required to keep building. To opt in, add to
82
265
  `content/course.config.ts`:
@@ -89,7 +272,7 @@ lessonExports: {
89
272
  canonicalBaseUrl: 'https://your-course.example.com',
90
273
  ```
91
274
 
92
- ### 4. Optional: turn on strict numbering cross-checks
275
+ ### 9. Optional: turn on strict numbering cross-checks
93
276
 
94
277
  `remarkNotation`'s new `strict: true` option — recommended via
95
278
  `createRemarkNotationOptions(manifest, { strict: true })` — fails the build on
@@ -131,6 +131,61 @@ _How to apply._ See the engine README's notation section and the `notation`
131
131
  collection schema. A letter that exists only so one entry's own `formula` can
132
132
  be stated is a **gloss**, not a card — it creates no curriculum edge.
133
133
 
134
+ Do not satisfy this rule by dumping every `[[key]]` a page uses into one
135
+ trailing "Notation used on this page" (or similarly named) section near the
136
+ end of the lesson. `LessonFooter.astro` already injects an interactive
137
+ "Page notation" panel — built from the page's actual notation manifest, with
138
+ hover/click cards — into every lesson's footer automatically; a hand-written
139
+ list repeats the same term set as flat, non-interactive prose immediately
140
+ above it. Introduce each shared symbol inline, in the sentence where it is
141
+ first defined or used, the way `[[eq-key]]` and other cross-references are
142
+ already used inline elsewhere in these lessons. The completeness gate only
143
+ checks that a `[[key]]`/`\explain{key}` appears *somewhere* in the page body
144
+ (document order does not matter to it), but inline-at-first-use is what keeps
145
+ the rendered page free of a redundant list — and matches what a reader
146
+ actually wants: the symbol explained where it shows up, not gathered into an
147
+ appendix.
148
+
149
+ A page's own `notation.local` entries (declared in frontmatter) are already in
150
+ scope everywhere on that page for the *math completeness gate* — a bare
151
+ occurrence of a local symbol never trips "Unresolved notation" even with zero
152
+ `[[key]]`/`\explain` references. But a **different** check,
153
+ `assertLocalNotationParity` (run at `astro build`, not `pnpm validate:content`),
154
+ still requires every `notation.local` entry to be *referenced* somewhere in
155
+ the body — otherwise the build fails with "Stale Explico manifest for
156
+ ‹lesson›: lesson notation.local has […], but the page notation bundle has
157
+ […]." So every local entry still needs one `\explain{key}{…}` (or `[[key]]`)
158
+ mention, even though the math gate alone wouldn't ask for it.
159
+
160
+ The auto-annotator (`resolveMathGlyphs` in `math-glyphs.mjs`) re-derives every
161
+ `\explain{…}` marker's insertion point from KaTeX's own parsed offsets, then
162
+ re-applies those offsets to the raw source. Three shapes used to corrupt that
163
+ offset math badly enough to either silently drop a symbol (no tooltip, no
164
+ error) or emit unparseable output: a bare roman-label symbol (`\mathrm{SSR}`)
165
+ could never auto-resolve at all regardless of where it sat on the page; a bare
166
+ symbol immediately adjacent to an explicit `\explain{…}{…}` call could have
167
+ its span swallow the whole following command; and a canonical whole-form
168
+ match could be found _through_ an explicit call's own wrapping and extend
169
+ past its span. All three are fixed at the resolver, not just detected — a
170
+ bare `\mathrm{SST}`, `\epsilon\explain{feature-function}{h}`, and similar
171
+ shapes now resolve correctly with no special authoring care required, and
172
+ `gate-math.ts` exercises the same resolver at `pnpm validate:content` time
173
+ (it reuses `resolveMathGlyphs` directly), so a regression here fails the
174
+ build immediately rather than passing silently through to a render-time
175
+ surprise.
176
+
177
+ One related shape is still worth knowing, because it is a genuine authoring
178
+ mistake rather than a resolver bug: a partial `\explain{key}{…}` wrap that
179
+ covers only part of the key's declared `latex`. If `latex: Z_N` and you write
180
+ `\explain{z-statistic}{Z}_N` (base wrapped, subscript left outside), the
181
+ resolver no longer corrupts the output — but the uncovered remainder (`N`)
182
+ has no binding of its own and correctly fails loudly as "Unresolved
183
+ notation," at `pnpm validate:content` time. Wrap the whole declared form —
184
+ `\explain{z-statistic}{Z_N}` — instead; bare `Z_N` elsewhere on the page then
185
+ resolves automatically. The same applies to accent-wrapped forms
186
+ (`latex: \widehat{\theta}`): wrap the accent and its argument together, not
187
+ just the inner letter.
188
+
134
189
  Math authored inside a nested Astro component is outside the lesson MDX scan,
135
190
  so the component must declare the formula text at render time. `Diagram` and
136
191
  `NotationFormula` do this automatically from the notation keys they actually
@@ -0,0 +1,69 @@
1
+ # Structure rules
2
+
3
+ `S` rules govern the shape of a lesson's headings and the short titles on its
4
+ keyed floats — the parts of a page a reader scans before reading prose. This
5
+ is not [structure.md](structure.md), which is the taxonomy these rules (and
6
+ the rest of the engine) speak in, not a rule set itself.
7
+
8
+ See [README.md](README.md) for what "strong recommendation", "Build-enforced",
9
+ and "Review-only" mean here, and how to amend a rule.
10
+
11
+ ---
12
+
13
+ ## S1
14
+
15
+ **No heading, and no figure/diagram/memo/short-note title, runs past 65
16
+ characters.**
17
+
18
+ This caps every Markdown heading (`##` through `######`), a lesson's own
19
+ `frontmatter.title` (rendered as the page's `<h1>`), and the short `title` /
20
+ `label` prop on `Figure`, `Diagram`, `Memo`, and `ShortNote` — the text shown
21
+ on a collapsed title bar, a diagram's caption line, or a chapter's sidebar
22
+ entry. `Table` is exempt: it has only a `caption`, no separate `title`, and
23
+ that caption is the LaTeX-convention full sentence shown above the table.
24
+
25
+ A heading or float title that runs long is usually doing a caption's job. Move
26
+ the rest of the sentence into the paragraph right after a heading, or into the
27
+ float's own `caption`.
28
+
29
+ ```mdx
30
+ <!-- Over the limit: the title carries the whole explanation. -->
31
+ <Figure
32
+ id="ar1-forecast-fan"
33
+ caption="Multi-step AR(1) forecast: the conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
34
+ >
35
+
36
+ <!-- At the limit: a short label, the rest moved into `caption`. -->
37
+ <Figure
38
+ id="ar1-forecast-fan"
39
+ title="Multi-step AR(1) forecast"
40
+ caption="The conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
41
+ >
42
+ ```
43
+
44
+ _Rationale._ A `Figure`'s `title` falls back to `caption` when omitted (see
45
+ `Figure.astro`), so an unset `title` on a long-captioned figure silently
46
+ becomes the text on the always-visible collapsible title bar — the reader
47
+ sees the entire caption twice, once as the "heading" and once again in
48
+ `<figcaption>` once they expand it. The same abuse happens with a heading
49
+ authored as a full sentence instead of a label: the sidebar, the table of
50
+ contents, and every anchor link now carry a paragraph instead of a title.
51
+ Sixty-five characters is roughly one sidebar line at normal zoom before
52
+ Starlight starts wrapping or truncating it.
53
+
54
+ _How to apply._ Write the heading or `title` as a short noun phrase (a topic,
55
+ not a claim about it); put the claim in the prose or the `caption`. If a
56
+ `Figure` needs no distinct title beyond its caption's own first clause,
57
+ extract that clause into `title` explicitly rather than omitting the prop and
58
+ relying on the fallback — an omitted `title` on a long caption is exactly the
59
+ failure this rule exists to catch.
60
+
61
+ _Enforcement._ Build-enforced (`gate-heading-length.ts`, `pnpm
62
+ validate:content`) for every Markdown heading in a lesson body and every
63
+ `Figure`/`Diagram`/`Memo`/`ShortNote` `title`/`label` prop. Review-only for a
64
+ lesson's `frontmatter.title` — the gate scans lesson body text and cannot see
65
+ frontmatter, so a long page title (`<h1>`) is caught by a reviewer, not the
66
+ build.
67
+
68
+ _Exceptions._ None — a heading or float title that must exceed the limit
69
+ should not be a heading or a float title; find the shorter label first.
@@ -53,8 +53,8 @@ the original diagram toward the full in-page region and shrink it back; it is
53
53
  not modal, follows the diagram's document position while the page scrolls, and
54
54
  adds internal vertical scrolling when the viewport is too short. The shrink
55
55
  control or Escape restores its original width. Clicking the title minimizes/
56
- restores the diagram and its text description; this state is remembered per
57
- page by default (`defaultOpen` / `rememberState` configure it).
56
+ restores the diagram; this state is remembered per page by default
57
+ (`defaultOpen` / `rememberState` configure it).
58
58
  The title also carries the same chain-link permalink treatment as headings.
59
59
 
60
60
  By default a laid-out width above `800px` emits a compile-time warning. Treat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explico",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -116,6 +116,43 @@ export interface CourseAbout {
116
116
  readonly changelogHref?: string;
117
117
  }
118
118
 
119
+ /**
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.
127
+ *
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.
131
+ */
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;
137
+ }
138
+
139
+ /** The stable, non-optional form `NormalizedLessonExports` mirrors. */
140
+ export interface NormalizedDisclosureDefaults {
141
+ readonly notation: boolean;
142
+ readonly references: boolean;
143
+ }
144
+
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 {
150
+ return {
151
+ notation: config.disclosureDefaults?.notation === true,
152
+ references: config.disclosureDefaults?.references === true,
153
+ };
154
+ }
155
+
119
156
  /** Optional, build-time lesson artifacts exposed to readers. */
120
157
  export interface LessonExportsConfig {
121
158
  /** Emit one self-contained, offline-capable HTML file per lesson. */
@@ -162,6 +199,9 @@ export interface CourseConfig {
162
199
  readonly abbreviations?: CourseAbbreviations;
163
200
  /** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
164
201
  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;
165
205
  }
166
206
 
167
207
  /** Validate and canonicalize the build-facing lesson-export configuration. */
@@ -88,6 +88,7 @@ import {
88
88
  gateProseNotationInAssessments,
89
89
  } from '../reference/gate-prose-notation';
90
90
  import { gateAbbreviations } from '../reference/gate-abbreviations';
91
+ import { gateHeadingLength } from '../reference/gate-heading-length';
91
92
  import {
92
93
  splitMixedContent,
93
94
  type ContentSegment,
@@ -109,8 +110,10 @@ import type {
109
110
  import {
110
111
  coursePartsOf,
111
112
  normalizeLessonExports,
113
+ normalizeDisclosureDefaults,
112
114
  type CourseConfig,
113
115
  type NormalizedLessonExports,
116
+ type NormalizedDisclosureDefaults,
114
117
  } from './course-config';
115
118
  import {
116
119
  resolveKnowledgeVersion,
@@ -177,8 +180,14 @@ import {
177
180
  * validation and can never get a stable number or `[[…]]` reference; this
178
181
  * flags it (warning-only — the manifest shape a client already reads is
179
182
  * unchanged).
183
+ * v14 (disclosure defaults, additive): `disclosureDefaults` — the resolved
184
+ * initial open/closed state for the "Page notation" panel and the generated
185
+ * "References" section, from `CourseConfig.disclosureDefaults`. Both are
186
+ * engine-authored sections, not written by a lesson, so they get their own
187
+ * default independent of a course's `##` heading default; closed unless the
188
+ * course sets one.
180
189
  */
181
- export const MANIFEST_SCHEMA_VERSION = 13;
190
+ export const MANIFEST_SCHEMA_VERSION = 14;
182
191
 
183
192
  const REPOSITORY_ROOT = process.cwd();
184
193
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -382,6 +391,8 @@ export interface Manifest {
382
391
  readonly schemaVersion: number;
383
392
  /** Normalized build/UI settings for opt-in per-lesson exports. */
384
393
  readonly lessonExports: NormalizedLessonExports;
394
+ /** Initial open/closed state for "Page notation" and "References". */
395
+ readonly disclosureDefaults: NormalizedDisclosureDefaults;
385
396
  /** Reader-facing identity + versions (the `[?]` About panel). */
386
397
  readonly about: ManifestAbout;
387
398
  readonly notation: {
@@ -703,6 +714,7 @@ export async function compileManifest(
703
714
  abbreviations: abbreviationsInput,
704
715
  courseAbbreviations: courseConfig.abbreviations,
705
716
  }),
717
+ ...gateHeadingLength(notationInput.lessons),
706
718
  ];
707
719
 
708
720
  // Pre-render every assessment `prompt` / `explanation` into text/math
@@ -1052,6 +1064,7 @@ export async function compileManifest(
1052
1064
  return {
1053
1065
  schemaVersion: MANIFEST_SCHEMA_VERSION,
1054
1066
  lessonExports: normalizeLessonExports(courseConfig),
1067
+ disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
1055
1068
  about,
1056
1069
  notation: {
1057
1070
  definitions: registry.definitions,
@@ -70,12 +70,6 @@ export interface Props {
70
70
  * Set `false` only when horizontal overflow is deliberate. Default: `800`.
71
71
  */
72
72
  warnAboveWidth?: number | false;
73
- /**
74
- * Prose text alternative for the SVG. When omitted, the fallback is
75
- * generated from the node labels and the edge chain — readable, but a
76
- * hand-written sentence usually reads better for a screen-reader user.
77
- */
78
- description?: string;
79
73
  }
80
74
 
81
75
  const {
@@ -85,7 +79,6 @@ const {
85
79
  edges,
86
80
  scope = [],
87
81
  direction = 'TB',
88
- description,
89
82
  number,
90
83
  anchorId,
91
84
  defaultOpen = true,
@@ -190,12 +183,6 @@ if (warnAboveWidth !== false && layout.width > warnAboveWidth) {
190
183
  );
191
184
  }
192
185
 
193
- /** A node's human label for the text fallback — never the internal id. */
194
- function fallbackLabel(nodeId: string): string {
195
- const node = nodeById.get(nodeId);
196
- return node?.text ?? node?.formula ?? nodeId;
197
- }
198
-
199
186
  function pathFrom(points: readonly { x: number; y: number }[]): string {
200
187
  if (points.length === 0) return '';
201
188
  const [first, ...rest] = points;
@@ -318,33 +305,6 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
318
305
  </div>
319
306
  </div>
320
307
  </div>
321
-
322
- <details class="diagram-fallback">
323
- <summary>Diagram description</summary>
324
- {
325
- description ? (
326
- <p>{description}</p>
327
- ) : (
328
- <Fragment>
329
- <ol>
330
- {rendered.map((node) => (
331
- <li>{fallbackLabel(node.id)}</li>
332
- ))}
333
- </ol>
334
- {edges.length > 0 && (
335
- <p class="diagram-fallback-flow">
336
- {edges.map((edge, index) => (
337
- <Fragment>
338
- {index > 0 && '; '}
339
- {fallbackLabel(edge.from)} → {fallbackLabel(edge.to)}
340
- </Fragment>
341
- ))}
342
- </p>
343
- )}
344
- </Fragment>
345
- )
346
- }
347
- </details>
348
308
  </div>
349
309
  </details>
350
310
 
@@ -635,21 +595,6 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
635
595
  fill: var(--sl-color-gray-4);
636
596
  }
637
597
 
638
- .diagram-fallback {
639
- margin-block-start: 0.5rem;
640
- color: var(--sl-color-gray-3);
641
- font-size: var(--sl-text-sm);
642
- }
643
-
644
- .diagram-fallback ol,
645
- .diagram-fallback p {
646
- margin: 0.35rem 0 0;
647
- }
648
-
649
- .diagram-fallback ol {
650
- padding-inline-start: 1.25rem;
651
- }
652
-
653
598
  @media print {
654
599
  .diagram-toolbar,
655
600
  .diagram__title-anchor,