explico 0.1.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 (113) hide show
  1. package/README.md +108 -0
  2. package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
  3. package/dist/analytics/AnalyticsEmitter.js +5 -0
  4. package/dist/compiler/cli.d.ts +189 -0
  5. package/dist/compiler/cli.js +552 -0
  6. package/dist/compiler/collections.d.ts +70 -0
  7. package/dist/compiler/collections.js +383 -0
  8. package/dist/compiler/course-config.d.ts +31 -0
  9. package/dist/compiler/course-config.js +0 -0
  10. package/dist/compiler/lesson-derivation.d.ts +89 -0
  11. package/dist/compiler/lesson-derivation.js +91 -0
  12. package/dist/compiler/manifest.d.ts +178 -0
  13. package/dist/compiler/manifest.js +345 -0
  14. package/dist/compiler/resolution-report.d.ts +45 -0
  15. package/dist/compiler/resolution-report.js +97 -0
  16. package/dist/curriculum/validation.d.ts +76 -0
  17. package/dist/curriculum/validation.js +320 -0
  18. package/dist/progress/ProgressRepository.d.ts +53 -0
  19. package/dist/progress/ProgressRepository.js +12 -0
  20. package/dist/reference/base-library.mjs +27 -0
  21. package/dist/reference/citation-format.mjs +133 -0
  22. package/dist/reference/consistency.d.ts +64 -0
  23. package/dist/reference/consistency.js +234 -0
  24. package/dist/reference/curriculum-alignment.d.ts +7 -0
  25. package/dist/reference/curriculum-alignment.js +166 -0
  26. package/dist/reference/equations-validate.d.ts +50 -0
  27. package/dist/reference/equations-validate.js +90 -0
  28. package/dist/reference/equations.mjs +144 -0
  29. package/dist/reference/gate-math.d.ts +41 -0
  30. package/dist/reference/gate-math.js +151 -0
  31. package/dist/reference/katex-options.mjs +91 -0
  32. package/dist/reference/lab-math-scope.d.ts +21 -0
  33. package/dist/reference/lab-math-scope.js +28 -0
  34. package/dist/reference/label.d.ts +11 -0
  35. package/dist/reference/label.js +38 -0
  36. package/dist/reference/lint-ignore.d.ts +32 -0
  37. package/dist/reference/lint-ignore.js +39 -0
  38. package/dist/reference/math-glyphs.mjs +830 -0
  39. package/dist/reference/prose.d.ts +15 -0
  40. package/dist/reference/prose.js +12 -0
  41. package/dist/reference/references.d.ts +17 -0
  42. package/dist/reference/references.js +42 -0
  43. package/dist/reference/registry.d.ts +6 -0
  44. package/dist/reference/registry.js +548 -0
  45. package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
  46. package/dist/reference/remark-citation.mjs +0 -0
  47. package/dist/reference/remark-notation.mjs +753 -0
  48. package/dist/reference/render-lab-math.d.ts +21 -0
  49. package/dist/reference/render-lab-math.js +58 -0
  50. package/dist/reference/types.d.ts +151 -0
  51. package/dist/reference/types.js +0 -0
  52. package/dist/reference/units-vocab.d.ts +26 -0
  53. package/dist/reference/units-vocab.js +119 -0
  54. package/dist/session/PreferenceStore.d.ts +28 -0
  55. package/dist/session/PreferenceStore.js +30 -0
  56. package/dist/session/user-context.d.ts +7 -0
  57. package/dist/session/user-context.js +10 -0
  58. package/dist/session/user.d.ts +21 -0
  59. package/dist/session/user.js +8 -0
  60. package/package.json +53 -0
  61. package/src/analytics/AnalyticsEmitter.ts +23 -0
  62. package/src/compiler/cli.ts +866 -0
  63. package/src/compiler/collections.ts +554 -0
  64. package/src/compiler/course-config.ts +32 -0
  65. package/src/compiler/lesson-derivation.ts +186 -0
  66. package/src/compiler/manifest.ts +601 -0
  67. package/src/compiler/resolution-report.ts +150 -0
  68. package/src/components/CurriculumMap.astro +208 -0
  69. package/src/components/HoverPanel.astro +67 -0
  70. package/src/components/assessments/AssessmentRunner.tsx +320 -0
  71. package/src/components/assessments/AssessmentSet.astro +54 -0
  72. package/src/components/citation/CitationLayer.astro +173 -0
  73. package/src/components/examples/CompactExample.astro +34 -0
  74. package/src/components/examples/CompactExamples.astro +253 -0
  75. package/src/components/hover-panel.ts +273 -0
  76. package/src/components/notation/NotationGlossary.astro +412 -0
  77. package/src/components/notation/NotationLayer.astro +594 -0
  78. package/src/components/notation/NotationMath.astro +24 -0
  79. package/src/components/notation/SafeMathNode.astro +23 -0
  80. package/src/components/notation/render-notation-math.ts +223 -0
  81. package/src/components/starlight/EquationEnhancer.astro +72 -0
  82. package/src/components/starlight/LayoutHeader.astro +102 -0
  83. package/src/components/starlight/LayoutPageSidebar.astro +33 -0
  84. package/src/components/starlight/LayoutSidebar.astro +29 -0
  85. package/src/components/starlight/LessonFooter.astro +58 -0
  86. package/src/content-config.ts +259 -0
  87. package/src/curriculum/validation.ts +481 -0
  88. package/src/progress/ProgressRepository.ts +64 -0
  89. package/src/reference/base-library.mjs +27 -0
  90. package/src/reference/citation-format.mjs +133 -0
  91. package/src/reference/consistency.ts +375 -0
  92. package/src/reference/curriculum-alignment.ts +228 -0
  93. package/src/reference/equations-validate.ts +167 -0
  94. package/src/reference/equations.mjs +144 -0
  95. package/src/reference/gate-math.ts +250 -0
  96. package/src/reference/katex-options.mjs +91 -0
  97. package/src/reference/lab-math-scope.ts +54 -0
  98. package/src/reference/label.ts +47 -0
  99. package/src/reference/lint-ignore.ts +75 -0
  100. package/src/reference/math-glyphs.mjs +830 -0
  101. package/src/reference/prose.ts +21 -0
  102. package/src/reference/references.ts +82 -0
  103. package/src/reference/registry.ts +723 -0
  104. package/src/reference/rehype-fail-katex-errors.mjs +55 -0
  105. package/src/reference/remark-citation.mjs +0 -0
  106. package/src/reference/remark-notation.mjs +753 -0
  107. package/src/reference/render-lab-math.ts +99 -0
  108. package/src/reference/types.ts +182 -0
  109. package/src/reference/units-vocab.ts +143 -0
  110. package/src/session/PreferenceStore.ts +54 -0
  111. package/src/session/user-context.ts +18 -0
  112. package/src/session/user.ts +24 -0
  113. package/src/styles/global.css +763 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Under-specification safeguard for notation `meaning` prose (Phase C1b).
3
+ *
4
+ * A page-local symbol must not get a glossary-style panel for a throwaway
5
+ * definition, so `meaning` has to be real prose: at least four words and not a
6
+ * placeholder phrase. Used by the content schema (`src/content.config.ts`) and
7
+ * the loader so a bad entry fails both `astro check` and `validate:content`.
8
+ */
9
+ /** Placeholder phrases that do not count as a substantive `meaning`. */
10
+ declare const PLACEHOLDER_PROSE: RegExp;
11
+ /** Minimum words for a `meaning` to read as a definition rather than a label. */
12
+ declare const MEANING_MIN_WORDS = 4;
13
+ declare function isSubstantiveMeaning(value: string): boolean;
14
+
15
+ export { MEANING_MIN_WORDS, PLACEHOLDER_PROSE, isSubstantiveMeaning };
@@ -0,0 +1,12 @@
1
+ const PLACEHOLDER_PROSE = /^(?:a\s+(?:variable|value|quantity|constant|parameter|symbol)|tbd|todo|fixme|see\s+above|as\s+above|placeholder|n\/a)\.?$/i;
2
+ const MEANING_MIN_WORDS = 4;
3
+ function isSubstantiveMeaning(value) {
4
+ const trimmed = value.trim();
5
+ if (PLACEHOLDER_PROSE.test(trimmed)) return false;
6
+ return trimmed.split(/\s+/).filter(Boolean).length >= MEANING_MIN_WORDS;
7
+ }
8
+ export {
9
+ MEANING_MIN_WORDS,
10
+ PLACEHOLDER_PROSE,
11
+ isSubstantiveMeaning
12
+ };
@@ -0,0 +1,17 @@
1
+ import { NotationDiagnostic, SourceSpan, NotationDefinitionScope } from './types.js';
2
+
3
+ declare const NOTATION_KEY_PATTERN: RegExp;
4
+ declare function isNotationKey(value: string): boolean;
5
+ declare function sharedDefinitionId(key: string): string;
6
+ declare function localDefinitionId(lessonId: string, key: string): string;
7
+ declare function definitionId(scope: NotationDefinitionScope, key: string): string;
8
+ /**
9
+ * Resolve by lexical scope. Page content and page-local definition bodies see
10
+ * local definitions first, then shared definitions. Shared definition bodies
11
+ * can only see shared definitions, so their meaning cannot change by caller.
12
+ */
13
+ declare function resolveReferenceId(key: string, scope: NotationDefinitionScope, availableDefinitionIds: ReadonlySet<string>): string | undefined;
14
+ declare function compareSourceSpans(left: SourceSpan | undefined, right: SourceSpan | undefined): number;
15
+ declare function compareDiagnostics(left: NotationDiagnostic, right: NotationDiagnostic): number;
16
+
17
+ export { NOTATION_KEY_PATTERN, compareDiagnostics, compareSourceSpans, definitionId, isNotationKey, localDefinitionId, resolveReferenceId, sharedDefinitionId };
@@ -0,0 +1,42 @@
1
+ const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
2
+ function isNotationKey(value) {
3
+ return NOTATION_KEY_PATTERN.test(value);
4
+ }
5
+ function sharedDefinitionId(key) {
6
+ return `shared:${key}`;
7
+ }
8
+ function localDefinitionId(lessonId, key) {
9
+ return `page:${lessonId}:${key}`;
10
+ }
11
+ function definitionId(scope, key) {
12
+ return scope.kind === "shared" ? sharedDefinitionId(key) : localDefinitionId(scope.lessonId, key);
13
+ }
14
+ function resolveReferenceId(key, scope, availableDefinitionIds) {
15
+ if (scope.kind === "page") {
16
+ const localId = localDefinitionId(scope.lessonId, key);
17
+ if (availableDefinitionIds.has(localId)) return localId;
18
+ }
19
+ const sharedId = sharedDefinitionId(key);
20
+ return availableDefinitionIds.has(sharedId) ? sharedId : void 0;
21
+ }
22
+ function compareSourceSpans(left, right) {
23
+ if (left === void 0) return right === void 0 ? 0 : 1;
24
+ if (right === void 0) return -1;
25
+ return left.file.localeCompare(right.file) || (left.line ?? 0) - (right.line ?? 0) || (left.column ?? 0) - (right.column ?? 0) || (left.endLine ?? 0) - (right.endLine ?? 0) || (left.endColumn ?? 0) - (right.endColumn ?? 0);
26
+ }
27
+ function compareDiagnostics(left, right) {
28
+ return severityRank(left.severity) - severityRank(right.severity) || left.code.localeCompare(right.code) || (left.lessonId ?? "").localeCompare(right.lessonId ?? "") || (left.key ?? "").localeCompare(right.key ?? "") || (left.definitionId ?? "").localeCompare(right.definitionId ?? "") || compareSourceSpans(left.source, right.source) || left.message.localeCompare(right.message);
29
+ }
30
+ function severityRank(severity) {
31
+ return severity === "error" ? 0 : 1;
32
+ }
33
+ export {
34
+ NOTATION_KEY_PATTERN,
35
+ compareDiagnostics,
36
+ compareSourceSpans,
37
+ definitionId,
38
+ isNotationKey,
39
+ localDefinitionId,
40
+ resolveReferenceId,
41
+ sharedDefinitionId
42
+ };
@@ -0,0 +1,6 @@
1
+ import { NotationRegistry, NotationRegistryInput } from './types.js';
2
+
3
+ declare function buildNotationRegistry(rawInput: NotationRegistryInput): NotationRegistry;
4
+ declare function assertValidNotation(registry: NotationRegistry): void;
5
+
6
+ export { assertValidNotation, buildNotationRegistry };
@@ -0,0 +1,548 @@
1
+ import {
2
+ compareDiagnostics,
3
+ compareSourceSpans,
4
+ isNotationKey,
5
+ localDefinitionId,
6
+ resolveReferenceId,
7
+ sharedDefinitionId
8
+ } from "./references";
9
+ function buildNotationRegistry(rawInput) {
10
+ const diagnostics = [];
11
+ const lessons = buildLessonIndex(rawInput.lessons, diagnostics);
12
+ const definitions = buildDefinitionIndex(
13
+ rawInput.sharedDefinitions,
14
+ lessons,
15
+ diagnostics
16
+ );
17
+ const availableDefinitionIds = new Set(definitions.keys());
18
+ for (const candidate of definitions.values()) {
19
+ const references = sortedReferences(candidate.input.references);
20
+ const resolved = [];
21
+ for (const reference of references) {
22
+ if (!validateReferenceKey(reference, candidate, diagnostics)) continue;
23
+ if (candidate.kind === "local") {
24
+ const lessonId = candidate.lesson.lessonId;
25
+ const definitionId2 = resolveReferenceId(
26
+ reference.key,
27
+ candidate.scope,
28
+ availableDefinitionIds
29
+ );
30
+ if (definitionId2 === void 0) {
31
+ diagnostics.push(
32
+ diagnostic({
33
+ code: "undefined-reference",
34
+ message: `${candidate.id} references unknown notation ${reference.key}`,
35
+ key: reference.key,
36
+ lessonId,
37
+ definitionId: candidate.id,
38
+ source: reference.source
39
+ })
40
+ );
41
+ continue;
42
+ }
43
+ resolved.push({ ...reference, definitionId: definitionId2 });
44
+ continue;
45
+ }
46
+ const definitionId = resolveReferenceId(
47
+ reference.key,
48
+ candidate.scope,
49
+ availableDefinitionIds
50
+ );
51
+ if (definitionId === void 0) {
52
+ diagnostics.push(
53
+ diagnostic({
54
+ code: "undefined-reference",
55
+ message: `${candidate.id} references unknown notation ${reference.key}`,
56
+ key: reference.key,
57
+ definitionId: candidate.id,
58
+ source: reference.source
59
+ })
60
+ );
61
+ continue;
62
+ }
63
+ resolved.push({ ...reference, definitionId });
64
+ }
65
+ candidate.resolvedReferences = sortResolvedReferences(resolved);
66
+ validateSeeAlsoReferences(candidate, availableDefinitionIds, diagnostics);
67
+ }
68
+ const directReferences = /* @__PURE__ */ new Map();
69
+ for (const lesson of lessons.values()) {
70
+ const resolved = [];
71
+ const scope = {
72
+ kind: "page",
73
+ lessonId: lesson.lessonId
74
+ };
75
+ for (const reference of sortedReferences(lesson.references)) {
76
+ if (!validatePageReferenceKey(reference, lesson, diagnostics)) continue;
77
+ const definitionId = resolveReferenceId(
78
+ reference.key,
79
+ scope,
80
+ availableDefinitionIds
81
+ );
82
+ if (definitionId === void 0) {
83
+ diagnostics.push(
84
+ diagnostic({
85
+ code: "undefined-reference",
86
+ message: `${lesson.lessonId} references unknown notation ${reference.key}`,
87
+ key: reference.key,
88
+ lessonId: lesson.lessonId,
89
+ source: reference.source
90
+ })
91
+ );
92
+ continue;
93
+ }
94
+ resolved.push({ ...reference, definitionId });
95
+ }
96
+ directReferences.set(lesson.lessonId, sortResolvedReferences(resolved));
97
+ }
98
+ const edgeIndex = /* @__PURE__ */ new Map();
99
+ for (const candidate of definitions.values()) {
100
+ edgeIndex.set(
101
+ candidate.id,
102
+ uniqueSorted(
103
+ candidate.resolvedReferences.map((reference) => reference.definitionId).filter((definitionId) => definitionId !== candidate.id)
104
+ )
105
+ );
106
+ }
107
+ detectCycles(definitions, edgeIndex, diagnostics);
108
+ const bundles = [];
109
+ const backlinks = [];
110
+ const reachableDefinitions = /* @__PURE__ */ new Set();
111
+ for (const lesson of lessons.values()) {
112
+ const references = directReferences.get(lesson.lessonId) ?? [];
113
+ const directIds = new Set(
114
+ references.map((reference) => reference.definitionId)
115
+ );
116
+ const definitionIds = transitiveClosure(directIds, edgeIndex);
117
+ for (const id of definitionIds) reachableDefinitions.add(id);
118
+ const bindingByKey = /* @__PURE__ */ new Map();
119
+ for (const reference of references) {
120
+ bindingByKey.set(reference.key, reference.definitionId);
121
+ }
122
+ bundles.push({
123
+ lessonId: lesson.lessonId,
124
+ status: lesson.status,
125
+ source: lesson.source,
126
+ bindings: [...bindingByKey].map(([key, definitionId]) => ({ key, definitionId })).sort(
127
+ (left, right) => left.key.localeCompare(right.key) || left.definitionId.localeCompare(right.definitionId)
128
+ ),
129
+ definitionIds
130
+ });
131
+ for (const definitionId of definitionIds) {
132
+ const definition = definitions.get(definitionId);
133
+ if (definition === void 0) continue;
134
+ backlinks.push({
135
+ definitionId,
136
+ key: definition.input.key,
137
+ lessonId: lesson.lessonId,
138
+ direct: directIds.has(definitionId)
139
+ });
140
+ }
141
+ }
142
+ for (const candidate of definitions.values()) {
143
+ if (!reachableDefinitions.has(candidate.id)) {
144
+ diagnostics.push({
145
+ code: "unused-definition",
146
+ severity: "warning",
147
+ message: `${candidate.id} is not reachable from any lesson reference`,
148
+ key: candidate.input.key,
149
+ lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
150
+ definitionId: candidate.id,
151
+ source: candidate.input.source
152
+ });
153
+ }
154
+ }
155
+ return {
156
+ definitions: [...definitions.values()].map(toDefinitionRecord).sort((left, right) => left.id.localeCompare(right.id)),
157
+ bundles: bundles.sort(
158
+ (left, right) => left.lessonId.localeCompare(right.lessonId)
159
+ ),
160
+ backlinks: backlinks.sort(
161
+ (left, right) => left.definitionId.localeCompare(right.definitionId) || left.lessonId.localeCompare(right.lessonId) || Number(right.direct) - Number(left.direct)
162
+ ),
163
+ diagnostics: diagnostics.sort(compareDiagnostics)
164
+ };
165
+ }
166
+ function assertValidNotation(registry) {
167
+ const errors = registry.diagnostics.filter(
168
+ (item) => item.severity === "error"
169
+ );
170
+ if (errors.length === 0) return;
171
+ throw new Error(
172
+ `Invalid notation registry:
173
+ ${errors.map((item) => `- [${item.code}] ${item.message}`).join("\n")}`
174
+ );
175
+ }
176
+ function buildLessonIndex(rawLessons, diagnostics) {
177
+ const lessons = /* @__PURE__ */ new Map();
178
+ const sorted = [...rawLessons].sort(
179
+ (left, right) => left.lessonId.localeCompare(right.lessonId) || compareSourceSpans(left.source, right.source)
180
+ );
181
+ for (const rawLesson of sorted) {
182
+ const lesson = normalizeLesson(rawLesson);
183
+ if (!isNotationKey(lesson.lessonId)) {
184
+ diagnostics.push(
185
+ diagnostic({
186
+ code: "invalid-key",
187
+ message: `lesson has invalid id ${JSON.stringify(lesson.lessonId)}`,
188
+ key: lesson.lessonId,
189
+ lessonId: lesson.lessonId,
190
+ source: lesson.source
191
+ })
192
+ );
193
+ continue;
194
+ }
195
+ const previous = lessons.get(lesson.lessonId);
196
+ if (previous !== void 0) {
197
+ diagnostics.push(
198
+ diagnostic({
199
+ code: "duplicate-page",
200
+ message: `duplicate notation lesson ${lesson.lessonId}`,
201
+ lessonId: lesson.lessonId,
202
+ source: lesson.source,
203
+ relatedSources: sortedSources([previous.source, lesson.source])
204
+ })
205
+ );
206
+ continue;
207
+ }
208
+ lessons.set(lesson.lessonId, lesson);
209
+ }
210
+ return lessons;
211
+ }
212
+ function buildDefinitionIndex(rawShared, lessons, diagnostics) {
213
+ const candidates = [
214
+ ...rawShared.map((raw) => {
215
+ const input = normalizeSharedDefinition(raw);
216
+ return {
217
+ kind: "shared",
218
+ id: sharedDefinitionId(input.key),
219
+ scope: { kind: "shared" },
220
+ input,
221
+ resolvedReferences: []
222
+ };
223
+ }),
224
+ ...[...lessons.values()].flatMap(
225
+ (lesson) => lesson.localDefinitions.map((input) => ({
226
+ kind: "local",
227
+ id: localDefinitionId(lesson.lessonId, input.key),
228
+ scope: { kind: "page", lessonId: lesson.lessonId },
229
+ lesson,
230
+ input,
231
+ resolvedReferences: []
232
+ }))
233
+ )
234
+ ].sort(
235
+ (left, right) => left.id.localeCompare(right.id) || compareSourceSpans(left.input.source, right.input.source)
236
+ );
237
+ const definitions = /* @__PURE__ */ new Map();
238
+ for (const candidate of candidates) {
239
+ if (!isNotationKey(candidate.input.key)) {
240
+ diagnostics.push(
241
+ diagnostic({
242
+ code: "invalid-key",
243
+ message: `${candidate.id} has invalid notation key ${JSON.stringify(candidate.input.key)}`,
244
+ key: candidate.input.key,
245
+ lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
246
+ definitionId: candidate.id,
247
+ source: candidate.input.source
248
+ })
249
+ );
250
+ continue;
251
+ }
252
+ const previous = definitions.get(candidate.id);
253
+ if (previous === void 0) {
254
+ definitions.set(candidate.id, candidate);
255
+ continue;
256
+ }
257
+ const duplicate = definitionFingerprint(previous) === definitionFingerprint(candidate);
258
+ diagnostics.push(
259
+ diagnostic({
260
+ code: duplicate ? "duplicate-definition" : "conflicting-definition",
261
+ message: duplicate ? `${candidate.id} is declared more than once` : `${candidate.id} has conflicting declarations`,
262
+ key: candidate.input.key,
263
+ lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
264
+ definitionId: candidate.id,
265
+ source: candidate.input.source,
266
+ relatedSources: sortedSources([
267
+ previous.input.source,
268
+ candidate.input.source
269
+ ])
270
+ })
271
+ );
272
+ }
273
+ return definitions;
274
+ }
275
+ function normalizeLesson(lesson) {
276
+ return {
277
+ ...lesson,
278
+ localDefinitions: lesson.localDefinitions.map(normalizeLocalDefinition).sort(
279
+ (left, right) => left.key.localeCompare(right.key) || compareSourceSpans(left.source, right.source)
280
+ ),
281
+ references: sortedReferences(lesson.references)
282
+ };
283
+ }
284
+ function normalizeSharedDefinition(input) {
285
+ return {
286
+ ...input,
287
+ aliases: [...input.aliases].sort(),
288
+ sources: [...input.sources].sort(),
289
+ seeAlso: [...input.seeAlso].sort(),
290
+ references: sortedReferences(input.references)
291
+ };
292
+ }
293
+ function normalizeLocalDefinition(input) {
294
+ return {
295
+ ...input,
296
+ sources: [...input.sources].sort(),
297
+ seeAlso: [...input.seeAlso].sort(),
298
+ references: sortedReferences(input.references)
299
+ };
300
+ }
301
+ function validateReferenceKey(reference, owner, diagnostics) {
302
+ if (isNotationKey(reference.key)) return true;
303
+ diagnostics.push(
304
+ diagnostic({
305
+ code: "invalid-key",
306
+ message: `${owner.id} references invalid notation key ${JSON.stringify(reference.key)}`,
307
+ key: reference.key,
308
+ lessonId: owner.kind === "local" ? owner.lesson.lessonId : void 0,
309
+ definitionId: owner.id,
310
+ source: reference.source
311
+ })
312
+ );
313
+ return false;
314
+ }
315
+ function validatePageReferenceKey(reference, lesson, diagnostics) {
316
+ if (isNotationKey(reference.key)) return true;
317
+ diagnostics.push(
318
+ diagnostic({
319
+ code: "invalid-key",
320
+ message: `${lesson.lessonId} references invalid notation key ${JSON.stringify(reference.key)}`,
321
+ key: reference.key,
322
+ lessonId: lesson.lessonId,
323
+ source: reference.source
324
+ })
325
+ );
326
+ return false;
327
+ }
328
+ function validateSeeAlsoReferences(candidate, availableDefinitionIds, diagnostics) {
329
+ for (const key of uniqueSorted(candidate.input.seeAlso)) {
330
+ const reference = {
331
+ key,
332
+ kind: "definition",
333
+ source: candidate.input.source
334
+ };
335
+ if (!validateReferenceKey(reference, candidate, diagnostics)) continue;
336
+ const definitionId = resolveReferenceId(
337
+ key,
338
+ candidate.scope,
339
+ availableDefinitionIds
340
+ );
341
+ if (definitionId === void 0) {
342
+ diagnostics.push(
343
+ diagnostic({
344
+ code: "undefined-reference",
345
+ message: `${candidate.id} has unknown seeAlso target ${key}`,
346
+ key,
347
+ lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
348
+ definitionId: candidate.id,
349
+ source: candidate.input.source
350
+ })
351
+ );
352
+ }
353
+ }
354
+ }
355
+ function toDefinitionRecord(candidate) {
356
+ if (candidate.kind === "shared") {
357
+ const input2 = candidate.input;
358
+ const record2 = {
359
+ kind: "shared",
360
+ id: candidate.id,
361
+ key: input2.key,
362
+ scope: candidate.scope,
363
+ notation: input2.notation,
364
+ label: input2.label,
365
+ summary: input2.summary,
366
+ aliases: input2.aliases,
367
+ domain: input2.domain,
368
+ sources: input2.sources,
369
+ seeAlso: input2.seeAlso,
370
+ alignment: input2.alignment,
371
+ status: input2.status,
372
+ aiAssisted: input2.aiAssisted,
373
+ body: input2.body,
374
+ source: input2.source,
375
+ resolvedReferences: candidate.resolvedReferences,
376
+ ...input2.formula === void 0 ? {} : { formula: input2.formula },
377
+ ...input2.units === void 0 ? {} : { units: input2.units }
378
+ };
379
+ return record2;
380
+ }
381
+ const input = candidate.input;
382
+ const record = {
383
+ kind: "local",
384
+ id: candidate.id,
385
+ key: input.key,
386
+ scope: candidate.scope,
387
+ lessonId: candidate.lesson.lessonId,
388
+ notation: input.notation,
389
+ label: input.label,
390
+ summary: input.summary,
391
+ sources: input.sources,
392
+ seeAlso: input.seeAlso,
393
+ alignment: input.alignment,
394
+ status: candidate.lesson.status,
395
+ source: input.source,
396
+ resolvedReferences: candidate.resolvedReferences,
397
+ ...input.formula === void 0 ? {} : { formula: input.formula },
398
+ ...input.units === void 0 ? {} : { units: input.units }
399
+ };
400
+ return record;
401
+ }
402
+ function definitionFingerprint(candidate) {
403
+ const references = sortedReferences(candidate.input.references).map(
404
+ (reference) => ({
405
+ key: reference.key,
406
+ kind: reference.kind
407
+ })
408
+ );
409
+ const inputAlignment = candidate.input.alignment;
410
+ const alignment = inputAlignment.kind === "general" ? {
411
+ kind: inputAlignment.kind,
412
+ rationale: normalizeText(inputAlignment.rationale)
413
+ } : {
414
+ kind: inputAlignment.kind,
415
+ introducedByCompetency: inputAlignment.introducedByCompetency,
416
+ introducedInLesson: inputAlignment.introducedInLesson
417
+ };
418
+ if (candidate.kind === "shared") {
419
+ const input2 = candidate.input;
420
+ return JSON.stringify({
421
+ kind: candidate.kind,
422
+ key: input2.key,
423
+ notation: input2.notation,
424
+ label: normalizeText(input2.label),
425
+ summary: normalizeText(input2.summary),
426
+ aliases: [...input2.aliases].sort(),
427
+ domain: input2.domain,
428
+ units: input2.units,
429
+ sources: [...input2.sources].sort(),
430
+ seeAlso: [...input2.seeAlso].sort(),
431
+ alignment,
432
+ status: input2.status,
433
+ aiAssisted: input2.aiAssisted,
434
+ body: normalizeText(input2.body),
435
+ references
436
+ });
437
+ }
438
+ const input = candidate.input;
439
+ return JSON.stringify({
440
+ kind: candidate.kind,
441
+ key: input.key,
442
+ notation: input.notation,
443
+ label: normalizeText(input.label),
444
+ summary: normalizeText(input.summary),
445
+ formula: input.formula,
446
+ units: input.units,
447
+ sources: [...input.sources].sort(),
448
+ seeAlso: [...input.seeAlso].sort(),
449
+ alignment,
450
+ references
451
+ });
452
+ }
453
+ function normalizeText(value) {
454
+ return value.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trimEnd()).join("\n").trim();
455
+ }
456
+ function detectCycles(definitions, edgeIndex, diagnostics) {
457
+ const states = /* @__PURE__ */ new Map();
458
+ const stack = [];
459
+ const reported = /* @__PURE__ */ new Set();
460
+ const visit = (definitionId) => {
461
+ states.set(definitionId, "visiting");
462
+ stack.push(definitionId);
463
+ for (const dependencyId of edgeIndex.get(definitionId) ?? []) {
464
+ const state = states.get(dependencyId);
465
+ if (state === void 0) {
466
+ visit(dependencyId);
467
+ } else if (state === "visiting") {
468
+ const start = stack.lastIndexOf(dependencyId);
469
+ const path = canonicalCycle([...stack.slice(start), dependencyId]);
470
+ const key = path.join(">");
471
+ if (!reported.has(key)) {
472
+ reported.add(key);
473
+ const first = definitions.get(path[0] ?? "");
474
+ diagnostics.push({
475
+ code: "reference-cycle",
476
+ severity: "error",
477
+ message: `notation reference cycle: ${path.join(" -> ")}`,
478
+ key: first?.input.key,
479
+ lessonId: first?.kind === "local" ? first.lesson.lessonId : void 0,
480
+ definitionId: path[0],
481
+ source: first?.input.source,
482
+ path
483
+ });
484
+ }
485
+ }
486
+ }
487
+ stack.pop();
488
+ states.set(definitionId, "visited");
489
+ };
490
+ for (const id of [...definitions.keys()].sort()) {
491
+ if (states.get(id) === void 0) visit(id);
492
+ }
493
+ }
494
+ function canonicalCycle(path) {
495
+ const nodes = path.slice(0, -1);
496
+ if (nodes.length === 0) return path;
497
+ let best = [...nodes];
498
+ let bestKey = best.join("\0");
499
+ for (let index = 1; index < nodes.length; index += 1) {
500
+ const rotation = [...nodes.slice(index), ...nodes.slice(0, index)];
501
+ const key = rotation.join("\0");
502
+ if (key < bestKey) {
503
+ best = rotation;
504
+ bestKey = key;
505
+ }
506
+ }
507
+ return [...best, best[0]];
508
+ }
509
+ function transitiveClosure(roots, edgeIndex) {
510
+ const reached = /* @__PURE__ */ new Set();
511
+ const pending = [...roots].sort().reverse();
512
+ while (pending.length > 0) {
513
+ const id = pending.pop();
514
+ if (id === void 0 || reached.has(id)) continue;
515
+ reached.add(id);
516
+ const dependencies = edgeIndex.get(id) ?? [];
517
+ for (let index = dependencies.length - 1; index >= 0; index -= 1) {
518
+ const dependency = dependencies[index];
519
+ if (dependency !== void 0 && !reached.has(dependency)) {
520
+ pending.push(dependency);
521
+ }
522
+ }
523
+ }
524
+ return [...reached].sort();
525
+ }
526
+ function sortedReferences(references) {
527
+ return [...references].sort(
528
+ (left, right) => compareSourceSpans(left.source, right.source) || left.key.localeCompare(right.key) || left.kind.localeCompare(right.kind)
529
+ );
530
+ }
531
+ function sortResolvedReferences(references) {
532
+ return [...references].sort(
533
+ (left, right) => compareSourceSpans(left.source, right.source) || left.key.localeCompare(right.key) || left.kind.localeCompare(right.kind) || left.definitionId.localeCompare(right.definitionId)
534
+ );
535
+ }
536
+ function uniqueSorted(values) {
537
+ return [...new Set(values)].sort();
538
+ }
539
+ function sortedSources(values) {
540
+ return [...values].sort(compareSourceSpans);
541
+ }
542
+ function diagnostic(input) {
543
+ return { ...input, severity: "error" };
544
+ }
545
+ export {
546
+ assertValidNotation,
547
+ buildNotationRegistry
548
+ };
@@ -0,0 +1,55 @@
1
+ function textContent(node) {
2
+ if (!node || typeof node !== 'object') return '';
3
+ if (node.type === 'text' && typeof node.value === 'string') return node.value;
4
+ if (!Array.isArray(node.children)) return '';
5
+ return node.children.map(textContent).join('');
6
+ }
7
+
8
+ function katexErrors(node, output = []) {
9
+ if (!node || typeof node !== 'object') return output;
10
+
11
+ const classes = node.properties?.className;
12
+ if (
13
+ node.type === 'element' &&
14
+ Array.isArray(classes) &&
15
+ classes.includes('katex-error')
16
+ ) {
17
+ output.push({
18
+ source: textContent(node).trim(),
19
+ detail:
20
+ typeof node.properties?.title === 'string'
21
+ ? node.properties.title
22
+ : 'Unknown KaTeX parse error',
23
+ position: node.position,
24
+ });
25
+ }
26
+
27
+ if (Array.isArray(node.children)) {
28
+ for (const child of node.children) katexErrors(child, output);
29
+ }
30
+
31
+ return output;
32
+ }
33
+
34
+ /**
35
+ * rehype-katex reports parse failures as non-fatal VFile messages and renders
36
+ * a `.katex-error` fallback. Turn those recovered failures into compiler
37
+ * errors so Astro cannot successfully build a lesson containing broken math.
38
+ */
39
+ export default function rehypeFailKatexErrors() {
40
+ return function failOnKatexErrors(tree, file) {
41
+ const errors = katexErrors(tree);
42
+ if (errors.length === 0) return;
43
+
44
+ const first = errors[0];
45
+ const source = first.source ? ` for "${first.source}"` : '';
46
+ const additional =
47
+ errors.length > 1 ? ` (${errors.length - 1} additional error(s))` : '';
48
+ const message = `KaTeX rendering failed${source}: ${first.detail}${additional}`;
49
+
50
+ if (typeof file?.fail === 'function') {
51
+ file.fail(message, first.position, 'rehype-katex:parse-error');
52
+ }
53
+ throw new Error(message);
54
+ };
55
+ }