explico 0.4.0 → 0.6.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 (150) hide show
  1. package/CHANGELOG.md +382 -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 +88 -1
  10. package/dist/compiler/course-config.js +35 -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 +98 -6
  14. package/dist/compiler/manifest.js +154 -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-heading-length.d.ts +12 -0
  31. package/dist/reference/gate-heading-length.js +79 -0
  32. package/dist/reference/gate-math.d.ts +32 -5
  33. package/dist/reference/gate-math.js +6 -2
  34. package/dist/reference/gate-prose-notation.d.ts +60 -0
  35. package/dist/reference/gate-prose-notation.js +125 -0
  36. package/dist/reference/keyed-float-validate.d.ts +5 -5
  37. package/dist/reference/keyed-jsx-floats.d.mts +7 -12
  38. package/dist/reference/keyed-jsx-floats.mjs +50 -25
  39. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  40. package/dist/reference/knowledge-checks-validate.js +78 -0
  41. package/dist/reference/knowledge-checks.d.mts +25 -0
  42. package/dist/reference/knowledge-checks.mjs +45 -0
  43. package/dist/reference/lab-math-scope.d.ts +9 -1
  44. package/dist/reference/lab-math-scope.js +32 -4
  45. package/dist/reference/math-glyphs.d.mts +42 -0
  46. package/dist/reference/math-glyphs.mjs +548 -40
  47. package/dist/reference/mdx-attributes.d.mts +13 -0
  48. package/dist/reference/mdx-attributes.mjs +38 -0
  49. package/dist/reference/mixed-content.d.ts +17 -0
  50. package/dist/reference/mixed-content.js +29 -0
  51. package/dist/reference/page-notation.d.ts +52 -0
  52. package/dist/reference/page-notation.js +171 -0
  53. package/dist/reference/raw-floats-validate.d.ts +18 -0
  54. package/dist/reference/raw-floats-validate.js +53 -0
  55. package/dist/reference/raw-floats.d.mts +18 -0
  56. package/dist/reference/raw-floats.mjs +129 -0
  57. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  58. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  59. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  60. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  61. package/dist/reference/remark-abbreviations.d.mts +7 -0
  62. package/dist/reference/remark-abbreviations.mjs +174 -0
  63. package/dist/reference/remark-citation.d.mts +5 -1
  64. package/dist/reference/remark-citation.mjs +0 -0
  65. package/dist/reference/remark-memo.d.mts +16 -0
  66. package/dist/reference/remark-memo.mjs +339 -0
  67. package/dist/reference/remark-notation.d.mts +57 -9
  68. package/dist/reference/remark-notation.mjs +354 -42
  69. package/dist/reference/remark-short-note.d.mts +4 -0
  70. package/dist/reference/remark-short-note.mjs +237 -0
  71. package/dist/reference/types.d.ts +16 -1
  72. package/dist/reference/units-vocab.js +4 -0
  73. package/docs/VERSIONING.md +53 -4
  74. package/docs/authoring/README.md +10 -0
  75. package/docs/authoring/agents.md +17 -2
  76. package/docs/authoring/components.md +138 -0
  77. package/docs/authoring/migrating.md +275 -7
  78. package/docs/authoring/notation.md +615 -1
  79. package/docs/authoring/structure-rules.md +69 -0
  80. package/docs/authoring/structure.md +120 -8
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +208 -3
  84. package/src/compiler/cli.ts +29 -2
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +132 -0
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/manifest.ts +281 -22
  89. package/src/components/HoverPanel.astro +45 -4
  90. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  91. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  92. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  93. package/src/components/assessments/AssessmentSet.astro +98 -14
  94. package/src/components/assessments/RenderedContent.tsx +55 -0
  95. package/src/components/assessments/assessment-randomization.ts +16 -0
  96. package/src/components/diagram/Diagram.astro +409 -103
  97. package/src/components/disclosure/hover-preview.ts +202 -0
  98. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  99. package/src/components/examples/CompactExamples.astro +2 -0
  100. package/src/components/figure/Figure.astro +279 -10
  101. package/src/components/memo/Memo.astro +453 -0
  102. package/src/components/memo/ShortNote.astro +223 -0
  103. package/src/components/notation/NotationFormula.astro +18 -1
  104. package/src/components/notation/NotationLayer.astro +260 -83
  105. package/src/components/notation/render-notation-math.ts +39 -2
  106. package/src/components/notation/safe-math-to-react.ts +50 -0
  107. package/src/components/reference/FloatRefLayer.astro +233 -15
  108. package/src/components/starlight/CollapsibleSections.astro +461 -0
  109. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  110. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  111. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  112. package/src/components/starlight/LayoutHeader.astro +50 -0
  113. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  114. package/src/components/starlight/LessonExportActions.astro +436 -0
  115. package/src/components/starlight/LessonFooter.astro +30 -3
  116. package/src/components/starlight/section-parts.ts +31 -0
  117. package/src/components/table/Table.astro +293 -7
  118. package/src/components/table/TableOverflowExpander.astro +489 -0
  119. package/src/content-config.ts +36 -1
  120. package/src/reference/base-abbreviations.mjs +60 -0
  121. package/src/reference/base-operators.mjs +40 -0
  122. package/src/reference/base-roman-labels.mjs +43 -0
  123. package/src/reference/consistency.ts +73 -2
  124. package/src/reference/diagrams-validate.ts +40 -0
  125. package/src/reference/equations.mjs +67 -14
  126. package/src/reference/gate-abbreviations.ts +128 -0
  127. package/src/reference/gate-heading-length.ts +131 -0
  128. package/src/reference/gate-math.ts +32 -7
  129. package/src/reference/gate-prose-notation.ts +224 -0
  130. package/src/reference/keyed-float-validate.ts +5 -5
  131. package/src/reference/keyed-jsx-floats.mjs +50 -25
  132. package/src/reference/knowledge-checks-validate.ts +130 -0
  133. package/src/reference/knowledge-checks.mjs +45 -0
  134. package/src/reference/lab-math-scope.ts +45 -3
  135. package/src/reference/math-glyphs.mjs +548 -40
  136. package/src/reference/mdx-attributes.mjs +38 -0
  137. package/src/reference/mixed-content.ts +69 -0
  138. package/src/reference/page-notation.ts +252 -0
  139. package/src/reference/raw-floats-validate.ts +103 -0
  140. package/src/reference/raw-floats.mjs +129 -0
  141. package/src/reference/rehype-heading-anchors.mjs +107 -1
  142. package/src/reference/rehype-table-header-labels.mjs +152 -0
  143. package/src/reference/remark-abbreviations.mjs +174 -0
  144. package/src/reference/remark-citation.mjs +0 -0
  145. package/src/reference/remark-memo.mjs +339 -0
  146. package/src/reference/remark-notation.mjs +354 -42
  147. package/src/reference/remark-short-note.mjs +237 -0
  148. package/src/reference/types.ts +16 -0
  149. package/src/reference/units-vocab.ts +4 -0
  150. package/src/styles/global.css +996 -50
@@ -64,10 +64,35 @@ 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 { gateHeadingLength } from '../reference/gate-heading-length';
92
+ import {
93
+ splitMixedContent,
94
+ type ContentSegment,
95
+ } from '../reference/mixed-content';
71
96
  import { loadLintIgnore } from '../reference/lint-ignore';
72
97
  import { buildNotationRegistry } from '../reference/registry';
73
98
  import { formulaGlyphScope } from '../reference/gloss';
@@ -82,12 +107,20 @@ import type {
82
107
  NotationLessonInput,
83
108
  NotationPageBundle,
84
109
  } from '../reference/types';
85
- import { coursePartsOf, type CourseConfig } from './course-config';
110
+ import {
111
+ coursePartsOf,
112
+ normalizeLessonExports,
113
+ normalizeDisclosureDefaults,
114
+ type CourseConfig,
115
+ type NormalizedLessonExports,
116
+ type NormalizedDisclosureDefaults,
117
+ } from './course-config';
86
118
  import {
87
119
  resolveKnowledgeVersion,
88
120
  type KnowledgeVersionDiagnostic,
89
121
  } from './knowledge-version';
90
122
  import {
123
+ loadAbbreviationRegistryInput,
91
124
  loadCurriculumCatalog,
92
125
  loadNotationDefinitions,
93
126
  loadNotationRegistryInput,
@@ -112,7 +145,7 @@ import {
112
145
  * `lessons[].equations`, and `diagnostics.equations`.
113
146
  * v4 (F-tables-figures): `tables`/`figures` (same shape as `equations`, for
114
147
  * `<Table>`/`<Figure>`), `lessons[].tables`/`lessons[].figures`, and
115
- * `diagnostics.tables`/`diagnostics.figures`.
148
+ * `diagnostics.tables`/`diagnostics.figures`/`diagnostics.diagrams`.
116
149
  * v5 (about): `about` (engine identity from `explico`'s `package.json`, the
117
150
  * course's reader-facing `about` block, and the knowledge version resolved
118
151
  * against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
@@ -122,8 +155,39 @@ import {
122
155
  * now the number before the dot), `lessons[].chapter` redefined as the
123
156
  * lesson's ordinal *within its part*, and `lessons[].numbering` — the
124
157
  * per-kind offsets that make `n` run continuously across a part.
158
+ * v8 (assessment math): `assessmentContent` — every assessment item's
159
+ * `prompt` / `explanation`, pre-split into text/math segments and resolved
160
+ * against the scope of the lesson that embeds it, so `AssessmentSet.astro`
161
+ * can render them through the same KaTeX pipeline as lesson body math instead
162
+ * of showing raw text.
163
+ * v9 (diagram identity): `diagrams` (the independent `<Diagram>` number and
164
+ * cross-page lookup), `lessons[].diagrams`, `lessons[].numbering.diagrams`,
165
+ * and `diagnostics.diagrams`.
166
+ * v10 (knowledge-check identity): `checks` (part-wide numbering and cross-page
167
+ * lookup), `lessons[].knowledgeChecks`, `lessons[].numbering.checks`, and
168
+ * `diagnostics.checks`.
169
+ * v11 (lesson exports): normalized standalone HTML and print build/UI settings.
170
+ * v12 (abbreviation hover cards): `manifest.abbreviations` — the
171
+ * `abbreviations` content collection (N14), flattened to plain
172
+ * `{key, abbr, expansion, aliases, domain?}` records sorted by key, the way
173
+ * `manifest.notation.raw` already carries notation frontmatter for
174
+ * `remarkNotation`. `remarkAbbreviations` reads it to wrap the first
175
+ * occurrence of a known abbreviation in a lesson's prose with a hover/click
176
+ * card.
177
+ * v13 (raw-float diagnostics, additive): `diagnostics.rawFloats` — a raw
178
+ * Markdown table, a raw HTML `<table>`, or an Explico `<Diagram>` that is
179
+ * unkeyed or nested outside `<Table>`/`<Figure>` never reaches keyed-float
180
+ * validation and can never get a stable number or `[[…]]` reference; this
181
+ * flags it (warning-only — the manifest shape a client already reads is
182
+ * unchanged).
183
+ * v14 (disclosure defaults, additive): `disclosureDefaults` — the resolved
184
+ * initial open/closed state for the "Page notation" panel and the generated
185
+ * "References" section, from `CourseConfig.disclosureDefaults`. Both are
186
+ * engine-authored sections, not written by a lesson, so they get their own
187
+ * default independent of a course's `##` heading default; closed unless the
188
+ * course sets one.
125
189
  */
126
- export const MANIFEST_SCHEMA_VERSION = 7;
190
+ export const MANIFEST_SCHEMA_VERSION = 14;
127
191
 
128
192
  const REPOSITORY_ROOT = process.cwd();
129
193
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -214,8 +278,9 @@ export interface ManifestLesson {
214
278
  /**
215
279
  * The 1-based ordinal of the **part** this lesson sits in — its sidebar
216
280
  * 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`.
281
+ * in every equation, table, figure, diagram, and knowledge-check number on
282
+ * the page: `(2.1)` is the first equation of part 2. See
283
+ * `docs/authoring/structure.md`.
219
284
  */
220
285
  readonly part: number;
221
286
  /**
@@ -233,6 +298,8 @@ export interface ManifestLesson {
233
298
  readonly equations: number;
234
299
  readonly tables: number;
235
300
  readonly figures: number;
301
+ readonly diagrams: number;
302
+ readonly checks: number;
236
303
  };
237
304
  readonly file: string;
238
305
  readonly title: string;
@@ -243,6 +310,11 @@ export interface ManifestLesson {
243
310
  readonly requires: readonly string[];
244
311
  readonly assumptions: readonly string[];
245
312
  readonly assessments: readonly string[];
313
+ /** Numbered knowledge checks from `assessments`, in `.checks.yml` order. */
314
+ readonly knowledgeChecks: readonly {
315
+ readonly key: string;
316
+ readonly number: string;
317
+ }[];
246
318
  readonly sources: readonly string[];
247
319
  readonly citations: readonly ManifestCitation[];
248
320
  /** Competency prerequisite edges this lesson introduces. */
@@ -262,6 +334,11 @@ export interface ManifestLesson {
262
334
  readonly key: string;
263
335
  readonly number: string;
264
336
  }[];
337
+ /** Keyed `<Diagram>` elements in appearance order. */
338
+ readonly diagrams: readonly {
339
+ readonly key: string;
340
+ readonly number: string;
341
+ }[];
265
342
  readonly notation: {
266
343
  readonly bindings: readonly {
267
344
  readonly key: string;
@@ -301,8 +378,21 @@ export interface ManifestAbout {
301
378
  readonly changelogHref?: string;
302
379
  }
303
380
 
381
+ /** One assessment item's pre-rendered `prompt` / `explanation` (v8). */
382
+ export interface ManifestAssessmentItemContent {
383
+ readonly lessonId: string;
384
+ readonly assessmentId: string;
385
+ readonly itemId: string;
386
+ readonly promptSegments: readonly ContentSegment[];
387
+ readonly explanationSegments: readonly ContentSegment[];
388
+ }
389
+
304
390
  export interface Manifest {
305
391
  readonly schemaVersion: number;
392
+ /** Normalized build/UI settings for opt-in per-lesson exports. */
393
+ readonly lessonExports: NormalizedLessonExports;
394
+ /** Initial open/closed state for "Page notation" and "References". */
395
+ readonly disclosureDefaults: NormalizedDisclosureDefaults;
306
396
  /** Reader-facing identity + versions (the `[?]` About panel). */
307
397
  readonly about: ManifestAbout;
308
398
  readonly notation: {
@@ -316,6 +406,8 @@ export interface Manifest {
316
406
  readonly competencies: readonly Record<string, unknown>[];
317
407
  readonly sources: readonly Record<string, unknown>[];
318
408
  readonly tracks: readonly Record<string, unknown>[];
409
+ /** Abbreviations table (N14), for `remarkAbbreviations`. Ordered by key. */
410
+ readonly abbreviations: readonly Record<string, unknown>[];
319
411
  readonly sidebar: readonly SidebarGroup[];
320
412
  readonly lessons: readonly ManifestLesson[];
321
413
  /** Full competency prerequisite graph. */
@@ -336,6 +428,26 @@ export interface Manifest {
336
428
  readonly labels: readonly FigureLabelRecord[];
337
429
  readonly numbersBySlug: Record<string, Record<string, string>>;
338
430
  };
431
+ /** Keyed-float identity for `<Diagram>` — independent of figure numbering. */
432
+ readonly diagrams: {
433
+ readonly labels: readonly DiagramLabelRecord[];
434
+ readonly numbersBySlug: Record<string, Record<string, string>>;
435
+ };
436
+ /** Knowledge-check identity, derived from each lesson's `.checks.yml`. */
437
+ readonly checks: {
438
+ readonly labels: readonly KnowledgeCheckLabelRecord[];
439
+ readonly numbersBySlug: Record<string, Record<string, string>>;
440
+ };
441
+ /**
442
+ * Every assessment item's `prompt` / `explanation`, pre-split into
443
+ * text/math segments (v8). `AssessmentSet.astro` filters this by
444
+ * `lessonId` + `assessmentId` and renders each `math` segment's `latex`
445
+ * through `renderNotationMath` before handing the result to
446
+ * `AssessmentRunner`. An assessment embedded in more than one lesson (via
447
+ * more than one `checks.yml`) appears once per lesson here, since each
448
+ * lesson's scope can resolve its glyphs differently.
449
+ */
450
+ readonly assessmentContent: readonly ManifestAssessmentItemContent[];
339
451
  readonly diagnostics: {
340
452
  readonly curriculum: readonly CurriculumIssue[];
341
453
  readonly notation: readonly NotationDiagnostic[];
@@ -351,6 +463,16 @@ export interface Manifest {
351
463
  readonly tables: readonly TableDiagnostic[];
352
464
  /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
353
465
  readonly figures: readonly FigureDiagnostic[];
466
+ /** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
467
+ readonly diagrams: readonly DiagramDiagnostic[];
468
+ /** Knowledge-check identity and `[[check-…]]` reference checks. */
469
+ readonly checks: readonly KnowledgeCheckDiagnostic[];
470
+ /**
471
+ * A raw Markdown table, a raw HTML `<table>`, or an Explico `<Diagram>`
472
+ * that is unkeyed or nested outside `<Table>`/`<Figure>` — invisible to
473
+ * keyed-float validation above. Warnings only.
474
+ */
475
+ readonly rawFloats: readonly RawFloatDiagnostic[];
354
476
  };
355
477
  readonly hashes: {
356
478
  /** `sha256` of every content source file, keyed by repo-relative path. */
@@ -546,9 +668,10 @@ export async function compileManifest(
546
668
  );
547
669
  }
548
670
 
549
- const [catalog, notationInput] = await Promise.all([
671
+ const [catalog, notationInput, abbreviationsInput] = await Promise.all([
550
672
  loadCurriculumCatalog(),
551
673
  loadNotationRegistryInput(),
674
+ loadAbbreviationRegistryInput(),
552
675
  ]);
553
676
 
554
677
  const registry = buildNotationRegistry(notationInput);
@@ -568,19 +691,66 @@ export async function compileManifest(
568
691
  registry.bundles.map((bundle) => [bundle.lessonId, bundle]),
569
692
  );
570
693
 
694
+ const assessmentsInput = catalog.assessments as unknown as {
695
+ id: string;
696
+ items?: { id?: string; prompt?: string; explanation?: string }[];
697
+ }[];
698
+
571
699
  const curriculumIssues = validateCurriculum(catalog);
572
700
  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
- });
701
+ const mathIssues = [
702
+ ...gateContentMath({
703
+ notationInput,
704
+ assessments: assessmentsInput,
705
+ lessonAssessments: catalog.lessons.map((lesson) => ({
706
+ lessonId: lesson.id,
707
+ assessmentIds: lesson.assessments,
708
+ })),
709
+ }),
710
+ ...gateProseNotation(notationInput.lessons),
711
+ ...gateProseNotationInAssessments(assessmentsInput),
712
+ ...gateAbbreviations({
713
+ notationInput,
714
+ abbreviations: abbreviationsInput,
715
+ courseAbbreviations: courseConfig.abbreviations,
716
+ }),
717
+ ...gateHeadingLength(notationInput.lessons),
718
+ ];
719
+
720
+ // Pre-render every assessment `prompt` / `explanation` into text/math
721
+ // segments, resolved against the scope of whichever lesson embeds it —
722
+ // same relationship, same `pageScope`, as the `gateContentMath` assessment
723
+ // check above, so a symbol that passes the gate is exactly the symbol
724
+ // `AssessmentSet.astro` renders. An assessment shared by two lessons is
725
+ // rendered once per (lesson, assessment) pair, since the two lessons can
726
+ // have different scopes and therefore different resolved LaTeX.
727
+ const notationLessonById = new Map(
728
+ notationInput.lessons.map((lesson) => [lesson.lessonId, lesson]),
729
+ );
730
+ const assessmentInputById = new Map(
731
+ assessmentsInput.map((assessment) => [assessment.id, assessment]),
732
+ );
733
+ const assessmentContent: ManifestAssessmentItemContent[] = [];
734
+ for (const lesson of catalog.lessons) {
735
+ if (lesson.assessments.length === 0) continue;
736
+ const lessonInput = notationLessonById.get(lesson.id);
737
+ if (!lessonInput) continue;
738
+ const scope = pageScope(lessonInput, cardsByKey);
739
+ for (const assessmentId of lesson.assessments) {
740
+ const assessment = assessmentInputById.get(assessmentId);
741
+ if (!assessment?.items) continue;
742
+ for (const item of assessment.items) {
743
+ if (!item.id) continue;
744
+ assessmentContent.push({
745
+ lessonId: lesson.id,
746
+ assessmentId,
747
+ itemId: item.id,
748
+ promptSegments: splitMixedContent(item.prompt ?? '', scope),
749
+ explanationSegments: splitMixedContent(item.explanation ?? '', scope),
750
+ });
751
+ }
752
+ }
753
+ }
584
754
 
585
755
  const prerequisitesById = new Map(
586
756
  catalog.competencies.map((competency) => [
@@ -615,8 +785,8 @@ export async function compileManifest(
615
785
  // Part and chapter, read straight off the sidebar so there is one
616
786
  // lesson-ordering source and not two: a part is a sidebar group (parts in
617
787
  // 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`
788
+ // track order, then slug). The part is what prefixes every numbered item —
789
+ // `(2.1)` is the first item of one independent kind in part 2 — while `n`
620
790
  // continues across the part's chapters rather than restarting on each page,
621
791
  // so every lesson also needs the running count that precedes it.
622
792
  // See `docs/authoring/structure.md`.
@@ -628,7 +798,13 @@ export async function compileManifest(
628
798
  {
629
799
  part: number;
630
800
  chapter: number;
631
- numbering: { equations: number; tables: number; figures: number };
801
+ numbering: {
802
+ equations: number;
803
+ tables: number;
804
+ figures: number;
805
+ diagrams: number;
806
+ checks: number;
807
+ };
632
808
  }
633
809
  >();
634
810
  let partCursor = 0;
@@ -639,7 +815,13 @@ export async function compileManifest(
639
815
  if (lessonIds.length === 0) continue; // the static Reference group
640
816
  partCursor += 1;
641
817
  // Running totals for this part, advanced one chapter at a time.
642
- const used = { equations: 0, tables: 0, figures: 0 };
818
+ const used = {
819
+ equations: 0,
820
+ tables: 0,
821
+ figures: 0,
822
+ diagrams: 0,
823
+ checks: 0,
824
+ };
643
825
  let chapterCursor = 0;
644
826
  for (const lessonId of lessonIds) {
645
827
  const body = bodyByLessonId.get(lessonId) ?? '';
@@ -651,10 +833,14 @@ export async function compileManifest(
651
833
  used.equations += countDisplayEquations(body);
652
834
  used.tables += countKeyedJsxFloats(body, { tagName: 'Table' });
653
835
  used.figures += countKeyedJsxFloats(body, { tagName: 'Figure' });
836
+ used.diagrams += countKeyedJsxFloats(body, { tagName: 'Diagram' });
837
+ used.checks += lessonMetaById.get(lessonId)?.assessments.length ?? 0;
654
838
  }
655
839
  }
656
840
 
657
- const floatInputs = (kind: 'equations' | 'tables' | 'figures') =>
841
+ const floatInputs = (
842
+ kind: 'equations' | 'tables' | 'figures' | 'diagrams',
843
+ ) =>
658
844
  notationInput.lessons.map((lesson) => {
659
845
  const placement = placementByLessonId.get(lesson.lessonId);
660
846
  return {
@@ -693,6 +879,48 @@ export async function compileManifest(
693
879
  Object.entries(table).map(([key, number]) => ({ key, number })),
694
880
  ]),
695
881
  );
882
+ const diagramValidation = validateDiagrams(floatInputs('diagrams'));
883
+ const diagramsBySlug = new Map(
884
+ Object.entries(diagramValidation.numbersBySlug).map(([slug, table]) => [
885
+ slug,
886
+ Object.entries(table).map(([key, number]) => ({ key, number })),
887
+ ]),
888
+ );
889
+ const checkValidation = validateKnowledgeChecks(
890
+ notationInput.lessons.map((lesson) => {
891
+ const placement = placementByLessonId.get(lesson.lessonId);
892
+ return {
893
+ lessonId: lesson.lessonId,
894
+ slug: lesson.lessonId.replace(/\./g, '/'),
895
+ file: lesson.source.file,
896
+ body: lesson.body,
897
+ assessmentIds:
898
+ lessonMetaById.get(lesson.lessonId)?.assessments ?? [],
899
+ ...(placement
900
+ ? {
901
+ numbering: {
902
+ prefix: placement.part,
903
+ offset: placement.numbering.checks,
904
+ },
905
+ }
906
+ : {}),
907
+ };
908
+ }),
909
+ );
910
+ const checksBySlug = new Map(
911
+ Object.entries(checkValidation.numbersBySlug).map(([slug, table]) => [
912
+ slug,
913
+ Object.entries(table).map(([key, number]) => ({ key, number })),
914
+ ]),
915
+ );
916
+ const rawFloatDiagnostics = validateRawFloats(
917
+ notationInput.lessons.map((lesson) => ({
918
+ lessonId: lesson.lessonId,
919
+ slug: lesson.lessonId.replace(/\./g, '/'),
920
+ file: lesson.source.file,
921
+ body: lesson.body,
922
+ })),
923
+ );
696
924
 
697
925
  const lessons: ManifestLesson[] = [];
698
926
  for (const notationLesson of notationInput.lessons) {
@@ -716,6 +944,8 @@ export async function compileManifest(
716
944
  equations: 0,
717
945
  tables: 0,
718
946
  figures: 0,
947
+ diagrams: 0,
948
+ checks: 0,
719
949
  },
720
950
  file: notationLesson.source.file,
721
951
  title: frontmatter.title,
@@ -725,11 +955,13 @@ export async function compileManifest(
725
955
  requires: [...meta.requires],
726
956
  assumptions: [...meta.assumptions],
727
957
  assessments: [...meta.assessments],
958
+ knowledgeChecks: checksBySlug.get(id.replace(/\./g, '/')) ?? [],
728
959
  sources: deriveSources(notationLesson.body),
729
960
  citations: lessonCitations(notationLesson.body),
730
961
  equations: equationsBySlug.get(id.replace(/\./g, '/')) ?? [],
731
962
  tables: tablesBySlug.get(id.replace(/\./g, '/')) ?? [],
732
963
  figures: figuresBySlug.get(id.replace(/\./g, '/')) ?? [],
964
+ diagrams: diagramsBySlug.get(id.replace(/\./g, '/')) ?? [],
733
965
  prereqEdges: sortEdges(
734
966
  teaches.flatMap((to) =>
735
967
  (prerequisitesById.get(to) ?? [])
@@ -761,6 +993,9 @@ export async function compileManifest(
761
993
  ...(courseConfig.operators === undefined
762
994
  ? {}
763
995
  : { operators: courseConfig.operators }),
996
+ ...(courseConfig.romanLabels === undefined
997
+ ? {}
998
+ : { romanLabels: courseConfig.romanLabels }),
764
999
  });
765
1000
 
766
1001
  const rawNotation = [...loadNotationDefinitions()].sort((a, b) =>
@@ -771,6 +1006,15 @@ export async function compileManifest(
771
1006
  .sort(byId);
772
1007
  const sources = [...loadSourceRecords()].sort(byId);
773
1008
  const tracks = catalog.tracks.map((entry) => ({ ...entry })).sort(byId);
1009
+ const abbreviations = abbreviationsInput
1010
+ .map((entry) => ({
1011
+ key: entry.key,
1012
+ abbr: entry.abbr,
1013
+ expansion: entry.expansion,
1014
+ aliases: entry.aliases,
1015
+ ...(entry.domain === undefined ? {} : { domain: entry.domain }),
1016
+ }))
1017
+ .sort((a, b) => a.key.localeCompare(b.key));
774
1018
 
775
1019
  const hashes = contentHashes();
776
1020
 
@@ -819,6 +1063,8 @@ export async function compileManifest(
819
1063
 
820
1064
  return {
821
1065
  schemaVersion: MANIFEST_SCHEMA_VERSION,
1066
+ lessonExports: normalizeLessonExports(courseConfig),
1067
+ disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
822
1068
  about,
823
1069
  notation: {
824
1070
  definitions: registry.definitions,
@@ -830,6 +1076,7 @@ export async function compileManifest(
830
1076
  competencies,
831
1077
  sources,
832
1078
  tracks,
1079
+ abbreviations,
833
1080
  sidebar,
834
1081
  lessons,
835
1082
  prereqEdges,
@@ -845,6 +1092,15 @@ export async function compileManifest(
845
1092
  labels: figureValidation.labels,
846
1093
  numbersBySlug: figureValidation.numbersBySlug,
847
1094
  },
1095
+ diagrams: {
1096
+ labels: diagramValidation.labels,
1097
+ numbersBySlug: diagramValidation.numbersBySlug,
1098
+ },
1099
+ checks: {
1100
+ labels: checkValidation.labels,
1101
+ numbersBySlug: checkValidation.numbersBySlug,
1102
+ },
1103
+ assessmentContent,
848
1104
  diagnostics: {
849
1105
  curriculum: curriculumIssues,
850
1106
  notation: registry.diagnostics,
@@ -854,7 +1110,10 @@ export async function compileManifest(
854
1110
  equations: equationValidation.diagnostics,
855
1111
  tables: tableValidation.diagnostics,
856
1112
  figures: figureValidation.diagnostics,
1113
+ diagrams: diagramValidation.diagnostics,
1114
+ checks: checkValidation.diagnostics,
857
1115
  knowledge: knowledge.diagnostics,
1116
+ rawFloats: rawFloatDiagnostics,
858
1117
  },
859
1118
  hashes,
860
1119
  };
@@ -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>