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
@@ -20,15 +20,16 @@ export function parseKeyedFloatRef(raw: any, { prefix }: {
20
20
  /**
21
21
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
22
22
  * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
23
- * With `chapter` (the lesson's course ordinal) the number is `‹chapter›.‹n›`,
24
- * `n` sequential over the whole lesson; without it, `##` headings advance a
25
- * section counter. Other JSX components' bodies are blanked first so a
26
- * `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like text
27
- * inside an unrelated component) is never counted.
23
+ * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
24
+ * this kind of float in earlier lessons of that part) the number is
25
+ * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
26
+ * headings advance a section counter. Other JSX components' bodies are blanked
27
+ * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
28
+ * text inside an unrelated component) is never counted.
28
29
  */
29
- export function scanKeyedJsxFloats(markdown: any, { tagName, chapter }: {
30
+ export function scanKeyedJsxFloats(markdown: any, { tagName, numbering }: {
30
31
  tagName: any;
31
- chapter: any;
32
+ numbering: any;
32
33
  }): {
33
34
  key: string | null | undefined;
34
35
  number: string;
@@ -36,21 +37,23 @@ export function scanKeyedJsxFloats(markdown: any, { tagName, chapter }: {
36
37
  indexInSection: number;
37
38
  }[];
38
39
  /**
39
- * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's *top-level*
40
- * children in document order, advancing the section counter on every
41
- * depth-2 heading and emitting a record for every `mdxJsxFlowElement` named
42
- * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `chapter`
43
- * is threaded the same way as in `scanKeyedJsxFloats`.
40
+ * How many floats of this kind a lesson body holds — the input to the next
41
+ * lesson's `offset` within the same part.
44
42
  */
45
- export function collectKeyedJsxFloats(tree: any, { tagName, chapter }: {
43
+ export function countKeyedJsxFloats(markdown: any, { tagName }: {
46
44
  tagName: any;
47
- chapter: any;
48
- }): {
49
- key: any;
50
- number: string;
51
- section: number;
52
- indexInSection: number;
53
- node: any;
54
- }[];
45
+ }): number;
46
+ /**
47
+ * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
48
+ * children (and through `<Memo>` bodies) in document order, advancing the
49
+ * section counter on every top-level depth-2 heading and emitting a record for
50
+ * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
51
+ * `collectDisplayEquations`. `numbering` is threaded the same way as in
52
+ * `scanKeyedJsxFloats`.
53
+ */
54
+ export function collectKeyedJsxFloats(tree: any, { tagName, numbering }: {
55
+ tagName: any;
56
+ numbering: any;
57
+ }): any[];
55
58
  /** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
56
59
  export const KEYED_FLOAT_KEY_PATTERN: RegExp;
@@ -1,20 +1,20 @@
1
1
  /**
2
- * Keyed-float identity for a JSX-wrapped table or figure, shared between
3
- * `<Table>` and `<Figure>` (Step F-tables-figures).
2
+ * Keyed-float identity for a JSX-wrapped table, figure, or diagram, shared
3
+ * between `<Table>`, `<Figure>`, and `<Diagram>`.
4
4
  *
5
5
  * This mirrors `equations.mjs`, but the key comes from a plain `id` prop on
6
6
  * the wrapper component rather than a `\label{eq:key}` embedded in shared
7
- * LaTeX source — a `<Table>`/`<Figure>` is purpose-built, so there is no
7
+ * LaTeX source — a `<Table>`/`<Figure>`/`<Diagram>` is purpose-built, so there is no
8
8
  * ambiguity an embedded marker would need to resolve. An author writes the
9
9
  * bare key (`id="kinematics-summary"`); the rendered DOM id and the `[[…]]`
10
10
  * reference prefix are computed the same way `\label{eq:key}` becomes
11
- * `#eq-key` — `keyedFloatAnchorId` prepends `tab-`/`fig-`.
11
+ * `#eq-key` — `keyedFloatAnchorId` prepends `tab-`/`fig-`/`dia-`.
12
12
  *
13
- * Only top-level occurrences count, exactly like `collectDisplayEquations`:
14
- * a `<Table>` nested inside a `<CompactExample>` (or any other component) is
15
- * illustrative, not part of the lesson's official sequence, so both the
16
- * mdast walker and the raw-text scanner only look at the document's direct
17
- * top-level blocks.
13
+ * Only top-level occurrences count, exactly like `collectDisplayEquations`,
14
+ * except that `<Memo>` is transparent: a float in a memo remains a side-note
15
+ * in the lesson's reading order. A keyed float nested inside a
16
+ * `<CompactExample>` (or any other component) is illustrative, not part of the
17
+ * official sequence.
18
18
  *
19
19
  * `scanKeyedJsxFloats` (raw Markdown, for the manifest builder) and
20
20
  * `collectKeyedJsxFloats` (mdast, for `remark-notation.mjs`) MUST agree —
@@ -22,7 +22,11 @@
22
22
  * `equations.test.ts` holds equations to.
23
23
  */
24
24
 
25
- import { stripFencedCode, stripJsxComponents } from './equations.mjs';
25
+ import {
26
+ resolveNumbering,
27
+ stripFencedCode,
28
+ stripJsxComponents,
29
+ } from './equations.mjs';
26
30
 
27
31
  /** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
28
32
  export const KEYED_FLOAT_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -33,15 +37,22 @@ function floatNumber(section, index) {
33
37
  }
34
38
 
35
39
  /**
36
- * The visible number for one float. With `chapter` (the lesson's course
37
- * ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings are
38
- * ignored; without it, the legacy `##`-section scheme. Mirrors
39
- * `displayEquationNumber` in `equations.mjs`.
40
+ * The visible number for one float. With a `prefix` (the part's ordinal) the
41
+ * scheme is `‹part›.‹offset + indexInLesson›` and `##` headings are ignored;
42
+ * without it, the legacy `##`-section scheme. Mirrors `displayEquationNumber`
43
+ * in `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
44
+ * advances the `<Figure>` or `<Diagram>` sequence — so `offset` is per kind too.
40
45
  */
41
- function keyedFloatNumber({ chapter, section, indexInSection, indexInLesson }) {
42
- return chapter == null
46
+ function keyedFloatNumber({
47
+ prefix,
48
+ offset = 0,
49
+ section,
50
+ indexInSection,
51
+ indexInLesson,
52
+ }) {
53
+ return prefix == null
43
54
  ? floatNumber(section, indexInSection)
44
- : `${chapter}.${indexInLesson}`;
55
+ : `${prefix}.${offset + indexInLesson}`;
45
56
  }
46
57
 
47
58
  /** The anchor id for a float, e.g. `{prefix:'tab', key:'foo'}` → `tab-foo`. */
@@ -71,14 +82,19 @@ function readIdAttribute(attributesSource) {
71
82
  /**
72
83
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
73
84
  * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
74
- * With `chapter` (the lesson's course ordinal) the number is `‹chapter›.‹n›`,
75
- * `n` sequential over the whole lesson; without it, `##` headings advance a
76
- * section counter. Other JSX components' bodies are blanked first so a
77
- * `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like text
78
- * inside an unrelated component) is never counted.
85
+ * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
86
+ * this kind of float in earlier lessons of that part) the number is
87
+ * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
88
+ * headings advance a section counter. Other JSX components' bodies are blanked
89
+ * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
90
+ * text inside an unrelated component) is never counted.
79
91
  */
80
- export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
81
- const text = stripJsxComponents(stripFencedCode(markdown), tagName);
92
+ export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
93
+ const { prefix, offset } = resolveNumbering(numbering);
94
+ const text = stripJsxComponents(stripFencedCode(markdown), tagName, [
95
+ 'Memo',
96
+ 'ShortNote',
97
+ ]);
82
98
  const tokens = [];
83
99
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
84
100
  const opening = new RegExp(`<${tagName}\\b([^>]*)>`, 'g');
@@ -106,7 +122,8 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
106
122
  labels.push({
107
123
  key: token.key,
108
124
  number: keyedFloatNumber({
109
- chapter,
125
+ prefix,
126
+ offset,
110
127
  section,
111
128
  indexInSection,
112
129
  indexInLesson,
@@ -119,26 +136,30 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
119
136
  }
120
137
 
121
138
  /**
122
- * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's *top-level*
123
- * children in document order, advancing the section counter on every
124
- * depth-2 heading and emitting a record for every `mdxJsxFlowElement` named
125
- * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `chapter`
126
- * is threaded the same way as in `scanKeyedJsxFloats`.
139
+ * How many floats of this kind a lesson body holds — the input to the next
140
+ * lesson's `offset` within the same part.
127
141
  */
128
- export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
142
+ export function countKeyedJsxFloats(markdown, { tagName }) {
143
+ return scanKeyedJsxFloats(markdown, { tagName }).length;
144
+ }
145
+
146
+ /**
147
+ * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
148
+ * children (and through `<Memo>` bodies) in document order, advancing the
149
+ * section counter on every top-level depth-2 heading and emitting a record for
150
+ * every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
151
+ * `collectDisplayEquations`. `numbering` is threaded the same way as in
152
+ * `scanKeyedJsxFloats`.
153
+ */
154
+ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
155
+ const { prefix, offset } = resolveNumbering(numbering);
129
156
  const floats = [];
130
157
  let section = 0;
131
158
  let indexInSection = 0;
132
159
  let indexInLesson = 0;
133
160
 
134
- const children = Array.isArray(tree?.children) ? tree.children : [];
135
- for (const node of children) {
136
- if (!node || typeof node !== 'object') continue;
137
- if (node.type === 'heading' && node.depth === 2) {
138
- section += 1;
139
- indexInSection = 0;
140
- continue;
141
- }
161
+ const collectFromDetail = (node) => {
162
+ if (!node || typeof node !== 'object') return;
142
163
  if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
143
164
  indexInSection += 1;
144
165
  indexInLesson += 1;
@@ -151,7 +172,8 @@ export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
151
172
  floats.push({
152
173
  key: key ?? null,
153
174
  number: keyedFloatNumber({
154
- chapter,
175
+ prefix,
176
+ offset,
155
177
  section,
156
178
  indexInSection,
157
179
  indexInLesson,
@@ -160,8 +182,35 @@ export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
160
182
  indexInSection,
161
183
  node,
162
184
  });
185
+ return;
186
+ }
187
+ if (
188
+ node.type === 'mdxJsxFlowElement' &&
189
+ node.name !== 'Memo' &&
190
+ node.name !== 'ShortNote'
191
+ ) {
192
+ return;
193
+ }
194
+ if (!Array.isArray(node.children)) return;
195
+ for (const child of node.children) collectFromDetail(child);
196
+ };
197
+
198
+ const children = Array.isArray(tree?.children) ? tree.children : [];
199
+ for (const node of children) {
200
+ if (!node || typeof node !== 'object') continue;
201
+ if (node.type === 'heading' && node.depth === 2) {
202
+ section += 1;
203
+ indexInSection = 0;
204
+ continue;
205
+ }
206
+ if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
207
+ collectFromDetail(node);
208
+ } else if (
209
+ node.type === 'mdxJsxFlowElement' &&
210
+ (node.name === 'Memo' || node.name === 'ShortNote')
211
+ ) {
212
+ collectFromDetail(node);
163
213
  }
164
214
  }
165
215
  return floats;
166
216
  }
167
-
@@ -0,0 +1,35 @@
1
+ /** Build-time identity and reference validation for knowledge checks. */
2
+ interface KnowledgeCheckLessonInput {
3
+ readonly lessonId: string;
4
+ readonly slug: string;
5
+ readonly file: string;
6
+ readonly body: string;
7
+ readonly assessmentIds: readonly string[];
8
+ readonly numbering?: {
9
+ readonly prefix: number;
10
+ readonly offset: number;
11
+ };
12
+ }
13
+ interface KnowledgeCheckLabelRecord {
14
+ readonly key: string;
15
+ readonly number: string;
16
+ readonly lessonId: string;
17
+ readonly slug: string;
18
+ readonly indexInLesson: number;
19
+ }
20
+ interface KnowledgeCheckDiagnostic {
21
+ readonly code: 'check-invalid-key' | 'check-duplicate-key' | 'check-ref-resolves';
22
+ readonly severity: 'error';
23
+ readonly message: string;
24
+ readonly file?: string;
25
+ readonly lessonId?: string;
26
+ readonly key?: string;
27
+ }
28
+ interface KnowledgeCheckValidation {
29
+ readonly labels: readonly KnowledgeCheckLabelRecord[];
30
+ readonly numbersBySlug: Record<string, Record<string, string>>;
31
+ readonly diagnostics: readonly KnowledgeCheckDiagnostic[];
32
+ }
33
+ declare function validateKnowledgeChecks(lessons: readonly KnowledgeCheckLessonInput[]): KnowledgeCheckValidation;
34
+
35
+ export { type KnowledgeCheckDiagnostic, type KnowledgeCheckLabelRecord, type KnowledgeCheckLessonInput, type KnowledgeCheckValidation, validateKnowledgeChecks };
@@ -0,0 +1,78 @@
1
+ import {
2
+ collectKnowledgeChecks,
3
+ KNOWLEDGE_CHECK_KEY_PATTERN,
4
+ parseKnowledgeCheckRef
5
+ } from "./knowledge-checks.mjs";
6
+ const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
7
+ function stripCode(text) {
8
+ return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
9
+ }
10
+ function validateKnowledgeChecks(lessons) {
11
+ const labels = [];
12
+ const diagnostics = [];
13
+ const numbersBySlug = {};
14
+ for (const lesson of [...lessons].sort(
15
+ (a, b) => a.lessonId.localeCompare(b.lessonId)
16
+ )) {
17
+ const seen = /* @__PURE__ */ new Set();
18
+ const table = {};
19
+ for (const check of collectKnowledgeChecks(
20
+ lesson.assessmentIds,
21
+ lesson.numbering
22
+ )) {
23
+ if (!KNOWLEDGE_CHECK_KEY_PATTERN.test(check.key)) {
24
+ diagnostics.push({
25
+ code: "check-invalid-key",
26
+ severity: "error",
27
+ message: `${lesson.lessonId} has a knowledge-check id "${check.key}" that is not a valid content key.`,
28
+ file: lesson.file,
29
+ lessonId: lesson.lessonId,
30
+ key: check.key
31
+ });
32
+ continue;
33
+ }
34
+ if (seen.has(check.key)) {
35
+ diagnostics.push({
36
+ code: "check-duplicate-key",
37
+ severity: "error",
38
+ message: `${lesson.lessonId} lists knowledge check "${check.key}" twice; an assessment id is unique per lesson.`,
39
+ file: lesson.file,
40
+ lessonId: lesson.lessonId,
41
+ key: check.key
42
+ });
43
+ continue;
44
+ }
45
+ seen.add(check.key);
46
+ table[check.key] = check.number;
47
+ labels.push({
48
+ ...check,
49
+ lessonId: lesson.lessonId,
50
+ slug: lesson.slug
51
+ });
52
+ }
53
+ numbersBySlug[lesson.slug] = table;
54
+ }
55
+ for (const lesson of lessons) {
56
+ for (const match of stripCode(lesson.body).matchAll(BRACKET_REF)) {
57
+ const ref = parseKnowledgeCheckRef((match[1] ?? "").trim());
58
+ if (!ref) continue;
59
+ const targetSlug = ref.lesson ?? lesson.slug;
60
+ if (numbersBySlug[targetSlug]?.[ref.key] !== void 0) continue;
61
+ diagnostics.push({
62
+ code: "check-ref-resolves",
63
+ severity: "error",
64
+ message: ref.lesson ? `${lesson.lessonId} references [[${ref.lesson}#check-${ref.key}]] but that lesson has no knowledge check "${ref.key}"` : `${lesson.lessonId} references [[check-${ref.key}]] but its .checks.yml does not list "${ref.key}"`,
65
+ file: lesson.file,
66
+ lessonId: lesson.lessonId,
67
+ key: ref.key
68
+ });
69
+ }
70
+ }
71
+ diagnostics.sort(
72
+ (a, b) => a.code.localeCompare(b.code) || (a.lessonId ?? "").localeCompare(b.lessonId ?? "") || (a.key ?? "").localeCompare(b.key ?? "") || a.message.localeCompare(b.message)
73
+ );
74
+ return { labels, numbersBySlug, diagnostics };
75
+ }
76
+ export {
77
+ validateKnowledgeChecks
78
+ };
@@ -0,0 +1,25 @@
1
+ /** `[[check-id]]`, or `[[lesson/slug#check-id]]` across pages. */
2
+ export function parseKnowledgeCheckRef(raw: any): {
3
+ lesson: string;
4
+ key: string;
5
+ } | {
6
+ key: string;
7
+ lesson?: undefined;
8
+ } | undefined;
9
+ /** The stable DOM fragment for a knowledge check. */
10
+ export function knowledgeCheckAnchorId(key: any): string;
11
+ /**
12
+ * Number an ordered assessment-id list. With manifest placement, the number
13
+ * is `part.offset + index`; without it, the list remains usefully page-local.
14
+ */
15
+ export function collectKnowledgeChecks(assessmentIds: any, numbering: any): any;
16
+ /**
17
+ * Stable identity and numbering helpers for lesson knowledge checks.
18
+ *
19
+ * A course owns check titles and questions in `content/assessments/*.json`,
20
+ * then places their ids in a lesson's colocated `.checks.yml`. That ordered
21
+ * list is the check sequence: numbers run independently across each part,
22
+ * exactly like the equation/table/figure/diagram streams.
23
+ */
24
+ /** Assessment ids use the content key grammar, including optional dots. */
25
+ export const KNOWLEDGE_CHECK_KEY_PATTERN: RegExp;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Stable identity and numbering helpers for lesson knowledge checks.
3
+ *
4
+ * A course owns check titles and questions in `content/assessments/*.json`,
5
+ * then places their ids in a lesson's colocated `.checks.yml`. That ordered
6
+ * list is the check sequence: numbers run independently across each part,
7
+ * exactly like the equation/table/figure/diagram streams.
8
+ */
9
+
10
+ /** Assessment ids use the content key grammar, including optional dots. */
11
+ export const KNOWLEDGE_CHECK_KEY_PATTERN =
12
+ /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
13
+
14
+ /** `[[check-id]]`, or `[[lesson/slug#check-id]]` across pages. */
15
+ export function parseKnowledgeCheckRef(raw) {
16
+ const match =
17
+ /^(?:([a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*)#)?check-([a-z0-9]+(?:[.-][a-z0-9]+)*)$/.exec(
18
+ String(raw).trim(),
19
+ );
20
+ if (!match) return undefined;
21
+ return match[1] ? { lesson: match[1], key: match[2] } : { key: match[2] };
22
+ }
23
+
24
+ /** The stable DOM fragment for a knowledge check. */
25
+ export function knowledgeCheckAnchorId(key) {
26
+ return `check-${key}`;
27
+ }
28
+
29
+ /**
30
+ * Number an ordered assessment-id list. With manifest placement, the number
31
+ * is `part.offset + index`; without it, the list remains usefully page-local.
32
+ */
33
+ export function collectKnowledgeChecks(assessmentIds, numbering) {
34
+ const prefix = Number.isInteger(numbering?.prefix) && numbering.prefix > 0
35
+ ? numbering.prefix
36
+ : undefined;
37
+ const offset = Number.isInteger(numbering?.offset) && numbering.offset >= 0
38
+ ? numbering.offset
39
+ : 0;
40
+ return assessmentIds.map((key, index) => ({
41
+ key,
42
+ number: prefix == null ? String(index + 1) : `${prefix}.${offset + index + 1}`,
43
+ indexInLesson: index + 1,
44
+ }));
45
+ }
@@ -15,7 +15,15 @@ interface LessonNotationFrontmatter {
15
15
  *
16
16
  * Shared definitions are read from the one compiler manifest (Phase D5); this
17
17
  * module never re-reads the content tree.
18
+ *
19
+ * A nested component whose `body` contains notation absent from lesson MDX
20
+ * passes `{ locals: Astro.locals, context: 'Component formula name' }`. That
21
+ * request-local declaration lets `LessonFooter` ship just those extra cards and
22
+ * gives an unknown `\explain` key an actionable render-time diagnostic.
18
23
  */
19
- declare function labMathScope(notation: LessonNotationFrontmatter | undefined, body?: string): Promise<LabMathScopeDefinition[]>;
24
+ declare function labMathScope(notation: LessonNotationFrontmatter | undefined, body?: string, component?: {
25
+ readonly locals: object;
26
+ readonly context: string;
27
+ }): Promise<LabMathScopeDefinition[]>;
20
28
 
21
29
  export { labMathScope };
@@ -1,25 +1,53 @@
1
1
  import { loadManifest } from "../compiler/manifest.js";
2
+ import {
3
+ registerPageNotationDependencies,
4
+ routeIdFromLocals
5
+ } from "./page-notation.js";
2
6
  const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
3
7
  const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
4
- async function labMathScope(notation, body = "") {
8
+ async function labMathScope(notation, body = "", component) {
5
9
  const local = notation?.local ?? [];
6
10
  const scope = local.map((definition) => ({
7
11
  key: definition.key,
8
12
  notation: definition.latex
9
13
  }));
10
14
  const referenced = /* @__PURE__ */ new Set();
15
+ const explicitlyExplained = /* @__PURE__ */ new Set();
11
16
  for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
12
- for (const match of body.matchAll(pattern)) referenced.add(match[1]);
17
+ for (const match of body.matchAll(pattern)) {
18
+ referenced.add(match[1]);
19
+ if (pattern === EXPLAIN_KEY) explicitlyExplained.add(match[1]);
20
+ }
13
21
  }
14
22
  const localKeys = new Set(local.map((definition) => definition.key));
15
23
  const manifest = await loadManifest();
16
24
  const sharedByKey = new Map(
17
25
  manifest.notation.definitions.filter((definition) => definition.kind === "shared").map((definition) => [definition.key, definition])
18
26
  );
27
+ const resolvedKeys = /* @__PURE__ */ new Set();
19
28
  for (const key of referenced) {
20
- if (localKeys.has(key)) continue;
29
+ if (localKeys.has(key)) {
30
+ resolvedKeys.add(key);
31
+ continue;
32
+ }
21
33
  const shared = sharedByKey.get(key);
22
- if (shared) scope.push({ key: shared.key, notation: shared.notation });
34
+ if (shared) {
35
+ scope.push({ key: shared.key, notation: shared.notation });
36
+ resolvedKeys.add(key);
37
+ continue;
38
+ }
39
+ if (explicitlyExplained.has(key)) {
40
+ throw new Error(
41
+ `Notation dependency error${component ? ` in lesson "${routeIdFromLocals(component.locals) ?? "unknown"}" (${component.context})` : ""}: unknown key "${key}". Define it in notation.local or the shared notation registry.`
42
+ );
43
+ }
44
+ }
45
+ if (component) {
46
+ registerPageNotationDependencies(
47
+ component.locals,
48
+ resolvedKeys,
49
+ component.context
50
+ );
23
51
  }
24
52
  return scope;
25
53
  }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Strip Explico's own `\explain{key}{latex}` / `\slot{key}{latex}` markers
3
+ * (see `katex-options.mjs`) down to the LaTeX they wrap, recursively. Lesson
4
+ * math — both a notation card's `formula` and an equation written directly
5
+ * in prose — carries these markers inline so every symbol is hoverable; a
6
+ * reader copying the equation out of this system wants the plain math
7
+ * underneath, not internal notation-key plumbing that means nothing to
8
+ * KaTeX, MathJax, or LaTeX anywhere else. Shared by `render-notation-math.ts`
9
+ * (a notation card's copy button) and `remark-notation.mjs` (a numbered
10
+ * display equation's copy button) — kept here, rather than in either, so
11
+ * neither module has to import the other.
12
+ */
13
+ export function stripNotationMarkers(latex: any): string;
1
14
  /**
2
15
  * Resolve and annotate one KaTeX expression against a page's flat glyph table.
3
16
  * Source offsets in the result always refer to the original, unmodified LaTeX.
@@ -36,6 +49,23 @@ export function collectOperators(latex: string): Array<{
36
49
  name: string;
37
50
  source: "named" | "operatorname";
38
51
  }>;
52
+ /**
53
+ * Every multi-letter `\mathrm{…}` label in one KaTeX expression (`KE`, `PV`,
54
+ * `opt`) — the upright-label counterpart to {@link collectOperators}. A
55
+ * single-letter body (`\mathrm{d}`, the N7 differential) is not collected:
56
+ * that one is a fixed typographic operator, not a named quantity a reader
57
+ * would need explained.
58
+ *
59
+ * Pure read: nothing here rewrites the source. Used by the
60
+ * `roman-label-explained` consistency check (rule N10); the glyph resolver
61
+ * still skips `\mathrm{…}` bodies whole ([N3](../../docs/authoring/notation.md#n3)).
62
+ *
63
+ * @param {string} latex
64
+ * @returns {Array<{ name: string }>}
65
+ */
66
+ export function collectRomanLabels(latex: string): Array<{
67
+ name: string;
68
+ }>;
39
69
  /**
40
70
  * Page-level glyph-map resolver (cleanup plan, D1 decision 2026-09-02).
41
71
  *