explico 0.7.0 → 0.11.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 (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -3,9 +3,14 @@ import { basename, join, relative } from "node:path";
3
3
  import { parseFrontmatter } from "@astrojs/markdown-remark";
4
4
  import { isSubstantiveMeaning } from "../reference/prose.js";
5
5
  import { parseEquationRef } from "../reference/equations.mjs";
6
+ import {
7
+ DECORATION_NAMES,
8
+ decorationOfEntry
9
+ } from "../reference/math-glyphs.mjs";
6
10
  import { parseKeyedFloatRef } from "../reference/keyed-jsx-floats.mjs";
7
11
  import { parseKnowledgeCheckRef } from "../reference/knowledge-checks.mjs";
8
12
  import { parseMemoRef } from "../reference/remark-memo.mjs";
13
+ import { parseSectionRef } from "../reference/sections.mjs";
9
14
  import { resolveLabel } from "../reference/label.js";
10
15
  import { resolveGlosses } from "../reference/gloss.js";
11
16
  import {
@@ -25,12 +30,24 @@ function filesBelow(directory, extensions) {
25
30
  return entry.isFile() && extensions.some((extension) => entry.name.endsWith(extension)) ? [path] : [];
26
31
  }).sort();
27
32
  }
33
+ function bodyStartLine(raw, body) {
34
+ const offset = raw.endsWith(body) ? raw.length - body.length : raw.indexOf(body);
35
+ if (offset <= 0) return 1;
36
+ let line = 1;
37
+ for (let index = 0; index < offset; index += 1) {
38
+ if (raw.charCodeAt(index) === 10) line += 1;
39
+ }
40
+ return line;
41
+ }
28
42
  function parseDocument(path) {
29
- const parsed = parseFrontmatter(readFileSync(path, "utf8"));
43
+ const raw = readFileSync(path, "utf8");
44
+ const parsed = parseFrontmatter(raw);
45
+ const body = parsed.content.replace(/^\n/, "");
30
46
  return {
31
47
  file: relative(REPOSITORY_ROOT, path),
32
48
  data: parsed.frontmatter ?? {},
33
- body: parsed.content.replace(/^\n/, "")
49
+ body,
50
+ bodyLine: bodyStartLine(raw, body)
34
51
  };
35
52
  }
36
53
  function record(value, label) {
@@ -139,7 +156,7 @@ function extractNotationReferences(text, file, kind) {
139
156
  for (const [pattern, referenceKind] of patterns) {
140
157
  for (const match of searchable.matchAll(pattern)) {
141
158
  const key = (match[1] ?? "").trim();
142
- if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" }) || parseKeyedFloatRef(key, { prefix: "dia" }) || parseKnowledgeCheckRef(key) || parseMemoRef(key)) {
159
+ if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" }) || parseKeyedFloatRef(key, { prefix: "dia" }) || parseKeyedFloatRef(key, { prefix: "ex" }) || parseKnowledgeCheckRef(key) || parseSectionRef(key) || parseMemoRef(key)) {
143
160
  continue;
144
161
  }
145
162
  references.push({
@@ -172,6 +189,29 @@ function lessonCheckIds(lessonPath) {
172
189
  const checksPath = lessonPath.replace(/\.mdx?$/, ".checks.yml");
173
190
  return existsSync(checksPath) ? parseCheckList(readFileSync(checksPath, "utf8")) : [];
174
191
  }
192
+ function loadPartOverviews(parts) {
193
+ const pages = [];
194
+ for (const { dir } of parts) {
195
+ for (const extension of [".mdx", ".md"]) {
196
+ const path = join(CONTENT_ROOT, "docs", dir, `index${extension}`);
197
+ if (!existsSync(path)) continue;
198
+ const { data, file, body, bodyLine } = parseDocument(path);
199
+ const title = typeof data.title === "string" ? data.title : dir;
200
+ const description = typeof data.description === "string" ? data.description : void 0;
201
+ pages.push({
202
+ dir,
203
+ slug: `${dir}/index`,
204
+ file,
205
+ title,
206
+ ...description === void 0 ? {} : { description },
207
+ body,
208
+ bodyLine
209
+ });
210
+ break;
211
+ }
212
+ }
213
+ return pages;
214
+ }
175
215
  function lessonEntries(prerequisitesById) {
176
216
  const lessons = [];
177
217
  for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
@@ -317,22 +357,56 @@ function abbreviationEntries() {
317
357
  async function loadAbbreviationRegistryInput() {
318
358
  return abbreviationEntries();
319
359
  }
360
+ function notationSuspensions(raw, localDefinitions, file) {
361
+ if (raw === void 0) return [];
362
+ if (!Array.isArray(raw)) {
363
+ throw new TypeError(`${file} notation.suspend must be an array`);
364
+ }
365
+ const seen = /* @__PURE__ */ new Set();
366
+ return raw.map((item, index) => {
367
+ const at = `${file} notation.suspend[${index}]`;
368
+ const data = record(item, at);
369
+ const operator = string(data.operator, `${at}.operator`).trim();
370
+ if (!DECORATION_NAMES.has(operator)) {
371
+ throw new Error(
372
+ `${at}.operator ${JSON.stringify(operator)} is not a decoration; use one of ${[...DECORATION_NAMES].join(", ")}`
373
+ );
374
+ }
375
+ if (seen.has(operator)) {
376
+ throw new Error(`${at} suspends ${JSON.stringify(operator)} a second time`);
377
+ }
378
+ seen.add(operator);
379
+ const local = localDefinitions.find(
380
+ (definition) => decorationOfEntry(definition.notation) === operator
381
+ );
382
+ if (local) {
383
+ throw new Error(
384
+ `${at} suspends ${JSON.stringify(operator)}, but notation.local entry ${local.key} explains it as an operator (latex ${JSON.stringify(local.notation)}); drop one of them`
385
+ );
386
+ }
387
+ const reason = optionalString(data.reason, `${at}.reason`);
388
+ return reason === void 0 ? { operator } : { operator, reason: reason.trim() };
389
+ });
390
+ }
320
391
  function notationLessonEntries() {
321
392
  const lessons = [];
322
393
  for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
323
394
  const slug = docSlug(path);
324
395
  if (!isLessonSlug(slug)) continue;
325
- const { file, data, body } = parseDocument(path);
396
+ const { file, data, body, bodyLine } = parseDocument(path);
326
397
  const notation = record(data.notation ?? {}, `${file} notation`);
327
398
  const local = Array.isArray(notation.local) ? notation.local : [];
399
+ const localDefinitions = local.map(
400
+ (entry, index) => localDefinition(entry, file, index)
401
+ );
328
402
  lessons.push({
329
403
  lessonId: lessonIdFromSlug(slug),
330
404
  status: status(data.editorialStatus, `${file} editorialStatus`),
331
- localDefinitions: local.map(
332
- (entry, index) => localDefinition(entry, file, index)
333
- ),
405
+ localDefinitions,
406
+ suspended: notationSuspensions(notation.suspend, localDefinitions, file),
334
407
  references: extractNotationReferences(body, file, void 0),
335
408
  body,
409
+ bodyLine,
336
410
  source: { file }
337
411
  });
338
412
  }
@@ -435,6 +509,8 @@ export {
435
509
  loadNotationDefinitions,
436
510
  loadNotationRegistryInput,
437
511
  loadNotationSpecs,
512
+ loadPartOverviews,
438
513
  loadSourceRecords,
439
- notationSourceLocatorGaps
514
+ notationSourceLocatorGaps,
515
+ notationSuspensions
440
516
  };
@@ -1,194 +1,3 @@
1
- /**
2
- * The shape `content/course.config.ts` must satisfy (Phase F).
3
- *
4
- * The engine never imports the course's config file; the wiring layer
5
- * (`astro.config.mjs`, `scripts/*`, `src/content.config.ts`) imports the plain
6
- * data value and passes it here, typed against `CourseConfig`. A second course
7
- * supplies its own value of this shape.
8
- */
9
- /**
10
- * One **part** — a sidebar group, holding the chapters (lessons) whose slugs
11
- * start with `dir`. A part's 1-based position in `CourseConfig.parts` is the
12
- * number before the dot in every equation, table, and figure number inside it.
13
- * See `docs/authoring/structure.md` for the full level taxonomy.
14
- */
15
- interface CoursePart {
16
- /** Lesson-slug prefix, e.g. `bonds` for `bonds/yield-to-maturity`. */
17
- readonly dir: string;
18
- /** Sidebar group heading. */
19
- readonly label: string;
20
- }
21
- /**
22
- * @deprecated Renamed to {@link CoursePart}. "Section" now only ever means an
23
- * `##` heading inside a lesson. Removed in the next MINOR release.
24
- */
25
- type CourseSection = CoursePart;
26
- interface CourseConventions {
27
- /** Notation key a lesson's stated sign convention must resolve to. */
28
- readonly signConventionKey: string;
29
- /** Competency id that also counts as declaring the cash-flow perspective. */
30
- readonly cashFlowPerspectiveCompetency: string;
31
- }
32
- /**
33
- * A course's edits to the shipped named-operator baseline (`BASE_OPERATORS`),
34
- * consumed by the `operator-explained` consistency check (rule N8). An operator
35
- * a lesson uses is fine when it is on the baseline plus `allow` minus `deny`;
36
- * otherwise it needs a notation entry the reader can hover.
37
- *
38
- * Names are the KaTeX command without its backslash (`erf`, not `\erf`) for a
39
- * built-in, and the verbatim body for an `\operatorname{…}` operator (`Var`).
40
- */
41
- interface CourseOperators {
42
- /** Operators this course also treats as universal (`Var`, `Cov`, `sgn`, …). */
43
- readonly allow?: readonly string[];
44
- /** Baseline operators this course wants explained anyway. */
45
- readonly deny?: readonly string[];
46
- }
47
- /**
48
- * A course's edits to the shipped roman-label baseline (`BASE_ROMAN_LABELS`),
49
- * consumed by the `roman-label-explained` consistency check (rule N10). A
50
- * `\mathrm{…}` multi-letter label a lesson uses is fine when it is on the
51
- * baseline plus `allow` minus `deny`; otherwise it needs a notation entry the
52
- * reader can hover. Same shape as {@link CourseOperators}, one level down:
53
- * this governs `\mathrm{KE}`-style labels, not `\operatorname{Var}`-style
54
- * operators.
55
- */
56
- interface CourseRomanLabels {
57
- /** Labels this course also treats as universal qualifiers (`eff`, `net`, …). */
58
- readonly allow?: readonly string[];
59
- /** Baseline labels this course wants explained anyway. */
60
- readonly deny?: readonly string[];
61
- }
62
- /**
63
- * A course's edits to the shipped abbreviation baseline (`BASE_ABBREVIATIONS`),
64
- * consumed by the `abbreviation-explained` gate (rule N14). An abbreviation-
65
- * shaped token — an all-caps acronym or a dotted form like `w.r.t.` — a
66
- * lesson uses in prose is fine when it is on the baseline plus `allow` minus
67
- * `deny`; otherwise it needs an entry in the `abbreviations` content
68
- * collection, or — if it also names a math quantity — a notation entry the
69
- * reader can hover. Same shape as {@link CourseOperators} / {@link
70
- * CourseRomanLabels}.
71
- */
72
- interface CourseAbbreviations {
73
- /** Abbreviations this course also treats as universal (`GDP`, `CPU`, …). */
74
- readonly allow?: readonly string[];
75
- /** Baseline abbreviations this course wants explained anyway. */
76
- readonly deny?: readonly string[];
77
- }
78
- /** One person or team credited for the study material. */
79
- interface CourseAuthor {
80
- readonly name: string;
81
- /**
82
- * Email, URL, or handle. Rendered in the About panel as a link when it looks
83
- * like a `mailto:` or `https:` target, plain text otherwise.
84
- */
85
- readonly contact?: string;
86
- }
87
- /**
88
- * The reader-facing "about this material" block, surfaced by the `[?]` panel in
89
- * the header (`components/starlight/AboutPanel.astro`) on every page.
90
- *
91
- * `version` is the course's **knowledge version** — semver over the
92
- * learning-facing content, independent of the `explico` engine's own version.
93
- * It must equal the newest entry in `content/CHANGELOG.md`; a mismatch is a
94
- * Tier-3 `knowledge-version-mismatch` warning (rule C3, `docs/authoring/versioning.md`).
95
- */
96
- interface CourseAbout {
97
- /** Display name of the material. Defaults to {@link CourseConfig.title}. */
98
- readonly materialName?: string;
99
- /** One or two sentences on what the material covers. Defaults to {@link CourseConfig.description}. */
100
- readonly summary?: string;
101
- /** Knowledge version — semver; equals the latest `content/CHANGELOG.md` heading. */
102
- readonly version: string;
103
- /** Who authored / maintains the material. At least one entry. */
104
- readonly authors: readonly CourseAuthor[];
105
- /** Shown verbatim in the About panel — scope, warranty, AI-assistance note. */
106
- readonly disclaimer: string;
107
- /** Link to the published content changelog (repo blob, docs page, …). */
108
- readonly changelogHref?: string;
109
- }
110
- /**
111
- * A course's chosen initial open/closed state for the engine's own built-in
112
- * collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
113
- * and the generated "References" section (`remark-citation.mjs`). Each is
114
- * independent of `defaultCollapsible.sections`/`.subsections` (an author's
115
- * own `##` headings): both of these are engine-authored, not written by a
116
- * lesson, so they need their own default rather than inheriting the site's
117
- * general heading default.
118
- *
119
- * `true` starts open, `false` (or omitted) starts closed — the engine's own
120
- * default is closed either way, matching a long panel a reader usually
121
- * doesn't need open on arrival.
122
- */
123
- interface CourseDisclosureDefaults {
124
- /** Initial state of the "Page notation" panel. Default: closed. */
125
- readonly notation?: boolean;
126
- /** Initial state of the generated "References" section. Default: closed. */
127
- readonly references?: boolean;
128
- }
129
- /** The stable, non-optional form `NormalizedLessonExports` mirrors. */
130
- interface NormalizedDisclosureDefaults {
131
- readonly notation: boolean;
132
- readonly references: boolean;
133
- }
134
- /** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
135
- * an absent sub-field) resolves to closed, the engine's own default. */
136
- declare function normalizeDisclosureDefaults(config: CourseConfig): NormalizedDisclosureDefaults;
137
- /** Optional, build-time lesson artifacts exposed to readers. */
138
- interface LessonExportsConfig {
139
- /** Emit one self-contained, offline-capable HTML file per lesson. */
140
- readonly standaloneHtml?: boolean;
141
- /** Offer the browser's native print dialog with explico's lesson print layout. */
142
- readonly print?: boolean;
143
- /**
144
- * Published course root used for links that intentionally leave the exported
145
- * lesson. Required at export time when such a link is present.
146
- */
147
- readonly canonicalBaseUrl?: string;
148
- }
149
- /** The stable, non-optional export settings carried by the manifest. */
150
- interface NormalizedLessonExports {
151
- readonly standaloneHtml: boolean;
152
- readonly print: boolean;
153
- readonly canonicalBaseUrl?: string;
154
- }
155
- interface CourseConfig {
156
- readonly title: string;
157
- readonly description: string;
158
- /**
159
- * The course's parts (sidebar groups), in order. Exactly one of `parts` or
160
- * the deprecated `sections` must be given.
161
- */
162
- readonly parts?: readonly CoursePart[];
163
- /**
164
- * @deprecated Renamed to {@link CourseConfig.parts}. Still honoured, so a
165
- * course on the old name keeps building; removed in the next MINOR release.
166
- */
167
- readonly sections?: readonly CoursePart[];
168
- /** Namespaces used by competency / notation `domain` fields. */
169
- readonly domains: readonly string[];
170
- readonly conventions: CourseConventions;
171
- /** Reader-facing identity + knowledge version (the `[?]` About panel). */
172
- readonly about: CourseAbout;
173
- /** Edits to the shipped named-operator baseline (rule N8). Optional. */
174
- readonly operators?: CourseOperators;
175
- /** Edits to the shipped roman-label baseline (rule N10). Optional. */
176
- readonly romanLabels?: CourseRomanLabels;
177
- /** Edits to the shipped abbreviation baseline (rule N14). Optional. */
178
- readonly abbreviations?: CourseAbbreviations;
179
- /** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
180
- readonly lessonExports?: LessonExportsConfig;
181
- /** Initial open/closed state for the engine's own built-in collapsible
182
- * sections ("Page notation", "References"). Closed when omitted. */
183
- readonly disclosureDefaults?: CourseDisclosureDefaults;
184
- }
185
- /** Validate and canonicalize the build-facing lesson-export configuration. */
186
- declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonExports;
187
- /**
188
- * The course's parts, from whichever field the config uses. Throws rather than
189
- * silently producing a course with no sidebar groups — a config with neither
190
- * field would otherwise number every float by the `##`-section fallback.
191
- */
192
- declare function coursePartsOf(config: CourseConfig): readonly CoursePart[];
193
-
194
- export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseDisclosureDefaults, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedDisclosureDefaults, type NormalizedLessonExports, coursePartsOf, normalizeDisclosureDefaults, normalizeLessonExports };
1
+ export { s as BLOCK_LABELS_DEFAULT, B as BlockLabels, a as CourseAbbreviations, t as CourseAbout, i as CourseAccents, u as CourseAuthor, C as CourseConfig, f as CourseConventions, v as CourseDisclosure, w as CourseHeadingDisclosure, x as CourseNumbering, y as CourseOpenDefault, g as CourseOperators, z as CoursePart, h as CourseRomanLabels, A as CourseSection, D as DISCLOSURE_DEFAULTS, L as LessonExportsConfig, c as NormalizedDisclosure, b as NormalizedLessonExports, d as NormalizedNumbering, N as NormalizedPartPages, E as coursePartsOf, F as normalizeBlockLabels, G as normalizeDisclosure, H as normalizeLessonExports, I as normalizeNumbering } from '../course-config-Dsnri9Tk.js';
2
+ import '../rules/course-rules.js';
3
+ import '../rules/catalog.js';
@@ -1,9 +1,78 @@
1
- function normalizeDisclosureDefaults(config) {
1
+ const DISCLOSURE_DEFAULTS = {
2
+ sections: { collapsible: false, defaultOpen: true },
3
+ subsections: { collapsible: false, defaultOpen: true },
4
+ knowledgeChecks: { collapsible: true, defaultOpen: true },
5
+ notation: { defaultOpen: false },
6
+ references: { defaultOpen: false },
7
+ memos: { defaultOpen: false },
8
+ folds: { defaultOpen: false },
9
+ figures: { defaultOpen: true },
10
+ workedExamples: { defaultOpen: false },
11
+ tables: { truncateAfter: 6 }
12
+ };
13
+ function normalizeDisclosure(config) {
14
+ const disclosure = config.disclosure ?? {};
15
+ if ("diagrams" in disclosure) {
16
+ throw new Error(
17
+ "CourseConfig disclosure.diagrams is gone: a diagram no longer folds by itself. Delete the key; to let a reader fold a diagram away, wrap it in a <Fold> or put it in a collapsible section."
18
+ );
19
+ }
20
+ const heading = (key) => ({
21
+ collapsible: disclosure[key]?.collapsible ?? DISCLOSURE_DEFAULTS[key].collapsible,
22
+ defaultOpen: disclosure[key]?.defaultOpen ?? DISCLOSURE_DEFAULTS[key].defaultOpen
23
+ });
24
+ const open = (key) => ({
25
+ defaultOpen: disclosure[key]?.defaultOpen ?? DISCLOSURE_DEFAULTS[key].defaultOpen
26
+ });
27
+ const truncateAfter = disclosure.tables?.truncateAfter ?? DISCLOSURE_DEFAULTS.tables.truncateAfter;
28
+ if (!Number.isInteger(truncateAfter) || truncateAfter < 2) {
29
+ throw new Error(
30
+ `CourseConfig disclosure.tables.truncateAfter must be a whole number of rows, at least 2; received ${JSON.stringify(truncateAfter)}.`
31
+ );
32
+ }
33
+ return {
34
+ sections: heading("sections"),
35
+ subsections: heading("subsections"),
36
+ knowledgeChecks: heading("knowledgeChecks"),
37
+ notation: open("notation"),
38
+ references: open("references"),
39
+ memos: open("memos"),
40
+ folds: open("folds"),
41
+ figures: open("figures"),
42
+ workedExamples: open("workedExamples"),
43
+ tables: { truncateAfter }
44
+ };
45
+ }
46
+ function normalizeNumbering(config) {
47
+ const numbering = config.numbering ?? {};
48
+ const depth = numbering.sections?.depth ?? 4;
49
+ if (!Number.isInteger(depth) || depth < 2 || depth > 6) {
50
+ throw new Error(
51
+ `CourseConfig numbering.sections.depth must be a heading level from 2 (##) to 6 (######); received ${JSON.stringify(depth)}.`
52
+ );
53
+ }
54
+ const floats = numbering.floats ?? "chapter";
55
+ if (floats !== "chapter" && floats !== "part") {
56
+ throw new Error(
57
+ `CourseConfig numbering.floats must be "chapter" or "part"; received ${JSON.stringify(floats)}.`
58
+ );
59
+ }
2
60
  return {
3
- notation: config.disclosureDefaults?.notation === true,
4
- references: config.disclosureDefaults?.references === true
61
+ sections: { show: numbering.sections?.show !== false, depth },
62
+ floats,
63
+ aliases: numbering.aliases !== false
5
64
  };
6
65
  }
66
+ const BLOCK_LABELS_DEFAULT = "icon-word";
67
+ function normalizeBlockLabels(config) {
68
+ const labels = config.blockLabels ?? BLOCK_LABELS_DEFAULT;
69
+ if (labels !== "icon" && labels !== "icon-word") {
70
+ throw new Error(
71
+ `CourseConfig blockLabels must be "icon" or "icon-word"; received ${JSON.stringify(labels)}.`
72
+ );
73
+ }
74
+ return labels;
75
+ }
7
76
  function normalizeLessonExports(config) {
8
77
  const standaloneHtml = config.lessonExports?.standaloneHtml === true;
9
78
  const print = config.lessonExports?.print === true;
@@ -40,7 +109,11 @@ function coursePartsOf(config) {
40
109
  return parts;
41
110
  }
42
111
  export {
112
+ BLOCK_LABELS_DEFAULT,
113
+ DISCLOSURE_DEFAULTS,
43
114
  coursePartsOf,
44
- normalizeDisclosureDefaults,
45
- normalizeLessonExports
115
+ normalizeBlockLabels,
116
+ normalizeDisclosure,
117
+ normalizeLessonExports,
118
+ normalizeNumbering
46
119
  };
@@ -1,7 +1,9 @@
1
1
  import { Manifest } from '../manifest.js';
2
2
  import '../../curriculum/validation.js';
3
3
  import '../../reference/consistency.js';
4
- import '../course-config.js';
4
+ import '../../course-config-Dsnri9Tk.js';
5
+ import '../../rules/catalog.js';
6
+ import '../../rules/course-rules.js';
5
7
  import '../collections.js';
6
8
  import '../../reference/types.js';
7
9
  import '../../reference/lint-ignore.js';
@@ -10,11 +12,14 @@ import '../../reference/tables-validate.js';
10
12
  import '../../reference/keyed-float-validate.js';
11
13
  import '../../reference/figures-validate.js';
12
14
  import '../../reference/diagrams-validate.js';
15
+ import '../../reference/examples-validate.js';
16
+ import '../../reference/folds-validate.js';
13
17
  import '../../reference/knowledge-checks-validate.js';
14
18
  import '../../reference/raw-floats-validate.js';
15
19
  import '../../reference/gate-math.js';
16
20
  import '../../reference/mixed-content.js';
17
21
  import '../../reference/gloss.js';
22
+ import '../../rules/waivers.js';
18
23
  import '../knowledge-version.js';
19
24
  import '../lesson-derivation.js';
20
25
 
@@ -61,7 +61,7 @@ function resolveKnowledgeVersion(options) {
61
61
  if (!latest) {
62
62
  warn(
63
63
  "knowledge-changelog-missing",
64
- `${file} has no released \`## [x.y.z]\` entry \u2014 only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C4).`
64
+ `${file} has no released \`## [x.y.z]\` entry \u2014 only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C3).`
65
65
  );
66
66
  return { version: declaredVersion, diagnostics };
67
67
  }
@@ -68,22 +68,39 @@ declare function mergeTrackOrder(tracks: readonly TrackLike[]): string[];
68
68
  interface SidebarSection {
69
69
  readonly dir: string;
70
70
  readonly label: string;
71
+ readonly overviewLabel?: string;
72
+ /**
73
+ * Href of this part's overview page (`/<dir>/`), set only when the part has
74
+ * one. It becomes the group's first item, so the page a part directory
75
+ * resolves to is reachable from the navigation that names the part.
76
+ */
77
+ readonly overviewLink?: string;
71
78
  }
72
- type SidebarGroup = {
73
- readonly label: string;
74
- readonly items: readonly string[];
75
- } | {
79
+ /**
80
+ * One sidebar entry: a lesson slug, or an explicit link. `attrs` is Starlight's
81
+ * own passthrough onto the rendered `<a>` — the engine uses it to mark a part's
82
+ * overview row so CSS can give it its own icon.
83
+ */
84
+ type SidebarItem = string | {
76
85
  readonly label: string;
77
- readonly items: readonly {
78
- readonly label: string;
79
- readonly link: string;
80
- }[];
86
+ readonly link: string;
87
+ readonly attrs?: Readonly<Record<string, string>>;
81
88
  };
89
+ interface SidebarGroup {
90
+ readonly label: string;
91
+ readonly items: readonly SidebarItem[];
92
+ }
93
+ /** Marks the overview row so `global.css` can give it its own icon. */
94
+ declare const PART_OVERVIEW_CLASS = "explico-part-overview";
82
95
  /**
83
96
  * The Starlight `sidebar` config: one group per section, lessons ordered by the
84
97
  * merged track order (ties broken by slug), then the static Reference group.
85
98
  * Replaces seven `autogenerate` blocks that read an authored `sidebar.order`.
99
+ * `floatIndex`: the course has a top-level "Figures and tables" page
100
+ * (`figures-and-tables.mdx`, `FloatIndex.astro`), listed in Reference.
86
101
  */
87
- declare function buildSidebar(tracks: readonly TrackLike[], lessonSlugs: readonly string[], parts: readonly SidebarSection[]): SidebarGroup[];
102
+ declare function buildSidebar(tracks: readonly TrackLike[], lessonSlugs: readonly string[], parts: readonly SidebarSection[], { floatIndex }?: {
103
+ readonly floatIndex?: boolean;
104
+ }): SidebarGroup[];
88
105
 
89
- export { type SidebarGroup, type SidebarSection, buildSidebar, citedSourceIds, deriveRequires, deriveSources, isLessonSlug, lessonIdFromSlug, mergeTrackOrder, parseCheckList };
106
+ export { PART_OVERVIEW_CLASS, type SidebarGroup, type SidebarItem, type SidebarSection, buildSidebar, citedSourceIds, deriveRequires, deriveSources, isLessonSlug, lessonIdFromSlug, mergeTrackOrder, parseCheckList };
@@ -59,27 +59,42 @@ function mergeTrackOrder(tracks) {
59
59
  }
60
60
  return ordered;
61
61
  }
62
- function buildSidebar(tracks, lessonSlugs, parts) {
62
+ const PART_OVERVIEW_CLASS = "explico-part-overview";
63
+ function buildSidebar(tracks, lessonSlugs, parts, { floatIndex = false } = {}) {
63
64
  const rank = new Map(
64
65
  mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index])
65
66
  );
66
67
  const positionOf = (slug) => rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
67
- const groups = parts.map(({ dir, label }) => ({
68
- label,
69
- items: lessonSlugs.filter((slug) => slug.startsWith(`${dir}/`)).sort(
70
- (left, right) => positionOf(left) - positionOf(right) || left.localeCompare(right)
71
- )
72
- }));
68
+ const groups = parts.map(
69
+ ({ dir, label, overviewLabel, overviewLink }) => {
70
+ const chapters = lessonSlugs.filter((slug) => slug.startsWith(`${dir}/`)).sort(
71
+ (left, right) => positionOf(left) - positionOf(right) || left.localeCompare(right)
72
+ );
73
+ return {
74
+ label,
75
+ items: overviewLink ? [
76
+ {
77
+ label: overviewLabel ?? "Overview",
78
+ link: overviewLink,
79
+ attrs: { class: PART_OVERVIEW_CLASS }
80
+ },
81
+ ...chapters
82
+ ] : chapters
83
+ };
84
+ }
85
+ );
73
86
  groups.push({
74
87
  label: "Reference",
75
88
  items: [
76
89
  { label: "Curriculum map", link: "/curriculum-map/" },
77
- { label: "Notation glossary", link: "/glossary/" }
90
+ { label: "Notation glossary", link: "/glossary/" },
91
+ ...floatIndex ? [{ label: "Figures and tables", link: "/figures-and-tables/" }] : []
78
92
  ]
79
93
  });
80
94
  return groups;
81
95
  }
82
96
  export {
97
+ PART_OVERVIEW_CLASS,
83
98
  buildSidebar,
84
99
  citedSourceIds,
85
100
  deriveRequires,
@@ -67,6 +67,11 @@ interface AnyDiagnostic {
67
67
  readonly file?: string;
68
68
  readonly line?: number;
69
69
  readonly column?: number;
70
+ /** `{ key, href }` when the finding belongs to a `\label{eq:…}` equation. */
71
+ readonly equation?: {
72
+ readonly key: string;
73
+ readonly href: string;
74
+ };
70
75
  }
71
76
  /** `manifest.diagnostics`, structurally — one array per subsystem. */
72
77
  type DiagnosticGroups = Readonly<Record<string, readonly AnyDiagnostic[]>>;
@@ -89,7 +94,15 @@ interface RunLog {
89
94
  /** Refreshes `latest` and reports what the run recorded. */
90
95
  close(): RunLogSummary;
91
96
  }
92
- /** `[code] message`, matching what the console has always printed. */
97
+ /**
98
+ * `[code] file:line:column: message`, matching what the console has always
99
+ * printed, with ` (eq:<key> -> <href>)` appended when the finding belongs to a
100
+ * keyed equation — so a size-only finding inside a long `aligned` block names
101
+ * the result it is about, not just its coordinates. A code owned by a lettered
102
+ * rule is prefixed with that rule's ID (`[N9 prose-notation]`), so the line
103
+ * names the rule page to read; an invariant's code stands alone, and a course
104
+ * rule's code *is* its ID (`[X1]`), so it is not repeated.
105
+ */
93
106
  declare function diagnosticLine(diagnostic: AnyDiagnostic): string;
94
107
  interface RunLogOptions {
95
108
  /** Overridden in tests; defaults to `build/logs` under the repository root. */
@@ -7,6 +7,7 @@ import {
7
7
  writeFileSync
8
8
  } from "node:fs";
9
9
  import { join, relative } from "node:path";
10
+ import { ruleIdOf } from "../rules/catalog.js";
10
11
  const REPOSITORY_ROOT = process.cwd();
11
12
  const LOGS_DIR = join(REPOSITORY_ROOT, "build", "logs");
12
13
  const LEVELS = ["ERROR", "WARNING", "INFO"];
@@ -37,8 +38,11 @@ function formatRecord(level, source, message, date) {
37
38
  function diagnosticLine(diagnostic) {
38
39
  const code = diagnostic.code ?? diagnostic.kind;
39
40
  const at = [diagnostic.file, diagnostic.line, diagnostic.column].filter((part) => part !== void 0).join(":");
40
- const prefix = code === void 0 ? "" : `[${code}] `;
41
- return at === "" ? `${prefix}${diagnostic.message}` : `${prefix}${at}: ${diagnostic.message}`;
41
+ const owner = code === void 0 ? void 0 : ruleIdOf(code);
42
+ const rule = owner === void 0 || owner === code ? "" : `${owner} `;
43
+ const prefix = code === void 0 ? "" : `[${rule}${code}] `;
44
+ const equation = diagnostic.equation === void 0 ? "" : ` (eq:${diagnostic.equation.key} -> ${diagnostic.equation.href})`;
45
+ return at === "" ? `${prefix}${diagnostic.message}${equation}` : `${prefix}${at}: ${diagnostic.message}${equation}`;
42
46
  }
43
47
  function createRunLog(options = {}) {
44
48
  const root = options.root ?? LOGS_DIR;