explico 0.14.0 → 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 (52) hide show
  1. package/CHANGELOG.md +112 -1
  2. package/dist/compiler/cli-main.d.ts +61 -4
  3. package/dist/compiler/cli-main.js +77 -36
  4. package/dist/compiler/cli-reference.d.ts +50 -0
  5. package/dist/compiler/cli-reference.js +213 -0
  6. package/dist/compiler/cli-verbs.d.ts +45 -0
  7. package/dist/compiler/cli-verbs.js +224 -0
  8. package/dist/compiler/cli-verify.d.ts +51 -0
  9. package/dist/compiler/cli-verify.js +151 -0
  10. package/dist/compiler/cli.d.ts +19 -50
  11. package/dist/compiler/cli.js +45 -6
  12. package/dist/compiler/content-release.d.ts +49 -0
  13. package/dist/compiler/content-release.js +63 -0
  14. package/dist/compiler/content-schemas.d.ts +2 -2
  15. package/dist/compiler/manifest.js +29 -24
  16. package/dist/compiler/page-versions.d.ts +25 -1
  17. package/dist/compiler/page-versions.js +19 -0
  18. package/dist/compiler/validate-content.js +2 -2
  19. package/dist/reference/consistency.js +7 -1
  20. package/dist/reference/diagram-fit.d.ts +5 -1
  21. package/dist/reference/diagram-fit.js +5 -1
  22. package/dist/rules/catalog.js +16 -0
  23. package/dist/rules/in-effect.d.ts +1 -1
  24. package/dist/rules/in-effect.js +5 -4
  25. package/dist/rules/rules.schema.json +21 -0
  26. package/docs/authoring/README.md +2 -0
  27. package/docs/authoring/agents.md +18 -11
  28. package/docs/authoring/cli-reference.json +2172 -0
  29. package/docs/authoring/cli.md +887 -0
  30. package/docs/authoring/deck.md +24 -0
  31. package/docs/authoring/migrating.md +168 -18
  32. package/docs/authoring/versioning.md +2 -1
  33. package/docs/course-agent/AGENTS.md +10 -7
  34. package/docs/course-agent/agents/rules-reviewer.md +2 -2
  35. package/docs/course-agent/skills/draft-lesson/SKILL.md +7 -6
  36. package/docs/course-agent/skills/migrate-course/SKILL.md +5 -4
  37. package/docs/course-agent/skills/release-content/SKILL.md +5 -3
  38. package/docs/diagram-scaling.md +2 -2
  39. package/package.json +1 -1
  40. package/src/compiler/cli-main.ts +135 -47
  41. package/src/compiler/cli-reference.ts +285 -0
  42. package/src/compiler/cli-verbs.ts +270 -0
  43. package/src/compiler/cli-verify.ts +241 -0
  44. package/src/compiler/cli.ts +77 -17
  45. package/src/compiler/content-release.ts +91 -0
  46. package/src/compiler/manifest.ts +34 -24
  47. package/src/compiler/page-versions.ts +42 -0
  48. package/src/compiler/validate-content.ts +5 -2
  49. package/src/reference/consistency.ts +13 -2
  50. package/src/reference/diagram-fit.ts +18 -4
  51. package/src/rules/catalog.ts +17 -0
  52. package/src/rules/in-effect.ts +5 -4
package/CHANGELOG.md CHANGED
@@ -14,6 +14,116 @@ under **Breaking** below; a PATCH never breaks.
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.15.0] - 2026-10-12
18
+
19
+ ### Added
20
+
21
+ - **Rule D11: a deck is flagged when its chapter changed after it did.**
22
+ `deck-stale`, an info diagnostic, names a lesson's own deck whose chapter's
23
+ rule C5 version is newer than the deck's — two files mean the chapter and
24
+ the deck can drift apart with nothing else to notice it. Scoped to a
25
+ lesson's own deck; a part's deck covers more than one chapter, so it has
26
+ none to compare against and is never flagged. `page-versions.ts` gains
27
+ `deckStaleDiagnostics`; the diagnostic joins `diagnostics.math` beside
28
+ D1–D10's frame codes. No manifest shape change.
29
+ - **`content help <verb> [--json]`: each verb's contract, machine-readable.**
30
+ `content help symbol` prints the verb's usage, what it is for, its flags,
31
+ what it writes, when it exits 1, and the shape of its `--json` as a tree of
32
+ named types; `--json` prints the same with a self-contained JSON Schema
33
+ (draft 2020-12). `content help --json` prints the whole reference. The verb
34
+ table is one source, `src/compiler/cli-verbs.ts` (`CONTENT_VERBS`, from which
35
+ `CONTENT_USAGE` is now derived, every line ending in `[--json]`);
36
+ `scripts/agent/gen-cli.mjs` reads the TypeScript report types with the
37
+ compiler API and writes `docs/authoring/cli-reference.json` and
38
+ `docs/authoring/cli.md`, both shipped, `--check` in verify and CI. New
39
+ module `explico/compiler/cli-reference` (`loadCliReference`, `verbHelp`,
40
+ `schemaOf`, `formatVerbHelp`, `cliReferenceMarkdown`, and
41
+ `jsonShapeErrors`, which checks a value against one of the schemas).
42
+ `cli-main` exports the named `--json` types its verbs print beside the
43
+ reports: `RulesJson`, `EditPlanJson`, `NewJson`, `ErrorJson`.
44
+ - **`content verify [--e2e] [--force]`: a course's one gate before handing
45
+ work back.** `validate:content` → `content check --changed` → `astro check`
46
+ (skipped, by name, when `@astrojs/check` is not installed) → the course's
47
+ `test` script → `astro build`, and with `--e2e` its `test:e2e` script (or
48
+ `playwright test`). Each step is its own process with its whole output in
49
+ `node_modules/.cache/explico/verify/<step>.log`; the run prints one line per
50
+ step, the tail of each failed step's log, and exits 1 on a failure. A pass
51
+ records the tree it left (git HEAD, the diff against it, the untracked
52
+ files), and a rerun on the same tree that asks for no more answers from the
53
+ record without running anything. New module `explico/compiler/cli-verify`
54
+ (`runVerify`, `VerifyReport`).
55
+ - `runContentCli` takes an optional fifth argument, a manifest already
56
+ compiled for the course, so a test running many verbs compiles once.
57
+ - **`content release [major|minor|patch] [--write]`: a content release in
58
+ every course.** `## [Unreleased]` in `content/CHANGELOG.md` becomes a dated
59
+ `## [x.y.z]` heading and `about.version` becomes `x.y.z` (rule C3), shown as
60
+ a diff and written with `--write`, like `move`, `rename` and `fix`; the bump
61
+ defaults to what the section's headings imply (rule C2), and the date is the
62
+ local one. It was the playground's own `scripts/release-content.ts`, which
63
+ the shipped `release-content` skill called as `pnpm release:content` in
64
+ courses that never had it. The playground's `release:content` script is now
65
+ `content release --write`. New module `explico/compiler/content-release`
66
+ (`releaseEdit`, `planContentRelease`); new error code `no-changelog`.
67
+
68
+ ### Changed
69
+
70
+ - `content init-agent` says `new` or `differs from the engine's copy` for each
71
+ file, where a dry run used to say `installed`; after `--write` every edit
72
+ verb prints what to run next.
73
+ - `content status` lists at most six of the pages with unreleased changes and
74
+ counts the rest; `content context` says how many of a lesson's diagnostics
75
+ sit in its own file and how many in the shared cards it shows (the first is
76
+ what `diagnostics --lesson` lists).
77
+ - The shipped procedures (`docs/course-agent/`) survive a course's prettier:
78
+ no inline code span crosses a line break, which prettier re-indented out of
79
+ its list item. `gen-map --check` fails on one.
80
+ - `migrating.md`, 0.13.1 → 0.14.0, corrected where upgrading
81
+ credit-and-derivatives found it wrong: the patch goes before `pnpm add`
82
+ (pnpm refuses with `ERR_PNPM_UNUSED_PATCH`) and its entry may sit in
83
+ `pnpm-workspace.yaml`; pnpm 11's `minimumReleaseAgeExclude`; a course that
84
+ commits `build/resolution/` fails its first run after the upgrade; stale
85
+ `lint-ignore.yml` lines and a course's own tier guidance (step 20); the 404
86
+ page without a raw `<div>` (step 24); tests comparing sidebar and page
87
+ colours (step 25). Upgrading time-series added two more: pnpm can instead
88
+ stop with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` (step 1), and the first
89
+ `content/versions.json` goes in before the upgrade's own changelog bullet,
90
+ or every page reads "not yet released" (step 10).
91
+ - `content fix` takes several lessons, or `--all`, as one plan: the lessons
92
+ with nothing to fix are counted, not listed. A course upgrading to 0.14.0
93
+ can have a thousand redundant `\explain` wraps (data-science-I: 1193).
94
+ - `content check` lists a lesson's errors and warnings line by line and
95
+ counts its info by code, with the command that lists them; `--json` is
96
+ unchanged.
97
+ - `definition-repeated` on a local entry whose glyph a shared card already
98
+ owns no longer says "move one copy to `content/notation/`": promoting it
99
+ would put two shared cards on one glyph. It says the entry is a reused
100
+ letter (rule N4) and how to acknowledge the copies; `content move` prints a
101
+ caution when the glyph is taken (time-series: 9 of its 34).
102
+ - `dia-too-wide` on `<CurriculumMap>`'s competency graph names the map's own
103
+ props (`graph="per-part"`, `direction`, `warnAboveWidth`) instead of a node
104
+ `width` and a shorter label, which the author does not write there.
105
+ - The rules-in-effect page says the course's `rules` and `courseRules` live
106
+ in `course.settings.json` or `course.config.ts`, not only the first.
107
+
108
+ ### Fixed
109
+
110
+ - `content move` wrote the new card a placeholder body ("Extend this body
111
+ with what a glossary reader needs."), which the glossary would publish, and
112
+ `domain: general` for a key without a prefix. The card now has no body, its
113
+ domain is the key's prefix when the course has that domain, else the one
114
+ most of the lesson's cards have; the plan says that the body is the
115
+ author's to write, and when the entry had no `label`.
116
+ - `validate:content`'s run log recorded each consistency warning
117
+ (`definition-repeated`, `glyph-unique-in-corpus`, …) at INFO and their
118
+ one-line digest as the only WARNING, so its `Run log: (… warning(s))` count
119
+ disagreed with `content status`. Each finding is recorded at WARNING now,
120
+ the digest at INFO.
121
+ - `ContextReport`'s `equations`, `tables`, `figures` and `diagrams` are typed
122
+ `ManifestLessonFloat[]`: `content context --json` always printed each
123
+ float's `short` and `caption`, which the type left out. The playground's
124
+ new `--json` contract test runs every read-only verb against the schemas and
125
+ caught it.
126
+
17
127
  ## [0.14.0] - 2026-10-11
18
128
 
19
129
  ### Breaking
@@ -4323,7 +4433,8 @@ n starts its row, and the panel hung from the button's end,
4323
4433
  system with its completeness gate, the curriculum-graph validator, the
4324
4434
  Astro/Starlight UI, the LMS / session / analytics seams, and the `content` CLI.
4325
4435
 
4326
- [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.14.0...HEAD
4436
+ [Unreleased]: https://github.com/danieltuzes/explico/compare/v0.15.0...HEAD
4437
+ [0.15.0]: https://github.com/danieltuzes/explico/compare/v0.14.0...v0.15.0
4327
4438
  [0.14.0]: https://github.com/danieltuzes/explico/compare/v0.13.1...v0.14.0
4328
4439
  [0.13.1]: https://github.com/danieltuzes/explico/compare/v0.13.0...v0.13.1
4329
4440
  [0.13.0]: https://github.com/danieltuzes/explico/compare/v0.12.0...v0.13.0
@@ -1,9 +1,60 @@
1
+ import { EditPlan, ContentCliCode, RulesReport } from './cli.js';
2
+ export { CONTENT_USAGE } from './cli-verbs.js';
1
3
  import { C as CourseConfig } from '../course-config-BLtIdkei.js';
4
+ import { Manifest } from './manifest.js';
5
+ import '../reference/types.js';
6
+ import '../reference/gate-math.js';
7
+ import './render-diagnostics.js';
2
8
  import '../rules/catalog.js';
9
+ import '../rules/in-effect.js';
3
10
  import '../rules/course-rules.js';
11
+ import '../curriculum/validation.js';
12
+ import '../reference/consistency.js';
13
+ import './collections.js';
14
+ import '../reference/gate-retired-keys.js';
15
+ import '../reference/lint-ignore.js';
16
+ import '../reference/equations-validate.js';
17
+ import '../reference/tables-validate.js';
18
+ import '../reference/keyed-float-validate.js';
19
+ import '../reference/figures-validate.js';
20
+ import '../reference/diagrams-validate.js';
21
+ import '../reference/examples-validate.js';
22
+ import '../reference/folds-validate.js';
23
+ import '../reference/knowledge-checks-validate.js';
24
+ import '../reference/raw-floats-validate.js';
25
+ import '../reference/lab-katex-validate.js';
26
+ import '../reference/mixed-content.js';
27
+ import '../reference/gloss.js';
28
+ import '../rules/waivers.js';
29
+ import './knowledge-version.js';
30
+ import './page-versions.js';
31
+ import './decks.js';
32
+ import '../reference/decks-validate.js';
33
+ import '../reference/deck-source.js';
34
+ import './lesson-derivation.js';
4
35
 
5
- /** The usage text `content help` prints; `scripts/agent/gen-map.mjs` lists the verbs from it. */
6
- declare const CONTENT_USAGE = "Usage:\n pnpm content status\n pnpm content diagnostics [info|warning|error] [--all | --code <c> | --lesson <id> | --changed] [--json]\n pnpm content warnings [--json]\n pnpm content context <lesson> [--json]\n pnpm content equation \"<latex>\" [--lesson <id>] [--json]\n pnpm content bindings <lesson> [--json]\n pnpm content symbol <latex | key> [--json]\n pnpm content check <lesson | deck>\u2026 [--changed] [--json]\n pnpm content rule <ID> [--json]\n pnpm content migrate [--from <version>] [--to <version>] [--json]\n pnpm content rules [--element <e>[,<e>]] [--enforcement script|partial|guide|locked] [--edition chapter|deck] [--json]\n pnpm content move <lesson>:<key> [--to <key>] [--write]\n pnpm content rename <old> <new> [--write]\n pnpm content fix <lesson> [--write]\n pnpm content new lesson <id>\n pnpm content new deck <lesson | part>\n pnpm content new term <key>\n pnpm content init-agent [--write]";
36
+ /** `--json` of `rules`: the report without its Markdown page. */
37
+ type RulesJson = Pick<RulesReport, 'filter' | 'count' | 'rules'>;
38
+ /** `--json` of `move`, `rename`, `fix` and `init-agent`: the plan, and whether `--write` applied it. */
39
+ type EditPlanJson = EditPlan & {
40
+ /** True when `--write` applied the edits; false for a dry run. */
41
+ readonly written: boolean;
42
+ };
43
+ /** `--json` of `new lesson`, `new deck` and `new term`. */
44
+ interface NewJson {
45
+ readonly ok: true;
46
+ /** Course-relative paths of the files it created. */
47
+ readonly created: readonly string[];
48
+ }
49
+ /** `--json` of any verb whose run failed: a stable code and a message, never a stack. */
50
+ interface ErrorJson {
51
+ readonly ok: false;
52
+ readonly error: {
53
+ /** `internal` for an unexpected throw; every other code is stable. */
54
+ readonly code: ContentCliCode | 'internal';
55
+ readonly message: string;
56
+ };
57
+ }
7
58
  interface ContentArgs {
8
59
  readonly command: string;
9
60
  readonly positionals: readonly string[];
@@ -26,6 +77,10 @@ interface ContentArgs {
26
77
  readonly to?: string;
27
78
  /** `--write`: apply a planned edit instead of only showing its diff. */
28
79
  readonly write: boolean;
80
+ /** `--e2e`: `verify` runs the browser tests too. */
81
+ readonly e2e: boolean;
82
+ /** `--force`: `verify` runs even on a tree its last pass covered. */
83
+ readonly force: boolean;
29
84
  }
30
85
  declare function parseContentArgs(argv: readonly string[]): ContentArgs;
31
86
  /** Where a run prints; the process streams by default, a buffer in a test. */
@@ -38,6 +93,8 @@ interface ContentIo {
38
93
  * success, 1 on a usage error, a `ContentCliError`, a check that failed, or
39
94
  * an unexpected throw (reported by message, never a stack).
40
95
  */
41
- declare function runContentCli(courseConfig: CourseConfig, argv?: readonly string[], io?: ContentIo): Promise<number>;
96
+ declare function runContentCli(courseConfig: CourseConfig, argv?: readonly string[], io?: ContentIo,
97
+ /** A manifest already compiled for this course, reused instead of compiling one: a test running many verbs compiles once. */
98
+ compiled?: Manifest): Promise<number>;
42
99
 
43
- export { CONTENT_USAGE, type ContentArgs, type ContentIo, parseContentArgs, runContentCli };
100
+ export { type ContentArgs, type ContentIo, type EditPlanJson, type ErrorJson, type NewJson, type RulesJson, parseContentArgs, runContentCli };
@@ -26,7 +26,7 @@ import {
26
26
  formatSymbolReport,
27
27
  ownerTag,
28
28
  parseRulesFilter,
29
- planFix,
29
+ planFixMany,
30
30
  planInitAgent,
31
31
  planMove,
32
32
  planRename,
@@ -36,26 +36,11 @@ import {
36
36
  scaffoldTerm,
37
37
  summarizeDiagnostics
38
38
  } from "./cli.js";
39
+ import { formatVerbHelp, loadCliReference, verbHelp } from "./cli-reference.js";
40
+ import { CONTENT_USAGE } from "./cli-verbs.js";
41
+ import { planContentRelease } from "./content-release.js";
42
+ import { formatVerifyReport, formatVerifyStep, runVerify } from "./cli-verify.js";
39
43
  import { compileManifest } from "./manifest.js";
40
- const CONTENT_USAGE = `Usage:
41
- pnpm content status
42
- pnpm content diagnostics [info|warning|error] [--all | --code <c> | --lesson <id> | --changed] [--json]
43
- pnpm content warnings [--json]
44
- pnpm content context <lesson> [--json]
45
- pnpm content equation "<latex>" [--lesson <id>] [--json]
46
- pnpm content bindings <lesson> [--json]
47
- pnpm content symbol <latex | key> [--json]
48
- pnpm content check <lesson | deck>\u2026 [--changed] [--json]
49
- pnpm content rule <ID> [--json]
50
- pnpm content migrate [--from <version>] [--to <version>] [--json]
51
- pnpm content rules [--element <e>[,<e>]] [--enforcement script|partial|guide|locked] [--edition chapter|deck] [--json]
52
- pnpm content move <lesson>:<key> [--to <key>] [--write]
53
- pnpm content rename <old> <new> [--write]
54
- pnpm content fix <lesson> [--write]
55
- pnpm content new lesson <id>
56
- pnpm content new deck <lesson | part>
57
- pnpm content new term <key>
58
- pnpm content init-agent [--write]`;
59
44
  const VALUED_FLAGS = ["element", "enforcement", "edition", "code", "lesson", "from", "to"];
60
45
  function parseContentArgs(argv) {
61
46
  const positionals = [];
@@ -64,6 +49,8 @@ function parseContentArgs(argv) {
64
49
  let changed = false;
65
50
  let all = false;
66
51
  let write = false;
52
+ let e2e = false;
53
+ let force = false;
67
54
  const valued = new RegExp(`^--(${VALUED_FLAGS.join("|")})(?:=(.*))?$`);
68
55
  for (let i = 0; i < argv.length; i += 1) {
69
56
  const arg = argv[i];
@@ -72,6 +59,8 @@ function parseContentArgs(argv) {
72
59
  else if (arg === "--changed") changed = true;
73
60
  else if (arg === "--all") all = true;
74
61
  else if (arg === "--write") write = true;
62
+ else if (arg === "--e2e") e2e = true;
63
+ else if (arg === "--force") force = true;
75
64
  else if (match) values[match[1]] = match[2] ?? argv[i += 1] ?? "";
76
65
  else positionals.push(arg);
77
66
  }
@@ -82,9 +71,15 @@ function parseContentArgs(argv) {
82
71
  changed,
83
72
  all,
84
73
  write,
74
+ e2e,
75
+ force,
85
76
  ...values
86
77
  };
87
78
  }
79
+ function diagnosticsWhere(diagnostics, file) {
80
+ const own = diagnostics.filter((diagnostic) => diagnostic.file === file).length;
81
+ return own === diagnostics.length ? "" : ` (${own} in ${file}, ${diagnostics.length - own} in the shared cards it shows)`;
82
+ }
88
83
  const PROCESS_IO = {
89
84
  out: (text) => process.stdout.write(text),
90
85
  err: (text) => process.stderr.write(text)
@@ -109,12 +104,12 @@ function writeScaffold(scaffold) {
109
104
  }
110
105
  return written;
111
106
  }
112
- async function runContentCli(courseConfig, argv = process.argv.slice(2), io = PROCESS_IO) {
107
+ async function runContentCli(courseConfig, argv = process.argv.slice(2), io = PROCESS_IO, compiled) {
113
108
  const args = parseContentArgs(argv);
114
109
  const json = (value) => io.out(`${JSON.stringify(value, null, 2)}
115
110
  `);
116
111
  try {
117
- return await dispatch(courseConfig, args, io, json);
112
+ return await dispatch(courseConfig, args, io, json, compiled);
118
113
  } catch (error) {
119
114
  const code = error instanceof Usage || error instanceof ContentCliError ? error.code : "internal";
120
115
  const message = error instanceof Error ? error.message : String(error);
@@ -124,13 +119,40 @@ async function runContentCli(courseConfig, argv = process.argv.slice(2), io = PR
124
119
  return 1;
125
120
  }
126
121
  }
127
- async function dispatch(courseConfig, args, io, json) {
122
+ async function dispatch(courseConfig, args, io, json, compiled) {
128
123
  const out = io.out;
129
- if (args.command === "" || args.command === "help" || args.command === "--help") {
124
+ if (args.command === "" || args.command === "--help" || args.command === "help" && args.positionals.length === 0 && !args.json) {
130
125
  out(`${CONTENT_USAGE}
131
126
  `);
132
127
  return 0;
133
128
  }
129
+ if (args.command === "help") {
130
+ if (args.positionals.length > 1) usage("usage", `content help takes one verb; got ${args.positionals.join(" ")}.`);
131
+ const reference = loadCliReference();
132
+ const [word] = args.positionals;
133
+ if (word === void 0) {
134
+ json(reference);
135
+ return 0;
136
+ }
137
+ const help = verbHelp(reference, word);
138
+ if (args.json) json(help);
139
+ else out(formatVerbHelp(reference, help));
140
+ return 0;
141
+ }
142
+ if (args.command === "verify") {
143
+ if (args.positionals.length > 0) usage("usage", `content verify takes no argument; got ${args.positionals.join(" ")}.`);
144
+ if (!args.json) out(`verify (${args.e2e ? "with" : "without"} e2e; whole output under node_modules/.cache/explico/verify/)
145
+ `);
146
+ const report = runVerify({
147
+ e2e: args.e2e,
148
+ force: args.force,
149
+ ...args.json ? {} : { onStep: (event) => event.phase === "end" && out(formatVerifyStep(event.step)) }
150
+ });
151
+ if (args.json) json(report);
152
+ else out(report.cached ? formatVerifyReport(report) : `
153
+ ${formatVerifyReport(report)}`);
154
+ return report.ok ? 0 : 1;
155
+ }
134
156
  if (args.command === "new") {
135
157
  const [kind, id] = args.positionals;
136
158
  if (kind !== "lesson" && kind !== "deck" && kind !== "term") {
@@ -151,18 +173,28 @@ ${written.map((p) => ` ${p}`).join("\n")}
151
173
  if (["status", "rules", "warnings"].includes(args.command) && args.positionals.length > 0) {
152
174
  usage("usage", `content ${args.command} takes no <lesson>; got ${args.positionals.join(" ")}.`);
153
175
  }
154
- if (args.command === "init-agent") {
155
- if (args.positionals.length > 0) usage("usage", `content init-agent takes no argument; got ${args.positionals.join(" ")}.`);
156
- const plan = planInitAgent();
176
+ if (args.command === "init-agent" || args.command === "release") {
177
+ let plan;
178
+ if (args.command === "init-agent") {
179
+ if (args.positionals.length > 0) usage("usage", `content init-agent takes no argument; got ${args.positionals.join(" ")}.`);
180
+ plan = planInitAgent();
181
+ } else {
182
+ if (args.positionals.length > 1) usage("usage", `content release takes one of major, minor, patch; got ${args.positionals.join(" ")}.`);
183
+ plan = planContentRelease(args.positionals[0]);
184
+ }
157
185
  if (args.json) json({ ...plan, written: args.write });
158
186
  else out(renderEditPlan(plan));
159
187
  if (plan.edits.length === 0) return 0;
188
+ const next = plan.next.length > 0 ? plan.next.join(" \xB7 ") : "";
160
189
  if (args.write) {
161
190
  const touched = applyEditPlan(plan);
162
191
  if (!args.json) out(`
163
- written: ${touched.join(", ")}
192
+ written: ${touched.join(", ")}${next === "" ? "" : `
193
+ next: ${next}`}
194
+ `);
195
+ } else if (!args.json) out(`
196
+ dry run: nothing written. Add --write to apply${next === "" ? "" : `; then ${next}`}.
164
197
  `);
165
- } else if (!args.json) out("\ndry run: nothing written. Add --write to install.\n");
166
198
  return 0;
167
199
  }
168
200
  if (args.command === "migrate") {
@@ -196,7 +228,7 @@ written: ${touched.join(", ")}
196
228
  usage("unknown-command", `Unknown command "${args.command}".
197
229
  ${CONTENT_USAGE}`);
198
230
  }
199
- const manifest = await compileManifest(courseConfig);
231
+ const manifest = compiled ?? await compileManifest(courseConfig);
200
232
  switch (args.command) {
201
233
  case "status": {
202
234
  const report = contentStatus(manifest);
@@ -206,9 +238,11 @@ ${CONTENT_USAGE}`);
206
238
  }
207
239
  const c = report.counts;
208
240
  const unreleased = report.pageVersions.unreleased;
241
+ const UNRELEASED_SHOWN = 6;
242
+ const unreleasedList = unreleased.length <= UNRELEASED_SHOWN ? unreleased.join(", ") : `${unreleased.slice(0, UNRELEASED_SHOWN).join(", ")}, \u2026 ${unreleased.length - UNRELEASED_SHOWN} more in --json`;
209
243
  out(
210
244
  `Knowledge version ${report.knowledgeVersion}
211
- Page versions: ${report.pageVersions.tracked} page(s) tracked, ${unreleased.length} with changes not yet released${unreleased.length > 0 ? ` (${unreleased.join(", ")})` : ""}
245
+ Page versions: ${report.pageVersions.tracked} page(s) tracked, ${unreleased.length} with changes not yet released${unreleased.length > 0 ? ` (${unreleasedList})` : ""}
212
246
  Lessons ${c.lessons} \xB7 competencies ${c.competencies} \xB7 assessments ${c.assessments} \xB7 sources ${c.sources} \xB7 tracks ${c.tracks} \xB7 notation ${c.notationDefinitions} \xB7 keyed equations ${c.keyedEquations} \xB7 tables ${c.keyedTables} \xB7 figures ${c.keyedFigures} \xB7 diagrams ${c.keyedDiagrams} \xB7 example sets ${c.keyedExamples}
213
247
  Diagnostics: ${report.diagnostics.errors} error(s), ${report.diagnostics.warnings} warning(s), ${report.diagnostics.infos} info
214
248
  Orphans: ${report.orphans.length}
@@ -232,9 +266,8 @@ Agent guide: ${report.agentGuide.agentsFile === null ? "no AGENTS.md \u2014 `pnp
232
266
  if (!oldKey || !newKey || extra.length > 0) usage("usage", "content rename needs <old> <new>.");
233
267
  plan = planRename(manifest, oldKey, newKey);
234
268
  } else {
235
- const [lesson, ...extra] = args.positionals;
236
- if (!lesson || extra.length > 0) usage("usage", "content fix needs one <lesson> id.");
237
- plan = planFix(manifest, lesson);
269
+ if (args.all === args.positionals.length > 0) usage("usage", "content fix needs <lesson> ids, or --all for every lesson.");
270
+ plan = planFixMany(manifest, args.all ? manifest.lessons.map((lesson) => lesson.id) : args.positionals);
238
271
  }
239
272
  if (args.json) json({ ...plan, written: args.write });
240
273
  else out(renderEditPlan(plan));
@@ -329,7 +362,7 @@ teaches: ${report.lesson.teaches.join(", ") || "(none)"}
329
362
  requires: ${report.lesson.requires.join(", ") || "(none)"}
330
363
  notation: ${report.notation.map((n) => n.key).join(", ") || "(none)"}
331
364
  sources: ${report.sources.map((s) => s.id).join(", ") || "(none)"}
332
- diagnostics: ${report.diagnostics.length}
365
+ diagnostics: ${report.diagnostics.length}${diagnosticsWhere(report.diagnostics, report.lesson.file)}
333
366
  tokens: context ${t.context} vs doc set ${t.docSet} + corpus ${t.corpus} (${(t.ratio * 100).toFixed(2)}% of the baseline)
334
367
  `
335
368
  );
@@ -371,8 +404,16 @@ tokens: context ${t.context} vs doc set ${t.docSet} + corpus ${t.corpus} (${(t.r
371
404
  for (const entry of report.lessons) {
372
405
  out(`${entry.ok ? "ok " : "FAIL"} ${entry.lesson}${entry.rendered ? "" : " (render failed)"}
373
406
  `);
407
+ const infos = /* @__PURE__ */ new Map();
374
408
  for (const diagnostic of entry.diagnostics) {
375
- out(` [${diagnostic.severity}] ${diagnostic.code}${ownerTag(diagnostic)}: ${diagnostic.message}
409
+ if (diagnostic.severity === "info") infos.set(diagnostic.code, (infos.get(diagnostic.code) ?? 0) + 1);
410
+ else out(` [${diagnostic.severity}] ${diagnostic.code}${ownerTag(diagnostic)}: ${diagnostic.message}
411
+ `);
412
+ }
413
+ if (infos.size > 0) {
414
+ const total = [...infos.values()].reduce((sum, n) => sum + n, 0);
415
+ const byCode = [...infos].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([code, n]) => `${code} ${n}`).join(", ");
416
+ out(` + ${total} info (${byCode}); content diagnostics --lesson ${entry.lesson} lists them
376
417
  `);
377
418
  }
378
419
  }
@@ -0,0 +1,50 @@
1
+ import { ContentVerb } from './cli-verbs.js';
2
+
3
+ /** A JSON Schema (draft 2020-12), the subset the generator writes. */
4
+ type JsonSchema = Readonly<Record<string, unknown>>;
5
+ /** `docs/authoring/cli-reference.json`, whole: what `content help --json` prints. */
6
+ interface CliReference {
7
+ /** Where it came from, and that it is generated. */
8
+ readonly generatedBy: string;
9
+ readonly verbs: readonly ContentVerb[];
10
+ /** The type every verb's `--json` prints when the run fails: a stable code and a message, never a stack. */
11
+ readonly error: string;
12
+ /** A JSON Schema of every type a verb's `--json` prints, and of every named type inside one, by name. */
13
+ readonly $defs: Readonly<Record<string, JsonSchema>>;
14
+ }
15
+ /** One verb's row, its usage line, and a self-contained JSON Schema of its `--json`. */
16
+ interface VerbHelp extends ContentVerb {
17
+ readonly usage: string;
18
+ readonly schema: JsonSchema;
19
+ }
20
+ /** What `content help <verb> --json` prints: every verb the word names (`new` names three). */
21
+ interface VerbHelpJson {
22
+ readonly verbs: readonly VerbHelp[];
23
+ }
24
+ /** `content help --json` prints the whole reference; `content help <verb> --json` that verb's rows. */
25
+ type HelpJson = CliReference | VerbHelpJson;
26
+ declare const CLI_REFERENCE_FILE = "cli-reference.json";
27
+ /** The reference beside the engine (`node_modules/explico/docs/authoring/`, or this repository's). */
28
+ declare function loadCliReference(): CliReference;
29
+ /** The verbs a word names: `symbol` one, `new` the three `new …` verbs. */
30
+ declare function verbsNamed(word: string): readonly ContentVerb[];
31
+ /** A self-contained schema of one named type: a `$ref` and the `$defs` it reaches. */
32
+ declare function schemaOf(reference: CliReference, name: string): JsonSchema;
33
+ declare function verbHelp(reference: CliReference, word: string): VerbHelpJson;
34
+ /** One line per property, `name?: type — description`, nested objects indented under their property. */
35
+ declare function shapeLines(schema: JsonSchema, indent?: string): string[];
36
+ /** A named type as a block: its name and description, then its shape. */
37
+ declare function defLines(name: string, schema: JsonSchema): string[];
38
+ /** `content help <verb>` as text: the usage, what it is for, its flags, and the shape of its `--json`. */
39
+ declare function formatVerbHelp(reference: CliReference, help: VerbHelpJson): string;
40
+ /** `docs/authoring/cli.md`, generated: one section per verb, then every `--json` type. */
41
+ declare function cliReferenceMarkdown(reference: CliReference): string;
42
+ /**
43
+ * Where `value` departs from `schema`, one message per place (`$.hits[0].tier:
44
+ * expected one of …`); empty when it conforms. The subset the generator
45
+ * writes: `$ref`, `type`, `const`, `enum`, `anyOf`, `properties`, `required`,
46
+ * `additionalProperties`, `items`.
47
+ */
48
+ declare function jsonShapeErrors(schema: JsonSchema, value: unknown, defs?: CliReference["$defs"], path?: string): string[];
49
+
50
+ export { CLI_REFERENCE_FILE, type CliReference, type HelpJson, type JsonSchema, type VerbHelp, type VerbHelpJson, cliReferenceMarkdown, defLines, formatVerbHelp, jsonShapeErrors, loadCliReference, schemaOf, shapeLines, verbHelp, verbsNamed };