explico 0.3.0 → 0.5.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 (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -156,7 +156,7 @@ export type SidebarGroup =
156
156
  export function buildSidebar(
157
157
  tracks: readonly TrackLike[],
158
158
  lessonSlugs: readonly string[],
159
- sections: readonly SidebarSection[],
159
+ parts: readonly SidebarSection[],
160
160
  ): SidebarGroup[] {
161
161
  const rank = new Map(
162
162
  mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index]),
@@ -164,7 +164,7 @@ export function buildSidebar(
164
164
  const positionOf = (slug: string) =>
165
165
  rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
166
166
 
167
- const groups: SidebarGroup[] = sections.map(({ dir, label }) => ({
167
+ const groups: SidebarGroup[] = parts.map(({ dir, label }) => ({
168
168
  label,
169
169
  items: lessonSlugs
170
170
  .filter((slug) => slug.startsWith(`${dir}/`))
@@ -47,6 +47,8 @@ import {
47
47
  type ConsistencyDiagnostic,
48
48
  } from '../reference/consistency';
49
49
  import { validateNotationAlignment } from '../reference/curriculum-alignment';
50
+ import { countDisplayEquations } from '../reference/equations.mjs';
51
+ import { countKeyedJsxFloats } from '../reference/keyed-jsx-floats.mjs';
50
52
  import {
51
53
  validateEquations,
52
54
  type EquationDiagnostic,
@@ -62,10 +64,34 @@ import {
62
64
  type FigureDiagnostic,
63
65
  type FigureLabelRecord,
64
66
  } from '../reference/figures-validate';
67
+ import {
68
+ validateDiagrams,
69
+ type DiagramDiagnostic,
70
+ type DiagramLabelRecord,
71
+ } from '../reference/diagrams-validate';
72
+ import {
73
+ validateKnowledgeChecks,
74
+ type KnowledgeCheckDiagnostic,
75
+ type KnowledgeCheckLabelRecord,
76
+ } from '../reference/knowledge-checks-validate';
77
+ import {
78
+ validateRawFloats,
79
+ type RawFloatDiagnostic,
80
+ } from '../reference/raw-floats-validate';
65
81
  import {
66
82
  gateContentMath,
83
+ pageScope,
67
84
  type MathGateDiagnostic,
68
85
  } from '../reference/gate-math';
86
+ import {
87
+ gateProseNotation,
88
+ gateProseNotationInAssessments,
89
+ } from '../reference/gate-prose-notation';
90
+ import { gateAbbreviations } from '../reference/gate-abbreviations';
91
+ import {
92
+ splitMixedContent,
93
+ type ContentSegment,
94
+ } from '../reference/mixed-content';
69
95
  import { loadLintIgnore } from '../reference/lint-ignore';
70
96
  import { buildNotationRegistry } from '../reference/registry';
71
97
  import { formulaGlyphScope } from '../reference/gloss';
@@ -80,12 +106,18 @@ import type {
80
106
  NotationLessonInput,
81
107
  NotationPageBundle,
82
108
  } from '../reference/types';
83
- import type { CourseConfig } from './course-config';
109
+ import {
110
+ coursePartsOf,
111
+ normalizeLessonExports,
112
+ type CourseConfig,
113
+ type NormalizedLessonExports,
114
+ } from './course-config';
84
115
  import {
85
116
  resolveKnowledgeVersion,
86
117
  type KnowledgeVersionDiagnostic,
87
118
  } from './knowledge-version';
88
119
  import {
120
+ loadAbbreviationRegistryInput,
89
121
  loadCurriculumCatalog,
90
122
  loadNotationDefinitions,
91
123
  loadNotationRegistryInput,
@@ -110,14 +142,43 @@ import {
110
142
  * `lessons[].equations`, and `diagnostics.equations`.
111
143
  * v4 (F-tables-figures): `tables`/`figures` (same shape as `equations`, for
112
144
  * `<Table>`/`<Figure>`), `lessons[].tables`/`lessons[].figures`, and
113
- * `diagnostics.tables`/`diagnostics.figures`.
145
+ * `diagnostics.tables`/`diagnostics.figures`/`diagnostics.diagrams`.
114
146
  * v5 (about): `about` (engine identity from `explico`'s `package.json`, the
115
147
  * course's reader-facing `about` block, and the knowledge version resolved
116
148
  * against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
117
149
  * v6 (chapter numbering): `lessons[].chapter` — the lesson's 1-based course
118
150
  * ordinal, the chapter in every equation/table/figure number (`(2.1)`).
151
+ * v7 (part numbering): `lessons[].part` (the sidebar group's 1-based ordinal,
152
+ * now the number before the dot), `lessons[].chapter` redefined as the
153
+ * lesson's ordinal *within its part*, and `lessons[].numbering` — the
154
+ * per-kind offsets that make `n` run continuously across a part.
155
+ * v8 (assessment math): `assessmentContent` — every assessment item's
156
+ * `prompt` / `explanation`, pre-split into text/math segments and resolved
157
+ * against the scope of the lesson that embeds it, so `AssessmentSet.astro`
158
+ * can render them through the same KaTeX pipeline as lesson body math instead
159
+ * of showing raw text.
160
+ * v9 (diagram identity): `diagrams` (the independent `<Diagram>` number and
161
+ * cross-page lookup), `lessons[].diagrams`, `lessons[].numbering.diagrams`,
162
+ * and `diagnostics.diagrams`.
163
+ * v10 (knowledge-check identity): `checks` (part-wide numbering and cross-page
164
+ * lookup), `lessons[].knowledgeChecks`, `lessons[].numbering.checks`, and
165
+ * `diagnostics.checks`.
166
+ * v11 (lesson exports): normalized standalone HTML and print build/UI settings.
167
+ * v12 (abbreviation hover cards): `manifest.abbreviations` — the
168
+ * `abbreviations` content collection (N14), flattened to plain
169
+ * `{key, abbr, expansion, aliases, domain?}` records sorted by key, the way
170
+ * `manifest.notation.raw` already carries notation frontmatter for
171
+ * `remarkNotation`. `remarkAbbreviations` reads it to wrap the first
172
+ * occurrence of a known abbreviation in a lesson's prose with a hover/click
173
+ * card.
174
+ * v13 (raw-float diagnostics, additive): `diagnostics.rawFloats` — a raw
175
+ * Markdown table, a raw HTML `<table>`, or an Explico `<Diagram>` that is
176
+ * unkeyed or nested outside `<Table>`/`<Figure>` never reaches keyed-float
177
+ * validation and can never get a stable number or `[[…]]` reference; this
178
+ * flags it (warning-only — the manifest shape a client already reads is
179
+ * unchanged).
119
180
  */
120
- export const MANIFEST_SCHEMA_VERSION = 6;
181
+ export const MANIFEST_SCHEMA_VERSION = 13;
121
182
 
122
183
  const REPOSITORY_ROOT = process.cwd();
123
184
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -206,11 +267,31 @@ export interface ManifestLesson {
206
267
  /** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
207
268
  readonly slug: string;
208
269
  /**
209
- * The lesson's 1-based ordinal in the course (merged track order, then any
210
- * unranked lessons by slug). It is the chapter in every equation, table, and
211
- * figure number on the page — `(2.1)` is the first equation of chapter 2.
270
+ * The 1-based ordinal of the **part** this lesson sits in — its sidebar
271
+ * group's position in `courseConfig.parts`. It is the number before the dot
272
+ * in every equation, table, figure, diagram, and knowledge-check number on
273
+ * the page: `(2.1)` is the first equation of part 2. See
274
+ * `docs/authoring/structure.md`.
275
+ */
276
+ readonly part: number;
277
+ /**
278
+ * The lesson's 1-based ordinal **within its part** (merged track order, then
279
+ * any unranked lessons by slug) — its chapter number. This orders the
280
+ * sidebar and the reading path; it does not appear in a float's number.
212
281
  */
213
282
  readonly chapter: number;
283
+ /**
284
+ * How many floats of each kind the earlier lessons of this part already
285
+ * used. The lesson's own floats continue from here, so a part's sequence
286
+ * runs unbroken across its chapters instead of restarting on every page.
287
+ */
288
+ readonly numbering: {
289
+ readonly equations: number;
290
+ readonly tables: number;
291
+ readonly figures: number;
292
+ readonly diagrams: number;
293
+ readonly checks: number;
294
+ };
214
295
  readonly file: string;
215
296
  readonly title: string;
216
297
  readonly description: string;
@@ -220,6 +301,11 @@ export interface ManifestLesson {
220
301
  readonly requires: readonly string[];
221
302
  readonly assumptions: readonly string[];
222
303
  readonly assessments: readonly string[];
304
+ /** Numbered knowledge checks from `assessments`, in `.checks.yml` order. */
305
+ readonly knowledgeChecks: readonly {
306
+ readonly key: string;
307
+ readonly number: string;
308
+ }[];
223
309
  readonly sources: readonly string[];
224
310
  readonly citations: readonly ManifestCitation[];
225
311
  /** Competency prerequisite edges this lesson introduces. */
@@ -239,6 +325,11 @@ export interface ManifestLesson {
239
325
  readonly key: string;
240
326
  readonly number: string;
241
327
  }[];
328
+ /** Keyed `<Diagram>` elements in appearance order. */
329
+ readonly diagrams: readonly {
330
+ readonly key: string;
331
+ readonly number: string;
332
+ }[];
242
333
  readonly notation: {
243
334
  readonly bindings: readonly {
244
335
  readonly key: string;
@@ -278,8 +369,19 @@ export interface ManifestAbout {
278
369
  readonly changelogHref?: string;
279
370
  }
280
371
 
372
+ /** One assessment item's pre-rendered `prompt` / `explanation` (v8). */
373
+ export interface ManifestAssessmentItemContent {
374
+ readonly lessonId: string;
375
+ readonly assessmentId: string;
376
+ readonly itemId: string;
377
+ readonly promptSegments: readonly ContentSegment[];
378
+ readonly explanationSegments: readonly ContentSegment[];
379
+ }
380
+
281
381
  export interface Manifest {
282
382
  readonly schemaVersion: number;
383
+ /** Normalized build/UI settings for opt-in per-lesson exports. */
384
+ readonly lessonExports: NormalizedLessonExports;
283
385
  /** Reader-facing identity + versions (the `[?]` About panel). */
284
386
  readonly about: ManifestAbout;
285
387
  readonly notation: {
@@ -293,6 +395,8 @@ export interface Manifest {
293
395
  readonly competencies: readonly Record<string, unknown>[];
294
396
  readonly sources: readonly Record<string, unknown>[];
295
397
  readonly tracks: readonly Record<string, unknown>[];
398
+ /** Abbreviations table (N14), for `remarkAbbreviations`. Ordered by key. */
399
+ readonly abbreviations: readonly Record<string, unknown>[];
296
400
  readonly sidebar: readonly SidebarGroup[];
297
401
  readonly lessons: readonly ManifestLesson[];
298
402
  /** Full competency prerequisite graph. */
@@ -313,6 +417,26 @@ export interface Manifest {
313
417
  readonly labels: readonly FigureLabelRecord[];
314
418
  readonly numbersBySlug: Record<string, Record<string, string>>;
315
419
  };
420
+ /** Keyed-float identity for `<Diagram>` — independent of figure numbering. */
421
+ readonly diagrams: {
422
+ readonly labels: readonly DiagramLabelRecord[];
423
+ readonly numbersBySlug: Record<string, Record<string, string>>;
424
+ };
425
+ /** Knowledge-check identity, derived from each lesson's `.checks.yml`. */
426
+ readonly checks: {
427
+ readonly labels: readonly KnowledgeCheckLabelRecord[];
428
+ readonly numbersBySlug: Record<string, Record<string, string>>;
429
+ };
430
+ /**
431
+ * Every assessment item's `prompt` / `explanation`, pre-split into
432
+ * text/math segments (v8). `AssessmentSet.astro` filters this by
433
+ * `lessonId` + `assessmentId` and renders each `math` segment's `latex`
434
+ * through `renderNotationMath` before handing the result to
435
+ * `AssessmentRunner`. An assessment embedded in more than one lesson (via
436
+ * more than one `checks.yml`) appears once per lesson here, since each
437
+ * lesson's scope can resolve its glyphs differently.
438
+ */
439
+ readonly assessmentContent: readonly ManifestAssessmentItemContent[];
316
440
  readonly diagnostics: {
317
441
  readonly curriculum: readonly CurriculumIssue[];
318
442
  readonly notation: readonly NotationDiagnostic[];
@@ -328,6 +452,16 @@ export interface Manifest {
328
452
  readonly tables: readonly TableDiagnostic[];
329
453
  /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
330
454
  readonly figures: readonly FigureDiagnostic[];
455
+ /** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
456
+ readonly diagrams: readonly DiagramDiagnostic[];
457
+ /** Knowledge-check identity and `[[check-…]]` reference checks. */
458
+ readonly checks: readonly KnowledgeCheckDiagnostic[];
459
+ /**
460
+ * A raw Markdown table, a raw HTML `<table>`, or an Explico `<Diagram>`
461
+ * that is unkeyed or nested outside `<Table>`/`<Figure>` — invisible to
462
+ * keyed-float validation above. Warnings only.
463
+ */
464
+ readonly rawFloats: readonly RawFloatDiagnostic[];
331
465
  };
332
466
  readonly hashes: {
333
467
  /** `sha256` of every content source file, keyed by repo-relative path. */
@@ -523,9 +657,10 @@ export async function compileManifest(
523
657
  );
524
658
  }
525
659
 
526
- const [catalog, notationInput] = await Promise.all([
660
+ const [catalog, notationInput, abbreviationsInput] = await Promise.all([
527
661
  loadCurriculumCatalog(),
528
662
  loadNotationRegistryInput(),
663
+ loadAbbreviationRegistryInput(),
529
664
  ]);
530
665
 
531
666
  const registry = buildNotationRegistry(notationInput);
@@ -545,19 +680,65 @@ export async function compileManifest(
545
680
  registry.bundles.map((bundle) => [bundle.lessonId, bundle]),
546
681
  );
547
682
 
683
+ const assessmentsInput = catalog.assessments as unknown as {
684
+ id: string;
685
+ items?: { id?: string; prompt?: string; explanation?: string }[];
686
+ }[];
687
+
548
688
  const curriculumIssues = validateCurriculum(catalog);
549
689
  const alignmentIssues = validateNotationAlignment(registry, catalog);
550
- const mathIssues = gateContentMath({
551
- notationInput,
552
- assessments: catalog.assessments as unknown as {
553
- id: string;
554
- items?: { id?: string; prompt?: string; explanation?: string }[];
555
- }[],
556
- lessonAssessments: catalog.lessons.map((lesson) => ({
557
- lessonId: lesson.id,
558
- assessmentIds: lesson.assessments,
559
- })),
560
- });
690
+ const mathIssues = [
691
+ ...gateContentMath({
692
+ notationInput,
693
+ assessments: assessmentsInput,
694
+ lessonAssessments: catalog.lessons.map((lesson) => ({
695
+ lessonId: lesson.id,
696
+ assessmentIds: lesson.assessments,
697
+ })),
698
+ }),
699
+ ...gateProseNotation(notationInput.lessons),
700
+ ...gateProseNotationInAssessments(assessmentsInput),
701
+ ...gateAbbreviations({
702
+ notationInput,
703
+ abbreviations: abbreviationsInput,
704
+ courseAbbreviations: courseConfig.abbreviations,
705
+ }),
706
+ ];
707
+
708
+ // Pre-render every assessment `prompt` / `explanation` into text/math
709
+ // segments, resolved against the scope of whichever lesson embeds it —
710
+ // same relationship, same `pageScope`, as the `gateContentMath` assessment
711
+ // check above, so a symbol that passes the gate is exactly the symbol
712
+ // `AssessmentSet.astro` renders. An assessment shared by two lessons is
713
+ // rendered once per (lesson, assessment) pair, since the two lessons can
714
+ // have different scopes and therefore different resolved LaTeX.
715
+ const notationLessonById = new Map(
716
+ notationInput.lessons.map((lesson) => [lesson.lessonId, lesson]),
717
+ );
718
+ const assessmentInputById = new Map(
719
+ assessmentsInput.map((assessment) => [assessment.id, assessment]),
720
+ );
721
+ const assessmentContent: ManifestAssessmentItemContent[] = [];
722
+ for (const lesson of catalog.lessons) {
723
+ if (lesson.assessments.length === 0) continue;
724
+ const lessonInput = notationLessonById.get(lesson.id);
725
+ if (!lessonInput) continue;
726
+ const scope = pageScope(lessonInput, cardsByKey);
727
+ for (const assessmentId of lesson.assessments) {
728
+ const assessment = assessmentInputById.get(assessmentId);
729
+ if (!assessment?.items) continue;
730
+ for (const item of assessment.items) {
731
+ if (!item.id) continue;
732
+ assessmentContent.push({
733
+ lessonId: lesson.id,
734
+ assessmentId,
735
+ itemId: item.id,
736
+ promptSegments: splitMixedContent(item.prompt ?? '', scope),
737
+ explanationSegments: splitMixedContent(item.explanation ?? '', scope),
738
+ });
739
+ }
740
+ }
741
+ }
561
742
 
562
743
  const prerequisitesById = new Map(
563
744
  catalog.competencies.map((competency) => [
@@ -586,51 +767,148 @@ export async function compileManifest(
586
767
  const sidebar = buildSidebar(
587
768
  catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
588
769
  lessonSlugs,
589
- courseConfig.sections,
770
+ coursePartsOf(courseConfig),
590
771
  );
591
772
 
592
- // A lesson's chapter is its position when the sidebar is read top to bottom
593
- // (sections in config order, lessons within a section by merged track order
594
- // then slug). It prefixes every equation / table / figure number on the page
595
- // (`(2.1)` = first equation of chapter 2). Derived from `sidebar` so there is
596
- // one lesson-ordering source, not two.
597
- const chapterByLessonId = new Map<string, number>();
598
- let chapterCursor = 0;
773
+ // Part and chapter, read straight off the sidebar so there is one
774
+ // lesson-ordering source and not two: a part is a sidebar group (parts in
775
+ // config order), a chapter is a lesson's position inside its part (merged
776
+ // track order, then slug). The part is what prefixes every numbered item —
777
+ // `(2.1)` is the first item of one independent kind in part 2 — while `n`
778
+ // continues across the part's chapters rather than restarting on each page,
779
+ // so every lesson also needs the running count that precedes it.
780
+ // See `docs/authoring/structure.md`.
781
+ const bodyByLessonId = new Map(
782
+ notationInput.lessons.map((lesson) => [lesson.lessonId, lesson.body]),
783
+ );
784
+ const placementByLessonId = new Map<
785
+ string,
786
+ {
787
+ part: number;
788
+ chapter: number;
789
+ numbering: {
790
+ equations: number;
791
+ tables: number;
792
+ figures: number;
793
+ diagrams: number;
794
+ checks: number;
795
+ };
796
+ }
797
+ >();
798
+ let partCursor = 0;
599
799
  for (const group of sidebar) {
600
- for (const item of group.items) {
601
- if (typeof item !== 'string') continue; // the static Reference group
602
- chapterByLessonId.set(lessonIdFromSlug(item), ++chapterCursor);
800
+ const lessonIds = group.items
801
+ .filter((item): item is string => typeof item === 'string')
802
+ .map(lessonIdFromSlug);
803
+ if (lessonIds.length === 0) continue; // the static Reference group
804
+ partCursor += 1;
805
+ // Running totals for this part, advanced one chapter at a time.
806
+ const used = {
807
+ equations: 0,
808
+ tables: 0,
809
+ figures: 0,
810
+ diagrams: 0,
811
+ checks: 0,
812
+ };
813
+ let chapterCursor = 0;
814
+ for (const lessonId of lessonIds) {
815
+ const body = bodyByLessonId.get(lessonId) ?? '';
816
+ placementByLessonId.set(lessonId, {
817
+ part: partCursor,
818
+ chapter: ++chapterCursor,
819
+ numbering: { ...used },
820
+ });
821
+ used.equations += countDisplayEquations(body);
822
+ used.tables += countKeyedJsxFloats(body, { tagName: 'Table' });
823
+ used.figures += countKeyedJsxFloats(body, { tagName: 'Figure' });
824
+ used.diagrams += countKeyedJsxFloats(body, { tagName: 'Diagram' });
825
+ used.checks += lessonMetaById.get(lessonId)?.assessments.length ?? 0;
603
826
  }
604
827
  }
605
828
 
606
- const keyedFloatLessonInputs = notationInput.lessons.map((lesson) => ({
607
- lessonId: lesson.lessonId,
608
- slug: lesson.lessonId.replace(/\./g, '/'),
609
- file: lesson.source.file,
610
- body: lesson.body,
611
- chapter: chapterByLessonId.get(lesson.lessonId),
612
- }));
613
- const equationValidation = validateEquations(keyedFloatLessonInputs);
829
+ const floatInputs = (
830
+ kind: 'equations' | 'tables' | 'figures' | 'diagrams',
831
+ ) =>
832
+ notationInput.lessons.map((lesson) => {
833
+ const placement = placementByLessonId.get(lesson.lessonId);
834
+ return {
835
+ lessonId: lesson.lessonId,
836
+ slug: lesson.lessonId.replace(/\./g, '/'),
837
+ file: lesson.source.file,
838
+ body: lesson.body,
839
+ ...(placement
840
+ ? {
841
+ numbering: {
842
+ prefix: placement.part,
843
+ offset: placement.numbering[kind],
844
+ },
845
+ }
846
+ : {}),
847
+ };
848
+ });
849
+ const equationValidation = validateEquations(floatInputs('equations'));
614
850
  const equationsBySlug = new Map(
615
851
  Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
616
852
  slug,
617
853
  Object.entries(table).map(([key, number]) => ({ key, number })),
618
854
  ]),
619
855
  );
620
- const tableValidation = validateTables(keyedFloatLessonInputs);
856
+ const tableValidation = validateTables(floatInputs('tables'));
621
857
  const tablesBySlug = new Map(
622
858
  Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
623
859
  slug,
624
860
  Object.entries(table).map(([key, number]) => ({ key, number })),
625
861
  ]),
626
862
  );
627
- const figureValidation = validateFigures(keyedFloatLessonInputs);
863
+ const figureValidation = validateFigures(floatInputs('figures'));
628
864
  const figuresBySlug = new Map(
629
865
  Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
630
866
  slug,
631
867
  Object.entries(table).map(([key, number]) => ({ key, number })),
632
868
  ]),
633
869
  );
870
+ const diagramValidation = validateDiagrams(floatInputs('diagrams'));
871
+ const diagramsBySlug = new Map(
872
+ Object.entries(diagramValidation.numbersBySlug).map(([slug, table]) => [
873
+ slug,
874
+ Object.entries(table).map(([key, number]) => ({ key, number })),
875
+ ]),
876
+ );
877
+ const checkValidation = validateKnowledgeChecks(
878
+ notationInput.lessons.map((lesson) => {
879
+ const placement = placementByLessonId.get(lesson.lessonId);
880
+ return {
881
+ lessonId: lesson.lessonId,
882
+ slug: lesson.lessonId.replace(/\./g, '/'),
883
+ file: lesson.source.file,
884
+ body: lesson.body,
885
+ assessmentIds:
886
+ lessonMetaById.get(lesson.lessonId)?.assessments ?? [],
887
+ ...(placement
888
+ ? {
889
+ numbering: {
890
+ prefix: placement.part,
891
+ offset: placement.numbering.checks,
892
+ },
893
+ }
894
+ : {}),
895
+ };
896
+ }),
897
+ );
898
+ const checksBySlug = new Map(
899
+ Object.entries(checkValidation.numbersBySlug).map(([slug, table]) => [
900
+ slug,
901
+ Object.entries(table).map(([key, number]) => ({ key, number })),
902
+ ]),
903
+ );
904
+ const rawFloatDiagnostics = validateRawFloats(
905
+ notationInput.lessons.map((lesson) => ({
906
+ lessonId: lesson.lessonId,
907
+ slug: lesson.lessonId.replace(/\./g, '/'),
908
+ file: lesson.source.file,
909
+ body: lesson.body,
910
+ })),
911
+ );
634
912
 
635
913
  const lessons: ManifestLesson[] = [];
636
914
  for (const notationLesson of notationInput.lessons) {
@@ -645,10 +923,18 @@ export async function compileManifest(
645
923
  lessons.push({
646
924
  id,
647
925
  slug: id.replace(/\./g, '/'),
648
- // 0 = the lesson is not placed in any configured section; a curriculum
926
+ // 0 = the lesson is not placed in any configured part; a curriculum
649
927
  // diagnostic covers that separately, and both numbering paths then fall
650
928
  // back to the legacy `##`-section scheme.
651
- chapter: chapterByLessonId.get(id) ?? 0,
929
+ part: placementByLessonId.get(id)?.part ?? 0,
930
+ chapter: placementByLessonId.get(id)?.chapter ?? 0,
931
+ numbering: placementByLessonId.get(id)?.numbering ?? {
932
+ equations: 0,
933
+ tables: 0,
934
+ figures: 0,
935
+ diagrams: 0,
936
+ checks: 0,
937
+ },
652
938
  file: notationLesson.source.file,
653
939
  title: frontmatter.title,
654
940
  description: frontmatter.description,
@@ -657,11 +943,13 @@ export async function compileManifest(
657
943
  requires: [...meta.requires],
658
944
  assumptions: [...meta.assumptions],
659
945
  assessments: [...meta.assessments],
946
+ knowledgeChecks: checksBySlug.get(id.replace(/\./g, '/')) ?? [],
660
947
  sources: deriveSources(notationLesson.body),
661
948
  citations: lessonCitations(notationLesson.body),
662
949
  equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
663
950
  tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
664
951
  figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
952
+ diagrams: diagramsBySlug.get(id.replace(/\./g, '/')) ?? [],
665
953
  prereqEdges: sortEdges(
666
954
  teaches.flatMap((to) =>
667
955
  (prerequisitesById.get(to) ?? [])
@@ -693,6 +981,9 @@ export async function compileManifest(
693
981
  ...(courseConfig.operators === undefined
694
982
  ? {}
695
983
  : { operators: courseConfig.operators }),
984
+ ...(courseConfig.romanLabels === undefined
985
+ ? {}
986
+ : { romanLabels: courseConfig.romanLabels }),
696
987
  });
697
988
 
698
989
  const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
@@ -703,6 +994,15 @@ export async function compileManifest(
703
994
  .sort(byId);
704
995
  const sources = [...loadSourceRecords()].sort(byId);
705
996
  const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
997
+ const abbreviations = abbreviationsInput
998
+ .map((entry) => ({
999
+ key: entry.key,
1000
+ abbr: entry.abbr,
1001
+ expansion: entry.expansion,
1002
+ aliases: entry.aliases,
1003
+ ...(entry.domain === undefined ? {} : { domain: entry.domain }),
1004
+ }))
1005
+ .sort((a, b) => a.key.localeCompare(b.key));
706
1006
 
707
1007
  const hashes = contentHashes();
708
1008
 
@@ -751,6 +1051,7 @@ export async function compileManifest(
751
1051
 
752
1052
  return {
753
1053
  schemaVersion: MANIFEST_SCHEMA_VERSION,
1054
+ lessonExports: normalizeLessonExports(courseConfig),
754
1055
  about,
755
1056
  notation: {
756
1057
  definitions: registry.definitions,
@@ -762,6 +1063,7 @@ export async function compileManifest(
762
1063
  competencies,
763
1064
  sources,
764
1065
  tracks,
1066
+ abbreviations,
765
1067
  sidebar,
766
1068
  lessons,
767
1069
  prereqEdges,
@@ -777,6 +1079,15 @@ export async function compileManifest(
777
1079
  labels: figureValidation.labels,
778
1080
  numbersBySlug: figureValidation.numbersBySlug,
779
1081
  },
1082
+ diagrams: {
1083
+ labels: diagramValidation.labels,
1084
+ numbersBySlug: diagramValidation.numbersBySlug,
1085
+ },
1086
+ checks: {
1087
+ labels: checkValidation.labels,
1088
+ numbersBySlug: checkValidation.numbersBySlug,
1089
+ },
1090
+ assessmentContent,
780
1091
  diagnostics: {
781
1092
  curriculum: curriculumIssues,
782
1093
  notation: registry.diagnostics,
@@ -786,7 +1097,10 @@ export async function compileManifest(
786
1097
  equations: equationValidation.diagnostics,
787
1098
  tables: tableValidation.diagnostics,
788
1099
  figures: figureValidation.diagnostics,
1100
+ diagrams: diagramValidation.diagnostics,
1101
+ checks: checkValidation.diagnostics,
789
1102
  knowledge: knowledge.diagnostics,
1103
+ rawFloats: rawFloatDiagnostics,
790
1104
  },
791
1105
  hashes,
792
1106
  };
@@ -842,13 +1156,17 @@ export function readManifestSync(): Manifest {
842
1156
  export async function loadManifest(
843
1157
  courseConfig?: CourseConfig,
844
1158
  ): Promise<Manifest> {
845
- if (existsSync(MANIFEST_PATH)) return readManifestSync();
846
- if (!courseConfig) {
847
- throw new Error(
848
- `${relative(REPOSITORY_ROOT, MANIFEST_PATH)} not found. Run \`pnpm validate:content\` first.`,
849
- );
1159
+ // A caller that has the course config is an authoring/build entry point. It
1160
+ // must compile the current content tree even when a persisted manifest is
1161
+ // present: using that file here can split the page in two, with remark
1162
+ // seeing current frontmatter while Astro components render stale cards.
1163
+ if (courseConfig) {
1164
+ const manifest = await compileManifest(courseConfig);
1165
+ writeManifest(manifest);
1166
+ return manifest;
850
1167
  }
851
- const manifest = await compileManifest(courseConfig);
852
- writeManifest(manifest);
853
- return manifest;
1168
+ if (existsSync(MANIFEST_PATH)) return readManifestSync();
1169
+ throw new Error(
1170
+ `${relative(REPOSITORY_ROOT, MANIFEST_PATH)} not found. Run \`pnpm validate:content\` first.`,
1171
+ );
854
1172
  }