explico 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +894 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1087 -14
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -4,7 +4,8 @@ import {
4
4
  equationLinkFor,
5
5
  mathSpans,
6
6
  pageScope,
7
- suspendedOperators
7
+ suspendedOperators,
8
+ scopeLocals
8
9
  } from "./gate-math.js";
9
10
  import { formulaGlyphScope } from "./gloss.js";
10
11
  import { indexOfEntry } from "./math-glyphs.mjs";
@@ -38,7 +39,7 @@ function message(lessonId, code, details, place = "body") {
38
39
  case "index-swallowed":
39
40
  return `${lessonId}: \\explain{${details.key}}{${details.token}} wraps an indexed symbol in ${details.key}, whose latex (${details.notation}) has no index, so its card cannot explain ${JSON.stringify(details.index)}. Drop the wrap and give the subscript an index entry (latex: "{}_${details.index}"), or wrap it in an entry for the indexed symbol (rule N23).`;
40
41
  case "index-range":
41
- return `${lessonId}: the running index ${details.letter} (first in ${JSON.stringify(details.token)}) has no stated set. Give ${details.where} a range (range: "1, \\\\dots, n"), state it after a \\quad on the first display line that uses it (\\quad ${details.letter} = 1, \\dots, n), or mark the card fixedIndex: true if ${details.letter} is a label there, not a running index (rule N23).`;
42
+ return `${lessonId}: the running index ${details.letter} (first in ${JSON.stringify(details.token)}) has no stated set. Give ${details.where} a range (range: "1, \\\\dots, n"), state it after a \\quad on the first display line that uses it (\\quad ${details.letter} = 1, \\dots, n), or mark the card fixedIndex: true if ${details.letter} is a label there, not a running index; a family indexed by ${details.letter} then explains none of its instances (rule N23).`;
42
43
  case "derived-key-taken":
43
44
  return `${lessonId}: ${JSON.stringify(details.token)} composes the derived card ${details.key}, but an authored entry has that key. Rename the entry: a key of the form <symbol>.sub.<index> is the build's (rule N23).`;
44
45
  }
@@ -51,7 +52,7 @@ function gateIndices(lessons, entries, extras = {}) {
51
52
  const bodyOffset = (lesson.bodyLine ?? 1) - 1;
52
53
  const scope = pageScope(lesson, entries);
53
54
  const local = new Map(
54
- lesson.localDefinitions.map((definition) => [definition.key, definition])
55
+ scopeLocals(lesson).map((definition) => [definition.key, definition])
55
56
  );
56
57
  const factsOf = (key) => key === null ? void 0 : local.get(key) ?? entries.get(key);
57
58
  const suspended = suspendedOperators(lesson);
@@ -185,7 +186,7 @@ function gateIndices(lessons, entries, extras = {}) {
185
186
  if (!lesson) continue;
186
187
  const scope = pageScope(lesson, entries);
187
188
  const local = new Map(
188
- lesson.localDefinitions.map((definition) => [definition.key, definition])
189
+ scopeLocals(lesson).map((definition) => [definition.key, definition])
189
190
  );
190
191
  const factsOf = (key) => key === null ? void 0 : local.get(key) ?? entries.get(key);
191
192
  for (const assessmentId of link.assessmentIds) {
@@ -43,7 +43,7 @@ interface MathGateDiagnostic {
43
43
  * one the engine ships; its severity is the course's choice, so it is the one
44
44
  * context whose severity is not fixed here.
45
45
  */
46
- readonly severity: 'error' | 'warning';
46
+ readonly severity: 'error' | 'warning' | 'info';
47
47
  /** Owned by exactly one rule or invariant in `src/rules/catalog.ts`. */
48
48
  readonly code: string;
49
49
  readonly file: string;
@@ -106,6 +106,33 @@ declare function scopeEntry(definition: {
106
106
  * assessment's math (`mixed-content.ts`), say — doesn't reconstruct it by
107
107
  * hand and risk drifting from what the completeness gate actually checks.
108
108
  */
109
+ /**
110
+ * The `notation.local` entries in scope on a page: its own, and for a deck
111
+ * beside its chapter the chapter's, which are the lesson's
112
+ * (`NotationLessonInput.inheritedLocalDefinitions`).
113
+ */
114
+ declare function scopeLocals(lesson: NotationLessonInput): readonly NotationLessonInput['localDefinitions'][number][];
115
+ /** Every `\explain{key}{inner}` in a span, in source order, with its offset. */
116
+ declare function explainCalls(latex: string): {
117
+ key: string;
118
+ inner: string;
119
+ index: number;
120
+ }[];
121
+ /**
122
+ * The `\explain` wraps in a span that bind nothing new (rule N4): the bare
123
+ * inner form already resolves to the same card, with no glyph left over and
124
+ * no decoration left unexplained (rule N22). Such a wrap costs an author
125
+ * tokens on every use and the resolver nothing; `\explain` is for a compound
126
+ * or a colliding glyph.
127
+ */
128
+ declare function redundantExplainWraps(latex: string, scope: readonly ScopeEntry[], options?: {
129
+ readonly suspended?: readonly string[];
130
+ readonly derive?: boolean;
131
+ }): {
132
+ key: string;
133
+ inner: string;
134
+ index: number;
135
+ }[];
109
136
  declare function pageScope(lesson: NotationLessonInput, sharedByKey: ReadonlyMap<string, ScopeEntry>): ScopeEntry[];
110
137
  /**
111
138
  * The `\label{eq:…}` a math source carries, as a key plus the anchor the
@@ -136,4 +163,4 @@ declare function gateContentMath(input: {
136
163
  readonly lessonAssessments: readonly LessonAssessmentLink[];
137
164
  }): MathGateDiagnostic[];
138
165
 
139
- export { type AssessmentItemLike, type AssessmentLike, type EquationLink, type LessonAssessmentLink, type MathGateDiagnostic, type MathSpan, type ScopeEntry, currencyDollars, equationLinkFor, gateContentMath, mathSpans, pageScope, scopeEntry, suspendedOperators };
166
+ export { type AssessmentItemLike, type AssessmentLike, type EquationLink, type LessonAssessmentLink, type MathGateDiagnostic, type MathSpan, type ScopeEntry, currencyDollars, equationLinkFor, explainCalls, gateContentMath, mathSpans, pageScope, redundantExplainWraps, scopeEntry, scopeLocals, suspendedOperators };
@@ -1,4 +1,4 @@
1
- import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
1
+ import { resolveMathGlyphs, GlyphResolutionError, fixedLenderMessage, mathExcerpt } from "./math-glyphs.mjs";
2
2
  import {
3
3
  EQ_LABEL_PATTERN,
4
4
  equationAnchorId,
@@ -7,6 +7,7 @@ import {
7
7
  import { lintDelimiterSizes } from "./delimiter-size.js";
8
8
  import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
9
9
  import { blankPropStrings } from "./gate-prop-text.js";
10
+ import { REGISTRY_BODY_FIX, registryGlyphScope } from "./registry-scope.mjs";
10
11
  const SEVERITY = {
11
12
  body: "error",
12
13
  formula: "error",
@@ -55,10 +56,40 @@ function scopeEntry(definition) {
55
56
  ...definition.fixedIndex ? { fixedIndex: true } : {}
56
57
  };
57
58
  }
59
+ function scopeLocals(lesson) {
60
+ const inherited = lesson.inheritedLocalDefinitions ?? [];
61
+ return inherited.length === 0 ? lesson.localDefinitions : [...lesson.localDefinitions, ...inherited];
62
+ }
63
+ const EXPLAIN_CALL = /\\explain\{([^{}]+)\}\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g;
64
+ function explainCalls(latex) {
65
+ return [...latex.matchAll(EXPLAIN_CALL)].map((match) => ({
66
+ key: match[1],
67
+ inner: match[2],
68
+ index: match.index ?? 0
69
+ }));
70
+ }
71
+ function redundantExplainWraps(latex, scope, options = {}) {
72
+ const redundant = [];
73
+ for (const call of explainCalls(latex)) {
74
+ let bare;
75
+ try {
76
+ bare = resolveMathGlyphs(call.inner, [...scope], {
77
+ suspended: options.suspended ?? [],
78
+ derive: options.derive ?? false
79
+ });
80
+ } catch {
81
+ continue;
82
+ }
83
+ if (bare.unresolved.length === 0 && bare.bindings.length > 0 && bare.bindings.every((binding) => binding.key === call.key && binding.level !== "explicit") && bare.decorations.every((decoration) => decoration.wholeForm !== null || decoration.operator !== null)) {
84
+ redundant.push(call);
85
+ }
86
+ }
87
+ return redundant;
88
+ }
58
89
  function pageScope(lesson, sharedByKey) {
59
- const local = lesson.localDefinitions.map(scopeEntry);
90
+ const local = scopeLocals(lesson).map(scopeEntry);
60
91
  const localKeys = new Set(local.map((entry) => entry.key));
61
- const referenced = /* @__PURE__ */ new Set();
92
+ const referenced = new Set(lesson.inheritedKeys ?? []);
62
93
  for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
63
94
  for (const match of lesson.body.matchAll(pattern)) referenced.add(match[1]);
64
95
  }
@@ -117,6 +148,21 @@ function resolveInto(latex, scope, context, file, line, column, fix, out, equati
117
148
  });
118
149
  return;
119
150
  }
151
+ if (context !== "formula") {
152
+ for (const wrap of redundantExplainWraps(stripEquationLabels(latex), scope, { suspended, derive })) {
153
+ out.push({
154
+ context,
155
+ code: "explain-redundant",
156
+ severity: "info",
157
+ file,
158
+ line,
159
+ ...column === void 0 ? {} : { column },
160
+ token: wrap.inner,
161
+ ...at,
162
+ message: `\\explain{${wrap.key}}{${wrap.inner}} wraps what ${wrap.inner} already resolves to on this page; write it bare \u2014 \\explain is for a compound or a colliding glyph (rule N4)`
163
+ });
164
+ }
165
+ }
120
166
  for (const item of result.unresolved) {
121
167
  const { token } = item;
122
168
  if ("suspended" in item && typeof item.suspended === "string") {
@@ -133,7 +179,12 @@ function resolveInto(latex, scope, context, file, line, column, fix, out, equati
133
179
  continue;
134
180
  }
135
181
  const fontHint = "font" in item && typeof item.font === "string" ? ` ${token} is a different symbol from the plain letter: give it its own entry (latex: ${token}) or wrap it in \\explain{key}{${token}}.` : "";
136
- const lenderHint = "lenders" in item && Array.isArray(item.lenders) && item.lenders.length > 0 ? ` ${item.lenders.join(", ")} ${item.lenders.length === 1 ? "has" : "have"} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${item.lenders[0]}}{${token}}.` : "";
182
+ const fixedLenders = "fixedLenders" in item && Array.isArray(item.fixedLenders) ? item.fixedLenders : [];
183
+ const instance = "instance" in item && typeof item.instance === "string" ? item.instance : token;
184
+ const fixedHint = fixedLenders.map((lender) => ` ${fixedLenderMessage(lender, instance)}`).join("");
185
+ const fixedKeys = new Set(fixedLenders.map((lender) => lender.key));
186
+ const lenders = "lenders" in item && Array.isArray(item.lenders) ? item.lenders.filter((key) => !fixedKeys.has(key)) : [];
187
+ const lenderHint = lenders.length > 0 ? ` ${lenders.join(", ")} ${lenders.length === 1 ? "has" : "have"} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${lenders[0]}}{${token}}.` : "";
137
188
  out.push({
138
189
  context,
139
190
  code: "unresolved-glyph",
@@ -142,7 +193,7 @@ function resolveInto(latex, scope, context, file, line, column, fix, out, equati
142
193
  line,
143
194
  token,
144
195
  ...at,
145
- message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint}${lenderHint} ${fix}`
196
+ message: `Unresolved notation ${JSON.stringify(token)} in ${context} math, in ${mathExcerpt(latex)}.${fontHint}${fixedHint}${lenderHint} ${fix}`
146
197
  });
147
198
  }
148
199
  }
@@ -170,6 +221,21 @@ function currencyDollars(body) {
170
221
  });
171
222
  return spans;
172
223
  }
224
+ function reportCurrencyDollars(body, file, bodyOffset, out) {
225
+ for (const span of currencyDollars(body)) {
226
+ const shown = span.latex.replace(/\s+$/, "");
227
+ out.push({
228
+ context: "body",
229
+ code: "currency-dollar",
230
+ severity: SEVERITY.body,
231
+ file,
232
+ line: span.line + bodyOffset,
233
+ column: span.column,
234
+ token: `$${span.latex}$`,
235
+ message: `"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), or the amount in words.`
236
+ });
237
+ }
238
+ }
173
239
  const BODY_FIX = "Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.";
174
240
  function dedupeScope(scope) {
175
241
  const seen = /* @__PURE__ */ new Set();
@@ -251,19 +317,7 @@ function gateContentMath(input) {
251
317
  true
252
318
  );
253
319
  }
254
- for (const span of currencyDollars(lesson.body)) {
255
- const shown = span.latex.replace(/\s+$/, "");
256
- diagnostics.push({
257
- context: "body",
258
- code: "currency-dollar",
259
- severity: SEVERITY.body,
260
- file,
261
- line: span.line + bodyOffset,
262
- column: span.column,
263
- token: `$${span.latex}$`,
264
- message: `"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), or the amount in words.`
265
- });
266
- }
320
+ reportCurrencyDollars(lesson.body, file, bodyOffset, diagnostics);
267
321
  for (const definition of lesson.localDefinitions) {
268
322
  if (!definition.formula) continue;
269
323
  resolveInto(
@@ -285,6 +339,30 @@ function gateContentMath(input) {
285
339
  reportUnknownScopeKeys(definition, cardKeys, file, diagnostics);
286
340
  }
287
341
  }
342
+ const registryScope = registryGlyphScope(
343
+ notationInput.sharedDefinitions
344
+ );
345
+ for (const definition of notationInput.sharedDefinitions) {
346
+ const file = definition.source.file;
347
+ const bodyOffset = (definition.bodyLine ?? 1) - 1;
348
+ for (const span of mathSpans(definition.body)) {
349
+ resolveInto(
350
+ span.latex,
351
+ registryScope,
352
+ "body",
353
+ file,
354
+ span.line + bodyOffset,
355
+ span.column,
356
+ REGISTRY_BODY_FIX,
357
+ diagnostics,
358
+ void 0,
359
+ // An entry's body suspends nothing and derives no cards.
360
+ [],
361
+ false
362
+ );
363
+ }
364
+ reportCurrencyDollars(definition.body, file, bodyOffset, diagnostics);
365
+ }
288
366
  for (const definition of notationInput.sharedDefinitions) {
289
367
  if (!definition.formula) continue;
290
368
  resolveInto(
@@ -349,9 +427,12 @@ function gateContentMath(input) {
349
427
  export {
350
428
  currencyDollars,
351
429
  equationLinkFor,
430
+ explainCalls,
352
431
  gateContentMath,
353
432
  mathSpans,
354
433
  pageScope,
434
+ redundantExplainWraps,
355
435
  scopeEntry,
436
+ scopeLocals,
356
437
  suspendedOperators
357
438
  };
@@ -1,4 +1,4 @@
1
- import { i as NormalizedPartPages } from '../course-config-rRtME5UV.js';
1
+ import { m as NormalizedPartPages } from '../course-config-BLtIdkei.js';
2
2
  import { MathGateDiagnostic } from './gate-math.js';
3
3
  import '../rules/catalog.js';
4
4
  import '../rules/course-rules.js';
@@ -7,9 +7,12 @@
7
7
  * Starting at each lesson's own imports, this follows every relative import
8
8
  * — the course's visuals, wherever they live — and reports each file that
9
9
  * imports `katex` as `lab-katex-direct`, with the chain of imports that
10
- * reaches it. A package import (`explico/…`, `react`) is not followed:
11
- * explico's own KaTeX use is the seam. A type-only import and the KaTeX
12
- * stylesheet draw nothing and are not reported.
10
+ * reaches it. The answer is per lesson: a helper that three lessons reach is
11
+ * reported three times, each with that lesson's own chain, and a lesson that
12
+ * reaches none is clean — so `content check <lesson>` can say so. A package
13
+ * import (`explico/…`, `react`) is not followed: explico's own KaTeX use is
14
+ * the seam. A type-only import and the KaTeX stylesheet draw nothing and are
15
+ * not reported.
13
16
  *
14
17
  * `figure-symbols.ts` checks the same promise on the rendered page, which
15
18
  * also catches math a lab draws some other way.
@@ -20,7 +23,7 @@ interface LabKatexDiagnostic {
20
23
  readonly message: string;
21
24
  /** The file that imports `katex`, repository-relative. */
22
25
  readonly file: string;
23
- /** The first lesson (by id) whose imports reach it. */
26
+ /** The lesson whose imports reach it; one diagnostic per lesson and file. */
24
27
  readonly lessonId: string;
25
28
  }
26
29
  interface LabKatexLessonInput {
@@ -35,8 +38,9 @@ interface LabKatexLessonInput {
35
38
  */
36
39
  declare function importSpecifiers(source: string): string[];
37
40
  /**
38
- * Each course file that a lesson's imports reach and that imports `katex`.
39
- * `root` is the repository root the lesson paths are relative to.
41
+ * Each course file that imports `katex`, once for every lesson whose imports
42
+ * reach it, by the shortest chain. `root` is the repository root the lesson
43
+ * paths are relative to.
40
44
  */
41
45
  declare function validateLabKatexImports(lessons: readonly LabKatexLessonInput[], root?: string): LabKatexDiagnostic[];
42
46
 
@@ -44,18 +44,23 @@ function resolveRelative(fromFile, specifier) {
44
44
  return candidates.find(isFile);
45
45
  }
46
46
  function validateLabKatexImports(lessons, root = process.cwd()) {
47
+ const sources = /* @__PURE__ */ new Map();
47
48
  const read = (path) => {
48
- try {
49
- return readFileSync(path, "utf8");
50
- } catch {
51
- return void 0;
49
+ if (!sources.has(path)) {
50
+ try {
51
+ sources.set(path, readFileSync(path, "utf8"));
52
+ } catch {
53
+ sources.set(path, void 0);
54
+ }
52
55
  }
56
+ return sources.get(path);
53
57
  };
54
58
  const repoPath = (path) => relative(root, path).split(sep).join("/");
55
- const diagnostics = /* @__PURE__ */ new Map();
56
- const seen = /* @__PURE__ */ new Set();
59
+ const diagnostics = [];
57
60
  for (const lesson of [...lessons].sort((a, b) => a.lessonId.localeCompare(b.lessonId))) {
58
61
  const lessonPath = join(root, lesson.file);
62
+ const reported = /* @__PURE__ */ new Set();
63
+ const seen = /* @__PURE__ */ new Set();
59
64
  const queue = [
60
65
  { path: lessonPath, source: stripFencedCode(lesson.body), chain: [lesson.file] }
61
66
  ];
@@ -64,8 +69,9 @@ function validateLabKatexImports(lessons, root = process.cwd()) {
64
69
  for (const specifier of importSpecifiers(source)) {
65
70
  if (isKatexModule(specifier)) {
66
71
  const file = repoPath(path);
67
- if (!diagnostics.has(file)) {
68
- diagnostics.set(file, {
72
+ if (!reported.has(file)) {
73
+ reported.add(file);
74
+ diagnostics.push({
69
75
  code: "lab-katex-direct",
70
76
  severity: "error",
71
77
  file,
@@ -88,7 +94,9 @@ function validateLabKatexImports(lessons, root = process.cwd()) {
88
94
  }
89
95
  }
90
96
  }
91
- return [...diagnostics.values()].sort((a, b) => a.file.localeCompare(b.file));
97
+ return diagnostics.sort(
98
+ (a, b) => a.file.localeCompare(b.file) || a.lessonId.localeCompare(b.lessonId)
99
+ );
92
100
  }
93
101
  export {
94
102
  importSpecifiers,
@@ -2,18 +2,44 @@ import { loadManifest } from "../compiler/manifest.js";
2
2
  import {
3
3
  bindScopeToPage,
4
4
  registerPageNotationDependencies,
5
- routeIdFromLocals
5
+ routeIdFromLocals,
6
+ scopeLessonsOf
6
7
  } from "./page-notation.js";
7
8
  const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
8
9
  const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
9
10
  async function labMathScope(notation, body = "", component) {
10
- const local = notation?.local ?? [];
11
+ const manifest = await loadManifest();
12
+ const routeId = component ? routeIdFromLocals(component.locals) : void 0;
13
+ const deck = routeId === void 0 ? void 0 : manifest.decks?.find(
14
+ (entry) => entry.slug === routeId && (entry.chapter !== void 0 || entry.part !== void 0)
15
+ );
16
+ const scopeLessons = deck ? scopeLessonsOf(manifest, deck.pageId) : [];
17
+ const lessonLocal = [];
18
+ for (const lessonId of scopeLessons) {
19
+ for (const definition of manifest.notation.definitions) {
20
+ if (definition.kind !== "local" || !definition.id.startsWith(`page:${lessonId}:`)) continue;
21
+ if (lessonLocal.some((known) => known.key === definition.key)) continue;
22
+ lessonLocal.push({
23
+ key: definition.key,
24
+ latex: definition.notation,
25
+ ...definition.fixedIndex ? { fixedIndex: true } : {}
26
+ });
27
+ }
28
+ }
29
+ const lessonKeys = scopeLessons.flatMap(
30
+ (lessonId) => (manifest.lessons.find((lesson) => lesson.id === lessonId)?.notation.bindings ?? []).map((binding) => binding.key)
31
+ );
32
+ const ownLocal = notation?.local ?? [];
33
+ const local = [
34
+ ...ownLocal,
35
+ ...lessonLocal.filter((definition) => !ownLocal.some((own) => own.key === definition.key))
36
+ ];
11
37
  const scope = local.map((definition) => ({
12
38
  key: definition.key,
13
39
  notation: definition.latex,
14
40
  ...definition.fixedIndex === true ? { fixedIndex: true } : {}
15
41
  }));
16
- const referenced = /* @__PURE__ */ new Set();
42
+ const referenced = new Set(lessonKeys);
17
43
  const explicitlyExplained = /* @__PURE__ */ new Set();
18
44
  for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
19
45
  for (const match of body.matchAll(pattern)) {
@@ -22,7 +48,6 @@ async function labMathScope(notation, body = "", component) {
22
48
  }
23
49
  }
24
50
  const localKeys = new Set(local.map((definition) => definition.key));
25
- const manifest = await loadManifest();
26
51
  const sharedByKey = new Map(
27
52
  manifest.notation.definitions.filter((definition) => definition.kind === "shared").map((definition) => [definition.key, definition])
28
53
  );
@@ -97,6 +97,31 @@ export function indexDisplayLatex(latex: string): string;
97
97
  * @returns {boolean}
98
98
  */
99
99
  export function entryHasIndex(latex: string): boolean;
100
+ /**
101
+ * The math span a finding sits in, as an author wrote it: `$…$`, whitespace
102
+ * collapsed, cut at 100 characters. A line number can drift (a loader that
103
+ * hands over a body, a generated file), but the span can be searched for, so a
104
+ * message that quotes it still leads to the spot.
105
+ *
106
+ * @param {string} latex
107
+ * @returns {string}
108
+ */
109
+ export function mathExcerpt(latex: string): string;
110
+ /**
111
+ * What to tell an author whose subscripted symbol a family would have
112
+ * explained, were its index not fixed (rule N23): the cause, and the repair
113
+ * that fits it. One sentence, for an unresolved item's `fixedLenders`.
114
+ *
115
+ * @param {{ key: string, via: string, card?: string, letter?: string }} lender
116
+ * @param {string} instance The subscripted symbol as written, `r_2`.
117
+ * @returns {string}
118
+ */
119
+ export function fixedLenderMessage(lender: {
120
+ key: string;
121
+ via: string;
122
+ card?: string;
123
+ letter?: string;
124
+ }, instance: string): string;
100
125
  /**
101
126
  * Resolve and annotate one KaTeX expression against a page's flat glyph table.
102
127
  * Source offsets in the result always refer to the original, unmodified LaTeX.