explico 0.2.0 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +77 -2
- package/dist/compiler/manifest.js +115 -18
- 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 +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +42 -11
- package/dist/reference/equations.mjs +58 -17
- 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 +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -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 +12 -0
- package/dist/reference/remark-notation.mjs +199 -9
- 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 +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +239 -17
- 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/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/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +4 -2
- package/src/components/table/Table.astro +104 -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 +6 -1
- package/src/reference/equations.mjs +58 -17
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +199 -9
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +115 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { CurriculumIssue } from '../curriculum/validation.js';
|
|
2
2
|
import { ConsistencyDiagnostic } from '../reference/consistency.js';
|
|
3
3
|
import { EquationLabelRecord, EquationDiagnostic } from '../reference/equations-validate.js';
|
|
4
|
+
import { TableLabelRecord, TableDiagnostic } from '../reference/tables-validate.js';
|
|
5
|
+
import { FigureLabelRecord, FigureDiagnostic } from '../reference/figures-validate.js';
|
|
4
6
|
import { MathGateDiagnostic } from '../reference/gate-math.js';
|
|
5
7
|
import { GlyphScopeEntry } from '../reference/gloss.js';
|
|
6
8
|
import { NotationDefinitionRecord, NotationPageBundle, NotationBacklink, NotationDiagnostic, EditorialStatus, NotationGlossEntry } from '../reference/types.js';
|
|
7
9
|
import { CourseConfig } from './course-config.js';
|
|
10
|
+
import { KnowledgeVersionDiagnostic } from './knowledge-version.js';
|
|
8
11
|
import { SidebarGroup } from './lesson-derivation.js';
|
|
9
12
|
import './collections.js';
|
|
10
13
|
import '../reference/lint-ignore.js';
|
|
14
|
+
import '../reference/keyed-float-validate.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* The one compiler manifest (Phase D5).
|
|
@@ -45,8 +49,16 @@ import '../reference/lint-ignore.js';
|
|
|
45
49
|
* `diagnostics.consistency` (Tier-3 corpus checks).
|
|
46
50
|
* v3 (D7): `equations` (labelled display equations + cross-page number table),
|
|
47
51
|
* `lessons[].equations`, and `diagnostics.equations`.
|
|
52
|
+
* v4 (F-tables-figures): `tables`/`figures` (same shape as `equations`, for
|
|
53
|
+
* `<Table>`/`<Figure>`), `lessons[].tables`/`lessons[].figures`, and
|
|
54
|
+
* `diagnostics.tables`/`diagnostics.figures`.
|
|
55
|
+
* v5 (about): `about` (engine identity from `explico`'s `package.json`, the
|
|
56
|
+
* course's reader-facing `about` block, and the knowledge version resolved
|
|
57
|
+
* against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
|
|
58
|
+
* v6 (chapter numbering): `lessons[].chapter` — the lesson's 1-based course
|
|
59
|
+
* ordinal, the chapter in every equation/table/figure number (`(2.1)`).
|
|
48
60
|
*/
|
|
49
|
-
declare const MANIFEST_SCHEMA_VERSION =
|
|
61
|
+
declare const MANIFEST_SCHEMA_VERSION = 6;
|
|
50
62
|
declare const MANIFEST_PATH: string;
|
|
51
63
|
interface ManifestCitation {
|
|
52
64
|
readonly id: string;
|
|
@@ -95,6 +107,12 @@ interface ManifestLesson {
|
|
|
95
107
|
readonly id: string;
|
|
96
108
|
/** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
|
|
97
109
|
readonly slug: string;
|
|
110
|
+
/**
|
|
111
|
+
* The lesson's 1-based ordinal in the course (merged track order, then any
|
|
112
|
+
* unranked lessons by slug). It is the chapter in every equation, table, and
|
|
113
|
+
* figure number on the page — `(2.1)` is the first equation of chapter 2.
|
|
114
|
+
*/
|
|
115
|
+
readonly chapter: number;
|
|
98
116
|
readonly file: string;
|
|
99
117
|
readonly title: string;
|
|
100
118
|
readonly description: string;
|
|
@@ -113,6 +131,16 @@ interface ManifestLesson {
|
|
|
113
131
|
readonly key: string;
|
|
114
132
|
readonly number: string;
|
|
115
133
|
}[];
|
|
134
|
+
/** Keyed `<Table>` elements in appearance order. */
|
|
135
|
+
readonly tables: readonly {
|
|
136
|
+
readonly key: string;
|
|
137
|
+
readonly number: string;
|
|
138
|
+
}[];
|
|
139
|
+
/** Keyed `<Figure>` elements in appearance order. */
|
|
140
|
+
readonly figures: readonly {
|
|
141
|
+
readonly key: string;
|
|
142
|
+
readonly number: string;
|
|
143
|
+
}[];
|
|
116
144
|
readonly notation: {
|
|
117
145
|
readonly bindings: readonly {
|
|
118
146
|
readonly key: string;
|
|
@@ -124,8 +152,39 @@ interface ManifestLesson {
|
|
|
124
152
|
readonly resolution: readonly ManifestNotationResolution[];
|
|
125
153
|
};
|
|
126
154
|
}
|
|
155
|
+
/** The reader-facing "about this material" bundle — the `[?]` panel's data. */
|
|
156
|
+
interface ManifestAbout {
|
|
157
|
+
/** `explico`'s own identity, from its `package.json` (the version's golden source). */
|
|
158
|
+
readonly engine: {
|
|
159
|
+
readonly name: string;
|
|
160
|
+
readonly version: string;
|
|
161
|
+
readonly homepage?: string;
|
|
162
|
+
};
|
|
163
|
+
readonly material: {
|
|
164
|
+
readonly name: string;
|
|
165
|
+
readonly summary: string;
|
|
166
|
+
};
|
|
167
|
+
/** The course's knowledge version — semver over the content, engine-independent. */
|
|
168
|
+
readonly knowledge: {
|
|
169
|
+
readonly version: string;
|
|
170
|
+
/** Date of the newest `content/CHANGELOG.md` entry, if it has one. */
|
|
171
|
+
readonly date?: string;
|
|
172
|
+
/** Bullet notes from that entry. */
|
|
173
|
+
readonly notes: readonly string[];
|
|
174
|
+
/** Content fingerprint at compile time — ties the shown version to a build. */
|
|
175
|
+
readonly contentTree: string;
|
|
176
|
+
};
|
|
177
|
+
readonly authors: readonly {
|
|
178
|
+
readonly name: string;
|
|
179
|
+
readonly contact?: string;
|
|
180
|
+
}[];
|
|
181
|
+
readonly disclaimer: string;
|
|
182
|
+
readonly changelogHref?: string;
|
|
183
|
+
}
|
|
127
184
|
interface Manifest {
|
|
128
185
|
readonly schemaVersion: number;
|
|
186
|
+
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
187
|
+
readonly about: ManifestAbout;
|
|
129
188
|
readonly notation: {
|
|
130
189
|
readonly definitions: readonly NotationDefinitionRecord[];
|
|
131
190
|
readonly bundles: readonly NotationPageBundle[];
|
|
@@ -147,6 +206,16 @@ interface Manifest {
|
|
|
147
206
|
/** `slug -> { eqKey: number }` — passed to `remarkNotation` for `[[slug#eq:key]]`. */
|
|
148
207
|
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
149
208
|
};
|
|
209
|
+
/** Keyed-float identity for `<Table>` — same shape as `equations`. */
|
|
210
|
+
readonly tables: {
|
|
211
|
+
readonly labels: readonly TableLabelRecord[];
|
|
212
|
+
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
213
|
+
};
|
|
214
|
+
/** Keyed-float identity for `<Figure>` — same shape as `equations`. */
|
|
215
|
+
readonly figures: {
|
|
216
|
+
readonly labels: readonly FigureLabelRecord[];
|
|
217
|
+
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
218
|
+
};
|
|
150
219
|
readonly diagnostics: {
|
|
151
220
|
readonly curriculum: readonly CurriculumIssue[];
|
|
152
221
|
readonly notation: readonly NotationDiagnostic[];
|
|
@@ -154,8 +223,14 @@ interface Manifest {
|
|
|
154
223
|
readonly math: readonly MathGateDiagnostic[];
|
|
155
224
|
/** Tier-3 corpus consistency checks (D6). Warnings only. */
|
|
156
225
|
readonly consistency: readonly ConsistencyDiagnostic[];
|
|
226
|
+
/** Knowledge-version checks (C3): `about.version` vs `content/CHANGELOG.md`. Warnings only. */
|
|
227
|
+
readonly knowledge: readonly KnowledgeVersionDiagnostic[];
|
|
157
228
|
/** Equation-identity checks (D7): `eq-duplicate-key`, `eq-ref-resolves` (error); `eq-key-unused` (warning). */
|
|
158
229
|
readonly equations: readonly EquationDiagnostic[];
|
|
230
|
+
/** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
|
|
231
|
+
readonly tables: readonly TableDiagnostic[];
|
|
232
|
+
/** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
|
|
233
|
+
readonly figures: readonly FigureDiagnostic[];
|
|
159
234
|
};
|
|
160
235
|
readonly hashes: {
|
|
161
236
|
/** `sha256` of every content source file, keyed by repo-relative path. */
|
|
@@ -184,4 +259,4 @@ declare function readManifestSync(): Manifest;
|
|
|
184
259
|
*/
|
|
185
260
|
declare function loadManifest(courseConfig?: CourseConfig): Promise<Manifest>;
|
|
186
261
|
|
|
187
|
-
export { MANIFEST_PATH, MANIFEST_SCHEMA_VERSION, type Manifest, type ManifestCitation, type ManifestLesson, type ManifestLessonNotation, type ManifestNotationResolution, type PrereqEdge, compileManifest, loadManifest, readManifestSync, serializeManifest, writeManifest };
|
|
262
|
+
export { MANIFEST_PATH, MANIFEST_SCHEMA_VERSION, type Manifest, type ManifestAbout, type ManifestCitation, type ManifestLesson, type ManifestLessonNotation, type ManifestNotationResolution, type PrereqEdge, compileManifest, loadManifest, readManifestSync, serializeManifest, writeManifest };
|
|
@@ -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 {
|
|
@@ -18,6 +19,12 @@ import { validateNotationAlignment } from "../reference/curriculum-alignment.js"
|
|
|
18
19
|
import {
|
|
19
20
|
validateEquations
|
|
20
21
|
} from "../reference/equations-validate.js";
|
|
22
|
+
import {
|
|
23
|
+
validateTables
|
|
24
|
+
} from "../reference/tables-validate.js";
|
|
25
|
+
import {
|
|
26
|
+
validateFigures
|
|
27
|
+
} from "../reference/figures-validate.js";
|
|
21
28
|
import {
|
|
22
29
|
gateContentMath
|
|
23
30
|
} from "../reference/gate-math.js";
|
|
@@ -25,6 +32,9 @@ import { loadLintIgnore } from "../reference/lint-ignore.js";
|
|
|
25
32
|
import { buildNotationRegistry } from "../reference/registry.js";
|
|
26
33
|
import { formulaGlyphScope } from "../reference/gloss.js";
|
|
27
34
|
import { resolveReferenceId } from "../reference/references.js";
|
|
35
|
+
import {
|
|
36
|
+
resolveKnowledgeVersion
|
|
37
|
+
} from "./knowledge-version.js";
|
|
28
38
|
import {
|
|
29
39
|
loadCurriculumCatalog,
|
|
30
40
|
loadNotationDefinitions,
|
|
@@ -35,12 +45,22 @@ import {
|
|
|
35
45
|
import {
|
|
36
46
|
buildSidebar,
|
|
37
47
|
deriveSources,
|
|
38
|
-
isLessonSlug
|
|
48
|
+
isLessonSlug,
|
|
49
|
+
lessonIdFromSlug
|
|
39
50
|
} from "./lesson-derivation.js";
|
|
40
|
-
const MANIFEST_SCHEMA_VERSION =
|
|
51
|
+
const MANIFEST_SCHEMA_VERSION = 6;
|
|
41
52
|
const REPOSITORY_ROOT = process.cwd();
|
|
42
53
|
const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
|
|
43
54
|
const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
|
|
55
|
+
let enginePackage;
|
|
56
|
+
function readEnginePackage() {
|
|
57
|
+
if (!enginePackage) {
|
|
58
|
+
enginePackage = createRequire(import.meta.url)(
|
|
59
|
+
"explico/package.json"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return enginePackage;
|
|
63
|
+
}
|
|
44
64
|
const CITE_PATTERN = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
|
|
45
65
|
function lessonCitations(body) {
|
|
46
66
|
const withoutCode = body.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
@@ -156,6 +176,11 @@ function lessonFrontmatterById() {
|
|
|
156
176
|
return result;
|
|
157
177
|
}
|
|
158
178
|
async function compileManifest(courseConfig) {
|
|
179
|
+
if (courseConfig.about === void 0 || courseConfig.about.version === void 0 || courseConfig.about.authors === void 0 || courseConfig.about.disclaimer === void 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"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."
|
|
182
|
+
);
|
|
183
|
+
}
|
|
159
184
|
const [catalog, notationInput] = await Promise.all([
|
|
160
185
|
loadCurriculumCatalog(),
|
|
161
186
|
loadNotationRegistryInput()
|
|
@@ -202,20 +227,48 @@ async function compileManifest(courseConfig) {
|
|
|
202
227
|
catalog.lessons.map((lesson) => [lesson.id, lesson])
|
|
203
228
|
);
|
|
204
229
|
const frontmatterById = lessonFrontmatterById();
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
body: lesson.body
|
|
211
|
-
}))
|
|
230
|
+
const lessonSlugs = [...notationInput.lessons].map((lesson) => lesson.lessonId.replace(/\./g, "/")).sort();
|
|
231
|
+
const sidebar = buildSidebar(
|
|
232
|
+
catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
|
|
233
|
+
lessonSlugs,
|
|
234
|
+
courseConfig.sections
|
|
212
235
|
);
|
|
236
|
+
const chapterByLessonId = /* @__PURE__ */ new Map();
|
|
237
|
+
let chapterCursor = 0;
|
|
238
|
+
for (const group of sidebar) {
|
|
239
|
+
for (const item of group.items) {
|
|
240
|
+
if (typeof item !== "string") continue;
|
|
241
|
+
chapterByLessonId.set(lessonIdFromSlug(item), ++chapterCursor);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const keyedFloatLessonInputs = notationInput.lessons.map((lesson) => ({
|
|
245
|
+
lessonId: lesson.lessonId,
|
|
246
|
+
slug: lesson.lessonId.replace(/\./g, "/"),
|
|
247
|
+
file: lesson.source.file,
|
|
248
|
+
body: lesson.body,
|
|
249
|
+
chapter: chapterByLessonId.get(lesson.lessonId)
|
|
250
|
+
}));
|
|
251
|
+
const equationValidation = validateEquations(keyedFloatLessonInputs);
|
|
213
252
|
const equationsBySlug = new Map(
|
|
214
253
|
Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
|
|
215
254
|
slug,
|
|
216
255
|
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
217
256
|
])
|
|
218
257
|
);
|
|
258
|
+
const tableValidation = validateTables(keyedFloatLessonInputs);
|
|
259
|
+
const tablesBySlug = new Map(
|
|
260
|
+
Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
|
|
261
|
+
slug,
|
|
262
|
+
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
263
|
+
])
|
|
264
|
+
);
|
|
265
|
+
const figureValidation = validateFigures(keyedFloatLessonInputs);
|
|
266
|
+
const figuresBySlug = new Map(
|
|
267
|
+
Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
|
|
268
|
+
slug,
|
|
269
|
+
Object.entries(table).map(([key, number]) => ({ key, number }))
|
|
270
|
+
])
|
|
271
|
+
);
|
|
219
272
|
const lessons = [];
|
|
220
273
|
for (const notationLesson of notationInput.lessons) {
|
|
221
274
|
const id = notationLesson.lessonId;
|
|
@@ -229,6 +282,10 @@ async function compileManifest(courseConfig) {
|
|
|
229
282
|
lessons.push({
|
|
230
283
|
id,
|
|
231
284
|
slug: id.replace(/\./g, "/"),
|
|
285
|
+
// 0 = the lesson is not placed in any configured section; a curriculum
|
|
286
|
+
// diagnostic covers that separately, and both numbering paths then fall
|
|
287
|
+
// back to the legacy `##`-section scheme.
|
|
288
|
+
chapter: chapterByLessonId.get(id) ?? 0,
|
|
232
289
|
file: notationLesson.source.file,
|
|
233
290
|
title: frontmatter.title,
|
|
234
291
|
description: frontmatter.description,
|
|
@@ -240,6 +297,8 @@ async function compileManifest(courseConfig) {
|
|
|
240
297
|
sources: deriveSources(notationLesson.body),
|
|
241
298
|
citations: lessonCitations(notationLesson.body),
|
|
242
299
|
equations: equationsBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
300
|
+
tables: tablesBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
301
|
+
figures: figuresBySlug.get(id.replace(/\./g, "/")) ?? [],
|
|
243
302
|
prereqEdges: sortEdges(
|
|
244
303
|
teaches.flatMap(
|
|
245
304
|
(to) => (prerequisitesById.get(to) ?? []).filter((from) => !teaches.includes(from)).map((from) => ({ from, to }))
|
|
@@ -264,7 +323,8 @@ async function compileManifest(courseConfig) {
|
|
|
264
323
|
specs: loadNotationSpecs(),
|
|
265
324
|
catalog,
|
|
266
325
|
lintIgnore: loadLintIgnore(),
|
|
267
|
-
conventions: courseConfig.conventions
|
|
326
|
+
conventions: courseConfig.conventions,
|
|
327
|
+
...courseConfig.operators === void 0 ? {} : { operators: courseConfig.operators }
|
|
268
328
|
});
|
|
269
329
|
const rawNotation = [...loadNotationDefinitions()].sort(
|
|
270
330
|
(a, b) => String(a.key).localeCompare(String(b.key))
|
|
@@ -272,14 +332,40 @@ async function compileManifest(courseConfig) {
|
|
|
272
332
|
const competencies = catalog.competencies.map((entry) => ({ ...entry })).sort(byId);
|
|
273
333
|
const sources = [...loadSourceRecords()].sort(byId);
|
|
274
334
|
const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
courseConfig.
|
|
280
|
-
|
|
335
|
+
const hashes = contentHashes();
|
|
336
|
+
const knowledgeChangelogPath = join(CONTENT_ROOT, "CHANGELOG.md");
|
|
337
|
+
const knowledge = resolveKnowledgeVersion({
|
|
338
|
+
changelogText: existsSync(knowledgeChangelogPath) ? readFileSync(knowledgeChangelogPath, "utf8") : void 0,
|
|
339
|
+
declaredVersion: courseConfig.about.version,
|
|
340
|
+
file: relative(REPOSITORY_ROOT, knowledgeChangelogPath).replace(/\\/g, "/")
|
|
341
|
+
});
|
|
342
|
+
const enginePkg = readEnginePackage();
|
|
343
|
+
const about = {
|
|
344
|
+
engine: {
|
|
345
|
+
name: enginePkg.name,
|
|
346
|
+
version: enginePkg.version,
|
|
347
|
+
...enginePkg.homepage === void 0 ? {} : { homepage: enginePkg.homepage }
|
|
348
|
+
},
|
|
349
|
+
material: {
|
|
350
|
+
name: courseConfig.about.materialName ?? courseConfig.title,
|
|
351
|
+
summary: courseConfig.about.summary ?? courseConfig.description
|
|
352
|
+
},
|
|
353
|
+
knowledge: {
|
|
354
|
+
version: knowledge.version,
|
|
355
|
+
...knowledge.latest?.date === void 0 ? {} : { date: knowledge.latest.date },
|
|
356
|
+
notes: knowledge.latest?.notes ?? [],
|
|
357
|
+
contentTree: hashes.contentTree
|
|
358
|
+
},
|
|
359
|
+
authors: courseConfig.about.authors.map((author) => ({
|
|
360
|
+
name: author.name,
|
|
361
|
+
...author.contact === void 0 ? {} : { contact: author.contact }
|
|
362
|
+
})),
|
|
363
|
+
disclaimer: courseConfig.about.disclaimer,
|
|
364
|
+
...courseConfig.about.changelogHref === void 0 ? {} : { changelogHref: courseConfig.about.changelogHref }
|
|
365
|
+
};
|
|
281
366
|
return {
|
|
282
367
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
368
|
+
about,
|
|
283
369
|
notation: {
|
|
284
370
|
definitions: registry.definitions,
|
|
285
371
|
bundles: registry.bundles,
|
|
@@ -297,15 +383,26 @@ async function compileManifest(courseConfig) {
|
|
|
297
383
|
labels: equationValidation.labels,
|
|
298
384
|
numbersBySlug: equationValidation.numbersBySlug
|
|
299
385
|
},
|
|
386
|
+
tables: {
|
|
387
|
+
labels: tableValidation.labels,
|
|
388
|
+
numbersBySlug: tableValidation.numbersBySlug
|
|
389
|
+
},
|
|
390
|
+
figures: {
|
|
391
|
+
labels: figureValidation.labels,
|
|
392
|
+
numbersBySlug: figureValidation.numbersBySlug
|
|
393
|
+
},
|
|
300
394
|
diagnostics: {
|
|
301
395
|
curriculum: curriculumIssues,
|
|
302
396
|
notation: registry.diagnostics,
|
|
303
397
|
alignment: alignmentIssues,
|
|
304
398
|
math: mathIssues,
|
|
305
399
|
consistency: consistencyIssues,
|
|
306
|
-
equations: equationValidation.diagnostics
|
|
400
|
+
equations: equationValidation.diagnostics,
|
|
401
|
+
tables: tableValidation.diagnostics,
|
|
402
|
+
figures: figureValidation.diagnostics,
|
|
403
|
+
knowledge: knowledge.diagnostics
|
|
307
404
|
},
|
|
308
|
-
hashes
|
|
405
|
+
hashes
|
|
309
406
|
};
|
|
310
407
|
}
|
|
311
408
|
function byId(a, b) {
|
|
@@ -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;
|