explico 0.13.1 → 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 +879 -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 +1045 -16
  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 +15 -7
  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
@@ -1082,24 +1082,24 @@ export function isRunningIndexLetter(text) {
1082
1082
  return RUNNING_INDEX_LETTER.test(text);
1083
1083
  }
1084
1084
 
1085
+ const isRunningLetterNode = (node) =>
1086
+ node.type === 'mathord' && RUNNING_INDEX_LETTER.test(node.text ?? '');
1087
+
1085
1088
  /**
1086
- * What one index slot is: `running` (a lowercase letter), `digits`, `named`
1087
- * (a `\mathrm{…}` / `\text{…}` label, or a subscript the page declares fixed:
1088
- * an index entry `{}_x`, a letter card with `fixedIndex`), `symbol` (anything
1089
- * else: `N`, `\theta`, `T_{\mathrm{fwd}}`, `t-1`), or `empty`.
1089
+ * What one index slot is: `running` (a lowercase letter), `run` (two or more
1090
+ * juxtaposed lowercase letters, the double index of `x_{ij}`, each a running
1091
+ * letter), `digits`, `named` (a `\mathrm{…}` / `\text{…}` label, or a subscript
1092
+ * the page declares fixed: an index entry `{}_x`, a letter card with
1093
+ * `fixedIndex`), `symbol` (anything else: `N`, `\theta`, `T_{\mathrm{fwd}}`,
1094
+ * `t-1`), or `empty`.
1090
1095
  */
1091
1096
  function slotKind(slot, fixedSlots) {
1092
1097
  if (slot.length === 0) return 'empty';
1093
1098
  if (fixedSlots.has(sequenceSignature(slot))) return 'named';
1094
1099
  if (slot.every(isDigitNode)) return 'digits';
1095
1100
  if (slot.length === 1 && isLabelNode(slot[0])) return 'named';
1096
- if (
1097
- slot.length === 1 &&
1098
- slot[0].type === 'mathord' &&
1099
- RUNNING_INDEX_LETTER.test(slot[0].text ?? '')
1100
- ) {
1101
- return 'running';
1102
- }
1101
+ if (slot.length === 1 && isRunningLetterNode(slot[0])) return 'running';
1102
+ if (slot.length > 1 && slot.every(isRunningLetterNode)) return 'run';
1103
1103
  return 'symbol';
1104
1104
  }
1105
1105
 
@@ -1168,7 +1168,8 @@ function coreSourceSpan(node, latex) {
1168
1168
  * slots, the same superscript or none on the family, and each slot fits. A
1169
1169
  * running slot takes anything but a fixed label (`x_1`, `x_{k+1}`, `x_N`; not
1170
1170
  * `x_{\mathrm{e}}`), a `symbol` slot any symbol (`T_{\mathrm{opt}}` in place
1171
- * of `T_{\mathrm{fwd}}`), and a fixed slot only itself.
1171
+ * of `T_{\mathrm{fwd}}`), a `run` slot (`x_{ij}`) a run of as many letters
1172
+ * and digits (`x_{i1}`, `x_{12}`, `x_{ji}`), and a fixed slot only itself.
1172
1173
  */
1173
1174
  function isFamilyInstance(family, form, fixedSlots) {
1174
1175
  if (family.form.baseSignature !== form.baseSignature) return false;
@@ -1180,6 +1181,12 @@ function isFamilyInstance(family, form, fixedSlots) {
1180
1181
  const kind = family.kinds[index];
1181
1182
  const filler = form.slots[index];
1182
1183
  const fillerKind = slotKind(filler, fixedSlots);
1184
+ if (kind === 'run') {
1185
+ return (
1186
+ filler.length === slot.length &&
1187
+ filler.every((node) => isRunningLetterNode(node) || isDigitNode(node))
1188
+ );
1189
+ }
1183
1190
  if (kind === 'running' || kind === 'symbol') {
1184
1191
  return fillerKind !== 'named' && fillerKind !== 'empty';
1185
1192
  }
@@ -1462,7 +1469,13 @@ function buildGlyphIndex(definitions) {
1462
1469
  const indexEntries = new Map();
1463
1470
  const headForms = new Map();
1464
1471
  const fixedSlots = new Set();
1472
+ // Who fixed a subscript (rule N23), and the entries marked `fixedIndex`
1473
+ // themselves, so a symbol that would have been a family's instance can say
1474
+ // why it is not (`unresolvedIdentifiers`).
1475
+ const fixedBy = new Map();
1476
+ const fixedEntries = new Set();
1465
1477
  const indexedEntries = [];
1478
+ const indexedForms = new Map();
1466
1479
  // What each bare glyph's indexed entries are, for the message of a symbol
1467
1480
  // that no longer borrows one's card (rule N4).
1468
1481
  const indexedBases = new Map();
@@ -1537,6 +1550,7 @@ function buildGlyphIndex(definitions) {
1537
1550
  }
1538
1551
  indexEntries.set(subscript, key);
1539
1552
  fixedSlots.add(subscript);
1553
+ if (!fixedBy.has(subscript)) fixedBy.set(subscript, { key, via: 'index-entry', notation });
1540
1554
  continue;
1541
1555
  }
1542
1556
 
@@ -1545,10 +1559,14 @@ function buildGlyphIndex(definitions) {
1545
1559
  if (significant.length === 1 && indexedForm(significant[0])) {
1546
1560
  const form = indexedForm(significant[0]);
1547
1561
  indexedEntries.push({ key, form, fixedIndex });
1562
+ if (fixedIndex) fixedEntries.add(key);
1563
+ indexedForms.set(key, form);
1548
1564
  noteIndexedBase(form.baseSignature, key);
1549
1565
  }
1550
1566
  if (fixedIndex && significant.length === 1 && isIdentifierNode(significant[0])) {
1551
- fixedSlots.add(sequenceSignature(significant));
1567
+ const letter = sequenceSignature(significant);
1568
+ fixedSlots.add(letter);
1569
+ if (!fixedBy.has(letter)) fixedBy.set(letter, { key, via: 'fixedIndex', notation });
1552
1570
  }
1553
1571
  const indexedHead = indexedFunctionHead(tree);
1554
1572
  if (indexedHead) {
@@ -1586,14 +1604,14 @@ function buildGlyphIndex(definitions) {
1586
1604
  }
1587
1605
  }
1588
1606
 
1589
- // A family is an indexed entry with a running slot (`x_k`, `F_{t,T}`); one
1607
+ // A family is an indexed entry with a running slot (`x_k`, `F_{t,T}`, `x_{ij}`); one
1590
1608
  // whose every slot is fixed (`v_0`, `T_{\mathrm{opt}}`, or `fixedIndex`)
1591
1609
  // is a single symbol and lends its card to nothing.
1592
1610
  const families = new Map();
1593
1611
  for (const { key, form, fixedIndex } of indexedEntries) {
1594
1612
  if (fixedIndex) continue;
1595
1613
  const kinds = form.slots.map((slot) => slotKind(slot, fixedSlots));
1596
- if (!kinds.includes('running')) continue;
1614
+ if (!kinds.includes('running') && !kinds.includes('run')) continue;
1597
1615
  const list = families.get(form.baseSignature) ?? [];
1598
1616
  list.push({ key, form, kinds });
1599
1617
  families.set(form.baseSignature, list);
@@ -1608,7 +1626,10 @@ function buildGlyphIndex(definitions) {
1608
1626
  indexEntries,
1609
1627
  headForms,
1610
1628
  fixedSlots,
1629
+ fixedBy,
1630
+ fixedEntries,
1611
1631
  families,
1632
+ indexedForms,
1612
1633
  indexedBases,
1613
1634
  maxNodeLength,
1614
1635
  };
@@ -1651,7 +1672,12 @@ function resolveIndexedNode(node, glyphs, source, derive) {
1651
1672
 
1652
1673
  const head = sequenceSignature([node]);
1653
1674
  if (glyphs.headForms.has(head) && !glyphs.ambiguousBase.has(head)) {
1654
- return { key: glyphs.headForms.get(head), span: whole, match: 'canonical' };
1675
+ return {
1676
+ key: glyphs.headForms.get(head),
1677
+ span: whole,
1678
+ match: 'canonical',
1679
+ sub: sourceSpan(node.sub, source),
1680
+ };
1655
1681
  }
1656
1682
  if (form.sup !== 'none') {
1657
1683
  const signature = coreSignature(form);
@@ -1660,7 +1686,9 @@ function resolveIndexedNode(node, glyphs, source, derive) {
1660
1686
  (glyphs.ambiguousBase.has(signature)
1661
1687
  ? undefined
1662
1688
  : glyphs.headForms.get(signature));
1663
- if (key !== undefined) return { key, span: core, match: 'canonical' };
1689
+ if (key !== undefined) {
1690
+ return { key, span: core, match: 'canonical', sub: sourceSpan(node.sub, source) };
1691
+ }
1664
1692
  }
1665
1693
 
1666
1694
  const families = (glyphs.families.get(form.baseSignature) ?? []).filter(
@@ -1682,6 +1710,7 @@ function resolveIndexedNode(node, glyphs, source, derive) {
1682
1710
  key: family.key,
1683
1711
  span: family.form.sup !== 'none' ? whole : core,
1684
1712
  match: 'family',
1713
+ sub: sourceSpan(node.sub, source),
1685
1714
  };
1686
1715
  }
1687
1716
 
@@ -1698,6 +1727,33 @@ function resolveIndexedNode(node, glyphs, source, derive) {
1698
1727
  };
1699
1728
  }
1700
1729
 
1730
+ /**
1731
+ * The base of each indexed symbol an author wrapped whole in the `\explain` of
1732
+ * an indexed entry with that same base — `\sigma` of
1733
+ * `\explain{component-sd}{\sigma_1}` under the family `\sigma_k`. The wrap
1734
+ * names the indexed symbol, not its base, so the base takes no card of its own
1735
+ * from another entry (a plain `\sigma` on the page): a binding for it would
1736
+ * open the wrong card on the letter. The index inside keeps resolving.
1737
+ */
1738
+ function wrappedBaseSpans(indexed, explicitCalls, glyphs, latex, source) {
1739
+ const tight = (span) => (span ? unwrapEnclosingGroupSpan(latex, span) : span);
1740
+ const spans = [];
1741
+ for (const call of explicitCalls) {
1742
+ const entry = glyphs.indexedForms.get(call.key);
1743
+ if (!entry) continue;
1744
+ for (const node of indexed) {
1745
+ const form = indexedForm(node);
1746
+ if (form?.baseSignature !== entry.baseSignature) continue;
1747
+ const exactly = [coreSourceSpan(node, source), sourceSpan(node, source)].some(
1748
+ (span) => span && sameSpan(tight(span), call),
1749
+ );
1750
+ const base = tight(sourceSpan(node.base, source));
1751
+ if (exactly && base) spans.push(base);
1752
+ }
1753
+ }
1754
+ return spans;
1755
+ }
1756
+
1701
1757
  /**
1702
1758
  * Bind whole canonical forms: greedy longest match, left to right, per run.
1703
1759
  * A symbol with a subscript that no whole form matches exactly is then
@@ -1716,6 +1772,7 @@ function matchCanonicalForms(
1716
1772
  scriptSpans,
1717
1773
  derive,
1718
1774
  explicitCalls,
1775
+ wrappedBases = [],
1719
1776
  ) {
1720
1777
  const bindings = [];
1721
1778
  const claimed = [];
@@ -1729,6 +1786,7 @@ function matchCanonicalForms(
1729
1786
  };
1730
1787
 
1731
1788
  const blocked = (span, nodes) =>
1789
+ wrappedBases.some((base) => contains(base, span)) ||
1732
1790
  claimed.some(
1733
1791
  (existing) =>
1734
1792
  sameSpan(existing, span) ||
@@ -1755,7 +1813,11 @@ function matchCanonicalForms(
1755
1813
  // or `b'` wraps the decoration too, not just the letter under it.
1756
1814
  const rawSpan = sourceSpan(nodes, strippedSource);
1757
1815
  if (!rawSpan) continue;
1758
- found = { key, nodes, rawSpan, match: 'canonical' };
1816
+ const sub =
1817
+ nodes.length === 1 && nodes[0].type === 'supsub' && nodes[0].sub
1818
+ ? sourceSpan(nodes[0].sub, strippedSource)
1819
+ : undefined;
1820
+ found = { key, nodes, rawSpan, match: 'canonical', ...(sub ? { sub } : {}) };
1759
1821
  break;
1760
1822
  }
1761
1823
  // A group around one node (an `aligned` cell's) shares that node's
@@ -1788,6 +1850,7 @@ function matchCanonicalForms(
1788
1850
  level: 'scope',
1789
1851
  match: found.match,
1790
1852
  ...(found.derived ? { derived: found.derived } : {}),
1853
+ ...(found.sub ? { sub: found.sub } : {}),
1791
1854
  });
1792
1855
  claimed.push({ ...span, match: found.match });
1793
1856
  }
@@ -1890,26 +1953,109 @@ function injectScopeBindings(latex, bindings) {
1890
1953
  return output;
1891
1954
  }
1892
1955
 
1956
+ /** The kinds a slot has when nothing on the page fixes it. */
1957
+ const NO_FIXED_SLOTS = new Set();
1958
+
1959
+ /**
1960
+ * The families a bare base would have lent its card to, had a card not fixed
1961
+ * the letter their index runs over (rule N23), for a base that is written with
1962
+ * a subscript in the source: `r_2` under the family `r_m` once the card of `m`
1963
+ * says `fixedIndex: true`. The bare `r` is what stays unresolved, and "never a
1964
+ * bare r" would blame the wrong thing, so name the instance and what fixed the
1965
+ * letter. `undefined` when the identifier carries no subscript here, or no
1966
+ * lender fails for that reason.
1967
+ *
1968
+ * @returns {{ instance: string, fixedLenders: Array<{ key: string, via: string, card?: string, letter?: string }> } | undefined}
1969
+ */
1970
+ function fixedLendersOf(identifier, lenders, latex, glyphs, indexed) {
1971
+ if (lenders.length === 0) return undefined;
1972
+ const symbol = indexed.find((node) => flattenGroup(node.base)[0] === identifier);
1973
+ const form = symbol === undefined ? undefined : indexedForm(symbol);
1974
+ if (symbol === undefined || form === undefined) return undefined;
1975
+
1976
+ const fixedLenders = [];
1977
+ for (const key of lenders) {
1978
+ const lender = glyphs.indexedForms.get(key);
1979
+ if (lender === undefined) continue;
1980
+ const family = { form: lender, kinds: lender.slots.map((slot) => slotKind(slot, NO_FIXED_SLOTS)) };
1981
+ if (!family.kinds.includes('running') && !family.kinds.includes('run')) continue;
1982
+ if (!isFamilyInstance(family, form, NO_FIXED_SLOTS)) continue;
1983
+ if (glyphs.fixedEntries.has(key)) {
1984
+ fixedLenders.push({ key, via: 'entry' });
1985
+ continue;
1986
+ }
1987
+ for (const slot of lender.slots) {
1988
+ const by = glyphs.fixedBy.get(sequenceSignature(slot));
1989
+ if (by === undefined) continue;
1990
+ fixedLenders.push({ key, via: by.via, card: by.key, letter: by.notation });
1991
+ break;
1992
+ }
1993
+ }
1994
+ if (fixedLenders.length === 0) return undefined;
1995
+ const span = coreSourceSpan(symbol, latex);
1996
+ return {
1997
+ instance: span ? latex.slice(span.start, span.end) : '',
1998
+ fixedLenders,
1999
+ };
2000
+ }
2001
+
2002
+ /**
2003
+ * The math span a finding sits in, as an author wrote it: `$…$`, whitespace
2004
+ * collapsed, cut at 100 characters. A line number can drift (a loader that
2005
+ * hands over a body, a generated file), but the span can be searched for, so a
2006
+ * message that quotes it still leads to the spot.
2007
+ *
2008
+ * @param {string} latex
2009
+ * @returns {string}
2010
+ */
2011
+ export function mathExcerpt(latex) {
2012
+ const flat = String(latex).replace(/\s+/g, ' ').trim();
2013
+ return `$${flat.length > 100 ? `${flat.slice(0, 99)}…` : flat}$`;
2014
+ }
2015
+
2016
+ /**
2017
+ * What to tell an author whose subscripted symbol a family would have
2018
+ * explained, were its index not fixed (rule N23): the cause, and the repair
2019
+ * that fits it. One sentence, for an unresolved item's `fixedLenders`.
2020
+ *
2021
+ * @param {{ key: string, via: string, card?: string, letter?: string }} lender
2022
+ * @param {string} instance The subscripted symbol as written, `r_2`.
2023
+ * @returns {string}
2024
+ */
2025
+ export function fixedLenderMessage(lender, instance) {
2026
+ const own = `give ${instance} an entry of its own (latex: ${instance})`;
2027
+ const lends = `${lender.key} is one symbol and lends its card to nothing, ${instance} included`;
2028
+ switch (lender.via) {
2029
+ case 'entry':
2030
+ return `${instance} would be an instance of ${lender.key}, but ${lender.key} is marked fixedIndex, so it is one symbol and lends its card to nothing (rule N23). Drop fixedIndex there if its index runs over a set, or ${own}.`;
2031
+ case 'index-entry':
2032
+ return `${instance} would be an instance of ${lender.key}, but the index entry ${lender.card} fixes that subscript, so ${lends} (rule N23). Drop that index entry, or ${own}.`;
2033
+ default:
2034
+ return `${instance} would be an instance of ${lender.key}, but ${lender.letter} is marked fixedIndex on ${lender.card}, so ${lends} (rule N23). If ${lender.letter} runs over a set, drop fixedIndex from ${lender.card} and give it a range; if it is a label, ${own}.`;
2035
+ }
2036
+ }
2037
+
1893
2038
  /**
1894
2039
  * Every identifier no binding covers, plus every decorated form whose
1895
2040
  * decoration is suspended on this page and that no whole form covers (rule
1896
2041
  * N22). A letter under such a decoration is reported once, as the decorated
1897
2042
  * form (`b''''`, not `b`): that whole form is what needs the entry.
1898
2043
  */
1899
- function unresolvedIdentifiers(latex, identifiers, bindings, suspended, indexedBases) {
2044
+ function unresolvedIdentifiers(latex, identifiers, bindings, suspended, glyphs, indexed) {
1900
2045
  const letters = identifiers
1901
2046
  .filter(
1902
2047
  (identifier) =>
1903
2048
  !bindings.some((binding) => contains(binding, identifier)) &&
1904
2049
  !suspended.some((decoration) => contains(decoration.part, identifier)),
1905
2050
  )
1906
- .map(({ start, end, font, signature }) => {
2051
+ .map(({ node, start, end, font, signature }) => {
1907
2052
  // Report `\mathbb{E}`, not a bare `E`, so the fix hint points at the
1908
2053
  // whole token the author has to wrap in `\explain`.
1909
2054
  const wrapped = expandFontWrapperSpan(latex, { start, end });
1910
2055
  // Rule N23: an indexed entry lends its card to no bare glyph, so name
1911
2056
  // the ones this letter used to borrow from.
1912
- const lenders = font === undefined ? (indexedBases?.get(signature) ?? []) : [];
2057
+ const lenders = font === undefined ? (glyphs.indexedBases?.get(signature) ?? []) : [];
2058
+ const fixed = fixedLendersOf(node, lenders, latex, glyphs, indexed);
1913
2059
  return {
1914
2060
  token: latex.slice(wrapped.start, wrapped.end),
1915
2061
  // A styled base-library letter (`\mathbf{e}`) is not Euler's number.
@@ -1921,6 +2067,7 @@ function unresolvedIdentifiers(latex, identifiers, bindings, suspended, indexedB
1921
2067
  end: wrapped.end,
1922
2068
  ...(font === undefined ? {} : { font }),
1923
2069
  ...(lenders.length === 0 ? {} : { lenders: [...lenders] }),
2070
+ ...(fixed === undefined ? {} : fixed),
1924
2071
  };
1925
2072
  })
1926
2073
  .filter(({ libraryToken }) => !BASE_LIBRARY.has(libraryToken))
@@ -1954,7 +2101,10 @@ function unresolvedIdentifiers(latex, identifiers, bindings, suspended, indexedB
1954
2101
  *
1955
2102
  * `running` lists the running index letters (`i`, `k`, `t`) in its slots that
1956
2103
  * the page has not declared fixed, with the key of the letter's own card and
1957
- * of the entry that covers the symbol, for the index-set check.
2104
+ * of the entry that covers the symbol, for the index-set check. A letter that
2105
+ * belongs to a larger symbol with its own card (`b` of `b_n` in
2106
+ * `\nabla_{b_n}`) and every letter of a slot that holds a relation
2107
+ * (`\mathbb{E}_{Z\sim g}`, a condition) is not an index and is not listed.
1958
2108
  */
1959
2109
  function classifyIndexed(nodes, context) {
1960
2110
  const { latex, source, bindings, explicitCalls, glyphs, identifiers, unresolved } =
@@ -1978,6 +2128,8 @@ function classifyIndexed(nodes, context) {
1978
2128
  const slots = indexSlots(node.sub).map((slot) => ({
1979
2129
  span: tight(sourceSpan(slot, source)),
1980
2130
  kind: slotKind(slot, glyphs.fixedSlots),
2131
+ // `Z\sim g` in `\mathbb{E}_{Z\sim g}` states a condition, not a position.
2132
+ condition: slot.some((item) => item.type === 'atom' && item.family === 'rel'),
1981
2133
  }));
1982
2134
  const exactly = (span) => sameSpan(span, core) || sameSpan(span, whole);
1983
2135
 
@@ -2037,7 +2189,7 @@ function classifyIndexed(nodes, context) {
2037
2189
  // A swallowed index is already an error; its letters wait for the fix.
2038
2190
  if (!['inside-wrap', 'derived', 'unresolved', 'swallowed'].includes(how)) {
2039
2191
  for (const slot of slots) {
2040
- if (slot.kind === 'named' || !slot.span) continue;
2192
+ if (slot.kind === 'named' || slot.condition || !slot.span) continue;
2041
2193
  for (const identifier of identifiers) {
2042
2194
  if (identifier.font !== undefined) continue;
2043
2195
  if (identifier.node.type !== 'mathord') continue;
@@ -2049,6 +2201,10 @@ function classifyIndexed(nodes, context) {
2049
2201
  contains(binding, identifier) && contains(slot.span, binding),
2050
2202
  )
2051
2203
  .sort((left, right) => size(left) - size(right))[0];
2204
+ // A letter inside a larger symbol with a card of its own is part of
2205
+ // that symbol, not an index: `b` of `b_n` in `\nabla_{b_n}`. The
2206
+ // inner symbol is classified in its own right.
2207
+ if (own && own.level === 'scope' && size(own) > size(identifier)) continue;
2052
2208
  running.push({
2053
2209
  letter: identifier.node.text,
2054
2210
  start: identifier.start,
@@ -2420,10 +2576,17 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
2420
2576
  scriptSpans,
2421
2577
  derive,
2422
2578
  explicitCalls,
2579
+ wrappedBaseSpans(indexed, explicitCalls, glyphs, latex, strippedSource),
2423
2580
  ).filter(
2424
2581
  (binding) =>
2425
2582
  !explicitCalls.some(
2426
- (call) => overlaps(binding, call) && !contains(call, binding),
2583
+ (call) =>
2584
+ overlaps(binding, call) &&
2585
+ !contains(call, binding) &&
2586
+ // A symbol that holds an `\explain` in its own subscript
2587
+ // (`\beta_{\explain{count}{K}}`) encloses the call whole: the wraps
2588
+ // nest, and the index keeps its card.
2589
+ !(binding.sub !== undefined && contains(binding.sub, call)),
2427
2590
  ),
2428
2591
  );
2429
2592
  const covered = [...canonical, ...explicitCalls];
@@ -2518,7 +2681,8 @@ export function resolveMathGlyphs(latex, definitions, options = {}) {
2518
2681
  identifiers,
2519
2682
  bindings,
2520
2683
  suspendedDecorations,
2521
- glyphs.indexedBases,
2684
+ glyphs,
2685
+ indexed,
2522
2686
  );
2523
2687
  const derived = new Map();
2524
2688
  for (const binding of bindings) {
@@ -1,8 +1,8 @@
1
- import { Manifest, ManifestDerivedNotation, ManifestLessonNotation } from '../compiler/manifest.js';
1
+ import { Manifest, ManifestDerivedNotation, ManifestLessonNotation, ManifestLesson } from '../compiler/manifest.js';
2
2
  import { DerivedUse } from './types.js';
3
3
  import '../curriculum/validation.js';
4
4
  import './consistency.js';
5
- import '../course-config-rRtME5UV.js';
5
+ import '../course-config-BLtIdkei.js';
6
6
  import '../rules/catalog.js';
7
7
  import '../rules/course-rules.js';
8
8
  import '../compiler/collections.js';
@@ -23,6 +23,10 @@ import './mixed-content.js';
23
23
  import './gloss.js';
24
24
  import '../rules/waivers.js';
25
25
  import '../compiler/knowledge-version.js';
26
+ import '../compiler/page-versions.js';
27
+ import '../compiler/decks.js';
28
+ import './decks-validate.js';
29
+ import './deck-source.js';
26
30
  import '../compiler/lesson-derivation.js';
27
31
 
28
32
  interface PageNotationDependency {
@@ -66,6 +70,21 @@ declare function notationKeysInMathNodes(nodes: readonly {
66
70
  readonly properties?: Readonly<Record<string, string>>;
67
71
  readonly children?: readonly unknown[];
68
72
  }[]): string[];
73
+ /**
74
+ * The compiled notation of a page: a lesson's (a chapter, or a slides-first
75
+ * lesson's deck), a page that is not a chapter (rule S10), or a deck beside
76
+ * its chapter (`manifest.decks`, by its page id).
77
+ */
78
+ declare function pageNotationEntry(manifest: Manifest, pageId: string): {
79
+ readonly notation: ManifestLesson['notation'];
80
+ } | undefined;
81
+ /**
82
+ * The lessons whose `notation.local` a page's symbols bind in, the first to
83
+ * define a key binding it (`NotationLessonInput.scopeLessonIds`): a lesson's
84
+ * deck's is its lesson; a part's deck's, every chapter of the part in
85
+ * reading order; any other page's, its own.
86
+ */
87
+ declare function scopeLessonsOf(manifest: Manifest, pageId: string): readonly string[];
69
88
  /**
70
89
  * The derived cards (rule N23) a page uses: the ones its body and assessment
71
90
  * math use, as the manifest composed them, and the ones a lab or a diagram on
@@ -86,4 +105,4 @@ declare function notationDefinitionsForRenderedPage(manifest: Manifest, pageId:
86
105
  */
87
106
  declare function missingNotationPayloadKeys(html: string): string[];
88
107
 
89
- export { type PageNotationDependency, type ScopePage, bindScopeToPage, derivedNotationForPage, missingNotationPayloadKeys, notationDefinitionsForRenderedPage, notationKeysInMathNodes, pageNotationDependencies, pageOfScope, registerPageDerivedNotation, registerPageNotationDependencies, routeIdFromLocals };
108
+ export { type PageNotationDependency, type ScopePage, bindScopeToPage, derivedNotationForPage, missingNotationPayloadKeys, notationDefinitionsForRenderedPage, notationKeysInMathNodes, pageNotationDependencies, pageNotationEntry, pageOfScope, registerPageDerivedNotation, registerPageNotationDependencies, routeIdFromLocals, scopeLessonsOf };
@@ -2,9 +2,18 @@ import {
2
2
  formulaExplainKeys,
3
3
  formulaGlyphScope
4
4
  } from "./gloss.js";
5
- import { resolveReferenceId } from "./references.js";
5
+ import { resolvePageReferenceId, resolveReferenceId } from "./references.js";
6
6
  import { derivedNotationEntries } from "./derived-entries.js";
7
- const dependenciesByLocals = /* @__PURE__ */ new WeakMap();
7
+ const REGISTRY_KEY = /* @__PURE__ */ Symbol.for("explico.reference.page-notation");
8
+ function sharedRegistry() {
9
+ const host = globalThis;
10
+ return host[REGISTRY_KEY] ??= {
11
+ dependenciesByLocals: /* @__PURE__ */ new WeakMap(),
12
+ derivedByLocals: /* @__PURE__ */ new WeakMap(),
13
+ pageByScope: /* @__PURE__ */ new WeakMap()
14
+ };
15
+ }
16
+ const { dependenciesByLocals, derivedByLocals, pageByScope } = sharedRegistry();
8
17
  function registerPageNotationDependencies(locals, keys, context) {
9
18
  let byKey = dependenciesByLocals.get(locals);
10
19
  if (!byKey) {
@@ -20,7 +29,6 @@ function registerPageNotationDependencies(locals, keys, context) {
20
29
  contexts.add(context);
21
30
  }
22
31
  }
23
- const derivedByLocals = /* @__PURE__ */ new WeakMap();
24
32
  function registerPageDerivedNotation(locals, uses, context) {
25
33
  let byKey = derivedByLocals.get(locals);
26
34
  if (!byKey) {
@@ -34,7 +42,6 @@ function registerPageDerivedNotation(locals, uses, context) {
34
42
  }
35
43
  registerPageNotationDependencies(locals, entries, context);
36
44
  }
37
- const pageByScope = /* @__PURE__ */ new WeakMap();
38
45
  function bindScopeToPage(scope, page) {
39
46
  pageByScope.set(scope, page);
40
47
  }
@@ -88,8 +95,21 @@ function manifestDefinition(definition, cardsByKey) {
88
95
  ...definition.range === void 0 ? {} : { range: definition.range }
89
96
  };
90
97
  }
98
+ function pageNotationEntry(manifest, pageId) {
99
+ return manifest.lessons.find((entry) => entry.id === pageId) ?? manifest.pages.find((entry) => entry.id === pageId) ?? (manifest.decks ?? []).find((entry) => entry.pageId === pageId);
100
+ }
101
+ function scopeLessonsOf(manifest, pageId) {
102
+ const deck = (manifest.decks ?? []).find((entry) => entry.pageId === pageId);
103
+ if (deck?.part !== void 0) {
104
+ const chapters = new Set(
105
+ manifest.lessons.filter((lesson) => (lesson.editions ?? ["chapter"]).includes("chapter")).map((lesson) => lesson.id)
106
+ );
107
+ return (manifest.parts.find((part) => part.dir === deck.part)?.lessons ?? []).filter((id) => chapters.has(id));
108
+ }
109
+ return [deck?.lessonId ?? pageId];
110
+ }
91
111
  function derivedNotationForPage(manifest, pageId, locals) {
92
- const page = manifest.lessons.find((entry) => entry.id === pageId) ?? manifest.pages.find((entry) => entry.id === pageId);
112
+ const page = pageNotationEntry(manifest, pageId);
93
113
  const compiled = page?.notation.derived ?? [];
94
114
  const registered = [...(locals && derivedByLocals.get(locals)?.values()) ?? []].filter(
95
115
  (use) => !compiled.some((entry) => entry.key === use.key)
@@ -102,7 +122,7 @@ function derivedNotationForPage(manifest, pageId, locals) {
102
122
  const records = /* @__PURE__ */ new Map();
103
123
  for (const use of registered) {
104
124
  for (const key of [use.base, use.index]) {
105
- const id = resolveReferenceId(key, { kind: "page", lessonId: pageId }, availableIds);
125
+ const id = resolvePageReferenceId(key, scopeLessonsOf(manifest, pageId), availableIds);
106
126
  const record = id === void 0 ? void 0 : definitionsById.get(id);
107
127
  if (record) records.set(key, record);
108
128
  }
@@ -115,7 +135,7 @@ function derivedNotationForPage(manifest, pageId, locals) {
115
135
  );
116
136
  }
117
137
  function notationDefinitionsForRenderedPage(manifest, pageId, dependencies) {
118
- const page = manifest.lessons.find((entry) => entry.id === pageId) ?? manifest.pages.find((entry) => entry.id === pageId);
138
+ const page = pageNotationEntry(manifest, pageId);
119
139
  if (!page) return [];
120
140
  const definitionsById = new Map(
121
141
  manifest.notation.definitions.map((definition) => [definition.id, definition])
@@ -130,9 +150,9 @@ function notationDefinitionsForRenderedPage(manifest, pageId, dependencies) {
130
150
  contextsByKey.set(dependency.key, contexts);
131
151
  }
132
152
  contexts.add(dependency.context);
133
- const definitionId = resolveReferenceId(
153
+ const definitionId = resolvePageReferenceId(
134
154
  dependency.key,
135
- { kind: "page", lessonId: pageId },
155
+ scopeLessonsOf(manifest, pageId),
136
156
  availableIds
137
157
  );
138
158
  if (!definitionId) {
@@ -216,8 +236,10 @@ export {
216
236
  notationDefinitionsForRenderedPage,
217
237
  notationKeysInMathNodes,
218
238
  pageNotationDependencies,
239
+ pageNotationEntry,
219
240
  pageOfScope,
220
241
  registerPageDerivedNotation,
221
242
  registerPageNotationDependencies,
222
- routeIdFromLocals
243
+ routeIdFromLocals,
244
+ scopeLessonsOf
223
245
  };
@@ -21,7 +21,20 @@ declare function definitionId(scope: NotationDefinitionScope, key: string): stri
21
21
  * can only see shared definitions, so their meaning cannot change by caller.
22
22
  */
23
23
  declare function resolveReferenceId(key: string, scope: NotationDefinitionScope, availableDefinitionIds: ReadonlySet<string>): string | undefined;
24
+ /**
25
+ * Resolve on a page whose symbols bind in several lessons' scopes, as a
26
+ * part's deck binds in its chapters' (`NotationLessonInput.scopeLessonIds`):
27
+ * the first lesson's local entry for the key, then the next lesson's, then
28
+ * the shared entry. With one lesson it is {@link resolveReferenceId}'s page
29
+ * scope.
30
+ */
31
+ declare function resolvePageReferenceId(key: string, lessonIds: readonly string[], availableDefinitionIds: ReadonlySet<string>): string | undefined;
32
+ /** The lessons whose `notation.local` a page's symbols bind in: a deck's, or the page's own. */
33
+ declare function pageScopeLessonIds(lesson: {
34
+ readonly lessonId: string;
35
+ readonly scopeLessonIds?: readonly string[];
36
+ }): readonly string[];
24
37
  declare function compareSourceSpans(left: SourceSpan | undefined, right: SourceSpan | undefined): number;
25
38
  declare function compareDiagnostics(left: NotationDiagnostic, right: NotationDiagnostic): number;
26
39
 
27
- export { NOTATION_KEY_PATTERN, RESERVED_KEY_PREFIXES, compareDiagnostics, compareSourceSpans, definitionId, isNotationKey, localDefinitionId, reservedKeyPrefix, resolveReferenceId, sharedDefinitionId };
40
+ export { NOTATION_KEY_PATTERN, RESERVED_KEY_PREFIXES, compareDiagnostics, compareSourceSpans, definitionId, isNotationKey, localDefinitionId, pageScopeLessonIds, reservedKeyPrefix, resolvePageReferenceId, resolveReferenceId, sharedDefinitionId };
@@ -23,6 +23,17 @@ function resolveReferenceId(key, scope, availableDefinitionIds) {
23
23
  const sharedId = sharedDefinitionId(key);
24
24
  return availableDefinitionIds.has(sharedId) ? sharedId : void 0;
25
25
  }
26
+ function resolvePageReferenceId(key, lessonIds, availableDefinitionIds) {
27
+ for (const lessonId of lessonIds) {
28
+ const localId = localDefinitionId(lessonId, key);
29
+ if (availableDefinitionIds.has(localId)) return localId;
30
+ }
31
+ const sharedId = sharedDefinitionId(key);
32
+ return availableDefinitionIds.has(sharedId) ? sharedId : void 0;
33
+ }
34
+ function pageScopeLessonIds(lesson) {
35
+ return lesson.scopeLessonIds ?? [lesson.lessonId];
36
+ }
26
37
  function compareSourceSpans(left, right) {
27
38
  if (left === void 0) return right === void 0 ? 0 : 1;
28
39
  if (right === void 0) return -1;
@@ -43,7 +54,9 @@ export {
43
54
  definitionId,
44
55
  isNotationKey,
45
56
  localDefinitionId,
57
+ pageScopeLessonIds,
46
58
  reservedKeyPrefix,
59
+ resolvePageReferenceId,
47
60
  resolveReferenceId,
48
61
  sharedDefinitionId
49
62
  };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The glyph table a notation entry's Markdown body resolves against, and the
3
+ * advice for a symbol it does not bind. The remark pass renders an entry's
4
+ * body with this table (`remark-notation.mjs`) and `validate:content` checks
5
+ * the same body with it (`gate-math.ts`), so the fast gate and the build read
6
+ * one table and cannot disagree on what binds.
7
+ *
8
+ * The table is every shared entry. An entry's body has no `notation.local`,
9
+ * names its neighbours without `[[key]]`, suspends no decoration (rule N22)
10
+ * and derives no cards (rule N23, a page's own): resolve it with
11
+ * `{ suspended: [], derive: false }`.
12
+ */
13
+ /**
14
+ * One row per shared definition. `fixedIndex` is kept (rule N23): an entry
15
+ * whose index is a fixed label is no family, in an entry's body as on a page.
16
+ *
17
+ * @param {Iterable<{ readonly key: string; readonly notation: string; readonly fixedIndex?: boolean }>} definitions
18
+ * @returns {{ key: string; notation: string; fixedIndex?: true }[]}
19
+ */
20
+ export function registryGlyphScope(definitions: Iterable<{
21
+ readonly key: string;
22
+ readonly notation: string;
23
+ readonly fixedIndex?: boolean;
24
+ }>): {
25
+ key: string;
26
+ notation: string;
27
+ fixedIndex?: true;
28
+ }[];
29
+ /**
30
+ * Every shared entry is already in scope, so neither `[[key]]` nor
31
+ * `notation.local` can bind a symbol the table missed.
32
+ */
33
+ export const REGISTRY_BODY_FIX: "A notation entry\u2019s body binds every shared entry and nothing else: give the symbol an entry of its own in content/notation/, or wrap it in \\explain{key}{latex} with the entry it stands for.";