@canonical/pragma-cli 0.32.0 → 0.33.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 (140) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +14 -13
  4. package/src/bin.ts +2 -2
  5. package/src/capabilities/block/blockList.test.ts +79 -0
  6. package/src/capabilities/block/blockList.verb.ts +18 -5
  7. package/src/capabilities/block/pack.ts +12 -1
  8. package/src/capabilities/block/parity.test.ts +9 -2
  9. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  10. package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
  11. package/src/capabilities/capabilities/catalog.ts +7 -3
  12. package/src/capabilities/capabilities/hints.ts +8 -17
  13. package/src/capabilities/capabilities/types.ts +2 -0
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +4 -1
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/pragmaColophon.ts +4 -4
  20. package/src/capabilities/colophon/types.ts +1 -1
  21. package/src/capabilities/config/fields.ts +21 -92
  22. package/src/capabilities/config/set.test.ts +6 -6
  23. package/src/capabilities/config/set.verb.ts +5 -5
  24. package/src/capabilities/config/show.render.test.ts +65 -0
  25. package/src/capabilities/config/show.render.ts +56 -10
  26. package/src/capabilities/config/show.verb.ts +5 -5
  27. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  28. package/src/capabilities/create/byteEquality.test.ts +7 -4
  29. package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/create.test.ts +32 -3
  31. package/src/capabilities/create/create.verb.ts +69 -11
  32. package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
  33. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  34. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
  35. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  36. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  37. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  38. package/src/capabilities/doctor/doctor.render.ts +116 -34
  39. package/src/capabilities/doctor/doctor.test.ts +31 -1
  40. package/src/capabilities/doctor/runChecks.ts +28 -4
  41. package/src/capabilities/doctor/types.ts +15 -0
  42. package/src/capabilities/ontology/index.ts +10 -3
  43. package/src/capabilities/ontology/ontology.test.ts +157 -5
  44. package/src/capabilities/ontology/verbs.ts +192 -93
  45. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  46. package/src/capabilities/prompt/prompt.render.ts +23 -6
  47. package/src/capabilities/prompt/prompt.test.ts +61 -0
  48. package/src/capabilities/reference.test.ts +32 -0
  49. package/src/capabilities/resources/provider.ts +28 -14
  50. package/src/capabilities/resources/resources.test.ts +99 -0
  51. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  52. package/src/capabilities/setup/index.ts +7 -1
  53. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  54. package/src/capabilities/setup/operations/setupGenerator.ts +157 -61
  55. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  56. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  57. package/src/capabilities/setup/operations/setupSkills.ts +7 -1
  58. package/src/capabilities/setup/setup.render.test.ts +109 -0
  59. package/src/capabilities/setup/setup.render.ts +61 -14
  60. package/src/capabilities/setup/setup.test.ts +352 -1
  61. package/src/capabilities/setup/setup.verb.ts +57 -5
  62. package/src/capabilities/setup/types.ts +59 -2
  63. package/src/capabilities/shared/bands.ts +16 -0
  64. package/src/capabilities/sources/collectStatus.ts +19 -4
  65. package/src/capabilities/sources/runUpdate.ts +58 -1
  66. package/src/capabilities/sources/sources.test.ts +187 -1
  67. package/src/capabilities/sources/status.render.test.ts +65 -0
  68. package/src/capabilities/sources/status.render.ts +50 -23
  69. package/src/capabilities/surface.test.ts +34 -9
  70. package/src/capabilities/tier/lookup.render.test.ts +72 -0
  71. package/src/capabilities/tier/lookup.render.ts +33 -8
  72. package/src/capabilities/tier/lookup.test.ts +15 -0
  73. package/src/capabilities/tier/runLookup.ts +28 -0
  74. package/src/constants.test.ts +1 -1
  75. package/src/constants.ts +2 -2
  76. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
  77. package/src/kernel/completion/bashDrive.test.ts +3 -3
  78. package/src/kernel/completion/complete.test.ts +3 -5
  79. package/src/kernel/completion/entitySource.ts +14 -6
  80. package/src/kernel/completion/model.test.ts +2 -2
  81. package/src/kernel/completion/model.ts +0 -1
  82. package/src/kernel/completion/parse.test.ts +2 -4
  83. package/src/kernel/error/PragmaError.test.ts +4 -4
  84. package/src/kernel/error/renderError.ts +1 -1
  85. package/src/kernel/packs/compile.ts +1 -1
  86. package/src/kernel/project/cli/buildProgram.test.ts +51 -7
  87. package/src/kernel/project/cli/buildProgram.ts +11 -0
  88. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  89. package/src/kernel/project/cli/globalFlags.ts +14 -14
  90. package/src/kernel/project/cli/rootHelp.ts +4 -2
  91. package/src/kernel/project/mcp/instructions.ts +1 -1
  92. package/src/kernel/project/mcp/mcpError.ts +52 -0
  93. package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
  94. package/src/kernel/project/mcp/prompts/provider.ts +36 -6
  95. package/src/kernel/render/formatters.ts +5 -4
  96. package/src/kernel/render/renderers.test.ts +34 -0
  97. package/src/kernel/render/renderers.ts +25 -7
  98. package/src/kernel/render/style.test.ts +31 -0
  99. package/src/kernel/render/style.ts +74 -0
  100. package/src/kernel/runtime/graphpack/build.ts +28 -0
  101. package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
  102. package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
  103. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
  104. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
  105. package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
  106. package/src/kernel/runtime/graphpack/read.ts +28 -13
  107. package/src/kernel/runtime/graphpack/types.ts +14 -0
  108. package/src/kernel/runtime/loadSession.ts +4 -12
  109. package/src/kernel/runtime/refs/resolve.ts +63 -3
  110. package/src/kernel/runtime/storeReadiness.ts +61 -0
  111. package/src/kernel/runtime/types.ts +1 -1
  112. package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
  113. package/src/kernel/spec/emitReference.test.ts +196 -0
  114. package/src/kernel/spec/emitReference.ts +491 -0
  115. package/src/kernel/spec/emitSurface.ts +4 -2
  116. package/src/kernel/spec/index.ts +2 -0
  117. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  118. package/src/testing/PARITY_GAPS.ts +1 -1
  119. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  120. package/src/testing/behavioral/completion.test.ts +2 -2
  121. package/src/testing/behavioral/disclosure.test.ts +12 -0
  122. package/src/testing/behavioral/docExamples.test.ts +283 -0
  123. package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
  124. package/src/testing/behavioral/journeys.cli.test.ts +3 -1
  125. package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -0
  126. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  127. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  128. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +176 -0
  129. package/src/testing/behavioral/mcpSurface.test.ts +1 -1
  130. package/src/testing/behavioral/staleCommands.test.ts +76 -0
  131. package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
  132. package/src/testing/eval/cases/assembly.ts +3 -3
  133. package/src/testing/eval/cases/effectful.ts +6 -3
  134. package/src/testing/eval/cases/readNouns.ts +4 -1
  135. package/src/testing/fixtures/fixtureCapability.ts +156 -0
  136. package/src/testing/fixtures/graph/defaultPack.ts +178 -0
  137. package/src/testing/fixtures/graph/owlExport.ts +95 -0
  138. package/src/testing/helpers/extractSnippets.ts +29 -0
  139. package/src/testing/helpers/golden.ts +1 -1
  140. package/src/capabilities/config/field.test.ts +0 -188
@@ -77,8 +77,10 @@ export const FIXED_SURFACE = {
77
77
  pragma: "pragma v2 CLI and MCP server host (stdio)",
78
78
  },
79
79
  globalFlags: [
80
- { flag: "--llm", doc: "Condensed Markdown output for agents" },
81
- { flag: "--format <json|plain>", doc: "Select output format" },
80
+ {
81
+ flag: "--format <plain|llm|json>",
82
+ doc: "Select output format (llm = condensed Markdown for agents)",
83
+ },
82
84
  { flag: "--verbose", doc: "Diagnostic output on stderr" },
83
85
  {
84
86
  flag: "--detail <level>",
@@ -6,6 +6,8 @@
6
6
  * `--help`/`__complete` path that reaches it — stays free of zod.
7
7
  */
8
8
 
9
+ export type { ReferenceDocs } from "./emitReference.js";
10
+ export { emitReference } from "./emitReference.js";
9
11
  export type {
10
12
  EmittedSurface,
11
13
  EmittedVerb,
@@ -84,8 +84,8 @@ describe("surface conformance (PROTECTED)", () => {
84
84
  );
85
85
  });
86
86
 
87
- it("freezes exactly 40 designed MCP tools including info, config_show, config_set, colophon", () => {
88
- expect(golden.mcpSurface.tools).toHaveLength(40);
87
+ it("freezes exactly 38 designed MCP tools including info, config_show, config_set, colophon", () => {
88
+ expect(golden.mcpSurface.tools).toHaveLength(38);
89
89
  expect(golden.mcpSurface.tools).toContain("info");
90
90
  expect(golden.mcpSurface.tools).toContain("config_show");
91
91
  expect(golden.mcpSurface.tools).toContain("config_set");
@@ -126,7 +126,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
126
126
  id: "info-entitycount-storeless-not-triplecount",
127
127
  area: "info",
128
128
  description:
129
- "`info`'s entity total comes from the storeless pack index (`readPackIndex` → sum of `instanceCountByType`), NEVER a store boot — so the storeless invariant (`store.booted === false`) holds. The old `collectStoreSummary`'s SPARQL triple-count is DROPPED (`sources status` owns the per-source breakdown; info shows one total). `capabilities/info/collectInfo.ts`.",
129
+ "`info`'s entity total comes from the storeless pack index (`readPackIndex` → `entityTotal` = count of DISTINCT abox subjects), NEVER a store boot — so the storeless invariant (`store.booted === false`) holds. It is deliberately NOT the sum of `instanceCountByType`: that raw multiset double-counts real OWL exports (individual typed as its class AND `owl:NamedIndividual`, plus `owl:Class`/property meta-buckets), so the total ran ~2× (A1). The old `collectStoreSummary`'s SPARQL triple-count is DROPPED (`sources status` owns the per-source breakdown; info shows one total). `capabilities/info/collectInfo.ts`.",
130
130
  },
131
131
  {
132
132
  id: "setup-skills-undo-recompute",
@@ -36,9 +36,19 @@ describe("auto-LLM on a piped (non-TTY) stdout (A10, e2e)", () => {
36
36
  expect(result.stdout).toContain("pragma v");
37
37
  });
38
38
 
39
- it("an explicit --llm matches the auto-selected output", () => {
39
+ it("an explicit --format llm matches the auto-selected output", () => {
40
40
  const auto = runCli(["info"]);
41
- const explicit = runCli(["info", "--llm"]);
41
+ const explicit = runCli(["info", "--format", "llm"]);
42
42
  expect(explicit.stdout).toBe(auto.stdout);
43
43
  });
44
+
45
+ it("rejects the removed --llm flag as an unknown option (exit 2)", () => {
46
+ // The covenant's whole premise: the dedicated `--llm` flag is GONE, folded
47
+ // into `--format llm` (and the piped auto-default). A real command must now
48
+ // FAIL on it as a usage error — not silently accept or ignore it.
49
+ const result = runCli(["block", "list", "--llm"]);
50
+ expect(result.exitCode).toBe(2);
51
+ expect(result.stderr).toContain("unknown option");
52
+ expect(result.stderr).toContain("--llm");
53
+ });
44
54
  });
@@ -125,8 +125,8 @@ describe("entity params resolve from a real built pack index (B10)", () => {
125
125
 
126
126
  describe("global flags complete for a dash prefix, at any position", () => {
127
127
  it("completes global flags regardless of position", async () => {
128
- await expect(runComplete(["--l"], capabilities)).resolves.toEqual([
129
- "--llm",
128
+ await expect(runComplete(["--d"], capabilities)).resolves.toEqual([
129
+ "--detail",
130
130
  ]);
131
131
  });
132
132
  });
@@ -64,6 +64,18 @@ describe("declares its levels within the canonical index (B6)", () => {
64
64
  });
65
65
  });
66
66
 
67
+ describe("disclosure lookups declare the aligned canonical level set (B4)", () => {
68
+ // B4: block used to declare only `[summary, detailed]` while standard declared
69
+ // the full `[summary, standard, detailed]`, so a config `detail=standard` named
70
+ // a level block never advertised. Every disclosure lookup now declares the same
71
+ // canonical ladder; per-noun DEFAULTS may still differ (domain-tuned).
72
+ it.each(
73
+ disclosureLookupVerbs,
74
+ )("$noun: levels === the canonical ladder", (v) => {
75
+ expect(v.spec.disclosure?.levels).toEqual([...DETAIL_LEVELS]);
76
+ });
77
+ });
78
+
67
79
  describe("detail:summary vs detail:detailed — key-set growth (B6)", () => {
68
80
  it.each(
69
81
  disclosureLookupVerbs,
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Doc-example tests: every `pragma` command shown in the published README and
3
+ * the getting-started guide must be REAL. Two tiers, both driven off the live
4
+ * grammar (never a hand-copied list):
5
+ *
6
+ * - Tier 1 (grammar) — every extracted command's noun/verb resolves and every
7
+ * `--flag` is one the verb (or the global/mutation set) declares. Storeless.
8
+ * - Tier 2 (execution) — a curated set of read commands is PARSED through the
9
+ * real CLI grammar (so the param bag is derived from the documented string,
10
+ * never hand-supplied) and then booted against the canonical fixture graph
11
+ * and asserted to exit 0, proving the documented reads run, not just parse.
12
+ */
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
17
+ import { capabilities } from "../../capabilities/index.js";
18
+ import {
19
+ executeVerb,
20
+ extractParams,
21
+ } from "../../kernel/project/cli/dispatch.js";
22
+ import {
23
+ nounVerbMap,
24
+ resolveUnknownCommand,
25
+ } from "../../kernel/project/cli/suggest.js";
26
+ import { kebabCase } from "../../kernel/spec/emitSurface.js";
27
+ import type { VerbSpec } from "../../kernel/spec/types.js";
28
+ import {
29
+ CANONICAL_CONFIG,
30
+ CANONICAL_TTL,
31
+ } from "../fixtures/graph/canonical.js";
32
+ import { extractPragmaCommands } from "../helpers/extractSnippets.js";
33
+ import { bootFixtureRuntime } from "../helpers/fixtureGraph.js";
34
+ import { projectCli } from "../helpers/projectCli.js";
35
+
36
+ /** Read a shipped doc exactly as a reader (or lychee) would. */
37
+ function readDoc(relPath: string): string {
38
+ return readFileSync(
39
+ fileURLToPath(new URL(relPath, import.meta.url)),
40
+ "utf-8",
41
+ );
42
+ }
43
+
44
+ /** Every hand-written doc whose fenced `pragma` examples must be real grammar. */
45
+ const HAND_WRITTEN_DOCS = [
46
+ "../../../README.md",
47
+ "../../../docs/getting-started.md",
48
+ "../../../docs/mcp-integration.md",
49
+ "../../../docs/config-model.md",
50
+ "../../../docs/architecture.md",
51
+ "../../../docs/skills.md",
52
+ ];
53
+
54
+ const commands = HAND_WRITTEN_DOCS.flatMap((doc) =>
55
+ extractPragmaCommands(readDoc(doc)),
56
+ );
57
+
58
+ /** Every non-hidden verb, flattened from the live catalog. */
59
+ const allVerbs: VerbSpec[] = capabilities
60
+ .flatMap((module) => [...module.verbs])
61
+ .filter((verb) => !verb.hidden);
62
+ const nouns = nounVerbMap(allVerbs);
63
+
64
+ /** `"noun verb"` (or `"noun"` for a self-verb) → the runnable spec. */
65
+ const specByKey = new Map<string, VerbSpec>();
66
+ for (const verb of allVerbs) {
67
+ const sub = verb.path[1];
68
+ specByKey.set(
69
+ sub === undefined ? verb.path[0] : `${verb.path[0]} ${sub}`,
70
+ verb,
71
+ );
72
+ }
73
+
74
+ /** Flags valid on every command, beyond a verb's own declared flags. */
75
+ const AMBIENT_FLAGS = new Set([
76
+ "--llm",
77
+ "--format",
78
+ "--verbose",
79
+ "--detail",
80
+ "--dry-run",
81
+ "--undo",
82
+ "--yes",
83
+ "--help",
84
+ "--version",
85
+ ]);
86
+
87
+ /** The positional tokens of a command (drops `pragma` and every `-`-prefixed token). */
88
+ function readPositionals(command: string): string[] {
89
+ return command
90
+ .split(/\s+/)
91
+ .slice(1)
92
+ .filter((token) => !token.startsWith("-"));
93
+ }
94
+
95
+ /** Resolve a command's `VerbSpec` from its positionals (verb pair, else self-verb). */
96
+ function resolveSpec(positionals: readonly string[]): VerbSpec | undefined {
97
+ const noun = positionals.at(0);
98
+ const verb = positionals.at(1);
99
+ if (noun === undefined) return undefined;
100
+ const paired =
101
+ verb !== undefined ? specByKey.get(`${noun} ${verb}`) : undefined;
102
+ return paired ?? specByKey.get(noun);
103
+ }
104
+
105
+ /** The set of flags a verb accepts: its own declared flags plus the ambient set. */
106
+ function collectValidFlags(verb: VerbSpec): Set<string> {
107
+ const flags = new Set(AMBIENT_FLAGS);
108
+ for (const param of verb.params) {
109
+ if (!param.positional) flags.add(`--${kebabCase(param.name)}`);
110
+ }
111
+ return flags;
112
+ }
113
+
114
+ describe("doc examples — Tier 1: every documented command is grammatical", () => {
115
+ it("extracts commands from the docs", () => {
116
+ // A guard against a broken extractor silently passing everything else.
117
+ expect(commands.length).toBeGreaterThan(0);
118
+ });
119
+
120
+ it("resolves every command's noun and verb", () => {
121
+ for (const command of commands) {
122
+ const unknown = resolveUnknownCommand(readPositionals(command), nouns);
123
+ expect(unknown, `unresolved command: ${command}`).toBeUndefined();
124
+ }
125
+ });
126
+
127
+ it("uses only flags the verb or the ambient set declares", () => {
128
+ for (const command of commands) {
129
+ const spec = resolveSpec(readPositionals(command));
130
+ expect(spec, `no spec for: ${command}`).toBeDefined();
131
+ if (!spec) continue;
132
+ const allowed = collectValidFlags(spec);
133
+ for (const token of command.split(/\s+/)) {
134
+ if (!token.startsWith("--")) continue;
135
+ const flag = token.split("=").at(0) ?? token;
136
+ expect(allowed.has(flag), `unknown flag ${flag} in: ${command}`).toBe(
137
+ true,
138
+ );
139
+ }
140
+ }
141
+ });
142
+ });
143
+
144
+ /**
145
+ * One curated read: its documented form, the verb key it must route to, and the
146
+ * param bag the real parser must derive from {@link ReadCase.command} (the
147
+ * oracle Tier 2 asserts the parse against before executing).
148
+ */
149
+ interface ReadCase {
150
+ readonly command: string;
151
+ readonly key: string;
152
+ readonly params: Record<string, unknown>;
153
+ }
154
+
155
+ /** Reads that can run storelessly or against the canonical fixture graph. */
156
+ const READ_CASES: readonly ReadCase[] = [
157
+ { command: "pragma block list", key: "block list", params: {} },
158
+ {
159
+ command: "pragma block lookup Button",
160
+ key: "block lookup",
161
+ params: { name: ["Button"] },
162
+ },
163
+ { command: "pragma standard list", key: "standard list", params: {} },
164
+ { command: "pragma token list", key: "token list", params: {} },
165
+ { command: "pragma tier list", key: "tier list", params: {} },
166
+ { command: "pragma ontology list", key: "ontology list", params: {} },
167
+ { command: "pragma config show", key: "config show", params: {} },
168
+ { command: "pragma sources status", key: "sources status", params: {} },
169
+ { command: "pragma doctor", key: "doctor", params: {} },
170
+ { command: "pragma info", key: "info", params: {} },
171
+ { command: "pragma capabilities", key: "capabilities", params: {} },
172
+ { command: "pragma colophon", key: "colophon", params: {} },
173
+ ];
174
+
175
+ const NO_MUTATION = { dryRun: false, undo: false, yes: false };
176
+
177
+ /**
178
+ * Thrown by the capture hook to halt a parse at the resolved verb — before the
179
+ * real dispatch (and its runtime boot) fires — carrying the routed path plus the
180
+ * Commander-parsed operands and options.
181
+ */
182
+ class ParsedCommand extends Error {
183
+ constructor(
184
+ readonly path: readonly string[],
185
+ readonly positionals: string[],
186
+ readonly opts: Record<string, unknown>,
187
+ ) {
188
+ super("parsed");
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Parse a documented `pragma …` line through the REAL CLI grammar and return the
194
+ * verb it routes to plus the coerced param bag {@link extractParams} derives —
195
+ * WITHOUT running it. A `preAction` hook captures the resolved command and
196
+ * throws before its action, so no runtime boots. Deriving the bag from the
197
+ * STRING (rather than hand-supplying it) is what closes the Tier-2 gap: a
198
+ * documented positional in the wrong slot now yields a different bag, instead of
199
+ * a hand-fed bag masking the mistake.
200
+ *
201
+ * @param command - The documented `pragma …` command line.
202
+ * @returns The routed verb key, its spec, and the parser-derived param bag.
203
+ */
204
+ async function parseDocumented(
205
+ command: string,
206
+ ): Promise<{ key: string; spec: VerbSpec; params: Record<string, unknown> }> {
207
+ const program = projectCli(capabilities);
208
+ program.hook("preAction", (_root, action) => {
209
+ const parentName = action.parent?.name();
210
+ const routed =
211
+ parentName !== undefined && parentName !== "pragma"
212
+ ? [parentName, action.name()]
213
+ : [action.name()];
214
+ throw new ParsedCommand(routed, [...action.args], action.opts());
215
+ });
216
+
217
+ const argv = command.split(/\s+/).slice(1); // drop the leading `pragma`
218
+ try {
219
+ await program.parseAsync(argv, { from: "user" });
220
+ } catch (error) {
221
+ if (!(error instanceof ParsedCommand)) throw error;
222
+ const key = error.path.join(" ");
223
+ const spec = specByKey.get(key);
224
+ if (spec === undefined) {
225
+ throw new Error(`documented command routed to unknown verb: ${key}`);
226
+ }
227
+ return {
228
+ key,
229
+ spec,
230
+ params: extractParams(spec.params, error.positionals, error.opts),
231
+ };
232
+ }
233
+ throw new Error(`documented command reached no verb action: ${command}`);
234
+ }
235
+
236
+ describe("doc examples — Tier 2: curated read commands run green", () => {
237
+ let fixture: Awaited<ReturnType<typeof bootFixtureRuntime>> | undefined;
238
+ const documented = new Set(commands);
239
+
240
+ beforeAll(async () => {
241
+ fixture = await bootFixtureRuntime({
242
+ ttl: CANONICAL_TTL,
243
+ config: CANONICAL_CONFIG,
244
+ });
245
+ });
246
+
247
+ afterAll(async () => {
248
+ await fixture?.dispose();
249
+ });
250
+
251
+ for (const readCase of READ_CASES) {
252
+ it(`\`${readCase.command}\` parses to its verb and exits 0`, async () => {
253
+ expect(
254
+ documented.has(readCase.command),
255
+ `not documented: ${readCase.command}`,
256
+ ).toBe(true);
257
+
258
+ // Derive the verb + param bag from the DOCUMENTED string via the real
259
+ // grammar, then assert both against the oracle: a mis-slotted positional
260
+ // would route elsewhere or yield a different bag, failing here.
261
+ const parsed = await parseDocumented(readCase.command);
262
+ expect(parsed.key, `routed to the wrong verb: ${readCase.command}`).toBe(
263
+ readCase.key,
264
+ );
265
+ expect(
266
+ parsed.params,
267
+ `parsed bag ≠ expected for: ${readCase.command}`,
268
+ ).toEqual(readCase.params);
269
+
270
+ if (!fixture) return;
271
+ const outcome = await executeVerb(
272
+ parsed.spec,
273
+ parsed.params,
274
+ NO_MUTATION,
275
+ fixture.runtime,
276
+ );
277
+ expect(
278
+ outcome.exitCode,
279
+ `${readCase.command} → ${outcome.stderr ?? ""}`,
280
+ ).toBe(0);
281
+ });
282
+ }
283
+ });
@@ -46,7 +46,8 @@ const MATRIX: readonly {
46
46
  {
47
47
  label: "validOptions",
48
48
  error: PragmaError.invalidInput("format", "yaml", {
49
- validOptions: ["plain", "json"],
49
+ // The live `--format` set (mirrors bin.ts): plain, llm, json.
50
+ validOptions: ["plain", "llm", "json"],
50
51
  }),
51
52
  },
52
53
  {
@@ -102,10 +102,12 @@ describe("block list — tier-chain inheritance (B9, in-process CLI dispatch)",
102
102
  config: { tier: "global", channel: "normal" },
103
103
  });
104
104
  fixtures.push(fixture);
105
- // Scoped (no flag) excludes LXD Panel; --all-tiers restores it.
105
+ // Scoped (no flag) excludes LXD Panel; --all-tiers restores it AND reveals
106
+ // the untiered Button Icon subcomponent the scoped view omits (A2).
106
107
  expect(await blockListNames(fixture, false)).toEqual(["Button", "Modal"]);
107
108
  expect(await blockListNames(fixture, true)).toEqual([
108
109
  "Button",
110
+ "Button Icon",
109
111
  "LXD Panel",
110
112
  "Modal",
111
113
  ]);