explico 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +990 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +100 -0
  6. package/dist/compiler/cli-main.js +433 -0
  7. package/dist/compiler/cli-reference.d.ts +50 -0
  8. package/dist/compiler/cli-reference.js +213 -0
  9. package/dist/compiler/cli-verbs.d.ts +45 -0
  10. package/dist/compiler/cli-verbs.js +224 -0
  11. package/dist/compiler/cli-verify.d.ts +51 -0
  12. package/dist/compiler/cli-verify.js +151 -0
  13. package/dist/compiler/cli.d.ts +408 -24
  14. package/dist/compiler/cli.js +1328 -8
  15. package/dist/compiler/collections.d.ts +88 -1
  16. package/dist/compiler/collections.js +170 -25
  17. package/dist/compiler/content-release.d.ts +49 -0
  18. package/dist/compiler/content-release.js +63 -0
  19. package/dist/compiler/content-schemas.d.ts +2 -2
  20. package/dist/compiler/course-config.d.ts +1 -1
  21. package/dist/compiler/course-config.js +105 -1
  22. package/dist/compiler/decks.d.ts +161 -0
  23. package/dist/compiler/decks.js +235 -0
  24. package/dist/compiler/exports/combined-html.d.ts +5 -1
  25. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  26. package/dist/compiler/exports/deck-fit.js +90 -0
  27. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  28. package/dist/compiler/exports/export-chapters.js +64 -5
  29. package/dist/compiler/exports/export-pages.d.ts +30 -3
  30. package/dist/compiler/exports/export-pages.js +24 -1
  31. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  32. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  33. package/dist/compiler/exports/latex/cache.js +129 -0
  34. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  35. package/dist/compiler/exports/latex/capture.js +3 -1
  36. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  37. package/dist/compiler/exports/latex/compile.js +12 -1
  38. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  39. package/dist/compiler/exports/latex/deck.js +349 -0
  40. package/dist/compiler/exports/latex/document.d.ts +51 -6
  41. package/dist/compiler/exports/latex/document.js +71 -12
  42. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  43. package/dist/compiler/exports/latex/explico.cls +13 -183
  44. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  45. package/dist/compiler/exports/latex/serialize.js +4 -2
  46. package/dist/compiler/exports/latex-export.d.ts +24 -2
  47. package/dist/compiler/exports/latex-export.js +107 -26
  48. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  49. package/dist/compiler/exports/markdown/serialize.js +85 -8
  50. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  51. package/dist/compiler/exports/markdown-export.js +50 -11
  52. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  53. package/dist/compiler/exports/standalone-html.js +3 -3
  54. package/dist/compiler/exports/static-tree.d.ts +38 -1
  55. package/dist/compiler/exports/static-tree.js +120 -4
  56. package/dist/compiler/knowledge-version.d.ts +26 -2
  57. package/dist/compiler/knowledge-version.js +27 -17
  58. package/dist/compiler/lesson-derivation.d.ts +143 -6
  59. package/dist/compiler/lesson-derivation.js +145 -13
  60. package/dist/compiler/manifest.d.ts +133 -4
  61. package/dist/compiler/manifest.js +364 -58
  62. package/dist/compiler/page-versions.d.ts +184 -0
  63. package/dist/compiler/page-versions.js +217 -0
  64. package/dist/compiler/render-diagnostics.d.ts +5 -1
  65. package/dist/compiler/resolution-report.d.ts +24 -6
  66. package/dist/compiler/resolution-report.js +22 -4
  67. package/dist/compiler/validate-content.d.ts +7 -0
  68. package/dist/compiler/validate-content.js +160 -0
  69. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  70. package/dist/curriculum/competency-graph.d.ts +54 -0
  71. package/dist/curriculum/competency-graph.js +41 -0
  72. package/dist/reference/base-library.d.mts +8 -0
  73. package/dist/reference/base-library.mjs +19 -0
  74. package/dist/reference/consistency.d.ts +8 -4
  75. package/dist/reference/consistency.js +82 -26
  76. package/dist/reference/deck-remark.d.mts +123 -0
  77. package/dist/reference/deck-remark.mjs +600 -0
  78. package/dist/reference/deck-source.d.ts +107 -0
  79. package/dist/reference/deck-source.js +140 -0
  80. package/dist/reference/decks-validate.d.ts +133 -0
  81. package/dist/reference/decks-validate.js +359 -0
  82. package/dist/reference/derived-entries.d.ts +5 -1
  83. package/dist/reference/diagram-fit.d.ts +6 -2
  84. package/dist/reference/diagram-fit.js +5 -1
  85. package/dist/reference/figure-symbols.d.ts +23 -6
  86. package/dist/reference/figure-symbols.js +51 -15
  87. package/dist/reference/float-name.d.ts +43 -0
  88. package/dist/reference/float-name.js +58 -0
  89. package/dist/reference/folds-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-indices.js +5 -4
  92. package/dist/reference/gate-math.d.ts +29 -2
  93. package/dist/reference/gate-math.js +99 -18
  94. package/dist/reference/gate-part-pages.d.ts +1 -1
  95. package/dist/reference/lab-katex-validate.d.ts +10 -6
  96. package/dist/reference/lab-katex-validate.js +17 -9
  97. package/dist/reference/lab-math-scope.js +29 -4
  98. package/dist/reference/math-glyphs.d.mts +25 -0
  99. package/dist/reference/math-glyphs.mjs +189 -25
  100. package/dist/reference/page-notation.d.ts +22 -3
  101. package/dist/reference/page-notation.js +32 -10
  102. package/dist/reference/references.d.ts +14 -1
  103. package/dist/reference/references.js +13 -0
  104. package/dist/reference/registry-scope.d.mts +33 -0
  105. package/dist/reference/registry-scope.mjs +34 -0
  106. package/dist/reference/registry.js +29 -10
  107. package/dist/reference/rehype-deck.d.mts +6 -0
  108. package/dist/reference/rehype-deck.mjs +286 -0
  109. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  110. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  111. package/dist/reference/remark-notation.mjs +249 -40
  112. package/dist/reference/source-lines.d.mts +12 -0
  113. package/dist/reference/source-lines.mjs +65 -0
  114. package/dist/reference/types.d.ts +28 -0
  115. package/dist/rules/catalog.d.ts +21 -4
  116. package/dist/rules/catalog.js +237 -2
  117. package/dist/rules/course-rules.d.ts +5 -1
  118. package/dist/rules/course-rules.js +17 -3
  119. package/dist/rules/course-rules.schema.json +12 -0
  120. package/dist/rules/in-effect.d.ts +13 -3
  121. package/dist/rules/in-effect.js +34 -10
  122. package/dist/rules/rules.schema.json +280 -1
  123. package/dist/rules/settings.d.ts +1 -1
  124. package/dist/session/PreferenceStore.d.ts +6 -0
  125. package/dist/session/PreferenceStore.js +18 -1
  126. package/dist/session/deck-memory.d.ts +116 -0
  127. package/dist/session/deck-memory.js +143 -0
  128. package/dist/session/reading-memory.d.ts +96 -0
  129. package/dist/session/reading-memory.js +133 -0
  130. package/docs/VERSIONING.md +21 -18
  131. package/docs/authoring/README.md +31 -9
  132. package/docs/authoring/agents.md +72 -18
  133. package/docs/authoring/cli-reference.json +2172 -0
  134. package/docs/authoring/cli.md +887 -0
  135. package/docs/authoring/components.md +15 -0
  136. package/docs/authoring/deck.md +612 -0
  137. package/docs/authoring/migrating.md +1195 -16
  138. package/docs/authoring/notation.md +49 -6
  139. package/docs/authoring/structure.md +80 -1
  140. package/docs/authoring/versioning.md +56 -3
  141. package/docs/authoring/visualizations.md +7 -2
  142. package/docs/course-agent/AGENTS.md +40 -0
  143. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  144. package/docs/course-agent/skills/draft-lesson/SKILL.md +38 -0
  145. package/docs/course-agent/skills/migrate-course/SKILL.md +32 -0
  146. package/docs/course-agent/skills/release-content/SKILL.md +26 -0
  147. package/docs/diagram-scaling.md +148 -0
  148. package/docs/privacy.md +59 -0
  149. package/package.json +8 -2
  150. package/src/compiler/astro.ts +60 -21
  151. package/src/compiler/cli-main.ts +557 -0
  152. package/src/compiler/cli-reference.ts +285 -0
  153. package/src/compiler/cli-verbs.ts +270 -0
  154. package/src/compiler/cli-verify.ts +241 -0
  155. package/src/compiler/cli.ts +2119 -170
  156. package/src/compiler/collections.ts +284 -25
  157. package/src/compiler/content-release.ts +91 -0
  158. package/src/compiler/course-config.ts +267 -0
  159. package/src/compiler/decks.ts +455 -0
  160. package/src/compiler/exports/deck-fit.ts +127 -0
  161. package/src/compiler/exports/export-chapters.ts +105 -6
  162. package/src/compiler/exports/export-pages.ts +46 -2
  163. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  164. package/src/compiler/exports/latex/cache.ts +183 -0
  165. package/src/compiler/exports/latex/capture.ts +5 -2
  166. package/src/compiler/exports/latex/compile.ts +18 -0
  167. package/src/compiler/exports/latex/deck.ts +419 -0
  168. package/src/compiler/exports/latex/document.ts +117 -21
  169. package/src/compiler/exports/latex/explico-common.sty +198 -0
  170. package/src/compiler/exports/latex/explico.cls +13 -183
  171. package/src/compiler/exports/latex/serialize.ts +25 -21
  172. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  173. package/src/compiler/exports/latex-export.ts +187 -45
  174. package/src/compiler/exports/markdown/serialize.ts +118 -9
  175. package/src/compiler/exports/markdown-export.ts +56 -11
  176. package/src/compiler/exports/standalone-html.ts +8 -7
  177. package/src/compiler/exports/static-tree.ts +175 -3
  178. package/src/compiler/knowledge-version.ts +56 -21
  179. package/src/compiler/lesson-derivation.ts +282 -16
  180. package/src/compiler/manifest.ts +568 -63
  181. package/src/compiler/page-versions.ts +401 -0
  182. package/src/compiler/resolution-report.ts +38 -7
  183. package/src/compiler/validate-content.ts +215 -0
  184. package/src/components/CourseContents.astro +124 -6
  185. package/src/components/CurriculumMap.astro +49 -1
  186. package/src/components/FloatIndex.astro +1 -1
  187. package/src/components/PartOverview.astro +51 -5
  188. package/src/components/assessments/AssessmentSet.astro +41 -15
  189. package/src/components/block-heading.ts +15 -6
  190. package/src/components/card-carry.ts +7 -4
  191. package/src/components/card-manager.ts +14 -2
  192. package/src/components/card-memory.ts +13 -9
  193. package/src/components/deck/DeckEnhancer.astro +13 -0
  194. package/src/components/deck/deck-context.ts +104 -0
  195. package/src/components/deck/deck-controller.ts +1287 -0
  196. package/src/components/deck/deck-map.ts +91 -0
  197. package/src/components/deck-lane.ts +163 -0
  198. package/src/components/diagram/Diagram.astro +131 -86
  199. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  200. package/src/components/figure/Figure.astro +7 -2
  201. package/src/components/figure/FloatCaption.astro +5 -3
  202. package/src/components/fold/Fold.astro +2 -2
  203. package/src/components/notation/NotationFormula.astro +81 -11
  204. package/src/components/notation/NotationGlossary.astro +132 -1
  205. package/src/components/notation/NotationLayer.astro +15 -7
  206. package/src/components/starlight/AboutPanel.astro +186 -3
  207. package/src/components/starlight/CollapsibleSections.astro +24 -3
  208. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  209. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  210. package/src/components/starlight/LayoutHeader.astro +17 -0
  211. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  212. package/src/components/starlight/LayoutSidebar.astro +807 -21
  213. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  214. package/src/components/starlight/LessonExportActions.astro +27 -9
  215. package/src/components/starlight/LessonFooter.astro +59 -0
  216. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  217. package/src/components/starlight/TextSizeControl.astro +24 -13
  218. package/src/components/starlight/page-marks.ts +139 -0
  219. package/src/components/starlight/section-summary.ts +1 -1
  220. package/src/components/starlight/sidebar-editions.ts +245 -0
  221. package/src/components/table/Table.astro +15 -7
  222. package/src/content-config.ts +87 -1
  223. package/src/curriculum/competency-graph.ts +95 -0
  224. package/src/reference/base-library.mjs +19 -0
  225. package/src/reference/consistency.ts +137 -39
  226. package/src/reference/deck-remark.mjs +600 -0
  227. package/src/reference/deck-source.ts +273 -0
  228. package/src/reference/decks-validate.ts +604 -0
  229. package/src/reference/diagram-fit.ts +18 -4
  230. package/src/reference/diagram-layout.ts +3 -1
  231. package/src/reference/figure-symbols.ts +108 -31
  232. package/src/reference/folds-validate.ts +1 -1
  233. package/src/reference/gate-indices.ts +5 -3
  234. package/src/reference/gate-math.ts +169 -25
  235. package/src/reference/lab-katex-validate.ts +30 -15
  236. package/src/reference/lab-math-scope.ts +34 -3
  237. package/src/reference/math-glyphs.mjs +189 -25
  238. package/src/reference/page-notation.ts +69 -15
  239. package/src/reference/references.ts +28 -0
  240. package/src/reference/registry-scope.mjs +34 -0
  241. package/src/reference/registry.ts +41 -11
  242. package/src/reference/rehype-deck.mjs +286 -0
  243. package/src/reference/rehype-heading-anchors.mjs +48 -6
  244. package/src/reference/remark-notation.mjs +249 -40
  245. package/src/reference/source-lines.mjs +65 -0
  246. package/src/reference/types.ts +28 -0
  247. package/src/rules/catalog.ts +274 -4
  248. package/src/rules/course-rules.ts +21 -1
  249. package/src/rules/in-effect.ts +58 -11
  250. package/src/session/PreferenceStore.ts +23 -0
  251. package/src/session/deck-memory.ts +263 -0
  252. package/src/session/reading-memory.ts +243 -0
  253. package/src/styles/deck.css +1150 -0
  254. package/src/styles/global.css +958 -27
  255. package/src/styles/zones.css +277 -0
  256. /package/src/{components → reference}/float-name.ts +0 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * The `content` CLI's machine-readable reference: every verb's row from
3
+ * `./cli-verbs`, and a JSON Schema of every type a verb's `--json` prints.
4
+ *
5
+ * TypeScript types are gone at run time, so `scripts/agent/gen-cli.mjs`
6
+ * reads them from the source with the compiler API and writes the schemas
7
+ * into `docs/authoring/cli-reference.json` beside the rule pages, where the
8
+ * CLI finds them in the workspace and in an installed package alike; it
9
+ * writes `docs/authoring/cli.md` from the same data with `cliReferenceMarkdown`.
10
+ * `--check` holds both to the source in verify and CI.
11
+ *
12
+ * `content help <verb>` prints a verb's row and the shape of its `--json`
13
+ * (`formatVerbHelp`); `--json` prints the row with a self-contained schema.
14
+ * `jsonShapeErrors` checks a value against a schema — the playground's
15
+ * contract test runs every read-only verb through it.
16
+ */
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ import { ContentCliError, authoringDocsDir } from './cli';
21
+ import { CONTENT_VERBS, usageLine, type ContentVerb } from './cli-verbs';
22
+
23
+ /** A JSON Schema (draft 2020-12), the subset the generator writes. */
24
+ export type JsonSchema = Readonly<Record<string, unknown>>;
25
+
26
+ /** `docs/authoring/cli-reference.json`, whole: what `content help --json` prints. */
27
+ export interface CliReference {
28
+ /** Where it came from, and that it is generated. */
29
+ readonly generatedBy: string;
30
+ readonly verbs: readonly ContentVerb[];
31
+ /** The type every verb's `--json` prints when the run fails: a stable code and a message, never a stack. */
32
+ readonly error: string;
33
+ /** A JSON Schema of every type a verb's `--json` prints, and of every named type inside one, by name. */
34
+ readonly $defs: Readonly<Record<string, JsonSchema>>;
35
+ }
36
+
37
+ /** One verb's row, its usage line, and a self-contained JSON Schema of its `--json`. */
38
+ export interface VerbHelp extends ContentVerb {
39
+ readonly usage: string;
40
+ readonly schema: JsonSchema;
41
+ }
42
+
43
+ /** What `content help <verb> --json` prints: every verb the word names (`new` names three). */
44
+ export interface VerbHelpJson {
45
+ readonly verbs: readonly VerbHelp[];
46
+ }
47
+
48
+ /** `content help --json` prints the whole reference; `content help <verb> --json` that verb's rows. */
49
+ export type HelpJson = CliReference | VerbHelpJson;
50
+
51
+ export const CLI_REFERENCE_FILE = 'cli-reference.json';
52
+
53
+ /** The reference beside the engine (`node_modules/explico/docs/authoring/`, or this repository's). */
54
+ export function loadCliReference(): CliReference {
55
+ const dir = authoringDocsDir();
56
+ const file = dir === undefined ? undefined : join(dir, CLI_REFERENCE_FILE);
57
+ if (file === undefined || !existsSync(file)) {
58
+ throw new ContentCliError('unknown-command', `No ${CLI_REFERENCE_FILE} beside the engine (node_modules/explico/docs/authoring).`);
59
+ }
60
+ return JSON.parse(readFileSync(file, 'utf8')) as CliReference;
61
+ }
62
+
63
+ /** The verbs a word names: `symbol` one, `new` the three `new …` verbs. */
64
+ export function verbsNamed(word: string): readonly ContentVerb[] {
65
+ const hits = CONTENT_VERBS.filter((verb) => verb.verb === word || verb.verb.startsWith(`${word} `));
66
+ if (hits.length === 0) {
67
+ throw new ContentCliError(
68
+ 'unknown-command',
69
+ `Unknown verb "${word}". The verbs: ${[...new Set(CONTENT_VERBS.map((verb) => verb.verb.split(' ')[0]))].join(', ')}.`,
70
+ );
71
+ }
72
+ return hits;
73
+ }
74
+
75
+ /** The names a schema refers to, itself and transitively. */
76
+ function reachableDefs(name: string, defs: CliReference['$defs'], seen = new Set<string>()): Set<string> {
77
+ if (seen.has(name) || defs[name] === undefined) return seen;
78
+ seen.add(name);
79
+ for (const match of JSON.stringify(defs[name]).matchAll(/"#\/\$defs\/([^"]+)"/g)) reachableDefs(match[1]!, defs, seen);
80
+ return seen;
81
+ }
82
+
83
+ /** A self-contained schema of one named type: a `$ref` and the `$defs` it reaches. */
84
+ export function schemaOf(reference: CliReference, name: string): JsonSchema {
85
+ const names = [...reachableDefs(name, reference.$defs)].sort();
86
+ return {
87
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
88
+ $ref: `#/$defs/${name}`,
89
+ $defs: Object.fromEntries(names.map((n) => [n, reference.$defs[n]])),
90
+ };
91
+ }
92
+
93
+ export function verbHelp(reference: CliReference, word: string): VerbHelpJson {
94
+ return {
95
+ verbs: verbsNamed(word).map((verb) => ({ ...verb, usage: usageLine(verb), schema: schemaOf(reference, verb.json) })),
96
+ };
97
+ }
98
+
99
+ // --- rendering a schema as a TypeScript-like shape -------------------------
100
+
101
+ function oneLine(text: unknown): string {
102
+ return typeof text === 'string' ? text.replace(/\s+/g, ' ').trim() : '';
103
+ }
104
+
105
+ function refName(ref: string): string {
106
+ return ref.replace('#/$defs/', '');
107
+ }
108
+
109
+ function isObjectShape(schema: JsonSchema): boolean {
110
+ return schema.type === 'object' && schema.properties !== undefined;
111
+ }
112
+
113
+ /** A schema's type as one TypeScript-like expression; an object with properties is `{…}` here and expanded by `shapeLines`. */
114
+ function typeExpression(schema: JsonSchema): string {
115
+ if (typeof schema.$ref === 'string') return refName(schema.$ref);
116
+ if ('const' in schema) return JSON.stringify(schema.const);
117
+ if (Array.isArray(schema.enum)) return schema.enum.map((value) => JSON.stringify(value)).join(' | ');
118
+ if (Array.isArray(schema.anyOf)) return (schema.anyOf as JsonSchema[]).map(typeExpression).join(' | ');
119
+ if (schema.type === 'array') {
120
+ const items = typeExpression((schema.items ?? {}) as JsonSchema);
121
+ return / \| /.test(items) ? `(${items})[]` : `${items}[]`;
122
+ }
123
+ if (schema.type === 'object') {
124
+ if (schema.properties !== undefined) return '{…}';
125
+ const value = schema.additionalProperties;
126
+ return `Record<string, ${typeof value === 'object' && value !== null ? typeExpression(value as JsonSchema) : 'unknown'}>`;
127
+ }
128
+ if (schema.type === 'integer') return 'number';
129
+ if (typeof schema.type === 'string') return schema.type;
130
+ return 'unknown';
131
+ }
132
+
133
+ /** The object a property's type expands to, when it has one: `{…}`, `{…}[]`, `{…} | null`. */
134
+ function expandable(schema: JsonSchema): JsonSchema | undefined {
135
+ if (isObjectShape(schema)) return schema;
136
+ if (schema.type === 'array') return expandable((schema.items ?? {}) as JsonSchema);
137
+ if (Array.isArray(schema.anyOf)) {
138
+ const objects = (schema.anyOf as JsonSchema[]).map(expandable).filter((s): s is JsonSchema => s !== undefined);
139
+ return objects.length === 1 ? objects[0] : undefined;
140
+ }
141
+ return undefined;
142
+ }
143
+
144
+ /** One line per property, `name?: type — description`, nested objects indented under their property. */
145
+ export function shapeLines(schema: JsonSchema, indent = ' '): string[] {
146
+ const lines: string[] = [];
147
+ const required = new Set((schema.required ?? []) as string[]);
148
+ for (const [name, raw] of Object.entries((schema.properties ?? {}) as Record<string, JsonSchema>)) {
149
+ const description = oneLine(raw.description);
150
+ lines.push(`${indent}${name}${required.has(name) ? '' : '?'}: ${typeExpression(raw)}${description === '' ? '' : ` — ${description}`}`);
151
+ const nested = expandable(raw);
152
+ if (nested !== undefined) lines.push(...shapeLines(nested, `${indent} `));
153
+ }
154
+ return lines;
155
+ }
156
+
157
+ /** A named type as a block: its name and description, then its shape. */
158
+ export function defLines(name: string, schema: JsonSchema): string[] {
159
+ const description = oneLine(schema.description);
160
+ const head = `${name}${isObjectShape(schema) ? '' : ` = ${typeExpression(schema)}`}`;
161
+ return [description === '' ? head : `${head} — ${description}`, ...(isObjectShape(schema) ? shapeLines(schema) : [])];
162
+ }
163
+
164
+ /** A verb's row as labelled lines: what it is for, what it prints and writes, when it exits 1. */
165
+ function verbFacts(verb: ContentVerb): (readonly [string, string])[] {
166
+ return [
167
+ ['For', verb.when],
168
+ ['Prints', verb.prints],
169
+ ['Writes', verb.writes],
170
+ ['Exits 1', `on a usage error${verb.exit === undefined ? '' : `, and ${verb.exit}`}`],
171
+ ];
172
+ }
173
+
174
+ /** The verb's own type first, then the types it reaches, by name. */
175
+ function typeOrder(verb: VerbHelp): string[] {
176
+ return [verb.json, ...Object.keys(verb.schema.$defs as object).filter((name) => name !== verb.json).sort()];
177
+ }
178
+
179
+ /** `content help <verb>` as text: the usage, what it is for, its flags, and the shape of its `--json`. */
180
+ export function formatVerbHelp(reference: CliReference, help: VerbHelpJson): string {
181
+ const blocks = help.verbs.map((verb) => {
182
+ const lines = [verb.usage, '', ...verbFacts(verb).map(([label, text]) => ` ${`${label}:`.padEnd(9)}${text}`)];
183
+ if (verb.flags.length > 0) {
184
+ lines.push('', 'Flags:');
185
+ const width = Math.max(...verb.flags.map((flag) => flag.flag.length));
186
+ for (const flag of verb.flags) lines.push(` ${flag.flag.padEnd(width)} ${flag.description}`);
187
+ }
188
+ lines.push('', `--json prints ${verb.json}:`);
189
+ for (const name of typeOrder(verb)) lines.push(...defLines(name, reference.$defs[name]!).map((line) => ` ${line}`), '');
190
+ return lines.join('\n').trimEnd();
191
+ });
192
+ return `${blocks.join('\n\n')}\n\nOn a failure --json prints ${reference.error}: { "ok": false, "error": { "code", "message" } }.\n`;
193
+ }
194
+
195
+ // --- docs/authoring/cli.md ---------------------------------------------------
196
+
197
+ function anchor(name: string): string {
198
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
199
+ }
200
+
201
+ /** `docs/authoring/cli.md`, generated: one section per verb, then every `--json` type. */
202
+ export function cliReferenceMarkdown(reference: CliReference): string {
203
+ const out = [
204
+ '<!-- generated by scripts/agent/gen-cli.mjs from src/compiler/cli-verbs.ts and the report types — do not edit; `node scripts/agent/gen-cli.mjs` rewrites it, `--check` gates it -->',
205
+ '',
206
+ '# The `content` CLI',
207
+ '',
208
+ 'Every verb of `pnpm content`, what it writes, when it exits 1, and the shape',
209
+ 'of its `--json`. The same data, with a JSON Schema per type, is',
210
+ '[`cli-reference.json`](cli-reference.json); `pnpm content help <verb> [--json]`',
211
+ 'prints one verb of it. Every verb takes `--json`: data only on stdout, and on',
212
+ `a failure \`{ "ok": false, "error": { "code", "message" } }\` ([\`${reference.error}\`](#${anchor(reference.error)})).`,
213
+ '',
214
+ '| Verb | You want to know or do | Writes |',
215
+ '| --- | --- | --- |',
216
+ ...reference.verbs.map((verb) => `| [\`${verb.verb}\`](#${anchor(`content ${verb.verb}`)}) | ${verb.when} | ${verb.writes} |`),
217
+ '',
218
+ ];
219
+ for (const verb of reference.verbs) {
220
+ out.push(`## content ${verb.verb}`, '', '```text', usageLine(verb), '```', '');
221
+ for (const [label, text] of verbFacts(verb)) out.push(`- **${label}:** ${text.replace(/\\explain/g, '`\\explain`')}`);
222
+ out.push('');
223
+ if (verb.flags.length > 0) {
224
+ out.push('| Flag | Effect |', '| --- | --- |');
225
+ for (const flag of verb.flags) out.push(`| \`${flag.flag}\` | ${flag.description} |`);
226
+ out.push('');
227
+ }
228
+ out.push(`\`--json\`: [\`${verb.json}\`](#${anchor(verb.json)})`, '');
229
+ }
230
+ out.push('## The `--json` types', '', 'A `?` marks a property that may be absent.', '');
231
+ for (const name of Object.keys(reference.$defs).sort()) {
232
+ out.push(`### ${name}`, '', '```ts', ...defLines(name, reference.$defs[name]!), '```', '');
233
+ }
234
+ return `${out.join('\n').trimEnd()}\n`;
235
+ }
236
+
237
+ // --- checking a value against a schema ---------------------------------------
238
+
239
+ /**
240
+ * Where `value` departs from `schema`, one message per place (`$.hits[0].tier:
241
+ * expected one of …`); empty when it conforms. The subset the generator
242
+ * writes: `$ref`, `type`, `const`, `enum`, `anyOf`, `properties`, `required`,
243
+ * `additionalProperties`, `items`.
244
+ */
245
+ export function jsonShapeErrors(schema: JsonSchema, value: unknown, defs = (schema.$defs ?? {}) as CliReference['$defs'], path = '$'): string[] {
246
+ if (typeof schema.$ref === 'string') {
247
+ const target = defs[refName(schema.$ref)];
248
+ return target === undefined ? [`${path}: unknown ${schema.$ref}`] : jsonShapeErrors(target, value, defs, path);
249
+ }
250
+ if ('const' in schema) return value === schema.const ? [] : [`${path}: expected ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`];
251
+ if (Array.isArray(schema.enum)) {
252
+ return schema.enum.includes(value) ? [] : [`${path}: expected one of ${schema.enum.map((v) => JSON.stringify(v)).join(', ')}, got ${JSON.stringify(value)}`];
253
+ }
254
+ if (Array.isArray(schema.anyOf)) {
255
+ const branches = (schema.anyOf as JsonSchema[]).map((branch) => jsonShapeErrors(branch, value, defs, path));
256
+ if (branches.some((errors) => errors.length === 0)) return [];
257
+ return [`${path}: matches none of ${typeExpression(schema)} (${branches.map((errors) => errors[0]).join('; ')})`];
258
+ }
259
+ const type = schema.type;
260
+ if (type === undefined) return [];
261
+ const actual = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
262
+ const ok = type === 'integer' ? Number.isInteger(value) : type === actual;
263
+ if (!ok) return [`${path}: expected ${type}, got ${actual}`];
264
+ if (type === 'array') {
265
+ const items = (schema.items ?? {}) as JsonSchema;
266
+ return (value as unknown[]).flatMap((item, index) => jsonShapeErrors(items, item, defs, `${path}[${index}]`));
267
+ }
268
+ if (type !== 'object') return [];
269
+ const record = value as Record<string, unknown>;
270
+ const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;
271
+ const errors: string[] = [];
272
+ for (const name of (schema.required ?? []) as string[]) {
273
+ if (!(name in record)) errors.push(`${path}.${name}: missing`);
274
+ }
275
+ for (const [name, item] of Object.entries(record)) {
276
+ if (item === undefined) continue;
277
+ const declared = properties[name];
278
+ if (declared !== undefined) errors.push(...jsonShapeErrors(declared, item, defs, `${path}.${name}`));
279
+ else if (schema.additionalProperties === false) errors.push(`${path}.${name}: not in the type`);
280
+ else if (typeof schema.additionalProperties === 'object') {
281
+ errors.push(...jsonShapeErrors(schema.additionalProperties as JsonSchema, item, defs, `${path}.${name}`));
282
+ }
283
+ }
284
+ return errors;
285
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Every `content` verb, as data: its arguments, what it is for, what it
3
+ * prints, what it writes, when it exits non-zero, and the exported type its
4
+ * `--json` prints. The one source for the usage text (`CONTENT_USAGE`),
5
+ * `content help <verb>`, `docs/authoring/cli.md` and
6
+ * `docs/authoring/cli-reference.json` — the last two generated by
7
+ * `scripts/agent/gen-cli.mjs`, which also turns each `json` type name into a
8
+ * JSON Schema from the TypeScript declaration (`--check` in verify and CI).
9
+ *
10
+ * A new verb is a row here, a `case` in `cli-main.ts`, and a regenerated
11
+ * reference; the `--json` contract test in the playground runs every
12
+ * read-only row against the course and validates its output.
13
+ */
14
+
15
+ /** One flag a verb takes. */
16
+ export interface ContentVerbFlag {
17
+ /** As typed: `--lesson <id>`, `--write`. */
18
+ readonly flag: string;
19
+ readonly description: string;
20
+ }
21
+
22
+ /** One verb's row. */
23
+ export interface ContentVerb {
24
+ /** The command word(s): `symbol`, `new lesson`. */
25
+ readonly verb: string;
26
+ /** What follows the verb on the usage line, `--json` left out (every verb takes it). */
27
+ readonly args: string;
28
+ /** What you want to know or do when you reach for it. */
29
+ readonly when: string;
30
+ /** What it prints as text. */
31
+ readonly prints: string;
32
+ /** The flags, each once; a positional argument is described in `prints` or `when`. */
33
+ readonly flags: readonly ContentVerbFlag[];
34
+ /** What it changes on disk. */
35
+ readonly writes: string;
36
+ /** When it exits 1, beyond a usage error. */
37
+ readonly exit?: string;
38
+ /** The type its `--json` prints, by its exported name in `explico/compiler/cli`, `cli-main` or `cli-verify`. */
39
+ readonly json: string;
40
+ }
41
+
42
+ const WRITE: ContentVerbFlag = { flag: '--write', description: 'apply the edits; without it the diff is printed and nothing changes' };
43
+ const READ_ONLY = 'nothing';
44
+
45
+ export const CONTENT_VERBS: readonly ContentVerb[] = [
46
+ {
47
+ verb: 'status',
48
+ args: '',
49
+ when: 'how the course stands',
50
+ prints:
51
+ "counts, diagnostic totals, the knowledge and page versions, the agent surfaces' sizes, whether the course's AGENTS.md points at the engine's guide and the procedures are installed",
52
+ flags: [],
53
+ writes: READ_ONLY,
54
+ json: 'StatusReport',
55
+ },
56
+ {
57
+ verb: 'diagnostics',
58
+ args: '[info|warning|error] [--all | --code <c> | --lesson <id> | --changed]',
59
+ when: 'what is wrong, where',
60
+ prints:
61
+ 'counts by code (with each code\'s rule) and by file, at the given severity or higher (info by default); a filter or --all lists the lines instead. --json always prints the lines',
62
+ flags: [
63
+ { flag: '--all', description: 'list every diagnostic instead of the counts' },
64
+ { flag: '--code <c>', description: 'only that diagnostic code' },
65
+ { flag: '--lesson <id>', description: "only that lesson's file" },
66
+ { flag: '--changed', description: 'only lessons whose .mdx or .checks.yml differs from git HEAD, tracked or not' },
67
+ ],
68
+ writes: READ_ONLY,
69
+ json: 'DiagnosticsReport',
70
+ },
71
+ {
72
+ verb: 'warnings',
73
+ args: '',
74
+ when: 'the warnings alone',
75
+ prints: 'every warning, one line each with its rule, then the count',
76
+ flags: [],
77
+ writes: READ_ONLY,
78
+ json: 'WarningsReport',
79
+ },
80
+ {
81
+ verb: 'context',
82
+ args: '<lesson>',
83
+ when: 'what a lesson draws on',
84
+ prints:
85
+ 'its outcomes, notation, sources, diagnostics and token counts; --json adds the body, the floats it can reference, its sections and domain signatures',
86
+ flags: [],
87
+ writes: READ_ONLY,
88
+ json: 'ContextReport',
89
+ },
90
+ {
91
+ verb: 'equation',
92
+ args: '"<latex>" [--lesson <id>]',
93
+ when: 'will this expression build',
94
+ prints: "each glyph's card, unresolved glyphs with where they are defined",
95
+ flags: [{ flag: '--lesson <id>', description: "resolve against that lesson's local notation as well as the shared cards" }],
96
+ writes: READ_ONLY,
97
+ exit: 'when the build would refuse the expression',
98
+ json: 'EquationReport',
99
+ },
100
+ {
101
+ verb: 'bindings',
102
+ args: '<lesson>',
103
+ when: "what a lesson's math binds",
104
+ prints: 'every math span: its bindings, unresolved glyphs, redundant \\explain wraps',
105
+ flags: [],
106
+ writes: READ_ONLY,
107
+ json: 'BindingsReport',
108
+ },
109
+ {
110
+ verb: 'symbol',
111
+ args: '<latex | key>',
112
+ when: 'is this symbol defined already',
113
+ prints: 'the shared card, each local entry with its lesson, each gloss with its card, near misses',
114
+ flags: [],
115
+ writes: READ_ONLY,
116
+ json: 'SymbolReport',
117
+ },
118
+ {
119
+ verb: 'check',
120
+ args: '<lesson | deck>… [--changed]',
121
+ when: 'does it render',
122
+ prints: 'ok or FAIL per lesson: schema, references, MDX → KaTeX; each error and warning with its rule, the info counted by code',
123
+ flags: [{ flag: '--changed', description: 'the lessons whose .mdx or .checks.yml differs from git HEAD, tracked or not' }],
124
+ writes: READ_ONLY,
125
+ exit: 'when a lesson has an error',
126
+ json: 'CheckReport',
127
+ },
128
+ {
129
+ verb: 'rule',
130
+ args: '<ID>',
131
+ when: 'one rule, whole',
132
+ prints: "the entry as this course has it (enforcement, codes, settings in effect) and the rule page's section, example included",
133
+ flags: [],
134
+ writes: READ_ONLY,
135
+ exit: 'for an ID the catalog and the course rules do not know',
136
+ json: 'RuleReport',
137
+ },
138
+ {
139
+ verb: 'migrate',
140
+ args: '[--from <version>] [--to <version>]',
141
+ when: 'what an upgrade needs',
142
+ prints: "the migration guide's sections between the version package.json pins at git HEAD and the one installed, oldest first",
143
+ flags: [
144
+ { flag: '--from <version>', description: 'start here instead of the version pinned at HEAD' },
145
+ { flag: '--to <version>', description: 'stop here instead of the installed version' },
146
+ ],
147
+ writes: READ_ONLY,
148
+ exit: 'when a section between the two versions is missing',
149
+ json: 'MigrateReport',
150
+ },
151
+ {
152
+ verb: 'rules',
153
+ args: '[--element <e>[,<e>]] [--enforcement script|partial|guide|locked] [--edition chapter|deck]',
154
+ when: 'which rules apply',
155
+ prints: 'the rules this course has on, by element, with enforcement, codes, settings in effect and a one-line summary',
156
+ flags: [
157
+ { flag: '--element <e>[,<e>]', description: 'only rules about these elements (prose, table, notation, …)' },
158
+ { flag: '--enforcement <kind>', description: 'only script, partial, guide or locked rules' },
159
+ { flag: '--edition <edition>', description: "chapter (the default) or deck: the rules for that edition's pages" },
160
+ ],
161
+ writes: READ_ONLY,
162
+ json: 'RulesJson',
163
+ },
164
+ {
165
+ verb: 'move',
166
+ args: '<lesson>:<key> [--to <key>] [--write]',
167
+ when: 'a local entry becomes shared',
168
+ prints: 'the new card and every copy and reference that changes, as a diff',
169
+ flags: [{ flag: '--to <key>', description: 'the shared card\'s key, when it differs from the local one' }, WRITE],
170
+ writes: 'only with --write',
171
+ json: 'EditPlanJson',
172
+ },
173
+ {
174
+ verb: 'rename',
175
+ args: '<old> <new> [--write]',
176
+ when: 'rename a shared card',
177
+ prints: 'the card file and every reference, as a diff',
178
+ flags: [WRITE],
179
+ writes: 'only with --write',
180
+ json: 'EditPlanJson',
181
+ },
182
+ {
183
+ verb: 'fix',
184
+ args: '<lesson>… | --all [--write]',
185
+ when: 'the mechanical repairs',
186
+ prints: 'redundant \\explain wraps unwrapped, as a diff',
187
+ flags: [{ flag: '--all', description: 'every lesson; the ones with nothing to fix are counted' }, WRITE],
188
+ writes: 'only with --write',
189
+ json: 'EditPlanJson',
190
+ },
191
+ {
192
+ verb: 'new lesson',
193
+ args: '<id>',
194
+ when: 'a chapter to start from',
195
+ prints: 'the files it created: the .mdx and its .checks.yml',
196
+ flags: [],
197
+ writes: 'new files; never overwrites',
198
+ json: 'NewJson',
199
+ },
200
+ {
201
+ verb: 'new deck',
202
+ args: '<lesson | part>',
203
+ when: 'slides to start from',
204
+ prints: "the deck it drafted from the chapter (or the part's chapters)",
205
+ flags: [],
206
+ writes: 'new files; never overwrites',
207
+ json: 'NewJson',
208
+ },
209
+ {
210
+ verb: 'new term',
211
+ args: '<key>',
212
+ when: 'a shared notation card to start from',
213
+ prints: 'the card file it created',
214
+ flags: [],
215
+ writes: 'new files; never overwrites',
216
+ json: 'NewJson',
217
+ },
218
+ {
219
+ verb: 'init-agent',
220
+ args: '[--write]',
221
+ when: 'install the agent procedures',
222
+ prints:
223
+ "the skills and the rules-reviewer agent it would copy into .claude/, and a baseline AGENTS.md when the course has none; rerun after an upgrade",
224
+ flags: [WRITE],
225
+ writes: 'only with --write',
226
+ json: 'EditPlanJson',
227
+ },
228
+ {
229
+ verb: 'release',
230
+ args: '[major|minor|patch] [--write]',
231
+ when: 'cut a content release (rule C3)',
232
+ prints:
233
+ "## [Unreleased] in content/CHANGELOG.md becoming a dated ## [x.y.z] heading and about.version becoming x.y.z, as a diff; the bump defaults to what the section's headings imply (Breaking or Removed → major, Added → minor, else patch)",
234
+ flags: [WRITE],
235
+ writes: 'only with --write',
236
+ json: 'EditPlanJson',
237
+ },
238
+ {
239
+ verb: 'verify',
240
+ args: '[--e2e] [--force]',
241
+ when: 'is it ready to hand back',
242
+ prints:
243
+ "one line per step — validate:content, check --changed, astro check, the course's tests, astro build, and with --e2e the browser tests — and the tail of each failed step's log; a rerun on an unchanged tree answers from the last pass",
244
+ flags: [
245
+ { flag: '--e2e', description: "also run the browser tests (the course's test:e2e script, or playwright test when it has a config)" },
246
+ { flag: '--force', description: 'run even when nothing changed since the last pass' },
247
+ ],
248
+ writes: 'what validate:content and astro build write; logs and the pass record under node_modules/.cache/explico/verify/',
249
+ exit: 'when a step fails',
250
+ json: 'VerifyReport',
251
+ },
252
+ {
253
+ verb: 'help',
254
+ args: '[<verb>]',
255
+ when: "a verb's flags and its --json shape",
256
+ prints: "the usage; with a verb, its flags, what it writes, its exit code and the shape of its --json",
257
+ flags: [],
258
+ writes: READ_ONLY,
259
+ exit: 'for an unknown verb',
260
+ json: 'HelpJson',
261
+ },
262
+ ];
263
+
264
+ /** One usage line: `pnpm content symbol <latex | key> [--json]`. */
265
+ export function usageLine(verb: ContentVerb): string {
266
+ return `pnpm content ${verb.verb}${verb.args === '' ? '' : ` ${verb.args}`} [--json]`;
267
+ }
268
+
269
+ /** The usage text `content help` prints; `scripts/agent/gen-map.mjs` lists the verbs from the generated reference. */
270
+ export const CONTENT_USAGE = `Usage:\n${CONTENT_VERBS.map((verb) => ` ${usageLine(verb)}`).join('\n')}`;