explico 0.2.0 → 0.4.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 (83) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +67 -7
  3. package/dist/compiler/astro.d.ts +30 -0
  4. package/dist/compiler/astro.js +79 -0
  5. package/dist/compiler/cli.d.ts +16 -0
  6. package/dist/compiler/cli.js +43 -9
  7. package/dist/compiler/collections.js +4 -1
  8. package/dist/compiler/course-config.d.ts +80 -4
  9. package/dist/compiler/course-config.js +12 -0
  10. package/dist/compiler/knowledge-version.d.ts +58 -0
  11. package/dist/compiler/knowledge-version.js +80 -0
  12. package/dist/compiler/lesson-derivation.d.ts +1 -1
  13. package/dist/compiler/lesson-derivation.js +2 -2
  14. package/dist/compiler/manifest.d.ts +98 -2
  15. package/dist/compiler/manifest.js +153 -24
  16. package/dist/compiler/resolution-report.d.ts +4 -0
  17. package/dist/reference/base-operators.d.mts +38 -0
  18. package/dist/reference/base-operators.mjs +103 -0
  19. package/dist/reference/consistency.d.ts +7 -2
  20. package/dist/reference/consistency.js +51 -2
  21. package/dist/reference/diagram-layout.d.ts +63 -0
  22. package/dist/reference/diagram-layout.js +122 -0
  23. package/dist/reference/equations-validate.d.ts +10 -0
  24. package/dist/reference/equations-validate.js +1 -1
  25. package/dist/reference/equations.d.mts +80 -13
  26. package/dist/reference/equations.mjs +109 -19
  27. package/dist/reference/figures-validate.d.ts +21 -0
  28. package/dist/reference/figures-validate.js +13 -0
  29. package/dist/reference/keyed-float-validate.d.ts +73 -0
  30. package/dist/reference/keyed-float-validate.js +110 -0
  31. package/dist/reference/keyed-jsx-floats.d.mts +64 -0
  32. package/dist/reference/keyed-jsx-floats.mjs +191 -0
  33. package/dist/reference/math-glyphs.d.mts +18 -0
  34. package/dist/reference/math-glyphs.mjs +92 -0
  35. package/dist/reference/remark-notation.d.mts +61 -0
  36. package/dist/reference/remark-notation.mjs +344 -10
  37. package/dist/reference/tables-validate.d.ts +21 -0
  38. package/dist/reference/tables-validate.js +13 -0
  39. package/dist/reference/units-vocab.js +19 -1
  40. package/docs/VERSIONING.md +66 -0
  41. package/docs/authoring/README.md +100 -0
  42. package/docs/authoring/agents.md +63 -0
  43. package/docs/authoring/migrating.md +169 -0
  44. package/docs/authoring/notation.md +297 -0
  45. package/docs/authoring/structure.md +139 -0
  46. package/docs/authoring/versioning.md +116 -0
  47. package/docs/authoring/visualizations.md +195 -0
  48. package/package.json +20 -5
  49. package/src/compiler/astro.ts +127 -0
  50. package/src/compiler/cli.ts +52 -7
  51. package/src/compiler/collections.ts +12 -3
  52. package/src/compiler/course-config.ts +93 -3
  53. package/src/compiler/knowledge-version.ts +157 -0
  54. package/src/compiler/lesson-derivation.ts +2 -2
  55. package/src/compiler/manifest.ts +320 -26
  56. package/src/components/CurriculumMap.astro +27 -0
  57. package/src/components/diagram/Diagram.astro +317 -0
  58. package/src/components/examples/CompactExamples.astro +1 -1
  59. package/src/components/figure/Figure.astro +104 -0
  60. package/src/components/notation/NotationLayer.astro +11 -0
  61. package/src/components/notation/render-notation-math.ts +13 -2
  62. package/src/components/reference/FloatRefLayer.astro +671 -0
  63. package/src/components/starlight/AboutPanel.astro +255 -0
  64. package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
  65. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  66. package/src/components/starlight/LayoutSidebar.astro +62 -1
  67. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  68. package/src/components/starlight/LessonFooter.astro +15 -2
  69. package/src/components/table/Table.astro +105 -0
  70. package/src/reference/base-operators.mjs +103 -0
  71. package/src/reference/consistency.ts +83 -2
  72. package/src/reference/diagram-layout.ts +246 -0
  73. package/src/reference/equations-validate.ts +8 -1
  74. package/src/reference/equations.mjs +109 -19
  75. package/src/reference/figures-validate.ts +40 -0
  76. package/src/reference/keyed-float-validate.ts +210 -0
  77. package/src/reference/keyed-jsx-floats.mjs +191 -0
  78. package/src/reference/math-glyphs.mjs +92 -0
  79. package/src/reference/remark-notation.mjs +344 -10
  80. package/src/reference/tables-validate.ts +40 -0
  81. package/src/reference/units-vocab.ts +18 -0
  82. package/src/styles/global.css +244 -7
  83. package/src/components/starlight/EquationEnhancer.astro +0 -87
@@ -0,0 +1,100 @@
1
+ # Authoring rules
2
+
3
+ Content rules for explico courses — strong recommendations for the people and
4
+ agents who write lessons, notation entries, and interactive figures.
5
+
6
+ ## What these are
7
+
8
+ A course built on explico is authored by many hands — lecturers, TAs, AI
9
+ drafters — over a long time. These files fix the decisions that should not be
10
+ re-litigated per lesson: how a symbol is written, which library draws a graph,
11
+ how a randomised figure picks its axes.
12
+
13
+ Follow a rule unless you have a concrete, stateable reason not to — and then
14
+ leave that reason in the diff or the PR description. "I preferred it" is not a
15
+ reason; "the cited source writes it this way and the passage shows the mapping"
16
+ is.
17
+
18
+ Two kinds of rule live here:
19
+
20
+ - **Build-enforced** — the engine already fails the build when the rule is
21
+ broken. The rule text tells you _how_ to satisfy the gate, not whether to.
22
+ - **Review-only** — nothing checks it automatically; a reviewer does. Breaking
23
+ one is a review comment, not a broken build.
24
+
25
+ Every rule is written to be falsifiable: you can look at a lesson and say
26
+ whether it complies.
27
+
28
+ ## Precedence
29
+
30
+ 1. Build-enforced engine behaviour. If a rule here contradicts what the engine
31
+ actually does, the engine wins and the rule is stale — fix the rule.
32
+ 2. These rules.
33
+ 3. Author discretion, recorded in the diff.
34
+
35
+ A course may add its own rules on top — a house style for prose, a domain glyph
36
+ table. It must not silently relax one of these; loosening a rule is a change to
37
+ this folder, reviewed like any other change.
38
+
39
+ ## How a rule is written
40
+
41
+ [`structure.md`](structure.md) is not a rule set — it is the taxonomy the rules
42
+ speak in: what a part, a chapter, a section, and a subsection are, and which of
43
+ them an equation number counts.
44
+
45
+ Each rule has a stable ID — a letter for its area (`N` notation,
46
+ `V` visualizations, `C` versioning) plus a number — and this shape:
47
+
48
+ > **N1 — One-line imperative.**
49
+ > _Rationale._ Why the rule exists; the failure it prevents.
50
+ > _How to apply._ Concrete steps, with the smallest example that shows it.
51
+ > _Enforcement._ Build-enforced (which gate) or Review-only.
52
+ > _Exceptions._ The stateable reasons that override it, if any.
53
+
54
+ IDs are permanent. A retired rule keeps its number, is struck through in the
55
+ index below, and gains a one-line `Retired <date>: <why>` note — so a `N3` in an
56
+ old commit message never goes ambiguous.
57
+
58
+ ## Amending
59
+
60
+ 1. Open a PR that touches exactly one area file plus the index table below.
61
+ 2. Give a new rule the next unused number in its area. Never renumber.
62
+ 3. Write the four parts above. If the rule could be build-enforced but is not
63
+ yet, say so and link the engine issue.
64
+ 4. One maintainer review. The bar: is it falsifiable, is the rationale real, is
65
+ it not already covered by an existing rule or a schema.
66
+
67
+ A new area — a new letter and file — is the same PR shape plus a row in the
68
+ file table.
69
+
70
+ ## Files
71
+
72
+ | File | Area | Covers |
73
+ | -------------------------------- | ---- | --------------------------------------------------------- |
74
+ | [notation.md](notation.md) | `N` | how math symbols are written and bound |
75
+ | [visualizations.md](visualizations.md) | `V` | diagrams, charts, interactive figures, randomness |
76
+ | [versioning.md](versioning.md) | `C` | the knowledge version and `content/CHANGELOG.md` |
77
+ | [agents.md](agents.md) | — | _guide_: working on a course repo (with an AI agent) |
78
+ | [migrating.md](migrating.md) | — | _guide_: per-version steps when bumping the `explico` dep |
79
+
80
+ ## Rule index
81
+
82
+ | ID | Rule | Enforcement |
83
+ | ------------------------------- | ---------------------------------------------------------------- | ------------------------------------------- |
84
+ | [N1](notation.md#n1) | The normal distribution is `\mathcal{N}` | Build-enforced (completeness gate) + review |
85
+ | [N2](notation.md#n2) | `\mathbb{N,Z,R,C}` are reserved for the number systems | Partial — gate forces every other `\mathbb{}` to resolve |
86
+ | [N3](notation.md#n3) | Multi-letter roman labels use `\mathrm{…}` | Build-enforced (completeness gate) |
87
+ | [N4](notation.md#n4) | One meaning per glyph; define once; `[[key]]` before first math | Build-enforced (completeness gate) |
88
+ | [N5](notation.md#n5) | Referenced display equations carry `\label{eq:key}` | Review-only |
89
+ | [N6](notation.md#n6) | Probability is `\mathbb{P}` (double-struck `P`) | Build-enforced (completeness gate) + review |
90
+ | [N7](notation.md#n7) | The differential `d` is upright `\mathrm{d}` | Build-enforced (completeness gate) |
91
+ | [N8](notation.md#n8) | A named operator is universal (shipped/course list) or explained | Tier-3 warning (`operator-explained`) |
92
+ | [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
93
+ | [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
94
+ | [V3](visualizations.md#v3) | Interactive figures: island + `renderLabMath` seam + text alt | Review-only |
95
+ | [V4](visualizations.md#v4) | Randomised figures expose an adjustable, deterministic seed | Review-only |
96
+ | [V5](visualizations.md#v5) | Axis ranges span the first 10 seeds at the current control values | Review-only |
97
+ | [C1](versioning.md#c1) | Every content change lands with a `content/CHANGELOG.md` entry | Review-only |
98
+ | [C2](versioning.md#c2) | The knowledge version is semver over learning-facing content | Review-only |
99
+ | [C3](versioning.md#c3) | `about.version` equals the newest `content/CHANGELOG.md` heading | Build-enforced (Tier-3 `knowledge-version-mismatch`) |
100
+ | [C4](versioning.md#c4) | On release, move `[Unreleased]` and bump `about.version` together | Review-only |
@@ -0,0 +1,63 @@
1
+ # Working on an explico course (with an agent)
2
+
3
+ Guidance for an AI agent — or a person — editing a course repo built on
4
+ `explico`. The engine ships this so a downstream course inherits a baseline; add
5
+ a repo-specific `AGENTS.md` at your course root for anything below that is
6
+ particular to your layout.
7
+
8
+ This is a guide, not a rule area. The enforceable rules are in
9
+ [`notation.md`](notation.md) (`N`), [`visualizations.md`](visualizations.md)
10
+ (`V`), and [`versioning.md`](versioning.md) (`C`). When the engine's behaviour
11
+ and a rule disagree, the engine wins — see [`README.md`](README.md).
12
+
13
+ ## The loop
14
+
15
+ - **Start from the CLI, not a file hunt.** `pnpm --filter <course> content
16
+ context <lesson>` prints a lesson plus exactly its notation, references, and
17
+ assumptions. `pnpm content status` is the draft-debt overview. `pnpm content
18
+ check <lesson> | --changed` is schema + refs + MDX→KaTeX, exits non-zero on an
19
+ error. Read those before opening files.
20
+ - **Validate, twice.** `pnpm --filter <course> validate:content` regenerates
21
+ `build/resolution/*` and then fails on the same run that changed them. Run it
22
+ again; the second run is clean. Commit the regenerated `build/` and
23
+ `resolution/` files.
24
+ - **Before handing work back:** `validate:content` (×2) → `astro check` →
25
+ `astro build`. `astro build` is the only step that runs every gate the CI
26
+ does.
27
+
28
+ ## Reading and running
29
+
30
+ - Grep for a symbol and open a line range; don't read a whole engine module out
31
+ of `node_modules/explico/`. The parts you actually need are
32
+ [`notation.md`](notation.md), this file, and the `content` CLI output.
33
+ - If a page 500s or a change doesn't show, the dev server's content store is
34
+ stale — a **new file in a `content/` collection needs a dev-server restart**,
35
+ not just a save. Restart it; don't diagnose it with `ps` / `curl` scrapes.
36
+ - Ad-hoc Node scripts run from the course package (workspace deps aren't at the
37
+ repo root); Playwright imports as `@playwright/test`.
38
+ - Pipe long command output to `| tail -n 40`. One-off probes and fetched pages
39
+ go to a scratch file you then grep — not into the conversation whole.
40
+
41
+ ## Notation `formula:` fields (the sharp edges)
42
+
43
+ - Every glyph in a `formula:` must resolve: to the entry itself, one of its
44
+ `glosses:`, a card named with `\explain{key}{latex}`, or the base library.
45
+ An unresolved bare letter fails the build with the entry name.
46
+ - The notation reference graph must be **acyclic** — if `a`'s formula names `b`
47
+ and `b`'s names `a`, the build rejects it. Link only *down* a chain
48
+ (acceleration → velocity → position, not back up).
49
+ - Use `\mathbf` for a vector, not `\vec`: `\vec` currently trips the KaTeX
50
+ sanitizer in `render-notation-math`. (Engine bug, not a rule — but `\mathbf`
51
+ is what builds today.)
52
+
53
+ ## Versioning
54
+
55
+ Every content change adds a bullet under `## [Unreleased]` in
56
+ `content/CHANGELOG.md`; a release moves it to a dated `## [x.y.z]` heading and
57
+ sets `about.version` to match, in the same commit. Rules C1–C4,
58
+ [`versioning.md`](versioning.md).
59
+
60
+ ## Upgrading the engine
61
+
62
+ [`migrating.md`](migrating.md) — the per-version steps when you bump the
63
+ `explico` dependency across a **Breaking** entry in `CHANGELOG.md`.
@@ -0,0 +1,169 @@
1
+ # Upgrading the explico engine
2
+
3
+ What a course repo has to change when it bumps its `explico` dependency across a
4
+ version that carries **Breaking** notes. Each section is the concrete work; the
5
+ matching `CHANGELOG.md` entry (shipped in the installed package) is the full
6
+ list of what moved and why.
7
+
8
+ The pattern every time:
9
+
10
+ 1. `pnpm add -D explico@<version>`
11
+ 2. Do the per-version steps below.
12
+ 3. `pnpm validate:content` (run it **twice** — the first run regenerates
13
+ `build/resolution/*` and reports it stale; the second is clean), then
14
+ `pnpm content check --changed`, then `astro check`, then `astro build`.
15
+ Each stage names what it doesn't like; fix and re-run.
16
+
17
+ The engine never fails silently on a migration it can detect: a missing
18
+ required config block throws with the section of this file to read.
19
+
20
+ ---
21
+
22
+ ## 0.3.x → 0.4.0
23
+
24
+ ### 1. Rename `sections` to `parts` in `content/course.config.ts`
25
+
26
+ A sidebar group is now a **part** — "section" only ever means an `##` heading
27
+ inside a lesson. See [`structure.md`](structure.md) for the full taxonomy.
28
+
29
+ ```diff
30
+ - sections: [
31
+ + parts: [
32
+ { dir: 'foundations', label: 'Foundations' },
33
+ { dir: 'bonds', label: 'Bonds' },
34
+ ],
35
+ ```
36
+
37
+ `sections` is still read, so this is not urgent; it is removed in the next
38
+ MINOR release.
39
+
40
+ ### 2. Pass the manifest to `remarkNotation`
41
+
42
+ Replace the individual numbering options with the manifest itself. This is what
43
+ guarantees the number a page prints and the number a reference to it prints are
44
+ computed from the same inputs — supplying one without the other is what made an
45
+ equation read `(2.2)` on its own page and `(4.5)` everywhere else.
46
+
47
+ ```diff
48
+ [remarkNotation, {
49
+ definitions: manifest.notation.raw,
50
+ - equations: manifest.equations.numbersBySlug,
51
+ + manifest,
52
+ base: publicBase,
53
+ }],
54
+ ```
55
+
56
+ The engine now cross-checks the two and **fails the build** when they disagree,
57
+ so a config left half-wired errors instead of shipping wrong numbers.
58
+
59
+ ### 3. Expect every equation, table, and figure number to change
60
+
61
+ Numbers are now `‹part›.‹n›`: the prefix is the part's ordinal, and `n` runs
62
+ across the whole part rather than restarting on each page. A course whose
63
+ lessons were chapters 1…16 now has as many prefixes as it has sidebar groups.
64
+
65
+ Nothing in `content/` needs editing — numbers were never hand-typed, and every
66
+ `[[eq-key]]` / `[[slug#eq-key]]` reference keeps resolving. Two things to check:
67
+
68
+ - **Prose that spells a number out** ("as equation 4.5 showed") was already
69
+ against rule N-references; find it with `grep -rnE '\((1?[0-9])\.[0-9]+\)'
70
+ content/` and convert it to a `[[eq-key]]` reference.
71
+ - **External links to a positional anchor** (`#eq-2-4`, an *unkeyed* equation)
72
+ now point elsewhere. Keyed anchors (`#eq-<key>`) are stable and unaffected —
73
+ give an equation a `\label{eq:…}` if you need to link to it from outside.
74
+
75
+ ### 4. Manifest schema is v7
76
+
77
+ `lessons[].part` is new; `lessons[].chapter` now means the lesson's ordinal
78
+ *within its part*, not down the whole course; `lessons[].numbering` carries the
79
+ running float counts each chapter starts from. A client asserting on
80
+ `MANIFEST_SCHEMA_VERSION` needs the bump.
81
+
82
+ ---
83
+
84
+ ## 0.2.x → 0.3.0
85
+
86
+ ### 1. Add the required `about` block to `content/course.config.ts`
87
+
88
+ `CourseConfig` now requires an `about` block — the reader-facing identity behind
89
+ the header `[?]` panel, and the course's **knowledge version**. Building without
90
+ it throws `course.config.ts is missing a complete about block …`.
91
+
92
+ ```ts
93
+ export const courseConfig = {
94
+ title: 'My Course',
95
+ description: '…',
96
+ // …existing fields…
97
+ about: {
98
+ // materialName and summary default to title / description
99
+ version: '0.1.0', // the knowledge version — semver
100
+ authors: [{ name: 'A. Lecturer', contact: 'a.lecturer@example.edu' }],
101
+ disclaimer:
102
+ 'Draft study material. No warranty. Parts are AI-assisted and under review.',
103
+ changelogHref: 'https://…/content/CHANGELOG.md', // optional
104
+ },
105
+ } satisfies CourseConfig;
106
+ ```
107
+
108
+ ### 2. Add `content/CHANGELOG.md`
109
+
110
+ The knowledge version (`about.version`) must equal the newest `## [x.y.z]`
111
+ heading in `content/CHANGELOG.md`. A missing file or a mismatch is a Tier-3
112
+ `knowledge-changelog-missing` / `knowledge-version-mismatch` warning (it does
113
+ not fail the build). Governance: [`versioning.md`](versioning.md), rules C1–C4.
114
+
115
+ ```markdown
116
+ # Knowledge changelog — <course>
117
+
118
+ ## [Unreleased]
119
+
120
+ ## [0.1.0] - 2026-01-01
121
+
122
+ - Initial content.
123
+ ```
124
+
125
+ ### 3. Show the About panel
126
+
127
+ The `[?]` panel renders through Starlight's `SocialIcons` slot. Add it to the
128
+ `components` map in `astro.config.mjs` (alongside the `Footer` override you
129
+ already have):
130
+
131
+ ```js
132
+ starlight({
133
+ components: {
134
+ Footer: 'explico/components/starlight/LessonFooter.astro',
135
+ SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro',
136
+ // …
137
+ },
138
+ })
139
+ ```
140
+
141
+ The build still succeeds without this; the panel just won't appear.
142
+
143
+ ### 4. Only if you imported the equation-reference components directly
144
+
145
+ `EquationEnhancer` and `EquationRefLayer` are gone, replaced by
146
+ `KeyedFloatEnhancer` + `FloatRefLayer` (one hover/pin/jump engine now shared by
147
+ equation, `Figure`, and `Table` references). **If you only wire
148
+ `Footer: 'explico/components/starlight/LessonFooter.astro'`, there is nothing to
149
+ do** — the swap is internal to that component. Only a course that forked
150
+ `LessonFooter` or imported the old components by hand updates its imports:
151
+
152
+ | Removed | Use |
153
+ | --- | --- |
154
+ | `explico/components/starlight/EquationEnhancer.astro` | `explico/components/starlight/KeyedFloatEnhancer.astro` |
155
+ | `explico/components/equation/EquationRefLayer.astro` | `explico/components/reference/FloatRefLayer.astro` |
156
+
157
+ ### 5. Regenerate — the manifest schema bumped
158
+
159
+ `MANIFEST_SCHEMA_VERSION` increased (additive: `manifest.about`,
160
+ `manifest.diagnostics.knowledge`; existing fields unchanged). `pnpm
161
+ validate:content` rewrites `build/manifest.json`; commit the result. An external
162
+ consumer that pins the schema version needs to accept the new number.
163
+
164
+ ### New, no action required
165
+
166
+ `Figure` / `Table` components with numbering and `[[fig-<id>]]` / `[[tab-<id>]]`
167
+ references; `Diagram` gains `direction="LR"` and an optional `description`;
168
+ `\explain{key}{latex}` works inside notation `formula:` fields; heading anchors
169
+ and numbered equations are on by default.
@@ -0,0 +1,297 @@
1
+ # Notation rules
2
+
3
+ `N` rules govern how a mathematical symbol is written in any rendered-math
4
+ context: lesson body, `notation.formula`, `checks.yml`, assessment prompts, and
5
+ the `formula` slots of `Diagram` / lab components. The completeness gate scans
6
+ all of these, so "the build passed" already means most of what follows holds —
7
+ these rules pin the _choices_ the gate leaves open.
8
+
9
+ See [README.md](README.md) for what "strong recommendation", "Build-enforced",
10
+ and "Review-only" mean here, and how to amend a rule.
11
+
12
+ ---
13
+
14
+ ## N1
15
+
16
+ **The normal distribution is `\mathcal{N}`.**
17
+
18
+ Write a Gaussian as `\mathcal{N}(\mu, \sigma^2)` — calligraphic `N`,
19
+ parameterised by **mean and variance**. Name any other named distribution the
20
+ same way: a calligraphic letter through `\mathcal{…}`.
21
+
22
+ Do not use `N(\mu, \sigma^2)`, `\mathrm{N}`, `Norm(...)`, or `\mathbb{N}` — the
23
+ last is the natural numbers ([N2](#n2)) and a build-reserved token.
24
+
25
+ _Rationale._ One corpus-wide spelling makes the symbol searchable, lets it bind
26
+ to a single notation card, and keeps it clear of `N` used as a count and
27
+ `\mathbb{N}` used as a set. Mean/variance is the form the standard-normal CDF
28
+ `\Phi` and PDF `\varphi` compose with cleanly.
29
+
30
+ _How to apply._
31
+
32
+ - Define it once. Add a notation entry — a shared card if the course teaches
33
+ probability, otherwise `notation.local` on the first lesson that needs it:
34
+
35
+ ```yaml
36
+ key: prob.normal
37
+ latex: \mathcal{N}
38
+ label: Normal distribution
39
+ meaning: Gaussian distribution with mean μ and variance σ².
40
+ ```
41
+
42
+ Then introduce it in prose with `[[prob.normal]]` before its first appearance
43
+ in math, exactly like any other shared glyph ([N4](#n4)).
44
+
45
+ - In a one-off context where a full entry is overkill, use the escape hatch:
46
+ `\explain{prob.normal}{\mathcal{N}}(\mu, \sigma^2)`. Prefer the entry.
47
+
48
+ - If a course standardises on standard deviation rather than variance, state
49
+ that once in the domain notes, then write `\mathcal{N}(\mu, \sigma)`
50
+ consistently. Never mix the two forms in one corpus.
51
+
52
+ - Compose consistently: `X \sim \mathcal{N}(\mu, \sigma^2)`,
53
+ `\Phi` for its CDF, `\varphi` for its PDF. Pick the CDF/PDF glyphs once per
54
+ course.
55
+
56
+ _Enforcement._ Build-enforced that the symbol resolves to a key — `\mathcal{N}`
57
+ is a bindable identifier atom to the glyph resolver, so an undefined one fails
58
+ the completeness gate at `file:line:token`. The choice of `\mathcal{N}` over
59
+ `N` / `\mathrm{N}` / `Norm` is convention, checked in review.
60
+
61
+ _Exceptions._ Quoting a source that uses a different glyph **and** showing the
62
+ mapping to `\mathcal{N}` in the same passage.
63
+
64
+ ---
65
+
66
+ ## N2
67
+
68
+ **`\mathbb{N}`, `\mathbb{Z}`, `\mathbb{R}`, `\mathbb{C}` are reserved for the
69
+ number systems.**
70
+
71
+ These four blackboard-bold tokens mean the naturals, integers, reals, and
72
+ complexes, always, and need no notation key. Every _other_ `\mathbb{…}` —
73
+ `\mathbb{E}`, `\mathbb{P}`, `\mathbb{Q}`, `\mathbb{1}` — is an ordinary
74
+ identifier that must resolve to a semantic key.
75
+
76
+ _Rationale._ The universal sets are genuinely universal; forcing a per-page
77
+ definition of `\mathbb{R}` would be noise. Everything else in blackboard bold
78
+ is domain-specific (an expectation, a probability measure, an indicator) and
79
+ earns a card.
80
+
81
+ _How to apply._ Need the rationals as a set on one lesson? That collides with a
82
+ reserved token — reach for `\explain{...}{\mathbb{Q}}` or a differently-styled
83
+ glyph, and note it in the domain vocabulary.
84
+
85
+ _Enforcement._ Partial. The gate forces every non-reserved `\mathbb{…}` to
86
+ resolve; keeping `N/Z/R/C` for the number systems (not, say, `\mathbb{N}` for a
87
+ sample size) is convention.
88
+
89
+ _Exceptions._ None for the four reserved tokens.
90
+
91
+ ---
92
+
93
+ ## N3
94
+
95
+ **Multi-letter roman labels use `\mathrm{…}`.**
96
+
97
+ An upright multi-letter name — `\mathrm{PV}`, `\mathrm{opt}`, `\mathrm{max}`
98
+ where no built-in operator exists — is wrapped in `\mathrm{}`. Never write it as
99
+ a run of bare italic letters (`PV`), which reads as a product `P \cdot V`.
100
+
101
+ _Rationale._ `\mathrm{}` is a typographic label, not a bindable atom — the glyph
102
+ resolver skips its body whole. Bare letter runs are parsed as identifier atoms,
103
+ so `PV` asks the gate to resolve `P` and `V` separately: a build failure at
104
+ best, a wrong binding at worst.
105
+
106
+ _How to apply._ Subscripts too: `v_{\mathrm{max}}`, not `v_{max}`.
107
+
108
+ _Enforcement._ Build-enforced indirectly — bare multi-letter runs hit the
109
+ completeness gate as unresolved identifiers.
110
+
111
+ _Exceptions._ Genuine products, and single-letter subscripts that are
112
+ themselves notation keys.
113
+
114
+ ---
115
+
116
+ ## N4
117
+
118
+ **One meaning per glyph per page; define once; introduce with `[[key]]` before
119
+ the first math use.**
120
+
121
+ A glyph has exactly one meaning on a page. A shared symbol is introduced in
122
+ prose with `[[key]]` before it appears in any equation. `\explain{key}{latex}`
123
+ is the escape hatch for a compound or glyph-colliding symbol only — not the
124
+ default way to bind.
125
+
126
+ _Rationale._ This is the core contract the completeness gate enforces
127
+ corpus-wide; the rule here is just its author-facing statement. A reader hovers
128
+ any symbol and gets exactly one card.
129
+
130
+ _How to apply._ See the engine README's notation section and the `notation`
131
+ collection schema. A letter that exists only so one entry's own `formula` can
132
+ be stated is a **gloss**, not a card — it creates no curriculum edge.
133
+
134
+ _Enforcement._ Build-enforced (completeness gate + one-meaning-per-glyph
135
+ consistency check).
136
+
137
+ _Exceptions._ None. Genuinely different meanings need different glyphs or
138
+ different pages.
139
+
140
+ ---
141
+
142
+ ## N5
143
+
144
+ **A display equation referenced later carries `\label{eq:key}`.**
145
+
146
+ If prose points back at a display equation — this page or another — the equation
147
+ has `\label{eq:key}` and the reference is `[[eq-key]]`. Do not hand-number
148
+ equations in the text.
149
+
150
+ _Rationale._ The engine numbers every display equation `‹part›.‹n›` — the part
151
+ is the sidebar group's position in `courseConfig.parts`, `n` counts the display
152
+ equations across that whole part in appearance order, continuing from one
153
+ chapter into the next (`##` and `###` headings do not affect it; see
154
+ [`structure.md`](structure.md)) — and derives the anchor and any cross-lesson
155
+ link from the label. A hand-typed "(3)" goes stale the moment an equation is
156
+ inserted above it, or the chapter moves within its part. `<Table>` and
157
+ `<Figure>` are numbered the same way, each with its own counter.
158
+
159
+ _How to apply._ Only label equations you actually reference; an unlabelled
160
+ display equation is still fine.
161
+
162
+ _Enforcement._ Review-only — an unlabelled-but-referenced equation builds; a
163
+ reviewer catches the hand-numbering.
164
+
165
+ _Exceptions._ None worth stating.
166
+
167
+ ---
168
+
169
+ ## N6
170
+
171
+ **Probability is `\mathbb{P}` — double-struck `P`.**
172
+
173
+ Write a probability as `\mathbb{P}(A)` — blackboard-bold `P`. Not `P(A)` (a bare
174
+ `P` is a variable — a price, a power, a point), not `\Pr`, not `\text{Prob}`.
175
+ Expectation is the same family: `\mathbb{E}[X]`.
176
+
177
+ _Rationale._ `\mathbb{P}` is unmistakably the probability functional, never a
178
+ quantity. It reads next to `\mathbb{E}` and `\mathbb{Q}` as one notation family
179
+ and never collides with a `P` the lesson uses for something physical.
180
+
181
+ _How to apply._
182
+
183
+ - Define it once, like [N1](#n1):
184
+
185
+ ```yaml
186
+ key: prob.p
187
+ latex: \mathbb{P}
188
+ label: Probability
189
+ meaning: Probability of an event.
190
+ ```
191
+
192
+ Introduce it in prose with `[[prob.p]]` before its first appearance in math.
193
+
194
+ - Conditional probability is `\mathbb{P}(A \mid B)` — `\mid`, not a bare `|`.
195
+
196
+ - A course doing measure theory may define `\mathbb{P}` as the real-world
197
+ measure and `\mathbb{Q}` as a risk-neutral one, consistent with the reserved
198
+ reading of `\mathbb{Q}` ([N2](#n2)). State that split once in the domain
199
+ notes.
200
+
201
+ _Enforcement._ Build-enforced that the symbol resolves to a key — `\mathbb{P}`
202
+ is a non-reserved `\mathbb{…}` and the completeness gate demands a binding
203
+ ([N2](#n2)). The choice of `\mathbb{P}` over `P` / `\Pr` is convention.
204
+
205
+ _Exceptions._ Quoting a source that writes `\Pr` or `P` **and** showing the
206
+ mapping to `\mathbb{P}` in the same passage.
207
+
208
+ ---
209
+
210
+ ## N7
211
+
212
+ **The differential `d` is upright `\mathrm{d}`.**
213
+
214
+ The `d` of an integrand and the `d` of a derivative are the same operator and
215
+ get the same upright form — distinct from an italic `d`, which is always a
216
+ variable (a distance, a day count, a dimension):
217
+
218
+ - `\int_a^b f(x)\,\mathrm{d}x` — thin space `\,`, then `\mathrm{d}x`.
219
+ - `\dfrac{\mathrm{d}f}{\mathrm{d}x}`, `\dfrac{\mathrm{d}^2 f}{\mathrm{d}x^2}`.
220
+ - `\mathrm{d}s`, `\mathrm{d}A`, `\mathrm{d}V`, `\mathrm{d}t` — same rule for
221
+ every differential.
222
+
223
+ A partial derivative uses `\partial` bare — `\dfrac{\partial f}{\partial x}` —
224
+ which is already distinct; do not wrap it in anything.
225
+
226
+ _Rationale._ This is the ISO 80000-2 convention. Upright `\mathrm{d}` cannot be
227
+ read as a factor `d \cdot x`, and it stays clear of a notation key `d` on a
228
+ lesson that happens to have one.
229
+
230
+ _How to apply._ `\mathrm{d}` is a typographic operator, not a bindable atom —
231
+ the glyph resolver skips its body like any `\mathrm{…}` label ([N3](#n3)), so it
232
+ needs no notation key. Only the variable of integration (`x`, `t`) still has to
233
+ resolve.
234
+
235
+ _Enforcement._ Build-enforced indirectly — a bare `\int f(x) dx` puts an
236
+ unresolved identifier `d` in front of the completeness gate ([N3](#n3)).
237
+
238
+ _Exceptions._ A verbatim quotation of a source in a blockquote.
239
+
240
+ ---
241
+
242
+ ## N8
243
+
244
+ **A named operator is universal, or it is explained.**
245
+
246
+ Every named operator a lesson uses — `\exp`, `\int`, `\sin`, … as built-ins,
247
+ `\operatorname{Var}`, `\operatorname{Cov}`, `\operatorname{E}` as declared ones
248
+ — is either on the shipped operator baseline (`src/reference/base-operators.mjs`
249
+ plus this course's `operators.allow` in `course.config.ts`) or it carries a
250
+ notation entry the reader can hover.
251
+
252
+ _Rationale._ The completeness gate has no opinion about operators: KaTeX parses
253
+ `\exp` as an operator node and `\operatorname{Var}` as an `operatorname` node,
254
+ and the glyph resolver skips both whole ([N3](#n3)). So `\operatorname{Var}`
255
+ renders fine while the reader is never told what "Var" means. `\exp` and `\int`
256
+ need no such help — everyone knows them — but a domain operator does, and the
257
+ line between the two is a course decision, made once, not per lesson.
258
+
259
+ _How to apply._
260
+
261
+ - Universal operators — trig, `\exp` / `\ln` / `\log`, `\lim`, `\max` / `\min`
262
+ / `\sup` / `\inf`, `\sum` / `\prod` / `\int` — just work. The full baseline is
263
+ the greppable set in `base-operators.mjs`.
264
+
265
+ - A domain operator gets a notation entry, exactly like a glyph ([N4](#n4)) — a
266
+ shared card if the course teaches it broadly, otherwise `notation.local`:
267
+
268
+ ```yaml
269
+ key: stats.variance
270
+ latex: \operatorname{Var}
271
+ label: Variance
272
+ meaning: Expected squared deviation of a random variable from its mean.
273
+ ```
274
+
275
+ Then introduce it in prose with `[[stats.variance]]` before its first
276
+ appearance in math, and write it `\operatorname{Var}(X)` — never `Var(X)`
277
+ (bare letters, [N3](#n3)) and never `\mathrm{Var}` (a label, not an operator).
278
+
279
+ - A course whose domain treats an operator as common — `\operatorname{Var}`,
280
+ `\operatorname{Cov}`, `\operatorname{Corr}`, `\operatorname{sgn}` in a
281
+ statistics course — lists it once in `course.config.ts` instead:
282
+
283
+ ```ts
284
+ operators: { allow: ['Var', 'Cov', 'Corr', 'sgn'] }
285
+ ```
286
+
287
+ Names are the `\operatorname{…}` body, or the command without its backslash
288
+ for a built-in. `operators.deny` forces a baseline operator to be explained
289
+ anyway.
290
+
291
+ _Enforcement._ Tier-3 warning (`operator-explained`), surfaced by
292
+ `pnpm validate:content`, recorded in the manifest, and silenced only by a
293
+ reviewed line in `lint-ignore.yml` (`operator-explained:Var — <reason>`). Like
294
+ `formula` before it (D15), the tier can be promoted once a corpus is clean.
295
+
296
+ _Exceptions._ Quoting a source that uses an operator glyph **and** showing what
297
+ it means in the same passage.