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,723 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareDiagnostics,
|
|
3
|
+
compareSourceSpans,
|
|
4
|
+
isNotationKey,
|
|
5
|
+
localDefinitionId,
|
|
6
|
+
resolveReferenceId,
|
|
7
|
+
sharedDefinitionId,
|
|
8
|
+
} from './references';
|
|
9
|
+
import type {
|
|
10
|
+
LocalNotationDefinitionInput,
|
|
11
|
+
LocalNotationDefinitionRecord,
|
|
12
|
+
NotationBacklink,
|
|
13
|
+
NotationDefinitionRecord,
|
|
14
|
+
NotationDefinitionScope,
|
|
15
|
+
NotationDiagnostic,
|
|
16
|
+
NotationLessonInput,
|
|
17
|
+
NotationPageBundle,
|
|
18
|
+
NotationReferenceInput,
|
|
19
|
+
NotationRegistry,
|
|
20
|
+
NotationRegistryInput,
|
|
21
|
+
ResolvedNotationReference,
|
|
22
|
+
SharedNotationDefinitionInput,
|
|
23
|
+
SharedNotationDefinitionRecord,
|
|
24
|
+
SourceSpan,
|
|
25
|
+
} from './types';
|
|
26
|
+
|
|
27
|
+
interface SharedCandidate {
|
|
28
|
+
readonly kind: 'shared';
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly scope: { readonly kind: 'shared' };
|
|
31
|
+
readonly input: SharedNotationDefinitionInput;
|
|
32
|
+
resolvedReferences: readonly ResolvedNotationReference[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface LocalCandidate {
|
|
36
|
+
readonly kind: 'local';
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly scope: {
|
|
39
|
+
readonly kind: 'page';
|
|
40
|
+
readonly lessonId: string;
|
|
41
|
+
};
|
|
42
|
+
readonly lesson: NotationLessonInput;
|
|
43
|
+
readonly input: LocalNotationDefinitionInput;
|
|
44
|
+
resolvedReferences: readonly ResolvedNotationReference[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type DefinitionCandidate = SharedCandidate | LocalCandidate;
|
|
48
|
+
|
|
49
|
+
export function buildNotationRegistry(
|
|
50
|
+
rawInput: NotationRegistryInput,
|
|
51
|
+
): NotationRegistry {
|
|
52
|
+
const diagnostics: NotationDiagnostic[] = [];
|
|
53
|
+
const lessons = buildLessonIndex(rawInput.lessons, diagnostics);
|
|
54
|
+
const definitions = buildDefinitionIndex(
|
|
55
|
+
rawInput.sharedDefinitions,
|
|
56
|
+
lessons,
|
|
57
|
+
diagnostics,
|
|
58
|
+
);
|
|
59
|
+
const availableDefinitionIds = new Set(definitions.keys());
|
|
60
|
+
|
|
61
|
+
// `notation.uses` is retired (D1 decision): a lesson pulls a shared key into
|
|
62
|
+
// scope by referencing it (`[[key]]`/`\explain`), and every reference resolves
|
|
63
|
+
// directly against page-local then shared definitions.
|
|
64
|
+
|
|
65
|
+
for (const candidate of definitions.values()) {
|
|
66
|
+
const references = sortedReferences(candidate.input.references);
|
|
67
|
+
const resolved: ResolvedNotationReference[] = [];
|
|
68
|
+
for (const reference of references) {
|
|
69
|
+
if (!validateReferenceKey(reference, candidate, diagnostics)) continue;
|
|
70
|
+
|
|
71
|
+
if (candidate.kind === 'local') {
|
|
72
|
+
const lessonId = candidate.lesson.lessonId;
|
|
73
|
+
const definitionId = resolveReferenceId(
|
|
74
|
+
reference.key,
|
|
75
|
+
candidate.scope,
|
|
76
|
+
availableDefinitionIds,
|
|
77
|
+
);
|
|
78
|
+
if (definitionId === undefined) {
|
|
79
|
+
diagnostics.push(
|
|
80
|
+
diagnostic({
|
|
81
|
+
code: 'undefined-reference',
|
|
82
|
+
message: `${candidate.id} references unknown notation ${reference.key}`,
|
|
83
|
+
key: reference.key,
|
|
84
|
+
lessonId,
|
|
85
|
+
definitionId: candidate.id,
|
|
86
|
+
source: reference.source,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resolved.push({ ...reference, definitionId });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const definitionId = resolveReferenceId(
|
|
97
|
+
reference.key,
|
|
98
|
+
candidate.scope,
|
|
99
|
+
availableDefinitionIds,
|
|
100
|
+
);
|
|
101
|
+
if (definitionId === undefined) {
|
|
102
|
+
diagnostics.push(
|
|
103
|
+
diagnostic({
|
|
104
|
+
code: 'undefined-reference',
|
|
105
|
+
message: `${candidate.id} references unknown notation ${reference.key}`,
|
|
106
|
+
key: reference.key,
|
|
107
|
+
definitionId: candidate.id,
|
|
108
|
+
source: reference.source,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
resolved.push({ ...reference, definitionId });
|
|
114
|
+
}
|
|
115
|
+
candidate.resolvedReferences = sortResolvedReferences(resolved);
|
|
116
|
+
validateSeeAlsoReferences(candidate, availableDefinitionIds, diagnostics);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const directReferences = new Map<string, ResolvedNotationReference[]>();
|
|
120
|
+
for (const lesson of lessons.values()) {
|
|
121
|
+
const resolved: ResolvedNotationReference[] = [];
|
|
122
|
+
const scope: NotationDefinitionScope = {
|
|
123
|
+
kind: 'page',
|
|
124
|
+
lessonId: lesson.lessonId,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const reference of sortedReferences(lesson.references)) {
|
|
128
|
+
if (!validatePageReferenceKey(reference, lesson, diagnostics)) continue;
|
|
129
|
+
const definitionId = resolveReferenceId(
|
|
130
|
+
reference.key,
|
|
131
|
+
scope,
|
|
132
|
+
availableDefinitionIds,
|
|
133
|
+
);
|
|
134
|
+
if (definitionId === undefined) {
|
|
135
|
+
diagnostics.push(
|
|
136
|
+
diagnostic({
|
|
137
|
+
code: 'undefined-reference',
|
|
138
|
+
message: `${lesson.lessonId} references unknown notation ${reference.key}`,
|
|
139
|
+
key: reference.key,
|
|
140
|
+
lessonId: lesson.lessonId,
|
|
141
|
+
source: reference.source,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resolved.push({ ...reference, definitionId });
|
|
148
|
+
}
|
|
149
|
+
directReferences.set(lesson.lessonId, sortResolvedReferences(resolved));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const edgeIndex = new Map<string, readonly string[]>();
|
|
153
|
+
for (const candidate of definitions.values()) {
|
|
154
|
+
edgeIndex.set(
|
|
155
|
+
candidate.id,
|
|
156
|
+
uniqueSorted(
|
|
157
|
+
candidate.resolvedReferences
|
|
158
|
+
// A definition may name its own symbol in its worked math
|
|
159
|
+
// (`\explain{key}{…}` in the entry's own body); that self-reference
|
|
160
|
+
// is not a dependency cycle.
|
|
161
|
+
.map((reference) => reference.definitionId)
|
|
162
|
+
.filter((definitionId) => definitionId !== candidate.id),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
detectCycles(definitions, edgeIndex, diagnostics);
|
|
167
|
+
|
|
168
|
+
const bundles: NotationPageBundle[] = [];
|
|
169
|
+
const backlinks: NotationBacklink[] = [];
|
|
170
|
+
const reachableDefinitions = new Set<string>();
|
|
171
|
+
|
|
172
|
+
for (const lesson of lessons.values()) {
|
|
173
|
+
const references = directReferences.get(lesson.lessonId) ?? [];
|
|
174
|
+
const directIds = new Set(
|
|
175
|
+
references.map((reference) => reference.definitionId),
|
|
176
|
+
);
|
|
177
|
+
const definitionIds = transitiveClosure(directIds, edgeIndex);
|
|
178
|
+
for (const id of definitionIds) reachableDefinitions.add(id);
|
|
179
|
+
|
|
180
|
+
const bindingByKey = new Map<string, string>();
|
|
181
|
+
for (const reference of references) {
|
|
182
|
+
bindingByKey.set(reference.key, reference.definitionId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
bundles.push({
|
|
186
|
+
lessonId: lesson.lessonId,
|
|
187
|
+
status: lesson.status,
|
|
188
|
+
source: lesson.source,
|
|
189
|
+
bindings: [...bindingByKey]
|
|
190
|
+
.map(([key, definitionId]) => ({ key, definitionId }))
|
|
191
|
+
.sort(
|
|
192
|
+
(left, right) =>
|
|
193
|
+
left.key.localeCompare(right.key) ||
|
|
194
|
+
left.definitionId.localeCompare(right.definitionId),
|
|
195
|
+
),
|
|
196
|
+
definitionIds,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
for (const definitionId of definitionIds) {
|
|
200
|
+
const definition = definitions.get(definitionId);
|
|
201
|
+
if (definition === undefined) continue;
|
|
202
|
+
backlinks.push({
|
|
203
|
+
definitionId,
|
|
204
|
+
key: definition.input.key,
|
|
205
|
+
lessonId: lesson.lessonId,
|
|
206
|
+
direct: directIds.has(definitionId),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const candidate of definitions.values()) {
|
|
212
|
+
if (!reachableDefinitions.has(candidate.id)) {
|
|
213
|
+
diagnostics.push({
|
|
214
|
+
code: 'unused-definition',
|
|
215
|
+
severity: 'warning',
|
|
216
|
+
message: `${candidate.id} is not reachable from any lesson reference`,
|
|
217
|
+
key: candidate.input.key,
|
|
218
|
+
lessonId:
|
|
219
|
+
candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
|
|
220
|
+
definitionId: candidate.id,
|
|
221
|
+
source: candidate.input.source,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
definitions: [...definitions.values()]
|
|
228
|
+
.map(toDefinitionRecord)
|
|
229
|
+
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
230
|
+
bundles: bundles.sort((left, right) =>
|
|
231
|
+
left.lessonId.localeCompare(right.lessonId),
|
|
232
|
+
),
|
|
233
|
+
backlinks: backlinks.sort(
|
|
234
|
+
(left, right) =>
|
|
235
|
+
left.definitionId.localeCompare(right.definitionId) ||
|
|
236
|
+
left.lessonId.localeCompare(right.lessonId) ||
|
|
237
|
+
Number(right.direct) - Number(left.direct),
|
|
238
|
+
),
|
|
239
|
+
diagnostics: diagnostics.sort(compareDiagnostics),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function assertValidNotation(registry: NotationRegistry): void {
|
|
244
|
+
const errors = registry.diagnostics.filter(
|
|
245
|
+
(item) => item.severity === 'error',
|
|
246
|
+
);
|
|
247
|
+
if (errors.length === 0) return;
|
|
248
|
+
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Invalid notation registry:\n${errors
|
|
251
|
+
.map((item) => `- [${item.code}] ${item.message}`)
|
|
252
|
+
.join('\n')}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildLessonIndex(
|
|
257
|
+
rawLessons: readonly NotationLessonInput[],
|
|
258
|
+
diagnostics: NotationDiagnostic[],
|
|
259
|
+
): Map<string, NotationLessonInput> {
|
|
260
|
+
const lessons = new Map<string, NotationLessonInput>();
|
|
261
|
+
const sorted = [...rawLessons].sort(
|
|
262
|
+
(left, right) =>
|
|
263
|
+
left.lessonId.localeCompare(right.lessonId) ||
|
|
264
|
+
compareSourceSpans(left.source, right.source),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
for (const rawLesson of sorted) {
|
|
268
|
+
const lesson = normalizeLesson(rawLesson);
|
|
269
|
+
if (!isNotationKey(lesson.lessonId)) {
|
|
270
|
+
diagnostics.push(
|
|
271
|
+
diagnostic({
|
|
272
|
+
code: 'invalid-key',
|
|
273
|
+
message: `lesson has invalid id ${JSON.stringify(lesson.lessonId)}`,
|
|
274
|
+
key: lesson.lessonId,
|
|
275
|
+
lessonId: lesson.lessonId,
|
|
276
|
+
source: lesson.source,
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const previous = lessons.get(lesson.lessonId);
|
|
282
|
+
if (previous !== undefined) {
|
|
283
|
+
diagnostics.push(
|
|
284
|
+
diagnostic({
|
|
285
|
+
code: 'duplicate-page',
|
|
286
|
+
message: `duplicate notation lesson ${lesson.lessonId}`,
|
|
287
|
+
lessonId: lesson.lessonId,
|
|
288
|
+
source: lesson.source,
|
|
289
|
+
relatedSources: sortedSources([previous.source, lesson.source]),
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
lessons.set(lesson.lessonId, lesson);
|
|
295
|
+
}
|
|
296
|
+
return lessons;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildDefinitionIndex(
|
|
300
|
+
rawShared: readonly SharedNotationDefinitionInput[],
|
|
301
|
+
lessons: ReadonlyMap<string, NotationLessonInput>,
|
|
302
|
+
diagnostics: NotationDiagnostic[],
|
|
303
|
+
): Map<string, DefinitionCandidate> {
|
|
304
|
+
const candidates: DefinitionCandidate[] = [
|
|
305
|
+
...rawShared.map((raw): SharedCandidate => {
|
|
306
|
+
const input = normalizeSharedDefinition(raw);
|
|
307
|
+
return {
|
|
308
|
+
kind: 'shared',
|
|
309
|
+
id: sharedDefinitionId(input.key),
|
|
310
|
+
scope: { kind: 'shared' },
|
|
311
|
+
input,
|
|
312
|
+
resolvedReferences: [],
|
|
313
|
+
};
|
|
314
|
+
}),
|
|
315
|
+
...[...lessons.values()].flatMap((lesson) =>
|
|
316
|
+
lesson.localDefinitions.map((input): LocalCandidate => ({
|
|
317
|
+
kind: 'local',
|
|
318
|
+
id: localDefinitionId(lesson.lessonId, input.key),
|
|
319
|
+
scope: { kind: 'page', lessonId: lesson.lessonId },
|
|
320
|
+
lesson,
|
|
321
|
+
input,
|
|
322
|
+
resolvedReferences: [],
|
|
323
|
+
})),
|
|
324
|
+
),
|
|
325
|
+
].sort(
|
|
326
|
+
(left, right) =>
|
|
327
|
+
left.id.localeCompare(right.id) ||
|
|
328
|
+
compareSourceSpans(left.input.source, right.input.source),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const definitions = new Map<string, DefinitionCandidate>();
|
|
332
|
+
for (const candidate of candidates) {
|
|
333
|
+
if (!isNotationKey(candidate.input.key)) {
|
|
334
|
+
diagnostics.push(
|
|
335
|
+
diagnostic({
|
|
336
|
+
code: 'invalid-key',
|
|
337
|
+
message: `${candidate.id} has invalid notation key ${JSON.stringify(candidate.input.key)}`,
|
|
338
|
+
key: candidate.input.key,
|
|
339
|
+
lessonId:
|
|
340
|
+
candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
|
|
341
|
+
definitionId: candidate.id,
|
|
342
|
+
source: candidate.input.source,
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const previous = definitions.get(candidate.id);
|
|
349
|
+
if (previous === undefined) {
|
|
350
|
+
definitions.set(candidate.id, candidate);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const duplicate =
|
|
355
|
+
definitionFingerprint(previous) === definitionFingerprint(candidate);
|
|
356
|
+
diagnostics.push(
|
|
357
|
+
diagnostic({
|
|
358
|
+
code: duplicate ? 'duplicate-definition' : 'conflicting-definition',
|
|
359
|
+
message: duplicate
|
|
360
|
+
? `${candidate.id} is declared more than once`
|
|
361
|
+
: `${candidate.id} has conflicting declarations`,
|
|
362
|
+
key: candidate.input.key,
|
|
363
|
+
lessonId:
|
|
364
|
+
candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
|
|
365
|
+
definitionId: candidate.id,
|
|
366
|
+
source: candidate.input.source,
|
|
367
|
+
relatedSources: sortedSources([
|
|
368
|
+
previous.input.source,
|
|
369
|
+
candidate.input.source,
|
|
370
|
+
]),
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return definitions;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeLesson(lesson: NotationLessonInput): NotationLessonInput {
|
|
378
|
+
return {
|
|
379
|
+
...lesson,
|
|
380
|
+
localDefinitions: lesson.localDefinitions
|
|
381
|
+
.map(normalizeLocalDefinition)
|
|
382
|
+
.sort(
|
|
383
|
+
(left, right) =>
|
|
384
|
+
left.key.localeCompare(right.key) ||
|
|
385
|
+
compareSourceSpans(left.source, right.source),
|
|
386
|
+
),
|
|
387
|
+
references: sortedReferences(lesson.references),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeSharedDefinition(
|
|
392
|
+
input: SharedNotationDefinitionInput,
|
|
393
|
+
): SharedNotationDefinitionInput {
|
|
394
|
+
return {
|
|
395
|
+
...input,
|
|
396
|
+
aliases: [...input.aliases].sort(),
|
|
397
|
+
sources: [...input.sources].sort(),
|
|
398
|
+
seeAlso: [...input.seeAlso].sort(),
|
|
399
|
+
references: sortedReferences(input.references),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function normalizeLocalDefinition(
|
|
404
|
+
input: LocalNotationDefinitionInput,
|
|
405
|
+
): LocalNotationDefinitionInput {
|
|
406
|
+
return {
|
|
407
|
+
...input,
|
|
408
|
+
sources: [...input.sources].sort(),
|
|
409
|
+
seeAlso: [...input.seeAlso].sort(),
|
|
410
|
+
references: sortedReferences(input.references),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateReferenceKey(
|
|
415
|
+
reference: NotationReferenceInput,
|
|
416
|
+
owner: DefinitionCandidate,
|
|
417
|
+
diagnostics: NotationDiagnostic[],
|
|
418
|
+
): boolean {
|
|
419
|
+
if (isNotationKey(reference.key)) return true;
|
|
420
|
+
diagnostics.push(
|
|
421
|
+
diagnostic({
|
|
422
|
+
code: 'invalid-key',
|
|
423
|
+
message: `${owner.id} references invalid notation key ${JSON.stringify(reference.key)}`,
|
|
424
|
+
key: reference.key,
|
|
425
|
+
lessonId: owner.kind === 'local' ? owner.lesson.lessonId : undefined,
|
|
426
|
+
definitionId: owner.id,
|
|
427
|
+
source: reference.source,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function validatePageReferenceKey(
|
|
434
|
+
reference: NotationReferenceInput,
|
|
435
|
+
lesson: NotationLessonInput,
|
|
436
|
+
diagnostics: NotationDiagnostic[],
|
|
437
|
+
): boolean {
|
|
438
|
+
if (isNotationKey(reference.key)) return true;
|
|
439
|
+
diagnostics.push(
|
|
440
|
+
diagnostic({
|
|
441
|
+
code: 'invalid-key',
|
|
442
|
+
message: `${lesson.lessonId} references invalid notation key ${JSON.stringify(reference.key)}`,
|
|
443
|
+
key: reference.key,
|
|
444
|
+
lessonId: lesson.lessonId,
|
|
445
|
+
source: reference.source,
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validateSeeAlsoReferences(
|
|
452
|
+
candidate: DefinitionCandidate,
|
|
453
|
+
availableDefinitionIds: ReadonlySet<string>,
|
|
454
|
+
diagnostics: NotationDiagnostic[],
|
|
455
|
+
): void {
|
|
456
|
+
for (const key of uniqueSorted(candidate.input.seeAlso)) {
|
|
457
|
+
const reference: NotationReferenceInput = {
|
|
458
|
+
key,
|
|
459
|
+
kind: 'definition',
|
|
460
|
+
source: candidate.input.source,
|
|
461
|
+
};
|
|
462
|
+
if (!validateReferenceKey(reference, candidate, diagnostics)) continue;
|
|
463
|
+
|
|
464
|
+
const definitionId = resolveReferenceId(
|
|
465
|
+
key,
|
|
466
|
+
candidate.scope,
|
|
467
|
+
availableDefinitionIds,
|
|
468
|
+
);
|
|
469
|
+
if (definitionId === undefined) {
|
|
470
|
+
diagnostics.push(
|
|
471
|
+
diagnostic({
|
|
472
|
+
code: 'undefined-reference',
|
|
473
|
+
message: `${candidate.id} has unknown seeAlso target ${key}`,
|
|
474
|
+
key,
|
|
475
|
+
lessonId:
|
|
476
|
+
candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
|
|
477
|
+
definitionId: candidate.id,
|
|
478
|
+
source: candidate.input.source,
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function toDefinitionRecord(
|
|
486
|
+
candidate: DefinitionCandidate,
|
|
487
|
+
): NotationDefinitionRecord {
|
|
488
|
+
if (candidate.kind === 'shared') {
|
|
489
|
+
const input = candidate.input;
|
|
490
|
+
const record: SharedNotationDefinitionRecord = {
|
|
491
|
+
kind: 'shared',
|
|
492
|
+
id: candidate.id,
|
|
493
|
+
key: input.key,
|
|
494
|
+
scope: candidate.scope,
|
|
495
|
+
notation: input.notation,
|
|
496
|
+
label: input.label,
|
|
497
|
+
summary: input.summary,
|
|
498
|
+
aliases: input.aliases,
|
|
499
|
+
domain: input.domain,
|
|
500
|
+
sources: input.sources,
|
|
501
|
+
seeAlso: input.seeAlso,
|
|
502
|
+
alignment: input.alignment,
|
|
503
|
+
status: input.status,
|
|
504
|
+
aiAssisted: input.aiAssisted,
|
|
505
|
+
body: input.body,
|
|
506
|
+
source: input.source,
|
|
507
|
+
resolvedReferences: candidate.resolvedReferences,
|
|
508
|
+
...(input.formula === undefined ? {} : { formula: input.formula }),
|
|
509
|
+
...(input.units === undefined ? {} : { units: input.units }),
|
|
510
|
+
};
|
|
511
|
+
return record;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const input = candidate.input;
|
|
515
|
+
const record: LocalNotationDefinitionRecord = {
|
|
516
|
+
kind: 'local',
|
|
517
|
+
id: candidate.id,
|
|
518
|
+
key: input.key,
|
|
519
|
+
scope: candidate.scope,
|
|
520
|
+
lessonId: candidate.lesson.lessonId,
|
|
521
|
+
notation: input.notation,
|
|
522
|
+
label: input.label,
|
|
523
|
+
summary: input.summary,
|
|
524
|
+
sources: input.sources,
|
|
525
|
+
seeAlso: input.seeAlso,
|
|
526
|
+
alignment: input.alignment,
|
|
527
|
+
status: candidate.lesson.status,
|
|
528
|
+
source: input.source,
|
|
529
|
+
resolvedReferences: candidate.resolvedReferences,
|
|
530
|
+
...(input.formula === undefined ? {} : { formula: input.formula }),
|
|
531
|
+
...(input.units === undefined ? {} : { units: input.units }),
|
|
532
|
+
};
|
|
533
|
+
return record;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function definitionFingerprint(candidate: DefinitionCandidate): string {
|
|
537
|
+
const references = sortedReferences(candidate.input.references).map(
|
|
538
|
+
(reference) => ({
|
|
539
|
+
key: reference.key,
|
|
540
|
+
kind: reference.kind,
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
const inputAlignment = candidate.input.alignment;
|
|
544
|
+
const alignment =
|
|
545
|
+
inputAlignment.kind === 'general'
|
|
546
|
+
? {
|
|
547
|
+
kind: inputAlignment.kind,
|
|
548
|
+
rationale: normalizeText(inputAlignment.rationale),
|
|
549
|
+
}
|
|
550
|
+
: {
|
|
551
|
+
kind: inputAlignment.kind,
|
|
552
|
+
introducedByCompetency: inputAlignment.introducedByCompetency,
|
|
553
|
+
introducedInLesson: inputAlignment.introducedInLesson,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
if (candidate.kind === 'shared') {
|
|
557
|
+
const input = candidate.input;
|
|
558
|
+
return JSON.stringify({
|
|
559
|
+
kind: candidate.kind,
|
|
560
|
+
key: input.key,
|
|
561
|
+
notation: input.notation,
|
|
562
|
+
label: normalizeText(input.label),
|
|
563
|
+
summary: normalizeText(input.summary),
|
|
564
|
+
aliases: [...input.aliases].sort(),
|
|
565
|
+
domain: input.domain,
|
|
566
|
+
units: input.units,
|
|
567
|
+
sources: [...input.sources].sort(),
|
|
568
|
+
seeAlso: [...input.seeAlso].sort(),
|
|
569
|
+
alignment,
|
|
570
|
+
status: input.status,
|
|
571
|
+
aiAssisted: input.aiAssisted,
|
|
572
|
+
body: normalizeText(input.body),
|
|
573
|
+
references,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const input = candidate.input;
|
|
578
|
+
return JSON.stringify({
|
|
579
|
+
kind: candidate.kind,
|
|
580
|
+
key: input.key,
|
|
581
|
+
notation: input.notation,
|
|
582
|
+
label: normalizeText(input.label),
|
|
583
|
+
summary: normalizeText(input.summary),
|
|
584
|
+
formula: input.formula,
|
|
585
|
+
units: input.units,
|
|
586
|
+
sources: [...input.sources].sort(),
|
|
587
|
+
seeAlso: [...input.seeAlso].sort(),
|
|
588
|
+
alignment,
|
|
589
|
+
references,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function normalizeText(value: string): string {
|
|
594
|
+
return value
|
|
595
|
+
.replace(/\r\n?/g, '\n')
|
|
596
|
+
.split('\n')
|
|
597
|
+
.map((line) => line.trimEnd())
|
|
598
|
+
.join('\n')
|
|
599
|
+
.trim();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function detectCycles(
|
|
603
|
+
definitions: ReadonlyMap<string, DefinitionCandidate>,
|
|
604
|
+
edgeIndex: ReadonlyMap<string, readonly string[]>,
|
|
605
|
+
diagnostics: NotationDiagnostic[],
|
|
606
|
+
): void {
|
|
607
|
+
const states = new Map<string, 'visiting' | 'visited'>();
|
|
608
|
+
const stack: string[] = [];
|
|
609
|
+
const reported = new Set<string>();
|
|
610
|
+
|
|
611
|
+
const visit = (definitionId: string): void => {
|
|
612
|
+
states.set(definitionId, 'visiting');
|
|
613
|
+
stack.push(definitionId);
|
|
614
|
+
|
|
615
|
+
for (const dependencyId of edgeIndex.get(definitionId) ?? []) {
|
|
616
|
+
const state = states.get(dependencyId);
|
|
617
|
+
if (state === undefined) {
|
|
618
|
+
visit(dependencyId);
|
|
619
|
+
} else if (state === 'visiting') {
|
|
620
|
+
const start = stack.lastIndexOf(dependencyId);
|
|
621
|
+
const path = canonicalCycle([...stack.slice(start), dependencyId]);
|
|
622
|
+
const key = path.join('>');
|
|
623
|
+
if (!reported.has(key)) {
|
|
624
|
+
reported.add(key);
|
|
625
|
+
const first = definitions.get(path[0] ?? '');
|
|
626
|
+
diagnostics.push({
|
|
627
|
+
code: 'reference-cycle',
|
|
628
|
+
severity: 'error',
|
|
629
|
+
message: `notation reference cycle: ${path.join(' -> ')}`,
|
|
630
|
+
key: first?.input.key,
|
|
631
|
+
lessonId:
|
|
632
|
+
first?.kind === 'local' ? first.lesson.lessonId : undefined,
|
|
633
|
+
definitionId: path[0],
|
|
634
|
+
source: first?.input.source,
|
|
635
|
+
path,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
stack.pop();
|
|
642
|
+
states.set(definitionId, 'visited');
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
for (const id of [...definitions.keys()].sort()) {
|
|
646
|
+
if (states.get(id) === undefined) visit(id);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function canonicalCycle(path: readonly string[]): readonly string[] {
|
|
651
|
+
const nodes = path.slice(0, -1);
|
|
652
|
+
if (nodes.length === 0) return path;
|
|
653
|
+
|
|
654
|
+
let best = [...nodes];
|
|
655
|
+
let bestKey = best.join('\u0000');
|
|
656
|
+
for (let index = 1; index < nodes.length; index += 1) {
|
|
657
|
+
const rotation = [...nodes.slice(index), ...nodes.slice(0, index)];
|
|
658
|
+
const key = rotation.join('\u0000');
|
|
659
|
+
if (key < bestKey) {
|
|
660
|
+
best = rotation;
|
|
661
|
+
bestKey = key;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return [...best, best[0] as string];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function transitiveClosure(
|
|
668
|
+
roots: ReadonlySet<string>,
|
|
669
|
+
edgeIndex: ReadonlyMap<string, readonly string[]>,
|
|
670
|
+
): readonly string[] {
|
|
671
|
+
const reached = new Set<string>();
|
|
672
|
+
const pending = [...roots].sort().reverse();
|
|
673
|
+
while (pending.length > 0) {
|
|
674
|
+
const id = pending.pop();
|
|
675
|
+
if (id === undefined || reached.has(id)) continue;
|
|
676
|
+
reached.add(id);
|
|
677
|
+
const dependencies = edgeIndex.get(id) ?? [];
|
|
678
|
+
for (let index = dependencies.length - 1; index >= 0; index -= 1) {
|
|
679
|
+
const dependency = dependencies[index];
|
|
680
|
+
if (dependency !== undefined && !reached.has(dependency)) {
|
|
681
|
+
pending.push(dependency);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return [...reached].sort();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function sortedReferences(
|
|
689
|
+
references: readonly NotationReferenceInput[],
|
|
690
|
+
): NotationReferenceInput[] {
|
|
691
|
+
return [...references].sort(
|
|
692
|
+
(left, right) =>
|
|
693
|
+
compareSourceSpans(left.source, right.source) ||
|
|
694
|
+
left.key.localeCompare(right.key) ||
|
|
695
|
+
left.kind.localeCompare(right.kind),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function sortResolvedReferences(
|
|
700
|
+
references: readonly ResolvedNotationReference[],
|
|
701
|
+
): ResolvedNotationReference[] {
|
|
702
|
+
return [...references].sort(
|
|
703
|
+
(left, right) =>
|
|
704
|
+
compareSourceSpans(left.source, right.source) ||
|
|
705
|
+
left.key.localeCompare(right.key) ||
|
|
706
|
+
left.kind.localeCompare(right.kind) ||
|
|
707
|
+
left.definitionId.localeCompare(right.definitionId),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function uniqueSorted(values: readonly string[]): string[] {
|
|
712
|
+
return [...new Set(values)].sort();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function sortedSources(values: readonly SourceSpan[]): SourceSpan[] {
|
|
716
|
+
return [...values].sort(compareSourceSpans);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function diagnostic(
|
|
720
|
+
input: Omit<NotationDiagnostic, 'severity'>,
|
|
721
|
+
): NotationDiagnostic {
|
|
722
|
+
return { ...input, severity: 'error' };
|
|
723
|
+
}
|