explico 0.4.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 (143) hide show
  1. package/CHANGELOG.md +273 -1
  2. package/README.md +103 -8
  3. package/dist/compiler/astro.d.ts +9 -1
  4. package/dist/compiler/astro.js +182 -3
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +28 -4
  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 +58 -1
  10. package/dist/compiler/course-config.js +28 -1
  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/manifest.d.ts +90 -6
  14. package/dist/compiler/manifest.js +150 -17
  15. package/dist/compiler/resolution-report.d.ts +4 -0
  16. package/dist/reference/base-abbreviations.d.mts +30 -0
  17. package/dist/reference/base-abbreviations.mjs +60 -0
  18. package/dist/reference/base-operators.d.mts +15 -0
  19. package/dist/reference/base-operators.mjs +40 -0
  20. package/dist/reference/base-roman-labels.d.mts +35 -0
  21. package/dist/reference/base-roman-labels.mjs +43 -0
  22. package/dist/reference/consistency.d.ts +8 -2
  23. package/dist/reference/consistency.js +50 -3
  24. package/dist/reference/diagrams-validate.d.ts +21 -0
  25. package/dist/reference/diagrams-validate.js +13 -0
  26. package/dist/reference/equations.d.mts +10 -15
  27. package/dist/reference/equations.mjs +67 -14
  28. package/dist/reference/gate-abbreviations.d.ts +17 -0
  29. package/dist/reference/gate-abbreviations.js +64 -0
  30. package/dist/reference/gate-math.d.ts +28 -5
  31. package/dist/reference/gate-math.js +5 -2
  32. package/dist/reference/gate-prose-notation.d.ts +60 -0
  33. package/dist/reference/gate-prose-notation.js +125 -0
  34. package/dist/reference/keyed-float-validate.d.ts +5 -5
  35. package/dist/reference/keyed-jsx-floats.d.mts +7 -12
  36. package/dist/reference/keyed-jsx-floats.mjs +50 -25
  37. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  38. package/dist/reference/knowledge-checks-validate.js +78 -0
  39. package/dist/reference/knowledge-checks.d.mts +25 -0
  40. package/dist/reference/knowledge-checks.mjs +45 -0
  41. package/dist/reference/lab-math-scope.d.ts +9 -1
  42. package/dist/reference/lab-math-scope.js +32 -4
  43. package/dist/reference/math-glyphs.d.mts +30 -0
  44. package/dist/reference/math-glyphs.mjs +367 -0
  45. package/dist/reference/mdx-attributes.d.mts +13 -0
  46. package/dist/reference/mdx-attributes.mjs +38 -0
  47. package/dist/reference/mixed-content.d.ts +17 -0
  48. package/dist/reference/mixed-content.js +29 -0
  49. package/dist/reference/page-notation.d.ts +52 -0
  50. package/dist/reference/page-notation.js +171 -0
  51. package/dist/reference/raw-floats-validate.d.ts +18 -0
  52. package/dist/reference/raw-floats-validate.js +53 -0
  53. package/dist/reference/raw-floats.d.mts +18 -0
  54. package/dist/reference/raw-floats.mjs +129 -0
  55. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  56. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  57. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  58. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  59. package/dist/reference/remark-abbreviations.d.mts +7 -0
  60. package/dist/reference/remark-abbreviations.mjs +174 -0
  61. package/dist/reference/remark-memo.d.mts +16 -0
  62. package/dist/reference/remark-memo.mjs +339 -0
  63. package/dist/reference/remark-notation.d.mts +57 -9
  64. package/dist/reference/remark-notation.mjs +354 -42
  65. package/dist/reference/remark-short-note.d.mts +4 -0
  66. package/dist/reference/remark-short-note.mjs +237 -0
  67. package/dist/reference/types.d.ts +16 -1
  68. package/dist/reference/units-vocab.js +4 -0
  69. package/docs/VERSIONING.md +33 -4
  70. package/docs/authoring/README.md +8 -0
  71. package/docs/authoring/agents.md +17 -2
  72. package/docs/authoring/components.md +135 -0
  73. package/docs/authoring/migrating.md +92 -7
  74. package/docs/authoring/notation.md +560 -1
  75. package/docs/authoring/structure.md +120 -8
  76. package/docs/authoring/visualizations.md +36 -3
  77. package/package.json +5 -2
  78. package/src/compiler/astro.ts +208 -3
  79. package/src/compiler/cli.ts +29 -2
  80. package/src/compiler/collections.ts +45 -4
  81. package/src/compiler/course-config.ts +92 -0
  82. package/src/compiler/exports/standalone-html.ts +963 -0
  83. package/src/compiler/manifest.ts +268 -22
  84. package/src/components/HoverPanel.astro +45 -4
  85. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  86. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  87. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  88. package/src/components/assessments/AssessmentSet.astro +98 -14
  89. package/src/components/assessments/RenderedContent.tsx +55 -0
  90. package/src/components/assessments/assessment-randomization.ts +16 -0
  91. package/src/components/diagram/Diagram.astro +444 -83
  92. package/src/components/disclosure/hover-preview.ts +202 -0
  93. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  94. package/src/components/examples/CompactExamples.astro +2 -0
  95. package/src/components/figure/Figure.astro +279 -10
  96. package/src/components/memo/Memo.astro +453 -0
  97. package/src/components/memo/ShortNote.astro +223 -0
  98. package/src/components/notation/NotationFormula.astro +18 -1
  99. package/src/components/notation/NotationLayer.astro +105 -8
  100. package/src/components/notation/render-notation-math.ts +39 -2
  101. package/src/components/notation/safe-math-to-react.ts +50 -0
  102. package/src/components/reference/FloatRefLayer.astro +233 -15
  103. package/src/components/starlight/CollapsibleSections.astro +461 -0
  104. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  105. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  106. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  107. package/src/components/starlight/LayoutHeader.astro +50 -0
  108. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  109. package/src/components/starlight/LessonExportActions.astro +436 -0
  110. package/src/components/starlight/LessonFooter.astro +27 -2
  111. package/src/components/starlight/section-parts.ts +31 -0
  112. package/src/components/table/Table.astro +293 -7
  113. package/src/components/table/TableOverflowExpander.astro +489 -0
  114. package/src/content-config.ts +20 -0
  115. package/src/reference/base-abbreviations.mjs +60 -0
  116. package/src/reference/base-operators.mjs +40 -0
  117. package/src/reference/base-roman-labels.mjs +43 -0
  118. package/src/reference/consistency.ts +73 -2
  119. package/src/reference/diagrams-validate.ts +40 -0
  120. package/src/reference/equations.mjs +67 -14
  121. package/src/reference/gate-abbreviations.ts +128 -0
  122. package/src/reference/gate-math.ts +27 -7
  123. package/src/reference/gate-prose-notation.ts +224 -0
  124. package/src/reference/keyed-float-validate.ts +5 -5
  125. package/src/reference/keyed-jsx-floats.mjs +50 -25
  126. package/src/reference/knowledge-checks-validate.ts +130 -0
  127. package/src/reference/knowledge-checks.mjs +45 -0
  128. package/src/reference/lab-math-scope.ts +45 -3
  129. package/src/reference/math-glyphs.mjs +367 -0
  130. package/src/reference/mdx-attributes.mjs +38 -0
  131. package/src/reference/mixed-content.ts +69 -0
  132. package/src/reference/page-notation.ts +252 -0
  133. package/src/reference/raw-floats-validate.ts +103 -0
  134. package/src/reference/raw-floats.mjs +129 -0
  135. package/src/reference/rehype-heading-anchors.mjs +107 -1
  136. package/src/reference/rehype-table-header-labels.mjs +152 -0
  137. package/src/reference/remark-abbreviations.mjs +174 -0
  138. package/src/reference/remark-memo.mjs +339 -0
  139. package/src/reference/remark-notation.mjs +354 -42
  140. package/src/reference/remark-short-note.mjs +237 -0
  141. package/src/reference/types.ts +16 -0
  142. package/src/reference/units-vocab.ts +4 -0
  143. package/src/styles/global.css +996 -50
@@ -64,10 +64,34 @@ import {
64
64
  type FigureDiagnostic,
65
65
  type FigureLabelRecord,
66
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';
67
81
  import {
68
82
  gateContentMath,
83
+ pageScope,
69
84
  type MathGateDiagnostic,
70
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';
71
95
  import { loadLintIgnore } from '../reference/lint-ignore';
72
96
  import { buildNotationRegistry } from '../reference/registry';
73
97
  import { formulaGlyphScope } from '../reference/gloss';
@@ -82,12 +106,18 @@ import type {
82
106
  NotationLessonInput,
83
107
  NotationPageBundle,
84
108
  } from '../reference/types';
85
- import { coursePartsOf, type CourseConfig } from './course-config';
109
+ import {
110
+ coursePartsOf,
111
+ normalizeLessonExports,
112
+ type CourseConfig,
113
+ type NormalizedLessonExports,
114
+ } from './course-config';
86
115
  import {
87
116
  resolveKnowledgeVersion,
88
117
  type KnowledgeVersionDiagnostic,
89
118
  } from './knowledge-version';
90
119
  import {
120
+ loadAbbreviationRegistryInput,
91
121
  loadCurriculumCatalog,
92
122
  loadNotationDefinitions,
93
123
  loadNotationRegistryInput,
@@ -112,7 +142,7 @@ import {
112
142
  * `lessons[].equations`, and `diagnostics.equations`.
113
143
  * v4 (F-tables-figures): `tables`/`figures` (same shape as `equations`, for
114
144
  * `<Table>`/`<Figure>`), `lessons[].tables`/`lessons[].figures`, and
115
- * `diagnostics.tables`/`diagnostics.figures`.
145
+ * `diagnostics.tables`/`diagnostics.figures`/`diagnostics.diagrams`.
116
146
  * v5 (about): `about` (engine identity from `explico`'s `package.json`, the
117
147
  * course's reader-facing `about` block, and the knowledge version resolved
118
148
  * against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
@@ -122,8 +152,33 @@ import {
122
152
  * now the number before the dot), `lessons[].chapter` redefined as the
123
153
  * lesson's ordinal *within its part*, and `lessons[].numbering` — the
124
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).
125
180
  */
126
- export const MANIFEST_SCHEMA_VERSION = 7;
181
+ export const MANIFEST_SCHEMA_VERSION = 13;
127
182
 
128
183
  const REPOSITORY_ROOT = process.cwd();
129
184
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -214,8 +269,9 @@ export interface ManifestLesson {
214
269
  /**
215
270
  * The 1-based ordinal of the **part** this lesson sits in — its sidebar
216
271
  * group's position in `courseConfig.parts`. It is the number before the dot
217
- * in every equation, table, and figure number on the page: `(2.1)` is the
218
- * first equation of part 2. See `docs/authoring/structure.md`.
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`.
219
275
  */
220
276
  readonly part: number;
221
277
  /**
@@ -233,6 +289,8 @@ export interface ManifestLesson {
233
289
  readonly equations: number;
234
290
  readonly tables: number;
235
291
  readonly figures: number;
292
+ readonly diagrams: number;
293
+ readonly checks: number;
236
294
  };
237
295
  readonly file: string;
238
296
  readonly title: string;
@@ -243,6 +301,11 @@ export interface ManifestLesson {
243
301
  readonly requires: readonly string[];
244
302
  readonly assumptions: readonly string[];
245
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
+ }[];
246
309
  readonly sources: readonly string[];
247
310
  readonly citations: readonly ManifestCitation[];
248
311
  /** Competency prerequisite edges this lesson introduces. */
@@ -262,6 +325,11 @@ export interface ManifestLesson {
262
325
  readonly key: string;
263
326
  readonly number: string;
264
327
  }[];
328
+ /** Keyed `<Diagram>` elements in appearance order. */
329
+ readonly diagrams: readonly {
330
+ readonly key: string;
331
+ readonly number: string;
332
+ }[];
265
333
  readonly notation: {
266
334
  readonly bindings: readonly {
267
335
  readonly key: string;
@@ -301,8 +369,19 @@ export interface ManifestAbout {
301
369
  readonly changelogHref?: string;
302
370
  }
303
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
+
304
381
  export interface Manifest {
305
382
  readonly schemaVersion: number;
383
+ /** Normalized build/UI settings for opt-in per-lesson exports. */
384
+ readonly lessonExports: NormalizedLessonExports;
306
385
  /** Reader-facing identity + versions (the `[?]` About panel). */
307
386
  readonly about: ManifestAbout;
308
387
  readonly notation: {
@@ -316,6 +395,8 @@ export interface Manifest {
316
395
  readonly competencies: readonly Record<string, unknown>[];
317
396
  readonly sources: readonly Record<string, unknown>[];
318
397
  readonly tracks: readonly Record<string, unknown>[];
398
+ /** Abbreviations table (N14), for `remarkAbbreviations`. Ordered by key. */
399
+ readonly abbreviations: readonly Record<string, unknown>[];
319
400
  readonly sidebar: readonly SidebarGroup[];
320
401
  readonly lessons: readonly ManifestLesson[];
321
402
  /** Full competency prerequisite graph. */
@@ -336,6 +417,26 @@ export interface Manifest {
336
417
  readonly labels: readonly FigureLabelRecord[];
337
418
  readonly numbersBySlug: Record<string, Record<string, string>>;
338
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[];
339
440
  readonly diagnostics: {
340
441
  readonly curriculum: readonly CurriculumIssue[];
341
442
  readonly notation: readonly NotationDiagnostic[];
@@ -351,6 +452,16 @@ export interface Manifest {
351
452
  readonly tables: readonly TableDiagnostic[];
352
453
  /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
353
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[];
354
465
  };
355
466
  readonly hashes: {
356
467
  /** `sha256` of every content source file, keyed by repo-relative path. */
@@ -546,9 +657,10 @@ export async function compileManifest(
546
657
  );
547
658
  }
548
659
 
549
- const [catalog, notationInput] = await Promise.all([
660
+ const [catalog, notationInput, abbreviationsInput] = await Promise.all([
550
661
  loadCurriculumCatalog(),
551
662
  loadNotationRegistryInput(),
663
+ loadAbbreviationRegistryInput(),
552
664
  ]);
553
665
 
554
666
  const registry = buildNotationRegistry(notationInput);
@@ -568,19 +680,65 @@ export async function compileManifest(
568
680
  registry.bundles.map((bundle) => [bundle.lessonId, bundle]),
569
681
  );
570
682
 
683
+ const assessmentsInput = catalog.assessments as unknown as {
684
+ id: string;
685
+ items?: { id?: string; prompt?: string; explanation?: string }[];
686
+ }[];
687
+
571
688
  const curriculumIssues = validateCurriculum(catalog);
572
689
  const alignmentIssues = validateNotationAlignment(registry, catalog);
573
- const mathIssues = gateContentMath({
574
- notationInput,
575
- assessments: catalog.assessments as unknown as {
576
- id: string;
577
- items?: { id?: string; prompt?: string; explanation?: string }[];
578
- }[],
579
- lessonAssessments: catalog.lessons.map((lesson) => ({
580
- lessonId: lesson.id,
581
- assessmentIds: lesson.assessments,
582
- })),
583
- });
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
+ }
584
742
 
585
743
  const prerequisitesById = new Map(
586
744
  catalog.competencies.map((competency) => [
@@ -615,8 +773,8 @@ export async function compileManifest(
615
773
  // Part and chapter, read straight off the sidebar so there is one
616
774
  // lesson-ordering source and not two: a part is a sidebar group (parts in
617
775
  // config order), a chapter is a lesson's position inside its part (merged
618
- // track order, then slug). The part is what prefixes every equation / table
619
- // / figure number — `(2.1)` is the first equation of part 2 — while `n`
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`
620
778
  // continues across the part's chapters rather than restarting on each page,
621
779
  // so every lesson also needs the running count that precedes it.
622
780
  // See `docs/authoring/structure.md`.
@@ -628,7 +786,13 @@ export async function compileManifest(
628
786
  {
629
787
  part: number;
630
788
  chapter: number;
631
- numbering: { equations: number; tables: number; figures: number };
789
+ numbering: {
790
+ equations: number;
791
+ tables: number;
792
+ figures: number;
793
+ diagrams: number;
794
+ checks: number;
795
+ };
632
796
  }
633
797
  >();
634
798
  let partCursor = 0;
@@ -639,7 +803,13 @@ export async function compileManifest(
639
803
  if (lessonIds.length === 0) continue; // the static Reference group
640
804
  partCursor += 1;
641
805
  // Running totals for this part, advanced one chapter at a time.
642
- const used = { equations: 0, tables: 0, figures: 0 };
806
+ const used = {
807
+ equations: 0,
808
+ tables: 0,
809
+ figures: 0,
810
+ diagrams: 0,
811
+ checks: 0,
812
+ };
643
813
  let chapterCursor = 0;
644
814
  for (const lessonId of lessonIds) {
645
815
  const body = bodyByLessonId.get(lessonId) ?? '';
@@ -651,10 +821,14 @@ export async function compileManifest(
651
821
  used.equations += countDisplayEquations(body);
652
822
  used.tables += countKeyedJsxFloats(body, { tagName: 'Table' });
653
823
  used.figures += countKeyedJsxFloats(body, { tagName: 'Figure' });
824
+ used.diagrams += countKeyedJsxFloats(body, { tagName: 'Diagram' });
825
+ used.checks += lessonMetaById.get(lessonId)?.assessments.length ?? 0;
654
826
  }
655
827
  }
656
828
 
657
- const floatInputs = (kind: 'equations' | 'tables' | 'figures') =>
829
+ const floatInputs = (
830
+ kind: 'equations' | 'tables' | 'figures' | 'diagrams',
831
+ ) =>
658
832
  notationInput.lessons.map((lesson) => {
659
833
  const placement = placementByLessonId.get(lesson.lessonId);
660
834
  return {
@@ -693,6 +867,48 @@ export async function compileManifest(
693
867
  Object.entries(table).map(([key, number]) => ({ key, number })),
694
868
  ]),
695
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
+ );
696
912
 
697
913
  const lessons: ManifestLesson[] = [];
698
914
  for (const notationLesson of notationInput.lessons) {
@@ -716,6 +932,8 @@ export async function compileManifest(
716
932
  equations: 0,
717
933
  tables: 0,
718
934
  figures: 0,
935
+ diagrams: 0,
936
+ checks: 0,
719
937
  },
720
938
  file: notationLesson.source.file,
721
939
  title: frontmatter.title,
@@ -725,11 +943,13 @@ export async function compileManifest(
725
943
  requires: [...meta.requires],
726
944
  assumptions: [...meta.assumptions],
727
945
  assessments: [...meta.assessments],
946
+ knowledgeChecks: checksBySlug.get(id.replace(/\./g, '/')) ?? [],
728
947
  sources: deriveSources(notationLesson.body),
729
948
  citations: lessonCitations(notationLesson.body),
730
949
  equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
731
950
  tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
732
951
  figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
952
+ diagrams: diagramsBySlug.get(id.replace(/\./g, '/')) ?? [],
733
953
  prereqEdges: sortEdges(
734
954
  teaches.flatMap((to) =>
735
955
  (prerequisitesById.get(to) ?? [])
@@ -761,6 +981,9 @@ export async function compileManifest(
761
981
  ...(courseConfig.operators === undefined
762
982
  ? {}
763
983
  : { operators: courseConfig.operators }),
984
+ ...(courseConfig.romanLabels === undefined
985
+ ? {}
986
+ : { romanLabels: courseConfig.romanLabels }),
764
987
  });
765
988
 
766
989
  const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
@@ -771,6 +994,15 @@ export async function compileManifest(
771
994
  .sort(byId);
772
995
  const sources = [...loadSourceRecords()].sort(byId);
773
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));
774
1006
 
775
1007
  const hashes = contentHashes();
776
1008
 
@@ -819,6 +1051,7 @@ export async function compileManifest(
819
1051
 
820
1052
  return {
821
1053
  schemaVersion: MANIFEST_SCHEMA_VERSION,
1054
+ lessonExports: normalizeLessonExports(courseConfig),
822
1055
  about,
823
1056
  notation: {
824
1057
  definitions: registry.definitions,
@@ -830,6 +1063,7 @@ export async function compileManifest(
830
1063
  competencies,
831
1064
  sources,
832
1065
  tracks,
1066
+ abbreviations,
833
1067
  sidebar,
834
1068
  lessons,
835
1069
  prereqEdges,
@@ -845,6 +1079,15 @@ export async function compileManifest(
845
1079
  labels: figureValidation.labels,
846
1080
  numbersBySlug: figureValidation.numbersBySlug,
847
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,
848
1091
  diagnostics: {
849
1092
  curriculum: curriculumIssues,
850
1093
  notation: registry.diagnostics,
@@ -854,7 +1097,10 @@ export async function compileManifest(
854
1097
  equations: equationValidation.diagnostics,
855
1098
  tables: tableValidation.diagnostics,
856
1099
  figures: figureValidation.diagnostics,
1100
+ diagrams: diagramValidation.diagnostics,
1101
+ checks: checkValidation.diagnostics,
857
1102
  knowledge: knowledge.diagnostics,
1103
+ rawFloats: rawFloatDiagnostics,
858
1104
  },
859
1105
  hashes,
860
1106
  };
@@ -50,7 +50,9 @@ const { id, label, class: className, ...rest } = Astro.props;
50
50
  >
51
51
  <span aria-hidden="true">&times;</span>
52
52
  </button>
53
- <slot />
53
+ <div class="hover-panel-scroll">
54
+ <slot />
55
+ </div>
54
56
  </aside>
55
57
 
56
58
  <style>
@@ -63,9 +65,15 @@ const { id, label, class: className, ...rest } = Astro.props;
63
65
  JS placement (which clears display equations) must start from zero. */
64
66
  margin: 0;
65
67
  width: min(23rem, calc(100vw - 1.5rem));
66
- overflow: auto;
67
- padding: 1rem;
68
- padding-inline-end: 2.4rem;
68
+ display: flex;
69
+ flex-direction: column;
70
+ /* The scrolling lives on `.hover-panel-scroll` below, not here — the
71
+ close button is an absolutely-positioned sibling of that scroller,
72
+ so it stays put when the content scrolls instead of scrolling away
73
+ with it (which is what happens to an absolutely-positioned child of
74
+ the scroll container itself). `hidden` also keeps the rounded
75
+ corners clean around the inner scroller. */
76
+ overflow: hidden;
69
77
  border: 1px solid var(--sl-color-gray-5);
70
78
  border-top: 0.25rem solid var(--sl-color-accent);
71
79
  border-radius: 0.65rem;
@@ -75,6 +83,39 @@ const { id, label, class: className, ...rest } = Astro.props;
75
83
  font-size: var(--sl-text-sm);
76
84
  }
77
85
 
86
+ /* `.hover-panel { display: flex }` above has the same specificity as the
87
+ UA stylesheet's `[hidden] { display: none }` and comes later in the
88
+ cascade, so it would otherwise win and defeat the `hidden` attribute
89
+ that every layer's JS relies on to actually hide the panel. */
90
+ .hover-panel[hidden] {
91
+ display: none;
92
+ }
93
+
94
+ /* The equation/table/figure preview card (`FloatRefLayer.astro`) is the
95
+ one panel whose content size varies wildly — a short reference vs. a
96
+ wide equation — so it grows to fit instead of staying pinned at the
97
+ default width. `--float-ref-max-width` is computed in JS from the live
98
+ layout (the room between the left sidebar and the viewport edge); the
99
+ notation and citation panels never set it and keep the fixed default. */
100
+ .hover-panel[data-float-ref-panel] {
101
+ width: fit-content;
102
+ min-width: min(23rem, var(--float-ref-max-width, 100vw));
103
+ max-width: var(--float-ref-max-width, min(23rem, calc(100vw - 1.5rem)));
104
+ }
105
+
106
+ .hover-panel-scroll {
107
+ overflow: auto;
108
+ /* Without these, this flex item's default `auto` minimum size is its
109
+ *content's* size, not the panel's — content wider or taller than the
110
+ panel would inflate the panel itself outward instead of scrolling
111
+ inside it (the same flexbox trap `.float-ref-panel-content` guards
112
+ against with its own `min-width: 0`). */
113
+ min-width: 0;
114
+ min-height: 0;
115
+ padding: 1rem;
116
+ padding-inline-end: 2.4rem;
117
+ }
118
+
78
119
  .hover-panel-close {
79
120
  position: absolute;
80
121
  top: 0.5rem;
@@ -0,0 +1,70 @@
1
+ ---
2
+ /**
3
+ * A plain, alphabetical listing of the `abbreviations` collection (rule N14)
4
+ * — deliberately the simplest possible reader-facing view of this content.
5
+ * Unlike `NotationGlossary.astro`, there is no hover popup, no formula
6
+ * rendering, and no `NotationLayer`: an abbreviation card is a name
7
+ * resolution only, so a definition list is the whole job.
8
+ */
9
+ import { getCollection } from 'astro:content';
10
+
11
+ const entries = (await getCollection('abbreviations')).sort((left, right) =>
12
+ left.data.abbr.localeCompare(right.data.abbr),
13
+ );
14
+ ---
15
+
16
+ {
17
+ entries.length === 0 ? (
18
+ <p class="abbreviation-glossary-empty">
19
+ No abbreviations are catalogued yet.
20
+ </p>
21
+ ) : (
22
+ <dl class="abbreviation-glossary">
23
+ {entries.map((entry) => (
24
+ <div class="abbreviation-entry" id={`abbr-${entry.data.key}`}>
25
+ <dt>
26
+ {entry.data.abbr}
27
+ {entry.data.aliases.length > 0 && (
28
+ <span class="abbreviation-aliases">
29
+ {' '}
30
+ (also {entry.data.aliases.join(', ')})
31
+ </span>
32
+ )}
33
+ </dt>
34
+ <dd>{entry.data.expansion}</dd>
35
+ </div>
36
+ ))}
37
+ </dl>
38
+ )
39
+ }
40
+
41
+ <style>
42
+ .abbreviation-glossary {
43
+ display: grid;
44
+ grid-template-columns: max-content 1fr;
45
+ column-gap: 1rem;
46
+ row-gap: 0.75rem;
47
+ }
48
+
49
+ .abbreviation-entry {
50
+ display: contents;
51
+ }
52
+
53
+ .abbreviation-entry dt {
54
+ font-weight: 600;
55
+ white-space: nowrap;
56
+ }
57
+
58
+ .abbreviation-entry dd {
59
+ margin: 0;
60
+ }
61
+
62
+ .abbreviation-aliases {
63
+ font-weight: 400;
64
+ color: var(--sl-color-gray-3);
65
+ }
66
+
67
+ .abbreviation-glossary-empty {
68
+ color: var(--sl-color-gray-3);
69
+ }
70
+ </style>