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
@@ -1,4 +1,5 @@
1
1
  const ELEMENTS = [
2
+ "slide",
2
3
  "prose",
3
4
  "math",
4
5
  "equation",
@@ -16,11 +17,16 @@ const COURSE_RULE_ID = /^X[1-9][0-9]?$/;
16
17
  function isCourseRuleId(id) {
17
18
  return COURSE_RULE_ID.test(id);
18
19
  }
20
+ function appliesTo(entry, edition) {
21
+ const editions = entry.editions ?? "both";
22
+ return editions === "both" || editions === edition;
23
+ }
19
24
  const GATE_MATH = { module: "src/reference/gate-math.ts", fn: "resolveInto" };
20
25
  const GLYPHS = { module: "src/reference/math-glyphs.mjs", fn: "resolveMathGlyphs" };
21
26
  const REGISTRY = { module: "src/reference/registry.ts", fn: "buildNotationRegistry" };
22
27
  const KEYED_FLOATS = { module: "src/reference/keyed-float-validate.ts", fn: "validateKeyedFloats" };
23
28
  const INDICES = { module: "src/reference/gate-indices.ts", fn: "gateIndices" };
29
+ const DECKS = { module: "src/reference/decks-validate.ts", fn: "validateDecks" };
24
30
  function consistency(fn) {
25
31
  return { module: "src/reference/consistency.ts", fn };
26
32
  }
@@ -138,12 +144,14 @@ const RULES = [
138
144
  title: "One meaning per glyph per page; define once; introduce with `[[key]]` before the first math use.",
139
145
  elements: ["math", "notation"],
140
146
  doc: "notation.md#n4",
141
- summary: "Every glyph in rendered math resolves to exactly one notation entry on the page: introduce it with `[[key]]` in prose before its first `$\u2026$`, define a page-local one in `notation.local`, or bind a compound with `\\explain{key}{latex}`. Do not reuse a glyph for a second meaning on the same page.",
147
+ summary: "Every glyph in rendered math resolves to exactly one notation entry on the page: introduce it with `[[key]]` in prose before its first `$\u2026$`, define a page-local one in `notation.local`, or bind a compound with `\\explain{key}{latex}`. Do not reuse a glyph for a second meaning on the same page. Declare a meaning once \u2014 a shared card unless the letter already means something else in the course, and only then `notation.local` or a gloss \u2014 and reference it everywhere else.",
142
148
  checks: [
143
149
  check("unresolved-glyph", "error", true, GATE_MATH),
144
150
  check("glyph-unique-in-corpus", "warning", false, consistency("checkGlyphUniqueness")),
145
151
  check("weak-local", "info", false, consistency("checkWeakLocal")),
146
- check("gloss-wants-promoting", "info", false, consistency("checkGlossWantsPromoting")),
152
+ check("definition-repeated", "warning", false, consistency("checkDefinitionRepeated")),
153
+ check("meaning-repeated", "info", false, consistency("checkMeaningRepeated")),
154
+ check("explain-redundant", "info", true, GATE_MATH),
147
155
  check("card-wants-demoting", "info", false, consistency("checkCardWantsDemoting"))
148
156
  ],
149
157
  locked: "the notation layer is this rule \u2014 an unresolved glyph has no hover card and the render path needs every glyph bound."
@@ -489,6 +497,8 @@ const RULES = [
489
497
  {
490
498
  kind: "rule",
491
499
  id: "V7",
500
+ // Chapter-only: on a deck, rule D2 (a frame fits its canvas) says it of a frame.
501
+ editions: "chapter",
492
502
  short: "A figure\u2019s picture fits on one screen",
493
503
  title: "A figure\u2019s picture takes at most `maxPercent` of the screen height.",
494
504
  elements: ["plot"],
@@ -513,6 +523,8 @@ const RULES = [
513
523
  {
514
524
  kind: "rule",
515
525
  id: "V8",
526
+ // Chapter-only: on a deck, rule D7 (a float is the frame) says it of a frame.
527
+ editions: "chapter",
516
528
  short: "A diagram fits the lesson column",
517
529
  title: "A diagram is no wider than the lesson column.",
518
530
  elements: ["diagram"],
@@ -578,10 +590,32 @@ const RULES = [
578
590
  ],
579
591
  unchecked: "that the two edits landed in one commit \u2014 the build only ever sees the end state, so a release split across commits warns until the second half lands."
580
592
  },
593
+ {
594
+ kind: "rule",
595
+ id: "C5",
596
+ short: "Each chapter and part page carries the release its content last changed in; `content/versions.json` is generated and committed",
597
+ title: "Each chapter and part page carries the knowledge version its content last changed in, kept in `content/versions.json`, which `validate:content` writes and the course commits with the change.",
598
+ elements: ["changelog"],
599
+ doc: "versioning.md#c5",
600
+ summary: 'A page\u2019s PDF, Markdown and `[?]` panel say "unchanged since version 0.2.0", the release its content last changed in, or "with changes after version 0.4.0, not yet released" while `## [Unreleased]` holds its change. The record is `content/versions.json`: `validate:content` works it out from the committed file and the content, writes it on a developer\u2019s machine, and in CI fails when the committed file is not the one it would write. Never edit it by hand; commit what `validate:content` wrote with the content change, and with a release.',
601
+ checks: [
602
+ check("page-versions-stale", "error", false, {
603
+ module: "src/compiler/page-versions.ts",
604
+ fn: "syncPageVersions"
605
+ }),
606
+ check("page-versions-invalid", "warning", false, {
607
+ module: "src/compiler/page-versions.ts",
608
+ fn: "readPageVersions"
609
+ })
610
+ ],
611
+ unchecked: "which bullet of `## [Unreleased]` belongs to which page \u2014 every page changed while there is any bullet counts as not yet released; and a change to a lab, `content/domain/` or a part\u2019s label, which changes the page\u2019s PDF without changing its version."
612
+ },
581
613
  // ---- S — structure ----------------------------------------------------------
582
614
  {
583
615
  kind: "rule",
584
616
  id: "S1",
617
+ // Chapter-only: on a deck, rule D5 (a frame title fits one line) says it of a frame.
618
+ editions: "chapter",
585
619
  short: "No heading, short caption or short-note title over 65 characters",
586
620
  title: "No heading, no short caption (`short`, an equation\u2019s `\\caption{}`) and no short-note title runs past 65 characters.",
587
621
  elements: ["heading"],
@@ -686,6 +720,8 @@ const RULES = [
686
720
  {
687
721
  kind: "rule",
688
722
  id: "S6",
723
+ // Chapter-only: on a deck, rule D1 (sections of frames) says it of a frame.
724
+ editions: "chapter",
689
725
  short: "Headings descend one level at a time, from `##`",
690
726
  title: "Headings descend one level at a time: a lesson body starts at `##` and never skips a level on the way down.",
691
727
  elements: ["heading"],
@@ -707,6 +743,8 @@ const RULES = [
707
743
  {
708
744
  kind: "rule",
709
745
  id: "S7",
746
+ // Chapter-only: on a deck, rule D1 (sections of frames) says it of a frame.
747
+ editions: "chapter",
710
748
  short: "A heading is followed by body content, never by another heading",
711
749
  title: "A heading is followed by body content, never directly by another heading.",
712
750
  elements: ["heading"],
@@ -853,6 +891,165 @@ const RULES = [
853
891
  doc: "prose.md#p9",
854
892
  summary: "The sentence before a display equation names the quantity it yields and the step or law it follows from \u2014 not \u201Cwe have:\u201D.",
855
893
  checks: []
894
+ },
895
+ // ---- D — decks --------------------------------------------------------------
896
+ {
897
+ kind: "rule",
898
+ id: "D1",
899
+ editions: "deck",
900
+ short: "A deck is sections of frames: `##` a section, `###` a frame",
901
+ title: "A deck is sections of frames: `##` opens a section, `###` is a frame, and nothing is deeper or outside a frame.",
902
+ elements: ["slide", "heading"],
903
+ doc: "deck.md#d1",
904
+ summary: "In a `<lesson>.deck.mdx`, `##` opens a section, a divider frame that shows only its title, and `###` is a frame whose title is the heading. Write every line of content inside a `###` frame: no `#`, no `####` or deeper, nothing before the first heading and nothing between a `##` and its first `###`.",
905
+ checks: [
906
+ check("frame-depth", "error", true, DECKS),
907
+ check("deck-loose-content", "error", true, DECKS),
908
+ check("deck-empty", "error", false, DECKS)
909
+ ],
910
+ locked: "a frame is the unit the deck page, Present mode and the PDF are built from \u2014 content outside one has no frame to show in."
911
+ },
912
+ {
913
+ kind: "rule",
914
+ id: "D2",
915
+ editions: "deck",
916
+ short: "A frame fits its canvas in every step",
917
+ title: "A frame fits its canvas in every step, and never continues on the next frame.",
918
+ elements: ["slide"],
919
+ doc: "deck.md#d2",
920
+ summary: "Everything a frame shows \u2014 title, body, legend line, at its last step \u2014 fits the course\u2019s canvas (`deck.aspectRatio`). `content check` warns when a frame passes `maxWords` words or `maxBullets` list items. A frame that does not fit is two frames; never shrink its type.",
921
+ checks: [
922
+ check("frame-dense", "warning", true, DECKS),
923
+ check("frame-overflow", "error", false, { module: "src/compiler/exports/deck-fit.ts", fn: "checkFrameFit" })
924
+ ],
925
+ unchecked: "the paper: a frame that fits its canvas on the web can still run over in the PDF, which the export reports as a warning naming the frame.",
926
+ settings: [
927
+ {
928
+ key: "maxWords",
929
+ title: "Most words on a frame",
930
+ description: "Words a frame may show (its prose, not its math or notes) before `content check` warns.",
931
+ schema: { type: "integer", minimum: 10, default: 70 }
932
+ },
933
+ {
934
+ key: "maxBullets",
935
+ title: "Most list items on a frame",
936
+ description: "List items a frame may show before `content check` warns.",
937
+ schema: { type: "integer", minimum: 1, default: 7 }
938
+ }
939
+ ]
940
+ },
941
+ {
942
+ kind: "rule",
943
+ id: "D3",
944
+ editions: "deck",
945
+ short: "A frame makes one claim, and its title states it",
946
+ title: "A frame makes one claim, and its title states the claim.",
947
+ elements: ["slide", "prose"],
948
+ doc: "deck.md#d3",
949
+ summary: "Title a frame with the sentence it shows to be true \u2014 \u201CRange is largest at a 45\xB0 launch angle\u201D, not \u201CRange\u201D. Everything on the frame serves that one claim; a second claim is a second frame.",
950
+ checks: []
951
+ },
952
+ {
953
+ kind: "rule",
954
+ id: "D4",
955
+ editions: "deck",
956
+ short: "A frame with text carries speaker notes",
957
+ title: "A frame with text carries speaker notes: the argument is in `<Notes>`, the frame shows its claims.",
958
+ elements: ["slide", "prose"],
959
+ doc: "deck.md#d4",
960
+ summary: "A `###` frame whose body has words carries a `<Notes>\u2026</Notes>` block with what the speaker says: the reasoning, the transitions, the question to ask the room. A frame that shows only a picture, an equation or a question needs none. The prose rules hold for the notes.",
961
+ checks: [check("frame-notes-missing", "warning", true, DECKS)]
962
+ },
963
+ {
964
+ kind: "rule",
965
+ id: "D5",
966
+ editions: "deck",
967
+ short: "A frame title fits one line",
968
+ title: "A frame title fits one line: at most `maxLength` characters.",
969
+ elements: ["slide", "heading"],
970
+ doc: "deck.md#d5",
971
+ summary: "A `##` or `###` heading in a deck runs to at most `maxLength` characters as the reader sees it (math as it reads, a `[[key]]` term as its label), so it stays on one line of the canvas. Move the rest into the frame.",
972
+ checks: [check("frame-title-length", "error", true, DECKS)],
973
+ settings: [
974
+ {
975
+ key: "maxLength",
976
+ title: "Longest frame title",
977
+ description: "Characters a frame or section title may run to.",
978
+ schema: { type: "integer", minimum: 10, default: 50 }
979
+ }
980
+ ]
981
+ },
982
+ {
983
+ kind: "rule",
984
+ id: "D6",
985
+ editions: "deck",
986
+ short: "A frame shows in at most `maxSteps` steps, each adding to the last",
987
+ title: "A frame shows in at most `maxSteps` steps, and each step adds to what is shown.",
988
+ elements: ["slide"],
989
+ doc: "deck.md#d6",
990
+ summary: "`<Pause />` is the one step mark: what follows it appears on the next key press, and nothing shown earlier changes or goes, so the last step is the whole frame \u2014 what the scroll view, the print and the PDF show. Keep to `maxSteps` steps; more is a second frame.",
991
+ checks: [check("frame-steps", "warning", true, DECKS)],
992
+ unchecked: "that a later step contradicts nothing already shown.",
993
+ settings: [
994
+ {
995
+ key: "maxSteps",
996
+ title: "Most steps in a frame",
997
+ description: "Steps a frame may show in (one more than its `<Pause />` marks).",
998
+ schema: { type: "integer", minimum: 1, default: 4 }
999
+ }
1000
+ ]
1001
+ },
1002
+ {
1003
+ kind: "rule",
1004
+ id: "D7",
1005
+ editions: "deck",
1006
+ short: "A frame holds one figure, table or diagram",
1007
+ title: "A frame holds at most one figure, table or diagram, and it is the frame\u2019s picture.",
1008
+ elements: ["slide", "plot", "table", "diagram"],
1009
+ doc: "deck.md#d7",
1010
+ summary: "One `<Figure>`, `<Table>`, `<Diagram>` or recalled float per frame, laid out at the canvas width; a lab\u2019s controls are the frame\u2019s tools, live in Present mode. A second picture is a second frame.",
1011
+ checks: [check("frame-floats", "warning", true, DECKS)]
1012
+ },
1013
+ {
1014
+ kind: "rule",
1015
+ id: "D8",
1016
+ editions: "deck",
1017
+ short: "Recall a chapter\u2019s result; never retype it",
1018
+ title: "A result the chapter numbers is recalled with `<Recall>`, never retyped; the deck\u2019s own floats carry no number.",
1019
+ elements: ["slide", "equation"],
1020
+ doc: "deck.md#d8",
1021
+ summary: 'Show the chapter\u2019s equation, figure, table or diagram with `<Recall ref="eq-range" />` (`fig-`, `tab-`, `dia-`): it renders as in the chapter, with the chapter\u2019s number, and links back to it. A display equation retyped from the chapter is a warning. An equation the deck labels itself numbers (1), (2) within the deck; the deck\u2019s figures, tables and diagrams are unnumbered.',
1022
+ checks: [check("frame-retyped", "warning", true, DECKS)]
1023
+ },
1024
+ {
1025
+ kind: "rule",
1026
+ id: "D9",
1027
+ editions: "deck",
1028
+ short: "A frame introduces at most `maxNewSymbols` symbols",
1029
+ title: "A frame introduces at most `maxNewSymbols` symbols the deck has not shown before.",
1030
+ elements: ["slide", "math", "notation"],
1031
+ doc: "deck.md#d9",
1032
+ summary: "The room reads a frame once. Count the symbols it shows for the first time in the deck and keep them to `maxNewSymbols`; the legend line under the frame names them. More than that is a frame of its own.",
1033
+ checks: [check("frame-new-symbols", "info", true, DECKS)],
1034
+ settings: [
1035
+ {
1036
+ key: "maxNewSymbols",
1037
+ title: "Most new symbols on a frame",
1038
+ description: "Symbols a frame may show for the first time in its deck.",
1039
+ schema: { type: "integer", minimum: 1, default: 4 }
1040
+ }
1041
+ ]
1042
+ },
1043
+ {
1044
+ kind: "rule",
1045
+ id: "D10",
1046
+ editions: "deck",
1047
+ short: "A deck opens on its title and closes on what the student can now do",
1048
+ title: "A deck opens on its title frame and closes on what the student can now do.",
1049
+ elements: ["slide"],
1050
+ doc: "deck.md#d10",
1051
+ summary: "The title frame (the lesson, the course, its authors and version) and the closing frame (the outcomes of the competencies the lesson teaches, `deck.outcomesFrame`) are generated: do not write them. With `deck.outcomesFrame` off, end on a `###` frame that says what the student can now do.",
1052
+ checks: []
856
1053
  }
857
1054
  ];
858
1055
  const RETIRED = [
@@ -1104,6 +1301,27 @@ const INVARIANTS = [
1104
1301
  fn: "gateMdxExpressions"
1105
1302
  })
1106
1303
  ]
1304
+ },
1305
+ {
1306
+ kind: "invariant",
1307
+ id: "decks",
1308
+ editions: "deck",
1309
+ title: "A deck belongs to a lesson or a part, and recalls what its chapters number.",
1310
+ elements: ["slide"],
1311
+ doc: "deck.md#the-deck-file",
1312
+ summary: "A deck is `content/docs/<part>/<lesson>.deck.mdx`, or a part\u2019s `content/docs/<part>/index.deck.mdx`. Beside a chapter, and as a part\u2019s, it sets only `title` and `description` \u2014 the chapter holds the lesson\u2019s `teaches`, `assumptions`, `studyMinutes` and `notation`; alone, it is the lesson and carries them. On a part\u2019s deck, `## Title [chapter: <name>]` names the chapter a section covers: a chapter of the part, once, and only on a `##` of a part\u2019s deck. `<Recall ref>` names an equation, figure, table or diagram of the deck\u2019s own chapter, or of a chapter a part\u2019s deck covers; `<AssessmentSet>` on a part\u2019s deck sits in a section that names a chapter; `<Pause>` takes only `control`, `from`, `to`, `seconds` (a driven figure) or `highlight` (a spotlight).",
1313
+ checks: [
1314
+ check("deck-frontmatter", "error", false, DECKS),
1315
+ check("deck-outside-part", "error", false, DECKS),
1316
+ check("recall-unresolved", "error", true, DECKS),
1317
+ check("pause-invalid", "error", true, DECKS),
1318
+ check("deck-chapter-unknown", "error", true, DECKS),
1319
+ check("deck-chapter-repeated", "error", true, DECKS),
1320
+ check("deck-chapter-outside-part", "error", true, DECKS),
1321
+ check("deck-marker-misplaced", "error", true, DECKS),
1322
+ check("deck-check-no-chapter", "error", true, DECKS),
1323
+ check("deck-route-taken", "error", false, DECKS)
1324
+ ]
1107
1325
  }
1108
1326
  ];
1109
1327
  const CATALOG = [...RULES, ...INVARIANTS];
@@ -1160,6 +1378,7 @@ function ownedCodes() {
1160
1378
  export {
1161
1379
  CATALOG,
1162
1380
  ELEMENTS,
1381
+ appliesTo,
1163
1382
  enforcementOf,
1164
1383
  entryById,
1165
1384
  invariants,
@@ -1,4 +1,4 @@
1
- import { CourseRuleId, Element, RuleEntry } from './catalog.js';
1
+ import { CourseRuleId, Element, Editions, RuleEntry } from './catalog.js';
2
2
 
3
3
  /**
4
4
  * A course's own rules — `courseRules` in `course.settings.json`.
@@ -65,6 +65,8 @@ interface CourseRuleInput {
65
65
  readonly forbid?: readonly CourseRulePatternInput[];
66
66
  readonly unchecked?: string;
67
67
  readonly doc?: string;
68
+ /** `chapter`, `deck` or `both` (the default): the edition of a lesson the rule is about. */
69
+ readonly editions?: string;
68
70
  }
69
71
  /** `CourseConfig.courseRules` — an array, so the settings form can add and remove one. */
70
72
  type CourseRuleDefinitions = readonly CourseRuleInput[];
@@ -85,6 +87,8 @@ interface CourseRule {
85
87
  readonly forbid: readonly CourseRulePattern[];
86
88
  readonly unchecked?: string;
87
89
  readonly doc?: string;
90
+ /** The edition of a lesson the rule is about (`docs/authoring/deck.md`). */
91
+ readonly editions: Editions;
88
92
  }
89
93
  /**
90
94
  * Validate `config.courseRules` and apply the defaults. Throws on anything that
@@ -97,7 +97,7 @@ function resolveCourseRules(config) {
97
97
  );
98
98
  }
99
99
  const raw = input;
100
- const known = ["id", "title", "summary", "elements", "enabled", "severity", "forbid", "unchecked", "doc"];
100
+ const known = ["id", "title", "summary", "elements", "enabled", "severity", "forbid", "unchecked", "doc", "editions"];
101
101
  for (const key of Object.keys(raw)) {
102
102
  if (!known.includes(key)) {
103
103
  throw new Error(`${at(index)}: unknown key \`${key}\`. Takes: ${known.join(", ")}.`);
@@ -128,6 +128,11 @@ function resolveCourseRules(config) {
128
128
  `${where}.severity must be "error" or "warning"; received ${describe(raw.severity)}.`
129
129
  );
130
130
  }
131
+ if (raw.editions !== void 0 && raw.editions !== "both" && raw.editions !== "chapter" && raw.editions !== "deck") {
132
+ throw new Error(
133
+ `${where}.editions must be "both", "chapter" or "deck"; received ${describe(raw.editions)}.`
134
+ );
135
+ }
131
136
  const forbidInput = raw.forbid;
132
137
  if (forbidInput !== void 0 && !Array.isArray(forbidInput)) {
133
138
  throw new Error(`${where}.forbid must be an array of patterns; received ${describe(forbidInput)}.`);
@@ -149,7 +154,8 @@ function resolveCourseRules(config) {
149
154
  severity: raw.severity ?? "error",
150
155
  forbid,
151
156
  ...raw.unchecked === void 0 ? {} : { unchecked: requireString(raw.unchecked, `${where}.unchecked`) },
152
- ...raw.doc === void 0 ? {} : { doc: requireString(raw.doc, `${where}.doc`) }
157
+ ...raw.doc === void 0 ? {} : { doc: requireString(raw.doc, `${where}.doc`) },
158
+ editions: raw.editions ?? "both"
153
159
  });
154
160
  });
155
161
  return out;
@@ -174,7 +180,8 @@ function courseRuleEntry(rule) {
174
180
  summary: rule.summary,
175
181
  checks,
176
182
  ...rule.unchecked === void 0 ? {} : { unchecked: rule.unchecked },
177
- source: "course"
183
+ source: "course",
184
+ ...rule.editions === "both" ? {} : { editions: rule.editions }
178
185
  };
179
186
  }
180
187
  function courseRulesJsonSchema() {
@@ -216,6 +223,13 @@ function courseRulesJsonSchema() {
216
223
  default: ["prose"],
217
224
  items: { title: "Element", type: "string", enum: [...ELEMENTS] }
218
225
  },
226
+ editions: {
227
+ title: "Editions",
228
+ description: "The edition of a lesson the rule is about: its chapter, its deck (`<lesson>.deck.mdx`), or both. A rule about one edition is checked there only, and listed on that edition\u2019s rules-in-effect page.",
229
+ type: "string",
230
+ enum: ["both", "chapter", "deck"],
231
+ default: "both"
232
+ },
219
233
  enabled: {
220
234
  title: "Enabled",
221
235
  description: 'Off: the rule stays in this file and moves to the "Off" list on the rules-in-effect page \u2014 nothing runs, and an agent neither applies it nor flags it in review. Use it to park a rule that is drafted but not in force.',
@@ -45,6 +45,7 @@
45
45
  "title": "Element",
46
46
  "type": "string",
47
47
  "enum": [
48
+ "slide",
48
49
  "prose",
49
50
  "math",
50
51
  "equation",
@@ -60,6 +61,17 @@
60
61
  ]
61
62
  }
62
63
  },
64
+ "editions": {
65
+ "title": "Editions",
66
+ "description": "The edition of a lesson the rule is about: its chapter, its deck (`<lesson>.deck.mdx`), or both. A rule about one edition is checked there only, and listed on that edition’s rules-in-effect page.",
67
+ "type": "string",
68
+ "enum": [
69
+ "both",
70
+ "chapter",
71
+ "deck"
72
+ ],
73
+ "default": "both"
74
+ },
63
75
  "enabled": {
64
76
  "title": "Enabled",
65
77
  "description": "Off: the rule stays in this file and moves to the \"Off\" list on the rules-in-effect page — nothing runs, and an agent neither applies it nor flags it in review. Use it to park a rule that is drafted but not in force.",
@@ -1,5 +1,5 @@
1
- import { Element, Enforcement } from './catalog.js';
2
- import { M as ManifestRules } from '../course-config-rRtME5UV.js';
1
+ import { Element, Enforcement, Editions, Edition } from './catalog.js';
2
+ import { M as ManifestRules } from '../course-config-BLtIdkei.js';
3
3
  import './course-rules.js';
4
4
 
5
5
  /** The slice of a manifest the page needs. */
@@ -17,11 +17,15 @@ interface RulesInEffectInput {
17
17
  readonly version: string;
18
18
  };
19
19
  };
20
+ /** The course's decks (`manifest.decks`): with any, the deck page is written too. */
21
+ readonly decks?: readonly unknown[];
20
22
  }
21
23
  interface RulesFilter {
22
24
  /** Keep a rule whose elements (any, not just primary) include one of these. */
23
25
  readonly elements?: readonly Element[];
24
26
  readonly enforcement?: Enforcement | 'locked';
27
+ /** The edition the page is for: `chapter` (the default) or `deck`. */
28
+ readonly edition?: Edition;
25
29
  }
26
30
  /** One rule as `content rules --json` reports it. */
27
31
  interface RuleInEffect {
@@ -39,6 +43,8 @@ interface RuleInEffect {
39
43
  readonly settings: Readonly<Record<string, unknown>>;
40
44
  readonly summary: string;
41
45
  readonly doc: string;
46
+ /** The edition it is about: `chapter`, `deck`, or `both`. */
47
+ readonly editions: Editions;
42
48
  }
43
49
  declare function rulesInEffect(input: RulesInEffectInput, filter?: RulesFilter): RuleInEffect[];
44
50
  /**
@@ -47,7 +53,11 @@ declare function rulesInEffect(input: RulesInEffectInput, filter?: RulesFilter):
47
53
  * the reader knows what was left out on purpose.
48
54
  */
49
55
  declare function renderRulesInEffect(input: RulesInEffectInput, filter?: RulesFilter): string;
50
- /** Write `build/rules-in-effect.md` under `root` (the course package). Returns the path. */
56
+ /**
57
+ * Write `build/rules-in-effect.md` under `root` (the course package), and
58
+ * `build/rules-in-effect-deck.md` when the course has a deck (a stale one goes
59
+ * when it has none). Returns the chapter page's path.
60
+ */
51
61
  declare function writeRulesInEffect(input: RulesInEffectInput, root?: string): string;
52
62
 
53
63
  export { type RuleInEffect, type RulesFilter, type RulesInEffectInput, renderRulesInEffect, rulesInEffect, writeRulesInEffect };
@@ -1,7 +1,8 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import {
4
4
  ELEMENTS,
5
+ appliesTo,
5
6
  enforcementOf,
6
7
  invariants,
7
8
  rules as catalogRules
@@ -19,6 +20,7 @@ function enforcementLabel(entry) {
19
20
  return entry.kind === "rule" && entry.locked !== void 0 ? "locked" : enforcementOf(entry);
20
21
  }
21
22
  function matches(entry, filter) {
23
+ if (!appliesTo(entry, filter.edition ?? "chapter")) return false;
22
24
  if (filter.elements && !entry.elements.some((e) => filter.elements.includes(e))) return false;
23
25
  if (filter.enforcement && enforcementLabel(entry) !== filter.enforcement) return false;
24
26
  return true;
@@ -38,7 +40,8 @@ function rulesInEffect(input, filter = {}) {
38
40
  codes: entry.checks.map((c) => c.code),
39
41
  settings: entry.kind === "rule" ? input.rules.settings[entry.id] ?? {} : {},
40
42
  summary: entry.summary,
41
- doc: docLink(entry)
43
+ doc: docLink(entry),
44
+ editions: entry.editions ?? "both"
42
45
  }));
43
46
  }
44
47
  function settingValue(value) {
@@ -69,23 +72,33 @@ function ruleLine(rule, settings) {
69
72
  return `- ${head}${also} \u2014 ${rule.summary}${settingsText(rule, settings)}${unchecked} \u2192 [${rule.doc}](${docLink(rule)})`;
70
73
  }
71
74
  function renderRulesInEffect(input, filter = {}) {
75
+ const edition = filter.edition ?? "chapter";
72
76
  const off = new Set(input.rules.disabled);
73
- const on = liveRules(input).filter((r) => !off.has(r.id));
77
+ const live = liveRules(input).filter((r) => appliesTo(r, edition));
78
+ const on = live.filter((r) => !off.has(r.id));
79
+ const offHere = [...off].filter((id) => live.some((r) => r.id === id));
74
80
  const ours = on.filter((r) => r.source === "course").length;
75
81
  const locked = on.filter((r) => r.locked !== void 0).map((r) => r.id);
76
82
  const filtered = filter.elements || filter.enforcement;
77
83
  const visible = on.filter((r) => matches(r, filter));
84
+ const editionInvariants = invariants().filter((i) => appliesTo(i, edition));
78
85
  const lines = [];
79
86
  lines.push(
80
- `# Rules in effect \u2014 ${input.about.material.name} (knowledge ${input.about.knowledge.version}, ${input.about.engine.name} ${input.about.engine.version})`,
87
+ `# Rules in effect${edition === "deck" ? " for decks" : ""} \u2014 ${input.about.material.name} (knowledge ${input.about.knowledge.version}, ${input.about.engine.name} ${input.about.engine.version})`,
81
88
  "",
89
+ ...edition === "deck" ? [
90
+ "For a lesson\u2019s deck, `<lesson>.deck.mdx` (`docs/authoring/deck.md`): the `D` rules",
91
+ "lead, then every rule that holds for a deck as for a chapter. A chapter-only rule is",
92
+ 'listed under "Chapter only" at the end; on a frame, a `D` rule says it instead.',
93
+ ""
94
+ ] : [],
82
95
  "Generated by `pnpm validate:content` (and `pnpm content rules`) from the rule",
83
96
  "catalog and `rules` / `courseRules` in `course.settings.json`. Do not edit. Read this before",
84
97
  "drafting, and open a rule page only when it is listed here and you need the",
85
98
  "example: a rule absent from this page is either off for this course or does",
86
99
  "not exist, and is neither applied nor flagged in review.",
87
100
  "",
88
- `${on.length} rules on (${locked.length} locked${ours === 0 ? "" : `, ${ours} this course's own`}), ${off.size} off, ${invariants().length} invariants always on.` + (filtered ? ` Filtered: ${[
101
+ `${on.length} rules on (${locked.length} locked${ours === 0 ? "" : `, ${ours} this course's own`}), ${offHere.length} off, ${editionInvariants.length} invariants always on.` + (filtered ? ` Filtered: ${[
89
102
  filter.elements ? `element ${filter.elements.join(", ")}` : "",
90
103
  filter.enforcement ? `enforcement ${filter.enforcement}` : ""
91
104
  ].filter(Boolean).join("; ")} \u2014 ${visible.length} shown.` : ""),
@@ -108,7 +121,7 @@ function renderRulesInEffect(input, filter = {}) {
108
121
  lines.push("", `## ${element}`, "");
109
122
  for (const rule of here) lines.push(ruleLine(rule, input.rules.settings[rule.id] ?? {}));
110
123
  }
111
- const visibleInvariants = invariants().filter((i) => matches(i, filter));
124
+ const visibleInvariants = editionInvariants.filter((i) => matches(i, filter));
112
125
  lines.push("", "## Always on (invariants)", "");
113
126
  if (visibleInvariants.length === 0) {
114
127
  lines.push("- (none match the filter)");
@@ -121,10 +134,17 @@ function renderRulesInEffect(input, filter = {}) {
121
134
  }
122
135
  lines.push("", "## Off", "");
123
136
  lines.push(
124
- off.size === 0 ? "- Nothing \u2014 every rule is on." : `- ${[...off].join(", ")} \u2014 turned off in \`course.settings.json\` (\`rules\`, or \`courseRules\` for an \`X\` rule). Do not apply them; do not flag them in review.`
137
+ offHere.length === 0 ? "- Nothing \u2014 every rule is on." : `- ${offHere.join(", ")} \u2014 turned off in \`course.settings.json\` (\`rules\`, or \`courseRules\` for an \`X\` rule). Do not apply them; do not flag them in review.`
125
138
  );
126
139
  lines.push("", "## Locked", "");
127
140
  lines.push(`- ${locked.join(", ")} \u2014 cannot be turned off; listed above under their element.`);
141
+ if (edition === "deck") {
142
+ const chapterOnly = liveRules(input).filter((r) => !appliesTo(r, "deck")).map((r) => r.id);
143
+ lines.push("", "## Chapter only", "");
144
+ lines.push(
145
+ chapterOnly.length === 0 ? "- Nothing \u2014 every rule holds for a deck too." : `- ${chapterOnly.join(", ")} \u2014 about a chapter\u2019s page, not a frame. On a deck, neither apply them nor flag them; the \`D\` rules above say the same of a frame.`
146
+ );
147
+ }
128
148
  return `${lines.join("\n")}
129
149
  `;
130
150
  }
@@ -133,6 +153,9 @@ function writeRulesInEffect(input, root = process.cwd()) {
133
153
  mkdirSync(dir, { recursive: true });
134
154
  const path = join(dir, "rules-in-effect.md");
135
155
  writeFileSync(path, renderRulesInEffect(input));
156
+ const deckPath = join(dir, "rules-in-effect-deck.md");
157
+ if ((input.decks?.length ?? 0) > 0) writeFileSync(deckPath, renderRulesInEffect(input, { edition: "deck" }));
158
+ else if (existsSync(deckPath)) rmSync(deckPath);
136
159
  return path;
137
160
  }
138
161
  export {