explico 0.13.0 → 0.14.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 (239) hide show
  1. package/CHANGELOG.md +894 -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 +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1087 -14
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
package/README.md CHANGED
@@ -51,15 +51,39 @@ What it gives a course:
51
51
  tick-labeled axes, log scales, or several series sharing a scale; Observable
52
52
  Plot pays off once a course is authoring many charts per lecture. Pick per
53
53
  figure — nothing in the engine assumes one plotting approach.
54
+ - **Slides from the same lesson** — a lesson's deck, `<lesson>.deck.mdx`, is
55
+ its slide edition: frames on a fixed canvas, steps that reveal, drive a lab
56
+ or light up a symbol, speaker notes, and the chapter's equations and figures
57
+ recalled with their numbers. The deck's page presents full screen, with a
58
+ presenter window, and as one offline HTML file; it exports to beamer slides
59
+ and a handout with every symbol linked to its entry, and to Marp's Markdown.
60
+ Rules D1–D10 keep frames short, and the build fails a frame that does not
61
+ fit. A lesson can also be slides only (`docs/authoring/deck.md`).
54
62
  - **LMS / analytics / identity seams** — framework-free interfaces shipped as
55
63
  no-ops; a host supplies real implementations without touching a component.
56
64
  - **A `content` CLI** — `status`, `diagnostics`, `warnings`, `context <lesson>`,
57
65
  `check <lesson>` (compiles MDX → remark → KaTeX and fails loudly),
58
- `new lesson|term`.
66
+ `new lesson|term|deck`.
59
67
 
60
68
  The prerequisite graph itself is authored by the lecturer or an AI; the engine
61
69
  validates it (DAG, ordering, coverage, orphans) rather than inferring it.
62
70
 
71
+ ## Start from a working course
72
+
73
+ Two complete courses built on explico are downloadable, always at their newest
74
+ version, from **[https://danieltuzes.github.io/explico/start/](https://danieltuzes.github.io/explico/start/)**:
75
+
76
+ - [the playground](https://danieltuzes.github.io/explico/downloads/explico-playground-latest.zip) — every engine feature on a short physics course, at
77
+ the engine's latest release, standalone (`pnpm install && pnpm
78
+ validate:content && pnpm dev`). A path in these docs that starts with
79
+ `playground/` is a file in it.
80
+ - [credit-and-derivatives](https://danieltuzes.github.io/explico/downloads/credit-and-derivatives-latest.zip) — a full course as it is taught
81
+ ([repository](https://github.com/danieltuzes/credit-and-derivatives)).
82
+
83
+ [`sources.json`](https://danieltuzes.github.io/explico/downloads/sources.json) lists both with the engine version, source commit and
84
+ checksum. An agent reads [`docs/authoring/agents.md`](https://cdn.jsdelivr.net/npm/explico@latest/docs/authoring/agents.md) first
85
+ (`node_modules/explico/docs/authoring/agents.md` once installed).
86
+
63
87
  ## Install
64
88
 
65
89
  ```bash
@@ -72,7 +96,7 @@ Peer dependencies (the course provides them, pinned): `astro`,
72
96
 
73
97
  ## Use
74
98
 
75
- A course provides `content/` (the six collections + `content/domain/` +
99
+ A course provides `content/` (the engine collections + `content/domain/` +
76
100
  `content/course.config.ts`) and wires the engine in three places:
77
101
 
78
102
  ```ts
@@ -97,36 +121,49 @@ import rehypeHeadingAnchors from 'explico/reference/rehype-heading-anchors.mjs';
97
121
  import { courseConfig } from './content/course.config.ts';
98
122
 
99
123
  const manifest = await loadManifest(courseConfig);
100
- // markdown.remarkPlugins: [
101
- // remarkMath,
102
- // remarkShortNote,
103
- // remarkMemo,
104
- // // `createRemarkNotationOptions` reads every numbering input off one
105
- // // manifest — the equation / table / figure number tables *and* the part
106
- // // each lesson sits in — so they can't drift apart. `strict: true` fails
107
- // // the build when a catalogued lesson has equations/tables/figures but no
108
- // // numbering or cross-page entry, instead of silently numbering it by
109
- // // `##` section while references to it read the part scheme.
110
- // // `base` is the Astro `base` of a subpath deployment (e.g. a GitHub
111
- // // Pages project site); the links these plugins write 404 without it.
112
- // [remarkNotation, createRemarkNotationOptions(manifest, { strict: true, base })],
113
- // // The generated References section's first state, from the course's
114
- // // `disclosure` settings block (`CourseConfig.disclosure`).
115
- // [remarkCitation, {
116
- // sources: manifest.sources,
117
- // defaultOpen: courseConfig.disclosure?.references?.defaultOpen === true,
118
- // }],
119
- // // Last: renders `:::note` / `:::tip` / `:::caution` / `:::danger` as
120
- // // Starlight's `<Aside>` (Starlight's own pass skips `content/docs/`).
121
- // remarkAsides,
122
- // ]
124
+ // import { unified } from '@astrojs/markdown-remark';
125
+ // import remarkMath from 'remark-math';
126
+ // import rehypeKatex from 'rehype-katex';
127
+ //
128
+ // markdown: {
129
+ // // Astro 7.3 deprecates the flat `markdown.remarkPlugins` / `rehypePlugins`
130
+ // // lists; the plugins go to `unified()` instead.
131
+ // processor: unified({
132
+ // remarkPlugins: [
133
+ // remarkMath,
134
+ // remarkShortNote,
135
+ // remarkMemo,
136
+ // // `createRemarkNotationOptions` reads every numbering input off one
137
+ // // manifest — the equation / table / figure number tables *and* the part
138
+ // // each lesson sits in — so they can't drift apart. `strict: true` fails
139
+ // // the build when a catalogued lesson has equations/tables/figures but no
140
+ // // numbering or cross-page entry, instead of silently numbering it by
141
+ // // `##` section while references to it read the part scheme.
142
+ // // `base` is the Astro `base` of a subpath deployment (e.g. a GitHub
143
+ // // Pages project site); the links these plugins write 404 without it.
144
+ // [remarkNotation, createRemarkNotationOptions(manifest, { strict: true, base })],
145
+ // // The generated References section's first state, from the course's
146
+ // // `disclosure` settings block (`CourseConfig.disclosure`).
147
+ // [remarkCitation, {
148
+ // sources: manifest.sources,
149
+ // defaultOpen: courseConfig.disclosure?.references?.defaultOpen === true,
150
+ // }],
151
+ // // Last: renders `:::note` / `:::tip` / `:::caution` / `:::danger` as
152
+ // // Starlight's `<Aside>` (Starlight's own pass skips `content/docs/`).
153
+ // remarkAsides,
154
+ // ],
155
+ // rehypePlugins: [
156
+ // [rehypeKatex, createNotationKatexOptions()],
157
+ // rehypeFailKatexErrors,
158
+ // // Heading collapse defaults: the same `disclosure` block.
159
+ // [rehypeHeadingAnchors, { defaultCollapsible: courseConfig.disclosure }],
160
+ // ],
161
+ // }),
162
+ // },
123
163
  // … integrations: [explicoContentRefresh(courseConfig, manifest),
124
164
  // starlight({ customCss: ['explico/styles/global.css'],
125
165
  // components: { Footer: 'explico/components/starlight/LessonFooter.astro',
126
166
  // SocialIcons: 'explico/components/starlight/LayoutSocialIcons.astro', … } })
127
- // … rehypePlugins: [[rehypeKatex, createNotationKatexOptions()], rehypeFailKatexErrors,
128
- // // Heading collapse defaults: the same `disclosure` block.
129
- // [rehypeHeadingAnchors, { defaultCollapsible: courseConfig.disclosure }]]
130
167
  ```
131
168
 
132
169
  `loadManifest(courseConfig)` always recompiles current content before Astro
@@ -185,6 +222,68 @@ or, without one, the numbers and short captions of the floats inside
185
222
  </Fold>
186
223
  ```
187
224
 
225
+ ### A page for paths that miss
226
+
227
+ Starlight looks for a docs entry named `404` on every build. Give the course a
228
+ `content/docs/404.mdx`; without it, Astro falls back correctly but prints
229
+ `Entry docs → 404 was not found`. This small page uses Astro's `BASE_URL`, so
230
+ its links work both at `/` and below a deployment `base`:
231
+
232
+ ```mdx
233
+ ---
234
+ title: "404: Page outside the syllabus"
235
+ description: The requested course page could not be found.
236
+ template: splash
237
+ hero:
238
+ title: "404"
239
+ tagline: We followed this URL as far as the course map goes. It does not land on a page.
240
+ pagefind: false
241
+ sidebar:
242
+ hidden: true
243
+ editUrl: false
244
+ ---
245
+
246
+ import { LinkButton } from '@astrojs/starlight/components';
247
+
248
+ <div style="display: flex; justify-content: center;">
249
+ <LinkButton href={import.meta.env.BASE_URL} icon="left-arrow" iconPlacement="start">
250
+ Return to the course
251
+ </LinkButton>
252
+ </div>
253
+ ```
254
+
255
+ Let the docs entry own `/404.html` by turning off Starlight's otherwise
256
+ duplicate special route:
257
+
258
+ ```js
259
+ starlight({
260
+ disable404Route: process.env.NODE_ENV === 'production',
261
+ // …the course's other Starlight settings
262
+ })
263
+ ```
264
+
265
+ That keeps Starlight's catch-all page during development. In a static build,
266
+ the authored docs entry supplies `/404.html`; disabling the duplicate injected
267
+ route there prevents a route-collision warning.
268
+
269
+ Set Astro's `site` to the published origin too; Starlight's sitemap integration
270
+ skips itself, with a warning, when it is missing. Keep a subpath in `base`, not
271
+ in `site`:
272
+
273
+ ```js
274
+ export default defineConfig({
275
+ site: 'https://example.edu',
276
+ base: '/course/',
277
+ // …
278
+ });
279
+ ```
280
+
281
+ `explico/content-config` also exports Starlight's `i18n` collection. It supplies
282
+ an empty English fallback for a course with no translations, avoiding Astro's
283
+ empty-collection warning. To translate the interface, add
284
+ `content/i18n/<locale>.json` (or `.yml` / `.yaml`); those authored entries are
285
+ loaded instead.
286
+
188
287
  ### Lesson exports
189
288
 
190
289
  A course can opt into self-contained HTML downloads of each chapter, each part
@@ -233,6 +332,12 @@ the same file becomes a link into it, whose hover preview works offline. Each
233
332
  page is a section with `content-visibility: auto`, which keeps a long course
234
333
  responsive, and starts a new sheet when printed.
235
334
 
335
+ A lesson's deck (`docs/authoring/deck.md`) has a row of its own in the menu, on
336
+ its page and on its chapter's: `downloads/<lesson>-slides.html` presents
337
+ offline, with its notes, labs and checks; with `latex`, `-slides.pdf` and the
338
+ handout `-slides-notes.pdf` compile in beamer from `-slides-latex.zip`; with
339
+ `markdown`, `-slides-markdown.zip` holds the deck in Marp's dialect.
340
+
236
341
  The **Print / save as PDF** action calls the browser's native print dialog; it
237
342
  does not download or build a `.pdf` file itself. The print layout removes site
238
343
  chrome and prints every disclosure as the reader left it on screen: an open
@@ -311,9 +416,74 @@ and still writes the source zips. On GitHub Actions:
311
416
  - run: pnpm build
312
417
  ```
313
418
 
419
+ A course sets no cookies and loads nothing from another site. It asks each
420
+ reader once, at the bottom of the window, whether it may remember their
421
+ reading in their browser (the chapters they opened, the cards they keep or
422
+ carry), and keeps nothing of it beyond the tab without a yes; settings the
423
+ reader chooses are kept either way, and the `[?]` panel changes the answer or
424
+ forgets everything. `readingMemory: "off"` in the course settings never asks.
425
+ [`docs/privacy.md`](docs/privacy.md) lists every key and when it is written,
426
+ for a course's own privacy notice.
427
+
428
+ Each part of the page has a colour of its own, so a reader can tell the
429
+ course's tools, its contents, the page's outline and the text apart:
430
+ `appearance.zones` in the course settings is `tonal` (the default, neutral
431
+ grey steps), `accent-header` (the header in the course's accent), `paper`
432
+ (the text as a sheet on a desk) or `none`, and `appearance.slides` gives each
433
+ slide on a deck's page an edge and a shadow (`lifted`, `edge` or `sorter`).
434
+
435
+ Every document says which version its content is at (rule C5): "From
436
+ *Course*, unchanged since version 0.2.0." under a chapter's title, the release
437
+ its content last changed in, or "with changes after version 0.4.0, not yet
438
+ released" while `content/CHANGELOG.md`'s `## [Unreleased]` holds a change to
439
+ it; a part's and the course's PDF carry the newest among their pages, and the
440
+ `[?]` panel shows the same line on the page. The record behind it is
441
+ `content/versions.json`, which `pnpm validate:content` writes and the course
442
+ commits with each content change, like a lockfile; in CI a stale record fails
443
+ the build.
444
+
445
+ Every build compiles every document, one LuaLaTeX run per chapter, part and
446
+ the course. `EXPLICO_LATEX_CACHE=<directory>` keeps each compile there, keyed
447
+ by every file of the document's folder, the TeX installation and the
448
+ document's date, and reuses it while they are unchanged: a push that changes
449
+ one chapter compiles that chapter, its part and the course, and so does a
450
+ release, since an unchanged document keeps its version line and its date. The build's summary line says
451
+ how many it compiled and reused. `EXPLICO_LATEX_CACHE_SALT` is the escape
452
+ hatch for what the key cannot see, such as a package updated with `tlmgr`.
453
+ Entries no build has used for a week are deleted. In CI, restore the
454
+ directory before the build and save it under a new key after:
455
+
456
+ ```yaml
457
+ - uses: actions/cache@v4
458
+ with:
459
+ path: .cache/explico-latex
460
+ key: explico-latex-${{ github.sha }}
461
+ restore-keys: explico-latex-
462
+ - run: pnpm build
463
+ env:
464
+ EXPLICO_LATEX_CACHE: .cache/explico-latex
465
+ ```
466
+
314
467
  ```jsonc
315
- // package.json — the read-side CLI
316
- "scripts": { "validate:content": "tsx scripts/compile-manifest.ts" }
468
+ // package.json
469
+ "scripts": {
470
+ "validate:content": "tsx scripts/compile-manifest.ts",
471
+ "content": "tsx scripts/content.ts"
472
+ }
473
+ ```
474
+
475
+ Both scripts are three lines; the engine owns what they run:
476
+
477
+ ```ts
478
+ // scripts/compile-manifest.ts
479
+ import { runValidateContent } from 'explico/compiler/validate-content';
480
+ import { courseConfig } from '../content/course.config';
481
+ process.exitCode = await runValidateContent(courseConfig);
482
+
483
+ // scripts/content.ts — `pnpm content help` lists every verb
484
+ import { runContentCli } from 'explico/compiler/cli-main';
485
+ import { courseConfig } from '../content/course.config';
486
+ process.exitCode = await runContentCli(courseConfig);
317
487
  ```
318
488
 
319
489
  Diagnostics have three distinct severities: an `error` blocks validation because
@@ -1,6 +1,7 @@
1
1
  import { AstroConfig, AstroIntegration } from 'astro';
2
- import { C as CourseConfig } from '../course-config-rRtME5UV.js';
2
+ import { C as CourseConfig } from '../course-config-BLtIdkei.js';
3
3
  import { Manifest } from './manifest.js';
4
+ import { RenderDiagnostic } from './render-diagnostics.js';
4
5
  import '../rules/catalog.js';
5
6
  import '../rules/course-rules.js';
6
7
  import '../curriculum/validation.js';
@@ -24,6 +25,10 @@ import '../reference/mixed-content.js';
24
25
  import '../reference/gloss.js';
25
26
  import '../rules/waivers.js';
26
27
  import './knowledge-version.js';
28
+ import './page-versions.js';
29
+ import './decks.js';
30
+ import '../reference/decks-validate.js';
31
+ import '../reference/deck-source.js';
27
32
  import './lesson-derivation.js';
28
33
 
29
34
  /** Match only a configured page artifact below Astro's deployment base. */
@@ -48,6 +53,14 @@ type MarkdownConfig = AstroConfig['markdown'];
48
53
  * text with no signal anywhere.
49
54
  */
50
55
  declare function assertRemarkPluginsWired(manifest: Pick<Manifest, 'abbreviations' | 'sources' | 'notation'>, markdown: MarkdownConfig): void;
56
+ /**
57
+ * Check every built page, and return the findings only a rendered page gives
58
+ * (`render-diagnostics.ts`) for `content warnings`. A page that fails a check
59
+ * does not stop the walk: every page is read first, then the build throws once
60
+ * with the whole list, so it reports each page at once and not one per run.
61
+ * Exported for the test.
62
+ */
63
+ declare function validateGeneratedPages(directory: URL, manifest: Pick<Manifest, 'lessons'>, warn: (message: string) => void): Promise<RenderDiagnostic[]>;
51
64
  /**
52
65
  * Restart Astro when an authored content file changes. Astro reloads MDX on
53
66
  * its own, but Explico's manifest is created by `astro.config.mjs`; a restart
@@ -58,4 +71,4 @@ declare function assertRemarkPluginsWired(manifest: Pick<Manifest, 'abbreviation
58
71
  */
59
72
  declare function explicoContentRefresh(courseConfig: CourseConfig, manifest: Manifest): AstroIntegration;
60
73
 
61
- export { assertLocalNotationParity, assertNotationPayloadHtml, assertRemarkPluginsWired, explicoContentRefresh, matchStandaloneDownload };
74
+ export { assertLocalNotationParity, assertNotationPayloadHtml, assertRemarkPluginsWired, explicoContentRefresh, matchStandaloneDownload, validateGeneratedPages };
@@ -8,7 +8,10 @@ import {
8
8
  writeManifest
9
9
  } from "./manifest.js";
10
10
  import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
- import { assertFigureSymbolsBound } from "../reference/figure-symbols.js";
11
+ import {
12
+ assertNoUnboundFigureSymbols,
13
+ unboundFigureSymbols
14
+ } from "../reference/figure-symbols.js";
12
15
  import { undrawnFigurePictures, undrawnFigureWarning } from "../reference/figure-pictures.js";
13
16
  import {
14
17
  pageRenderDiagnostics,
@@ -20,7 +23,8 @@ import { writeLatexExports } from "./exports/latex-export.js";
20
23
  import { writeMarkdownExports } from "./exports/markdown-export.js";
21
24
  import { prepareChapters } from "./exports/export-chapters.js";
22
25
  import { writeDownloadIndex } from "./exports/download-index.js";
23
- import { standaloneFilename, standalonePageSlugs } from "./exports/export-pages.js";
26
+ import { checkFrameFit } from "./exports/deck-fit.js";
27
+ import { standaloneDownloads, standaloneFilename } from "./exports/export-pages.js";
24
28
  import remarkAbbreviations from "../reference/remark-abbreviations.mjs";
25
29
  import remarkCitation from "../reference/remark-citation.mjs";
26
30
  import remarkNotation from "../reference/remark-notation.mjs";
@@ -124,17 +128,17 @@ function assertRemarkPluginsWired(manifest, markdown) {
124
128
  const missing = [];
125
129
  if (manifest.abbreviations.length > 0 && !remarkPluginIsWired(remarkPlugins, remarkAbbreviations)) {
126
130
  missing.push(
127
- `remarkAbbreviations is missing from markdown.remarkPlugins, but manifest.abbreviations has ${manifest.abbreviations.length} entr${manifest.abbreviations.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-abbreviations.mjs' and add [remarkAbbreviations, { abbreviations: manifest.abbreviations }] to markdown.remarkPlugins \u2014 see docs/authoring/migrating.md#4-rule-n14.`
131
+ `remarkAbbreviations is missing from the remark plugins of astro.config.mjs, but manifest.abbreviations has ${manifest.abbreviations.length} entr${manifest.abbreviations.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-abbreviations.mjs' and add [remarkAbbreviations, { abbreviations: manifest.abbreviations }] to the \`remarkPlugins\` of \`markdown.processor: unified({ \u2026 })\` \u2014 see docs/authoring/migrating.md#4-rule-n14.`
128
132
  );
129
133
  }
130
134
  if (manifest.sources.length > 0 && !remarkPluginIsWired(remarkPlugins, remarkCitation)) {
131
135
  missing.push(
132
- `remarkCitation is missing from markdown.remarkPlugins, but manifest.sources has ${manifest.sources.length} entr${manifest.sources.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-citation.mjs' and add [remarkCitation, { sources: manifest.sources }] to markdown.remarkPlugins.`
136
+ `remarkCitation is missing from the remark plugins of astro.config.mjs, but manifest.sources has ${manifest.sources.length} entr${manifest.sources.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-citation.mjs' and add [remarkCitation, { sources: manifest.sources }] to the \`remarkPlugins\` of \`markdown.processor: unified({ \u2026 })\`.`
133
137
  );
134
138
  }
135
139
  if (manifest.notation.raw.length > 0 && !remarkPluginIsWired(remarkPlugins, remarkNotation)) {
136
140
  missing.push(
137
- `remarkNotation is missing from markdown.remarkPlugins, but manifest.notation.raw has ${manifest.notation.raw.length} entr${manifest.notation.raw.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-notation.mjs' and add [remarkNotation, createRemarkNotationOptions(manifest)] to markdown.remarkPlugins.`
141
+ `remarkNotation is missing from the remark plugins of astro.config.mjs, but manifest.notation.raw has ${manifest.notation.raw.length} entr${manifest.notation.raw.length === 1 ? "y" : "ies"}. Import it from 'explico/reference/remark-notation.mjs' and add [remarkNotation, createRemarkNotationOptions(manifest)] to the \`remarkPlugins\` of \`markdown.processor: unified({ \u2026 })\`.`
138
142
  );
139
143
  }
140
144
  if (missing.length === 0) return;
@@ -146,16 +150,27 @@ ${missing.map((line) => ` - ${line}`).join("\n")}`
146
150
  async function validateGeneratedPages(directory, manifest, warn) {
147
151
  const root = fileURLToPath(directory);
148
152
  const rendered = [];
153
+ const failures = [];
154
+ const unbound = [];
155
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
149
156
  const walk = async (path) => {
150
- for (const entry of await readdir(path, { withFileTypes: true })) {
157
+ const entries = (await readdir(path, { withFileTypes: true })).sort(
158
+ (a, b) => a.name.localeCompare(b.name)
159
+ );
160
+ for (const entry of entries) {
151
161
  const child = join(path, entry.name);
152
162
  if (entry.isDirectory()) {
153
163
  await walk(child);
154
164
  } else if (entry.isFile() && entry.name.endsWith(".html")) {
155
165
  const html = await readFile(child, "utf8");
156
166
  const page = relative(root, child);
157
- assertNotationPayloadHtml(page, html);
158
- assertFigureSymbolsBound(page, html);
167
+ try {
168
+ assertNotationPayloadHtml(page, html);
169
+ } catch (error) {
170
+ failures.push(messageOf(error));
171
+ }
172
+ const figures = unboundFigureSymbols(html);
173
+ if (figures.length > 0) unbound.push({ page, figures });
159
174
  const undrawn = undrawnFigurePictures(html);
160
175
  if (undrawn.length > 0) warn(undrawnFigureWarning(page, undrawn));
161
176
  rendered.push(...pageRenderDiagnostics(manifest, page, html));
@@ -163,6 +178,12 @@ async function validateGeneratedPages(directory, manifest, warn) {
163
178
  }
164
179
  };
165
180
  await walk(root);
181
+ try {
182
+ assertNoUnboundFigureSymbols(unbound);
183
+ } catch (error) {
184
+ failures.push(messageOf(error));
185
+ }
186
+ if (failures.length > 0) throw new Error(failures.join("\n\n"));
166
187
  return rendered;
167
188
  }
168
189
  function replaceInPlace(target, source) {
@@ -260,7 +281,7 @@ function explicoContentRefresh(courseConfig, manifest) {
260
281
  const slug = matchStandaloneDownload(
261
282
  pathname,
262
283
  configuredDevelopment.base,
263
- standalonePageSlugs(manifest)
284
+ standaloneDownloads(manifest).map(({ download }) => download)
264
285
  );
265
286
  if (!slug) return next();
266
287
  try {
@@ -269,7 +290,7 @@ function explicoContentRefresh(courseConfig, manifest) {
269
290
  );
270
291
  developmentDownloads ??= buildDevelopmentDownloads(
271
292
  configuredDevelopment,
272
- standalonePageSlugs(manifest)
293
+ standaloneDownloads(manifest).map(({ download }) => download)
273
294
  );
274
295
  const currentDownloads = developmentDownloads;
275
296
  currentDownloads.catch(() => {
@@ -310,6 +331,13 @@ ${message}
310
331
  for (const diagnostic of writeRenderDiagnostics(rendered)) {
311
332
  logger.warn(`[${diagnostic.code}] ${diagnostic.file}: ${diagnostic.message}`);
312
333
  }
334
+ const overflowing = await checkFrameFit(fileURLToPath(dir), manifest, { logger });
335
+ if (overflowing.length > 0) {
336
+ throw new Error(
337
+ `${overflowing.length} deck frame(s) do not fit their canvas (rule D2, frame-overflow):
338
+ ` + overflowing.map((finding) => ` - ${finding.message}`).join("\n")
339
+ );
340
+ }
313
341
  await writeStandaloneLessonExports(dir, manifest);
314
342
  await writeCombinedExports(dir, manifest);
315
343
  const { latex, markdown } = manifest.lessonExports;
@@ -326,5 +354,6 @@ export {
326
354
  assertNotationPayloadHtml,
327
355
  assertRemarkPluginsWired,
328
356
  explicoContentRefresh,
329
- matchStandaloneDownload
357
+ matchStandaloneDownload,
358
+ validateGeneratedPages
330
359
  };
@@ -0,0 +1,43 @@
1
+ import { C as CourseConfig } from '../course-config-BLtIdkei.js';
2
+ import '../rules/catalog.js';
3
+ import '../rules/course-rules.js';
4
+
5
+ /** The usage text `content help` prints; `scripts/agent/gen-map.mjs` lists the verbs from it. */
6
+ declare const CONTENT_USAGE = "Usage:\n pnpm content status\n pnpm content diagnostics [info|warning|error] [--all | --code <c> | --lesson <id> | --changed] [--json]\n pnpm content warnings [--json]\n pnpm content context <lesson> [--json]\n pnpm content equation \"<latex>\" [--lesson <id>] [--json]\n pnpm content bindings <lesson> [--json]\n pnpm content symbol <latex | key> [--json]\n pnpm content check <lesson | deck>\u2026 [--changed] [--json]\n pnpm content rule <ID> [--json]\n pnpm content migrate [--from <version>] [--to <version>] [--json]\n pnpm content rules [--element <e>[,<e>]] [--enforcement script|partial|guide|locked] [--edition chapter|deck] [--json]\n pnpm content move <lesson>:<key> [--to <key>] [--write]\n pnpm content rename <old> <new> [--write]\n pnpm content fix <lesson> [--write]\n pnpm content new lesson <id>\n pnpm content new deck <lesson | part>\n pnpm content new term <key>\n pnpm content init-agent [--write]";
7
+ interface ContentArgs {
8
+ readonly command: string;
9
+ readonly positionals: readonly string[];
10
+ readonly json: boolean;
11
+ readonly changed: boolean;
12
+ /** `--element a,b` — a value-taking flag; also `--element=a,b`. */
13
+ readonly element?: string;
14
+ /** `--enforcement script` — a value-taking flag; also `--enforcement=script`. */
15
+ readonly enforcement?: string;
16
+ /** `--edition deck` — a value-taking flag; also `--edition=deck`. */
17
+ readonly edition?: string;
18
+ /** `--all`: list every diagnostic instead of the counts. */
19
+ readonly all: boolean;
20
+ /** `--code <code>`: only that diagnostic code. */
21
+ readonly code?: string;
22
+ /** `--lesson <id>`: only that lesson's diagnostics; the scope for `equation`. */
23
+ readonly lesson?: string;
24
+ /** `--from <version>` / `--to <version>` for `migrate`; `--to <key>` for `move`. */
25
+ readonly from?: string;
26
+ readonly to?: string;
27
+ /** `--write`: apply a planned edit instead of only showing its diff. */
28
+ readonly write: boolean;
29
+ }
30
+ declare function parseContentArgs(argv: readonly string[]): ContentArgs;
31
+ /** Where a run prints; the process streams by default, a buffer in a test. */
32
+ interface ContentIo {
33
+ readonly out: (text: string) => void;
34
+ readonly err: (text: string) => void;
35
+ }
36
+ /**
37
+ * Run one `content` command for this course and return its exit code: 0 on
38
+ * success, 1 on a usage error, a `ContentCliError`, a check that failed, or
39
+ * an unexpected throw (reported by message, never a stack).
40
+ */
41
+ declare function runContentCli(courseConfig: CourseConfig, argv?: readonly string[], io?: ContentIo): Promise<number>;
42
+
43
+ export { CONTENT_USAGE, type ContentArgs, type ContentIo, parseContentArgs, runContentCli };