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.
- package/README.md +108 -0
- package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
- package/dist/analytics/AnalyticsEmitter.js +5 -0
- package/dist/compiler/cli.d.ts +189 -0
- package/dist/compiler/cli.js +552 -0
- package/dist/compiler/collections.d.ts +70 -0
- package/dist/compiler/collections.js +383 -0
- package/dist/compiler/course-config.d.ts +31 -0
- package/dist/compiler/course-config.js +0 -0
- package/dist/compiler/lesson-derivation.d.ts +89 -0
- package/dist/compiler/lesson-derivation.js +91 -0
- package/dist/compiler/manifest.d.ts +178 -0
- package/dist/compiler/manifest.js +345 -0
- package/dist/compiler/resolution-report.d.ts +45 -0
- package/dist/compiler/resolution-report.js +97 -0
- package/dist/curriculum/validation.d.ts +76 -0
- package/dist/curriculum/validation.js +320 -0
- package/dist/progress/ProgressRepository.d.ts +53 -0
- package/dist/progress/ProgressRepository.js +12 -0
- package/dist/reference/base-library.mjs +27 -0
- package/dist/reference/citation-format.mjs +133 -0
- package/dist/reference/consistency.d.ts +64 -0
- package/dist/reference/consistency.js +234 -0
- package/dist/reference/curriculum-alignment.d.ts +7 -0
- package/dist/reference/curriculum-alignment.js +166 -0
- package/dist/reference/equations-validate.d.ts +50 -0
- package/dist/reference/equations-validate.js +90 -0
- package/dist/reference/equations.mjs +144 -0
- package/dist/reference/gate-math.d.ts +41 -0
- package/dist/reference/gate-math.js +151 -0
- package/dist/reference/katex-options.mjs +91 -0
- package/dist/reference/lab-math-scope.d.ts +21 -0
- package/dist/reference/lab-math-scope.js +28 -0
- package/dist/reference/label.d.ts +11 -0
- package/dist/reference/label.js +38 -0
- package/dist/reference/lint-ignore.d.ts +32 -0
- package/dist/reference/lint-ignore.js +39 -0
- package/dist/reference/math-glyphs.mjs +830 -0
- package/dist/reference/prose.d.ts +15 -0
- package/dist/reference/prose.js +12 -0
- package/dist/reference/references.d.ts +17 -0
- package/dist/reference/references.js +42 -0
- package/dist/reference/registry.d.ts +6 -0
- package/dist/reference/registry.js +548 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +753 -0
- package/dist/reference/render-lab-math.d.ts +21 -0
- package/dist/reference/render-lab-math.js +58 -0
- package/dist/reference/types.d.ts +151 -0
- package/dist/reference/types.js +0 -0
- package/dist/reference/units-vocab.d.ts +26 -0
- package/dist/reference/units-vocab.js +119 -0
- package/dist/session/PreferenceStore.d.ts +28 -0
- package/dist/session/PreferenceStore.js +30 -0
- package/dist/session/user-context.d.ts +7 -0
- package/dist/session/user-context.js +10 -0
- package/dist/session/user.d.ts +21 -0
- package/dist/session/user.js +8 -0
- package/package.json +53 -0
- package/src/analytics/AnalyticsEmitter.ts +23 -0
- package/src/compiler/cli.ts +866 -0
- package/src/compiler/collections.ts +554 -0
- package/src/compiler/course-config.ts +32 -0
- package/src/compiler/lesson-derivation.ts +186 -0
- package/src/compiler/manifest.ts +601 -0
- package/src/compiler/resolution-report.ts +150 -0
- package/src/components/CurriculumMap.astro +208 -0
- package/src/components/HoverPanel.astro +67 -0
- package/src/components/assessments/AssessmentRunner.tsx +320 -0
- package/src/components/assessments/AssessmentSet.astro +54 -0
- package/src/components/citation/CitationLayer.astro +173 -0
- package/src/components/examples/CompactExample.astro +34 -0
- package/src/components/examples/CompactExamples.astro +253 -0
- package/src/components/hover-panel.ts +273 -0
- package/src/components/notation/NotationGlossary.astro +412 -0
- package/src/components/notation/NotationLayer.astro +594 -0
- package/src/components/notation/NotationMath.astro +24 -0
- package/src/components/notation/SafeMathNode.astro +23 -0
- package/src/components/notation/render-notation-math.ts +223 -0
- package/src/components/starlight/EquationEnhancer.astro +72 -0
- package/src/components/starlight/LayoutHeader.astro +102 -0
- package/src/components/starlight/LayoutPageSidebar.astro +33 -0
- package/src/components/starlight/LayoutSidebar.astro +29 -0
- package/src/components/starlight/LessonFooter.astro +58 -0
- package/src/content-config.ts +259 -0
- package/src/curriculum/validation.ts +481 -0
- package/src/progress/ProgressRepository.ts +64 -0
- package/src/reference/base-library.mjs +27 -0
- package/src/reference/citation-format.mjs +133 -0
- package/src/reference/consistency.ts +375 -0
- package/src/reference/curriculum-alignment.ts +228 -0
- package/src/reference/equations-validate.ts +167 -0
- package/src/reference/equations.mjs +144 -0
- package/src/reference/gate-math.ts +250 -0
- package/src/reference/katex-options.mjs +91 -0
- package/src/reference/lab-math-scope.ts +54 -0
- package/src/reference/label.ts +47 -0
- package/src/reference/lint-ignore.ts +75 -0
- package/src/reference/math-glyphs.mjs +830 -0
- package/src/reference/prose.ts +21 -0
- package/src/reference/references.ts +82 -0
- package/src/reference/registry.ts +723 -0
- package/src/reference/rehype-fail-katex-errors.mjs +55 -0
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +753 -0
- package/src/reference/render-lab-math.ts +99 -0
- package/src/reference/types.ts +182 -0
- package/src/reference/units-vocab.ts +143 -0
- package/src/session/PreferenceStore.ts +54 -0
- package/src/session/user-context.ts +18 -0
- package/src/session/user.ts +24 -0
- 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
|
+
}
|
|
Binary file
|