explico 0.13.1 → 0.15.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 (256) hide show
  1. package/CHANGELOG.md +990 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +100 -0
  6. package/dist/compiler/cli-main.js +433 -0
  7. package/dist/compiler/cli-reference.d.ts +50 -0
  8. package/dist/compiler/cli-reference.js +213 -0
  9. package/dist/compiler/cli-verbs.d.ts +45 -0
  10. package/dist/compiler/cli-verbs.js +224 -0
  11. package/dist/compiler/cli-verify.d.ts +51 -0
  12. package/dist/compiler/cli-verify.js +151 -0
  13. package/dist/compiler/cli.d.ts +408 -24
  14. package/dist/compiler/cli.js +1328 -8
  15. package/dist/compiler/collections.d.ts +88 -1
  16. package/dist/compiler/collections.js +170 -25
  17. package/dist/compiler/content-release.d.ts +49 -0
  18. package/dist/compiler/content-release.js +63 -0
  19. package/dist/compiler/content-schemas.d.ts +2 -2
  20. package/dist/compiler/course-config.d.ts +1 -1
  21. package/dist/compiler/course-config.js +105 -1
  22. package/dist/compiler/decks.d.ts +161 -0
  23. package/dist/compiler/decks.js +235 -0
  24. package/dist/compiler/exports/combined-html.d.ts +5 -1
  25. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  26. package/dist/compiler/exports/deck-fit.js +90 -0
  27. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  28. package/dist/compiler/exports/export-chapters.js +64 -5
  29. package/dist/compiler/exports/export-pages.d.ts +30 -3
  30. package/dist/compiler/exports/export-pages.js +24 -1
  31. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  32. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  33. package/dist/compiler/exports/latex/cache.js +129 -0
  34. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  35. package/dist/compiler/exports/latex/capture.js +3 -1
  36. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  37. package/dist/compiler/exports/latex/compile.js +12 -1
  38. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  39. package/dist/compiler/exports/latex/deck.js +349 -0
  40. package/dist/compiler/exports/latex/document.d.ts +51 -6
  41. package/dist/compiler/exports/latex/document.js +71 -12
  42. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  43. package/dist/compiler/exports/latex/explico.cls +13 -183
  44. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  45. package/dist/compiler/exports/latex/serialize.js +4 -2
  46. package/dist/compiler/exports/latex-export.d.ts +24 -2
  47. package/dist/compiler/exports/latex-export.js +107 -26
  48. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  49. package/dist/compiler/exports/markdown/serialize.js +85 -8
  50. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  51. package/dist/compiler/exports/markdown-export.js +50 -11
  52. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  53. package/dist/compiler/exports/standalone-html.js +3 -3
  54. package/dist/compiler/exports/static-tree.d.ts +38 -1
  55. package/dist/compiler/exports/static-tree.js +120 -4
  56. package/dist/compiler/knowledge-version.d.ts +26 -2
  57. package/dist/compiler/knowledge-version.js +27 -17
  58. package/dist/compiler/lesson-derivation.d.ts +143 -6
  59. package/dist/compiler/lesson-derivation.js +145 -13
  60. package/dist/compiler/manifest.d.ts +133 -4
  61. package/dist/compiler/manifest.js +364 -58
  62. package/dist/compiler/page-versions.d.ts +184 -0
  63. package/dist/compiler/page-versions.js +217 -0
  64. package/dist/compiler/render-diagnostics.d.ts +5 -1
  65. package/dist/compiler/resolution-report.d.ts +24 -6
  66. package/dist/compiler/resolution-report.js +22 -4
  67. package/dist/compiler/validate-content.d.ts +7 -0
  68. package/dist/compiler/validate-content.js +160 -0
  69. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  70. package/dist/curriculum/competency-graph.d.ts +54 -0
  71. package/dist/curriculum/competency-graph.js +41 -0
  72. package/dist/reference/base-library.d.mts +8 -0
  73. package/dist/reference/base-library.mjs +19 -0
  74. package/dist/reference/consistency.d.ts +8 -4
  75. package/dist/reference/consistency.js +82 -26
  76. package/dist/reference/deck-remark.d.mts +123 -0
  77. package/dist/reference/deck-remark.mjs +600 -0
  78. package/dist/reference/deck-source.d.ts +107 -0
  79. package/dist/reference/deck-source.js +140 -0
  80. package/dist/reference/decks-validate.d.ts +133 -0
  81. package/dist/reference/decks-validate.js +359 -0
  82. package/dist/reference/derived-entries.d.ts +5 -1
  83. package/dist/reference/diagram-fit.d.ts +6 -2
  84. package/dist/reference/diagram-fit.js +5 -1
  85. package/dist/reference/figure-symbols.d.ts +23 -6
  86. package/dist/reference/figure-symbols.js +51 -15
  87. package/dist/reference/float-name.d.ts +43 -0
  88. package/dist/reference/float-name.js +58 -0
  89. package/dist/reference/folds-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-indices.js +5 -4
  92. package/dist/reference/gate-math.d.ts +29 -2
  93. package/dist/reference/gate-math.js +99 -18
  94. package/dist/reference/gate-part-pages.d.ts +1 -1
  95. package/dist/reference/lab-katex-validate.d.ts +10 -6
  96. package/dist/reference/lab-katex-validate.js +17 -9
  97. package/dist/reference/lab-math-scope.js +29 -4
  98. package/dist/reference/math-glyphs.d.mts +25 -0
  99. package/dist/reference/math-glyphs.mjs +189 -25
  100. package/dist/reference/page-notation.d.ts +22 -3
  101. package/dist/reference/page-notation.js +32 -10
  102. package/dist/reference/references.d.ts +14 -1
  103. package/dist/reference/references.js +13 -0
  104. package/dist/reference/registry-scope.d.mts +33 -0
  105. package/dist/reference/registry-scope.mjs +34 -0
  106. package/dist/reference/registry.js +29 -10
  107. package/dist/reference/rehype-deck.d.mts +6 -0
  108. package/dist/reference/rehype-deck.mjs +286 -0
  109. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  110. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  111. package/dist/reference/remark-notation.mjs +249 -40
  112. package/dist/reference/source-lines.d.mts +12 -0
  113. package/dist/reference/source-lines.mjs +65 -0
  114. package/dist/reference/types.d.ts +28 -0
  115. package/dist/rules/catalog.d.ts +21 -4
  116. package/dist/rules/catalog.js +237 -2
  117. package/dist/rules/course-rules.d.ts +5 -1
  118. package/dist/rules/course-rules.js +17 -3
  119. package/dist/rules/course-rules.schema.json +12 -0
  120. package/dist/rules/in-effect.d.ts +13 -3
  121. package/dist/rules/in-effect.js +34 -10
  122. package/dist/rules/rules.schema.json +280 -1
  123. package/dist/rules/settings.d.ts +1 -1
  124. package/dist/session/PreferenceStore.d.ts +6 -0
  125. package/dist/session/PreferenceStore.js +18 -1
  126. package/dist/session/deck-memory.d.ts +116 -0
  127. package/dist/session/deck-memory.js +143 -0
  128. package/dist/session/reading-memory.d.ts +96 -0
  129. package/dist/session/reading-memory.js +133 -0
  130. package/docs/VERSIONING.md +21 -18
  131. package/docs/authoring/README.md +31 -9
  132. package/docs/authoring/agents.md +72 -18
  133. package/docs/authoring/cli-reference.json +2172 -0
  134. package/docs/authoring/cli.md +887 -0
  135. package/docs/authoring/components.md +15 -0
  136. package/docs/authoring/deck.md +612 -0
  137. package/docs/authoring/migrating.md +1195 -16
  138. package/docs/authoring/notation.md +49 -6
  139. package/docs/authoring/structure.md +80 -1
  140. package/docs/authoring/versioning.md +56 -3
  141. package/docs/authoring/visualizations.md +7 -2
  142. package/docs/course-agent/AGENTS.md +40 -0
  143. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  144. package/docs/course-agent/skills/draft-lesson/SKILL.md +38 -0
  145. package/docs/course-agent/skills/migrate-course/SKILL.md +32 -0
  146. package/docs/course-agent/skills/release-content/SKILL.md +26 -0
  147. package/docs/diagram-scaling.md +148 -0
  148. package/docs/privacy.md +59 -0
  149. package/package.json +8 -2
  150. package/src/compiler/astro.ts +60 -21
  151. package/src/compiler/cli-main.ts +557 -0
  152. package/src/compiler/cli-reference.ts +285 -0
  153. package/src/compiler/cli-verbs.ts +270 -0
  154. package/src/compiler/cli-verify.ts +241 -0
  155. package/src/compiler/cli.ts +2119 -170
  156. package/src/compiler/collections.ts +284 -25
  157. package/src/compiler/content-release.ts +91 -0
  158. package/src/compiler/course-config.ts +267 -0
  159. package/src/compiler/decks.ts +455 -0
  160. package/src/compiler/exports/deck-fit.ts +127 -0
  161. package/src/compiler/exports/export-chapters.ts +105 -6
  162. package/src/compiler/exports/export-pages.ts +46 -2
  163. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  164. package/src/compiler/exports/latex/cache.ts +183 -0
  165. package/src/compiler/exports/latex/capture.ts +5 -2
  166. package/src/compiler/exports/latex/compile.ts +18 -0
  167. package/src/compiler/exports/latex/deck.ts +419 -0
  168. package/src/compiler/exports/latex/document.ts +117 -21
  169. package/src/compiler/exports/latex/explico-common.sty +198 -0
  170. package/src/compiler/exports/latex/explico.cls +13 -183
  171. package/src/compiler/exports/latex/serialize.ts +25 -21
  172. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  173. package/src/compiler/exports/latex-export.ts +187 -45
  174. package/src/compiler/exports/markdown/serialize.ts +118 -9
  175. package/src/compiler/exports/markdown-export.ts +56 -11
  176. package/src/compiler/exports/standalone-html.ts +8 -7
  177. package/src/compiler/exports/static-tree.ts +175 -3
  178. package/src/compiler/knowledge-version.ts +56 -21
  179. package/src/compiler/lesson-derivation.ts +282 -16
  180. package/src/compiler/manifest.ts +568 -63
  181. package/src/compiler/page-versions.ts +401 -0
  182. package/src/compiler/resolution-report.ts +38 -7
  183. package/src/compiler/validate-content.ts +215 -0
  184. package/src/components/CourseContents.astro +124 -6
  185. package/src/components/CurriculumMap.astro +49 -1
  186. package/src/components/FloatIndex.astro +1 -1
  187. package/src/components/PartOverview.astro +51 -5
  188. package/src/components/assessments/AssessmentSet.astro +41 -15
  189. package/src/components/block-heading.ts +15 -6
  190. package/src/components/card-carry.ts +7 -4
  191. package/src/components/card-manager.ts +14 -2
  192. package/src/components/card-memory.ts +13 -9
  193. package/src/components/deck/DeckEnhancer.astro +13 -0
  194. package/src/components/deck/deck-context.ts +104 -0
  195. package/src/components/deck/deck-controller.ts +1287 -0
  196. package/src/components/deck/deck-map.ts +91 -0
  197. package/src/components/deck-lane.ts +163 -0
  198. package/src/components/diagram/Diagram.astro +131 -86
  199. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  200. package/src/components/figure/Figure.astro +7 -2
  201. package/src/components/figure/FloatCaption.astro +5 -3
  202. package/src/components/fold/Fold.astro +2 -2
  203. package/src/components/notation/NotationFormula.astro +81 -11
  204. package/src/components/notation/NotationGlossary.astro +132 -1
  205. package/src/components/notation/NotationLayer.astro +15 -7
  206. package/src/components/starlight/AboutPanel.astro +186 -3
  207. package/src/components/starlight/CollapsibleSections.astro +24 -3
  208. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  209. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  210. package/src/components/starlight/LayoutHeader.astro +17 -0
  211. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  212. package/src/components/starlight/LayoutSidebar.astro +807 -21
  213. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  214. package/src/components/starlight/LessonExportActions.astro +27 -9
  215. package/src/components/starlight/LessonFooter.astro +59 -0
  216. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  217. package/src/components/starlight/TextSizeControl.astro +24 -13
  218. package/src/components/starlight/page-marks.ts +139 -0
  219. package/src/components/starlight/section-summary.ts +1 -1
  220. package/src/components/starlight/sidebar-editions.ts +245 -0
  221. package/src/components/table/Table.astro +15 -7
  222. package/src/content-config.ts +87 -1
  223. package/src/curriculum/competency-graph.ts +95 -0
  224. package/src/reference/base-library.mjs +19 -0
  225. package/src/reference/consistency.ts +137 -39
  226. package/src/reference/deck-remark.mjs +600 -0
  227. package/src/reference/deck-source.ts +273 -0
  228. package/src/reference/decks-validate.ts +604 -0
  229. package/src/reference/diagram-fit.ts +18 -4
  230. package/src/reference/diagram-layout.ts +3 -1
  231. package/src/reference/figure-symbols.ts +108 -31
  232. package/src/reference/folds-validate.ts +1 -1
  233. package/src/reference/gate-indices.ts +5 -3
  234. package/src/reference/gate-math.ts +169 -25
  235. package/src/reference/lab-katex-validate.ts +30 -15
  236. package/src/reference/lab-math-scope.ts +34 -3
  237. package/src/reference/math-glyphs.mjs +189 -25
  238. package/src/reference/page-notation.ts +69 -15
  239. package/src/reference/references.ts +28 -0
  240. package/src/reference/registry-scope.mjs +34 -0
  241. package/src/reference/registry.ts +41 -11
  242. package/src/reference/rehype-deck.mjs +286 -0
  243. package/src/reference/rehype-heading-anchors.mjs +48 -6
  244. package/src/reference/remark-notation.mjs +249 -40
  245. package/src/reference/source-lines.mjs +65 -0
  246. package/src/reference/types.ts +28 -0
  247. package/src/rules/catalog.ts +274 -4
  248. package/src/rules/course-rules.ts +21 -1
  249. package/src/rules/in-effect.ts +58 -11
  250. package/src/session/PreferenceStore.ts +23 -0
  251. package/src/session/deck-memory.ts +263 -0
  252. package/src/session/reading-memory.ts +243 -0
  253. package/src/styles/deck.css +1150 -0
  254. package/src/styles/global.css +958 -27
  255. package/src/styles/zones.css +277 -0
  256. /package/src/{components → reference}/float-name.ts +0 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `pnpm validate:content` — regenerate `build/manifest.json` and report its
3
+ * diagnostics, whole. A course's `scripts/compile-manifest.ts` is three lines:
4
+ *
5
+ * import { runValidateContent } from 'explico/compiler/validate-content';
6
+ * import { courseConfig } from '../content/course.config';
7
+ * process.exitCode = await runValidateContent(courseConfig);
8
+ *
9
+ * It compiles the one manifest (the single walk of the content tree), writes
10
+ * it deterministically, prints the warnings, refreshes the per-lesson
11
+ * resolution reports, `content/versions.json` (rule C5) and the
12
+ * rules-in-effect page, and returns 1 if any blocking diagnostic is present.
13
+ * `pnpm build` runs it first, so `astro build` and every page component read a
14
+ * current manifest and never re-read the content tree.
15
+ *
16
+ * Every line printed is also recorded, in Python's `logging` format, to
17
+ * `build/logs/<timestamp>/{error,warning,info}.log` (`./logging`); `log.<level>()`
18
+ * returns its message so the printed line and the recorded one cannot drift.
19
+ */
20
+ import { notationSourceLocatorGaps } from './collections';
21
+ import type { CourseConfig } from './course-config';
22
+ import { createRunLog, diagnosticLine } from './logging';
23
+ import { compileManifest, MANIFEST_PATH, writeManifest } from './manifest';
24
+ import { syncPageVersions } from './page-versions';
25
+ import { syncResolutionReports } from './resolution-report';
26
+ import { consistencyCounts } from '../reference/consistency';
27
+ import { curriculumErrors } from '../curriculum/validation';
28
+ import { writeRulesInEffect } from '../rules/in-effect';
29
+
30
+ export async function runValidateContent(courseConfig: CourseConfig): Promise<number> {
31
+ const log = createRunLog();
32
+
33
+ let manifest;
34
+ try {
35
+ manifest = await compileManifest(courseConfig);
36
+ } catch (error) {
37
+ // The log keeps why the run ended; the console gets the message, not a stack.
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ log.error('manifest', `Manifest compilation failed: ${message}`);
40
+ const summary = log.close();
41
+ console.error(`Manifest compilation failed: ${message}\nRun log: ${summary.relativeDirectory}.`);
42
+ return 1;
43
+ }
44
+ writeManifest(manifest);
45
+
46
+ const {
47
+ curriculum,
48
+ notation,
49
+ alignment,
50
+ math,
51
+ consistency,
52
+ equations,
53
+ tables,
54
+ figures,
55
+ diagrams,
56
+ checks,
57
+ examples,
58
+ folds,
59
+ knowledge,
60
+ rawFloats,
61
+ config,
62
+ waivers,
63
+ } = manifest.diagnostics;
64
+
65
+ for (const warning of curriculum.filter((issue) => issue.severity === 'warning')) {
66
+ console.warn(log.warning('curriculum', `[${warning.kind}] ${warning.message}`));
67
+ }
68
+ for (const warning of notation.filter((d) => d.severity === 'warning')) {
69
+ console.warn(log.warning('notation', `[${warning.code}] ${warning.message}`));
70
+ }
71
+ for (const warning of alignment.filter((d) => d.severity === 'warning')) {
72
+ console.warn(log.warning('alignment', `[${warning.code}] ${warning.message}`));
73
+ }
74
+ const floatGroups = [
75
+ ['equations', equations],
76
+ ['tables', tables],
77
+ ['figures', figures],
78
+ ['diagrams', diagrams],
79
+ ['checks', checks],
80
+ ['examples', examples],
81
+ ['folds', folds],
82
+ ['rawFloats', rawFloats],
83
+ ] as const;
84
+ for (const [source, group] of floatGroups) {
85
+ for (const warning of group.filter((d) => d.severity === 'warning')) {
86
+ console.warn(log.warning(source, `[${warning.code}] ${warning.message}`));
87
+ }
88
+ }
89
+ // `diagnosticLine` prefixes a code owned by a lettered rule with its ID
90
+ // (`[N9 prose-notation] file:line: …`), so the console names the rule page.
91
+ for (const issue of math.filter((item) => item.severity === 'warning')) {
92
+ console.warn(log.warning('math', diagnosticLine(issue)));
93
+ }
94
+ for (const warning of knowledge) {
95
+ console.warn(log.warning('knowledge', `[${warning.code}] ${warning.message}`));
96
+ }
97
+ // The course settings themselves: a deprecated top-level block, a retired rule.
98
+ for (const warning of config) {
99
+ console.warn(log.warning('config', `[${warning.code}] ${warning.message}`));
100
+ }
101
+ // Per-place waivers: a stale one is a warning; a malformed or invalid one
102
+ // fails below with the other errors.
103
+ for (const warning of waivers.filter((d) => d.severity === 'warning')) {
104
+ console.warn(log.warning('waivers', diagnosticLine(warning)));
105
+ }
106
+
107
+ const locatorGaps = notationSourceLocatorGaps();
108
+ if (locatorGaps.bare > 0) {
109
+ console.warn(
110
+ log.warning(
111
+ 'consistency',
112
+ `[notation-source-locator] ${locatorGaps.bare} of ${locatorGaps.total} notation source citations lack a non-empty locator.`,
113
+ ),
114
+ );
115
+ }
116
+
117
+ const consistencyWarnings = consistency.filter((diagnostic) => diagnostic.severity === 'warning');
118
+ if (consistencyWarnings.length > 0) {
119
+ // The console gets one digest line; the log records each finding at
120
+ // WARNING (below), so its counts agree with `content status`, and the
121
+ // digest at INFO, so it is not counted as one more warning.
122
+ console.warn(
123
+ log.info(
124
+ 'consistency',
125
+ `[consistency] ${consistencyWarnings.length} Tier-3 finding(s): ` +
126
+ Object.entries(consistencyCounts(consistencyWarnings))
127
+ .filter(([, count]) => count > 0)
128
+ .map(([code, count]) => `${code}=${count}`)
129
+ .join(', '),
130
+ ),
131
+ );
132
+ }
133
+
134
+ // Every severity-`info` diagnostic goes to the log only.
135
+ log.infoDiagnostics(manifest.diagnostics);
136
+ for (const diagnostic of consistencyWarnings) {
137
+ log.warning('consistency', `[${diagnostic.code}] ${diagnostic.message}`);
138
+ }
139
+
140
+ const reports = syncResolutionReports(manifest);
141
+ console.log(
142
+ log.info(
143
+ 'manifest',
144
+ `Resolution report: ${reports.written} file(s) under resolution/` +
145
+ (reports.refreshed.length > 0
146
+ ? ` (${reports.refreshed.length} refreshed; git does not track them, nothing to commit).`
147
+ : '.'),
148
+ ),
149
+ );
150
+ // Each page's own version (rule C5): rewritten here and committed with the
151
+ // change; in CI a record that would change fails below, like a lockfile.
152
+ const pageVersions = syncPageVersions(manifest);
153
+ if (pageVersions.written) {
154
+ console.log(
155
+ log.info(
156
+ 'page-versions',
157
+ `content/versions.json ${
158
+ pageVersions.changed[0] === '*' ? 'written' : `updated for ${pageVersions.changed.length} page(s)`
159
+ } — commit it with the change (rule C5).`,
160
+ ),
161
+ );
162
+ }
163
+ // The page an agent reads before drafting. Regenerated every run, never committed.
164
+ writeRulesInEffect(manifest);
165
+ console.log(
166
+ log.info('manifest', `Rules in effect: build/rules-in-effect.md (${manifest.rules.disabled.length} rule(s) off).`),
167
+ );
168
+
169
+ const failures: string[] = [];
170
+ // `block(source, …)`: a blocking finding under its log source (not a diagnostic code).
171
+ const block = (source: string, message: string): void => {
172
+ failures.push(log.error(source, message));
173
+ };
174
+ for (const stalePath of reports.stale) {
175
+ block('resolution-report', `[resolution-report] ${stalePath} is out of date — commit the regenerated resolution/ files`);
176
+ }
177
+ if (pageVersions.diagnostic) block('page-versions', diagnosticLine(pageVersions.diagnostic));
178
+ for (const issue of curriculumErrors(curriculum)) block('curriculum', `[curriculum:${issue.kind}] ${issue.message}`);
179
+ for (const issue of notation.filter((d) => d.severity === 'error')) block('notation', `[notation:${issue.code}] ${issue.message}`);
180
+ for (const issue of alignment.filter((d) => d.severity === 'error')) block('alignment', `[alignment:${issue.code}] ${issue.message}`);
181
+ for (const issue of math.filter((item) => item.severity === 'error')) block('math', diagnosticLine(issue));
182
+ for (const issue of waivers.filter((d) => d.severity === 'error')) block('waivers', diagnosticLine(issue));
183
+ for (const [source, group] of floatGroups) {
184
+ for (const issue of group.filter((d) => d.severity === 'error')) block(source, `[${issue.code}] ${issue.message}`);
185
+ }
186
+
187
+ if (failures.length > 0) {
188
+ const summary = log.close();
189
+ console.error(
190
+ `Content manifest has ${failures.length} blocking diagnostic(s):\n${failures.map((line) => `- ${line}`).join('\n')}\n` +
191
+ `Run log: ${summary.relativeDirectory} (${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s)).`,
192
+ );
193
+ return 1;
194
+ }
195
+
196
+ const itemCount = manifest.lessons.reduce((count, lesson) => count + lesson.assessments.length, 0);
197
+ const relPath = MANIFEST_PATH.replace(`${process.cwd()}/`, '');
198
+ console.log(
199
+ log.info(
200
+ 'manifest',
201
+ `Manifest written to ${relPath} (schema v${manifest.schemaVersion}, knowledge v${manifest.about.knowledge.version}): ` +
202
+ `${manifest.competencies.length} competencies, ${manifest.lessons.length} lessons, ` +
203
+ `${itemCount} lesson assessment links, ${manifest.notation.definitions.length} notation definitions, ` +
204
+ `${manifest.sources.length} sources, ${manifest.tracks.length} track(s). ` +
205
+ `Content fingerprint ${manifest.hashes.contentTree}.`,
206
+ ),
207
+ );
208
+
209
+ const summary = log.close();
210
+ console.log(
211
+ `Run log: ${summary.relativeDirectory} ` +
212
+ `(${summary.counts.ERROR} error(s), ${summary.counts.WARNING} warning(s), ${summary.counts.INFO} info).`,
213
+ );
214
+ return 0;
215
+ }
@@ -9,6 +9,13 @@
9
9
  * each part heading linking its overview page where one exists, and study
10
10
  * times totalled per part and for the course when every chapter states one.
11
11
  *
12
+ * When the course has a deck, each title starts with the glyph of what it
13
+ * opens (a chapter's sheet, a slides-only lesson's frame), a key and a deck
14
+ * count join the counts, and a slides column ends every row: a lesson's own
15
+ * deck is a tile on its title line, a part's deck one bar along the chapters
16
+ * it covers (`deck-lane.ts`). It lists every edition whatever the sidebar's
17
+ * controls say: it answers "what is in the course".
18
+ *
12
19
  * Usage, on a splash page:
13
20
  *
14
21
  * ```mdx
@@ -18,15 +25,21 @@
18
25
  * ```
19
26
  */
20
27
  import { loadManifest } from '../compiler/manifest';
28
+ import { contentsKey, deckLanes, laneLink, titleGlyph } from './deck-lane';
21
29
 
22
30
  interface Props {
23
31
  /** Heading level for each part's name. Defaults to `h2`. */
24
32
  readonly headingLevel?: 2 | 3;
25
33
  /** Set `false` to drop the per-chapter description column. */
26
34
  readonly descriptions?: boolean;
35
+ /**
36
+ * Set `false` to drop the slides column, the glyphs, the key and the deck
37
+ * count. They show only when the course has a deck.
38
+ */
39
+ readonly slides?: boolean;
27
40
  }
28
41
 
29
- const { headingLevel = 2, descriptions = true } = Astro.props;
42
+ const { headingLevel = 2, descriptions = true, slides = true } = Astro.props;
30
43
  const Heading = `h${headingLevel}` as 'h2' | 'h3';
31
44
 
32
45
  const manifest = await loadManifest();
@@ -44,7 +57,24 @@ const parts = manifest.parts
44
57
  (lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined,
45
58
  ),
46
59
  }))
47
- .filter((part) => part.chapters.length > 0);
60
+ .filter((part) => part.chapters.length > 0)
61
+ .map((part) => ({
62
+ ...part,
63
+ lanes: slides ? deckLanes({ lessons: part.chapters, decks: manifest.decks, part }) : [],
64
+ }));
65
+
66
+ // The glyphs, the key and the count say which lessons have slides: in a
67
+ // course with no deck they would explain one glyph, so the list stays as it
68
+ // was. The column is as wide as the part that needs most lanes, so it lines
69
+ // up across parts.
70
+ const key = slides ? contentsKey(manifest.lessons, manifest.decks) : [];
71
+ const glyphs = key.length > 0;
72
+ const laneCount = Math.max(0, ...parts.map((part) => part.lanes.length));
73
+ const keyLabel = {
74
+ chapter: 'Chapter',
75
+ deck: 'Slides',
76
+ 'part-deck': 'Slides for a whole part',
77
+ } as const;
48
78
 
49
79
  const chapterCount = parts.reduce(
50
80
  (total, part) => total + part.chapters.length,
@@ -74,19 +104,42 @@ const hours = (range: { min: number; max: number }) => {
74
104
  };
75
105
  ---
76
106
 
77
- <div class="course-contents not-content">
107
+ <div
108
+ class="course-contents not-content"
109
+ data-deck-lanes={laneCount > 0 ? laneCount : undefined}
110
+ style={laneCount > 0 ? `--deck-lanes: ${laneCount}` : undefined}
111
+ >
78
112
  <p class="course-meta">
79
113
  <span>{parts.length} parts</span>
80
114
  <span>{chapterCount} chapters</span>
115
+ {
116
+ glyphs && (
117
+ <span>
118
+ {manifest.decks.length} {manifest.decks.length === 1 ? 'slide deck' : 'slide decks'}
119
+ </span>
120
+ )
121
+ }
81
122
  {courseMinutes && <span>{hours(courseMinutes)} of study</span>}
82
123
  </p>
124
+ {
125
+ glyphs && (
126
+ <ul class="contents-key" aria-label="Key">
127
+ {key.map((glyph) => (
128
+ <li>
129
+ <span class={`contents-glyph contents-glyph--${glyph}`} aria-hidden="true" />
130
+ {keyLabel[glyph]}
131
+ </li>
132
+ ))}
133
+ </ul>
134
+ )
135
+ }
83
136
 
84
137
  {
85
138
  parts.map((part) => (
86
139
  <section class="course-part">
87
140
  <Heading id={`contents-${part.dir}`}>
88
141
  {part.overview ? (
89
- <a href={`${base}${part.link}`}>{part.label}</a>
142
+ <a href={`${base}${part.link}`}>{glyphs && <span class="contents-glyph contents-glyph--part" aria-hidden="true" />}{part.label}</a>
90
143
  ) : (
91
144
  part.label
92
145
  )}
@@ -99,13 +152,23 @@ const hours = (range: { min: number; max: number }) => {
99
152
  {part.studyMinutes && <span>{minutes(part.studyMinutes)}</span>}
100
153
  </p>
101
154
  <ol aria-labelledby={`contents-${part.dir}`}>
102
- {part.chapters.map((lesson) => (
155
+ {part.chapters.map((lesson, row) => (
103
156
  <li>
104
- <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
157
+ <a href={`${base}/${lesson.slug}/`}>{glyphs && <span class={`contents-glyph contents-glyph--${titleGlyph(lesson)}`} aria-hidden="true" />}{lesson.title}</a>
105
158
  {lesson.studyMinutes && (
106
159
  <span class="chapter-time">{minutes(lesson.studyMinutes)}</span>
107
160
  )}
108
161
  {descriptions && <p>{lesson.description}</p>}
162
+ {part.lanes.map((lane, index) => {
163
+ const cell = lane[row];
164
+ return (
165
+ cell && (
166
+ <a {...laneLink(cell, index, laneCount, base)}>
167
+ <span class="sr-only">{cell.name}</span>
168
+ </a>
169
+ )
170
+ );
171
+ })}
109
172
  </li>
110
173
  ))}
111
174
  </ol>
@@ -224,4 +287,59 @@ const hours = (range: { min: number; max: number }) => {
224
287
  text-align: start;
225
288
  }
226
289
  }
290
+
291
+ /* The key under the counts: the glyphs the list uses, in the accent. */
292
+ .contents-key {
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: 0.25rem 1.25rem;
296
+ margin: -1.1rem 0 0;
297
+ padding: 0;
298
+ list-style: none;
299
+ color: var(--sl-color-gray-2);
300
+ font-size: var(--sl-text-sm);
301
+ }
302
+
303
+ .contents-key li {
304
+ margin: 0;
305
+ }
306
+
307
+ .contents-key :global(.contents-glyph) {
308
+ color: var(--sl-color-text-accent);
309
+ }
310
+
311
+ /* The slides column (`global.css`): one 1.7rem column per lane at the
312
+ row's end. The description keeps to the title's and time's columns. A
313
+ bar's cell spans a row's title and description, and reaches the line
314
+ between two rows: the row's bottom padding and the list's gap below it,
315
+ the next row's top border and padding above. */
316
+ .course-contents[data-deck-lanes] {
317
+ --deck-lane-rows: 2;
318
+ --deck-lane-down: 0.5rem;
319
+ --deck-lane-up: calc(0.4rem + 1px);
320
+ }
321
+
322
+ .course-contents[data-deck-lanes] .course-part li {
323
+ grid-template-columns: 1.9rem minmax(0, 1fr) auto repeat(var(--deck-lanes), 1.7rem);
324
+ }
325
+
326
+ .course-contents[data-deck-lanes] .course-part li > p {
327
+ grid-column: 2 / 4;
328
+ }
329
+
330
+ /* On a phone the time goes under the title, and the cells span the three
331
+ rows. */
332
+ @media (max-width: 40rem) {
333
+ .course-contents[data-deck-lanes] {
334
+ --deck-lane-rows: 3;
335
+ }
336
+
337
+ .course-contents[data-deck-lanes] .course-part li {
338
+ grid-template-columns: 1.9rem minmax(0, 1fr) repeat(var(--deck-lanes), 1.7rem);
339
+ }
340
+
341
+ .course-contents[data-deck-lanes] .course-part li > p {
342
+ grid-column: 2;
343
+ }
344
+ }
227
345
  </style>
@@ -2,8 +2,29 @@
2
2
  import { getCollection } from 'astro:content';
3
3
  import { loadManifest } from '../compiler/manifest';
4
4
  import { isLessonSlug, lessonIdFromSlug } from '../compiler/lesson-derivation';
5
+ import { competencyGraphsByPart } from '../curriculum/competency-graph';
5
6
  import Diagram, { type DiagramEdge, type DiagramNode } from './diagram/Diagram.astro';
6
7
 
8
+ export interface Props {
9
+ /**
10
+ * `'whole'` (the default) draws the competency graph as one diagram;
11
+ * `'per-part'` draws one diagram per part, in sidebar order, holding the
12
+ * competencies its lessons teach first and, labelled with their part, the
13
+ * earlier competencies they build on. A graph too wide for the column as
14
+ * one diagram (rule V8) usually fits as one per part.
15
+ */
16
+ graph?: 'whole' | 'per-part';
17
+ /** The layout direction of every competency diagram; `Diagram`'s default is `'TB'`. */
18
+ direction?: 'TB' | 'LR';
19
+ /**
20
+ * Passed to every competency diagram: the width, in px, above which rule V8
21
+ * warns, or `false` to let a whole-course graph scroll without a warning.
22
+ */
23
+ warnAboveWidth?: number | false;
24
+ }
25
+
26
+ const { graph = 'whole', direction, warnAboveWidth } = Astro.props;
27
+
7
28
  const [tracks, docs, competencies, manifest] = await Promise.all([
8
29
  getCollection('tracks'),
9
30
  getCollection('docs'),
@@ -37,6 +58,15 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
37
58
  to: data.id,
38
59
  })),
39
60
  );
61
+
62
+ const partGraphs =
63
+ graph === 'per-part'
64
+ ? competencyGraphsByPart(
65
+ manifest.parts,
66
+ new Map(manifest.lessons.map((lesson) => [lesson.id, lesson.teaches])),
67
+ competencies.map(({ data }) => data),
68
+ )
69
+ : [];
40
70
  ---
41
71
 
42
72
  {
@@ -127,12 +157,30 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
127
157
  </p>
128
158
 
129
159
  {
130
- competencyNodes.length > 0 && (
160
+ partGraphs.map((partGraph) => (
161
+ <>
162
+ <h3 id={`${partGraph.id}-heading`}>{partGraph.title}</h3>
163
+ <Diagram
164
+ id={partGraph.id}
165
+ caption={`The competencies taught in ${partGraph.title}: an arrow runs from a prerequisite to the competency that builds on it, and a box labelled with an earlier part is a competency taught there`}
166
+ nodes={partGraph.nodes}
167
+ edges={partGraph.edges}
168
+ direction={direction}
169
+ warnAboveWidth={warnAboveWidth}
170
+ />
171
+ </>
172
+ ))
173
+ }
174
+
175
+ {
176
+ graph === 'whole' && competencyNodes.length > 0 && (
131
177
  <Diagram
132
178
  id="competency-graph"
133
179
  caption="The competency dependency graph: an arrow runs from a prerequisite to the competency that builds on it"
134
180
  nodes={competencyNodes}
135
181
  edges={competencyEdges}
182
+ direction={direction}
183
+ warnAboveWidth={warnAboveWidth}
136
184
  />
137
185
  )
138
186
  }
@@ -27,7 +27,7 @@
27
27
  * ```
28
28
  */
29
29
  import { loadManifest, type ManifestLessonFloat } from '../compiler/manifest';
30
- import { FLOAT_KIND_WORDS, type FloatKind } from './float-name';
30
+ import { FLOAT_KIND_WORDS, type FloatKind } from '../reference/float-name';
31
31
 
32
32
  interface Props {
33
33
  /** Heading level for each part's name; a chapter's is one below. Defaults to `h2`. */
@@ -7,7 +7,9 @@
7
7
  * collection — chapter order, titles, descriptions, float counts, and which
8
8
  * parts this one builds on or feeds. A course authors the page's prose; it
9
9
  * never re-types the list, so a renamed chapter or a new prerequisite cannot
10
- * leave a stale copy behind on the part page.
10
+ * leave a stale copy behind on the part page. When the course has a deck, the
11
+ * list has the contents list's glyphs and slides column (`deck-lane.ts`,
12
+ * `CourseContents.astro`).
11
13
  *
12
14
  * Usage, in `content/docs/<part>/index.mdx`:
13
15
  *
@@ -38,6 +40,7 @@
38
40
  */
39
41
  import { getCollection } from 'astro:content';
40
42
  import { loadManifest, type ManifestPart } from '../compiler/manifest';
43
+ import { deckLanes, laneLink, titleGlyph } from './deck-lane';
41
44
 
42
45
  interface Props {
43
46
  /**
@@ -53,9 +56,14 @@ interface Props {
53
56
  readonly chain?: readonly string[];
54
57
  /** Set `false` to drop the rail and keep only the reading list. */
55
58
  readonly rail?: boolean;
59
+ /**
60
+ * Set `false` to drop the slides column and the glyphs before the titles.
61
+ * They show only when the course has a deck.
62
+ */
63
+ readonly slides?: boolean;
56
64
  }
57
65
 
58
- const { part: partProp, chain = [], rail = true } = Astro.props;
66
+ const { part: partProp, chain = [], rail = true, slides = true } = Astro.props;
59
67
 
60
68
  const routeSlug = Astro.locals.starlightRoute.entry.id;
61
69
  const partDir = partProp ?? routeSlug.replace(/\/index$/, '').split('/')[0];
@@ -82,6 +90,11 @@ const chapters = part.lessons
82
90
  .map((id) => lessonById.get(id))
83
91
  .filter((lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined);
84
92
 
93
+ // As in the contents list: glyphs only in a course with a deck, and the
94
+ // column only when this part has one.
95
+ const glyphs = slides && manifest.decks.length > 0;
96
+ const lanes = glyphs ? deckLanes({ lessons: chapters, decks: manifest.decks, part }) : [];
97
+
85
98
  const competencyById = new Map(
86
99
  competencyEntries.map(({ data }) => [data.id, data]),
87
100
  );
@@ -169,11 +182,15 @@ const hasRail =
169
182
  </ul>
170
183
 
171
184
  <div class="part-body">
172
- <ol class="part-chapters">
185
+ <ol
186
+ class="part-chapters"
187
+ data-deck-lanes={lanes.length > 0 ? lanes.length : undefined}
188
+ style={lanes.length > 0 ? `--deck-lanes: ${lanes.length}` : undefined}
189
+ >
173
190
  {
174
- chapters.map((lesson) => (
191
+ chapters.map((lesson, row) => (
175
192
  <li>
176
- <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
193
+ <a href={`${base}/${lesson.slug}/`}>{glyphs && <span class={`contents-glyph contents-glyph--${titleGlyph(lesson)}`} aria-hidden="true" />}{lesson.title}</a>
177
194
  <p>{lesson.description}</p>
178
195
  <p class="chapter-meta">
179
196
  {lesson.studyMinutes && (
@@ -192,6 +209,16 @@ const hasRail =
192
209
  </span>
193
210
  )}
194
211
  </p>
212
+ {lanes.map((lane, index) => {
213
+ const cell = lane[row];
214
+ return (
215
+ cell && (
216
+ <a {...laneLink(cell, index, lanes.length, base)}>
217
+ <span class="sr-only">{cell.name}</span>
218
+ </a>
219
+ )
220
+ );
221
+ })}
195
222
  </li>
196
223
  ))
197
224
  }
@@ -342,6 +369,25 @@ const hasRail =
342
369
  line-height: 1.5;
343
370
  }
344
371
 
372
+ /* The slides column (`global.css`): one 1.7rem column per lane at the
373
+ row's end, the title, description and meta line kept to the second
374
+ column. A bar's cell spans the three, and reaches the line under its
375
+ row: the row's bottom padding, then the next row's top padding and the
376
+ line itself above. */
377
+ .part-chapters[data-deck-lanes] {
378
+ --deck-lane-rows: 3;
379
+ --deck-lane-down: 0.85rem;
380
+ --deck-lane-up: calc(0.85rem + 1px);
381
+ }
382
+
383
+ .part-chapters[data-deck-lanes] > li {
384
+ grid-template-columns: 2.25rem minmax(0, 1fr) repeat(var(--deck-lanes), 1.7rem);
385
+ }
386
+
387
+ .part-chapters[data-deck-lanes] > li > :not(.deck-lane) {
388
+ grid-column: 2;
389
+ }
390
+
345
391
  .chapter-meta {
346
392
  display: flex;
347
393
  flex-wrap: wrap;
@@ -7,6 +7,8 @@ import { renderNotationMath } from '../notation/render-notation-math';
7
7
  import type { MixedContentNode } from '../notation/render-notation-math';
8
8
  import { knowledgeCheckAnchorId } from '../../reference/knowledge-checks.mjs';
9
9
  import BlockHeading from '../BlockHeading.astro';
10
+ import { deckOfRoute } from '../deck/deck-context';
11
+ import { routeIdFromLocals } from '../../reference/page-notation';
10
12
  import AssessmentRunner from './AssessmentRunner';
11
13
 
12
14
  interface Props {
@@ -17,17 +19,29 @@ interface Props {
17
19
  * `[[check-…]]` references; undeclared legacy embeds keep a local fallback.
18
20
  */
19
21
  id?: string;
22
+ /**
23
+ * The lesson whose checks the set asks, by its id. Set by the engine on a
24
+ * part's deck (`deck-remark.mjs`): the chapter the frame's section covers.
25
+ * Elsewhere the page's own lesson.
26
+ */
27
+ lesson?: string;
20
28
  }
21
29
 
22
30
  const explicitId = Astro.props.id;
23
31
 
24
32
  const base = import.meta.env.BASE_URL.replace(/\/$/, '');
25
- const lessonId = Astro.url.pathname
26
- .replace(new RegExp(`^${base}/`), '')
27
- .replace(/\/$/, '')
28
- .replace(/\//g, '.');
29
-
30
33
  const manifest = await loadManifest();
34
+ // On a deck (docs/authoring/deck.md) the checks are its lesson's, and a check
35
+ // is a question frame: its label, not a heading of its own. On a part's deck
36
+ // they are the chapter's whose section the frame is in.
37
+ const deck = deckOfRoute(manifest, routeIdFromLocals(Astro.locals));
38
+ const lessonId =
39
+ Astro.props.lesson ??
40
+ deck?.lessonId ??
41
+ Astro.url.pathname
42
+ .replace(new RegExp(`^${base}/`), '')
43
+ .replace(/\/$/, '')
44
+ .replace(/\//g, '.');
31
45
  // A check folds like a section, by the course's `disclosure.knowledgeChecks`
32
46
  // (collapsible and open unless the course says otherwise).
33
47
  const { knowledgeChecks } = manifest.disclosure ?? DISCLOSURE_DEFAULTS;
@@ -101,16 +115,28 @@ function renderedContent(
101
115
  {
102
116
  entries.map(({ entry, number, anchorId }) => (
103
117
  <>
104
- <BlockHeading
105
- kind="check"
106
- anchorId={anchorId}
107
- title={entry.data.title}
108
- number={number}
109
- collapsible={knowledgeChecks.collapsible}
110
- defaultOpen={knowledgeChecks.defaultOpen}
111
- card="accent"
112
- class="assessment-heading-wrapper"
113
- />
118
+ {deck ? (
119
+ <p
120
+ class="deck-check__label"
121
+ id={anchorId}
122
+ data-block-kind="check"
123
+ data-check-number={number}
124
+ data-check-title={entry.data.title}
125
+ >
126
+ <span class="deck-check__kind">Knowledge check {number}</span> {entry.data.title}
127
+ </p>
128
+ ) : (
129
+ <BlockHeading
130
+ kind="check"
131
+ anchorId={anchorId}
132
+ title={entry.data.title}
133
+ number={number}
134
+ collapsible={knowledgeChecks.collapsible}
135
+ defaultOpen={knowledgeChecks.defaultOpen}
136
+ card="accent"
137
+ class="assessment-heading-wrapper"
138
+ />
139
+ )}
114
140
  <AssessmentRunner
115
141
  client:visible
116
142
  assessmentId={entry.data.id}