explico 0.13.1 → 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 +879 -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 +1045 -16
  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 +15 -7
  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
@@ -46,7 +46,9 @@ import {
46
46
  import {
47
47
  gateContentMath,
48
48
  pageScope,
49
- scopeEntry
49
+ scopeEntry,
50
+ scopeLocals,
51
+ suspendedOperators
50
52
  } from "../reference/gate-math.js";
51
53
  import { gateIndices } from "../reference/gate-indices.js";
52
54
  import { derivedNotationEntries } from "../reference/derived-entries.js";
@@ -81,14 +83,19 @@ import {
81
83
  import { loadLintIgnore } from "../reference/lint-ignore.js";
82
84
  import { buildNotationRegistry } from "../reference/registry.js";
83
85
  import { formulaGlyphScope } from "../reference/gloss.js";
84
- import { resolveReferenceId } from "../reference/references.js";
86
+ import { pageScopeLessonIds, resolvePageReferenceId } from "../reference/references.js";
85
87
  import {
86
88
  coursePartsOf,
87
89
  normalizeBlockLabels,
88
90
  normalizeCards,
91
+ normalizeSidebarMarks,
92
+ normalizeSidebarEditions,
93
+ normalizeReadingMemory,
94
+ normalizeAppearance,
89
95
  normalizeLessonExports,
90
96
  normalizeDisclosure,
91
- normalizeNumbering
97
+ normalizeNumbering,
98
+ normalizeDeck
92
99
  } from "./course-config.js";
93
100
  import {
94
101
  disabledCodes,
@@ -104,6 +111,13 @@ import {
104
111
  import {
105
112
  resolveKnowledgeVersion
106
113
  } from "./knowledge-version.js";
114
+ import {
115
+ nextPageVersions,
116
+ PAGE_VERSIONS_FILE,
117
+ pageSourceDigest,
118
+ readPageVersions,
119
+ sinceOf
120
+ } from "./page-versions.js";
107
121
  import {
108
122
  loadAbbreviationRegistryInput,
109
123
  loadContentRecords,
@@ -113,15 +127,24 @@ import {
113
127
  loadNonChapterPages,
114
128
  loadNotationSpecs,
115
129
  loadPartOverviews,
116
- loadSourceRecords
130
+ loadSourceRecords,
131
+ loadDeckFiles,
132
+ loadDeckNotationPages,
133
+ loadLessonFiles
117
134
  } from "./collections.js";
135
+ import { buildDecks, chapterEquationLatex } from "./decks.js";
136
+ import { readDeckSource } from "../reference/deck-source.js";
137
+ import { coveredSections } from "../reference/decks-validate.js";
118
138
  import {
119
139
  buildSidebar,
120
140
  deriveSources,
121
141
  isLessonSlug,
122
- lessonIdFromSlug
142
+ lessonIdFromSlug,
143
+ mergeTrackOrder,
144
+ sidebarLessonSlug,
145
+ withPartDeckFrames
123
146
  } from "./lesson-derivation.js";
124
- const MANIFEST_SCHEMA_VERSION = 31;
147
+ const MANIFEST_SCHEMA_VERSION = 37;
125
148
  const REPOSITORY_ROOT = process.cwd();
126
149
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
127
150
  const MANIFEST_PATH = join(REPOSITORY_ROOT, "build", "manifest.json");
@@ -173,11 +196,11 @@ function contentHashes() {
173
196
  return { content, contentTree: `sha256:${tree.digest("hex")}` };
174
197
  }
175
198
  function lessonNotationResolution(lesson, definitionsById, availableDefinitionIds) {
176
- const scope = { kind: "page", lessonId: lesson.lessonId };
199
+ const scopes = pageScopeLessonIds(lesson);
177
200
  return lesson.references.map((reference) => {
178
- const definitionId = resolveReferenceId(
201
+ const definitionId = resolvePageReferenceId(
179
202
  reference.key,
180
- scope,
203
+ scopes,
181
204
  availableDefinitionIds
182
205
  );
183
206
  const definition = definitionId === void 0 ? void 0 : definitionsById.get(definitionId);
@@ -245,29 +268,16 @@ function studyMinutesRange(value) {
245
268
  return void 0;
246
269
  }
247
270
  function lessonFrontmatterById() {
248
- const base = join(CONTENT_ROOT, "docs");
249
271
  const result = /* @__PURE__ */ new Map();
250
- const walk = (directory) => {
251
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
252
- const path = join(directory, entry.name);
253
- if (entry.isDirectory()) {
254
- walk(path);
255
- continue;
256
- }
257
- if (!entry.isFile() || !/\.mdx?$/.test(entry.name)) continue;
258
- const slug = relative(base, path).replace(/\\/g, "/");
259
- if (!isLessonSlug(slug)) continue;
260
- const id = slug.replace(/\.(md|mdx)$/, "").replace(/\//g, ".");
261
- const data = parseFrontmatter(readFileSync(path, "utf8")).frontmatter ?? {};
262
- const studyMinutes = studyMinutesRange(data.studyMinutes);
263
- result.set(id, {
264
- title: typeof data.title === "string" ? data.title : id,
265
- description: typeof data.description === "string" ? data.description : "",
266
- ...studyMinutes === void 0 ? {} : { studyMinutes }
267
- });
268
- }
269
- };
270
- walk(base);
272
+ for (const { path, lessonId: id } of loadLessonFiles()) {
273
+ const data = parseFrontmatter(readFileSync(path, "utf8")).frontmatter ?? {};
274
+ const studyMinutes = studyMinutesRange(data.studyMinutes);
275
+ result.set(id, {
276
+ title: typeof data.title === "string" ? data.title : id,
277
+ description: typeof data.description === "string" ? data.description : "",
278
+ ...studyMinutes === void 0 ? {} : { studyMinutes }
279
+ });
280
+ }
271
281
  return result;
272
282
  }
273
283
  async function compileManifest(courseConfig) {
@@ -280,17 +290,36 @@ async function compileManifest(courseConfig) {
280
290
  const off = disabledCodes(rules);
281
291
  const numberingSettings = normalizeNumbering(courseConfig);
282
292
  const blockLabels = normalizeBlockLabels(courseConfig);
293
+ const deckSettings = normalizeDeck(courseConfig);
283
294
  const [catalog, notationInput, abbreviationsInput] = await Promise.all([
284
295
  loadCurriculumCatalog(),
285
296
  loadNotationRegistryInput(),
286
297
  loadAbbreviationRegistryInput()
287
298
  ]);
288
299
  const nonChapterPages = loadNonChapterPages();
300
+ const deckFiles = loadDeckFiles();
301
+ const lessonDeckFiles = deckFiles.filter((deck) => deck.scope === "lesson");
302
+ const trackRank = new Map(
303
+ mergeTrackOrder(catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons }))).map(
304
+ (lessonId, index) => [lessonId, index]
305
+ )
306
+ );
307
+ const readingOrder = (lessonIds) => [...lessonIds].sort(
308
+ (left, right) => (trackRank.get(left) ?? Number.POSITIVE_INFINITY) - (trackRank.get(right) ?? Number.POSITIVE_INFINITY) || left.localeCompare(right)
309
+ );
310
+ const deckPages = loadDeckNotationPages(deckFiles, { order: readingOrder });
311
+ const deckLessonIds = new Set(
312
+ loadLessonFiles().filter((file) => file.edition === "deck").map((file) => file.lessonId)
313
+ );
314
+ const isChapter = (lessonId) => !deckLessonIds.has(lessonId);
315
+ const deckPageIds = new Set(deckPages.map((page) => page.notation.lessonId));
316
+ const chapterInputs = notationInput.lessons.filter((lesson) => isChapter(lesson.lessonId));
289
317
  const withPages = {
290
318
  ...notationInput,
291
319
  lessons: [
292
320
  ...notationInput.lessons,
293
- ...nonChapterPages.map((page) => page.notation)
321
+ ...nonChapterPages.map((page) => page.notation),
322
+ ...deckPages.map((page) => page.notation)
294
323
  ]
295
324
  };
296
325
  const registry = buildNotationRegistry(withPages);
@@ -325,9 +354,9 @@ async function compileManifest(courseConfig) {
325
354
  notationInput.sharedDefinitions.map((definition) => [definition.key, definition.label])
326
355
  );
327
356
  const localLabelsByLesson = new Map(
328
- notationInput.lessons.map((lesson) => [
357
+ withPages.lessons.map((lesson) => [
329
358
  lesson.lessonId,
330
- new Map(lesson.localDefinitions.map((definition) => [definition.key, definition.label]))
359
+ new Map(scopeLocals(lesson).map((definition) => [definition.key, definition.label]))
331
360
  ])
332
361
  );
333
362
  const termLabel = (lessonId, key) => localLabelsByLesson.get(lessonId)?.get(key) ?? sharedLabels.get(key);
@@ -372,18 +401,26 @@ async function compileManifest(courseConfig) {
372
401
  abbreviations: abbreviationsInput,
373
402
  courseAbbreviations: rules.settings("N14")
374
403
  }) : [],
375
- ...rules.isEnabled("S1") ? gateHeadingLength(notationInput.lessons, {
404
+ ...rules.isEnabled("S1") ? gateHeadingLength(chapterInputs, {
376
405
  maxLength: rules.number("S1", "maxLength"),
377
406
  labelFor: termLabel
378
407
  }) : [],
379
- ...gateHeadingStructure(notationInput.lessons, {
408
+ ...gateHeadingStructure(chapterInputs, {
380
409
  levels: rules.isEnabled("S6"),
381
410
  adjacency: rules.isEnabled("S7")
382
411
  }),
383
412
  // The course's own rules (`courseRules`): one pass for every enabled `X`
384
413
  // rule that carries a `forbid` pattern. A disabled one, and a guide rule
385
414
  // with no pattern, do no work here.
386
- ...gateCourseRules(withPages.lessons, rules.course),
415
+ // A course rule about one edition is checked on that edition's pages.
416
+ ...gateCourseRules(
417
+ withPages.lessons.filter((lesson) => isChapter(lesson.lessonId) && !deckPageIds.has(lesson.lessonId)),
418
+ rules.course.filter((rule) => rule.editions !== "deck")
419
+ ),
420
+ ...gateCourseRules(
421
+ withPages.lessons.filter((lesson) => !isChapter(lesson.lessonId) || deckPageIds.has(lesson.lessonId)),
422
+ rules.course.filter((rule) => rule.editions !== "chapter")
423
+ ),
387
424
  // S10 is locked: a float on a non-chapter page has no number to render.
388
425
  ...gateNonChapterPages(nonChapterPages)
389
426
  ];
@@ -472,6 +509,11 @@ async function compileManifest(courseConfig) {
472
509
  message: `course.settings.json \`parts\` lists "${part.dir}" (${JSON.stringify(part.label)}), but content/docs/${part.dir}/ has no chapter and no overview page yet, so the part is left out of the sidebar and the numbering until it has one.`
473
510
  });
474
511
  }
512
+ const configuredDirs = new Set(configuredParts.map((part) => part.dir));
513
+ const partDeckFiles = deckFiles.filter(
514
+ (deck) => deck.scope === "part" && configuredDirs.has(deck.partDir)
515
+ );
516
+ const partChapterNames = (dir) => lessonSlugs.filter((slug) => slug.startsWith(`${dir}/`) && isChapter(lessonIdFromSlug(slug))).map((slug) => slug.slice(dir.length + 1));
475
517
  const partDirs = new Set(listedParts.map((part) => part.dir));
476
518
  for (const lesson of notationInput.lessons) {
477
519
  const dir = lesson.lessonId.split(".")[0] ?? "";
@@ -493,16 +535,6 @@ async function compileManifest(courseConfig) {
493
535
  )
494
536
  );
495
537
  }
496
- const waiverDiagnostics = [];
497
- const waiverRules = { courseRules: rules.course };
498
- const waivers = withPages.lessons.flatMap((lesson) => {
499
- const collected = collectWaivers(lesson, waiverRules);
500
- waiverDiagnostics.push(...collected.diagnostics);
501
- return collected.waivers;
502
- });
503
- const waived = applyWaivers(mathIssues, waivers, waiverRules);
504
- mathIssues.splice(0, mathIssues.length, ...waived.kept);
505
- waiverDiagnostics.push(...waived.unused);
506
538
  const sidebar = buildSidebar(
507
539
  catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
508
540
  lessonSlugs,
@@ -513,7 +545,31 @@ async function compileManifest(courseConfig) {
513
545
  {
514
546
  floatIndex: [".mdx", ".md"].some(
515
547
  (extension) => existsSync(join(CONTENT_ROOT, "docs", `figures-and-tables${extension}`))
516
- )
548
+ ),
549
+ // A deck beside its chapter is listed right under it.
550
+ decks: new Map(
551
+ lessonDeckFiles.filter((deck) => deck.chapterPath !== void 0 && deck.inPart).map((deck) => [deck.lessonSlug, deck.route])
552
+ ),
553
+ // A deck with no chapter is the lesson: its row carries the deck's glyph.
554
+ slidesFirst: new Set(
555
+ lessonDeckFiles.filter((deck) => deck.chapterPath === void 0 && deck.inPart).map((deck) => deck.lessonSlug)
556
+ ),
557
+ // A part's deck follows the part's Overview; a chapter it covers that
558
+ // has no deck of its own links into it, at its section's divider.
559
+ partDecks: new Map(
560
+ partDeckFiles.map((deck) => {
561
+ const { covered } = coveredSections(readDeckSource(deck.body), new Set(partChapterNames(deck.partDir)));
562
+ return [
563
+ deck.partDir,
564
+ {
565
+ route: deck.route,
566
+ covers: new Map(covered.map(({ name, position }) => [`${deck.partDir}/${name}`, position + 2]))
567
+ }
568
+ ];
569
+ })
570
+ ),
571
+ // Each edition's row names its lesson, which the sidebar's own row shows.
572
+ titleOf: (slug) => frontmatterById.get(lessonIdFromSlug(slug))?.title
517
573
  }
518
574
  );
519
575
  const bodyByLessonId = new Map(
@@ -524,7 +580,10 @@ async function compileManifest(courseConfig) {
524
580
  let partCursor = 0;
525
581
  for (const group of sidebar.slice(0, configuredParts.length)) {
526
582
  partCursor += 1;
527
- const lessonIds = group.items.filter((item) => typeof item === "string").map(lessonIdFromSlug);
583
+ const lessonIds = group.items.flatMap((item) => {
584
+ const slug = sidebarLessonSlug(item);
585
+ return slug === void 0 ? [] : [lessonIdFromSlug(slug)];
586
+ });
528
587
  if (lessonIds.length === 0) continue;
529
588
  const used = {
530
589
  equations: 0,
@@ -536,7 +595,7 @@ async function compileManifest(courseConfig) {
536
595
  };
537
596
  let chapterCursor = 0;
538
597
  for (const lessonId of lessonIds) {
539
- const body = bodyByLessonId.get(lessonId) ?? "";
598
+ const body = isChapter(lessonId) ? bodyByLessonId.get(lessonId) ?? "" : "";
540
599
  const chapter = ++chapterCursor;
541
600
  placementByLessonId.set(lessonId, {
542
601
  part: partCursor,
@@ -565,7 +624,8 @@ async function compileManifest(courseConfig) {
565
624
  return [
566
625
  lesson.lessonId,
567
626
  scanSectionHeadings(lesson.body, {
568
- ...placement ? { prefix: `${placement.part}.${placement.chapter}` } : {},
627
+ // A deck's headings are sections and frames, numbered as frames.
628
+ ...placement && isChapter(lesson.lessonId) ? { prefix: `${placement.part}.${placement.chapter}` } : {},
569
629
  depth: numberingSettings.sections.depth,
570
630
  labelFor: (key) => termLabel(lesson.lessonId, key)
571
631
  })
@@ -615,7 +675,7 @@ async function compileManifest(courseConfig) {
615
675
  } : {}
616
676
  };
617
677
  });
618
- const floatInputs = (kind) => notationInput.lessons.map((lesson) => {
678
+ const floatInputs = (kind) => chapterInputs.map((lesson) => {
619
679
  const placement = placementByLessonId.get(lesson.lessonId);
620
680
  return {
621
681
  lessonId: lesson.lessonId,
@@ -725,6 +785,7 @@ async function compileManifest(courseConfig) {
725
785
  )
726
786
  );
727
787
  const titledBlocksFor = (lessonId) => {
788
+ if (!isChapter(lessonId)) return [];
728
789
  const slug = lessonId.replace(/\./g, "/");
729
790
  const body = bodyByLessonId.get(lessonId) ?? "";
730
791
  const headings = headingsByLessonId.get(lessonId) ?? [];
@@ -749,7 +810,7 @@ async function compileManifest(courseConfig) {
749
810
  );
750
811
  };
751
812
  const labKatexDiagnostics = validateLabKatexImports(
752
- notationInput.lessons.map((lesson) => ({
813
+ [...notationInput.lessons, ...deckPages.map((page) => page.notation)].map((lesson) => ({
753
814
  lessonId: lesson.lessonId,
754
815
  file: lesson.source.file,
755
816
  body: lesson.body
@@ -757,13 +818,112 @@ async function compileManifest(courseConfig) {
757
818
  REPOSITORY_ROOT
758
819
  );
759
820
  const rawFloatDiagnostics = validateRawFloats(
760
- notationInput.lessons.map((lesson) => ({
821
+ [...notationInput.lessons, ...deckPages.map((page) => page.notation)].map((lesson) => ({
761
822
  lessonId: lesson.lessonId,
762
823
  slug: lesson.lessonId.replace(/\./g, "/"),
763
824
  file: lesson.source.file,
764
825
  body: lesson.body
765
826
  }))
766
827
  );
828
+ const pageInputById = new Map(withPages.lessons.map((lesson) => [lesson.lessonId, lesson]));
829
+ const chapterBodyById = new Map(chapterInputs.map((lesson) => [lesson.lessonId, lesson.body]));
830
+ const floatNumbers = (floats) => new Map((floats ?? []).map((float) => [float.key, float.number]));
831
+ const outcomeById = new Map(
832
+ catalog.competencies.flatMap((competency) => {
833
+ const outcome = competency.outcome;
834
+ return typeof outcome === "string" ? [[competency.id, outcome]] : [];
835
+ })
836
+ );
837
+ const chapterFloatsOf = (lessonId) => {
838
+ const slug = lessonId.replace(/\./g, "/");
839
+ const chapterBody = chapterBodyById.get(lessonId);
840
+ const latex = chapterBody === void 0 ? /* @__PURE__ */ new Map() : chapterEquationLatex(chapterBody);
841
+ return {
842
+ equations: new Map(
843
+ (equationsBySlug.get(slug) ?? []).map((equation) => [
844
+ equation.key,
845
+ { number: equation.number, ...latex.has(equation.key) ? { latex: latex.get(equation.key) } : {} }
846
+ ])
847
+ ),
848
+ figures: floatNumbers(figuresBySlug.get(slug)),
849
+ tables: floatNumbers(tablesBySlug.get(slug)),
850
+ diagrams: floatNumbers(diagramsBySlug.get(slug))
851
+ };
852
+ };
853
+ const outcomesOf = (lessonId) => (lessonMetaById.get(lessonId)?.teaches ?? []).flatMap((id) => {
854
+ const outcome = outcomeById.get(id);
855
+ return outcome ? [outcome] : [];
856
+ });
857
+ const deckParts = new Map(
858
+ partDeckFiles.map((deck) => {
859
+ const part = manifestParts.find((entry) => entry.dir === deck.partDir);
860
+ const lessonIds = part?.lessons ?? [];
861
+ const nameOf = (lessonId) => lessonId.replace(/\./g, "/").slice(deck.partDir.length + 1);
862
+ return [
863
+ deck.partDir,
864
+ {
865
+ label: part?.label ?? deck.partDir,
866
+ chapters: lessonIds.filter(isChapter).map((lessonId) => ({
867
+ name: nameOf(lessonId),
868
+ slug: lessonId.replace(/\./g, "/"),
869
+ teaches: lessonMetaById.get(lessonId)?.teaches ?? [],
870
+ outcomes: outcomesOf(lessonId),
871
+ floats: chapterFloatsOf(lessonId)
872
+ })),
873
+ slidesFirst: lessonIds.filter((lessonId) => !isChapter(lessonId)).map(nameOf)
874
+ }
875
+ ];
876
+ })
877
+ );
878
+ const deckModel = buildDecks({
879
+ decks: deckFiles,
880
+ settings: deckSettings,
881
+ rules: {
882
+ maxWords: rules.number("D2", "maxWords"),
883
+ maxBullets: rules.number("D2", "maxBullets"),
884
+ maxTitleLength: rules.number("D5", "maxLength"),
885
+ maxSteps: rules.number("D6", "maxSteps"),
886
+ maxNewSymbols: rules.number("D9", "maxNewSymbols")
887
+ },
888
+ lessons: new Map(
889
+ lessonDeckFiles.map((deck) => [
890
+ deck.lessonId,
891
+ {
892
+ title: frontmatterById.get(deck.lessonId)?.title ?? deck.lessonId,
893
+ teaches: lessonMetaById.get(deck.lessonId)?.teaches ?? [],
894
+ outcomes: outcomesOf(deck.lessonId),
895
+ ...deck.chapterPath === void 0 ? {} : { chapterFloats: chapterFloatsOf(deck.lessonId) }
896
+ }
897
+ ])
898
+ ),
899
+ parts: deckParts,
900
+ // As the title frame shows them (`titleFrameNode` in `deck-remark.mjs`).
901
+ course: {
902
+ name: courseConfig.about.materialName ?? courseConfig.title,
903
+ authors: courseConfig.about.authors.map((author) => author.name)
904
+ },
905
+ scopeOf: (pageId) => {
906
+ const page = pageInputById.get(pageId);
907
+ return page === void 0 ? [] : pageScope(page, cardsByKey);
908
+ },
909
+ suspendedOf: (pageId) => {
910
+ const page = pageInputById.get(pageId);
911
+ return page === void 0 ? [] : suspendedOperators(page);
912
+ },
913
+ labelFor: termLabel
914
+ });
915
+ mathIssues.push(...deckModel.diagnostics);
916
+ const sidebarWithFrames = withPartDeckFrames(sidebar, deckModel.decks);
917
+ const waiverDiagnostics = [];
918
+ const waiverRules = { courseRules: rules.course };
919
+ const waivers = withPages.lessons.flatMap((lesson) => {
920
+ const collected = collectWaivers(lesson, waiverRules);
921
+ waiverDiagnostics.push(...collected.diagnostics);
922
+ return collected.waivers;
923
+ });
924
+ const waived = applyWaivers(mathIssues, waivers, waiverRules);
925
+ mathIssues.splice(0, mathIssues.length, ...waived.kept);
926
+ waiverDiagnostics.push(...waived.unused);
767
927
  const lessons = [];
768
928
  for (const notationLesson of notationInput.lessons) {
769
929
  const id = notationLesson.lessonId;
@@ -777,6 +937,10 @@ async function compileManifest(courseConfig) {
777
937
  lessons.push({
778
938
  id,
779
939
  slug: id.replace(/\./g, "/"),
940
+ editions: [
941
+ ...isChapter(id) ? ["chapter"] : [],
942
+ ...lessonDeckFiles.some((deck) => deck.lessonId === id) ? ["deck"] : []
943
+ ],
780
944
  // 0 = the lesson is not placed in any configured part; a curriculum
781
945
  // diagnostic covers that separately, and both numbering paths then fall
782
946
  // back to the legacy `##`-section scheme.
@@ -876,6 +1040,107 @@ async function compileManifest(courseConfig) {
876
1040
  declaredVersion: courseConfig.about.version,
877
1041
  file: relative(REPOSITORY_ROOT, knowledgeChangelogPath).replace(/\\/g, "/")
878
1042
  });
1043
+ const trackPageVersions = !rules.disabled.includes("C5");
1044
+ const previousVersions = trackPageVersions ? readPageVersions(join(CONTENT_ROOT, "versions.json"), PAGE_VERSIONS_FILE) : { record: void 0, diagnostics: [] };
1045
+ const digestOf = (file) => hashes.content[file] ?? "missing";
1046
+ const presentFiles = (files) => files.filter((file) => hashes.content[file] !== void 0);
1047
+ const sourceFileById = /* @__PURE__ */ new Map();
1048
+ for (const record of contentRecords) {
1049
+ const file = record.file.replace(/\\/g, "/");
1050
+ if (file.startsWith("content/sources/") && typeof record.data.id === "string") {
1051
+ sourceFileById.set(record.data.id, file);
1052
+ }
1053
+ }
1054
+ const sharedEntryInputs = (definitionIds) => definitionIds.flatMap((definitionId) => {
1055
+ const definition = definitionsById.get(definitionId);
1056
+ return definition?.kind === "shared" ? [{ label: `notation ${definition.source.file}`, digest: digestOf(definition.source.file) }] : [];
1057
+ });
1058
+ const recalledFloatFacts = (recalledFrom) => Object.fromEntries(
1059
+ Object.entries(recalledFrom).map(([ref, chapter]) => {
1060
+ const [, prefix, key] = /^(eq|fig|tab|dia)-(.+)$/u.exec(ref) ?? [];
1061
+ const floats = prefix === "eq" ? equationsBySlug : prefix === "fig" ? figuresBySlug : prefix === "tab" ? tablesBySlug : diagramsBySlug;
1062
+ const float = floats.get(chapter)?.find((entry) => entry.key === key);
1063
+ const latex = prefix === "eq" ? chapterFloatsOf(chapter.replace(/\//g, ".")).equations.get(key ?? "")?.latex : void 0;
1064
+ return [ref, { chapter, ...float, ...latex === void 0 ? {} : { latex } }];
1065
+ })
1066
+ );
1067
+ const trackedPages = [
1068
+ ...lessons.map((lesson) => ({
1069
+ slug: lesson.slug,
1070
+ source: pageSourceDigest(
1071
+ [
1072
+ { label: `page ${lesson.file}`, digest: digestOf(lesson.file) },
1073
+ ...presentFiles([lesson.file.replace(/\.mdx?$/u, ".checks.yml")]).map((file) => ({
1074
+ label: `checks ${file}`,
1075
+ digest: digestOf(file)
1076
+ })),
1077
+ ...presentFiles(lesson.assessments.map((id) => `content/assessments/${id}.json`)).map((file) => ({
1078
+ label: `assessment ${file}`,
1079
+ digest: digestOf(file)
1080
+ })),
1081
+ ...sharedEntryInputs(lesson.notation.definitionIds),
1082
+ ...lesson.sources.map((id) => {
1083
+ const file = sourceFileById.get(id);
1084
+ return { label: `source ${id}`, digest: file === void 0 ? "missing" : digestOf(file) };
1085
+ })
1086
+ ],
1087
+ { part: lesson.part, chapter: lesson.chapter, numbering: lesson.numbering }
1088
+ )
1089
+ })),
1090
+ // A deck beside its chapter has a version of its own, by its route, and
1091
+ // so has a part's deck; a slides-first lesson's deck is the lesson, above.
1092
+ ...deckModel.decks.flatMap((deck) => {
1093
+ if (deck.chapter === void 0 && deck.part === void 0) return [];
1094
+ const page = deckPages.find((entry) => entry.deck.route === deck.slug);
1095
+ return [
1096
+ {
1097
+ slug: deck.slug,
1098
+ source: pageSourceDigest(
1099
+ [
1100
+ { label: `page ${deck.file}`, digest: digestOf(deck.file) },
1101
+ ...sharedEntryInputs(bundlesByLesson.get(deck.pageId)?.definitionIds ?? []),
1102
+ ...(page === void 0 ? [] : deriveSources(page.notation.body)).map((id) => {
1103
+ const file = sourceFileById.get(id);
1104
+ return { label: `source ${id}`, digest: file === void 0 ? "missing" : digestOf(file) };
1105
+ })
1106
+ ],
1107
+ // A part's deck shows each float it recalls as its chapter
1108
+ // numbers and captions it: those count as the page's too.
1109
+ deck.recalledFrom === void 0 ? {} : { recalls: recalledFloatFacts(deck.recalledFrom) }
1110
+ )
1111
+ }
1112
+ ];
1113
+ }),
1114
+ ...manifestParts.flatMap((part) => {
1115
+ if (!part.overview) return [];
1116
+ const slug = `${part.dir}/index`;
1117
+ const page = pages.find((entry) => entry.slug === slug);
1118
+ return [
1119
+ {
1120
+ slug,
1121
+ source: pageSourceDigest([
1122
+ { label: `page ${part.overview.file}`, digest: digestOf(part.overview.file) },
1123
+ ...sharedEntryInputs(page?.notation.definitionIds ?? [])
1124
+ ])
1125
+ }
1126
+ ];
1127
+ })
1128
+ ];
1129
+ const pageVersionsRecord = nextPageVersions({
1130
+ previous: previousVersions.record,
1131
+ version: knowledge.version,
1132
+ unreleased: knowledge.unreleased.length > 0,
1133
+ pages: trackedPages
1134
+ });
1135
+ const pageVersions = {
1136
+ enabled: trackPageVersions,
1137
+ file: PAGE_VERSIONS_FILE,
1138
+ record: pageVersionsRecord
1139
+ };
1140
+ const sinceOfPage = (slug) => {
1141
+ const entry = pageVersionsRecord.pages[slug];
1142
+ return trackPageVersions && entry !== void 0 ? sinceOf(entry) : knowledge.version;
1143
+ };
879
1144
  const enginePkg = readEnginePackage();
880
1145
  const about = {
881
1146
  engine: {
@@ -891,6 +1156,11 @@ async function compileManifest(courseConfig) {
891
1156
  version: knowledge.version,
892
1157
  ...knowledge.latest?.date === void 0 ? {} : { date: knowledge.latest.date },
893
1158
  notes: knowledge.latest?.notes ?? [],
1159
+ releases: knowledge.entries.map((entry) => ({
1160
+ version: entry.version,
1161
+ ...entry.date === void 0 ? {} : { date: entry.date }
1162
+ })),
1163
+ unreleased: knowledge.unreleased.length > 0,
894
1164
  contentTree: hashes.contentTree
895
1165
  },
896
1166
  authors: courseConfig.about.authors.map((author) => ({
@@ -907,6 +1177,34 @@ async function compileManifest(courseConfig) {
907
1177
  numbering: numberingSettings,
908
1178
  blockLabels,
909
1179
  cards: normalizeCards(courseConfig),
1180
+ sidebarMarks: normalizeSidebarMarks(courseConfig),
1181
+ sidebarEditions: normalizeSidebarEditions(courseConfig),
1182
+ readingMemory: normalizeReadingMemory(courseConfig),
1183
+ appearance: normalizeAppearance(courseConfig),
1184
+ deck: deckSettings,
1185
+ decks: deckModel.decks.map((deck) => {
1186
+ const page = pageInputById.get(deck.pageId);
1187
+ return {
1188
+ ...deck,
1189
+ since: sinceOfPage(deck.slug),
1190
+ notation: lessonNotationBundle(
1191
+ bundlesByLesson.get(deck.pageId),
1192
+ definitionsById,
1193
+ page === void 0 ? [] : lessonNotationResolution(page, definitionsById, availableDefinitionIds),
1194
+ cardsByKey,
1195
+ // A deck shows its lessons' symbols: the derived cards (rule N23)
1196
+ // its chapters' bodies and checks use ship with it, as its bundle
1197
+ // holds the chapters' entries.
1198
+ [
1199
+ ...indexGate.derivedByLesson.get(deck.pageId) ?? [],
1200
+ ...(page?.scopeLessonIds ?? []).flatMap((lessonId) => [
1201
+ ...indexGate.derivedByLesson.get(lessonId) ?? [],
1202
+ ...assessmentDerived.get(lessonId)?.values() ?? []
1203
+ ])
1204
+ ].filter((use, index, all) => all.findIndex((other) => other.key === use.key) === index)
1205
+ )
1206
+ };
1207
+ }),
910
1208
  about,
911
1209
  notation: {
912
1210
  definitions: registry.definitions,
@@ -919,9 +1217,12 @@ async function compileManifest(courseConfig) {
919
1217
  sources,
920
1218
  tracks,
921
1219
  abbreviations,
922
- sidebar,
923
- parts: manifestParts,
924
- lessons,
1220
+ sidebar: sidebarWithFrames,
1221
+ parts: manifestParts.map(
1222
+ ({ overview, ...part }) => overview ? { ...part, overview: { ...overview, since: sinceOfPage(`${part.dir}/index`) } } : part
1223
+ ),
1224
+ lessons: lessons.map((lesson) => ({ ...lesson, since: sinceOfPage(lesson.slug) })),
1225
+ pageVersions,
925
1226
  pages,
926
1227
  prereqEdges,
927
1228
  equations: {
@@ -963,7 +1264,7 @@ async function compileManifest(courseConfig) {
963
1264
  checks: dropDisabled(checkValidation.diagnostics, off),
964
1265
  examples: dropDisabled(exampleValidation.diagnostics, off),
965
1266
  folds: dropDisabled(foldValidation.diagnostics, off),
966
- knowledge: dropDisabled(knowledge.diagnostics, off),
1267
+ knowledge: dropDisabled([...knowledge.diagnostics, ...previousVersions.diagnostics], off),
967
1268
  rawFloats: dropDisabled(rawFloatDiagnostics, off),
968
1269
  config: rules.diagnostics,
969
1270
  waivers: waiverDiagnostics