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
@@ -0,0 +1,469 @@
1
+ /**
2
+ * `pnpm content <command>` — the AI-authoring surface, whole.
3
+ *
4
+ * The engine owns the dispatch, the usage text, the flag parsing, the text
5
+ * rendering and the exit codes; a course's `scripts/content.ts` is three
6
+ * lines that hand it the course config:
7
+ *
8
+ * import { runContentCli } from 'explico/compiler/cli-main';
9
+ * import { courseConfig } from '../content/course.config';
10
+ * process.exitCode = await runContentCli(courseConfig);
11
+ *
12
+ * so every verb reaches every course with the package and no course forks
13
+ * the router. Each verb's logic lives in `./cli`; this module only parses
14
+ * argv, prints (`--json` = data only, no stack traces), and returns the exit
15
+ * code. Everything except `new` is read-only; `move`, `rename` and `fix`
16
+ * print a diff and change nothing without `--write`.
17
+ */
18
+ import { mkdirSync, writeFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+
21
+ import {
22
+ ContentCliError,
23
+ applyEditPlan,
24
+ changedLessonIds,
25
+ contentBindings,
26
+ contentCheck,
27
+ contentContext,
28
+ contentDiagnostics,
29
+ contentEquation,
30
+ contentMigrate,
31
+ contentRule,
32
+ contentRules,
33
+ contentStatus,
34
+ contentSymbol,
35
+ contentWarnings,
36
+ filterDiagnostics,
37
+ findLesson,
38
+ formatBindingsReport,
39
+ formatCliDiagnostic,
40
+ formatDiagnosticsSummary,
41
+ formatEquationReport,
42
+ formatMigrateReport,
43
+ formatRuleReport,
44
+ formatSymbolReport,
45
+ ownerTag,
46
+ parseRulesFilter,
47
+ planFix,
48
+ planInitAgent,
49
+ planMove,
50
+ planRename,
51
+ renderEditPlan,
52
+ scaffoldDeck,
53
+ scaffoldLesson,
54
+ scaffoldTerm,
55
+ summarizeDiagnostics,
56
+ type Scaffold,
57
+ } from './cli';
58
+ import type { CourseConfig } from './course-config';
59
+ import { compileManifest } from './manifest';
60
+
61
+ /** The usage text `content help` prints; `scripts/agent/gen-map.mjs` lists the verbs from it. */
62
+ export const CONTENT_USAGE = `Usage:
63
+ pnpm content status
64
+ pnpm content diagnostics [info|warning|error] [--all | --code <c> | --lesson <id> | --changed] [--json]
65
+ pnpm content warnings [--json]
66
+ pnpm content context <lesson> [--json]
67
+ pnpm content equation "<latex>" [--lesson <id>] [--json]
68
+ pnpm content bindings <lesson> [--json]
69
+ pnpm content symbol <latex | key> [--json]
70
+ pnpm content check <lesson | deck>… [--changed] [--json]
71
+ pnpm content rule <ID> [--json]
72
+ pnpm content migrate [--from <version>] [--to <version>] [--json]
73
+ pnpm content rules [--element <e>[,<e>]] [--enforcement script|partial|guide|locked] [--edition chapter|deck] [--json]
74
+ pnpm content move <lesson>:<key> [--to <key>] [--write]
75
+ pnpm content rename <old> <new> [--write]
76
+ pnpm content fix <lesson> [--write]
77
+ pnpm content new lesson <id>
78
+ pnpm content new deck <lesson | part>
79
+ pnpm content new term <key>
80
+ pnpm content init-agent [--write]`;
81
+
82
+ export interface ContentArgs {
83
+ readonly command: string;
84
+ readonly positionals: readonly string[];
85
+ readonly json: boolean;
86
+ readonly changed: boolean;
87
+ /** `--element a,b` — a value-taking flag; also `--element=a,b`. */
88
+ readonly element?: string;
89
+ /** `--enforcement script` — a value-taking flag; also `--enforcement=script`. */
90
+ readonly enforcement?: string;
91
+ /** `--edition deck` — a value-taking flag; also `--edition=deck`. */
92
+ readonly edition?: string;
93
+ /** `--all`: list every diagnostic instead of the counts. */
94
+ readonly all: boolean;
95
+ /** `--code <code>`: only that diagnostic code. */
96
+ readonly code?: string;
97
+ /** `--lesson <id>`: only that lesson's diagnostics; the scope for `equation`. */
98
+ readonly lesson?: string;
99
+ /** `--from <version>` / `--to <version>` for `migrate`; `--to <key>` for `move`. */
100
+ readonly from?: string;
101
+ readonly to?: string;
102
+ /** `--write`: apply a planned edit instead of only showing its diff. */
103
+ readonly write: boolean;
104
+ }
105
+
106
+ const VALUED_FLAGS = ['element', 'enforcement', 'edition', 'code', 'lesson', 'from', 'to'] as const;
107
+
108
+ export function parseContentArgs(argv: readonly string[]): ContentArgs {
109
+ const positionals: string[] = [];
110
+ const values: Partial<Record<(typeof VALUED_FLAGS)[number], string>> = {};
111
+ let json = false;
112
+ let changed = false;
113
+ let all = false;
114
+ let write = false;
115
+ const valued = new RegExp(`^--(${VALUED_FLAGS.join('|')})(?:=(.*))?$`);
116
+ for (let i = 0; i < argv.length; i += 1) {
117
+ const arg = argv[i]!;
118
+ const match = valued.exec(arg);
119
+ if (arg === '--json') json = true;
120
+ else if (arg === '--changed') changed = true;
121
+ else if (arg === '--all') all = true;
122
+ else if (arg === '--write') write = true;
123
+ else if (match) values[match[1] as (typeof VALUED_FLAGS)[number]] = match[2] ?? argv[(i += 1)] ?? '';
124
+ else positionals.push(arg);
125
+ }
126
+ return {
127
+ command: positionals[0] ?? '',
128
+ positionals: positionals.slice(1),
129
+ json,
130
+ changed,
131
+ all,
132
+ write,
133
+ ...values,
134
+ };
135
+ }
136
+
137
+ /** Where a run prints; the process streams by default, a buffer in a test. */
138
+ export interface ContentIo {
139
+ readonly out: (text: string) => void;
140
+ readonly err: (text: string) => void;
141
+ }
142
+
143
+ const PROCESS_IO: ContentIo = {
144
+ out: (text) => process.stdout.write(text),
145
+ err: (text) => process.stderr.write(text),
146
+ };
147
+
148
+ /** A usage or input mistake, reported like a `ContentCliError` and ending the run with 1. */
149
+ class Usage extends Error {
150
+ constructor(
151
+ readonly code: string,
152
+ message: string,
153
+ ) {
154
+ super(message);
155
+ }
156
+ }
157
+
158
+ function usage(code: string, message: string): never {
159
+ throw new Usage(code, message);
160
+ }
161
+
162
+ function writeScaffold(scaffold: Scaffold): string[] {
163
+ const written: string[] = [];
164
+ for (const file of [{ path: scaffold.path, contents: scaffold.contents }, ...scaffold.companions]) {
165
+ const absolute = join(process.cwd(), file.path);
166
+ mkdirSync(dirname(absolute), { recursive: true });
167
+ writeFileSync(absolute, file.contents, { flag: 'wx' });
168
+ written.push(file.path);
169
+ }
170
+ return written;
171
+ }
172
+
173
+ /**
174
+ * Run one `content` command for this course and return its exit code: 0 on
175
+ * success, 1 on a usage error, a `ContentCliError`, a check that failed, or
176
+ * an unexpected throw (reported by message, never a stack).
177
+ */
178
+ export async function runContentCli(
179
+ courseConfig: CourseConfig,
180
+ argv: readonly string[] = process.argv.slice(2),
181
+ io: ContentIo = PROCESS_IO,
182
+ ): Promise<number> {
183
+ const args = parseContentArgs(argv);
184
+ const json = (value: unknown) => io.out(`${JSON.stringify(value, null, 2)}\n`);
185
+ try {
186
+ return await dispatch(courseConfig, args, io, json);
187
+ } catch (error) {
188
+ const code =
189
+ error instanceof Usage || error instanceof ContentCliError ? error.code : 'internal';
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ if (args.json) json({ ok: false, error: { code, message } });
192
+ else io.err(`Error [${code}]: ${message}\n`);
193
+ return 1;
194
+ }
195
+ }
196
+
197
+ async function dispatch(
198
+ courseConfig: CourseConfig,
199
+ args: ContentArgs,
200
+ io: ContentIo,
201
+ json: (value: unknown) => void,
202
+ ): Promise<number> {
203
+ const out = io.out;
204
+ if (args.command === '' || args.command === 'help' || args.command === '--help') {
205
+ out(`${CONTENT_USAGE}\n`);
206
+ return 0;
207
+ }
208
+
209
+ if (args.command === 'new') {
210
+ const [kind, id] = args.positionals;
211
+ if (kind !== 'lesson' && kind !== 'deck' && kind !== 'term') {
212
+ usage('unknown-kind', 'content new expects "lesson", "deck" or "term".');
213
+ }
214
+ if (!id) usage('usage', `content new ${kind} needs an id.`);
215
+ if (args.positionals.length > 2) {
216
+ usage('usage', `content new ${kind} takes one id; got also ${args.positionals.slice(2).join(' ')}.`);
217
+ }
218
+ const scaffold = kind === 'lesson' ? scaffoldLesson(id) : kind === 'deck' ? scaffoldDeck(id) : scaffoldTerm(id);
219
+ const written = writeScaffold(scaffold);
220
+ if (args.json) json({ ok: true, created: written });
221
+ else out(`Created:\n${written.map((p) => ` ${p}`).join('\n')}\n`);
222
+ return 0;
223
+ }
224
+
225
+ // An argument a command does not take is a mistake, never silently dropped:
226
+ // `content status extra` would otherwise look like it did something with it.
227
+ if (['status', 'rules', 'warnings'].includes(args.command) && args.positionals.length > 0) {
228
+ usage('usage', `content ${args.command} takes no <lesson>; got ${args.positionals.join(' ')}.`);
229
+ }
230
+
231
+ // `init-agent` copies the engine's procedures into the course: no manifest.
232
+ if (args.command === 'init-agent') {
233
+ if (args.positionals.length > 0) usage('usage', `content init-agent takes no argument; got ${args.positionals.join(' ')}.`);
234
+ const plan = planInitAgent();
235
+ if (args.json) json({ ...plan, written: args.write });
236
+ else out(renderEditPlan(plan));
237
+ if (plan.edits.length === 0) return 0;
238
+ if (args.write) {
239
+ const touched = applyEditPlan(plan);
240
+ if (!args.json) out(`\nwritten: ${touched.join(', ')}\n`);
241
+ } else if (!args.json) out('\ndry run: nothing written. Add --write to install.\n');
242
+ return 0;
243
+ }
244
+
245
+ // `migrate` reads the guide beside the engine, not the course: no manifest.
246
+ if (args.command === 'migrate') {
247
+ if (args.positionals.length > 0) {
248
+ usage('usage', `content migrate takes no positional argument; got ${args.positionals.join(' ')}. Use --from and --to.`);
249
+ }
250
+ const report = contentMigrate({
251
+ ...(args.from === undefined ? {} : { from: args.from }),
252
+ ...(args.to === undefined ? {} : { to: args.to }),
253
+ });
254
+ if (args.json) json(report);
255
+ else out(formatMigrateReport(report));
256
+ return 0;
257
+ }
258
+
259
+ const known = [
260
+ 'status', 'move', 'rename', 'fix', 'rule', 'rules', 'warnings', 'diagnostics',
261
+ 'context', 'equation', 'bindings', 'symbol', 'check',
262
+ ];
263
+ if (!known.includes(args.command)) {
264
+ usage('unknown-command', `Unknown command "${args.command}".\n${CONTENT_USAGE}`);
265
+ }
266
+
267
+ const manifest = await compileManifest(courseConfig);
268
+
269
+ switch (args.command) {
270
+ case 'status': {
271
+ const report = contentStatus(manifest);
272
+ if (args.json) {
273
+ json(report);
274
+ return 0;
275
+ }
276
+ const c = report.counts;
277
+ const unreleased = report.pageVersions.unreleased;
278
+ out(
279
+ `Knowledge version ${report.knowledgeVersion}\n` +
280
+ `Page versions: ${report.pageVersions.tracked} page(s) tracked, ` +
281
+ `${unreleased.length} with changes not yet released` +
282
+ `${unreleased.length > 0 ? ` (${unreleased.join(', ')})` : ''}\n` +
283
+ `Lessons ${c.lessons} · competencies ${c.competencies} · assessments ${c.assessments} · ` +
284
+ `sources ${c.sources} · tracks ${c.tracks} · notation ${c.notationDefinitions} · ` +
285
+ `keyed equations ${c.keyedEquations} · tables ${c.keyedTables} · ` +
286
+ `figures ${c.keyedFigures} · diagrams ${c.keyedDiagrams} · example sets ${c.keyedExamples}\n` +
287
+ `Diagnostics: ${report.diagnostics.errors} error(s), ${report.diagnostics.warnings} warning(s), ${report.diagnostics.infos} info\n` +
288
+ `Orphans: ${report.orphans.length}\n` +
289
+ `Agent surfaces (estimated tokens): ${report.agentSurfaces
290
+ .map((s) => `${s.name} ${(s.tokens / 1000).toFixed(1)}k`)
291
+ .join(' · ')}\n` +
292
+ `Agent guide: ${
293
+ report.agentGuide.agentsFile === null
294
+ ? 'no AGENTS.md — `pnpm content init-agent --write` installs the baseline'
295
+ : report.agentGuide.linksGuide
296
+ ? `${report.agentGuide.agentsFile} points at the engine's guide`
297
+ : `${report.agentGuide.agentsFile} does not point at node_modules/explico/docs/authoring/agents.md — add that line`
298
+ }; procedures ${report.agentGuide.skillsInstalled ? 'installed' : 'not installed (`pnpm content init-agent --write`)'}\n`,
299
+ );
300
+ return 0;
301
+ }
302
+
303
+ case 'move':
304
+ case 'rename':
305
+ case 'fix': {
306
+ let plan;
307
+ if (args.command === 'move') {
308
+ const [target, ...extra] = args.positionals;
309
+ const [lesson, key] = (target ?? '').split(':');
310
+ if (!lesson || !key || extra.length > 0) usage('usage', 'content move needs <lesson>:<key> and optionally --to <key>.');
311
+ plan = planMove(manifest, lesson, key, args.to === undefined ? {} : { to: args.to });
312
+ } else if (args.command === 'rename') {
313
+ const [oldKey, newKey, ...extra] = args.positionals;
314
+ if (!oldKey || !newKey || extra.length > 0) usage('usage', 'content rename needs <old> <new>.');
315
+ plan = planRename(manifest, oldKey, newKey);
316
+ } else {
317
+ const [lesson, ...extra] = args.positionals;
318
+ if (!lesson || extra.length > 0) usage('usage', 'content fix needs one <lesson> id.');
319
+ plan = planFix(manifest, lesson);
320
+ }
321
+ if (args.json) json({ ...plan, written: args.write });
322
+ else out(renderEditPlan(plan));
323
+ if (plan.edits.length === 0) return 0;
324
+ if (args.write) {
325
+ const touched = applyEditPlan(plan);
326
+ if (!args.json) out(`\nwritten: ${touched.join(', ')}\nnext: ${plan.next.join(' · ')}\n`);
327
+ } else if (!args.json) {
328
+ out(`\ndry run: nothing written. Add --write to apply; then ${plan.next.join(' · ')}.\n`);
329
+ }
330
+ return 0;
331
+ }
332
+
333
+ case 'rule': {
334
+ const [id, ...extra] = args.positionals;
335
+ if (!id) usage('usage', 'content rule needs a rule ID: N4, V8, X1, or an invariant such as keyed-floats.');
336
+ if (extra.length > 0) usage('usage', `content rule takes one ID; got also ${extra.join(' ')}.`);
337
+ const report = contentRule(manifest, id);
338
+ if (args.json) json(report);
339
+ else out(formatRuleReport(report));
340
+ return report.status === 'unknown' ? 1 : 0;
341
+ }
342
+
343
+ case 'rules': {
344
+ const filter = parseRulesFilter({
345
+ ...(args.element === undefined ? {} : { element: args.element }),
346
+ ...(args.enforcement === undefined ? {} : { enforcement: args.enforcement }),
347
+ ...(args.edition === undefined ? {} : { edition: args.edition }),
348
+ });
349
+ const report = contentRules(manifest, filter);
350
+ if (args.json) json({ filter: report.filter, count: report.count, rules: report.rules });
351
+ else out(report.markdown);
352
+ return 0;
353
+ }
354
+
355
+ case 'warnings': {
356
+ const report = contentWarnings(manifest);
357
+ if (args.json) json(report);
358
+ else {
359
+ for (const warning of report.warnings) out(`${formatCliDiagnostic(warning)}\n`);
360
+ out(`${report.count} warning(s)\n`);
361
+ }
362
+ return 0;
363
+ }
364
+
365
+ case 'diagnostics': {
366
+ const [requested = 'info', ...extra] = args.positionals;
367
+ if (extra.length > 0 || (requested !== 'info' && requested !== 'warning' && requested !== 'error')) {
368
+ usage('usage', 'content diagnostics expects one of: info, warning, error.');
369
+ }
370
+ // The counts are the default; a filter or --all lists the lines.
371
+ let report = contentDiagnostics(manifest, requested);
372
+ let narrowed = false;
373
+ if (args.code !== undefined) {
374
+ report = filterDiagnostics(report, { code: args.code });
375
+ narrowed = true;
376
+ }
377
+ if (args.lesson !== undefined) {
378
+ report = filterDiagnostics(report, { files: [findLesson(manifest, args.lesson).file] });
379
+ narrowed = true;
380
+ }
381
+ if (args.changed) {
382
+ const ids = new Set(changedLessonIds());
383
+ const files = manifest.lessons.filter((l) => ids.has(l.id)).map((l) => l.file);
384
+ report = filterDiagnostics(report, { files });
385
+ narrowed = true;
386
+ }
387
+ if (args.json) json(report);
388
+ else if (args.all || narrowed) {
389
+ for (const diagnostic of report.diagnostics) out(`${formatCliDiagnostic(diagnostic)}\n`);
390
+ out(`${report.count} diagnostic(s) at ${report.minimumSeverity} or higher\n`);
391
+ } else out(formatDiagnosticsSummary(summarizeDiagnostics(report)));
392
+ return 0;
393
+ }
394
+
395
+ case 'context': {
396
+ const [lesson, ...extra] = args.positionals;
397
+ if (!lesson) usage('usage', 'content context needs a <lesson> id.');
398
+ if (extra.length > 0) usage('usage', `content context takes one <lesson> id; got also ${extra.join(' ')}.`);
399
+ const report = contentContext(manifest, lesson);
400
+ if (args.json) {
401
+ json(report);
402
+ return 0;
403
+ }
404
+ const t = report.tokens;
405
+ out(
406
+ `${report.lesson.id} — ${report.lesson.title}\n` +
407
+ `teaches: ${report.lesson.teaches.join(', ') || '(none)'}\n` +
408
+ `requires: ${report.lesson.requires.join(', ') || '(none)'}\n` +
409
+ `notation: ${report.notation.map((n) => n.key).join(', ') || '(none)'}\n` +
410
+ `sources: ${report.sources.map((s) => s.id).join(', ') || '(none)'}\n` +
411
+ `diagnostics: ${report.diagnostics.length}\n` +
412
+ `tokens: context ${t.context} vs doc set ${t.docSet} + corpus ${t.corpus} ` +
413
+ `(${(t.ratio * 100).toFixed(2)}% of the baseline)\n`,
414
+ );
415
+ return 0;
416
+ }
417
+
418
+ case 'equation': {
419
+ const [latex, ...extra] = args.positionals;
420
+ if (!latex) usage('usage', "content equation needs one quoted LaTeX string: content equation 'v_0 \\sin(\\theta)' --lesson <id>.");
421
+ if (extra.length > 0) usage('usage', `content equation takes one string; got also ${extra.join(' ')}. Quote the whole expression.`);
422
+ const report = contentEquation(manifest, latex, args.lesson === undefined ? {} : { lesson: args.lesson });
423
+ if (args.json) json(report);
424
+ else out(formatEquationReport(report));
425
+ return report.ok ? 0 : 1;
426
+ }
427
+
428
+ case 'bindings': {
429
+ const [lesson, ...extra] = args.positionals;
430
+ if (!lesson) usage('usage', 'content bindings needs a <lesson> id.');
431
+ if (extra.length > 0) usage('usage', `content bindings takes one <lesson> id; got also ${extra.join(' ')}.`);
432
+ const report = contentBindings(manifest, lesson);
433
+ if (args.json) json(report);
434
+ else out(formatBindingsReport(report));
435
+ return 0;
436
+ }
437
+
438
+ case 'symbol': {
439
+ const [query, ...extra] = args.positionals;
440
+ if (!query) usage('usage', "content symbol needs a <latex | key>; quote a backslash: '\\theta'.");
441
+ if (extra.length > 0) {
442
+ usage('usage', `content symbol takes one <latex | key>; got also ${extra.join(' ')}. Quote a symbol with spaces: 'v(t)'.`);
443
+ }
444
+ const report = contentSymbol(manifest, query);
445
+ if (args.json) json(report);
446
+ else out(formatSymbolReport(report));
447
+ return 0;
448
+ }
449
+
450
+ case 'check': {
451
+ // Every lesson named is checked, not only the first.
452
+ const report = await contentCheck(manifest, { lessons: args.positionals, changed: args.changed });
453
+ if (args.json) json(report);
454
+ else {
455
+ for (const entry of report.lessons) {
456
+ out(`${entry.ok ? 'ok ' : 'FAIL'} ${entry.lesson}${entry.rendered ? '' : ' (render failed)'}\n`);
457
+ for (const diagnostic of entry.diagnostics) {
458
+ out(` [${diagnostic.severity}] ${diagnostic.code}${ownerTag(diagnostic)}: ${diagnostic.message}\n`);
459
+ }
460
+ }
461
+ if (report.lessons.length === 0) out('No changed lessons.\n');
462
+ }
463
+ return report.ok ? 0 : 1;
464
+ }
465
+
466
+ default:
467
+ return usage('unknown-command', `Unknown command "${args.command}".\n${CONTENT_USAGE}`);
468
+ }
469
+ }