explico 0.3.0 → 0.5.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 (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The base abbreviation baseline: acronyms and dotted abbreviations every
3
+ * reader is assumed to know, so a lesson never has to introduce them with an
4
+ * abbreviations-table entry. The `\operatorname{…}` / `\mathrm{…}` twins are
5
+ * `base-operators.mjs` (rule N8) / `base-roman-labels.mjs` (rule N10); this is
6
+ * the plain-prose equivalent for rule N14.
7
+ *
8
+ * Kept intentionally small, same rationale as `BASE_ROMAN_LABELS`: a
9
+ * course-specific acronym (`OLS`, `SSR`, `MSE`) is exactly the thing a reader
10
+ * benefits from having spelled out, so it belongs in the course's own
11
+ * `content/abbreviations/*.md` table, not here. This baseline only holds
12
+ * abbreviations so common in ordinary English/academic writing that spelling
13
+ * them out on every course would be noise, not help. A course adds a real
14
+ * one via `abbreviations.allow` in `course.config.ts`, the same shape as
15
+ * `operators.allow` / `romanLabels.allow`.
16
+ */
17
+ export const BASE_ABBREVIATIONS = Object.freeze(
18
+ new Set([
19
+ // dotted Latin/English abbreviations
20
+ 'e.g.',
21
+ 'i.e.',
22
+ 'etc.',
23
+ 'cf.',
24
+ 'vs.',
25
+ 'a.m.',
26
+ 'p.m.',
27
+ 'a.k.a.',
28
+ 'et al.',
29
+ // generic acronyms with no domain-specific meaning in this engine
30
+ 'AI',
31
+ 'ID',
32
+ 'OK',
33
+ 'URL',
34
+ 'FAQ',
35
+ 'US',
36
+ 'UK',
37
+ 'EU',
38
+ ]),
39
+ );
40
+
41
+ /**
42
+ * The abbreviations a course treats as common, given its `course.config.ts`
43
+ * `abbreviations` block: the shipped baseline, plus `allow`, minus `deny`.
44
+ *
45
+ * @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} abbreviations
46
+ * @returns {Set<string>}
47
+ */
48
+ export function effectiveAbbreviationAllowlist(abbreviations) {
49
+ const allow = abbreviations?.allow ?? [];
50
+ const deny = new Set(abbreviations?.deny ?? []);
51
+ const effective = new Set(BASE_ABBREVIATIONS);
52
+ for (const name of allow) effective.add(name);
53
+ for (const name of deny) effective.delete(name);
54
+ return effective;
55
+ }
56
+
57
+ /** True when `token` needs no abbreviations-table entry for this course. */
58
+ export function isBaseAbbreviation(token, abbreviations) {
59
+ return effectiveAbbreviationAllowlist(abbreviations).has(token);
60
+ }
@@ -36,3 +36,18 @@ export function isBaseOperator(name: any, operators: any): boolean;
36
36
  * Matching is case-sensitive.
37
37
  */
38
38
  export const BASE_OPERATORS: Readonly<Set<string>>;
39
+ /**
40
+ * The subset of {@link BASE_OPERATORS} that must never appear with a bare
41
+ * (unparenthesized) argument: every trigonometric, inverse-trigonometric, and
42
+ * hyperbolic function, plus the exponential and logarithms (N13). `\sin x`
43
+ * leaves how far the argument extends ambiguous the moment anything follows
44
+ * it (`\sin x + 1` — is the `+1` inside or outside the sine?); `\sin(x)`
45
+ * never does, even for a single-symbol argument. Kept separate from
46
+ * `BASE_OPERATORS`: this is a formatting rule layered on top of the "does it
47
+ * need a notation card" question that set answers, so an operator can be
48
+ * universal (N8) and still owe a parenthesized argument. Enforced by
49
+ * `resolveMathGlyphs` in `math-glyphs.mjs`, which every rendered-math context
50
+ * (lesson body, notation `formula`, assessments, `Diagram` nodes, lab math)
51
+ * already calls.
52
+ */
53
+ export const PARENTHESIZED_FUNCTIONS: Readonly<Set<string>>;
@@ -81,6 +81,46 @@ export const BASE_OPERATORS = Object.freeze(
81
81
  ]),
82
82
  );
83
83
 
84
+ /**
85
+ * The subset of {@link BASE_OPERATORS} that must never appear with a bare
86
+ * (unparenthesized) argument: every trigonometric, inverse-trigonometric, and
87
+ * hyperbolic function, plus the exponential and logarithms (N13). `\sin x`
88
+ * leaves how far the argument extends ambiguous the moment anything follows
89
+ * it (`\sin x + 1` — is the `+1` inside or outside the sine?); `\sin(x)`
90
+ * never does, even for a single-symbol argument. Kept separate from
91
+ * `BASE_OPERATORS`: this is a formatting rule layered on top of the "does it
92
+ * need a notation card" question that set answers, so an operator can be
93
+ * universal (N8) and still owe a parenthesized argument. Enforced by
94
+ * `resolveMathGlyphs` in `math-glyphs.mjs`, which every rendered-math context
95
+ * (lesson body, notation `formula`, assessments, `Diagram` nodes, lab math)
96
+ * already calls.
97
+ */
98
+ export const PARENTHESIZED_FUNCTIONS = Object.freeze(
99
+ new Set([
100
+ // exponential / logarithms
101
+ 'exp',
102
+ 'log',
103
+ 'ln',
104
+ 'lg',
105
+ // trigonometric
106
+ 'sin',
107
+ 'cos',
108
+ 'tan',
109
+ 'cot',
110
+ 'sec',
111
+ 'csc',
112
+ // inverse trigonometric
113
+ 'arcsin',
114
+ 'arccos',
115
+ 'arctan',
116
+ // hyperbolic
117
+ 'sinh',
118
+ 'cosh',
119
+ 'tanh',
120
+ 'coth',
121
+ ]),
122
+ );
123
+
84
124
  /**
85
125
  * The operators a course treats as common, given its `course.config.ts`
86
126
  * `operators` block: the shipped baseline, plus `allow`, minus `deny`.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The roman labels a course treats as common, given its `course.config.ts`
3
+ * `romanLabels` block: the shipped baseline, plus `allow`, minus `deny`.
4
+ *
5
+ * @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} romanLabels
6
+ * @returns {Set<string>}
7
+ */
8
+ export function effectiveRomanLabelAllowlist(romanLabels: {
9
+ allow?: readonly string[];
10
+ deny?: readonly string[];
11
+ } | undefined): Set<string>;
12
+ /** True when `name` needs no notation entry for this course. */
13
+ export function isBaseRomanLabel(name: any, romanLabels: any): boolean;
14
+ /**
15
+ * The base roman-label baseline: `\mathrm{…}` multi-letter labels every
16
+ * reader is assumed to know, so a lesson never has to introduce them with a
17
+ * notation entry. The `\operatorname{…}` counterpart is `base-operators.mjs`
18
+ * (rule N8); this is its `\mathrm{…}` twin (rule N10).
19
+ *
20
+ * Rationale: the completeness gate has no opinion about `\mathrm{…}` bodies —
21
+ * they are a typographic label to KaTeX, not a bindable symbol, and N3
22
+ * requires the format without asking whether the label is ever explained.
23
+ * The `roman-label-explained` consistency check fills that gap: a label a
24
+ * lesson uses is either on this list (or the course's additions to it) or it
25
+ * carries a notation entry the reader can hover.
26
+ *
27
+ * Kept intentionally small — unlike `sin`/`exp`, an upright label is almost
28
+ * always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
29
+ * genuinely benefits from a hover card, so the shipped baseline holds only
30
+ * the qualifiers that function like an operator's extremum subscript
31
+ * (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. A course
32
+ * adds a real one via `romanLabels.allow` in `course.config.ts`, the same
33
+ * shape as `operators.allow`.
34
+ */
35
+ export const BASE_ROMAN_LABELS: Readonly<Set<string>>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The base roman-label baseline: `\mathrm{…}` multi-letter labels every
3
+ * reader is assumed to know, so a lesson never has to introduce them with a
4
+ * notation entry. The `\operatorname{…}` counterpart is `base-operators.mjs`
5
+ * (rule N8); this is its `\mathrm{…}` twin (rule N10).
6
+ *
7
+ * Rationale: the completeness gate has no opinion about `\mathrm{…}` bodies —
8
+ * they are a typographic label to KaTeX, not a bindable symbol, and N3
9
+ * requires the format without asking whether the label is ever explained.
10
+ * The `roman-label-explained` consistency check fills that gap: a label a
11
+ * lesson uses is either on this list (or the course's additions to it) or it
12
+ * carries a notation entry the reader can hover.
13
+ *
14
+ * Kept intentionally small — unlike `sin`/`exp`, an upright label is almost
15
+ * always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
16
+ * genuinely benefits from a hover card, so the shipped baseline holds only
17
+ * the qualifiers that function like an operator's extremum subscript
18
+ * (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. A course
19
+ * adds a real one via `romanLabels.allow` in `course.config.ts`, the same
20
+ * shape as `operators.allow`.
21
+ */
22
+ export const BASE_ROMAN_LABELS = Object.freeze(new Set(['opt']));
23
+
24
+ /**
25
+ * The roman labels a course treats as common, given its `course.config.ts`
26
+ * `romanLabels` block: the shipped baseline, plus `allow`, minus `deny`.
27
+ *
28
+ * @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} romanLabels
29
+ * @returns {Set<string>}
30
+ */
31
+ export function effectiveRomanLabelAllowlist(romanLabels) {
32
+ const allow = romanLabels?.allow ?? [];
33
+ const deny = new Set(romanLabels?.deny ?? []);
34
+ const effective = new Set(BASE_ROMAN_LABELS);
35
+ for (const name of allow) effective.add(name);
36
+ for (const name of deny) effective.delete(name);
37
+ return effective;
38
+ }
39
+
40
+ /** True when `name` needs no notation entry for this course. */
41
+ export function isBaseRomanLabel(name, romanLabels) {
42
+ return effectiveRomanLabelAllowlist(romanLabels).has(name);
43
+ }
@@ -1,4 +1,4 @@
1
- import { CourseConventions, CourseOperators } from '../compiler/course-config.js';
1
+ import { CourseConventions, CourseOperators, CourseRomanLabels } from '../compiler/course-config.js';
2
2
  import { CurriculumCatalog } from '../curriculum/validation.js';
3
3
  import { NotationSpec } from '../compiler/collections.js';
4
4
  import { LintIgnore } from './lint-ignore.js';
@@ -28,12 +28,16 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
28
28
  * (h) operator-explained a named operator a lesson uses is on the
29
29
  * shipped/course operator allowlist or
30
30
  * carries a notation entry (rule N8)
31
+ * (i) roman-label-explained a `\mathrm{…}` multi-letter label a lesson
32
+ * uses is on the shipped/course roman-label
33
+ * allowlist or carries a notation entry
34
+ * (rule N10)
31
35
  *
32
36
  * Check (d) — the committed per-lesson resolution report — lives in
33
37
  * `src/content/resolution-report.ts`.
34
38
  */
35
39
 
36
- type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained';
40
+ type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained' | 'roman-label-explained';
37
41
  interface ConsistencyDiagnostic {
38
42
  readonly code: ConsistencyCode;
39
43
  /** Tier 3 — every finding here is a warning until content is brought clean. */
@@ -56,6 +60,8 @@ interface ConsistencyInput {
56
60
  readonly conventions: CourseConventions;
57
61
  /** This course's edits to the named-operator baseline (rule N8). */
58
62
  readonly operators?: CourseOperators;
63
+ /** This course's edits to the roman-label baseline (rule N10). */
64
+ readonly romanLabels?: CourseRomanLabels;
59
65
  }
60
66
  /**
61
67
  * Run every consistency check. The result is sorted deterministically (code,
@@ -1,5 +1,6 @@
1
1
  import { effectiveOperatorAllowlist } from "./base-operators.mjs";
2
- import { collectOperators } from "./math-glyphs.mjs";
2
+ import { effectiveRomanLabelAllowlist } from "./base-roman-labels.mjs";
3
+ import { collectOperators, collectRomanLabels } from "./math-glyphs.mjs";
3
4
  import { offVocabularyTokens } from "./units-vocab.js";
4
5
  const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
5
6
  function stripCode(text) {
@@ -332,6 +333,50 @@ function checkOperatorExplained(input, out) {
332
333
  }
333
334
  }
334
335
  }
336
+ function checkRomanLabelExplained(input, out) {
337
+ const allowlist = effectiveRomanLabelAllowlist(input.romanLabels);
338
+ const byId = new Map(
339
+ input.registry.definitions.map((definition) => [definition.id, definition])
340
+ );
341
+ const bindingsByLesson = new Map(
342
+ input.registry.bundles.map((bundle) => [bundle.lessonId, bundle.bindings])
343
+ );
344
+ for (const lesson of input.notationInput.lessons) {
345
+ const explained = /* @__PURE__ */ new Set();
346
+ const noteEntryLabels = (latex) => {
347
+ for (const label of collectRomanLabels(latex)) {
348
+ explained.add(label.name);
349
+ }
350
+ };
351
+ for (const definition of lesson.localDefinitions) {
352
+ noteEntryLabels(definition.notation);
353
+ }
354
+ for (const binding of bindingsByLesson.get(lesson.lessonId) ?? []) {
355
+ const definition = byId.get(binding.definitionId);
356
+ if (definition) noteEntryLabels(definition.notation);
357
+ }
358
+ const reported = /* @__PURE__ */ new Set();
359
+ for (const span of mathSpans(lesson.body)) {
360
+ for (const label of collectRomanLabels(span.latex)) {
361
+ if (allowlist.has(label.name)) continue;
362
+ if (explained.has(label.name)) continue;
363
+ if (reported.has(label.name)) continue;
364
+ reported.add(label.name);
365
+ if (input.lintIgnore.matches("roman-label-explained", label.name)) {
366
+ continue;
367
+ }
368
+ out.push({
369
+ code: "roman-label-explained",
370
+ severity: "warning",
371
+ lessonId: lesson.lessonId,
372
+ file: lesson.source.file,
373
+ glyph: label.name,
374
+ message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to romanLabels.allow in course.config.ts`
375
+ });
376
+ }
377
+ }
378
+ }
379
+ }
335
380
  const CHECKS = [
336
381
  checkGlyphUniqueness,
337
382
  checkNotationUnits,
@@ -342,7 +387,8 @@ const CHECKS = [
342
387
  checkGlossWantsPromoting,
343
388
  checkCardWantsDemoting,
344
389
  checkGlossNameShape,
345
- checkOperatorExplained
390
+ checkOperatorExplained,
391
+ checkRomanLabelExplained
346
392
  ];
347
393
  function runConsistencyChecks(input) {
348
394
  const diagnostics = [];
@@ -363,7 +409,8 @@ function consistencyCounts(diagnostics) {
363
409
  "gloss-wants-promoting": 0,
364
410
  "card-wants-demoting": 0,
365
411
  "gloss-name-shape": 0,
366
- "operator-explained": 0
412
+ "operator-explained": 0,
413
+ "roman-label-explained": 0
367
414
  };
368
415
  for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
369
416
  return counts;
@@ -0,0 +1,21 @@
1
+ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } from './keyed-float-validate.js';
2
+
3
+ /** `validateKeyedFloats` for `<Diagram>` — see `keyed-float-validate.ts`. */
4
+
5
+ type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused';
6
+ interface DiagramDiagnostic {
7
+ readonly code: DiagramDiagnosticCode;
8
+ readonly severity: 'error' | 'warning';
9
+ readonly message: string;
10
+ readonly file?: string;
11
+ readonly lessonId?: string;
12
+ readonly key?: string;
13
+ }
14
+ type DiagramLabelRecord = KeyedFloatLabelRecord;
15
+ type DiagramLessonInput = KeyedFloatLessonInput;
16
+ interface DiagramValidation extends KeyedFloatValidation {
17
+ readonly diagnostics: readonly DiagramDiagnostic[];
18
+ }
19
+ declare function validateDiagrams(lessons: readonly DiagramLessonInput[]): DiagramValidation;
20
+
21
+ export { type DiagramDiagnostic, type DiagramDiagnosticCode, type DiagramLabelRecord, type DiagramLessonInput, type DiagramValidation, validateDiagrams };
@@ -0,0 +1,13 @@
1
+ import {
2
+ validateKeyedFloats
3
+ } from "./keyed-float-validate.js";
4
+ function validateDiagrams(lessons) {
5
+ return validateKeyedFloats(lessons, {
6
+ tagName: "Diagram",
7
+ prefix: "dia",
8
+ noun: "diagram"
9
+ });
10
+ }
11
+ export {
12
+ validateDiagrams
13
+ };
@@ -45,10 +45,15 @@ interface EquationLessonInput {
45
45
  readonly file: string;
46
46
  readonly body: string;
47
47
  /**
48
- * The lesson's 1-based ordinal in the course. When set, equations are
49
- * numbered `‹chapter›.‹n›`; when omitted, the legacy `##`-section scheme.
48
+ * `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
49
+ * and how many display equations earlier lessons of that part already used.
50
+ * When set, equations are numbered `‹part›.‹offset + n›`; when omitted, the
51
+ * legacy `##`-section scheme. See `docs/authoring/structure.md`.
50
52
  */
51
- readonly chapter?: number;
53
+ readonly numbering?: {
54
+ readonly prefix: number;
55
+ readonly offset: number;
56
+ };
52
57
  }
53
58
  declare function validateEquations(lessons: readonly EquationLessonInput[]): EquationValidation;
54
59
 
@@ -25,7 +25,7 @@ function validateEquations(lessons) {
25
25
  )) {
26
26
  const seen = /* @__PURE__ */ new Set();
27
27
  const table = {};
28
- for (const raw of scanEquationLabels(lesson.body, lesson.chapter)) {
28
+ for (const raw of scanEquationLabels(lesson.body, lesson.numbering)) {
29
29
  if (seen.has(raw.key)) {
30
30
  diagnostics.push({
31
31
  code: "eq-duplicate-key",
@@ -9,12 +9,28 @@ export function parseEquationRef(raw: any): {
9
9
  /** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
10
10
  export function stripEquationLabels(latex: any): string;
11
11
  /**
12
- * The visible number for one display equation. With `chapter` (the lesson's
13
- * course ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings
12
+ * Normalise the numbering input to `{ prefix, offset }`.
13
+ *
14
+ * `prefix` is the number before the dot — the **part**'s ordinal (its position
15
+ * among the sidebar groups). `offset` is how many display equations already
16
+ * appeared in earlier lessons of that same part, so `n` runs continuously
17
+ * across a part instead of restarting on every page. A bare number is the
18
+ * legacy per-lesson form (`prefix = n`, `offset = 0`) and is still accepted so
19
+ * hand-built trees and unit tests keep working. `undefined`/`null`/`0` ⇒ the
20
+ * `##`-section fallback for non-lesson pages.
21
+ */
22
+ export function resolveNumbering(input: any): {
23
+ prefix: any;
24
+ offset: number;
25
+ };
26
+ /**
27
+ * The visible number for one display equation. With a `prefix` (the part's
28
+ * ordinal) the scheme is `‹part›.‹offset + indexInLesson›` and `##` headings
14
29
  * are ignored; without it, the legacy `##`-section scheme.
15
30
  */
16
- export function displayEquationNumber({ chapter, section, indexInSection, indexInLesson, }: {
17
- chapter: any;
31
+ export function displayEquationNumber({ prefix, offset, section, indexInSection, indexInLesson, }: {
32
+ prefix: any;
33
+ offset?: number | undefined;
18
34
  section: any;
19
35
  indexInSection: any;
20
36
  indexInLesson: any;
@@ -35,58 +51,73 @@ export function stripFencedCode(markdown: any): string;
35
51
  * `keep`, when given, leaves a component of that name untouched instead of
36
52
  * blanking it — `keyed-jsx-floats.mjs` uses this to blank every *other*
37
53
  * component while still finding `<Table>`/`<Figure>` tags at the top level.
54
+ *
55
+ * `transparent`, when given, removes only those components' opening/closing
56
+ * tags and exposes their body to the later blanking pass. `<Memo>` and
57
+ * `<ShortNote>` are intentional uses: unlike a worked-example/presentation
58
+ * wrapper, their bodies are details in the lesson's reading order, so numbered
59
+ * equations/floats in them stay in the official sequence. Section headings in
60
+ * a transparent body do not change the surrounding page's section counter.
38
61
  */
39
- export function stripJsxComponents(markdown: any, keep: any): string;
62
+ export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]): string;
40
63
  /**
41
- * Every keyed display equation in a Markdown body, in appearance order, with
42
- * its assigned number. `$$…$$` blocks only. With `chapter` (the lesson's
43
- * course ordinal) the number is `‹chapter›.‹n›`, `n` sequential over every
44
- * display block in the lesson; without it, `##` headings advance a section
45
- * counter and the number is `‹section›.‹indexInSection›`. Either way every
46
- * display block advances the index, so a keyed equation's number reflects its
47
- * position among *all* display equations, not only the keyed ones.
64
+ * Every display equation in a Markdown body, keyed or not, in appearance order
65
+ * with its assigned number. `$$…$$` blocks only. With `numbering`
66
+ * (`{ prefix, offset }`, the part's ordinal and the count of display equations
67
+ * in earlier lessons of that part) the number is `‹part›.‹offset + n›`, `n`
68
+ * sequential over every display block in the lesson; without it, `##` headings
69
+ * advance a section counter and the number is `‹section›.‹indexInSection›`.
70
+ * Either way every display block advances the index, so a keyed equation's
71
+ * number reflects its position among *all* display equations, not only the
72
+ * keyed ones.
48
73
  */
49
- export function scanEquationLabels(markdown: any, chapter: any): {
50
- key: string;
74
+ export function scanDisplayEquations(markdown: any, numbering: any): {
75
+ key: string | null | undefined;
76
+ number: string;
77
+ section: number;
78
+ indexInSection: number;
79
+ }[];
80
+ /** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
81
+ export function scanEquationLabels(markdown: any, numbering: any): {
82
+ key: string | null | undefined;
51
83
  number: string;
52
84
  section: number;
53
85
  indexInSection: number;
54
86
  }[];
87
+ /**
88
+ * How many display equations a lesson body holds. This is what makes `n` run
89
+ * continuously across a part: a lesson's `offset` is the sum of this over every
90
+ * earlier lesson in the same part.
91
+ */
92
+ export function countDisplayEquations(markdown: any): number;
55
93
  /**
56
94
  * The mdast equivalent of `scanEquationLabels` for *every* display equation:
57
95
  * walk the tree in document order, advancing the section counter on every
58
96
  * depth-2 heading and emitting a record for every `math` node. `key` is the
59
- * `\label{eq:…}` payload when present, else `null`. `chapter` is threaded the
97
+ * `\label{eq:…}` payload when present, else `null`. `numbering` is threaded the
60
98
  * same way as in `scanEquationLabels`.
61
99
  */
62
- export function collectDisplayEquations(tree: any, chapter: any): {
63
- key: string | null;
64
- number: string;
65
- section: number;
66
- indexInSection: number;
67
- node: any;
68
- }[];
100
+ export function collectDisplayEquations(tree: any, numbering: any): any[];
69
101
  /**
70
102
  * The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
71
103
  * `scanEquationLabels`. Numbers reflect position among all display equations.
72
104
  */
73
- export function collectEquationLabels(tree: any, chapter: any): {
74
- key: string | null;
75
- number: string;
76
- section: number;
77
- indexInSection: number;
78
- node: any;
79
- }[];
105
+ export function collectEquationLabels(tree: any, numbering: any): any[];
80
106
  /**
81
107
  * Equation identity (Step D7, extended F3) — shared, framework-free helpers.
82
108
  *
83
109
  * Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
84
- * from appearance order. When the caller supplies a `chapter` (the lesson's
85
- * ordinal position in the course), the number is `‹chapter›.‹n›` — `n` counts
86
- * every display equation in the lesson and `##` headings do not affect it
87
- * (`(2.4)` = fourth display equation of the second lesson). With no `chapter`
88
- * the number falls back to the `##`-section scheme `‹section›.‹indexInSection›`
89
- * (used by hand-built test trees and non-lesson pages). An author additionally
110
+ * from appearance order. When the caller supplies `numbering`
111
+ * (`{ prefix, offset }` — see `resolveNumbering`), the number is
112
+ * `‹part›.‹offset + n›`: the prefix is the **part**'s ordinal among the sidebar
113
+ * groups, `n` counts every display equation in the lesson, and `offset` carries
114
+ * the count from earlier lessons of the same part so the sequence runs
115
+ * continuously across a part rather than restarting on every page. `##` and
116
+ * `###` headings never affect it — `(2.4)` is the fourth display equation of
117
+ * part 2, wherever in that part it sits. See `docs/authoring/structure.md` for
118
+ * the level names. With no `numbering` the number falls back to the
119
+ * `##`-section scheme `‹section›.‹indexInSection›` (used by hand-built test
120
+ * trees and non-lesson pages). An author additionally
90
121
  * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
91
122
  * block (inside math, so the `:` is safe and the form matches amsmath) when
92
123
  * they want a *stable* anchor that survives edits and cross-page references: