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.
Files changed (73) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +46 -5
  3. package/dist/compiler/cli.d.ts +16 -0
  4. package/dist/compiler/cli.js +41 -3
  5. package/dist/compiler/collections.js +4 -1
  6. package/dist/compiler/course-config.d.ts +52 -1
  7. package/dist/compiler/knowledge-version.d.ts +58 -0
  8. package/dist/compiler/knowledge-version.js +80 -0
  9. package/dist/compiler/manifest.d.ts +77 -2
  10. package/dist/compiler/manifest.js +115 -18
  11. package/dist/compiler/resolution-report.d.ts +4 -0
  12. package/dist/reference/base-operators.d.mts +38 -0
  13. package/dist/reference/base-operators.mjs +103 -0
  14. package/dist/reference/consistency.d.ts +7 -2
  15. package/dist/reference/consistency.js +51 -2
  16. package/dist/reference/diagram-layout.d.ts +63 -0
  17. package/dist/reference/diagram-layout.js +122 -0
  18. package/dist/reference/equations-validate.d.ts +5 -0
  19. package/dist/reference/equations-validate.js +1 -1
  20. package/dist/reference/equations.d.mts +42 -11
  21. package/dist/reference/equations.mjs +58 -17
  22. package/dist/reference/figures-validate.d.ts +21 -0
  23. package/dist/reference/figures-validate.js +13 -0
  24. package/dist/reference/keyed-float-validate.d.ts +67 -0
  25. package/dist/reference/keyed-float-validate.js +110 -0
  26. package/dist/reference/keyed-jsx-floats.d.mts +56 -0
  27. package/dist/reference/keyed-jsx-floats.mjs +167 -0
  28. package/dist/reference/math-glyphs.d.mts +18 -0
  29. package/dist/reference/math-glyphs.mjs +92 -0
  30. package/dist/reference/remark-notation.d.mts +12 -0
  31. package/dist/reference/remark-notation.mjs +199 -9
  32. package/dist/reference/tables-validate.d.ts +21 -0
  33. package/dist/reference/tables-validate.js +13 -0
  34. package/dist/reference/units-vocab.js +19 -1
  35. package/docs/VERSIONING.md +66 -0
  36. package/docs/authoring/README.md +96 -0
  37. package/docs/authoring/agents.md +63 -0
  38. package/docs/authoring/migrating.md +107 -0
  39. package/docs/authoring/notation.md +296 -0
  40. package/docs/authoring/versioning.md +116 -0
  41. package/docs/authoring/visualizations.md +195 -0
  42. package/package.json +20 -5
  43. package/src/compiler/cli.ts +49 -0
  44. package/src/compiler/collections.ts +12 -3
  45. package/src/compiler/course-config.ts +54 -0
  46. package/src/compiler/knowledge-version.ts +157 -0
  47. package/src/compiler/manifest.ts +239 -17
  48. package/src/components/CurriculumMap.astro +27 -0
  49. package/src/components/diagram/Diagram.astro +317 -0
  50. package/src/components/examples/CompactExamples.astro +1 -1
  51. package/src/components/figure/Figure.astro +104 -0
  52. package/src/components/notation/render-notation-math.ts +13 -2
  53. package/src/components/reference/FloatRefLayer.astro +671 -0
  54. package/src/components/starlight/AboutPanel.astro +255 -0
  55. package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
  56. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  57. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  58. package/src/components/starlight/LessonFooter.astro +4 -2
  59. package/src/components/table/Table.astro +104 -0
  60. package/src/reference/base-operators.mjs +103 -0
  61. package/src/reference/consistency.ts +83 -2
  62. package/src/reference/diagram-layout.ts +246 -0
  63. package/src/reference/equations-validate.ts +6 -1
  64. package/src/reference/equations.mjs +58 -17
  65. package/src/reference/figures-validate.ts +40 -0
  66. package/src/reference/keyed-float-validate.ts +207 -0
  67. package/src/reference/keyed-jsx-floats.mjs +167 -0
  68. package/src/reference/math-glyphs.mjs +92 -0
  69. package/src/reference/remark-notation.mjs +199 -9
  70. package/src/reference/tables-validate.ts +40 -0
  71. package/src/reference/units-vocab.ts +18 -0
  72. package/src/styles/global.css +115 -7
  73. 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 = 3;
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 = 3;
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 equationValidation = validateEquations(
206
- notationInput.lessons.map((lesson) => ({
207
- lessonId: lesson.lessonId,
208
- slug: lesson.lessonId.replace(/\./g, "/"),
209
- file: lesson.source.file,
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 lessonSlugs = [...notationInput.lessons].map((lesson) => lesson.lessonId.replace(/\./g, "/")).sort();
276
- const sidebar = buildSidebar(
277
- catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
278
- lessonSlugs,
279
- courseConfig.sections
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: contentHashes()
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;