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
@@ -37,6 +37,8 @@ import {
37
37
  GlyphResolutionError,
38
38
  baseSymbolLatex,
39
39
  derivedNotationLatex,
40
+ fixedLenderMessage,
41
+ mathExcerpt,
40
42
  resolveMathGlyphs,
41
43
  stripNotationMarkers,
42
44
  } from './math-glyphs.mjs';
@@ -62,7 +64,23 @@ import {
62
64
  parseKnowledgeCheckRef,
63
65
  } from './knowledge-checks.mjs';
64
66
  import { injectMdxStringAttributes } from './mdx-attributes.mjs';
67
+ import {
68
+ collectDeckFloats,
69
+ deckOfFile,
70
+ deckScopeChapters,
71
+ deckTeaches,
72
+ expandRecalls,
73
+ insertLegends,
74
+ manifestDeckOf,
75
+ markDeckChecks,
76
+ outcomesFrameNode,
77
+ readChapter,
78
+ stampDeckHeadings,
79
+ titleFrameNode,
80
+ } from './deck-remark.mjs';
65
81
  import { collectSectionHeadings, parseSectionRef } from './sections.mjs';
82
+ import { REGISTRY_BODY_FIX, registryGlyphScope } from './registry-scope.mjs';
83
+ import { sourceLineOffset, shiftedPlace } from './source-lines.mjs';
66
84
 
67
85
  const PROSE_SKIP_TYPES = new Set([
68
86
  'code',
@@ -340,7 +358,7 @@ function readBracedGroup(value, start) {
340
358
  }
341
359
 
342
360
  function fail(file, message, node) {
343
- if (typeof file?.fail === 'function') file.fail(message, node);
361
+ if (typeof file?.fail === 'function') file.fail(message, shiftedPlace(file, node));
344
362
 
345
363
  const location = file?.path ? `${file.path}: ` : '';
346
364
  throw new Error(`${location}${message}`);
@@ -348,7 +366,7 @@ function fail(file, message, node) {
348
366
 
349
367
  function warn(file, message, node) {
350
368
  if (typeof file?.message === 'function') {
351
- file.message(message, node);
369
+ file.message(message, shiftedPlace(file, node));
352
370
  return;
353
371
  }
354
372
  console.warn(message);
@@ -365,7 +383,7 @@ function tokenLocation(file, node, value, offset) {
365
383
  if (!start || typeof start.line !== 'number') return path;
366
384
  const before = String(value).slice(0, offset);
367
385
  const newlines = before.split('\n').length - 1;
368
- const line = start.line + newlines;
386
+ const line = start.line + sourceLineOffset(file) + newlines;
369
387
  const column =
370
388
  newlines === 0
371
389
  ? (start.column ?? 1) + offset
@@ -442,13 +460,16 @@ function inlineEquationNode(mathNode) {
442
460
  * author-written one; either way the reader copies plain LaTeX, never this
443
461
  * system's own notation-key plumbing (`math-glyphs.mjs:stripNotationMarkers`).
444
462
  */
445
- function equationNode(mathNode, { key, number, short }) {
463
+ function equationNode(mathNode, { key, number, short, href }) {
446
464
  const id = equationAnchorId({ key, number });
447
465
  const hProperties = {
448
466
  id,
449
467
  className: ['keyed-equation'],
450
468
  'data-eq-number': number,
451
469
  };
470
+ // A deck's recall of its chapter's equation: the chapter's number, which
471
+ // links to the chapter (`deck-remark.mjs`).
472
+ if (href) hProperties['data-eq-recall'] = href;
452
473
  if (key) hProperties['data-eq-key'] = key;
453
474
  else hProperties['data-eq-auto'] = '';
454
475
  // The `\caption{}` short caption names the equation wherever it is shown
@@ -465,7 +486,7 @@ function equationNode(mathNode, { key, number, short }) {
465
486
  el('div', { className: ['keyed-equation__controls'] }, [
466
487
  {
467
488
  type: 'link',
468
- url: `#${id}`,
489
+ url: href ?? `#${id}`,
469
490
  children: [{ type: 'text', value: `(${number})` }],
470
491
  data: {
471
492
  hProperties: {
@@ -515,6 +536,20 @@ function injectFloatNumberAttributes(node, float, prefix) {
515
536
  });
516
537
  }
517
538
 
539
+ /**
540
+ * A float on a deck page: a recalled one shows the chapter's number, which
541
+ * links to the chapter; the deck's own is unnumbered (rule D8) and anchors at
542
+ * its key, or at its place among the deck's floats of its kind.
543
+ */
544
+ function injectDeckFloatAttributes(node, float) {
545
+ // The component sees it is on a deck (`components/deck/deck-context.ts`):
546
+ // it takes no number of its own, and a recalled one links to its chapter.
547
+ injectMdxStringAttributes(node, {
548
+ ...(float.number === undefined ? {} : { number: float.number }),
549
+ anchorId: float.key ? `${float.prefix}-${float.key}` : `${float.prefix}-deck-${float.position}`,
550
+ });
551
+ }
552
+
518
553
  /**
519
554
  * The URL path Starlight serves a docs page at: `motion/index.mdx` is
520
555
  * `/motion/`, `motion/speed.mdx` is `/motion/speed/`, `index.mdx` is `/`.
@@ -646,6 +681,29 @@ function unnumberedFloatMessage(tagName, component) {
646
681
  );
647
682
  }
648
683
 
684
+ /**
685
+ * The chapter a deck's unqualified `[[eq-…]]`, `[[fig-…]]` or `[[sec-…]]`
686
+ * names when the deck shows no such float or frame itself: its chapter's,
687
+ * or on a part's deck the one covered chapter whose table (`crossPage`, by
688
+ * lesson slug) has `key`. Two that do leave it open: the author writes
689
+ * `[[<chapter>#…]]`.
690
+ */
691
+ function fallbackLessonFor(ctx, key, written, file, node) {
692
+ const lessons = ctx.fallbackLessons;
693
+ if (lessons.length === 1) return lessons[0];
694
+ const having = lessons.filter((lesson) => ctx.crossPage?.[lesson]?.[key] !== undefined);
695
+ if (having.length !== 1) {
696
+ fail(
697
+ file,
698
+ having.length === 0
699
+ ? `Unknown reference ${written}: neither this deck nor a chapter it covers (${lessons.join(', ')}) has it.`
700
+ : `Reference ${written} on a part's deck: ${having.join(' and ')} each have one. Name the chapter: [[<chapter>#…]].`,
701
+ node,
702
+ );
703
+ }
704
+ return having[0];
705
+ }
706
+
649
707
  /** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
650
708
  function resolveEquationRef(eqRef, eqCtx, file, node) {
651
709
  if (eqRef.lesson) {
@@ -663,6 +721,11 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
663
721
  };
664
722
  }
665
723
  const number = eqCtx.pageLabels.get(eqRef.key);
724
+ // On a deck, an equation the deck neither labels nor recalls is its chapter's.
725
+ if (number === undefined && eqCtx.fallbackLessons) {
726
+ const lesson = fallbackLessonFor(eqCtx, eqRef.key, `[[eq-${eqRef.key}]]`, file, node);
727
+ return resolveEquationRef({ lesson, key: eqRef.key }, eqCtx, file, node);
728
+ }
666
729
  if (number === undefined) {
667
730
  fail(
668
731
  file,
@@ -694,6 +757,16 @@ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
694
757
  };
695
758
  }
696
759
  const number = ctx.pageLabels.get(floatRef.key);
760
+ // On a deck, a float the deck does not recall is its chapter's.
761
+ if (number === undefined && ctx.fallbackLessons) {
762
+ return resolveKeyedFloatRef(
763
+ { lesson: fallbackLessonFor(ctx, floatRef.key, `[[${prefix}-${floatRef.key}]]`, file, node), key: floatRef.key },
764
+ ctx,
765
+ { prefix, tagName },
766
+ file,
767
+ node,
768
+ );
769
+ }
697
770
  if (number === undefined) {
698
771
  fail(
699
772
  file,
@@ -711,6 +784,10 @@ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
711
784
  * reference never prints a number the reader cannot find on the page.
712
785
  */
713
786
  function resolveSectionRef(secRef, ctx, file, node) {
787
+ if (!secRef.lesson && ctx.fallbackLessons && !ctx.pageHeadings.has(secRef.key)) {
788
+ const lesson = fallbackLessonFor(ctx, secRef.key, `[[sec-${secRef.key}]]`, file, node);
789
+ return resolveSectionRef({ ...secRef, lesson }, ctx, file, node);
790
+ }
714
791
  const heading = secRef.lesson
715
792
  ? ctx.crossPage?.[secRef.lesson]?.[secRef.key]
716
793
  : ctx.pageHeadings.get(secRef.key);
@@ -1471,6 +1548,9 @@ function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }
1471
1548
  * covers every catalogued lesson.
1472
1549
  */
1473
1550
  export default function remarkNotation(options = {}) {
1551
+ // The processor this plugin runs in: a deck's `<Recall>` parses its chapter
1552
+ // with the page's own parser (`deck-remark.mjs`).
1553
+ const processor = this;
1474
1554
  const unknown = options.unknown ?? 'error';
1475
1555
  const strict = options.strict ?? false;
1476
1556
  // The manifest supplies every numbering input at once; an explicit option
@@ -1495,13 +1575,44 @@ export default function remarkNotation(options = {}) {
1495
1575
  ? options.definitions()
1496
1576
  : options.definitions;
1497
1577
  const sharedDefinitions = definitionsMap(suppliedDefinitions);
1498
- const registryMathDefinitions = [...sharedDefinitions.values()].map(
1499
- (definition) => ({ key: definition.key, notation: definition.notation }),
1500
- );
1501
- const localDefinitions = localDefinitionsFromFile(file);
1502
- const suspendedOperators = suspendedOperatorsFromFile(file);
1503
- const lessonId = lessonIdFrom(file);
1578
+ // The same table `validate:content` checks an entry's body against.
1579
+ const registryMathDefinitions = registryGlyphScope(sharedDefinitions.values());
1580
+ // A deck (`<lesson>.deck.mdx`) is a page of its lesson: its symbols bind
1581
+ // in the chapter's scope, and its numbers are the chapter's for what it
1582
+ // recalls and its own for what it labels (`deck-remark.mjs`). A part's
1583
+ // deck (`<part>/index.deck.mdx`) binds in its chapters' scopes, the
1584
+ // first to define a key binding it, and recalls from the chapters its
1585
+ // sections cover.
1586
+ const deck = deckOfFile(file);
1587
+ const deckEntry = deck ? manifestDeckOf(options.manifest, deck) : undefined;
1588
+ const scopeChapters = deck ? deckScopeChapters(deck, options.manifest).map((path) => readChapter(path)) : [];
1589
+ const localDefinitions = deck?.scope === 'part' || scopeChapters.length > 0
1590
+ ? new Map(
1591
+ scopeChapters
1592
+ .flatMap((chapter) => [...definitionsMap(chapter.data.notation?.local)])
1593
+ .filter(([key], index, all) => all.findIndex(([other]) => other === key) === index),
1594
+ )
1595
+ : localDefinitionsFromFile(file);
1596
+ const suspendedOperators = deck?.scope === 'part' || scopeChapters.length > 0
1597
+ ? [...new Set(scopeChapters.flatMap((chapter) => suspendedOperatorsFromFile({ data: chapter.data })))]
1598
+ : suspendedOperatorsFromFile(file);
1599
+ const lessonId = deck ? (deck.scope === 'part' ? deck.pageId : deck.lessonId) : lessonIdFrom(file);
1504
1600
  const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
1601
+ const failHere = (message, node) => fail(file, message, node);
1602
+ if (deck) {
1603
+ expandRecalls(tree, {
1604
+ processor,
1605
+ deck,
1606
+ deckEntry,
1607
+ manifest: options.manifest,
1608
+ base: options.base,
1609
+ fail: failHere,
1610
+ });
1611
+ if (deckEntry) stampDeckHeadings(tree, deckEntry, failHere);
1612
+ // A question frame on a part's deck asks its section's chapter's checks.
1613
+ if (deckEntry?.part !== undefined) markDeckChecks(tree, deckEntry, failHere);
1614
+ }
1615
+ const deckFloats = deck ? collectDeckFloats(tree, deckEntry) : undefined;
1505
1616
  // The landing page, a part's overview page (rule S10): its math binds
1506
1617
  // like a chapter's, but it numbers nothing.
1507
1618
  const nonChapterPage = !lessonId && isDocsPageFile(file);
@@ -1509,11 +1620,15 @@ export default function remarkNotation(options = {}) {
1509
1620
  // Missing ⇒ the legacy `##`-section numbering.
1510
1621
  const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
1511
1622
  const registryFile = isNotationRegistryFile(file);
1512
- const mathScope = pageGlyphScope(
1513
- localDefinitions,
1514
- sharedDefinitions,
1515
- collectPageSharedKeys(tree),
1516
- );
1623
+ // A deck shows its lessons' symbols: those its chapters name count as
1624
+ // named here, as the manifest's gates count them.
1625
+ const pageSharedKeys = collectPageSharedKeys(tree);
1626
+ for (const chapter of scopeChapters) {
1627
+ for (const key of collectPageSharedKeys({ type: 'root', children: [{ type: 'text', value: chapter.body }] })) {
1628
+ pageSharedKeys.add(key);
1629
+ }
1630
+ }
1631
+ const mathScope = pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys);
1517
1632
  const references = [];
1518
1633
  const resolved = new Map();
1519
1634
 
@@ -1521,7 +1636,9 @@ export default function remarkNotation(options = {}) {
1521
1636
  // math node is rewritten, so a `[[eq-key]]` earlier in the page still
1522
1637
  // resolves. `equationNumbering` picks the `‹part›.‹n›` vs `##`-section
1523
1638
  // scheme.
1524
- const equationLabels = collectEquationLabels(tree, equationNumbering);
1639
+ const equationLabels = deckFloats
1640
+ ? [...deckFloats.equations.values()].filter((equation) => !equation.href)
1641
+ : collectEquationLabels(tree, equationNumbering);
1525
1642
  const pageLabels = new Map();
1526
1643
  for (const label of equationLabels) {
1527
1644
  if (nonChapterPage) {
@@ -1540,33 +1657,65 @@ export default function remarkNotation(options = {}) {
1540
1657
  }
1541
1658
  pageLabels.set(label.key, label.number);
1542
1659
  }
1660
+ // A deck's recalled equations answer to their chapter's key on the page;
1661
+ // a reference to anything else of the chapter's goes to the chapter.
1662
+ if (deckFloats) {
1663
+ for (const equation of deckFloats.equations.values()) {
1664
+ if (equation.href && !pageLabels.has(equation.key)) pageLabels.set(equation.key, equation.number);
1665
+ }
1666
+ }
1667
+ // What a deck's `[[eq-…]]`, `[[fig-…]]`, `[[sec-…]]` name when the deck
1668
+ // shows no such float itself: its chapter's, or the one chapter of those a
1669
+ // part's deck covers that has it.
1670
+ const fallbackLessons = deck?.scope === 'part'
1671
+ ? (deckEntry?.covers ?? []).map((cover) => cover.chapter)
1672
+ : deck?.chapterPath
1673
+ ? [lessonSlug]
1674
+ : [];
1675
+ const fallback = fallbackLessons.length > 0 ? { fallbackLessons } : {};
1543
1676
  const eqCtx = {
1544
1677
  base: options.base ?? '',
1545
1678
  pageLabels,
1546
1679
  crossPage: numbering.equations ?? {},
1680
+ ...fallback,
1547
1681
  };
1548
1682
  // WERN guard: what this page prints next to the equation must be what a
1549
- // reference from another page prints for it.
1550
- assertManifestNumberParity({
1551
- file,
1552
- slug: lessonSlug,
1553
- pageLabels,
1554
- crossPage: eqCtx.crossPage,
1555
- placement: equationNumbering,
1556
- noun: 'equation',
1557
- syntax: (key) => `eq:${key}`,
1558
- });
1683
+ // reference from another page prints for it. A deck's numbers are its
1684
+ // own, checked against the manifest's deck instead.
1685
+ if (!deck) {
1686
+ assertManifestNumberParity({
1687
+ file,
1688
+ slug: lessonSlug,
1689
+ pageLabels,
1690
+ crossPage: eqCtx.crossPage,
1691
+ placement: equationNumbering,
1692
+ noun: 'equation',
1693
+ syntax: (key) => `eq:${key}`,
1694
+ });
1695
+ } else {
1696
+ for (const equation of deckFloats.equations.values()) {
1697
+ if (equation.number === '?') {
1698
+ fail(
1699
+ file,
1700
+ `Stale Explico manifest for the deck ${deckEntry?.slug ?? lessonSlug}: it lists no equation labelled eq:${equation.key}. Recompile the manifest.`,
1701
+ );
1702
+ }
1703
+ }
1704
+ }
1559
1705
 
1560
1706
  // Number and anchor every display equation on a lesson page, keyed or
1561
1707
  // not (F3). Non-lesson pages (notation registry, stray docs) keep the
1562
- // keyed-only behaviour so their body math is left untouched.
1563
- const displayEquations = lessonId
1564
- ? collectDisplayEquations(tree, equationNumbering)
1565
- : [];
1708
+ // keyed-only behaviour so their body math is left untouched. A deck
1709
+ // numbers only what it labels or recalls.
1710
+ const displayEquations = deckFloats
1711
+ ? [...deckFloats.equations].map(([node, equation]) => ({ ...equation, node }))
1712
+ : lessonId
1713
+ ? collectDisplayEquations(tree, equationNumbering)
1714
+ : [];
1566
1715
  const equationByNode = new Map(
1567
1716
  displayEquations.map((equation) => [equation.node, equation]),
1568
1717
  );
1569
- if (strict) {
1718
+ if (strict && !deck) {
1570
1719
  assertStrictNumbering({
1571
1720
  file,
1572
1721
  slug: lessonSlug,
@@ -1581,6 +1730,25 @@ export default function remarkNotation(options = {}) {
1581
1730
  // page is numbered and anchored, keyed or not — same rule as equations,
1582
1731
  // via the shared engine in `keyed-jsx-floats.mjs`.
1583
1732
  const makeFloatCtx = (tagName, crossPage, kind) => {
1733
+ // A deck's floats: recalled ones keep the chapter's number, the deck's
1734
+ // own are unnumbered (rule D8).
1735
+ if (deckFloats) {
1736
+ const prefix = { Table: 'tab', Figure: 'fig', Diagram: 'dia' }[tagName];
1737
+ const entries = [...deckFloats.floats].filter(([, float]) => float.prefix === prefix);
1738
+ return {
1739
+ ctx: {
1740
+ base: options.base ?? '',
1741
+ pageLabels: new Map(
1742
+ entries
1743
+ .filter(([, float]) => float.number !== undefined)
1744
+ .map(([, float]) => [float.key, float.number]),
1745
+ ),
1746
+ crossPage: crossPage ?? {},
1747
+ ...fallback,
1748
+ },
1749
+ byNode: new Map(entries),
1750
+ };
1751
+ }
1584
1752
  const floatNumbering = pageNumbering(numbering, lessonSlug, kind);
1585
1753
  const floats = lessonId
1586
1754
  ? collectKeyedJsxFloats(tree, { tagName, numbering: floatNumbering })
@@ -1670,7 +1838,7 @@ export default function remarkNotation(options = {}) {
1670
1838
  // Outline numbering: the lesson's top-level headings take the manifest's
1671
1839
  // anchors and numbers before anything else reads them, and `[[sec-…]]`
1672
1840
  // resolves against that same table.
1673
- const publishedHeadings = lessonSlug
1841
+ const publishedHeadings = lessonSlug && !deck
1674
1842
  ? numbering.sections?.[lessonSlug]
1675
1843
  : undefined;
1676
1844
  if (lessonId && publishedHeadings) {
@@ -1686,8 +1854,12 @@ export default function remarkNotation(options = {}) {
1686
1854
  const sectionCtx = {
1687
1855
  base: options.base ?? '',
1688
1856
  show: sectionSettings.show,
1689
- pageHeadings: new Map(Object.entries(headingsBySlug[lessonSlug] ?? {})),
1857
+ // Beside a chapter, a deck's `[[sec-…]]` names a section of the
1858
+ // chapter (`fallbackLessons`), on a part's deck one of a chapter it
1859
+ // covers; a slides-first deck's names its own frame.
1860
+ pageHeadings: new Map(fallbackLessons.length > 0 ? [] : Object.entries(headingsBySlug[lessonSlug] ?? {})),
1690
1861
  crossPage: headingsBySlug,
1862
+ ...fallback,
1691
1863
  };
1692
1864
 
1693
1865
  const resolve = (key, kind, node) => {
@@ -1806,23 +1978,29 @@ export default function remarkNotation(options = {}) {
1806
1978
  if (result.unresolved.length > 0) {
1807
1979
  const unresolved = result.unresolved
1808
1980
  .map(
1809
- ({ token, start, suspended, font, lenders }) =>
1981
+ ({ token, start, suspended, font, lenders, fixedLenders, instance }) =>
1810
1982
  `${tokenLocation(file, node, value, start)}: "${token}"${
1811
1983
  suspended
1812
1984
  ? ` (${suspended} is suspended on this page, so this form needs its own entry)`
1813
1985
  : font
1814
1986
  ? ' (a different symbol from the plain letter; it needs its own entry)'
1815
- : lenders
1816
- ? ` (${lenders.join(', ')} explains only its own instances with a subscript, never a bare "${token}", rule N4: give it its own entry, or \\explain{${lenders[0]}}{${token}} if it is that entry's shorthand)`
1817
- : ''
1987
+ : fixedLenders?.length
1988
+ ? ` (${fixedLenders
1989
+ .map((lender) => fixedLenderMessage(lender, instance ?? token))
1990
+ .join(' ')})`
1991
+ : lenders
1992
+ ? ` (${lenders.join(', ')} explains only its own instances with a subscript, never a bare "${token}", rule N4: give it its own entry, or \\explain{${lenders[0]}}{${token}} if it is that entry's shorthand)`
1993
+ : ''
1818
1994
  }`,
1819
1995
  )
1820
1996
  .join(', ');
1821
1997
  fail(
1822
1998
  file,
1823
1999
  nonChapterPage
1824
- ? `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], or wrap it in \\explain{key}{latex}: a page that is not a chapter recalls shared notation and defines none (rule S10).`
1825
- : `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`,
2000
+ ? `Unresolved notation in ${context}: ${unresolved}, in ${mathExcerpt(value)}. Introduce the symbol with [[key]], or wrap it in \\explain{key}{latex}: a page that is not a chapter recalls shared notation and defines none (rule S10).`
2001
+ : lessonId
2002
+ ? `Unresolved notation in ${context}: ${unresolved}, in ${mathExcerpt(value)}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`
2003
+ : `Unresolved notation in ${context}: ${unresolved}, in ${mathExcerpt(value)}. ${REGISTRY_BODY_FIX}`,
1826
2004
  node,
1827
2005
  );
1828
2006
  }
@@ -1888,6 +2066,11 @@ export default function remarkNotation(options = {}) {
1888
2066
  // Scoped to lesson pages, same as `displayEquations` above — a
1889
2067
  // registry/glossary page's inline math is left exactly as before.
1890
2068
  node.children[index] = inlineEquationNode(child);
2069
+ } else if (deck && child?.type === 'mdxJsxFlowElement' && (tableByNode.has(child) || figureByNode.has(child) || diagramByNode.has(child))) {
2070
+ injectDeckFloatAttributes(
2071
+ child,
2072
+ tableByNode.get(child) ?? figureByNode.get(child) ?? diagramByNode.get(child),
2073
+ );
1891
2074
  } else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
1892
2075
  injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
1893
2076
  } else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
@@ -1981,6 +2164,32 @@ export default function remarkNotation(options = {}) {
1981
2164
  titleChapterLinks(tree, file, chapterTitleBySlug, options.base);
1982
2165
  visit(tree);
1983
2166
 
2167
+ if (deck && deckEntry) {
2168
+ // The legend lines first: they read the frames between the headings.
2169
+ insertLegends(tree, {
2170
+ resolved: Object.fromEntries(resolved),
2171
+ mode: options.manifest?.deck?.legend ?? 'first-use',
2172
+ });
2173
+ const firstContent = tree.children.findIndex((node) => node?.type !== 'mdxjsEsm');
2174
+ tree.children.splice(firstContent === -1 ? tree.children.length : firstContent, 0, titleFrameNode(options.manifest, deckEntry));
2175
+ const outcomes = outcomesFrameNode(options.manifest, deckEntry, { teaches: deckTeaches(options.manifest, deckEntry) });
2176
+ if (outcomes) tree.children.push(outcomes);
2177
+ file.data.explicoDeck = {
2178
+ slug: deckEntry.slug,
2179
+ frames: deckEntry.frames.length,
2180
+ // Each frame's id, by its number less one: `rehype-deck.mjs` stamps
2181
+ // every frame with it, the references frame too, whose heading the
2182
+ // citations pass writes after this one.
2183
+ frameIds: deckEntry.frames.map((frame) => frame.id),
2184
+ // On a part's deck, the chapter each frame's section covers, by its
2185
+ // lesson slug (`data-chapter`), which the sidebar lights.
2186
+ frameChapters: deckEntry.frames.map((frame) => frame.chapter ?? null),
2187
+ aspectRatio: options.manifest?.deck?.aspectRatio ?? '16:9',
2188
+ canvas: options.manifest?.deck?.canvas ?? { width: 1280, height: 720 },
2189
+ notes: options.manifest?.deck?.notes ?? 'reader',
2190
+ };
2191
+ }
2192
+
1984
2193
  file.data.notationReferences = references;
1985
2194
  file.data.notationResolved = Object.fromEntries(resolved);
1986
2195
  file.data.equationLabels = equationLabels.map(
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Lines to add to a position in `file.value` to get the line in `file.path`:
3
+ * 0 when the text is the whole file (or the file cannot be read), the height
4
+ * of the frontmatter when it is the body alone.
5
+ */
6
+ export function sourceLineOffset(file: any): any;
7
+ /**
8
+ * `node` with its `position` moved to the file's own lines, for
9
+ * `file.fail` / `file.message`, which print it. The node itself is left alone:
10
+ * the tree keeps the positions the parser gave it.
11
+ */
12
+ export function shiftedPlace(file: any, node: any): any;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Where a vfile's text sits in the file on disk.
3
+ *
4
+ * Astro's glob loader renders a content-collection entry (a notation entry)
5
+ * from its body alone: the frontmatter is gone, so every `position` the remark
6
+ * pass reads counts lines from the first line after it, and an error naming
7
+ * `entry.md:1:43` points at the wrong line. A page Astro compiles whole (a
8
+ * lesson, through MDX) carries its full text, and its positions are already the
9
+ * file's own. This finds which of the two a file is, so a location can be
10
+ * reported the way an editor opens it.
11
+ *
12
+ * It reads the file, and only when a location is being reported (a failure or
13
+ * a warning), never on the happy path.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ const offsets = new WeakMap();
18
+
19
+ /**
20
+ * Lines to add to a position in `file.value` to get the line in `file.path`:
21
+ * 0 when the text is the whole file (or the file cannot be read), the height
22
+ * of the frontmatter when it is the body alone.
23
+ */
24
+ export function sourceLineOffset(file) {
25
+ if (!file || typeof file !== 'object') return 0;
26
+ if (offsets.has(file)) return offsets.get(file);
27
+
28
+ let offset = 0;
29
+ try {
30
+ const text = typeof file.value === 'string' ? file.value : undefined;
31
+ if (text && typeof file.path === 'string') {
32
+ const raw = readFileSync(file.path, 'utf8');
33
+ const at = raw.endsWith(text) ? raw.length - text.length : raw.indexOf(text);
34
+ if (at > 0) {
35
+ for (let index = 0; index < at; index += 1) {
36
+ if (raw.charCodeAt(index) === 10) offset += 1;
37
+ }
38
+ }
39
+ }
40
+ } catch {
41
+ // A hand-built tree has no file on disk: its positions are all there is.
42
+ offset = 0;
43
+ }
44
+ offsets.set(file, offset);
45
+ return offset;
46
+ }
47
+
48
+ /**
49
+ * `node` with its `position` moved to the file's own lines, for
50
+ * `file.fail` / `file.message`, which print it. The node itself is left alone:
51
+ * the tree keeps the positions the parser gave it.
52
+ */
53
+ export function shiftedPlace(file, node) {
54
+ const position = node?.position;
55
+ if (!position?.start || typeof position.start.line !== 'number') return node;
56
+ const offset = sourceLineOffset(file);
57
+ if (offset === 0) return node;
58
+ const move = (point) =>
59
+ point && typeof point.line === 'number' ? { ...point, line: point.line + offset } : point;
60
+ // vfile-message reads a place from a node only when it has a `type`.
61
+ return {
62
+ type: node.type ?? 'unknown',
63
+ position: { start: move(position.start), end: move(position.end) },
64
+ };
65
+ }
@@ -60,6 +60,12 @@ interface SharedNotationDefinitionInput {
60
60
  readonly alignment: NotationAlignment;
61
61
  readonly aiAssisted: boolean;
62
62
  readonly body: string;
63
+ /**
64
+ * 1-based line in `source.file` where `body` starts, as on a lesson: the
65
+ * gate's body-relative findings are shifted by it. Optional for a
66
+ * hand-built input, which then behaves as 1.
67
+ */
68
+ readonly bodyLine?: number;
63
69
  /** References harvested from the Markdown body. */
64
70
  readonly references: readonly NotationReferenceInput[];
65
71
  /** Symbols this entry's `formula` names but does not make cards of. */
@@ -109,6 +115,28 @@ interface NotationLessonInput {
109
115
  * so a hand-built input (a test, an adapter) stays valid.
110
116
  */
111
117
  readonly suspended?: readonly NotationSuspension[];
118
+ /**
119
+ * A deck is a page of its own whose symbols resolve in other lessons'
120
+ * scopes: the ids of the lessons whose `notation.local` entries are in
121
+ * scope on this page, the first that defines a key binding it. A deck
122
+ * beside its chapter (`<lesson>.deck.mdx`, page `<lessonId>.slides`) has
123
+ * its chapter's; a part's deck (`<part>/index.deck.mdx`, page
124
+ * `<part>.slides`) every chapter of its part, in reading order. Absent:
125
+ * the page's own `lessonId`, and the page is no deck.
126
+ */
127
+ readonly scopeLessonIds?: readonly string[];
128
+ /**
129
+ * Those lessons' `notation.local` entries, for the gates that build a
130
+ * page's glyph table, the first of a key kept. They stay the chapters'
131
+ * definitions: the registry defines each once, under its chapter.
132
+ */
133
+ readonly inheritedLocalDefinitions?: readonly LocalNotationDefinitionInput[];
134
+ /**
135
+ * The shared keys those lessons' chapters name (`[[key]]`, `\explain{key}`):
136
+ * a deck may show any symbol of its lessons — what it recalls, and what its
137
+ * legend line introduces at its first use (`docs/authoring/deck.md`).
138
+ */
139
+ readonly inheritedKeys?: readonly string[];
112
140
  /** References harvested from lesson prose and equations, excluding definitions. */
113
141
  readonly references: readonly NotationReferenceInput[];
114
142
  /** Original lesson body, retained for lossless loader adapters. */
@@ -26,14 +26,23 @@
26
26
  * `unchecked` by {@link enforcementOf}.
27
27
  */
28
28
  /** The part of a page a rule is about. The first tag on an entry is primary. */
29
- type Element = 'prose' | 'math' | 'equation' | 'notation' | 'heading' | 'part-page' | 'plot' | 'diagram' | 'table' | 'assessment' | 'source' | 'changelog';
29
+ type Element = 'slide' | 'prose' | 'math' | 'equation' | 'notation' | 'heading' | 'part-page' | 'plot' | 'diagram' | 'table' | 'assessment' | 'source' | 'changelog';
30
30
  /** Every element, in the order the rules-in-effect page lists them. */
31
31
  declare const ELEMENTS: readonly Element[];
32
32
  /**
33
- * The five areas the engine ships, plus `X` — reserved for a course's own rules
33
+ * The six areas the engine ships, plus `X` — reserved for a course's own rules
34
34
  * and never used by an engine rule (see {@link CourseRuleId}).
35
35
  */
36
- type RuleArea = 'N' | 'V' | 'C' | 'S' | 'P' | 'X';
36
+ type RuleArea = 'N' | 'V' | 'C' | 'S' | 'P' | 'D' | 'X';
37
+ /**
38
+ * Which edition of a lesson a rule is about (`docs/authoring/deck.md`): its
39
+ * chapter, its deck (`<lesson>.deck.mdx`), or `both`. A rule without one is
40
+ * `both`. The rules-in-effect page is written per edition, so an author of
41
+ * either reads one list: a chapter-only rule is off on a deck, where a `D`
42
+ * rule says the same thing about a frame.
43
+ */
44
+ type Edition = 'chapter' | 'deck';
45
+ type Editions = Edition | 'both';
37
46
  type RuleId = `${RuleArea}${number}`;
38
47
  /**
39
48
  * A course's own rule, defined in `courseRules` in the course settings rather
@@ -113,6 +122,8 @@ interface RuleEntry {
113
122
  * absent means "shipped with the engine".
114
123
  */
115
124
  readonly source?: 'course';
125
+ /** The edition the rule is about; absent is `both` ({@link Editions}). */
126
+ readonly editions?: Editions;
116
127
  }
117
128
  interface InvariantEntry {
118
129
  readonly kind: 'invariant';
@@ -123,8 +134,14 @@ interface InvariantEntry {
123
134
  readonly doc: string;
124
135
  readonly summary: string;
125
136
  readonly checks: readonly [RuleCheck, ...RuleCheck[]];
137
+ /** The edition the invariant is about; absent is `both` ({@link Editions}). */
138
+ readonly editions?: Editions;
126
139
  }
127
140
  type CatalogEntry = RuleEntry | InvariantEntry;
141
+ /** True when `entry` is in force for `edition` ({@link Editions}). */
142
+ declare function appliesTo(entry: {
143
+ readonly editions?: Editions;
144
+ }, edition: Edition): boolean;
128
145
  type Enforcement = 'script' | 'partial' | 'guide';
129
146
  /**
130
147
  * A rule that is no longer in force. It keeps its ID forever — an old commit
@@ -173,4 +190,4 @@ declare function enforcementOf(entry: CatalogEntry): Enforcement;
173
190
  /** Every code the catalog owns, sorted. */
174
191
  declare function ownedCodes(): readonly string[];
175
192
 
176
- export { CATALOG, type CatalogEntry, type CourseRuleId, ELEMENTS, type Element, type Emitter, type Enforcement, type InvariantEntry, type RetiredRule, type RuleArea, type RuleCheck, type RuleEntry, type RuleId, type SettingSchema, type SettingSpec, type Severity, enforcementOf, entryById, invariants, isCourseRuleId, ownedCodes, ownerOf, retiredRule, retiredRules, ruleById, ruleIdOf, rules };
193
+ export { CATALOG, type CatalogEntry, type CourseRuleId, ELEMENTS, type Edition, type Editions, type Element, type Emitter, type Enforcement, type InvariantEntry, type RetiredRule, type RuleArea, type RuleCheck, type RuleEntry, type RuleId, type SettingSchema, type SettingSpec, type Severity, appliesTo, enforcementOf, entryById, invariants, isCourseRuleId, ownedCodes, ownerOf, retiredRule, retiredRules, ruleById, ruleIdOf, rules };