@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
@@ -2,6 +2,8 @@
2
2
  * Formatters + data shape for `tier lookup <name>`.
3
3
  */
4
4
 
5
+ import type { RenderLookupOptions } from "../../kernel/render/contracts.js";
6
+ import { renderLookupPlain } from "../../kernel/render/renderers.js";
5
7
  import type { Formatters } from "../../kernel/spec/types.js";
6
8
 
7
9
  /** The looked-up tier: its IRI, name, and the blocks scoped directly to it. */
@@ -11,18 +13,41 @@ export interface TierLookupData {
11
13
  readonly blocks: readonly string[];
12
14
  }
13
15
 
16
+ /**
17
+ * The shared plain-lookup layout for a tier: a `name (uri)` title over an ═ rule,
18
+ * then the directly-scoped blocks as one inline field. Delegating to
19
+ * {@link renderLookupPlain} single-sources the frame AND the TTY styling (bold
20
+ * title, dim rule, cyan label) that block/skill lookups already earn — so a tier
21
+ * title no longer stays unstyled on a terminal while theirs are bold (B7).
22
+ */
23
+ const tierPlainOptions: RenderLookupOptions<TierLookupData> = {
24
+ title: (tier) => `${tier.name} (${tier.uri})`,
25
+ fields: [
26
+ {
27
+ label: "blocks",
28
+ // Pre-joined so the empty case still renders (the shared field renderer
29
+ // skips an empty array). Blocks are `ds:name` display strings, never IRIs,
30
+ // so the shared renderer's URI compaction is a no-op here — byte-stable.
31
+ value: (tier) =>
32
+ tier.blocks.length > 0
33
+ ? tier.blocks.join(", ")
34
+ : "(none scoped directly to this tier)",
35
+ },
36
+ ],
37
+ sections: [],
38
+ };
39
+
14
40
  export const tierLookupFormatters: Formatters<TierLookupData> = {
15
41
  plain(data) {
16
- const lines = [`${data.name} (${data.uri})`];
17
- lines.push(
18
- data.blocks.length > 0
19
- ? ` blocks: ${data.blocks.join(", ")}`
20
- : " blocks: (none scoped directly to this tier)",
21
- );
22
- return lines.join("\n");
42
+ return renderLookupPlain(data, tierPlainOptions);
23
43
  },
24
44
  llm(data) {
25
- const lines = [`# ${data.name}`, `- IRI: \`${data.uri}\``];
45
+ // A byte-frozen agent contract that DIVERGES from the generic
46
+ // `renderLookupLlm`: an H2 `name` title (not `name (uri)`) with the IRI and
47
+ // blocks backtick-wrapped (`\`cs:…\``) — a shape the shared renderer does not
48
+ // emit. The llm path is never styled, so it has no TTY drift to single-source;
49
+ // kept inline so the frozen bytes hold (B2).
50
+ const lines = [`## ${data.name}`, `- IRI: \`${data.uri}\``];
26
51
  if (data.blocks.length > 0) {
27
52
  lines.push(`- Blocks: ${data.blocks.map((b) => `\`${b}\``).join(", ")}`);
28
53
  }
@@ -39,6 +39,21 @@ describe("tier_lookup — bespoke single-name lookup (PROTECTED)", () => {
39
39
  const error = result.error as { code: string };
40
40
  expect(error.code).toBe("ENTITY_NOT_FOUND");
41
41
  });
42
+
43
+ it("suggests a near-miss tier name from the full list (did-you-mean parity)", async () => {
44
+ // B6: tier lookup was the only entity lookup that omitted suggestions,
45
+ // though — like the others — it can read the full tier list on a miss.
46
+ let caught: unknown;
47
+ try {
48
+ await runTierLookup(fixture.runtime, "apps/lxdd");
49
+ } catch (error) {
50
+ caught = error;
51
+ }
52
+ expect(caught).toMatchObject({ code: "ENTITY_NOT_FOUND" });
53
+ expect((caught as { suggestions: string[] }).suggestions).toContain(
54
+ "apps/lxd",
55
+ );
56
+ });
42
57
  });
43
58
 
44
59
  describe("tier_lookup — cold/unseeded store (ROOT A)", () => {
@@ -9,11 +9,34 @@
9
9
 
10
10
  import { PragmaError } from "../../kernel/error/PragmaError.js";
11
11
  import { runSelect } from "../../kernel/packs/sparql/runSelect.js";
12
+ import { suggestNames } from "../../kernel/project/cli/suggestNames.js";
12
13
  import { compactUri } from "../../kernel/render/compactUri.js";
13
14
  import { DEFAULT_PREFIX_MAP } from "../../kernel/render/prefixes.js";
14
15
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
15
16
  import type { TierLookupData } from "./lookup.render.js";
16
17
 
18
+ /**
19
+ * Read every tier name in the store (its `ds:name`), ordered.
20
+ *
21
+ * The bespoke lookup already holds the machinery to reach the store, so a
22
+ * name-not-found can rank "did you mean?" candidates the same way every other
23
+ * entity lookup does — the tier list is a single cheap SELECT away.
24
+ *
25
+ * @param rt - The runtime (its store is booted by the projector for needsStore).
26
+ * @returns The tier names, ordered.
27
+ * @note Impure — queries the store through `runSelect`.
28
+ */
29
+ async function selectTierNames(rt: PragmaRuntime): Promise<string[]> {
30
+ const rows = await runSelect(
31
+ rt,
32
+ "SELECT ?name WHERE { ?tier a ds:Tier ; ds:name ?name } ORDER BY ?name",
33
+ "tier",
34
+ );
35
+ return rows
36
+ .map((row) => row.name as string | undefined)
37
+ .filter((name): name is string => Boolean(name));
38
+ }
39
+
17
40
  /**
18
41
  * Look up one tier by name.
19
42
  *
@@ -43,7 +66,12 @@ export async function runTierLookup(
43
66
  const rows = await runSelect(rt, query, "tier");
44
67
  const uri = rows[0]?.uri;
45
68
  if (!uri) {
69
+ // Rank "did you mean?" candidates from the full tier list — the only entity
70
+ // lookup that used to omit suggestions, though (like the others) it has the
71
+ // names in hand. The suggestion query runs only on this cold, not-found path.
72
+ const suggestions = suggestNames(name, await selectTierNames(rt));
46
73
  throw PragmaError.notFound("tier", name, {
74
+ suggestions,
47
75
  recovery: {
48
76
  message: "List available tiers.",
49
77
  cli: "pragma tier list",
@@ -25,7 +25,7 @@ describe("constants", () => {
25
25
  });
26
26
 
27
27
  it("exposes the stable output formats and detail levels", () => {
28
- expect(OUTPUT_FORMATS).toEqual(["plain", "json"]);
28
+ expect(OUTPUT_FORMATS).toEqual(["plain", "llm", "json"]);
29
29
  expect(DETAIL_LEVELS).toEqual(["summary", "standard", "detailed"]);
30
30
  expect(DETAIL_LEVELS).toContain(DEFAULT_DETAIL_LEVEL);
31
31
  expect(PROGRAM_DESCRIPTION.length).toBeGreaterThan(0);
package/src/constants.ts CHANGED
@@ -31,8 +31,8 @@ const VERSION: string = pkg.version;
31
31
  */
32
32
  const RECOVERY_CLI_PREFIX = "pragma ";
33
33
 
34
- /** The two machine/human output formats the renderer selects between. */
35
- const OUTPUT_FORMATS = ["plain", "json"] as const;
34
+ /** The output formats the renderer selects between (`llm` = condensed Markdown). */
35
+ const OUTPUT_FORMATS = ["plain", "llm", "json"] as const;
36
36
 
37
37
  /** A selected output format. `--format text` is normalised to `plain`. */
38
38
  type OutputFormat = (typeof OUTPUT_FORMATS)[number];
@@ -58,21 +58,21 @@ _pragma() {
58
58
  block/list/--tags) return ;;
59
59
  esac
60
60
  case "$prev" in
61
- --format) COMPREPLY=($(compgen -W "plain json" -- "$cur")); return ;;
61
+ --format) COMPREPLY=($(compgen -W "plain llm json" -- "$cur")); return ;;
62
62
  --detail) COMPREPLY=($(compgen -W "summary standard detailed" -- "$cur")); return ;;
63
63
  esac
64
64
 
65
65
  # flag names
66
66
  if [[ "$cur" == -* ]]; then
67
67
  case "$noun/$verb" in
68
- block/get) COMPREPLY=($(compgen -W "--view --out --with-meta --note --llm --format --verbose --detail --help" -- "$cur")); return ;;
69
- block/list) COMPREPLY=($(compgen -W "--tags --llm --format --verbose --detail --help" -- "$cur")); return ;;
70
- block/remove) COMPREPLY=($(compgen -W "--dry-run --undo --yes --llm --format --verbose --detail --help" -- "$cur")); return ;;
68
+ block/get) COMPREPLY=($(compgen -W "--view --out --with-meta --note --format --verbose --detail --help" -- "$cur")); return ;;
69
+ block/list) COMPREPLY=($(compgen -W "--tags --format --verbose --detail --help" -- "$cur")); return ;;
70
+ block/remove) COMPREPLY=($(compgen -W "--dry-run --undo --yes --format --verbose --detail --help" -- "$cur")); return ;;
71
71
  esac
72
72
  if [ -z "$noun" ]; then
73
- COMPREPLY=($(compgen -W "--llm --format --verbose --detail --help --version" -- "$cur"))
73
+ COMPREPLY=($(compgen -W "--format --verbose --detail --help --version" -- "$cur"))
74
74
  else
75
- COMPREPLY=($(compgen -W "--llm --format --verbose --detail --help" -- "$cur"))
75
+ COMPREPLY=($(compgen -W "--format --verbose --detail --help" -- "$cur"))
76
76
  fi
77
77
  return
78
78
  fi
@@ -172,8 +172,7 @@ complete -c pragma -f
172
172
  complete -c pragma -n "__pragma_at ''" -a "block mcp standard status"
173
173
 
174
174
  # global flags
175
- complete -c pragma -l llm
176
- complete -c pragma -l format -x -a "plain json"
175
+ complete -c pragma -l format -x -a "plain llm json"
177
176
  complete -c pragma -l verbose
178
177
  complete -c pragma -l detail -x -a "summary standard detailed"
179
178
  complete -c pragma -l help
@@ -268,21 +267,21 @@ _pragma() {
268
267
  block/list/--tags) return 0 ;;
269
268
  esac
270
269
  case "$prev" in
271
- --format) compadd -- plain json; return 0 ;;
270
+ --format) compadd -- plain llm json; return 0 ;;
272
271
  --detail) compadd -- summary standard detailed; return 0 ;;
273
272
  esac
274
273
 
275
274
  # flag names
276
275
  if [[ "$cur" == -* ]]; then
277
276
  case "$noun/$verb" in
278
- block/get) compadd -- --view --out --with-meta --note --llm --format --verbose --detail --help; return 0 ;;
279
- block/list) compadd -- --tags --llm --format --verbose --detail --help; return 0 ;;
280
- block/remove) compadd -- --dry-run --undo --yes --llm --format --verbose --detail --help; return 0 ;;
277
+ block/get) compadd -- --view --out --with-meta --note --format --verbose --detail --help; return 0 ;;
278
+ block/list) compadd -- --tags --format --verbose --detail --help; return 0 ;;
279
+ block/remove) compadd -- --dry-run --undo --yes --format --verbose --detail --help; return 0 ;;
281
280
  esac
282
281
  if [[ -z "$noun" ]]; then
283
- compadd -- --llm --format --verbose --detail --help --version
282
+ compadd -- --format --verbose --detail --help --version
284
283
  else
285
- compadd -- --llm --format --verbose --detail --help
284
+ compadd -- --format --verbose --detail --help
286
285
  fi
287
286
  return 0
288
287
  fi
@@ -51,7 +51,7 @@ describe.skipIf(!bashOk)("generated bash — --flag=value wordbreak (M1)", () =>
51
51
  it("routes --format=<TAB> to the format values (not nouns/positionals)", () => {
52
52
  // Default COMP_WORDBREAKS split: `--format` `=` `` (empty current word).
53
53
  const reply = driveBash(["pragma", "--format", "=", ""], 3);
54
- expect(reply.sort()).toEqual(["json", "plain"]);
54
+ expect(reply.sort()).toEqual(["json", "llm", "plain"]);
55
55
  });
56
56
 
57
57
  it("filters the value by the partial typed after =", () => {
@@ -63,11 +63,11 @@ describe.skipIf(!bashOk)("generated bash — --flag=value wordbreak (M1)", () =>
63
63
  // After a noun/verb the value must still route as a flag value, never as
64
64
  // block's positional (which would offer entity/enum candidates instead).
65
65
  const reply = driveBash(["pragma", "block", "get", "--format", "=", ""], 5);
66
- expect(reply.sort()).toEqual(["json", "plain"]);
66
+ expect(reply.sort()).toEqual(["json", "llm", "plain"]);
67
67
  });
68
68
 
69
69
  it("still completes the space form --format <TAB> (unbroken)", () => {
70
70
  const reply = driveBash(["pragma", "--format", ""], 2);
71
- expect(reply.sort()).toEqual(["json", "plain"]);
71
+ expect(reply.sort()).toEqual(["json", "llm", "plain"]);
72
72
  });
73
73
  });
@@ -19,16 +19,14 @@ describe("runComplete — the __complete pipeline", () => {
19
19
  "config",
20
20
  ]);
21
21
  // The live `config` sub-verbs, prefix-filtered and sorted by the resolver.
22
+ // AV-228 B3 retired the per-field tier/channel/detail verbs, leaving set + show.
22
23
  await expect(runComplete(["config", ""], capabilities)).resolves.toEqual([
23
- "channel",
24
- "detail",
25
24
  "set",
26
25
  "show",
27
- "tier",
28
26
  ]);
29
27
  await expect(runComplete(["mc"], capabilities)).resolves.toEqual(["mcp"]);
30
- await expect(runComplete(["--l"], capabilities)).resolves.toEqual([
31
- "--llm",
28
+ await expect(runComplete(["--d"], capabilities)).resolves.toEqual([
29
+ "--detail",
32
30
  ]);
33
31
  });
34
32
 
@@ -98,18 +98,26 @@ export function readPackIndex(cwd: string): PackIndex | undefined {
98
98
  }
99
99
 
100
100
  /**
101
- * Sum a pack index's per-type instance counts — the "total entities" figure
101
+ * Count a pack index's DISTINCT abox subjects — the "total entities" figure
102
102
  * `info` and `doctor` report. Works over any {@link PackIndex}, whether read
103
103
  * storelessly via {@link readPackIndex} or taken from a booted store session.
104
104
  *
105
+ * NOT a sum of `instanceCountByType`: on a real OWL/Protégé export that raw
106
+ * multiset double-counts — every individual is typed as both its domain class
107
+ * AND `owl:NamedIndividual`, and the `owl:Class`/property meta-buckets pile on
108
+ * top — so the total ran ~2× the real entity count (A1). Counting distinct
109
+ * abox subjects (the individuals, each once) is the figure users expect; the
110
+ * tbox schema classes/properties are not "entities" in this count.
111
+ *
105
112
  * @param index - A pack index.
106
- * @returns The total number of indexed entity instances across every type.
113
+ * @returns The number of distinct abox subjects indexed.
107
114
  */
108
115
  export function entityTotal(index: PackIndex): number {
109
- return Object.values(index.instanceCountByType).reduce(
110
- (sum, n) => sum + n,
111
- 0,
112
- );
116
+ const subjects = new Set<string>();
117
+ for (const entity of index.entities) {
118
+ if (entity.box === "abox") subjects.add(entity.uri ?? entity.name);
119
+ }
120
+ return subjects.size;
113
121
  }
114
122
 
115
123
  /** Load the active pack's index: the locked pack, else the embedded fallback. */
@@ -264,13 +264,13 @@ describe("buildCompletionModel — flags and sources", () => {
264
264
  const flags = Object.fromEntries(model.globalFlags.map((f) => [f.flag, f]));
265
265
  expect(flags["--format"]?.source).toEqual({
266
266
  kind: "values",
267
- values: ["plain", "json"],
267
+ values: ["plain", "llm", "json"],
268
268
  });
269
269
  expect(flags["--detail"]?.source).toEqual({
270
270
  kind: "values",
271
271
  values: ["summary", "standard", "detailed"],
272
272
  });
273
- expect(flags["--llm"]?.takesValue).toBe(false);
273
+ expect(flags["--llm"]).toBeUndefined();
274
274
  expect(flags["--version"]?.rootOnly).toBe(true);
275
275
  expect(flags["--help"]?.rootOnly).toBeUndefined();
276
276
  });
@@ -61,7 +61,6 @@ export function assertSafeToken(token: string, where: string): void {
61
61
 
62
62
  /** The global flags completion offers, mirroring the surface's globalFlags. */
63
63
  const GLOBAL_FLAGS: readonly FlagEntry[] = [
64
- { flag: "--llm", takesValue: false, repeatable: false, source: none() },
65
64
  {
66
65
  flag: "--format",
67
66
  takesValue: true,
@@ -73,7 +73,7 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
73
73
  context: { kind: "noun" },
74
74
  partial: "blo",
75
75
  });
76
- expect(parse(["--llm", "block", ""]).context).toEqual({
76
+ expect(parse(["--verbose", "block", ""]).context).toEqual({
77
77
  kind: "verb",
78
78
  noun: "block",
79
79
  });
@@ -83,7 +83,6 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
83
83
  describe("parseWords — flag names (PROTECTED)", () => {
84
84
  it("offers only globals (with --version) at the root", () => {
85
85
  expect(flagsOf(parse(["--"]).context)).toEqual([
86
- "--llm",
87
86
  "--format",
88
87
  "--verbose",
89
88
  "--detail",
@@ -105,7 +104,6 @@ describe("parseWords — flag names (PROTECTED)", () => {
105
104
  "--out",
106
105
  "--with-meta",
107
106
  "--note",
108
- "--llm",
109
107
  "--format",
110
108
  "--verbose",
111
109
  "--detail",
@@ -192,7 +190,7 @@ describe("parseWords — flag values (PROTECTED)", () => {
192
190
  context: {
193
191
  kind: "flag-value",
194
192
  flag: "--format",
195
- source: { kind: "values", values: ["plain", "json"] },
193
+ source: { kind: "values", values: ["plain", "llm", "json"] },
196
194
  },
197
195
  });
198
196
  expect(parse(["block", "get", "--detail", "s"])).toMatchObject({
@@ -26,10 +26,10 @@ describe("PragmaError factories", () => {
26
26
 
27
27
  it("invalidInput enumerates valid options", () => {
28
28
  const error = PragmaError.invalidInput("format", "yaml", {
29
- validOptions: ["plain", "json"],
29
+ validOptions: ["plain", "llm", "json"],
30
30
  });
31
31
  expect(error.code).toBe("INVALID_INPUT");
32
- expect(error.validOptions).toEqual(["plain", "json"]);
32
+ expect(error.validOptions).toEqual(["plain", "llm", "json"]);
33
33
  });
34
34
 
35
35
  it("unknownVerb names the token", () => {
@@ -105,14 +105,14 @@ describe("envelope builders", () => {
105
105
 
106
106
  it("wraps an error into the failure envelope", () => {
107
107
  const error = PragmaError.invalidInput("format", "yaml", {
108
- validOptions: ["plain", "json"],
108
+ validOptions: ["plain", "llm", "json"],
109
109
  });
110
110
  expect(errorEnvelope(error)).toEqual({
111
111
  ok: false,
112
112
  error: {
113
113
  code: "INVALID_INPUT",
114
114
  message: 'Invalid format "yaml".',
115
- validOptions: ["plain", "json"],
115
+ validOptions: ["plain", "llm", "json"],
116
116
  },
117
117
  });
118
118
  });
@@ -2,7 +2,7 @@
2
2
  * Render a {@link PragmaError} for each output surface.
3
3
  *
4
4
  * Plain text targets a human terminal (stderr), condensed Markdown targets an
5
- * agent reading `--llm`, and JSON emits the `{ ok: false, error }` envelope —
5
+ * agent reading `--format llm`, and JSON emits the `{ ok: false, error }` envelope —
6
6
  * the same failure shape the MCP projector returns, so agents see one contract
7
7
  * whichever transport they use.
8
8
  */
@@ -145,7 +145,7 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
145
145
  },
146
146
  ]
147
147
  : []),
148
- { cmd: `pragma ${meta.noun} ${meta.verb} --llm` },
148
+ { cmd: `pragma ${meta.noun} ${meta.verb} --format llm` },
149
149
  ],
150
150
  capability: READ_CAPABILITY,
151
151
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
@@ -61,6 +61,50 @@ describe("buildProgram — flags", () => {
61
61
  });
62
62
  });
63
63
 
64
+ describe("buildProgram — default-true boolean negation (B9)", () => {
65
+ const negatableVerb: VerbSpec = {
66
+ path: ["thing", "do"],
67
+ summary: "thing do summary",
68
+ params: [
69
+ { kind: "boolean", name: "ssr", doc: "Include SSR.", default: true },
70
+ {
71
+ kind: "boolean",
72
+ name: "runInstall",
73
+ doc: "Install now.",
74
+ default: false,
75
+ },
76
+ { kind: "boolean", name: "withHistory", doc: "Seed history." },
77
+ ],
78
+ output: {
79
+ formatters: {
80
+ plain: (d) => String(d),
81
+ llm: (d) => String(d),
82
+ json: (d) => JSON.stringify(d),
83
+ },
84
+ },
85
+ capability: { needsStore: false, mutates: false, mcp: { expose: true } },
86
+ run: async () => null,
87
+ };
88
+ const program = projectCli([{ name: "thing", verbs: [negatableVerb] }]);
89
+ const doCmd = program.commands
90
+ .find((c) => c.name() === "thing")
91
+ ?.commands.find((c) => c.name() === "do");
92
+
93
+ it("pairs a `--no-` negation onto a default-true boolean so it is disableable", () => {
94
+ const longs = doCmd?.options.map((o) => o.long) ?? [];
95
+ expect(longs).toContain("--ssr");
96
+ expect(longs).toContain("--no-ssr");
97
+ });
98
+
99
+ it("leaves default-false / defaultless booleans un-negated (no Commander default-flip)", () => {
100
+ const longs = doCmd?.options.map((o) => o.long) ?? [];
101
+ expect(longs).toContain("--run-install");
102
+ expect(longs).not.toContain("--no-run-install");
103
+ expect(longs).toContain("--with-history");
104
+ expect(longs).not.toContain("--no-with-history");
105
+ });
106
+ });
107
+
64
108
  describe("buildProgram — early exits", () => {
65
109
  it("throws commander.version for --version, writing the version", async () => {
66
110
  const program = projectCli([fixtureModule]);
@@ -353,7 +397,8 @@ describe("formatRootHelp — grouping", () => {
353
397
 
354
398
  it("shows the global flags block", () => {
355
399
  expect(help).toContain("Global flags");
356
- expect(help).toContain("--llm");
400
+ expect(help).not.toContain("--llm");
401
+ expect(help).toContain("--format <plain|llm|json>");
357
402
  expect(help).toContain("--detail <level>");
358
403
  });
359
404
 
@@ -374,12 +419,11 @@ describe("formatRootHelp — grouping", () => {
374
419
  mcp Start the MCP server over stdio
375
420
 
376
421
  Global flags
377
- --llm Condensed Markdown output for agents
378
- --format <json|plain> Select output format
379
- --detail <level> Progressive-disclosure level (summary, standard, detailed)
380
- --verbose Diagnostic output on stderr
381
- --help Show help (works on any command)
382
- --version Show the CLI version
422
+ --format <plain|llm|json> Select output format (llm = condensed Markdown for agents)
423
+ --detail <level> Progressive-disclosure level (summary, standard, detailed)
424
+ --verbose Diagnostic output on stderr
425
+ --help Show help (works on any command)
426
+ --version Show the CLI version
383
427
 
384
428
  Run \`pragma <command> --help\` for details, or \`pragma capabilities\` to get oriented."
385
429
  `);
@@ -70,6 +70,17 @@ function registerParams(command: Command, verb: VerbSpec): void {
70
70
  } else {
71
71
  command.option(spec, param.doc);
72
72
  }
73
+ // B9: a default-`true` boolean is otherwise undisableable — the grammar has
74
+ // no negation form, so `create`'s sibling booleans (--ssr/--with-styles/…)
75
+ // could never be turned off. Register a paired `--no-<flag>` so they can:
76
+ // Commander keeps the `true` default and lets `--no-<flag>` set it false
77
+ // (verified: an unset default-false boolean is left alone). This is a CLI
78
+ // PARSE convenience, never a ParamSpec — `emitSurface`/MCP/the covenant are
79
+ // untouched — so the frozen flag list stands and negation stays additive.
80
+ if (param.kind === "boolean" && param.default === true) {
81
+ const flag = kebabCase(param.name);
82
+ command.option(`--no-${flag}`, `Disable --${flag} (on by default).`);
83
+ }
73
84
  }
74
85
 
75
86
  if (verb.capability.mutates) {
@@ -9,10 +9,18 @@ const TTY = { isTty: true, noAutoLlm: false };
9
9
  const PIPE = { isTty: false, noAutoLlm: false };
10
10
 
11
11
  describe("parseGlobalFlags", () => {
12
- it("honours an explicit --llm", () => {
13
- expect(parseGlobalFlags(["--llm"], TTY)).toMatchObject({
12
+ it("honours an explicit --format llm (condensed even on a TTY)", () => {
13
+ expect(parseGlobalFlags(["--format", "llm"], TTY)).toMatchObject({
14
14
  llm: true,
15
15
  autoLlm: false,
16
+ format: "llm",
17
+ });
18
+ });
19
+
20
+ it("forces human output with --format plain down a pipe", () => {
21
+ expect(parseGlobalFlags(["--format", "plain"], PIPE)).toMatchObject({
22
+ llm: false,
23
+ autoLlm: false,
16
24
  format: "plain",
17
25
  });
18
26
  });
@@ -69,7 +77,7 @@ describe("stripGlobalFlags", () => {
69
77
  "list",
70
78
  ]);
71
79
  expect(
72
- stripGlobalFlags(["block", "--llm", "list", "--detail", "detailed"]),
80
+ stripGlobalFlags(["block", "--verbose", "list", "--detail", "detailed"]),
73
81
  ).toEqual(["block", "list"]);
74
82
  expect(stripGlobalFlags(["--format=json", "config", "show"])).toEqual([
75
83
  "config",
@@ -90,7 +98,7 @@ describe("readRawFormat", () => {
90
98
 
91
99
  it("reports a valueless --format as empty string", () => {
92
100
  expect(readRawFormat(["--format"])).toBe("");
93
- expect(readRawFormat(["--format", "--llm"])).toBe("");
101
+ expect(readRawFormat(["--format", "--verbose"])).toBe("");
94
102
  });
95
103
 
96
104
  it("returns undefined when --format is absent", () => {
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Global-flag pre-parsing, ahead of Commander.
3
3
  *
4
- * `--llm`, `--format`, `--verbose`, and `--detail` may appear anywhere on the
5
- * line, so they are scanned and stripped before Commander sees argv — otherwise
4
+ * `--format`, `--verbose`, and `--detail` may appear anywhere on the line, so
5
+ * they are scanned and stripped before Commander sees argv — otherwise
6
6
  * `enablePositionalOptions()` scoping would reject a flag placed after a verb.
7
7
  * Ported from the v1 `parseGlobalFlags`, with two v2 changes: a new `--detail`
8
8
  * flag and the `--format text` value renamed to `plain` (the kernel's
9
- * {@link OutputFormat}). Both the space (`--format json`) and equals
10
- * (`--format=json`) forms are recognized.
9
+ * {@link OutputFormat}). The dedicated `--llm` flag was folded into
10
+ * `--format llm`, leaving auto-detection as the sole implicit trigger. Both the
11
+ * space (`--format json`) and equals (`--format=json`) forms are recognized.
11
12
  */
12
13
 
13
14
  import { DETAIL_LEVELS, type DetailLevel } from "../../../constants.js";
@@ -86,10 +87,11 @@ function readDetail(argv: readonly string[]): DetailLevel | undefined {
86
87
  /**
87
88
  * Extract the global flags from raw argv (the user's arguments, argv[2:]).
88
89
  *
89
- * Auto-LLM: with no `--llm`/`--format` and a non-interactive stdout (piped or
90
+ * Auto-LLM: with no explicit `--format` and a non-interactive stdout (piped or
90
91
  * redirected — the shape an agent captures), `llm` defaults to true so agents
91
- * get condensed Markdown without a flag. `--llm`, `--format`, and
92
- * `PRAGMA_NO_AUTO_LLM` all override it.
92
+ * get condensed Markdown without a flag. Any explicit `--format` overrides it —
93
+ * `--format plain` forces human output down a pipe, `--format llm` forces the
94
+ * condensed form even on a TTY — as does `PRAGMA_NO_AUTO_LLM`.
93
95
  *
94
96
  * @param argv - The user's arguments (no `node`/script prefix).
95
97
  * @param env - Output environment probe (defaults to the real process).
@@ -100,15 +102,14 @@ export function parseGlobalFlags(
100
102
  argv: readonly string[],
101
103
  env: OutputEnvironment = readOutputEnvironment(),
102
104
  ): GlobalFlags {
103
- const explicitLlm = argv.includes("--llm");
104
105
  const rawFormat = readFlagValue(argv, "--format");
105
- const format = rawFormat === "json" ? "json" : "plain";
106
+ const format =
107
+ rawFormat === "json" ? "json" : rawFormat === "llm" ? "llm" : "plain";
106
108
  const formatRequested = rawFormat !== undefined;
107
- const autoLlm =
108
- !explicitLlm && !formatRequested && !env.isTty && !env.noAutoLlm;
109
+ const autoLlm = !formatRequested && !env.isTty && !env.noAutoLlm;
109
110
  const detail = readDetail(argv);
110
111
  return {
111
- llm: explicitLlm || autoLlm,
112
+ llm: format === "llm" || autoLlm,
112
113
  autoLlm,
113
114
  format,
114
115
  verbose: argv.includes("--verbose"),
@@ -130,10 +131,9 @@ export function stripGlobalFlags(argv: readonly string[]): string[] {
130
131
  const result: string[] = [];
131
132
  for (let i = 0; i < argv.length; i++) {
132
133
  const arg = argv[i];
133
- if (arg === "--llm" || arg === "--verbose") continue;
134
+ if (arg === "--verbose") continue;
134
135
  if (
135
136
  arg?.startsWith("--format=") ||
136
- arg?.startsWith("--llm=") ||
137
137
  arg?.startsWith("--verbose=") ||
138
138
  arg?.startsWith("--detail=")
139
139
  ) {
@@ -195,8 +195,10 @@ export function formatRootHelp(
195
195
  // never these strings.
196
196
  lines.push(helpHeading("Global flags"));
197
197
  const flags: [string, string][] = [
198
- ["--llm", "Condensed Markdown output for agents"],
199
- ["--format <json|plain>", "Select output format"],
198
+ [
199
+ "--format <plain|llm|json>",
200
+ "Select output format (llm = condensed Markdown for agents)",
201
+ ],
200
202
  [
201
203
  "--detail <level>",
202
204
  "Progressive-disclosure level (summary, standard, detailed)",
@@ -38,7 +38,7 @@ export function buildInstructions(
38
38
 
39
39
  return [
40
40
  "pragma is a CLI and MCP server over a design-system knowledge graph.",
41
- `${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying}`,
41
+ `${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying} ${CONVENTIONS.mutations}`,
42
42
  "",
43
43
  "Discovery sequence:",
44
44
  steps,