explico 0.13.0 → 0.14.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 (239) hide show
  1. package/CHANGELOG.md +894 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1087 -14
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -11,13 +11,22 @@
11
11
  * `figure.keyed-figure` (a lab's tools included), every letter KaTeX draws
12
12
  * must sit in a `[data-notation-key]`. Upright text is not a symbol:
13
13
  * `\text{…}`, `\mathrm{…}` (the resolver skips its body, rule N10) and a
14
- * named operator (`\sin`, `\max`). `compiler/astro.ts` runs it on every
15
- * built page, and a course's browser test can run it on a hydrated figure's
16
- * `outerHTML`, which also holds what an island draws only in the browser.
14
+ * named operator (`\sin`, `\max`). Nor is a plain base-library atom (`d`,
15
+ * `e`, `i`, `\pi`; `base-library.mjs`), which the resolver leaves without a
16
+ * card in lab math as in a lesson's, because every reader knows it — in a
17
+ * power too (`e^{i\pi}`). A styled one (`\mathbf{e}`), a decorated one
18
+ * (`\hat{e}`, rule N22) and an index (`x_i`, `e_i`, rule N23) are other
19
+ * symbols and still need a card; a subscript is told from a power by
20
+ * KaTeX's `vlist-t2`, the vertical list that has a lowered row.
21
+ * `compiler/astro.ts` runs it on every built page, and a course's browser
22
+ * test can run it on a hydrated figure's `outerHTML`, which also holds what
23
+ * an island draws only in the browser.
17
24
  */
18
25
 
19
26
  import { parse, type DefaultTreeAdapterMap } from 'parse5';
20
27
 
28
+ import { isBaseLibraryGlyph } from './base-library.mjs';
29
+
21
30
  type HtmlNode = DefaultTreeAdapterMap['node'];
22
31
  type HtmlElement = DefaultTreeAdapterMap['element'];
23
32
 
@@ -30,6 +39,10 @@ export interface UnboundFigureSymbols {
30
39
 
31
40
  /** KaTeX classes of upright text, which is never a symbol. */
32
41
  const UPRIGHT = new Set(['text', 'mathrm', 'textrm', 'mop']);
42
+ /** KaTeX classes of a letter in its plain italic form, `\mathit{e}` included. */
43
+ const PLAIN = new Set(['mathnormal', 'mathit']);
44
+ /** KaTeX class of an accent: the letter under it is not the plain atom. */
45
+ const DECORATED = new Set(['accent']);
33
46
  const LETTER = /\p{L}/u;
34
47
 
35
48
  function isElement(node: HtmlNode): node is HtmlElement {
@@ -59,59 +72,123 @@ export function unboundFigureSymbols(html: string): UnboundFigureSymbols[] {
59
72
  if (!html.includes('keyed-figure') || !html.includes('katex-html')) return [];
60
73
  const found = new Map<string, string[]>();
61
74
 
62
- const visit = (
63
- node: HtmlNode,
64
- figure: string | undefined,
65
- inKatex: boolean,
66
- covered: boolean,
67
- ): void => {
75
+ interface Context {
76
+ readonly figure: string | undefined;
77
+ readonly inKatex: boolean;
78
+ readonly covered: boolean;
79
+ /** Under an accent or in a subscript: not the plain atom. */
80
+ readonly decorated: boolean;
81
+ /** The element that holds this text draws it in plain italic. */
82
+ readonly plain: boolean;
83
+ /** In an `msupsub` with a subscript (`vlist-t2`), whose first row it is. */
84
+ readonly lowered: boolean;
85
+ }
86
+
87
+ const visit = (node: HtmlNode, context: Context, row?: number): void => {
88
+ let next = context;
68
89
  if (isElement(node)) {
69
90
  const names = classes(node);
91
+ next = {
92
+ ...context,
93
+ // A letter's own element says how it is drawn; its ancestors do not.
94
+ plain: names.some((name) => PLAIN.has(name)),
95
+ // Only the rows of a script's own vertical list are its sub and sup.
96
+ lowered: names.includes('msupsub')
97
+ ? childrenOf(node).some((child) => isElement(child) && classes(child).includes('vlist-t2'))
98
+ : row === undefined
99
+ ? context.lowered
100
+ : false,
101
+ };
70
102
  if (node.tagName === 'figure' && names.includes('keyed-figure')) {
71
- figure = attribute(node, 'id') ?? '(a figure with no id)';
103
+ next = { ...next, figure: attribute(node, 'id') ?? '(a figure with no id)' };
104
+ }
105
+ if (names.includes('katex-html')) next = { ...next, inKatex: true };
106
+ if (names.some((name) => DECORATED.has(name)) || (row === 0 && context.lowered)) {
107
+ next = { ...next, decorated: true };
72
108
  }
73
- if (names.includes('katex-html')) inKatex = true;
74
109
  if (
75
110
  attribute(node, 'data-notation-key') !== undefined ||
76
- (inKatex && names.some((name) => UPRIGHT.has(name)))
111
+ (next.inKatex && names.some((name) => UPRIGHT.has(name)))
77
112
  ) {
78
- covered = true;
113
+ next = { ...next, covered: true };
79
114
  }
80
115
  } else if (
81
116
  node.nodeName === '#text' &&
82
- figure !== undefined &&
83
- inKatex &&
84
- !covered
117
+ context.figure !== undefined &&
118
+ context.inKatex &&
119
+ !context.covered
85
120
  ) {
86
121
  const text = (node as DefaultTreeAdapterMap['textNode']).value.trim();
87
- if (LETTER.test(text)) {
88
- const symbols = found.get(figure) ?? [];
122
+ // KaTeX draws a run of plain letters as one text node (`iπ` of `e^{i\pi}`).
123
+ const library =
124
+ context.plain &&
125
+ !context.decorated &&
126
+ [...text].every((glyph) => !LETTER.test(glyph) || isBaseLibraryGlyph(glyph));
127
+ if (LETTER.test(text) && !library) {
128
+ const symbols = found.get(context.figure) ?? [];
89
129
  if (!symbols.includes(text)) symbols.push(text);
90
- found.set(figure, symbols);
130
+ found.set(context.figure, symbols);
91
131
  }
92
132
  }
93
- for (const child of childrenOf(node)) visit(child, figure, inKatex, covered);
133
+ // The rows of a `vlist` are its element children; the first of a lowered
134
+ // one is the subscript, the second a superscript over it.
135
+ const isVlist = isElement(node) && classes(node).includes('vlist');
136
+ let rows = 0;
137
+ for (const child of childrenOf(node)) {
138
+ const isRow = isVlist && isElement(child);
139
+ visit(child, next, isRow ? rows : undefined);
140
+ if (isRow) rows += 1;
141
+ }
94
142
  };
95
143
 
96
- visit(parse(html), undefined, false, false);
144
+ visit(parse(html), {
145
+ figure: undefined,
146
+ inKatex: false,
147
+ covered: false,
148
+ decorated: false,
149
+ plain: false,
150
+ lowered: false,
151
+ });
97
152
  return [...found].map(([figure, symbols]) => ({ figure, symbols }));
98
153
  }
99
154
 
155
+ /** One rendered page and the figures on it that draw a symbol with no card. */
156
+ export interface PageUnboundFigureSymbols {
157
+ /** The page's path in the build output, e.g. `lecture-5/chernoff/index.html`. */
158
+ readonly page: string;
159
+ readonly figures: readonly UnboundFigureSymbols[];
160
+ }
161
+
162
+ const describeFigures = (figures: readonly UnboundFigureSymbols[]): string =>
163
+ figures
164
+ .map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(', ')}`)
165
+ .join('; ');
166
+
100
167
  /**
101
- * Fail a build whose rendered page shows a symbol in a figure that opens no
102
- * card, naming the figure, the symbols, and the way to bind them.
168
+ * Fail a build over the pages that show a symbol in a figure that opens no
169
+ * card, naming every page, its figures and symbols, and the way to bind them.
170
+ * A build collects the whole list first (`compiler/astro.ts`), so it reports
171
+ * every page at once instead of one per run.
103
172
  */
104
- export function assertFigureSymbolsBound(page: string, html: string): void {
105
- const unbound = unboundFigureSymbols(html);
106
- if (unbound.length === 0) return;
173
+ export function assertNoUnboundFigureSymbols(pages: readonly PageUnboundFigureSymbols[]): void {
174
+ const found = pages.filter(({ figures }) => figures.length > 0);
175
+ const [only] = found;
176
+ if (!only) return;
177
+ const head =
178
+ found.length === 1
179
+ ? `Explico figure symbols without a notation card on rendered page "${only.page}": ` +
180
+ describeFigures(only.figures)
181
+ : `Explico figure symbols without a notation card on ${found.length} rendered pages:\n` +
182
+ found.map(({ page, figures }) => ` "${page}": ${describeFigures(figures)}`).join('\n');
107
183
  throw new Error(
108
- `Explico figure symbols without a notation card on rendered page "${page}": ` +
109
- unbound
110
- .map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(', ')}`)
111
- .join('; ') +
112
- '. Every symbol a figure shows opens its card (rule N4): render its math ' +
184
+ `${head}. Every symbol a figure shows opens its card (rule N4): render its math ` +
113
185
  'with renderLabMath (`{ display: false }` for a legend, an axis title or ' +
114
186
  'a label; a `{ math }` run for the caption clause), never with katex ' +
115
187
  'directly. Write a unit as `\\text{…}`.',
116
188
  );
117
189
  }
190
+
191
+ /** Fail a build whose rendered page shows a symbol in a figure that opens no card. */
192
+ export function assertFigureSymbolsBound(page: string, html: string): void {
193
+ assertNoUnboundFigureSymbols([{ page, figures: unboundFigureSymbols(html) }]);
194
+ }
@@ -2,7 +2,7 @@
2
2
  * `<Fold>` validation, run inside `compileManifest()`
3
3
  * (docs/plans/figures-and-short-captions.md, decision 11). A fold's bar is
4
4
  * its `label` or, without one, the numbers and short captions of the floats
5
- * it holds (`../components/float-name.ts`), so a fold needs one or the other:
5
+ * it holds (`./float-name.ts`), so a fold needs one or the other:
6
6
  *
7
7
  * - `fold-unnamed` (error): a `<Fold>` with no `label` and no `<Figure>`,
8
8
  * `<Table>` or `<Diagram>` that takes a number inside it — its bar would
@@ -34,6 +34,7 @@ import {
34
34
  type LessonAssessmentLink,
35
35
  type MathGateDiagnostic,
36
36
  type ScopeEntry,
37
+ scopeLocals,
37
38
  } from './gate-math';
38
39
  import { formulaGlyphScope, type GlyphScopeEntry, type ResolvedGloss } from './gloss';
39
40
  import { indexOfEntry } from './math-glyphs.mjs';
@@ -136,7 +137,8 @@ function message(
136
137
  `${lessonId}: the running index ${details.letter} (first in ${JSON.stringify(details.token)}) ` +
137
138
  `has no stated set. Give ${details.where} a range (range: "1, \\\\dots, n"), state it after a ` +
138
139
  `\\quad on the first display line that uses it (\\quad ${details.letter} = 1, \\dots, n), or ` +
139
- `mark the card fixedIndex: true if ${details.letter} is a label there, not a running index (rule N23).`
140
+ `mark the card fixedIndex: true if ${details.letter} is a label there, not a running index; ` +
141
+ `a family indexed by ${details.letter} then explains none of its instances (rule N23).`
140
142
  );
141
143
  case 'derived-key-taken':
142
144
  return (
@@ -173,7 +175,7 @@ export function gateIndices(
173
175
  const bodyOffset = (lesson.bodyLine ?? 1) - 1;
174
176
  const scope = pageScope(lesson, entries);
175
177
  const local = new Map<string, IndexEntryFacts>(
176
- lesson.localDefinitions.map((definition) => [definition.key, definition]),
178
+ scopeLocals(lesson).map((definition) => [definition.key, definition]),
177
179
  );
178
180
  const factsOf = (key: string | null): IndexEntryFacts | undefined =>
179
181
  key === null ? undefined : (local.get(key) ?? entries.get(key));
@@ -325,7 +327,7 @@ export function gateIndices(
325
327
  if (!lesson) continue;
326
328
  const scope = pageScope(lesson, entries);
327
329
  const local = new Map<string, IndexEntryFacts>(
328
- lesson.localDefinitions.map((definition) => [definition.key, definition]),
330
+ scopeLocals(lesson).map((definition) => [definition.key, definition]),
329
331
  );
330
332
  const factsOf = (key: string | null): IndexEntryFacts | undefined =>
331
333
  key === null ? undefined : (local.get(key) ?? entries.get(key));
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The completeness gate, run from the CLI (`pnpm validate:content`) over every
3
3
  * rendered-math context — lesson body `$…$` / `$$…$$` (including math inside
4
- * component slots), each `notation.local` `formula`, and any `$…$` in an
5
- * assessment `prompt` / `explanation`. It reuses the one resolver the Astro
4
+ * component slots), a notation entry's Markdown body, each `notation.local`
5
+ * and shared `formula`, and any `$…$` in an assessment `prompt` /
6
+ * `explanation`. It reuses the one resolver the Astro
6
7
  * remark pass uses (`resolveMathGlyphs`), so there is a single glyph table and
7
8
  * a single diagnostic shape; `astro build` re-runs the same resolver on the
8
9
  * render path as a backstop.
@@ -10,7 +11,7 @@
10
11
  * Every finding names its source and the fix. N17's size-only findings are
11
12
  * warnings and include a column when the math came from lesson/assessment text.
12
13
  */
13
- import { resolveMathGlyphs, GlyphResolutionError } from './math-glyphs.mjs';
14
+ import { resolveMathGlyphs, GlyphResolutionError, fixedLenderMessage, mathExcerpt } from './math-glyphs.mjs';
14
15
  import {
15
16
  EQ_LABEL_PATTERN,
16
17
  equationAnchorId,
@@ -25,6 +26,7 @@ import type {
25
26
  SharedNotationDefinitionInput,
26
27
  } from './types';
27
28
  import { blankPropStrings } from './gate-prop-text';
29
+ import { REGISTRY_BODY_FIX, registryGlyphScope } from './registry-scope.mjs';
28
30
 
29
31
  /**
30
32
  * The equation a finding sits in, when its math carries a `\label{eq:…}`.
@@ -77,7 +79,7 @@ export interface MathGateDiagnostic {
77
79
  * one the engine ships; its severity is the course's choice, so it is the one
78
80
  * context whose severity is not fixed here.
79
81
  */
80
- readonly severity: 'error' | 'warning';
82
+ readonly severity: 'error' | 'warning' | 'info';
81
83
  /** Owned by exactly one rule or invariant in `src/rules/catalog.ts`. */
82
84
  readonly code: string;
83
85
  readonly file: string;
@@ -192,14 +194,76 @@ export function scopeEntry(definition: {
192
194
  * assessment's math (`mixed-content.ts`), say — doesn't reconstruct it by
193
195
  * hand and risk drifting from what the completeness gate actually checks.
194
196
  */
197
+ /**
198
+ * The `notation.local` entries in scope on a page: its own, and for a deck
199
+ * beside its chapter the chapter's, which are the lesson's
200
+ * (`NotationLessonInput.inheritedLocalDefinitions`).
201
+ */
202
+ export function scopeLocals(
203
+ lesson: NotationLessonInput,
204
+ ): readonly NotationLessonInput['localDefinitions'][number][] {
205
+ const inherited = lesson.inheritedLocalDefinitions ?? [];
206
+ return inherited.length === 0 ? lesson.localDefinitions : [...lesson.localDefinitions, ...inherited];
207
+ }
208
+
209
+ const EXPLAIN_CALL = /\\explain\{([^{}]+)\}\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g;
210
+
211
+ /** Every `\explain{key}{inner}` in a span, in source order, with its offset. */
212
+ export function explainCalls(latex: string): { key: string; inner: string; index: number }[] {
213
+ return [...latex.matchAll(EXPLAIN_CALL)].map((match) => ({
214
+ key: match[1]!,
215
+ inner: match[2]!,
216
+ index: match.index ?? 0,
217
+ }));
218
+ }
219
+
220
+ /**
221
+ * The `\explain` wraps in a span that bind nothing new (rule N4): the bare
222
+ * inner form already resolves to the same card, with no glyph left over and
223
+ * no decoration left unexplained (rule N22). Such a wrap costs an author
224
+ * tokens on every use and the resolver nothing; `\explain` is for a compound
225
+ * or a colliding glyph.
226
+ */
227
+ export function redundantExplainWraps(
228
+ latex: string,
229
+ scope: readonly ScopeEntry[],
230
+ options: { readonly suspended?: readonly string[]; readonly derive?: boolean } = {},
231
+ ): { key: string; inner: string; index: number }[] {
232
+ const redundant: { key: string; inner: string; index: number }[] = [];
233
+ for (const call of explainCalls(latex)) {
234
+ let bare;
235
+ try {
236
+ bare = resolveMathGlyphs(call.inner, [...scope], {
237
+ suspended: options.suspended ?? [],
238
+ derive: options.derive ?? false,
239
+ }) as {
240
+ bindings: readonly { key: string; level: string }[];
241
+ unresolved: readonly unknown[];
242
+ decorations: readonly { wholeForm: string | null; operator: string | null }[];
243
+ };
244
+ } catch {
245
+ continue;
246
+ }
247
+ if (
248
+ bare.unresolved.length === 0 &&
249
+ bare.bindings.length > 0 &&
250
+ bare.bindings.every((binding) => binding.key === call.key && binding.level !== 'explicit') &&
251
+ bare.decorations.every((decoration) => decoration.wholeForm !== null || decoration.operator !== null)
252
+ ) {
253
+ redundant.push(call);
254
+ }
255
+ }
256
+ return redundant;
257
+ }
258
+
195
259
  export function pageScope(
196
260
  lesson: NotationLessonInput,
197
261
  sharedByKey: ReadonlyMap<string, ScopeEntry>,
198
262
  ): ScopeEntry[] {
199
- const local = lesson.localDefinitions.map(scopeEntry);
263
+ const local = scopeLocals(lesson).map(scopeEntry);
200
264
  const localKeys = new Set(local.map((entry) => entry.key));
201
265
 
202
- const referenced = new Set<string>();
266
+ const referenced = new Set<string>(lesson.inheritedKeys ?? []);
203
267
  for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
204
268
  for (const match of lesson.body.matchAll(pattern)) referenced.add(match[1]);
205
269
  }
@@ -237,6 +301,15 @@ export function suspendedOperators(lesson: NotationLessonInput): string[] {
237
301
  return (lesson.suspended ?? []).map((item) => item.operator);
238
302
  }
239
303
 
304
+ /** A family that would explain a subscripted symbol, were its index not fixed (rule N23). */
305
+ interface FixedLender {
306
+ readonly key: string;
307
+ /** What fixed it: the entry's own `fixedIndex`, a letter card's, or an index entry. */
308
+ readonly via: 'entry' | 'fixedIndex' | 'index-entry';
309
+ readonly card?: string;
310
+ readonly letter?: string;
311
+ }
312
+
240
313
  function resolveInto(
241
314
  latex: string,
242
315
  scope: readonly ScopeEntry[],
@@ -300,6 +373,24 @@ function resolveInto(
300
373
  });
301
374
  return;
302
375
  }
376
+ // Rule N4: an `\explain` wrap whose bare symbol already resolves to the
377
+ // same card binds nothing new. In a formula the wrap is what puts the card
378
+ // in scope (`formulaGlyphScope`), so the check does not apply there.
379
+ if (context !== 'formula') {
380
+ for (const wrap of redundantExplainWraps(stripEquationLabels(latex), scope, { suspended, derive })) {
381
+ out.push({
382
+ context,
383
+ code: 'explain-redundant',
384
+ severity: 'info',
385
+ file,
386
+ line,
387
+ ...(column === undefined ? {} : { column }),
388
+ token: wrap.inner,
389
+ ...at,
390
+ message: `\\explain{${wrap.key}}{${wrap.inner}} wraps what ${wrap.inner} already resolves to on this page; write it bare — \\explain is for a compound or a colliding glyph (rule N4)`,
391
+ });
392
+ }
393
+ }
303
394
  for (const item of result.unresolved) {
304
395
  const { token } = item;
305
396
  if ('suspended' in item && typeof item.suspended === 'string') {
@@ -320,10 +411,23 @@ function resolveInto(
320
411
  'font' in item && typeof item.font === 'string'
321
412
  ? ` ${token} is a different symbol from the plain letter: give it its own entry (latex: ${token}) or wrap it in \\explain{key}{${token}}.`
322
413
  : '';
414
+ // Rule N23: a family whose index letter a card fixed would have explained
415
+ // this `r_2`; blaming "a bare r" for it sends the author to the wrong repair.
416
+ const fixedLenders: readonly FixedLender[] =
417
+ 'fixedLenders' in item && Array.isArray(item.fixedLenders)
418
+ ? (item.fixedLenders as FixedLender[])
419
+ : [];
420
+ const instance = 'instance' in item && typeof item.instance === 'string' ? item.instance : token;
421
+ const fixedHint = fixedLenders.map((lender) => ` ${fixedLenderMessage(lender, instance)}`).join('');
323
422
  // Rule N23: an indexed entry with this base explains no bare letter.
423
+ const fixedKeys = new Set(fixedLenders.map((lender) => lender.key));
424
+ const lenders =
425
+ 'lenders' in item && Array.isArray(item.lenders)
426
+ ? (item.lenders as string[]).filter((key) => !fixedKeys.has(key))
427
+ : [];
324
428
  const lenderHint =
325
- 'lenders' in item && Array.isArray(item.lenders) && item.lenders.length > 0
326
- ? ` ${item.lenders.join(', ')} ${item.lenders.length === 1 ? 'has' : 'have'} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${item.lenders[0]}}{${token}}.`
429
+ lenders.length > 0
430
+ ? ` ${lenders.join(', ')} ${lenders.length === 1 ? 'has' : 'have'} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${lenders[0]}}{${token}}.`
327
431
  : '';
328
432
  out.push({
329
433
  context,
@@ -333,7 +437,7 @@ function resolveInto(
333
437
  line,
334
438
  token,
335
439
  ...at,
336
- message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint}${lenderHint} ${fix}`,
440
+ message: `Unresolved notation ${JSON.stringify(token)} in ${context} math, in ${mathExcerpt(latex)}.${fontHint}${fixedHint}${lenderHint} ${fix}`,
337
441
  });
338
442
  }
339
443
  }
@@ -372,6 +476,31 @@ export function currencyDollars(body: string): MathSpan[] {
372
476
  return spans;
373
477
  }
374
478
 
479
+ /** A body's `$5 to $6`: money remark-math would render as math. */
480
+ function reportCurrencyDollars(
481
+ body: string,
482
+ file: string,
483
+ bodyOffset: number,
484
+ out: MathGateDiagnostic[],
485
+ ): void {
486
+ for (const span of currencyDollars(body)) {
487
+ const shown = span.latex.replace(/\s+$/, '');
488
+ out.push({
489
+ context: 'body',
490
+ code: 'currency-dollar',
491
+ severity: SEVERITY.body,
492
+ file,
493
+ line: span.line + bodyOffset,
494
+ column: span.column,
495
+ token: `$${span.latex}$`,
496
+ message:
497
+ `"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the ` +
498
+ `page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), ` +
499
+ 'or the amount in words.',
500
+ });
501
+ }
502
+ }
503
+
375
504
  const BODY_FIX =
376
505
  'Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.';
377
506
  function dedupeScope(scope: readonly GlyphScopeEntry[]): GlyphScopeEntry[] {
@@ -491,22 +620,7 @@ export function gateContentMath(input: {
491
620
  );
492
621
  }
493
622
 
494
- for (const span of currencyDollars(lesson.body)) {
495
- const shown = span.latex.replace(/\s+$/, '');
496
- diagnostics.push({
497
- context: 'body',
498
- code: 'currency-dollar',
499
- severity: SEVERITY.body,
500
- file,
501
- line: span.line + bodyOffset,
502
- column: span.column,
503
- token: `$${span.latex}$`,
504
- message:
505
- `"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the ` +
506
- `page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), ` +
507
- 'or the amount in words.',
508
- });
509
- }
623
+ reportCurrencyDollars(lesson.body, file, bodyOffset, diagnostics);
510
624
 
511
625
  for (const definition of lesson.localDefinitions) {
512
626
  if (!definition.formula) continue;
@@ -533,6 +647,36 @@ export function gateContentMath(input: {
533
647
  }
534
648
  }
535
649
 
650
+ // A shared entry's Markdown body renders in the glossary through the same
651
+ // remark pass as a lesson, against the whole registry (`registry-scope.mjs`).
652
+ // A symbol it misses fails that render, and Astro's content layer only logs
653
+ // the failure and ships the entry without its body, so this is the check
654
+ // that stops the build.
655
+ const registryScope: ScopeEntry[] = registryGlyphScope(
656
+ notationInput.sharedDefinitions,
657
+ );
658
+ for (const definition of notationInput.sharedDefinitions) {
659
+ const file = definition.source.file;
660
+ const bodyOffset = (definition.bodyLine ?? 1) - 1;
661
+ for (const span of mathSpans(definition.body)) {
662
+ resolveInto(
663
+ span.latex,
664
+ registryScope,
665
+ 'body',
666
+ file,
667
+ span.line + bodyOffset,
668
+ span.column,
669
+ REGISTRY_BODY_FIX,
670
+ diagnostics,
671
+ undefined,
672
+ // An entry's body suspends nothing and derives no cards.
673
+ [],
674
+ false,
675
+ );
676
+ }
677
+ reportCurrencyDollars(definition.body, file, bodyOffset, diagnostics);
678
+ }
679
+
536
680
  // Shared cards carry their own rigorous `formula`. It resolves against the
537
681
  // entry-scoped table (D15) — the card, its glosses, and the cards the
538
682
  // formula names with `\explain` — not the registry-wide table the card's
@@ -7,9 +7,12 @@
7
7
  * Starting at each lesson's own imports, this follows every relative import
8
8
  * — the course's visuals, wherever they live — and reports each file that
9
9
  * imports `katex` as `lab-katex-direct`, with the chain of imports that
10
- * reaches it. A package import (`explico/…`, `react`) is not followed:
11
- * explico's own KaTeX use is the seam. A type-only import and the KaTeX
12
- * stylesheet draw nothing and are not reported.
10
+ * reaches it. The answer is per lesson: a helper that three lessons reach is
11
+ * reported three times, each with that lesson's own chain, and a lesson that
12
+ * reaches none is clean — so `content check <lesson>` can say so. A package
13
+ * import (`explico/…`, `react`) is not followed: explico's own KaTeX use is
14
+ * the seam. A type-only import and the KaTeX stylesheet draw nothing and are
15
+ * not reported.
13
16
  *
14
17
  * `figure-symbols.ts` checks the same promise on the rendered page, which
15
18
  * also catches math a lab draws some other way.
@@ -26,7 +29,7 @@ export interface LabKatexDiagnostic {
26
29
  readonly message: string;
27
30
  /** The file that imports `katex`, repository-relative. */
28
31
  readonly file: string;
29
- /** The first lesson (by id) whose imports reach it. */
32
+ /** The lesson whose imports reach it; one diagnostic per lesson and file. */
30
33
  readonly lessonId: string;
31
34
  }
32
35
 
@@ -94,26 +97,35 @@ function resolveRelative(fromFile: string, specifier: string): string | undefine
94
97
  }
95
98
 
96
99
  /**
97
- * Each course file that a lesson's imports reach and that imports `katex`.
98
- * `root` is the repository root the lesson paths are relative to.
100
+ * Each course file that imports `katex`, once for every lesson whose imports
101
+ * reach it, by the shortest chain. `root` is the repository root the lesson
102
+ * paths are relative to.
99
103
  */
100
104
  export function validateLabKatexImports(
101
105
  lessons: readonly LabKatexLessonInput[],
102
106
  root: string = process.cwd(),
103
107
  ): LabKatexDiagnostic[] {
108
+ // A component many lessons import is read once.
109
+ const sources = new Map<string, string | undefined>();
104
110
  const read = (path: string): string | undefined => {
105
- try {
106
- return readFileSync(path, 'utf8');
107
- } catch {
108
- return undefined;
111
+ if (!sources.has(path)) {
112
+ try {
113
+ sources.set(path, readFileSync(path, 'utf8'));
114
+ } catch {
115
+ sources.set(path, undefined);
116
+ }
109
117
  }
118
+ return sources.get(path);
110
119
  };
111
120
  const repoPath = (path: string) => relative(root, path).split(sep).join('/');
112
121
 
113
- const diagnostics = new Map<string, LabKatexDiagnostic>();
114
- const seen = new Set<string>();
122
+ const diagnostics: LabKatexDiagnostic[] = [];
115
123
  for (const lesson of [...lessons].sort((a, b) => a.lessonId.localeCompare(b.lessonId))) {
116
124
  const lessonPath = join(root, lesson.file);
125
+ // What this lesson reaches; another lesson reaching it too is not an answer
126
+ // for this one.
127
+ const reported = new Set<string>();
128
+ const seen = new Set<string>();
117
129
  // Breadth first, so a reported chain is the shortest one.
118
130
  const queue: { path: string; source: string; chain: readonly string[] }[] = [
119
131
  { path: lessonPath, source: stripFencedCode(lesson.body), chain: [lesson.file] },
@@ -123,8 +135,9 @@ export function validateLabKatexImports(
123
135
  for (const specifier of importSpecifiers(source)) {
124
136
  if (isKatexModule(specifier)) {
125
137
  const file = repoPath(path);
126
- if (!diagnostics.has(file)) {
127
- diagnostics.set(file, {
138
+ if (!reported.has(file)) {
139
+ reported.add(file);
140
+ diagnostics.push({
128
141
  code: 'lab-katex-direct',
129
142
  severity: 'error',
130
143
  file,
@@ -152,5 +165,7 @@ export function validateLabKatexImports(
152
165
  }
153
166
  }
154
167
  }
155
- return [...diagnostics.values()].sort((a, b) => a.file.localeCompare(b.file));
168
+ return diagnostics.sort(
169
+ (a, b) => a.file.localeCompare(b.file) || a.lessonId.localeCompare(b.lessonId),
170
+ );
156
171
  }
@@ -4,6 +4,7 @@ import {
4
4
  bindScopeToPage,
5
5
  registerPageNotationDependencies,
6
6
  routeIdFromLocals,
7
+ scopeLessonsOf,
7
8
  } from './page-notation';
8
9
 
9
10
  interface LessonNotationFrontmatter {
@@ -37,14 +38,45 @@ export async function labMathScope(
37
38
  body = '',
38
39
  component?: { readonly locals: object; readonly context: string },
39
40
  ): Promise<LabMathScopeDefinition[]> {
40
- const local = notation?.local ?? [];
41
+ const manifest = await loadManifest();
42
+ // On a deck beside its chapter (`docs/authoring/deck.md`), a lab shows its
43
+ // lesson's symbols: the chapter's `notation.local` and the keys it names,
44
+ // as the deck's own math does. On a part's deck, its chapters' symbols,
45
+ // the first chapter to define a key binding it.
46
+ const routeId = component ? routeIdFromLocals(component.locals) : undefined;
47
+ const deck = routeId === undefined
48
+ ? undefined
49
+ : manifest.decks?.find(
50
+ (entry) => entry.slug === routeId && (entry.chapter !== undefined || entry.part !== undefined),
51
+ );
52
+ const scopeLessons = deck ? scopeLessonsOf(manifest, deck.pageId) : [];
53
+ const lessonLocal: { key: string; latex: string; fixedIndex?: boolean }[] = [];
54
+ for (const lessonId of scopeLessons) {
55
+ for (const definition of manifest.notation.definitions) {
56
+ if (definition.kind !== 'local' || !definition.id.startsWith(`page:${lessonId}:`)) continue;
57
+ if (lessonLocal.some((known) => known.key === definition.key)) continue;
58
+ lessonLocal.push({
59
+ key: definition.key,
60
+ latex: definition.notation,
61
+ ...(definition.fixedIndex ? { fixedIndex: true } : {}),
62
+ });
63
+ }
64
+ }
65
+ const lessonKeys = scopeLessons.flatMap((lessonId) =>
66
+ (manifest.lessons.find((lesson) => lesson.id === lessonId)?.notation.bindings ?? []).map((binding) => binding.key),
67
+ );
68
+ const ownLocal = notation?.local ?? [];
69
+ const local = [
70
+ ...ownLocal,
71
+ ...lessonLocal.filter((definition) => !ownLocal.some((own) => own.key === definition.key)),
72
+ ];
41
73
  const scope: LabMathScopeDefinition[] = local.map((definition) => ({
42
74
  key: definition.key,
43
75
  notation: definition.latex,
44
76
  ...(definition.fixedIndex === true ? { fixedIndex: true as const } : {}),
45
77
  }));
46
78
 
47
- const referenced = new Set<string>();
79
+ const referenced = new Set<string>(lessonKeys);
48
80
  const explicitlyExplained = new Set<string>();
49
81
  for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
50
82
  for (const match of body.matchAll(pattern)) {
@@ -54,7 +86,6 @@ export async function labMathScope(
54
86
  }
55
87
 
56
88
  const localKeys = new Set(local.map((definition) => definition.key));
57
- const manifest = await loadManifest();
58
89
  const sharedByKey = new Map(
59
90
  manifest.notation.definitions
60
91
  .filter((definition) => definition.kind === 'shared')