@canonical/pragma-cli 0.30.0 → 0.31.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 (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -48,6 +48,26 @@ const mixed: DoctorData = {
48
48
  skipped: 1,
49
49
  };
50
50
 
51
+ const withItems: DoctorData = {
52
+ checks: [
53
+ {
54
+ name: "package refs",
55
+ status: "pass",
56
+ detail: "1 package resolved",
57
+ items: [
58
+ {
59
+ label: "@canonical/design-system",
60
+ status: "pass",
61
+ detail: "git v0.1.2 · 362 graphs, 3 skills",
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ passed: 1,
67
+ failed: 0,
68
+ skipped: 0,
69
+ };
70
+
51
71
  describe("doctor formatters", () => {
52
72
  describe("plain", () => {
53
73
  it("renders passing checks with ✓", () => {
@@ -74,6 +94,29 @@ describe("doctor formatters", () => {
74
94
  const output = formatters.plain(allPass);
75
95
  expect(output).not.toContain("Run ");
76
96
  });
97
+
98
+ it("renders a summary tally", () => {
99
+ const output = formatters.plain(mixed);
100
+ expect(output).toContain("1 passed");
101
+ expect(output).toContain("2 failed");
102
+ expect(output).toContain("1 skipped");
103
+ });
104
+
105
+ it("renders sub-items indented under a check", () => {
106
+ const output = formatters.plain(withItems);
107
+ expect(output).toContain("@canonical/design-system");
108
+ expect(output).toContain("git v0.1.2 · 362 graphs, 3 skills");
109
+ // sub-item lines are indented beneath the check headline
110
+ const lines = output.split("\n");
111
+ const sub = lines.find((l) => l.includes("@canonical/design-system"));
112
+ expect(sub?.startsWith(" ")).toBe(true);
113
+ });
114
+
115
+ it("renders the remedy inline with a fix label", () => {
116
+ const output = formatters.plain(mixed);
117
+ expect(output).toContain("fix:");
118
+ expect(output).toContain("pragma config tier <tier>");
119
+ });
77
120
  });
78
121
 
79
122
  describe("llm", () => {
@@ -87,12 +130,17 @@ describe("doctor formatters", () => {
87
130
  expect(output).toContain("- ✓ **Node version**: v24.1.0");
88
131
  });
89
132
 
90
- it("includes remedial section for failures", () => {
133
+ it("includes inline fixes for failures", () => {
91
134
  const output = formatters.llm(mixed);
92
- expect(output).toContain("### Remedial");
135
+ expect(output).toContain("_fix:_");
93
136
  expect(output).toContain("`pragma config tier <tier>`");
94
137
  expect(output).toContain("`pragma setup completions`");
95
138
  });
139
+
140
+ it("renders sub-items as nested bullets", () => {
141
+ const output = formatters.llm(withItems);
142
+ expect(output).toContain(" - ✓ @canonical/design-system");
143
+ });
96
144
  });
97
145
 
98
146
  describe("json", () => {
@@ -1,78 +1,130 @@
1
1
  import chalk from "chalk";
2
2
  import type { Formatters } from "../../shared/formatters.js";
3
- import type { CheckResult, DoctorData } from "../operations/types.js";
3
+ import type {
4
+ CheckResult,
5
+ CheckStatus,
6
+ DoctorData,
7
+ } from "../operations/types.js";
4
8
 
5
- /** Status icons for pass/fail/skip check results. */
6
- const STATUS_ICONS = {
9
+ /** Status icons for pass/fail/skip results (checks and sub-items). */
10
+ const STATUS_ICONS: Record<CheckStatus, string> = {
7
11
  pass: chalk.green("✓"),
8
12
  fail: chalk.red("✗"),
9
13
  skip: chalk.yellow("○"),
10
- } as const;
14
+ };
15
+
16
+ const SUB_BULLET = chalk.dim("·");
17
+ const FIX_ARROW = chalk.cyan("↳");
18
+ const INDENT = " "; // aligns sub-lines under the check name
11
19
 
12
20
  /**
13
- * Format a single check result as a styled terminal line.
21
+ * Render one check as terminal lines: a headline row (icon, aligned name,
22
+ * detail), an optional indented breakdown of sub-items, and — for failures —
23
+ * an inline remedial instruction.
14
24
  *
15
- * @param check - The check result to format.
16
- * @returns A single-line string with status icon, name, and detail.
25
+ * @param check - The check result to render.
26
+ * @param nameWidth - Width to pad check names to so details align in a column.
27
+ * @returns The lines for this check.
17
28
  */
18
- function formatCheckPlain(check: CheckResult): string {
19
- return `${STATUS_ICONS[check.status]} ${check.name} ${chalk.dim(check.detail)}`;
29
+ function formatCheckPlain(check: CheckResult, nameWidth: number): string[] {
30
+ const name = check.name.padEnd(nameWidth);
31
+ const label = check.status === "fail" ? chalk.red(name) : chalk.bold(name);
32
+ const lines = [
33
+ ` ${STATUS_ICONS[check.status]} ${label} ${chalk.dim(check.detail)}`,
34
+ ];
35
+
36
+ if (check.items && check.items.length > 0) {
37
+ const itemWidth = Math.max(...check.items.map((i) => i.label.length));
38
+ const anyStatus = check.items.some((i) => i.status !== undefined);
39
+ for (const item of check.items) {
40
+ const icon = anyStatus
41
+ ? item.status
42
+ ? `${STATUS_ICONS[item.status]} `
43
+ : " "
44
+ : "";
45
+ const itemLabel = item.detail ? item.label.padEnd(itemWidth) : item.label;
46
+ const detail = item.detail ? ` ${chalk.dim(item.detail)}` : "";
47
+ lines.push(`${INDENT}${SUB_BULLET} ${icon}${itemLabel}${detail}`);
48
+ }
49
+ }
50
+
51
+ if (check.status === "fail" && check.remedy) {
52
+ lines.push(`${INDENT}${FIX_ARROW} ${chalk.cyan("fix:")} ${check.remedy}`);
53
+ }
54
+
55
+ return lines;
56
+ }
57
+
58
+ /** Render the pass/fail/skip tally, coloring non-zero fail/skip. */
59
+ function formatSummary(data: DoctorData): string {
60
+ const parts = [chalk.green(`${data.passed} passed`)];
61
+ parts.push(
62
+ data.failed > 0
63
+ ? chalk.red(`${data.failed} failed`)
64
+ : chalk.dim(`${data.failed} failed`),
65
+ );
66
+ parts.push(
67
+ data.skipped > 0
68
+ ? chalk.yellow(`${data.skipped} skipped`)
69
+ : chalk.dim(`${data.skipped} skipped`),
70
+ );
71
+ return ` ${parts.join(chalk.dim(" · "))}`;
20
72
  }
21
73
 
22
74
  /**
23
75
  * Formatters for `pragma doctor` output.
24
76
  *
25
- * - **plain** renders a check list with chalk icons and remedial commands.
26
- * - **llm** renders a markdown list with remedial section.
27
- * - **json** serializes the raw DoctorData.
77
+ * - **plain** renders an aligned check list with indented sub-items and
78
+ * inline remedies, followed by a summary tally.
79
+ * - **llm** renders a Markdown list with nested sub-items and inline fixes.
80
+ * - **json** serializes the raw DoctorData (including `items`).
28
81
  */
29
82
  const formatters: Formatters<DoctorData> = {
30
83
  plain(data) {
31
- const lines: string[] = [];
84
+ const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
85
+ const lines: string[] = [chalk.bold("pragma doctor"), ""];
32
86
 
33
87
  for (const check of data.checks) {
34
- lines.push(formatCheckPlain(check));
88
+ lines.push(...formatCheckPlain(check, nameWidth));
35
89
  }
36
90
 
37
- const remedies = data.checks.filter(
38
- (c): c is CheckResult & { remedy: string } =>
39
- c.status === "fail" && c.remedy !== undefined,
40
- );
41
-
42
- if (remedies.length > 0) {
43
- lines.push("");
44
- for (const r of remedies) {
45
- lines.push(
46
- `Run ${chalk.cyan(`\`${r.remedy}\``)} to fix ${chalk.dim(r.name)}.`,
47
- );
48
- }
49
- }
91
+ lines.push("");
92
+ lines.push(formatSummary(data));
50
93
 
51
94
  return lines.join("\n");
52
95
  },
53
96
 
54
97
  llm(data) {
55
- const lines: string[] = [];
56
-
57
- lines.push("## Doctor");
58
- lines.push("");
98
+ const lines: string[] = ["## Doctor", ""];
59
99
 
60
100
  for (const check of data.checks) {
61
101
  const icon =
62
102
  check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "○";
63
103
  lines.push(`- ${icon} **${check.name}**: ${check.detail}`);
64
- }
65
104
 
66
- const remedies = data.checks.filter((c) => c.status === "fail" && c.remedy);
105
+ for (const item of check.items ?? []) {
106
+ const itemIcon =
107
+ item.status === "pass"
108
+ ? "✓ "
109
+ : item.status === "fail"
110
+ ? "✗ "
111
+ : item.status === "skip"
112
+ ? "○ "
113
+ : "";
114
+ const detail = item.detail ? `: ${item.detail}` : "";
115
+ lines.push(` - ${itemIcon}${item.label}${detail}`);
116
+ }
67
117
 
68
- if (remedies.length > 0) {
69
- lines.push("");
70
- lines.push("### Remedial");
71
- for (const r of remedies) {
72
- lines.push(`- \`${r.remedy}\``);
118
+ if (check.status === "fail" && check.remedy) {
119
+ lines.push(` - _fix:_ \`${check.remedy}\``);
73
120
  }
74
121
  }
75
122
 
123
+ lines.push("");
124
+ lines.push(
125
+ `_${data.passed} passed, ${data.failed} failed, ${data.skipped} skipped_`,
126
+ );
127
+
76
128
  return lines.join("\n");
77
129
  },
78
130
 
@@ -3,7 +3,7 @@ import { delimiter, isAbsolute, join } from "node:path";
3
3
  import type { DetectedHarness } from "@canonical/harnesses";
4
4
  import { detectHarnesses, readMcpConfig } from "@canonical/harnesses";
5
5
  import { runTask } from "@canonical/task/node";
6
- import type { CheckContext, CheckResult } from "../types.js";
6
+ import type { CheckContext, CheckItem, CheckResult } from "../types.js";
7
7
 
8
8
  const NAME = "MCP commands";
9
9
 
@@ -79,7 +79,7 @@ export default async function checkMcpCommands(
79
79
  };
80
80
  }
81
81
 
82
- const broken: string[] = [];
82
+ const broken: CheckItem[] = [];
83
83
  let checked = 0;
84
84
 
85
85
  for (const d of withConfig) {
@@ -97,9 +97,11 @@ export default async function checkMcpCommands(
97
97
 
98
98
  checked += 1;
99
99
  if (!commandResolves(command, ctx.cwd)) {
100
- broken.push(
101
- `"${serverName}" ("${command}" not found, ${d.configPath})`,
102
- );
100
+ broken.push({
101
+ label: `"${serverName}"`,
102
+ status: "fail",
103
+ detail: `"${command}" not found · ${d.configPath}`,
104
+ });
103
105
  }
104
106
  }
105
107
  }
@@ -123,7 +125,8 @@ export default async function checkMcpCommands(
123
125
  return {
124
126
  name: NAME,
125
127
  status: "fail",
126
- detail: `unresolvable: ${broken.join("; ")}`,
128
+ detail: `${broken.length} of ${checked} unresolvable`,
129
+ items: broken,
127
130
  remedy:
128
131
  "Install the missing command or remove the entry from the MCP config — every agent session tries and fails to boot it.",
129
132
  };
@@ -13,7 +13,12 @@ import {
13
13
  } from "../../../shared/loaders/index.js";
14
14
  import { mergeAndParseRefs } from "../../../shared/mergeAndParseRefs.js";
15
15
  import { resolveSemanticPackages } from "../../../shared/semanticPackage.js";
16
- import type { CheckContext, CheckResult } from "../types.js";
16
+ import type { CheckContext, CheckItem, CheckResult } from "../types.js";
17
+
18
+ /** Pluralize a count, e.g. `1 graph` / `2 graphs`. */
19
+ function count(n: number, unit: string): string {
20
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
21
+ }
17
22
 
18
23
  export default async function checkPackageRefs(
19
24
  ctx: CheckContext,
@@ -27,32 +32,45 @@ export default async function checkPackageRefs(
27
32
  ];
28
33
  const packages = await resolveSemanticPackages(refs, loaders);
29
34
 
30
- const details: string[] = [];
31
- let hasIssue = false;
35
+ const items: CheckItem[] = [];
36
+ let unresolved = 0;
32
37
 
33
38
  for (const ref of refs) {
34
39
  const resolved = packages.find((p) => p.name === ref.pkg);
35
40
 
36
41
  if (resolved) {
37
- details.push(
38
- `${ref.pkg}: ${resolved.source} v${resolved.version} (${resolved.graphs.length} graphs, ${resolved.skills.length} skills)`,
39
- );
42
+ items.push({
43
+ label: ref.pkg,
44
+ status: "pass",
45
+ detail: `${resolved.source} v${resolved.version} · ${count(resolved.graphs.length, "graph")}, ${count(resolved.skills.length, "skill")}`,
46
+ });
40
47
  } else {
41
48
  // Check if covered by bundled fallback
42
49
  const bundled = packages.find((p) => p.name === "(bundled)");
43
50
  if (bundled) {
44
- details.push(`${ref.pkg}: bundled v${bundled.version}`);
51
+ items.push({
52
+ label: ref.pkg,
53
+ status: "pass",
54
+ detail: `bundled fallback v${bundled.version}`,
55
+ });
45
56
  } else {
46
- details.push(`${ref.pkg}: NOT RESOLVED`);
47
- hasIssue = true;
57
+ items.push({ label: ref.pkg, status: "fail", detail: "not resolved" });
58
+ unresolved += 1;
48
59
  }
49
60
  }
50
61
  }
51
62
 
63
+ const total = refs.length;
64
+ const detail =
65
+ unresolved > 0
66
+ ? `${unresolved} of ${count(total, "package")} not resolved`
67
+ : `${count(total, "package")} resolved`;
68
+
52
69
  return {
53
70
  name: "package refs",
54
- status: hasIssue ? "fail" : "pass",
55
- detail: details.join("; "),
56
- ...(hasIssue ? { remedy: "pragma update-refs" } : {}),
71
+ status: unresolved > 0 ? "fail" : "pass",
72
+ detail,
73
+ items,
74
+ ...(unresolved > 0 ? { remedy: "pragma update-refs" } : {}),
57
75
  };
58
76
  }
@@ -268,8 +268,9 @@ describe("checkMcpCommands", () => {
268
268
 
269
269
  const result = await checkMcpCommands(ctx);
270
270
  expect(result.status).toBe("fail");
271
- expect(result.detail).toContain('"sem"');
272
- expect(result.detail).toContain("/test/.mcp.json");
271
+ const semItem = result.items?.find((i) => i.label === '"sem"');
272
+ expect(semItem).toBeDefined();
273
+ expect(semItem?.detail).toContain("/test/.mcp.json");
273
274
  expect(result.remedy).toBeDefined();
274
275
  });
275
276
 
@@ -291,8 +292,9 @@ describe("checkMcpCommands", () => {
291
292
 
292
293
  const result = await checkMcpCommands(ctx);
293
294
  expect(result.status).toBe("fail");
294
- expect(result.detail).toContain('"sem"');
295
- expect(result.detail).not.toContain('"pragma"');
295
+ const labels = (result.items ?? []).map((i) => i.label);
296
+ expect(labels).toContain('"sem"');
297
+ expect(labels).not.toContain('"pragma"');
296
298
  });
297
299
  });
298
300
 
@@ -1,8 +1,30 @@
1
+ /** Status of a doctor check or one of its sub-items. */
2
+ export type CheckStatus = "pass" | "fail" | "skip";
3
+
4
+ /**
5
+ * A structured sub-item under a check — e.g. one resolved package under
6
+ * `package refs`, or one unresolvable server under `MCP commands`. Lets the
7
+ * formatter render an indented, aligned breakdown instead of cramming
8
+ * everything into a single `detail` string.
9
+ */
10
+ export interface CheckItem {
11
+ /** Left-column label (e.g. the package or server name). */
12
+ readonly label: string;
13
+ /** Right-column detail (e.g. `git v0.1.2 · 362 graphs`). */
14
+ readonly detail?: string;
15
+ /** Optional per-item status, rendered as its own icon. */
16
+ readonly status?: CheckStatus;
17
+ }
18
+
1
19
  /** Result of a single doctor check. */
2
20
  export interface CheckResult {
3
21
  readonly name: string;
4
- readonly status: "pass" | "fail" | "skip";
22
+ readonly status: CheckStatus;
23
+ /** One-line headline shown next to the check name. */
5
24
  readonly detail: string;
25
+ /** Optional structured breakdown, rendered as indented sub-items. */
26
+ readonly items?: readonly CheckItem[];
27
+ /** Remedial instruction shown inline under a failing check. */
6
28
  readonly remedy?: string;
7
29
  }
8
30
 
@@ -2,7 +2,7 @@ import type { CommandDefinition } from "@canonical/cli-core";
2
2
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
3
  import createTestRuntime from "../../../testing/helpers/createTestRuntime.js";
4
4
  import type { PragmaContext } from "../../shared/context.js";
5
- import { PREFIX_MAP } from "../../shared/prefixes.js";
5
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
6
6
  import type { PragmaRuntime } from "../../shared/runtime.js";
7
7
  import type { InspectResult } from "../../shared/types/index.js";
8
8
  import buildInspectCommand from "./inspect.js";
@@ -37,7 +37,7 @@ async function executeOutput(
37
37
  }
38
38
 
39
39
  describe("graph inspect command", () => {
40
- const uri = `${PREFIX_MAP.ds}global.component.button`;
40
+ const uri = `${DEFAULT_PREFIX_MAP.ds}global.component.button`;
41
41
 
42
42
  it("returns inspect results", async () => {
43
43
  const ctx = makeCtx();
@@ -58,15 +58,17 @@ describe("graph query command", () => {
58
58
  expect(text).toContain("n");
59
59
  });
60
60
 
61
- it("renders llm output", async () => {
61
+ it("renders llm output as a Markdown table for SELECT", async () => {
62
62
  const ctx = makeCtx({
63
63
  globalFlags: { llm: true, format: "text" as const, verbose: false },
64
64
  });
65
65
  const cmd = buildQueryCommand(ctx);
66
66
  const { text } = await executeOutput(cmd, { sparql }, ctx);
67
67
 
68
- const parsed = JSON.parse(text) as QueryResult;
69
- expect(parsed.type).toBe("select");
68
+ // Condensed Markdown, not a raw JSON dump.
69
+ expect(text).toContain("| n |");
70
+ expect(text).toContain("| --- |");
71
+ expect(() => JSON.parse(text)).toThrow();
70
72
  });
71
73
 
72
74
  it("renders json output", async () => {
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { formatHeading } from "#pipeline";
3
3
  import compactUri from "../../shared/compactUri.js";
4
4
  import type { Formatters } from "../../shared/formatters.js";
5
- import { PREFIX_MAP } from "../../shared/prefixes.js";
5
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
6
6
  import type { InspectResult } from "../../shared/types/index.js";
7
7
 
8
8
  /**
@@ -15,13 +15,13 @@ import type { InspectResult } from "../../shared/types/index.js";
15
15
  const formatters: Formatters<InspectResult> = {
16
16
  plain(result) {
17
17
  const lines: string[] = [];
18
- lines.push(formatHeading(compactUri(result.uri, PREFIX_MAP)));
18
+ lines.push(formatHeading(compactUri(result.uri, DEFAULT_PREFIX_MAP)));
19
19
 
20
20
  for (const g of result.groups) {
21
21
  lines.push("");
22
- lines.push(chalk.bold(compactUri(g.predicate, PREFIX_MAP)));
22
+ lines.push(chalk.bold(compactUri(g.predicate, DEFAULT_PREFIX_MAP)));
23
23
  for (const o of g.objects) {
24
- lines.push(` ${compactUri(o, PREFIX_MAP)}`);
24
+ lines.push(` ${compactUri(o, DEFAULT_PREFIX_MAP)}`);
25
25
  }
26
26
  }
27
27
 
@@ -30,13 +30,13 @@ const formatters: Formatters<InspectResult> = {
30
30
 
31
31
  llm(result) {
32
32
  const lines = [
33
- `## ${compactUri(result.uri, PREFIX_MAP)} (${result.uri})`,
33
+ `## ${compactUri(result.uri, DEFAULT_PREFIX_MAP)} (${result.uri})`,
34
34
  "",
35
35
  ];
36
36
  for (const g of result.groups) {
37
- lines.push(`**${compactUri(g.predicate, PREFIX_MAP)}:**`);
37
+ lines.push(`**${compactUri(g.predicate, DEFAULT_PREFIX_MAP)}:**`);
38
38
  for (const o of g.objects) {
39
- lines.push(` - ${compactUri(o, PREFIX_MAP)}`);
39
+ lines.push(` - ${compactUri(o, DEFAULT_PREFIX_MAP)}`);
40
40
  }
41
41
  }
42
42
  return lines.join("\n");
@@ -0,0 +1,87 @@
1
+ import type { QueryResult } from "@canonical/ke";
2
+ import { describe, expect, it } from "vitest";
3
+ import formatters from "./query.js";
4
+
5
+ const selectResult = {
6
+ type: "select",
7
+ bindings: [
8
+ { s: "a", n: "1" },
9
+ { s: "b", n: "2" },
10
+ ],
11
+ } as unknown as QueryResult;
12
+
13
+ const emptySelect = {
14
+ type: "select",
15
+ bindings: [],
16
+ } as unknown as QueryResult;
17
+
18
+ const askResult = { type: "ask", result: true } as unknown as QueryResult;
19
+
20
+ const constructResult = {
21
+ type: "construct",
22
+ triples: [
23
+ { subject: "s1", predicate: "p1", object: "line one\nline two" },
24
+ { subject: "s2", predicate: "p2", object: "o2" },
25
+ ],
26
+ } as unknown as QueryResult;
27
+
28
+ const emptyConstruct = {
29
+ type: "construct",
30
+ triples: [],
31
+ } as unknown as QueryResult;
32
+
33
+ describe("graph query formatters — plain", () => {
34
+ it("renders SELECT as a tab table", () => {
35
+ const text = formatters.plain(selectResult);
36
+ expect(text.split("\n")).toEqual(["s\tn", "a\t1", "b\t2"]);
37
+ });
38
+
39
+ it("reports no results for an empty SELECT", () => {
40
+ expect(formatters.plain(emptySelect)).toBe("No results.");
41
+ });
42
+
43
+ it("renders ASK as a boolean", () => {
44
+ expect(formatters.plain(askResult)).toBe("ASK: true");
45
+ });
46
+
47
+ it("renders CONSTRUCT as one-line triples", () => {
48
+ const text = formatters.plain(constructResult);
49
+ expect(text).toContain("s1\tp1\tline one line two");
50
+ expect(text).toContain("s2\tp2\to2");
51
+ });
52
+
53
+ it("reports no triples for an empty CONSTRUCT", () => {
54
+ expect(formatters.plain(emptyConstruct)).toBe("No triples.");
55
+ });
56
+ });
57
+
58
+ describe("graph query formatters — llm", () => {
59
+ it("renders SELECT as a Markdown table", () => {
60
+ const text = formatters.llm(selectResult);
61
+ expect(text).toContain("| s | n |");
62
+ expect(text).toContain("| --- | --- |");
63
+ expect(text).toContain("| a | 1 |");
64
+ });
65
+
66
+ it("renders an empty SELECT as italic no-results", () => {
67
+ expect(formatters.llm(emptySelect)).toBe("_No results._");
68
+ });
69
+
70
+ it("renders ASK as bold", () => {
71
+ expect(formatters.llm(askResult)).toBe("**ASK** → true");
72
+ });
73
+
74
+ it("renders CONSTRUCT triples as a bullet list", () => {
75
+ const text = formatters.llm(constructResult);
76
+ expect(text).toContain("- s1\tp1\tline one line two");
77
+ });
78
+ });
79
+
80
+ describe("graph query formatters — json", () => {
81
+ it("serializes the raw result", () => {
82
+ expect(JSON.parse(formatters.json(askResult))).toMatchObject({
83
+ type: "ask",
84
+ result: true,
85
+ });
86
+ });
87
+ });
@@ -1,12 +1,33 @@
1
1
  import type { QueryResult } from "@canonical/ke";
2
2
  import type { Formatters } from "../../shared/formatters.js";
3
3
 
4
+ /**
5
+ * Render CONSTRUCT triples as readable `subject predicate object` lines,
6
+ * collapsing any newlines in literal objects so each triple stays on one line.
7
+ */
8
+ function formatTriples(
9
+ triples: ReadonlyArray<{
10
+ subject: string;
11
+ predicate: string;
12
+ object: string;
13
+ }>,
14
+ ): string {
15
+ if (triples.length === 0) return "No triples.";
16
+ return triples
17
+ .map((t) => {
18
+ const object = t.object.replace(/\s*\n\s*/g, " ").trim();
19
+ return `${t.subject}\t${t.predicate}\t${object}`;
20
+ })
21
+ .join("\n");
22
+ }
23
+
4
24
  /**
5
25
  * Formatters for `pragma graph query` output.
6
26
  *
7
- * - **plain**: tab-separated table for SELECT results, `ASK: true/false` for ASK
8
- * queries, JSON fallback for CONSTRUCT.
9
- * - **llm**: indented JSON for all query types.
27
+ * - **plain**: tab-separated table for SELECT, `ASK: true/false` for ASK, and
28
+ * readable triple lines for CONSTRUCT.
29
+ * - **llm**: condensed Markdown (table for SELECT, one line for ASK, a triple
30
+ * list for CONSTRUCT) rather than a raw JSON dump.
10
31
  * - **json**: serialized {@link QueryResult} as indented JSON.
11
32
  */
12
33
  const formatters: Formatters<QueryResult> = {
@@ -22,11 +43,27 @@ const formatters: Formatters<QueryResult> = {
22
43
  if (result.type === "ask") {
23
44
  return `ASK: ${String(result.result)}`;
24
45
  }
25
- return JSON.stringify(result, null, 2);
46
+ return formatTriples(result.triples);
26
47
  },
27
48
 
28
49
  llm(result) {
29
- return JSON.stringify(result, null, 2);
50
+ if (result.type === "select") {
51
+ if (result.bindings.length === 0) return "_No results._";
52
+ const cols = Object.keys(result.bindings[0] ?? {});
53
+ const header = `| ${cols.join(" | ")} |`;
54
+ const divider = `| ${cols.map(() => "---").join(" | ")} |`;
55
+ const rows = result.bindings.map(
56
+ (b) => `| ${cols.map((c) => b[c] ?? "").join(" | ")} |`,
57
+ );
58
+ return [header, divider, ...rows].join("\n");
59
+ }
60
+ if (result.type === "ask") {
61
+ return `**ASK** → ${String(result.result)}`;
62
+ }
63
+ return formatTriples(result.triples)
64
+ .split("\n")
65
+ .map((line) => (line.includes("\t") ? `- ${line}` : line))
66
+ .join("\n");
30
67
  },
31
68
 
32
69
  json(result) {