explico 0.11.0 → 0.12.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 (63) hide show
  1. package/CHANGELOG.md +189 -1
  2. package/dist/compiler/astro.d.ts +7 -1
  3. package/dist/compiler/astro.js +5 -2
  4. package/dist/compiler/cli.d.ts +1 -0
  5. package/dist/compiler/collections.d.ts +25 -2
  6. package/dist/compiler/collections.js +37 -4
  7. package/dist/compiler/exports/standalone-html.d.ts +1 -0
  8. package/dist/compiler/manifest.d.ts +30 -4
  9. package/dist/compiler/manifest.js +49 -10
  10. package/dist/compiler/resolution-report.d.ts +1 -0
  11. package/dist/reference/base-operators.d.mts +6 -0
  12. package/dist/reference/base-operators.mjs +6 -8
  13. package/dist/reference/figure-symbols.d.ts +32 -0
  14. package/dist/reference/figure-symbols.js +55 -0
  15. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  16. package/dist/reference/gate-non-chapter-pages.js +156 -0
  17. package/dist/reference/gate-part-pages.d.ts +3 -1
  18. package/dist/reference/gate-part-pages.js +2 -1
  19. package/dist/reference/lab-katex-validate.d.ts +43 -0
  20. package/dist/reference/lab-katex-validate.js +96 -0
  21. package/dist/reference/lint-ignore.js +1 -1
  22. package/dist/reference/page-notation.d.ts +5 -3
  23. package/dist/reference/page-notation.js +8 -8
  24. package/dist/reference/raw-floats.mjs +37 -13
  25. package/dist/reference/registry.js +11 -1
  26. package/dist/reference/remark-notation.mjs +195 -7
  27. package/dist/reference/render-lab-math.d.ts +11 -2
  28. package/dist/reference/render-lab-math.js +2 -2
  29. package/dist/rules/catalog.js +33 -3
  30. package/dist/rules/rules.schema.json +22 -4
  31. package/docs/VERSIONING.md +3 -1
  32. package/docs/authoring/README.md +2 -1
  33. package/docs/authoring/components.md +4 -4
  34. package/docs/authoring/migrating.md +271 -0
  35. package/docs/authoring/notation.md +4 -1
  36. package/docs/authoring/structure-rules.md +75 -5
  37. package/docs/authoring/structure.md +14 -0
  38. package/docs/authoring/visualizations.md +26 -10
  39. package/package.json +1 -1
  40. package/src/compiler/astro.ts +17 -6
  41. package/src/compiler/collections.ts +67 -9
  42. package/src/compiler/manifest.ts +89 -13
  43. package/src/components/CourseContents.astro +6 -0
  44. package/src/components/PartOverview.astro +6 -0
  45. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  46. package/src/components/disclosure/print-as-shown.ts +42 -6
  47. package/src/components/figure/Figure.astro +6 -1
  48. package/src/components/figure/figure-state.ts +26 -17
  49. package/src/components/starlight/LessonFooter.astro +27 -7
  50. package/src/components/table/Table.astro +1 -1
  51. package/src/reference/base-operators.mjs +6 -8
  52. package/src/reference/figure-symbols.ts +117 -0
  53. package/src/reference/gate-non-chapter-pages.ts +241 -0
  54. package/src/reference/gate-part-pages.ts +1 -1
  55. package/src/reference/lab-katex-validate.ts +156 -0
  56. package/src/reference/lint-ignore.ts +4 -1
  57. package/src/reference/page-notation.ts +14 -11
  58. package/src/reference/raw-floats.mjs +37 -13
  59. package/src/reference/registry.ts +16 -1
  60. package/src/reference/remark-notation.mjs +195 -7
  61. package/src/reference/render-lab-math.ts +12 -1
  62. package/src/rules/catalog.ts +37 -3
  63. package/src/styles/global.css +57 -38
package/CHANGELOG.md CHANGED
@@ -14,6 +14,192 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.12.0] - 2026-09-28
18
+
19
+ ### Breaking
20
+
21
+ - **Every symbol a figure shows opens its notation card, and the build
22
+ checks it** (rule V3, rule N4). Two checks fail a lab that draws math with
23
+ KaTeX itself, whose symbols have no card:
24
+ - **`lab-katex-direct`** (error, rule V3; `reference/lab-katex-validate.ts`,
25
+ in `diagnostics.figures`): `pnpm validate:content` follows each lesson's
26
+ relative imports to the course's components and fails a file that
27
+ imports `katex` (not a type-only import, not the stylesheet), naming the
28
+ chain of imports that reaches it. V3 now has a check, so it counts as
29
+ partly enforced.
30
+ - **`astro build` fails a figure's unbound symbol**: at `astro:build:done`,
31
+ every letter KaTeX drew inside a `figure.keyed-figure` (its tools
32
+ included) must sit in a `[data-notation-key]`; upright text
33
+ (`\text{}`, `\mathrm{}`, a named operator such as `\sin`) is exempt
34
+ (`reference/figure-symbols.ts`, `assertFigureSymbolsBound`).
35
+ - **`setFigureState` takes no symbol typed as text.** A run is a string or
36
+ `{ math }`; the italic (`{ text, italic: true }`), plain `{ text }` and
37
+ subscript (`{ sub }`) runs are gone, and one passed anyway throws, naming
38
+ the fix. `FigureStateRun` is `string | { math: RenderedLabMath }`, so
39
+ `astro check` fails an old run.
40
+ - **A page that is not a chapter numbers nothing, and `validate:content` says
41
+ so** (rule S10, locked). On the landing page, a part's overview page, or
42
+ any other page outside a part directory, `pnpm validate:content`
43
+ (`reference/gate-non-chapter-pages.ts`) fails `<Table>`, `<Figure>`,
44
+ `<Diagram>`, `<CompactExamples>`, `<AssessmentSet>`, a lab (a component
45
+ imported from a `labs/` module, or any `client:*` island) and
46
+ `\label{eq:…}` (`non-chapter-float`); `notation.local` and
47
+ `notation.suspend` (`non-chapter-local-notation`); and `$…$` or `[[…]]` in
48
+ `hero.title`, `hero.tagline` or a `<PartOverview chain>` step, which print
49
+ literally (`non-chapter-plain-text`). A `<Diagram>` rendered there
50
+ unnumbered before, and `<Table>` and `<Figure>` threw at render;
51
+ `remarkNotation` now fails the same blocks and labels at render, naming
52
+ S10. Link the chapter's float instead: `[[<chapter>#tab-<key>]]` shows it
53
+ in its card.
54
+ - **Math on a page that is not a chapter binds like a chapter's** (rule N4,
55
+ rule S10). Every symbol needs a `[[key]]` or an `\explain` on the page, or
56
+ the build fails; plain `$v$` on a part page used to render with no card
57
+ and no check. The text and math gates (rule N4, rule N8, rule N9,
58
+ rule N10, rule N12, rule N14, rule N22, and a course's `X` rules) and
59
+ per-place waivers now scan these pages too.
60
+
61
+ ### Added
62
+
63
+ - **`renderLabMath(template, scope, { display: false })`** renders inline lab
64
+ math — a `.katex` with no `.katex-display` — for a formula that sits in a
65
+ line: a legend entry, an axis title, a control label. It never scrolls.
66
+ `RenderLabMathOptions` is exported; the default stays a display equation.
67
+ - **A `{ math }` run for the caption's live clause.** `setFigureState`
68
+ writes a `RenderedLabMath` from `renderLabMath(…, { display: false })` as
69
+ `span.float-caption__math`, so "Shown for v₀ = 20 m/s" names the lesson's
70
+ own `v₀`, which opens its card.
71
+ - **`unboundFigureSymbols(html)`** (`explico/reference/figure-symbols`) lists
72
+ the figures in an HTML string, and the symbols in each, that KaTeX drew
73
+ outside a notation binding. A course's browser test runs it on a hydrated
74
+ figure's `outerHTML`, which also holds what an island draws only in the
75
+ browser; the playground's `labs.spec.ts` does.
76
+ - **A link to a chapter shows the chapter's full title as its tooltip.** A
77
+ Markdown link or a JSX `<a href>` the author wrote to another chapter —
78
+ root-relative, relative, or under the course's `base`, with or without a
79
+ `#` anchor — gets the chapter's `title` as its `title` attribute
80
+ (`remarkNotation`, from the manifest's lessons). A link with a title of its
81
+ own, a same-page anchor, and a `[[…]]` reference, which opens a card of its
82
+ own, are left alone.
83
+ - **Cards on a page that is not a chapter** (rule S10). A `[[key]]` term,
84
+ bound math, a citation, an abbreviation and a `[[chapter#tab-key]]`
85
+ reference open their cards on the landing page and on a part's overview
86
+ page: `LessonFooter` mounts the notation, citation, abbreviation and
87
+ float-reference layers there, from the page's own notation bundle (shared
88
+ entries only). The manifest schema is now v25: `pages` lists every such
89
+ page with its `id` (`index`, `motion.index`), `slug`, `file` and a
90
+ `notation` bundle of the lesson shape, and `notation.bundles` and
91
+ `notation.backlinks` carry those pages' ids too.
92
+
93
+ ### Changed
94
+
95
+ - **The operator baseline drops `\limsup` / `\liminf` and the
96
+ category-theory `\injlim` / `\projlim` / `\varlimsup` / `\varliminf` /
97
+ `\varinjlim` / `\varprojlim`.** Only `\lim` stays in `BASE_OPERATORS`
98
+ (`reference/base-operators.mjs`): the other eight are not "everyone knows
99
+ this" the way a plain limit, `\max`, or `\min` is, so rule N8's
100
+ `operator-explained` warning now asks a course that uses one for a
101
+ notation entry, like any other domain operator. A course that relies on
102
+ the old baseline can restore it with `rules.N8.allow: ['limsup',
103
+ 'liminf', 'injlim', 'projlim', 'varlimsup', 'varliminf', 'varinjlim',
104
+ 'varprojlim']` in its settings.
105
+ - **A lab's math wrapper never scrolls.** `.lab-math` loses its
106
+ `overflow-x: auto`, `overflow-y: hidden` and `padding-block`: a display
107
+ template's `.katex-display` scrolls sideways when it is wider than its
108
+ box, like a lesson equation, and carries the padding. `.lab-math` keeps
109
+ its `margin-block` only when it holds display math.
110
+ - **A float's caption line is at least as tall as its buttons**
111
+ (`min-block-size: 1.5rem` while a `.float-tool` shows, reset in print), so
112
+ the row never hangs below the line — past a figure's body, or into a
113
+ table's header — even when a course hides the caption's text.
114
+ - **The local-notation mismatch names what it compared.**
115
+ `assertLocalNotationParity` says "notation mismatch", names the page's
116
+ frontmatter `notation.local` and the manifest's bundle for the page, and
117
+ gives both causes left: a stale manifest, or an entry `validate:content`
118
+ rejected.
119
+ - **`notationDefinitionsForRenderedPage(manifest, pageId, dependencies)`**
120
+ takes a lesson's id or a non-chapter page's (`manifest.pages[].id`), and
121
+ its errors say "on page" instead of "in lesson". The `<Table>` and
122
+ `<Figure>` missing-number errors name rule S10.
123
+
124
+ ### Fixed
125
+
126
+ - **An abbreviation glossary entry can be scrolled to.** Its anchor
127
+ (`#abbr-…`) sat on a `display: contents` wrapper, which has no box, so a
128
+ direct URL, a full navigation and a same-page click all left the reader
129
+ where they were. The `id` is on the entry's `<dt>`, with the
130
+ `scroll-margin-block-start: 5rem` the other anchor targets carry.
131
+ - **On a narrow screen, the header no longer covers Starlight's menu
132
+ button**, so the navigation opens again on a phone: the button sits one
133
+ level above the raised header (`starlight-menu-button > button`).
134
+ - **Links on a part overview page and in `<CourseContents>` use the course
135
+ accent** (`--sl-color-text-accent`) instead of the browser's default link
136
+ colour, which `.not-content` let through.
137
+ - **Printing no longer breaks a knowledge check that hydrates while the page
138
+ is prepared for print** (React error #418). A collapsed-content note never
139
+ goes inside an Astro island: a closed disclosure inside one gets its note
140
+ after the outermost island, once per target URL, so the folded questions
141
+ of one check share one note after the check.
142
+ - **A `notation.local` entry that a page uses only by shape** — an operator
143
+ entry `{}'` for `w'`, as rule N22 suggests — **no longer fails the build
144
+ as a stale manifest.** A page's own local entries seed its notation bundle
145
+ (`buildNotationRegistry`), so none is `unused-definition` any longer; the
146
+ `weak-local` advice still names one the page barely uses.
147
+ - **A `lint-ignore.yml` `<detail>` can name a hyphenated notation key.**
148
+ `parseLintIgnore`'s line grammar excluded `-` from the detail segment, so
149
+ `- notation-units:arg-min — …` (or any acknowledgement keyed to a
150
+ kebab-case notation key, the shape every key in a course that follows the
151
+ authoring docs' own naming convention takes) never matched the line
152
+ pattern and `validate:content` threw "expected `- <code>[:<detail>] —
153
+ <reason>`" instead of loading the file. Only whitespace and the em-dash
154
+ separator are excluded from `<detail>` now; a bare `-`, `--`, or ` - ` was
155
+ never valid *inside* a detail on its own (the separator needs the em dash,
156
+ or the ASCII forms flanked the way the grammar already required), so this
157
+ only admits what the docstring already promised: "a notation key".
158
+ - **A table inside a worked example no longer reports rule S3's
159
+ `raw-table-markdown`** (or `raw-table-html`): such a table is
160
+ illustrative, like an equation there. The scan counts a table only where a
161
+ `<Table>` would number it, at the lesson's top level or in a `<Memo>`,
162
+ `<ShortNote>` or `<Fold>` (`scanRawFloats`, `collectRawFloats`), as
163
+ `structure-rules.md#s3` always said.
164
+ - **A `<Table>` or `<Figure>` nested in a worked example fails with the real
165
+ reason.** `remarkNotation` names the component it sits in and the edit to
166
+ make, at its line; the components' own guards name the nesting before the
167
+ Astro configuration.
168
+ - **A figure never draws a vertical scrollbar.** `.keyed-figure__body` kept
169
+ `overflow-x: auto`, which computes `overflow-y` to `auto` as well, so a
170
+ fractional height at a browser zoom scrolled by a pixel; it now clips
171
+ vertically (`overflow-y: hidden`).
172
+ - **A display equation that just fits no longer scrolls by 2 px.** KaTeX's
173
+ stacked scripts hang a 2 px spacer outside their box; `.katex-display` (in
174
+ lesson content and in `.lab-math`) reserves it with `padding-inline: 2px`
175
+ on both sides, which keeps a centred equation centred.
176
+ - **A `[[key]]` or an `\explain` on a part overview page no longer fails
177
+ `astro build` after `validate:content` passed.** The page had no notation
178
+ entries, so the payload invariant found a trigger with no card. Rule S2's
179
+ "its math and `[[key]]` references render" now holds.
180
+ - **An indented `[[key]]` inside a component of an `.mdx` page reaches the
181
+ page's bundle.** The reference extractor masked a line indented by four
182
+ spaces as an indented code block, which MDX does not have, so text
183
+ indented inside `<Card>` or `<Memo>` rendered a trigger its bundle lacked,
184
+ and the build failed. A `.md` file keeps CommonMark's indented code.
185
+
186
+ ## [0.11.1] - 2026-09-28
187
+
188
+ ### Fixed
189
+
190
+ - **A side panel's labels wrap instead of scrolling sideways.** A chapter,
191
+ lesson or heading longer than the navigation or the "On this page" panel
192
+ is wide goes onto more lines, at any width, including one the reader
193
+ dragged a panel down to; a word longer than the panel hyphenates where the
194
+ language allows, else breaks. A resized panel used to keep each label on
195
+ one line and scroll sideways (`white-space: nowrap` on
196
+ `html[data-navigation-resized]` / `html[data-contents-resized]`), and the
197
+ "On this page" column kept a width taken from the navigation's
198
+ (`--sl-sidebar-width`), running past a narrowed panel's edge; it now fills
199
+ its panel. `.sidebar-pane` clips sideways (`overflow-x: hidden`), where a
200
+ compact chapter's invisible resting tooltip reached past it; a showing
201
+ tooltip is `position: fixed` and is never cut.
202
+
17
203
  ## [0.11.0] - 2026-09-28
18
204
 
19
205
  ### Breaking
@@ -2126,7 +2312,9 @@ under **Breaking** below; a PATCH never breaks.
2126
2312
  system with its completeness gate, the curriculum-graph validator, the
2127
2313
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
2128
2314
 
2129
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.11.0...HEAD
2315
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.12.0...HEAD
2316
+ [0.12.0]: https://github.com/danieltuzes/explico/compare/v0.11.1...v0.12.0
2317
+ [0.11.1]: https://github.com/danieltuzes/explico/compare/v0.11.0...v0.11.1
2130
2318
  [0.11.0]: https://github.com/danieltuzes/explico/compare/v0.10.0...v0.11.0
2131
2319
  [0.10.0]: https://github.com/danieltuzes/explico/compare/v0.9.0...v0.10.0
2132
2320
  [0.9.0]: https://github.com/danieltuzes/explico/compare/v0.8.0...v0.9.0
@@ -17,6 +17,7 @@ import '../reference/examples-validate.js';
17
17
  import '../reference/folds-validate.js';
18
18
  import '../reference/knowledge-checks-validate.js';
19
19
  import '../reference/raw-floats-validate.js';
20
+ import '../reference/lab-katex-validate.js';
20
21
  import '../reference/gate-math.js';
21
22
  import '../reference/mixed-content.js';
22
23
  import '../reference/gloss.js';
@@ -26,7 +27,12 @@ import './lesson-derivation.js';
26
27
 
27
28
  /** Match only a configured lesson artifact below Astro's deployment base. */
28
29
  declare function matchStandaloneDownload(pathname: string, base: string, lessonSlugs: readonly string[]): string | undefined;
29
- /** Fail loudly instead of rendering math triggers whose local cards are stale. */
30
+ /**
31
+ * Fail loudly instead of rendering math triggers whose local cards are
32
+ * missing. The manifest bundles every `notation.local` entry with its page,
33
+ * so the two lists differ only when the manifest is older than the page, or
34
+ * when `validate:content` rejected an entry.
35
+ */
30
36
  declare function assertLocalNotationParity(lessonId: string, authoredKeys: readonly string[], renderedKeys: readonly string[]): void;
31
37
  /** Fail an Astro build if final rendered HTML contains an unusable trigger. */
32
38
  declare function assertNotationPayloadHtml(page: string, html: string): void;
@@ -8,6 +8,7 @@ import {
8
8
  writeManifest
9
9
  } from "./manifest.js";
10
10
  import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
+ import { assertFigureSymbolsBound } from "../reference/figure-symbols.js";
11
12
  import { writeStandaloneLessonExports } from "./exports/standalone-html.js";
12
13
  import remarkAbbreviations from "../reference/remark-abbreviations.mjs";
13
14
  import remarkCitation from "../reference/remark-citation.mjs";
@@ -87,7 +88,7 @@ function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
87
88
  return;
88
89
  }
89
90
  throw new Error(
90
- `Stale Explico manifest for ${lessonId}: lesson notation.local has [${authored.join(", ")}], but the page notation bundle has [${rendered.join(", ")}]. Pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations.`
91
+ `Explico notation mismatch for ${lessonId}: the page's frontmatter notation.local declares [${authored.join(", ")}], but the manifest's notation bundle for the page holds [${rendered.join(", ")}]. The manifest bundles every notation.local entry with its page, so either it is stale \u2014 pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations \u2014 or \`pnpm validate:content\` rejected an entry: fix the errors it reports.`
91
92
  );
92
93
  }
93
94
  function assertNotationPayloadHtml(page, html) {
@@ -139,7 +140,9 @@ async function validateGeneratedNotationPayloads(directory) {
139
140
  if (entry.isDirectory()) {
140
141
  await walk(child);
141
142
  } else if (entry.isFile() && entry.name.endsWith(".html")) {
142
- assertNotationPayloadHtml(relative(root, child), await readFile(child, "utf8"));
143
+ const html = await readFile(child, "utf8");
144
+ assertNotationPayloadHtml(relative(root, child), html);
145
+ assertFigureSymbolsBound(relative(root, child), html);
143
146
  }
144
147
  }
145
148
  };
@@ -18,6 +18,7 @@ import '../reference/examples-validate.js';
18
18
  import '../reference/folds-validate.js';
19
19
  import '../reference/knowledge-checks-validate.js';
20
20
  import '../reference/raw-floats-validate.js';
21
+ import '../reference/lab-katex-validate.js';
21
22
  import '../reference/mixed-content.js';
22
23
  import '../reference/gloss.js';
23
24
  import '../rules/waivers.js';
@@ -1,5 +1,5 @@
1
1
  import { CurriculumCatalog } from '../curriculum/validation.js';
2
- import { NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryInput, LocalNotationDefinitionInput, NotationSuspension } from '../reference/types.js';
2
+ import { NotationLessonInput, NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryInput, LocalNotationDefinitionInput, NotationSuspension } from '../reference/types.js';
3
3
 
4
4
  /**
5
5
  * The single path from content files to typed data.
@@ -54,6 +54,29 @@ declare function loadAbbreviationRegistryInput(): Promise<readonly AbbreviationD
54
54
  * decoration (the page would contradict itself) each fail the load.
55
55
  */
56
56
  declare function notationSuspensions(raw: unknown, localDefinitions: readonly LocalNotationDefinitionInput[], file: string): NotationSuspension[];
57
+ /**
58
+ * A docs page that is **not a chapter** (rule S10): the landing page, a part's
59
+ * overview page, and any other page outside a part directory — every slug
60
+ * `isLessonSlug` rejects. Such a page may explain its symbols like a chapter,
61
+ * but it recalls them only: it takes no `notation.local` and no float numbers.
62
+ */
63
+ interface NonChapterPage {
64
+ /** `lessonIdFromSlug(slug)` — `index`, `motion.index`, `glossary`. Never a lesson id. */
65
+ readonly id: string;
66
+ /** Docs slug without extension, e.g. `motion/index`. */
67
+ readonly slug: string;
68
+ readonly file: string;
69
+ readonly data: Record<string, unknown>;
70
+ readonly body: string;
71
+ readonly bodyLine: number;
72
+ /**
73
+ * The page as notation input — no local entries, no suspensions — so the
74
+ * registry resolves its `[[key]]` references and the text and math gates
75
+ * scan it exactly as they scan a chapter.
76
+ */
77
+ readonly notation: NotationLessonInput;
78
+ }
79
+ declare function loadNonChapterPages(): NonChapterPage[];
57
80
  declare function loadNotationRegistryInput(): Promise<NotationRegistryInput>;
58
81
  /**
59
82
  * Notation frontmatter objects for `remark-notation`'s `definitions` option.
@@ -101,4 +124,4 @@ declare function notationSourceLocatorGaps(): {
101
124
  total: number;
102
125
  };
103
126
 
104
- export { type NotationSpec, type PartOverviewPage, extractNotationReferences, loadAbbreviationRegistryInput, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadPartOverviews, loadSourceRecords, notationSourceLocatorGaps, notationSuspensions };
127
+ export { type NonChapterPage, type NotationSpec, type PartOverviewPage, extractNotationReferences, loadAbbreviationRegistryInput, loadCurriculumCatalog, loadLessonChecks, loadNonChapterPages, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadPartOverviews, loadSourceRecords, notationSourceLocatorGaps, notationSuspensions };
@@ -139,16 +139,20 @@ function sourceSpan(file, text, index) {
139
139
  column: (lines.at(-1)?.length ?? 0) + 1
140
140
  };
141
141
  }
142
- function maskMarkdownLiterals(text) {
142
+ function maskMarkdownLiterals(text, mdx) {
143
143
  const mask = (value) => value.replace(/[^\n]/g, " ");
144
- return text.replace(
144
+ const fenced = text.replace(
145
145
  /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\n[ \t]{0,3}\2[^\n]*(?=\n|$)|$)/g,
146
146
  mask
147
- ).replace(/^(?: {4}|\t).*$/gm, mask).replace(/(`+)(?!`)([^\n]*?)\1/g, mask);
147
+ );
148
+ return (mdx ? fenced : fenced.replace(/^(?: {4}|\t).*$/gm, mask)).replace(
149
+ /(`+)(?!`)([^\n]*?)\1/g,
150
+ mask
151
+ );
148
152
  }
149
153
  function extractNotationReferences(text, file, kind) {
150
154
  const references = [];
151
- const searchable = maskMarkdownLiterals(text);
155
+ const searchable = maskMarkdownLiterals(text, file.endsWith(".mdx"));
152
156
  const patterns = [
153
157
  [/\[\[\s*([^\][]+?)\s*\]\]/g, kind ?? "prose"],
154
158
  [/\\explain\{([^{}]+)\}/g, kind ?? "math"]
@@ -412,6 +416,34 @@ function notationLessonEntries() {
412
416
  }
413
417
  return lessons;
414
418
  }
419
+ function loadNonChapterPages() {
420
+ const pages = [];
421
+ for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
422
+ const slug = docSlug(path);
423
+ if (isLessonSlug(slug)) continue;
424
+ const { file, data, body, bodyLine } = parseDocument(path);
425
+ const id = lessonIdFromSlug(slug);
426
+ pages.push({
427
+ id,
428
+ slug: slug.replace(/\.(md|mdx)$/, ""),
429
+ file,
430
+ data,
431
+ body,
432
+ bodyLine,
433
+ notation: {
434
+ lessonId: id,
435
+ status: status(data.editorialStatus ?? "draft", `${file} editorialStatus`),
436
+ localDefinitions: [],
437
+ suspended: [],
438
+ references: extractNotationReferences(body, file, void 0),
439
+ body,
440
+ bodyLine,
441
+ source: { file }
442
+ }
443
+ });
444
+ }
445
+ return pages;
446
+ }
415
447
  async function loadNotationRegistryInput() {
416
448
  return {
417
449
  sharedDefinitions: sharedNotationEntries(),
@@ -506,6 +538,7 @@ export {
506
538
  loadAbbreviationRegistryInput,
507
539
  loadCurriculumCatalog,
508
540
  loadLessonChecks,
541
+ loadNonChapterPages,
509
542
  loadNotationDefinitions,
510
543
  loadNotationRegistryInput,
511
544
  loadNotationSpecs,
@@ -16,6 +16,7 @@ import '../../reference/examples-validate.js';
16
16
  import '../../reference/folds-validate.js';
17
17
  import '../../reference/knowledge-checks-validate.js';
18
18
  import '../../reference/raw-floats-validate.js';
19
+ import '../../reference/lab-katex-validate.js';
19
20
  import '../../reference/gate-math.js';
20
21
  import '../../reference/mixed-content.js';
21
22
  import '../../reference/gloss.js';
@@ -8,6 +8,7 @@ import { ExampleLabelRecord, ExampleDiagnostic } from '../reference/examples-val
8
8
  import { FoldDiagnostic } from '../reference/folds-validate.js';
9
9
  import { KnowledgeCheckLabelRecord, KnowledgeCheckDiagnostic } from '../reference/knowledge-checks-validate.js';
10
10
  import { RawFloatDiagnostic } from '../reference/raw-floats-validate.js';
11
+ import { LabKatexDiagnostic } from '../reference/lab-katex-validate.js';
11
12
  import { MathGateDiagnostic } from '../reference/gate-math.js';
12
13
  import { ContentSegment } from '../reference/mixed-content.js';
13
14
  import { GlyphScopeEntry } from '../reference/gloss.js';
@@ -167,8 +168,14 @@ import '../rules/course-rules.js';
167
168
  * state of a `<Fold>`; `disclosure.figures` now says whether a lab's tools
168
169
  * start open; `diagnostics.folds` holds `fold-unnamed`
169
170
  * (docs/plans/figures-and-short-captions.md, Parts 1–2).
171
+ *
172
+ * v25 (cards on non-chapter pages, rule S10): `pages` — every docs page that
173
+ * is not a chapter (the landing page, a part's overview page, …) with its
174
+ * `id`, `slug`, `file` and a `notation` bundle of the lesson shape, so the
175
+ * footer renders the cards its `[[key]]` terms and math open.
176
+ * `notation.bundles` and `notation.backlinks` now carry those pages' ids too.
170
177
  */
171
- declare const MANIFEST_SCHEMA_VERSION = 24;
178
+ declare const MANIFEST_SCHEMA_VERSION = 25;
172
179
  declare const MANIFEST_PATH: string;
173
180
  interface ManifestCitation {
174
181
  readonly id: string;
@@ -340,6 +347,19 @@ interface ManifestLesson {
340
347
  readonly resolution: readonly ManifestNotationResolution[];
341
348
  };
342
349
  }
350
+ /**
351
+ * A docs page that is not a chapter (v25, rule S10) — the landing page, a
352
+ * part's overview page, any page outside a part directory. It numbers
353
+ * nothing, so it carries only what its cards need: the notation bundle its
354
+ * `[[key]]` terms and math reach, shared entries only.
355
+ */
356
+ interface ManifestPage {
357
+ /** `index`, `motion.index`, `glossary` — `lessonIdFromSlug` of the slug; never a lesson id. */
358
+ readonly id: string;
359
+ readonly slug: string;
360
+ readonly file: string;
361
+ readonly notation: ManifestLesson['notation'];
362
+ }
343
363
  /** The reader-facing "about this material" bundle — the `[?]` panel's data. */
344
364
  interface ManifestAbout {
345
365
  /** `explico`'s own identity, from its `package.json` (the version's golden source). */
@@ -438,6 +458,8 @@ interface Manifest {
438
458
  /** The course's parts in config order, with their chapters and overview pages. */
439
459
  readonly parts: readonly ManifestPart[];
440
460
  readonly lessons: readonly ManifestLesson[];
461
+ /** Every docs page that is not a chapter, by id (v25). */
462
+ readonly pages: readonly ManifestPage[];
441
463
  /** Full competency prerequisite graph. */
442
464
  readonly prereqEdges: readonly PrereqEdge[];
443
465
  /** Equation identity (D7): every labelled display equation + the render-path lookup. */
@@ -501,8 +523,12 @@ interface Manifest {
501
523
  readonly equations: readonly EquationDiagnostic[];
502
524
  /** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
503
525
  readonly tables: readonly TableDiagnostic[];
504
- /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
505
- readonly figures: readonly FigureDiagnostic[];
526
+ /**
527
+ * `<Figure>` identity checks (F-tables-figures) — same codes, `fig-`
528
+ * prefixed — and rule V3's `lab-katex-direct`: a file a lesson imports
529
+ * that calls KaTeX itself.
530
+ */
531
+ readonly figures: readonly (FigureDiagnostic | LabKatexDiagnostic)[];
506
532
  /** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
507
533
  readonly diagrams: readonly DiagramDiagnostic[];
508
534
  /** Knowledge-check identity and `[[check-…]]` reference checks. */
@@ -549,4 +575,4 @@ declare function readManifestSync(): Manifest;
549
575
  */
550
576
  declare function loadManifest(courseConfig?: CourseConfig): Promise<Manifest>;
551
577
 
552
- export { MANIFEST_PATH, MANIFEST_SCHEMA_VERSION, type Manifest, type ManifestAbout, type ManifestAssessmentItemContent, type ManifestCitation, type ManifestLesson, type ManifestLessonFloat, type ManifestLessonNotation, type ManifestNotationResolution, type ManifestPart, type ManifestTitledBlock, type PrereqEdge, compileManifest, loadManifest, readManifestSync, serializeManifest, writeManifest };
578
+ export { MANIFEST_PATH, MANIFEST_SCHEMA_VERSION, type Manifest, type ManifestAbout, type ManifestAssessmentItemContent, type ManifestCitation, type ManifestLesson, type ManifestLessonFloat, type ManifestLessonNotation, type ManifestNotationResolution, type ManifestPage, type ManifestPart, type ManifestTitledBlock, type PrereqEdge, compileManifest, loadManifest, readManifestSync, serializeManifest, writeManifest };
@@ -40,6 +40,9 @@ import {
40
40
  import {
41
41
  validateRawFloats
42
42
  } from "../reference/raw-floats-validate.js";
43
+ import {
44
+ validateLabKatexImports
45
+ } from "../reference/lab-katex-validate.js";
43
46
  import {
44
47
  gateContentMath,
45
48
  pageScope
@@ -57,6 +60,7 @@ import { gateCourseRules } from "../reference/gate-course-rules.js";
57
60
  import { gateHeadingLength } from "../reference/gate-heading-length.js";
58
61
  import { gateHeadingStructure } from "../reference/gate-heading-structure.js";
59
62
  import { gatePartPages } from "../reference/gate-part-pages.js";
63
+ import { gateNonChapterPages } from "../reference/gate-non-chapter-pages.js";
60
64
  import {
61
65
  scanSectionHeadings,
62
66
  scanTitledBlocks
@@ -96,6 +100,7 @@ import {
96
100
  loadCurriculumCatalog,
97
101
  loadNotationDefinitions,
98
102
  loadNotationRegistryInput,
103
+ loadNonChapterPages,
99
104
  loadNotationSpecs,
100
105
  loadPartOverviews,
101
106
  loadSourceRecords
@@ -106,7 +111,7 @@ import {
106
111
  isLessonSlug,
107
112
  lessonIdFromSlug
108
113
  } from "./lesson-derivation.js";
109
- const MANIFEST_SCHEMA_VERSION = 24;
114
+ const MANIFEST_SCHEMA_VERSION = 25;
110
115
  const REPOSITORY_ROOT = process.cwd();
111
116
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
112
117
  const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
@@ -260,7 +265,15 @@ async function compileManifest(courseConfig) {
260
265
  loadNotationRegistryInput(),
261
266
  loadAbbreviationRegistryInput()
262
267
  ]);
263
- const registry = buildNotationRegistry(notationInput);
268
+ const nonChapterPages = loadNonChapterPages();
269
+ const withPages = {
270
+ ...notationInput,
271
+ lessons: [
272
+ ...notationInput.lessons,
273
+ ...nonChapterPages.map((page) => page.notation)
274
+ ]
275
+ };
276
+ const registry = buildNotationRegistry(withPages);
264
277
  const cardsByKey = new Map(
265
278
  notationInput.sharedDefinitions.map((definition) => [
266
279
  definition.key,
@@ -283,7 +296,7 @@ async function compileManifest(courseConfig) {
283
296
  };
284
297
  const mathIssues = [
285
298
  ...gateContentMath({
286
- notationInput,
299
+ notationInput: withPages,
287
300
  assessments: assessmentsInput,
288
301
  lessonAssessments: catalog.lessons.map((lesson) => ({
289
302
  lessonId: lesson.id,
@@ -291,7 +304,7 @@ async function compileManifest(courseConfig) {
291
304
  }))
292
305
  }),
293
306
  ...rules.isEnabled("N9") ? [
294
- ...gateProseNotation(notationInput.lessons, {
307
+ ...gateProseNotation(withPages.lessons, {
295
308
  allowSpelledUnits: rules.strings("N9", "allow")
296
309
  }),
297
310
  ...gateProseNotationInAssessments(assessmentsInput, {
@@ -299,11 +312,11 @@ async function compileManifest(courseConfig) {
299
312
  })
300
313
  ] : [],
301
314
  ...rules.isEnabled("N12") ? [
302
- ...gateMathPunctuation(notationInput.lessons, mathEndAbbreviations),
315
+ ...gateMathPunctuation(withPages.lessons, mathEndAbbreviations),
303
316
  ...gateMathPunctuationInAssessments(assessmentsInput, mathEndAbbreviations)
304
317
  ] : [],
305
318
  ...rules.isEnabled("N14") ? gateAbbreviations({
306
- notationInput,
319
+ notationInput: withPages,
307
320
  abbreviations: abbreviationsInput,
308
321
  courseAbbreviations: rules.settings("N14")
309
322
  }) : [],
@@ -317,7 +330,9 @@ async function compileManifest(courseConfig) {
317
330
  // The course's own rules (`courseRules`): one pass for every enabled `X`
318
331
  // rule that carries a `forbid` pattern. A disabled one, and a guide rule
319
332
  // with no pattern, do no work here.
320
- ...gateCourseRules(notationInput.lessons, rules.course)
333
+ ...gateCourseRules(withPages.lessons, rules.course),
334
+ // S10 is locked: a float on a non-chapter page has no number to render.
335
+ ...gateNonChapterPages(nonChapterPages)
321
336
  ];
322
337
  const notationLessonById = new Map(
323
338
  notationInput.lessons.map((lesson) => [lesson.lessonId, lesson])
@@ -381,7 +396,7 @@ async function compileManifest(courseConfig) {
381
396
  }
382
397
  const waiverDiagnostics = [];
383
398
  const waiverRules = { courseRules: rules.course };
384
- const waivers = notationInput.lessons.flatMap((lesson) => {
399
+ const waivers = withPages.lessons.flatMap((lesson) => {
385
400
  const collected = collectWaivers(lesson, waiverRules);
386
401
  waiverDiagnostics.push(...collected.diagnostics);
387
402
  return collected.waivers;
@@ -593,6 +608,14 @@ async function compileManifest(courseConfig) {
593
608
  }
594
609
  );
595
610
  };
611
+ const labKatexDiagnostics = validateLabKatexImports(
612
+ notationInput.lessons.map((lesson) => ({
613
+ lessonId: lesson.lessonId,
614
+ file: lesson.source.file,
615
+ body: lesson.body
616
+ })),
617
+ REPOSITORY_ROOT
618
+ );
596
619
  const rawFloatDiagnostics = validateRawFloats(
597
620
  notationInput.lessons.map((lesson) => ({
598
621
  lessonId: lesson.lessonId,
@@ -665,9 +688,24 @@ async function compileManifest(courseConfig) {
665
688
  });
666
689
  }
667
690
  lessons.sort((a, b) => a.id.localeCompare(b.id));
691
+ const pages = nonChapterPages.map((page) => ({
692
+ id: page.id,
693
+ slug: page.slug,
694
+ file: page.file,
695
+ notation: lessonNotationBundle(
696
+ bundlesByLesson.get(page.id),
697
+ definitionsById,
698
+ lessonNotationResolution(
699
+ page.notation,
700
+ definitionsById,
701
+ availableDefinitionIds
702
+ ),
703
+ cardsByKey
704
+ )
705
+ })).sort((a, b) => a.id.localeCompare(b.id));
668
706
  const consistencyIssues = runConsistencyChecks({
669
707
  registry,
670
- notationInput,
708
+ notationInput: withPages,
671
709
  specs: loadNotationSpecs(),
672
710
  catalog,
673
711
  lintIgnore: loadLintIgnore(),
@@ -741,6 +779,7 @@ async function compileManifest(courseConfig) {
741
779
  sidebar,
742
780
  parts: manifestParts,
743
781
  lessons,
782
+ pages,
744
783
  prereqEdges,
745
784
  equations: {
746
785
  labels: equationValidation.labels,
@@ -776,7 +815,7 @@ async function compileManifest(courseConfig) {
776
815
  consistency: dropDisabled(consistencyIssues, off),
777
816
  equations: dropDisabled(equationValidation.diagnostics, off),
778
817
  tables: dropDisabled(tableValidation.diagnostics, off),
779
- figures: dropDisabled(figureValidation.diagnostics, off),
818
+ figures: dropDisabled([...figureValidation.diagnostics, ...labKatexDiagnostics], off),
780
819
  diagrams: dropDisabled(diagramValidation.diagnostics, off),
781
820
  checks: dropDisabled(checkValidation.diagnostics, off),
782
821
  examples: dropDisabled(exampleValidation.diagnostics, off),
@@ -16,6 +16,7 @@ import '../reference/examples-validate.js';
16
16
  import '../reference/folds-validate.js';
17
17
  import '../reference/knowledge-checks-validate.js';
18
18
  import '../reference/raw-floats-validate.js';
19
+ import '../reference/lab-katex-validate.js';
19
20
  import '../reference/gate-math.js';
20
21
  import '../reference/mixed-content.js';
21
22
  import '../reference/gloss.js';
@@ -26,6 +26,12 @@ export function isBaseOperator(name: any, operators: any): boolean;
26
26
  * baseline — trig, the exponential and logarithms, limits, extrema, and the
27
27
  * big operators — kept greppable and small like the base library.
28
28
  *
29
+ * Only the plain limit, `\lim`, is baseline. `\limsup` / `\liminf` and the
30
+ * category-theory `\injlim` / `\projlim` / `\varlimsup` / `\varliminf` /
31
+ * `\varinjlim` / `\varprojlim` read fine to someone who already knows them,
32
+ * but none is "everyone knows this" the way `\lim` or `\max` is — a course
33
+ * that uses one owes it a notation entry, like any other domain operator.
34
+ *
29
35
  * A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
30
36
  * written `\operatorname{…}` and is NOT here: it earns a card, a
31
37
  * `notation.local` entry, or an explicit line in `course.config.ts`