explico 0.2.0 → 0.4.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/CHANGELOG.md +173 -0
- package/README.md +67 -7
- package/dist/compiler/astro.d.ts +30 -0
- package/dist/compiler/astro.js +79 -0
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +43 -9
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +80 -4
- package/dist/compiler/course-config.js +12 -0
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/lesson-derivation.d.ts +1 -1
- package/dist/compiler/lesson-derivation.js +2 -2
- package/dist/compiler/manifest.d.ts +98 -2
- package/dist/compiler/manifest.js +153 -24
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +51 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +10 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +80 -13
- package/dist/reference/equations.mjs +109 -19
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/keyed-float-validate.d.ts +73 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +64 -0
- package/dist/reference/keyed-jsx-floats.mjs +191 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/remark-notation.d.mts +61 -0
- package/dist/reference/remark-notation.mjs +344 -10
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +100 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +169 -0
- package/docs/authoring/notation.md +297 -0
- package/docs/authoring/structure.md +139 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/astro.ts +127 -0
- package/src/compiler/cli.ts +52 -7
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +93 -3
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/lesson-derivation.ts +2 -2
- package/src/compiler/manifest.ts +320 -26
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/notation/NotationLayer.astro +11 -0
- package/src/components/notation/render-notation-math.ts +13 -2
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSidebar.astro +62 -1
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +15 -2
- package/src/components/table/Table.astro +105 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +83 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +8 -1
- package/src/reference/equations.mjs +109 -19
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +210 -0
- package/src/reference/keyed-jsx-floats.mjs +191 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +344 -10
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +244 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
readdirSync,
|
|
7
7
|
writeFileSync
|
|
8
8
|
} from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
import { dirname, join, relative } from "node:path";
|
|
10
11
|
import { parseFrontmatter } from "@astrojs/markdown-remark";
|
|
11
12
|
import {
|
|
@@ -15,9 +16,17 @@ import {
|
|
|
15
16
|
runConsistencyChecks
|
|
16
17
|
} from "../reference/consistency.js";
|
|
17
18
|
import { validateNotationAlignment } from "../reference/curriculum-alignment.js";
|
|
19
|
+
import { countDisplayEquations } from "../reference/equations.mjs";
|
|
20
|
+
import { countKeyedJsxFloats } from "../reference/keyed-jsx-floats.mjs";
|
|
18
21
|
import {
|
|
19
22
|
validateEquations
|
|
20
23
|
} from "../reference/equations-validate.js";
|
|
24
|
+
import {
|
|
25
|
+
validateTables
|
|
26
|
+
} from "../reference/tables-validate.js";
|
|
27
|
+
import {
|
|
28
|
+
validateFigures
|
|
29
|
+
} from "../reference/figures-validate.js";
|
|
21
30
|
import {
|
|
22
31
|
gateContentMath
|
|
23
32
|
} from "../reference/gate-math.js";
|
|
@@ -25,6 +34,10 @@ import { loadLintIgnore } from "../reference/lint-ignore.js";
|
|
|
25
34
|
import { buildNotationRegistry } from "../reference/registry.js";
|
|
26
35
|
import { formulaGlyphScope } from "../reference/gloss.js";
|
|
27
36
|
import { resolveReferenceId } from "../reference/references.js";
|
|
37
|
+
import { coursePartsOf } from "./course-config.js";
|
|
38
|
+
import {
|
|
39
|
+
resolveKnowledgeVersion
|
|
40
|
+
} from "./knowledge-version.js";
|
|
28
41
|
import {
|
|
29
42
|
loadCurriculumCatalog,
|
|
30
43
|
loadNotationDefinitions,
|
|
@@ -35,12 +48,22 @@ import {
|
|
|
35
48
|
import {
|
|
36
49
|
buildSidebar,
|
|
37
50
|
deriveSources,
|
|
38
|
-
isLessonSlug
|
|
51
|
+
isLessonSlug,
|
|
52
|
+
lessonIdFromSlug
|
|
39
53
|
} from "./lesson-derivation.js";
|
|
40
|
-
const MANIFEST_SCHEMA_VERSION =
|
|
54
|
+
const MANIFEST_SCHEMA_VERSION = 7;
|
|
41
55
|
const REPOSITORY_ROOT = process.cwd();
|
|
42
56
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
43
57
|
const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
|
|
58
|
+
let enginePackage;
|
|
59
|
+
function readEnginePackage() {
|
|
60
|
+
if (!enginePackage) {
|
|
61
|
+
enginePackage = createRequire(import.meta.url)(
|
|
62
|
+
"explico/package.json"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return enginePackage;
|
|
66
|
+
}
|
|
44
67
|
const CITE_PATTERN = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
|
|
45
68
|
function lessonCitations(body) {
|
|
46
69
|
const withoutCode = body.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
@@ -156,6 +179,11 @@ function lessonFrontmatterById() {
|
|
|
156
179
|
return result;
|
|
157
180
|
}
|
|
158
181
|
async function compileManifest(courseConfig) {
|
|
182
|
+
if (courseConfig.about === void 0 || courseConfig.about.version === void 0 || courseConfig.about.authors === void 0 || courseConfig.about.disclaimer === void 0) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"course.config.ts is missing a complete `about` block (required since explico 0.3.0): `about: { version, authors: [{ name }], disclaimer, materialName?, summary?, changelogHref? }`. See docs/authoring/migrating.md (\xA7 0.2.x \u2192 0.3.0) in the installed package."
|
|
185
|
+
);
|
|
186
|
+
}
|
|
159
187
|
const [catalog, notationInput] = await Promise.all([
|
|
160
188
|
loadCurriculumCatalog(),
|
|
161
189
|
loadNotationRegistryInput()
|
|
@@ -202,20 +230,71 @@ async function compileManifest(courseConfig) {
|
|
|
202
230
|
catalog.lessons.map((lesson) => [lesson.id, lesson])
|
|
203
231
|
);
|
|
204
232
|
const frontmatterById = lessonFrontmatterById();
|
|
205
|
-
const
|
|
206
|
-
|
|
233
|
+
const lessonSlugs = [...notationInput.lessons].map((lesson) => lesson.lessonId.replace(/\./g, "/")).sort();
|
|
234
|
+
const sidebar = buildSidebar(
|
|
235
|
+
catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
|
|
236
|
+
lessonSlugs,
|
|
237
|
+
coursePartsOf(courseConfig)
|
|
238
|
+
);
|
|
239
|
+
const bodyByLessonId = new Map(
|
|
240
|
+
notationInput.lessons.map((lesson) => [lesson.lessonId, lesson.body])
|
|
241
|
+
);
|
|
242
|
+
const placementByLessonId = /* @__PURE__ */ new Map();
|
|
243
|
+
let partCursor = 0;
|
|
244
|
+
for (const group of sidebar) {
|
|
245
|
+
const lessonIds = group.items.filter((item) => typeof item === "string").map(lessonIdFromSlug);
|
|
246
|
+
if (lessonIds.length === 0) continue;
|
|
247
|
+
partCursor += 1;
|
|
248
|
+
const used = { equations: 0, tables: 0, figures: 0 };
|
|
249
|
+
let chapterCursor = 0;
|
|
250
|
+
for (const lessonId of lessonIds) {
|
|
251
|
+
const body = bodyByLessonId.get(lessonId) ?? "";
|
|
252
|
+
placementByLessonId.set(lessonId, {
|
|
253
|
+
part: partCursor,
|
|
254
|
+
chapter: ++chapterCursor,
|
|
255
|
+
numbering: { ...used }
|
|
256
|
+
});
|
|
257
|
+
used.equations += countDisplayEquations(body);
|
|
258
|
+
used.tables += countKeyedJsxFloats(body, { tagName: "Table" });
|
|
259
|
+
used.figures += countKeyedJsxFloats(body, { tagName: "Figure" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const floatInputs = (kind) => notationInput.lessons.map((lesson) => {
|
|
263
|
+
const placement = placementByLessonId.get(lesson.lessonId);
|
|
264
|
+
return {
|
|
207
265
|
lessonId: lesson.lessonId,
|
|
208
266
|
slug: lesson.lessonId.replace(/\./g, "/"),
|
|
209
267
|
file: lesson.source.file,
|
|
210
|
-
body: lesson.body
|
|
211
|
-
|
|
212
|
-
|
|
268
|
+
body: lesson.body,
|
|
269
|
+
...placement ? {
|
|
270
|
+
numbering: {
|
|
271
|
+
prefix: placement.part,
|
|
272
|
+
offset: placement.numbering[kind]
|
|
273
|
+
}
|
|
274
|
+
} : {}
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
const equationValidation = validateEquations(floatInputs("equations"));
|
|
213
278
|
const equationsBySlug = new Map(
|
|
214
279
|
Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
|
|
215
280
|
slug,
|
|
216
281
|
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
217
282
|
])
|
|
218
283
|
);
|
|
284
|
+
const tableValidation = validateTables(floatInputs("tables"));
|
|
285
|
+
const tablesBySlug = new Map(
|
|
286
|
+
Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
|
|
287
|
+
slug,
|
|
288
|
+
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
289
|
+
])
|
|
290
|
+
);
|
|
291
|
+
const figureValidation = validateFigures(floatInputs("figures"));
|
|
292
|
+
const figuresBySlug = new Map(
|
|
293
|
+
Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
|
|
294
|
+
slug,
|
|
295
|
+
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
296
|
+
])
|
|
297
|
+
);
|
|
219
298
|
const lessons = [];
|
|
220
299
|
for (const notationLesson of notationInput.lessons) {
|
|
221
300
|
const id = notationLesson.lessonId;
|
|
@@ -229,6 +308,16 @@ async function compileManifest(courseConfig) {
|
|
|
229
308
|
lessons.push({
|
|
230
309
|
id,
|
|
231
310
|
slug: id.replace(/\./g, "/"),
|
|
311
|
+
// 0 = the lesson is not placed in any configured part; a curriculum
|
|
312
|
+
// diagnostic covers that separately, and both numbering paths then fall
|
|
313
|
+
// back to the legacy `##`-section scheme.
|
|
314
|
+
part: placementByLessonId.get(id)?.part ?? 0,
|
|
315
|
+
chapter: placementByLessonId.get(id)?.chapter ?? 0,
|
|
316
|
+
numbering: placementByLessonId.get(id)?.numbering ?? {
|
|
317
|
+
equations: 0,
|
|
318
|
+
tables: 0,
|
|
319
|
+
figures: 0
|
|
320
|
+
},
|
|
232
321
|
file: notationLesson.source.file,
|
|
233
322
|
title: frontmatter.title,
|
|
234
323
|
description: frontmatter.description,
|
|
@@ -240,6 +329,8 @@ async function compileManifest(courseConfig) {
|
|
|
240
329
|
sources: deriveSources(notationLesson.body),
|
|
241
330
|
citations: lessonCitations(notationLesson.body),
|
|
242
331
|
equations: equationsBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
332
|
+
tables: tablesBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
333
|
+
figures: figuresBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
243
334
|
prereqEdges: sortEdges(
|
|
244
335
|
teaches.flatMap(
|
|
245
336
|
(to) => (prerequisitesById.get(to) ?? []).filter((from) => !teaches.includes(from)).map((from) => ({ from, to }))
|
|
@@ -264,7 +355,8 @@ async function compileManifest(courseConfig) {
|
|
|
264
355
|
specs: loadNotationSpecs(),
|
|
265
356
|
catalog,
|
|
266
357
|
lintIgnore: loadLintIgnore(),
|
|
267
|
-
conventions: courseConfig.conventions
|
|
358
|
+
conventions: courseConfig.conventions,
|
|
359
|
+
...courseConfig.operators === void 0 ? {} : { operators: courseConfig.operators }
|
|
268
360
|
});
|
|
269
361
|
const rawNotation = [...loadNotationDefinitions()].sort(
|
|
270
362
|
(a, b) => String(a.key).localeCompare(String(b.key))
|
|
@@ -272,14 +364,40 @@ async function compileManifest(courseConfig) {
|
|
|
272
364
|
const competencies = catalog.competencies.map((entry) => ({ ...entry })).sort(byId);
|
|
273
365
|
const sources = [...loadSourceRecords()].sort(byId);
|
|
274
366
|
const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
courseConfig.
|
|
280
|
-
|
|
367
|
+
const hashes = contentHashes();
|
|
368
|
+
const knowledgeChangelogPath = join(CONTENT_ROOT, "CHANGELOG.md");
|
|
369
|
+
const knowledge = resolveKnowledgeVersion({
|
|
370
|
+
changelogText: existsSync(knowledgeChangelogPath) ? readFileSync(knowledgeChangelogPath, "utf8") : void 0,
|
|
371
|
+
declaredVersion: courseConfig.about.version,
|
|
372
|
+
file: relative(REPOSITORY_ROOT, knowledgeChangelogPath).replace(/\\/g, "/")
|
|
373
|
+
});
|
|
374
|
+
const enginePkg = readEnginePackage();
|
|
375
|
+
const about = {
|
|
376
|
+
engine: {
|
|
377
|
+
name: enginePkg.name,
|
|
378
|
+
version: enginePkg.version,
|
|
379
|
+
...enginePkg.homepage === void 0 ? {} : { homepage: enginePkg.homepage }
|
|
380
|
+
},
|
|
381
|
+
material: {
|
|
382
|
+
name: courseConfig.about.materialName ?? courseConfig.title,
|
|
383
|
+
summary: courseConfig.about.summary ?? courseConfig.description
|
|
384
|
+
},
|
|
385
|
+
knowledge: {
|
|
386
|
+
version: knowledge.version,
|
|
387
|
+
...knowledge.latest?.date === void 0 ? {} : { date: knowledge.latest.date },
|
|
388
|
+
notes: knowledge.latest?.notes ?? [],
|
|
389
|
+
contentTree: hashes.contentTree
|
|
390
|
+
},
|
|
391
|
+
authors: courseConfig.about.authors.map((author) => ({
|
|
392
|
+
name: author.name,
|
|
393
|
+
...author.contact === void 0 ? {} : { contact: author.contact }
|
|
394
|
+
})),
|
|
395
|
+
disclaimer: courseConfig.about.disclaimer,
|
|
396
|
+
...courseConfig.about.changelogHref === void 0 ? {} : { changelogHref: courseConfig.about.changelogHref }
|
|
397
|
+
};
|
|
281
398
|
return {
|
|
282
399
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
400
|
+
about,
|
|
283
401
|
notation: {
|
|
284
402
|
definitions: registry.definitions,
|
|
285
403
|
bundles: registry.bundles,
|
|
@@ -297,15 +415,26 @@ async function compileManifest(courseConfig) {
|
|
|
297
415
|
labels: equationValidation.labels,
|
|
298
416
|
numbersBySlug: equationValidation.numbersBySlug
|
|
299
417
|
},
|
|
418
|
+
tables: {
|
|
419
|
+
labels: tableValidation.labels,
|
|
420
|
+
numbersBySlug: tableValidation.numbersBySlug
|
|
421
|
+
},
|
|
422
|
+
figures: {
|
|
423
|
+
labels: figureValidation.labels,
|
|
424
|
+
numbersBySlug: figureValidation.numbersBySlug
|
|
425
|
+
},
|
|
300
426
|
diagnostics: {
|
|
301
427
|
curriculum: curriculumIssues,
|
|
302
428
|
notation: registry.diagnostics,
|
|
303
429
|
alignment: alignmentIssues,
|
|
304
430
|
math: mathIssues,
|
|
305
431
|
consistency: consistencyIssues,
|
|
306
|
-
equations: equationValidation.diagnostics
|
|
432
|
+
equations: equationValidation.diagnostics,
|
|
433
|
+
tables: tableValidation.diagnostics,
|
|
434
|
+
figures: figureValidation.diagnostics,
|
|
435
|
+
knowledge: knowledge.diagnostics
|
|
307
436
|
},
|
|
308
|
-
hashes
|
|
437
|
+
hashes
|
|
309
438
|
};
|
|
310
439
|
}
|
|
311
440
|
function byId(a, b) {
|
|
@@ -340,15 +469,15 @@ function readManifestSync() {
|
|
|
340
469
|
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
341
470
|
}
|
|
342
471
|
async function loadManifest(courseConfig) {
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
472
|
+
if (courseConfig) {
|
|
473
|
+
const manifest = await compileManifest(courseConfig);
|
|
474
|
+
writeManifest(manifest);
|
|
475
|
+
return manifest;
|
|
348
476
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
477
|
+
if (existsSync(MANIFEST_PATH)) return readManifestSync();
|
|
478
|
+
throw new Error(
|
|
479
|
+
`${relative(REPOSITORY_ROOT, MANIFEST_PATH)} not found. Run \`pnpm validate:content\` first.`
|
|
480
|
+
);
|
|
352
481
|
}
|
|
353
482
|
export {
|
|
354
483
|
MANIFEST_PATH,
|
|
@@ -6,8 +6,12 @@ import './collections.js';
|
|
|
6
6
|
import '../reference/types.js';
|
|
7
7
|
import '../reference/lint-ignore.js';
|
|
8
8
|
import '../reference/equations-validate.js';
|
|
9
|
+
import '../reference/tables-validate.js';
|
|
10
|
+
import '../reference/keyed-float-validate.js';
|
|
11
|
+
import '../reference/figures-validate.js';
|
|
9
12
|
import '../reference/gate-math.js';
|
|
10
13
|
import '../reference/gloss.js';
|
|
14
|
+
import './knowledge-version.js';
|
|
11
15
|
import './lesson-derivation.js';
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operators a course treats as common, given its `course.config.ts`
|
|
3
|
+
* `operators` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
4
|
+
*
|
|
5
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} operators
|
|
6
|
+
* @returns {Set<string>}
|
|
7
|
+
*/
|
|
8
|
+
export function effectiveOperatorAllowlist(operators: {
|
|
9
|
+
allow?: readonly string[];
|
|
10
|
+
deny?: readonly string[];
|
|
11
|
+
} | undefined): Set<string>;
|
|
12
|
+
/** True when `name` needs no notation entry for this course. */
|
|
13
|
+
export function isBaseOperator(name: any, operators: any): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The base operator library: the named mathematical operators every reader is
|
|
16
|
+
* assumed to know, so a lesson never has to introduce them with a notation
|
|
17
|
+
* entry.
|
|
18
|
+
*
|
|
19
|
+
* Rationale (parallel to `base-library.mjs`): the completeness gate has no
|
|
20
|
+
* opinion about operators — KaTeX parses `\exp`, `\int`, `\sin`, … as operator
|
|
21
|
+
* nodes, and `\operatorname{Var}` as an `operatorname` node, none of which the
|
|
22
|
+
* glyph resolver treats as a bindable symbol. The `operator-explained`
|
|
23
|
+
* consistency check (rule N8) fills that gap: an operator a lesson uses is
|
|
24
|
+
* either on this list (or the course's additions to it) or it carries a
|
|
25
|
+
* notation entry the reader can hover. This set is the "everyone knows these"
|
|
26
|
+
* baseline — trig, the exponential and logarithms, limits, extrema, and the
|
|
27
|
+
* big operators — kept greppable and small like the base library.
|
|
28
|
+
*
|
|
29
|
+
* A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
|
|
30
|
+
* written `\operatorname{…}` and is NOT here: it earns a card, a
|
|
31
|
+
* `notation.local` entry, or an explicit line in `course.config.ts`
|
|
32
|
+
* (`operators.allow`).
|
|
33
|
+
*
|
|
34
|
+
* Entries are the KaTeX command without its backslash (`exp`, not `\exp`) for
|
|
35
|
+
* `op` nodes, and the verbatim body for `\operatorname{…}` nodes (`Var`).
|
|
36
|
+
* Matching is case-sensitive.
|
|
37
|
+
*/
|
|
38
|
+
export const BASE_OPERATORS: Readonly<Set<string>>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base operator library: the named mathematical operators every reader is
|
|
3
|
+
* assumed to know, so a lesson never has to introduce them with a notation
|
|
4
|
+
* entry.
|
|
5
|
+
*
|
|
6
|
+
* Rationale (parallel to `base-library.mjs`): the completeness gate has no
|
|
7
|
+
* opinion about operators — KaTeX parses `\exp`, `\int`, `\sin`, … as operator
|
|
8
|
+
* nodes, and `\operatorname{Var}` as an `operatorname` node, none of which the
|
|
9
|
+
* glyph resolver treats as a bindable symbol. The `operator-explained`
|
|
10
|
+
* consistency check (rule N8) fills that gap: an operator a lesson uses is
|
|
11
|
+
* either on this list (or the course's additions to it) or it carries a
|
|
12
|
+
* notation entry the reader can hover. This set is the "everyone knows these"
|
|
13
|
+
* baseline — trig, the exponential and logarithms, limits, extrema, and the
|
|
14
|
+
* big operators — kept greppable and small like the base library.
|
|
15
|
+
*
|
|
16
|
+
* A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
|
|
17
|
+
* written `\operatorname{…}` and is NOT here: it earns a card, a
|
|
18
|
+
* `notation.local` entry, or an explicit line in `course.config.ts`
|
|
19
|
+
* (`operators.allow`).
|
|
20
|
+
*
|
|
21
|
+
* Entries are the KaTeX command without its backslash (`exp`, not `\exp`) for
|
|
22
|
+
* `op` nodes, and the verbatim body for `\operatorname{…}` nodes (`Var`).
|
|
23
|
+
* Matching is case-sensitive.
|
|
24
|
+
*/
|
|
25
|
+
export const BASE_OPERATORS = Object.freeze(
|
|
26
|
+
new Set([
|
|
27
|
+
// exponential / logarithms
|
|
28
|
+
'exp',
|
|
29
|
+
'log',
|
|
30
|
+
'ln',
|
|
31
|
+
'lg',
|
|
32
|
+
// trigonometric
|
|
33
|
+
'sin',
|
|
34
|
+
'cos',
|
|
35
|
+
'tan',
|
|
36
|
+
'cot',
|
|
37
|
+
'sec',
|
|
38
|
+
'csc',
|
|
39
|
+
// inverse trigonometric
|
|
40
|
+
'arcsin',
|
|
41
|
+
'arccos',
|
|
42
|
+
'arctan',
|
|
43
|
+
// hyperbolic
|
|
44
|
+
'sinh',
|
|
45
|
+
'cosh',
|
|
46
|
+
'tanh',
|
|
47
|
+
'coth',
|
|
48
|
+
// limits
|
|
49
|
+
'lim',
|
|
50
|
+
'limsup',
|
|
51
|
+
'liminf',
|
|
52
|
+
'injlim',
|
|
53
|
+
'projlim',
|
|
54
|
+
'varlimsup',
|
|
55
|
+
'varliminf',
|
|
56
|
+
'varinjlim',
|
|
57
|
+
'varprojlim',
|
|
58
|
+
// extrema
|
|
59
|
+
'max',
|
|
60
|
+
'min',
|
|
61
|
+
'sup',
|
|
62
|
+
'inf',
|
|
63
|
+
// linear algebra / number theory / misc
|
|
64
|
+
'arg',
|
|
65
|
+
'deg',
|
|
66
|
+
'det',
|
|
67
|
+
'dim',
|
|
68
|
+
'gcd',
|
|
69
|
+
'hom',
|
|
70
|
+
'ker',
|
|
71
|
+
'Pr',
|
|
72
|
+
'mod',
|
|
73
|
+
// big operators
|
|
74
|
+
'sum',
|
|
75
|
+
'prod',
|
|
76
|
+
'coprod',
|
|
77
|
+
'int',
|
|
78
|
+
'iint',
|
|
79
|
+
'iiint',
|
|
80
|
+
'oint',
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The operators a course treats as common, given its `course.config.ts`
|
|
86
|
+
* `operators` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} operators
|
|
89
|
+
* @returns {Set<string>}
|
|
90
|
+
*/
|
|
91
|
+
export function effectiveOperatorAllowlist(operators) {
|
|
92
|
+
const allow = operators?.allow ?? [];
|
|
93
|
+
const deny = new Set(operators?.deny ?? []);
|
|
94
|
+
const effective = new Set(BASE_OPERATORS);
|
|
95
|
+
for (const name of allow) effective.add(name);
|
|
96
|
+
for (const name of deny) effective.delete(name);
|
|
97
|
+
return effective;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True when `name` needs no notation entry for this course. */
|
|
101
|
+
export function isBaseOperator(name, operators) {
|
|
102
|
+
return effectiveOperatorAllowlist(operators).has(name);
|
|
103
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CourseConventions } from '../compiler/course-config.js';
|
|
1
|
+
import { CourseConventions, CourseOperators } from '../compiler/course-config.js';
|
|
2
2
|
import { CurriculumCatalog } from '../curriculum/validation.js';
|
|
3
3
|
import { NotationSpec } from '../compiler/collections.js';
|
|
4
4
|
import { LintIgnore } from './lint-ignore.js';
|
|
@@ -25,12 +25,15 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
|
|
|
25
25
|
* (f) weak-local a `notation.local` symbol used only in its
|
|
26
26
|
* own defining equation and prose
|
|
27
27
|
* (g) notation-source-locator every notation `sources` entry has a locator
|
|
28
|
+
* (h) operator-explained a named operator a lesson uses is on the
|
|
29
|
+
* shipped/course operator allowlist or
|
|
30
|
+
* carries a notation entry (rule N8)
|
|
28
31
|
*
|
|
29
32
|
* Check (d) — the committed per-lesson resolution report — lives in
|
|
30
33
|
* `src/content/resolution-report.ts`.
|
|
31
34
|
*/
|
|
32
35
|
|
|
33
|
-
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape';
|
|
36
|
+
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained';
|
|
34
37
|
interface ConsistencyDiagnostic {
|
|
35
38
|
readonly code: ConsistencyCode;
|
|
36
39
|
/** Tier 3 — every finding here is a warning until content is brought clean. */
|
|
@@ -51,6 +54,8 @@ interface ConsistencyInput {
|
|
|
51
54
|
readonly lintIgnore: LintIgnore;
|
|
52
55
|
/** Convention keys this course anchors on (from `content/course.config.ts`). */
|
|
53
56
|
readonly conventions: CourseConventions;
|
|
57
|
+
/** This course's edits to the named-operator baseline (rule N8). */
|
|
58
|
+
readonly operators?: CourseOperators;
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
56
61
|
* Run every consistency check. The result is sorted deterministically (code,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effectiveOperatorAllowlist } from "./base-operators.mjs";
|
|
2
|
+
import { collectOperators } from "./math-glyphs.mjs";
|
|
1
3
|
import { offVocabularyTokens } from "./units-vocab.js";
|
|
2
4
|
const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
|
|
3
5
|
function stripCode(text) {
|
|
@@ -285,6 +287,51 @@ function checkGlossNameShape(input, out) {
|
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
}
|
|
290
|
+
function checkOperatorExplained(input, out) {
|
|
291
|
+
const allowlist = effectiveOperatorAllowlist(input.operators);
|
|
292
|
+
const byId = new Map(
|
|
293
|
+
input.registry.definitions.map((definition) => [definition.id, definition])
|
|
294
|
+
);
|
|
295
|
+
const bindingsByLesson = new Map(
|
|
296
|
+
input.registry.bundles.map((bundle) => [bundle.lessonId, bundle.bindings])
|
|
297
|
+
);
|
|
298
|
+
for (const lesson of input.notationInput.lessons) {
|
|
299
|
+
const explained = /* @__PURE__ */ new Set();
|
|
300
|
+
const noteEntryOperators = (latex) => {
|
|
301
|
+
for (const operator of collectOperators(latex)) {
|
|
302
|
+
explained.add(operator.name);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
for (const definition of lesson.localDefinitions) {
|
|
306
|
+
noteEntryOperators(definition.notation);
|
|
307
|
+
}
|
|
308
|
+
for (const binding of bindingsByLesson.get(lesson.lessonId) ?? []) {
|
|
309
|
+
const definition = byId.get(binding.definitionId);
|
|
310
|
+
if (definition) noteEntryOperators(definition.notation);
|
|
311
|
+
}
|
|
312
|
+
const reported = /* @__PURE__ */ new Set();
|
|
313
|
+
for (const span of mathSpans(lesson.body)) {
|
|
314
|
+
for (const operator of collectOperators(span.latex)) {
|
|
315
|
+
if (allowlist.has(operator.name)) continue;
|
|
316
|
+
if (explained.has(operator.name)) continue;
|
|
317
|
+
if (reported.has(operator.name)) continue;
|
|
318
|
+
reported.add(operator.name);
|
|
319
|
+
if (input.lintIgnore.matches("operator-explained", operator.name)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const display = operator.source === "operatorname" ? `\\operatorname{${operator.name}}` : `\\${operator.name}`;
|
|
323
|
+
out.push({
|
|
324
|
+
code: "operator-explained",
|
|
325
|
+
severity: "warning",
|
|
326
|
+
lessonId: lesson.lessonId,
|
|
327
|
+
file: lesson.source.file,
|
|
328
|
+
glyph: operator.name,
|
|
329
|
+
message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to operators.allow in course.config.ts`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
288
335
|
const CHECKS = [
|
|
289
336
|
checkGlyphUniqueness,
|
|
290
337
|
checkNotationUnits,
|
|
@@ -294,7 +341,8 @@ const CHECKS = [
|
|
|
294
341
|
checkNotationSourceLocator,
|
|
295
342
|
checkGlossWantsPromoting,
|
|
296
343
|
checkCardWantsDemoting,
|
|
297
|
-
checkGlossNameShape
|
|
344
|
+
checkGlossNameShape,
|
|
345
|
+
checkOperatorExplained
|
|
298
346
|
];
|
|
299
347
|
function runConsistencyChecks(input) {
|
|
300
348
|
const diagnostics = [];
|
|
@@ -314,7 +362,8 @@ function consistencyCounts(diagnostics) {
|
|
|
314
362
|
"notation-source-locator": 0,
|
|
315
363
|
"gloss-wants-promoting": 0,
|
|
316
364
|
"card-wants-demoting": 0,
|
|
317
|
-
"gloss-name-shape": 0
|
|
365
|
+
"gloss-name-shape": 0,
|
|
366
|
+
"operator-explained": 0
|
|
318
367
|
};
|
|
319
368
|
for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
|
|
320
369
|
return counts;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface DiagramNodeInput {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
}
|
|
6
|
+
interface DiagramEdgeInput {
|
|
7
|
+
readonly from: string;
|
|
8
|
+
readonly to: string;
|
|
9
|
+
}
|
|
10
|
+
interface DiagramLayoutOptions {
|
|
11
|
+
/** Top-to-bottom (default) or left-to-right. */
|
|
12
|
+
readonly direction?: 'TB' | 'LR';
|
|
13
|
+
readonly nodeSeparation?: number;
|
|
14
|
+
readonly rankSeparation?: number;
|
|
15
|
+
/** Uniform padding added around the computed bounding box. */
|
|
16
|
+
readonly padding?: number;
|
|
17
|
+
}
|
|
18
|
+
interface Point {
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
}
|
|
22
|
+
interface LaidOutNode {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Top-left corner, not the center dagre itself works in. */
|
|
25
|
+
readonly x: number;
|
|
26
|
+
readonly y: number;
|
|
27
|
+
readonly width: number;
|
|
28
|
+
readonly height: number;
|
|
29
|
+
}
|
|
30
|
+
interface LaidOutEdge {
|
|
31
|
+
readonly from: string;
|
|
32
|
+
readonly to: string;
|
|
33
|
+
readonly points: readonly Point[];
|
|
34
|
+
}
|
|
35
|
+
interface DiagramLayout {
|
|
36
|
+
readonly nodes: readonly LaidOutNode[];
|
|
37
|
+
readonly edges: readonly LaidOutEdge[];
|
|
38
|
+
/** The SVG canvas size a caller should use as the `viewBox`. */
|
|
39
|
+
readonly width: number;
|
|
40
|
+
readonly height: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lay out a small directed graph. Node ids must be unique; an edge naming an
|
|
44
|
+
* undeclared node fails loudly rather than silently dropping it (dagre itself
|
|
45
|
+
* would otherwise create a phantom zero-size node).
|
|
46
|
+
*/
|
|
47
|
+
declare function layoutDiagram(nodes: readonly DiagramNodeInput[], edges: readonly DiagramEdgeInput[], options?: DiagramLayoutOptions): DiagramLayout;
|
|
48
|
+
interface BoxSizeEstimate {
|
|
49
|
+
readonly width: number;
|
|
50
|
+
readonly height: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Estimate a diagram node's box from its label text (or raw, pre-resolution
|
|
54
|
+
* LaTeX for a formula node). Deliberately generous: it is meant to avoid
|
|
55
|
+
* clipping, not to match a real render pixel-for-pixel. Pass an explicit
|
|
56
|
+
* `width`/`height` on the node to override for a label this estimate gets
|
|
57
|
+
* badly wrong.
|
|
58
|
+
*/
|
|
59
|
+
declare function estimateBoxSize(content: string, kind: 'text' | 'formula', options?: {
|
|
60
|
+
readonly maxWidth?: number;
|
|
61
|
+
}): BoxSizeEstimate;
|
|
62
|
+
|
|
63
|
+
export { type BoxSizeEstimate, type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, type LaidOutEdge, type LaidOutNode, type Point, estimateBoxSize, layoutDiagram };
|