@canonical/pragma-cli 0.18.0 → 0.21.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 (200) hide show
  1. package/LICENSE +166 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +31 -18
  4. package/src/constants.ts +1 -1
  5. package/src/domains/block/blockConfig.ts +47 -3
  6. package/src/domains/block/commands/list.test.ts +1 -1
  7. package/src/domains/block/commands/list.ts +27 -0
  8. package/src/domains/block/commands/lookup.ts +26 -52
  9. package/src/domains/block/formatters/index.ts +1 -1
  10. package/src/domains/block/formatters/list.test.ts +1 -1
  11. package/src/domains/block/formatters/list.ts +28 -42
  12. package/src/domains/block/formatters/lookup.test.ts +3 -2
  13. package/src/domains/block/formatters/lookup.ts +91 -296
  14. package/src/domains/block/formatters/types.ts +1 -1
  15. package/src/domains/block/operations/list.test.ts +1 -1
  16. package/src/domains/block/operations/list.ts +1 -1
  17. package/src/domains/block/operations/lookup.test.ts +29 -8
  18. package/src/domains/block/operations/lookup.ts +94 -16
  19. package/src/domains/block/orchestration/blockEmptyError.ts +1 -1
  20. package/src/domains/block/orchestration/buildBlockFilters.ts +1 -1
  21. package/src/domains/block/orchestration/enrichBlocks.ts +6 -2
  22. package/src/domains/block/orchestration/resolveBlockList.ts +1 -1
  23. package/src/domains/block/orchestration/resolveBlockLookup.ts +8 -2
  24. package/src/domains/block/types.ts +1 -1
  25. package/src/domains/config/commands/channel.ts +6 -4
  26. package/src/domains/config/commands/tier.ts +6 -4
  27. package/src/domains/config/mcp/specs.ts +18 -6
  28. package/src/domains/config/operations/validateTier.ts +1 -1
  29. package/src/domains/config/tasks/index.ts +3 -0
  30. package/src/domains/config/tasks/setChannel.ts +30 -0
  31. package/src/domains/config/tasks/setTier.ts +33 -0
  32. package/src/domains/create/commands/package.test.ts +2 -2
  33. package/src/domains/doctor/mcp/index.ts +1 -1
  34. package/src/domains/doctor/mcp/specs.ts +2 -49
  35. package/src/domains/graph/commands/inspect.test.ts +1 -1
  36. package/src/domains/graph/formatters/inspect.ts +12 -7
  37. package/src/domains/graph/operations/inspectUri.ts +4 -1
  38. package/src/domains/info/commands/upgrade.ts +6 -3
  39. package/src/domains/info/index.ts +3 -1
  40. package/src/domains/info/mcp/index.ts +2 -0
  41. package/src/domains/info/mcp/specs.ts +65 -0
  42. package/src/domains/llm/data/commandReference.ts +4 -4
  43. package/src/domains/llm/data/decisionTrees.ts +14 -12
  44. package/src/domains/llm/operations/collectContext.ts +1 -1
  45. package/src/domains/modifier/commands/list.ts +9 -0
  46. package/src/domains/modifier/commands/lookup.ts +22 -42
  47. package/src/domains/modifier/formatters/index.ts +4 -1
  48. package/src/domains/modifier/formatters/list.test.ts +1 -1
  49. package/src/domains/modifier/formatters/list.ts +13 -26
  50. package/src/domains/modifier/formatters/lookup.test.ts +1 -1
  51. package/src/domains/modifier/formatters/lookup.ts +41 -29
  52. package/src/domains/modifier/index.ts +1 -0
  53. package/src/domains/modifier/modifierConfig.ts +36 -0
  54. package/src/domains/modifier/operations/list.ts +1 -1
  55. package/src/domains/modifier/operations/lookup.ts +4 -3
  56. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +1 -1
  57. package/src/domains/modifier/types.ts +1 -1
  58. package/src/domains/ontology/commands/list.test.ts +1 -1
  59. package/src/domains/ontology/commands/list.ts +18 -0
  60. package/src/domains/ontology/commands/show.test.ts +1 -1
  61. package/src/domains/ontology/formatters/list.ts +5 -3
  62. package/src/domains/ontology/formatters/show.ts +38 -13
  63. package/src/domains/ontology/helpers/queryClasses.ts +1 -1
  64. package/src/domains/ontology/helpers/queryProperties.ts +1 -1
  65. package/src/domains/ontology/operations/listOntologies.ts +1 -1
  66. package/src/domains/ontology/operations/showOntology.ts +1 -1
  67. package/src/domains/setup/helpers/completionScripts.test.ts +3 -3
  68. package/src/domains/setup/helpers/completionScripts.ts +4 -4
  69. package/src/domains/shared/ToolSpec.ts +1 -1
  70. package/src/domains/shared/context.ts +3 -2
  71. package/src/domains/shared/contracts.ts +40 -1
  72. package/src/domains/shared/filters/buildFilterConfig.ts +1 -2
  73. package/src/domains/shared/filters/buildFilters.ts +1 -1
  74. package/src/domains/shared/filters/describeFilters.ts +1 -1
  75. package/src/domains/shared/formatters.ts +47 -0
  76. package/src/domains/shared/renderers.ts +410 -0
  77. package/src/domains/shared/runtime.ts +7 -22
  78. package/src/domains/shared/types/block.ts +107 -0
  79. package/src/domains/shared/types/common.ts +33 -0
  80. package/src/domains/shared/types/graph.ts +17 -0
  81. package/src/domains/shared/types/index.ts +49 -0
  82. package/src/domains/shared/types/modifier.ts +22 -0
  83. package/src/domains/shared/types/ontology.ts +50 -0
  84. package/src/domains/shared/types/runtime.ts +21 -0
  85. package/src/domains/shared/types/standard.ts +64 -0
  86. package/src/domains/shared/types/tier.ts +15 -0
  87. package/src/domains/shared/types/token.ts +30 -0
  88. package/src/domains/standard/commands/list.ts +9 -0
  89. package/src/domains/standard/commands/lookup.ts +22 -50
  90. package/src/domains/standard/formatters/categories.test.ts +1 -1
  91. package/src/domains/standard/formatters/categories.ts +1 -1
  92. package/src/domains/standard/formatters/index.ts +4 -1
  93. package/src/domains/standard/formatters/list.test.ts +4 -1
  94. package/src/domains/standard/formatters/list.ts +112 -84
  95. package/src/domains/standard/formatters/lookup.test.ts +1 -1
  96. package/src/domains/standard/formatters/lookup.ts +85 -70
  97. package/src/domains/standard/formatters/types.ts +1 -1
  98. package/src/domains/standard/index.ts +1 -0
  99. package/src/domains/standard/operations/categories.ts +1 -1
  100. package/src/domains/standard/operations/list.ts +1 -1
  101. package/src/domains/standard/operations/lookup.ts +1 -1
  102. package/src/domains/standard/orchestration/buildStandardFilters.ts +1 -1
  103. package/src/domains/standard/orchestration/resolveStandardList.ts +4 -1
  104. package/src/domains/standard/orchestration/resolveStandardLookup.ts +1 -1
  105. package/src/domains/standard/orchestration/standardEmptyError.ts +1 -1
  106. package/src/domains/standard/standardConfig.ts +42 -0
  107. package/src/domains/standard/types.ts +1 -1
  108. package/src/domains/tier/commands/list.ts +16 -0
  109. package/src/domains/tier/formatters/list.test.ts +1 -1
  110. package/src/domains/tier/formatters/list.ts +11 -6
  111. package/src/domains/tier/operations/list.ts +1 -1
  112. package/src/domains/token/commands/list.ts +9 -0
  113. package/src/domains/token/commands/lookup.ts +22 -48
  114. package/src/domains/token/formatters/index.ts +4 -1
  115. package/src/domains/token/formatters/list.test.ts +1 -1
  116. package/src/domains/token/formatters/list.ts +15 -28
  117. package/src/domains/token/formatters/lookup.test.ts +2 -2
  118. package/src/domains/token/formatters/lookup.ts +80 -45
  119. package/src/domains/token/index.ts +1 -0
  120. package/src/domains/token/operations/list.ts +1 -1
  121. package/src/domains/token/operations/lookup.ts +1 -1
  122. package/src/domains/token/orchestration/resolveTokenLookup.ts +1 -1
  123. package/src/domains/token/tokenConfig.ts +38 -0
  124. package/src/domains/token/types.ts +1 -1
  125. package/src/index.ts +1 -1
  126. package/src/mcp/runMcpServer.test.ts +2 -2
  127. package/src/mcp/tools/index.ts +2 -0
  128. package/src/mcp/tools/registerTools.test.ts +8 -3
  129. package/src/package-manager/types.ts +1 -3
  130. package/src/pipeline/createProgram.ts +20 -1
  131. package/src/pipeline/index.ts +4 -1
  132. package/src/pipeline/parseGlobalFlags.test.ts +92 -0
  133. package/src/pipeline/parseGlobalFlags.ts +25 -0
  134. package/src/pipeline/runCli.orchestration.test.ts +1 -0
  135. package/src/pipeline/runCli.ts +11 -4
  136. package/src/pipeline/runInteractiveCommand.ts +198 -0
  137. package/src/testing/cli.ts +2 -2
  138. package/src/testing/fixtures.ts +2 -2
  139. package/src/testing/integration/boot.test.ts +2 -2
  140. package/src/testing/integration/cli-user-stories.test.ts +198 -0
  141. package/src/testing/integration/edge-cases.test.ts +263 -0
  142. package/src/testing/integration/parity.test.ts +377 -27
  143. package/src/testing/store.ts +1 -1
  144. package/src/tui/components/BulletList/BulletList.tsx +46 -0
  145. package/src/tui/components/BulletList/index.ts +2 -0
  146. package/src/tui/components/BulletList/types.ts +6 -0
  147. package/src/tui/components/Card/Card.tsx +73 -0
  148. package/src/tui/components/Card/index.ts +1 -0
  149. package/src/tui/components/Card/types.ts +12 -0
  150. package/src/tui/components/CodeBlock/CodeBlock.tsx +37 -0
  151. package/src/tui/components/CodeBlock/index.ts +2 -0
  152. package/src/tui/components/CodeBlock/types.ts +6 -0
  153. package/src/tui/components/DataRow/DataRow.tsx +80 -0
  154. package/src/tui/components/DataRow/index.ts +2 -0
  155. package/src/tui/components/DataRow/types.ts +14 -0
  156. package/src/tui/components/FieldRow/FieldRow.tsx +24 -0
  157. package/src/tui/components/FieldRow/index.ts +2 -0
  158. package/src/tui/components/FieldRow/types.ts +10 -0
  159. package/src/tui/components/HeaderRow/HeaderRow.tsx +32 -0
  160. package/src/tui/components/HeaderRow/index.ts +2 -0
  161. package/src/tui/components/HeaderRow/types.ts +8 -0
  162. package/src/tui/components/MarkdownText/MarkdownText.tsx +19 -0
  163. package/src/tui/components/MarkdownText/index.ts +2 -0
  164. package/src/tui/components/MarkdownText/types.ts +4 -0
  165. package/src/tui/components/MiniTable/MiniTable.tsx +82 -0
  166. package/src/tui/components/MiniTable/index.ts +2 -0
  167. package/src/tui/components/MiniTable/types.ts +6 -0
  168. package/src/tui/components/SectionRenderer/SectionRenderer.tsx +91 -0
  169. package/src/tui/components/SectionRenderer/index.ts +2 -0
  170. package/src/tui/components/SectionRenderer/types.ts +15 -0
  171. package/src/tui/components/TreeView/TreeView.tsx +84 -0
  172. package/src/tui/components/TreeView/index.ts +2 -0
  173. package/src/tui/components/TreeView/types.ts +6 -0
  174. package/src/tui/components/index.ts +19 -0
  175. package/src/tui/constants.ts +39 -0
  176. package/src/tui/createListView.ts +23 -0
  177. package/src/tui/createLookupView.ts +26 -0
  178. package/src/tui/helpers/computeWidths.ts +53 -0
  179. package/src/tui/helpers/fitColumns.ts +33 -0
  180. package/src/tui/helpers/formatMarkdown.ts +19 -0
  181. package/src/tui/helpers/getTerminalSize.ts +16 -0
  182. package/src/tui/helpers/index.ts +6 -0
  183. package/src/tui/helpers/syntaxColor.ts +67 -0
  184. package/src/tui/helpers/truncateText.ts +12 -0
  185. package/src/tui/hooks/index.ts +3 -0
  186. package/src/tui/hooks/types.ts +7 -0
  187. package/src/tui/hooks/useAutoExit.ts +17 -0
  188. package/src/tui/hooks/useTerminalSize.ts +19 -0
  189. package/src/tui/index.ts +15 -0
  190. package/src/tui/renderInk.tsx +18 -0
  191. package/src/tui/types.ts +12 -0
  192. package/src/tui/views/ListView/ListView.tsx +104 -0
  193. package/src/tui/views/ListView/index.ts +2 -0
  194. package/src/tui/views/ListView/types.ts +14 -0
  195. package/src/tui/views/LookupView/LookupView.tsx +244 -0
  196. package/src/tui/views/LookupView/index.ts +2 -0
  197. package/src/tui/views/LookupView/types.ts +16 -0
  198. package/src/tui/views/index.ts +4 -0
  199. package/src/domains/shared/types.test-d.ts +0 -127
  200. package/src/domains/shared/types.ts +0 -348
@@ -3,28 +3,68 @@
3
3
  *
4
4
  * Each test calls the same query through both the operation layer
5
5
  * (what CLI uses) and the MCP tool (what agents use), then asserts
6
- * data equality.
6
+ * data equality. Condensed-mode tests verify that the MCP condensed
7
+ * text matches what the LLM formatter produces for the same data.
7
8
  */
8
9
 
9
10
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
11
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
11
- import { listFormatters as blockListFmt } from "../../domains/block/formatters/index.js";
12
+ // ---- Shared utilities ----
13
+ import { VERSION } from "#constants";
14
+ import { detectInstallSource } from "#package-manager";
15
+ // ---- Formatters ----
16
+ import {
17
+ listFormatters as blockListFmt,
18
+ lookupFormatters as blockLookupFmt,
19
+ } from "../../domains/block/formatters/index.js";
20
+ // ---- Operations ----
12
21
  import {
13
22
  listBlocks,
14
23
  lookupBlock,
15
24
  } from "../../domains/block/operations/index.js";
25
+ import { showFormatters as configShowFmt } from "../../domains/config/formatters/index.js";
26
+ import { resolveConfigShow } from "../../domains/config/operations/index.js";
27
+ import { renderInfoLlm } from "../../domains/info/formatters/index.js";
28
+ import { collectStoreSummary } from "../../domains/info/operations/index.js";
29
+ import type { InfoData } from "../../domains/info/types.js";
30
+ import {
31
+ listFormatters as modifierListFmt,
32
+ lookupFormatters as modifierLookupFmt,
33
+ } from "../../domains/modifier/formatters/index.js";
16
34
  import {
17
35
  listModifiers,
18
36
  lookupModifier,
19
37
  } from "../../domains/modifier/operations/index.js";
20
- import { listOntologies } from "../../domains/ontology/operations/index.js";
38
+ import {
39
+ listOntologies,
40
+ showOntology,
41
+ } from "../../domains/ontology/operations/index.js";
42
+ import { CHANNEL_RELEASES } from "../../domains/shared/filters/buildChannelFilter.js";
43
+ import { resolveTierChain } from "../../domains/shared/filters/buildTierFilter.js";
21
44
  import type { PragmaRuntime } from "../../domains/shared/runtime.js";
22
- import { listStandards } from "../../domains/standard/operations/index.js";
45
+ import {
46
+ type StandardListOutput,
47
+ categoriesFormatters as standardCatFmt,
48
+ listFormatters as standardListFmt,
49
+ lookupFormatters as standardLookupFmt,
50
+ } from "../../domains/standard/formatters/index.js";
51
+ import {
52
+ listCategories,
53
+ listStandards,
54
+ lookupStandard,
55
+ } from "../../domains/standard/operations/index.js";
56
+ import { listFormatters as tierListFmt } from "../../domains/tier/formatters/index.js";
23
57
  import { listTiers } from "../../domains/tier/operations/index.js";
58
+ import {
59
+ createLookupFormatters as createTokenLookupFmt,
60
+ listFormatters as tokenListFmt,
61
+ } from "../../domains/token/formatters/index.js";
24
62
  import {
25
63
  listTokens,
26
64
  lookupToken,
27
65
  } from "../../domains/token/operations/index.js";
66
+
67
+ // ---- Test helpers ----
28
68
  import assertParity from "../helpers/assertParity.js";
29
69
  import createTestMcpClient from "../helpers/createTestMcpClient.js";
30
70
  import createTestRuntime from "../helpers/createTestRuntime.js";
@@ -45,6 +85,30 @@ afterAll(async () => {
45
85
  rt.dispose();
46
86
  });
47
87
 
88
+ /** Parse condensed MCP response envelope. */
89
+ function parseCondensed(mcpResponse: Record<string, unknown>): {
90
+ text: string;
91
+ condensed: boolean;
92
+ tokens: string;
93
+ } {
94
+ const content = mcpResponse.content as unknown[];
95
+ const first = content[0] as { text: string };
96
+ return JSON.parse(first.text) as {
97
+ text: string;
98
+ condensed: boolean;
99
+ tokens: string;
100
+ };
101
+ }
102
+
103
+ /** Parse MCP response envelope. */
104
+ function parseEnvelope(
105
+ mcpResponse: Record<string, unknown>,
106
+ ): Record<string, unknown> {
107
+ const content = mcpResponse.content as unknown[];
108
+ const first = content[0] as { text: string };
109
+ return JSON.parse(first.text) as Record<string, unknown>;
110
+ }
111
+
48
112
  // ---------------------------------------------------------------------------
49
113
  // Block parity
50
114
  // ---------------------------------------------------------------------------
@@ -67,24 +131,43 @@ describe("block parity", () => {
67
131
  name: "block_list",
68
132
  arguments: { condensed: true },
69
133
  });
70
- const content = mcpRes.content as unknown[];
71
- const first = content[0] as { text: string };
72
- const body = JSON.parse(first.text) as { text: string };
134
+ const body = parseCondensed(mcpRes);
73
135
  expect(body.text).toBe(expectedText);
74
136
  });
75
137
 
76
- it("block_lookup: detailed fields match", async () => {
77
- const opResult = await lookupBlock(rt.store, "Button", rt.config);
138
+ it("block_lookup: detailed result matches", async () => {
139
+ const opResults = await lookupBlock(rt.store, "Button", rt.config);
140
+ expect(opResults.length).toBeGreaterThan(0);
141
+ const opResult = opResults[0];
78
142
  const mcpRes = await client.callTool({
79
143
  name: "block_lookup",
80
144
  arguments: { names: ["Button"] },
81
145
  });
82
- const content = mcpRes.content as unknown[];
83
- const first = content[0] as { text: string };
84
- const body = JSON.parse(first.text) as {
85
- data: { results: unknown[] };
86
- };
87
- expect(body.data.results[0]).toEqual(opResult);
146
+ const body = parseEnvelope(mcpRes);
147
+ expect(body.data).toEqual({ results: [opResult], errors: [] });
148
+ });
149
+
150
+ it("block_lookup condensed: matches llm formatter", async () => {
151
+ const opResults = await lookupBlock(rt.store, "Button", rt.config);
152
+ expect(opResults.length).toBeGreaterThan(0);
153
+ const opResult = opResults[0];
154
+ const expectedText = blockLookupFmt.llm({
155
+ block: opResult,
156
+ detailed: true,
157
+ aspects: {
158
+ anatomy: true,
159
+ modifiers: true,
160
+ tokens: true,
161
+ implementations: true,
162
+ },
163
+ });
164
+
165
+ const mcpRes = await client.callTool({
166
+ name: "block_lookup",
167
+ arguments: { names: ["Button"], condensed: true },
168
+ });
169
+ const body = parseCondensed(mcpRes);
170
+ expect(body.text).toBe(expectedText);
88
171
  });
89
172
  });
90
173
 
@@ -101,6 +184,80 @@ describe("standard parity", () => {
101
184
  });
102
185
  assertParity(opResult, mcpRes);
103
186
  });
187
+
188
+ it("standard_list condensed: matches llm formatter", async () => {
189
+ const opResult = await listStandards(rt.store);
190
+ const output: StandardListOutput = {
191
+ items: opResult,
192
+ details: undefined,
193
+ disclosure: { level: "summary" },
194
+ };
195
+ const expectedText = standardListFmt.llm(output);
196
+
197
+ const mcpRes = await client.callTool({
198
+ name: "standard_list",
199
+ arguments: { condensed: true },
200
+ });
201
+ const body = parseCondensed(mcpRes);
202
+ expect(body.text).toBe(expectedText);
203
+ });
204
+
205
+ it("standard_lookup: detailed fields match", async () => {
206
+ const opResult = await lookupStandard(
207
+ rt.store,
208
+ "react/component/folder-structure",
209
+ );
210
+ const mcpRes = await client.callTool({
211
+ name: "standard_lookup",
212
+ arguments: { names: ["react/component/folder-structure"] },
213
+ });
214
+ const body = parseEnvelope(mcpRes);
215
+ const data = body.data as { results: unknown[]; errors: unknown[] };
216
+ expect(data.results[0]).toEqual(opResult);
217
+ expect(data.errors).toEqual([]);
218
+ });
219
+
220
+ it("standard_lookup condensed: matches llm formatter", async () => {
221
+ const opResult = await lookupStandard(
222
+ rt.store,
223
+ "react/component/folder-structure",
224
+ );
225
+ const expectedText = standardLookupFmt.llm({
226
+ standard: opResult,
227
+ detailed: true,
228
+ });
229
+
230
+ const mcpRes = await client.callTool({
231
+ name: "standard_lookup",
232
+ arguments: {
233
+ names: ["react/component/folder-structure"],
234
+ condensed: true,
235
+ },
236
+ });
237
+ const body = parseCondensed(mcpRes);
238
+ expect(body.text).toBe(expectedText);
239
+ });
240
+
241
+ it("standard_categories: data matches", async () => {
242
+ const opResult = await listCategories(rt.store);
243
+ const mcpRes = await client.callTool({
244
+ name: "standard_categories",
245
+ arguments: {},
246
+ });
247
+ assertParity(opResult, mcpRes);
248
+ });
249
+
250
+ it("standard_categories condensed: matches llm formatter", async () => {
251
+ const opResult = await listCategories(rt.store);
252
+ const expectedText = standardCatFmt.llm(opResult);
253
+
254
+ const mcpRes = await client.callTool({
255
+ name: "standard_categories",
256
+ arguments: { condensed: true },
257
+ });
258
+ const body = parseCondensed(mcpRes);
259
+ expect(body.text).toBe(expectedText);
260
+ });
104
261
  });
105
262
 
106
263
  // ---------------------------------------------------------------------------
@@ -116,6 +273,18 @@ describe("tier parity", () => {
116
273
  });
117
274
  assertParity(opResult, mcpRes);
118
275
  });
276
+
277
+ it("tier_list condensed: matches llm formatter", async () => {
278
+ const opResult = await listTiers(rt.store);
279
+ const expectedText = tierListFmt.llm(opResult);
280
+
281
+ const mcpRes = await client.callTool({
282
+ name: "tier_list",
283
+ arguments: { condensed: true },
284
+ });
285
+ const body = parseCondensed(mcpRes);
286
+ expect(body.text).toBe(expectedText);
287
+ });
119
288
  });
120
289
 
121
290
  // ---------------------------------------------------------------------------
@@ -132,18 +301,39 @@ describe("modifier parity", () => {
132
301
  assertParity(opResult, mcpRes);
133
302
  });
134
303
 
304
+ it("modifier_list condensed: matches llm formatter", async () => {
305
+ const opResult = await listModifiers(rt.store);
306
+ const expectedText = modifierListFmt.llm([...opResult]);
307
+
308
+ const mcpRes = await client.callTool({
309
+ name: "modifier_list",
310
+ arguments: { condensed: true },
311
+ });
312
+ const body = parseCondensed(mcpRes);
313
+ expect(body.text).toBe(expectedText);
314
+ });
315
+
135
316
  it("modifier_lookup: values match", async () => {
136
317
  const opResult = await lookupModifier(rt.store, "importance");
137
318
  const mcpRes = await client.callTool({
138
319
  name: "modifier_lookup",
139
320
  arguments: { names: ["importance"] },
140
321
  });
141
- const content = mcpRes.content as unknown[];
142
- const first = content[0] as { text: string };
143
- const body = JSON.parse(first.text) as {
144
- data: { results: unknown[] };
145
- };
146
- expect(body.data.results[0]).toEqual(opResult);
322
+ const body = parseEnvelope(mcpRes);
323
+ const data = body.data as { results: unknown[]; errors: unknown[] };
324
+ expect(data.results[0]).toEqual(opResult);
325
+ });
326
+
327
+ it("modifier_lookup condensed: matches llm formatter", async () => {
328
+ const opResult = await lookupModifier(rt.store, "importance");
329
+ const expectedText = modifierLookupFmt.llm(opResult);
330
+
331
+ const mcpRes = await client.callTool({
332
+ name: "modifier_lookup",
333
+ arguments: { names: ["importance"], condensed: true },
334
+ });
335
+ const body = parseCondensed(mcpRes);
336
+ expect(body.text).toBe(expectedText);
147
337
  });
148
338
  });
149
339
 
@@ -161,18 +351,40 @@ describe("token parity", () => {
161
351
  assertParity(opResult, mcpRes);
162
352
  });
163
353
 
354
+ it("token_list condensed: matches llm formatter", async () => {
355
+ const opResult = await listTokens(rt.store);
356
+ const expectedText = tokenListFmt.llm([...opResult]);
357
+
358
+ const mcpRes = await client.callTool({
359
+ name: "token_list",
360
+ arguments: { condensed: true },
361
+ });
362
+ const body = parseCondensed(mcpRes);
363
+ expect(body.text).toBe(expectedText);
364
+ });
365
+
164
366
  it("token_lookup: values match", async () => {
165
367
  const opResult = await lookupToken(rt.store, "color.primary");
166
368
  const mcpRes = await client.callTool({
167
369
  name: "token_lookup",
168
370
  arguments: { names: ["color.primary"] },
169
371
  });
170
- const content = mcpRes.content as unknown[];
171
- const first = content[0] as { text: string };
172
- const body = JSON.parse(first.text) as {
173
- data: { results: unknown[] };
174
- };
175
- expect(body.data.results[0]).toEqual(opResult);
372
+ const body = parseEnvelope(mcpRes);
373
+ const data = body.data as { results: unknown[]; errors: unknown[] };
374
+ expect(data.results[0]).toEqual(opResult);
375
+ });
376
+
377
+ it("token_lookup condensed: matches llm formatter", async () => {
378
+ const opResult = await lookupToken(rt.store, "color.primary");
379
+ const fmt = createTokenLookupFmt({ detailed: true });
380
+ const expectedText = fmt.llm(opResult);
381
+
382
+ const mcpRes = await client.callTool({
383
+ name: "token_lookup",
384
+ arguments: { names: ["color.primary"], condensed: true },
385
+ });
386
+ const body = parseCondensed(mcpRes);
387
+ expect(body.text).toBe(expectedText);
176
388
  });
177
389
  });
178
390
 
@@ -189,4 +401,142 @@ describe("ontology parity", () => {
189
401
  });
190
402
  assertParity(opResult, mcpRes);
191
403
  });
404
+
405
+ it("ontology_list condensed: text contains ontology data", async () => {
406
+ const mcpRes = await client.callTool({
407
+ name: "ontology_list",
408
+ arguments: { condensed: true },
409
+ });
410
+ const body = parseCondensed(mcpRes);
411
+ expect(body.condensed).toBe(true);
412
+ expect(body.text).toContain("Ontologies");
413
+ expect(body.text).toContain("ds:");
414
+ });
415
+
416
+ it("ontology_show: data matches operation", async () => {
417
+ const opResult = await showOntology(rt.store, "ds");
418
+ const mcpRes = await client.callTool({
419
+ name: "ontology_show",
420
+ arguments: { prefix: "ds" },
421
+ });
422
+ const body = parseEnvelope(mcpRes);
423
+ expect(body.data).toEqual(opResult);
424
+ });
425
+
426
+ it("ontology_show condensed: text contains schema data", async () => {
427
+ const mcpRes = await client.callTool({
428
+ name: "ontology_show",
429
+ arguments: { prefix: "ds", condensed: true },
430
+ });
431
+ const body = parseCondensed(mcpRes);
432
+ expect(body.condensed).toBe(true);
433
+ expect(body.text).toContain("ds:");
434
+ expect(body.text).toContain("Classes");
435
+ });
436
+ });
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // Config parity
440
+ // ---------------------------------------------------------------------------
441
+
442
+ describe("config parity", () => {
443
+ it("config_show: data matches resolveConfigShow", async () => {
444
+ const install = detectInstallSource();
445
+ const opResult = resolveConfigShow(rt.config, {
446
+ packageManager: install.packageManager,
447
+ installSource: install.label,
448
+ configFilePath: "pragma.config.json",
449
+ configFileExists: true,
450
+ });
451
+
452
+ const mcpRes = await client.callTool({
453
+ name: "config_show",
454
+ arguments: {},
455
+ });
456
+ const body = parseEnvelope(mcpRes);
457
+ expect(body.data).toEqual(opResult);
458
+ });
459
+
460
+ it("config_show condensed: matches llm formatter", async () => {
461
+ const install = detectInstallSource();
462
+ const opResult = resolveConfigShow(rt.config, {
463
+ packageManager: install.packageManager,
464
+ installSource: install.label,
465
+ configFilePath: "pragma.config.json",
466
+ configFileExists: true,
467
+ });
468
+ const expectedText = configShowFmt.llm(opResult);
469
+
470
+ const mcpRes = await client.callTool({
471
+ name: "config_show",
472
+ arguments: { condensed: true },
473
+ });
474
+ const body = parseCondensed(mcpRes);
475
+ expect(body.text).toBe(expectedText);
476
+ });
477
+ });
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // Info parity
481
+ // ---------------------------------------------------------------------------
482
+
483
+ describe("info parity", () => {
484
+ it("info: data matches shared operations", async () => {
485
+ const install = detectInstallSource();
486
+ const tierChain =
487
+ rt.config.tier !== undefined ? resolveTierChain(rt.config.tier) : [];
488
+ const channelReleases = CHANNEL_RELEASES[rt.config.channel];
489
+ const storeSummary = await collectStoreSummary(rt.store);
490
+
491
+ const expected: InfoData = {
492
+ version: VERSION,
493
+ pm: install.packageManager,
494
+ installSource: install.label,
495
+ configPath: "pragma.config.json",
496
+ tier: rt.config.tier,
497
+ tierChain,
498
+ channel: rt.config.channel,
499
+ channelReleases: [...channelReleases],
500
+ update: undefined,
501
+ updateSkipped: true,
502
+ store: storeSummary,
503
+ };
504
+
505
+ const mcpRes = await client.callTool({
506
+ name: "info",
507
+ arguments: {},
508
+ });
509
+ const body = parseEnvelope(mcpRes);
510
+ expect(body.data).toEqual(expected);
511
+ });
512
+
513
+ it("info condensed: matches llm formatter", async () => {
514
+ const install = detectInstallSource();
515
+ const tierChain =
516
+ rt.config.tier !== undefined ? resolveTierChain(rt.config.tier) : [];
517
+ const channelReleases = CHANNEL_RELEASES[rt.config.channel];
518
+ const storeSummary = await collectStoreSummary(rt.store);
519
+
520
+ const data: InfoData = {
521
+ version: VERSION,
522
+ pm: install.packageManager,
523
+ installSource: install.label,
524
+ configPath: "pragma.config.json",
525
+ tier: rt.config.tier,
526
+ tierChain,
527
+ channel: rt.config.channel,
528
+ channelReleases: [...channelReleases],
529
+ update: undefined,
530
+ updateSkipped: true,
531
+ store: storeSummary,
532
+ };
533
+ const expectedText = renderInfoLlm(data);
534
+
535
+ const mcpRes = await client.callTool({
536
+ name: "info",
537
+ arguments: { condensed: true },
538
+ });
539
+ const body = parseCondensed(mcpRes);
540
+ expect(body.text).toBe(expectedText);
541
+ });
192
542
  });
@@ -35,5 +35,5 @@ async function createTestStore(
35
35
  });
36
36
  }
37
37
 
38
- export { createTestStore };
39
38
  export type { PragmaTestStoreOptions };
39
+ export { createTestStore };
@@ -0,0 +1,46 @@
1
+ import { Box, Text } from "ink";
2
+ import compactUri from "../../../domains/shared/compactUri.js";
3
+ import { PREFIX_MAP } from "../../../domains/shared/prefixes.js";
4
+ import type { BulletListProps } from "./types.js";
5
+
6
+ /**
7
+ * A simple bulleted list with `•` prefix.
8
+ *
9
+ * Formats each item as a string, compacting URIs when detected.
10
+ */
11
+ export default function BulletList({ items, prefixes }: BulletListProps) {
12
+ const prefixMap = prefixes ?? PREFIX_MAP;
13
+
14
+ return (
15
+ <Box flexDirection="column" paddingLeft={2}>
16
+ {items.map((item) => {
17
+ const text = formatItem(item, prefixMap);
18
+ return <Text key={text}>• {text}</Text>;
19
+ })}
20
+ </Box>
21
+ );
22
+ }
23
+
24
+ function formatItem(
25
+ value: unknown,
26
+ prefixes: Readonly<Record<string, string>>,
27
+ ): string {
28
+ if (value === null || value === undefined) return "";
29
+ if (typeof value === "string") {
30
+ if (
31
+ value.startsWith("http://") ||
32
+ value.startsWith("https://") ||
33
+ /^[a-z][a-z0-9+.-]*:/i.test(value)
34
+ ) {
35
+ return compactUri(value, prefixes);
36
+ }
37
+ return value;
38
+ }
39
+ if (typeof value === "object" && !Array.isArray(value)) {
40
+ return Object.entries(value as Record<string, unknown>)
41
+ .filter(([, v]) => v !== null && v !== undefined)
42
+ .map(([k, v]) => `${k}: ${String(v)}`)
43
+ .join(" | ");
44
+ }
45
+ return String(value);
46
+ }
@@ -0,0 +1,2 @@
1
+ export { default as BulletList } from "./BulletList.js";
2
+ export type * from "./types.js";
@@ -0,0 +1,6 @@
1
+ export interface BulletListProps {
2
+ /** Items to render as a bulleted list. */
3
+ readonly items: readonly unknown[];
4
+ /** URI prefix map for compacting IRIs. */
5
+ readonly prefixes?: Readonly<Record<string, string>>;
6
+ }
@@ -0,0 +1,73 @@
1
+ import chalk from "chalk";
2
+ import { BOX, DOMAIN_COLORS } from "../../constants.js";
3
+ import { truncateText } from "../../helpers/index.js";
4
+
5
+ /**
6
+ * Build a bordered card string for a single lookup result.
7
+ *
8
+ * Uses double-line box-drawing characters for the border. The title
9
+ * renders in the domain's instance color. When a badge is present
10
+ * (e.g., "[1 of 2]"), it appears right-aligned in the header.
11
+ * Every body line is wrapped with `║` side borders.
12
+ *
13
+ * @param title - Card title for the header bar.
14
+ * @param domain - Domain name for color lookup.
15
+ * @param bodyLines - Pre-formatted body lines to wrap with borders.
16
+ * @param termWidth - Terminal width in columns.
17
+ * @param badge - Optional right-aligned badge text (e.g., "[1 of 2]").
18
+ * @returns A fully bordered card as a single string.
19
+ */
20
+ export default function buildCard(
21
+ title: string,
22
+ domain: string,
23
+ bodyLines: readonly string[],
24
+ termWidth: number,
25
+ badge?: string,
26
+ ): string {
27
+ const innerWidth = Math.max(termWidth - 4, 20);
28
+ const colors = DOMAIN_COLORS[domain];
29
+ const colorFn = resolveChalkColor(colors?.instanceFg);
30
+
31
+ const badgeText = badge ? ` ${badge}` : "";
32
+ const maxTitleWidth = innerWidth - badgeText.length;
33
+ const displayTitle = truncateText(title, maxTitleWidth);
34
+ const titlePad = innerWidth - displayTitle.length - badgeText.length;
35
+
36
+ const topBorder = `${BOX.topLeft}${BOX.horizontal.repeat(innerWidth + 2)}${BOX.topRight}`;
37
+ const badgePart = badge ? chalk.dim(badgeText) : "";
38
+ const titleLine = `${BOX.vertical} ${chalk.bold(colorFn(displayTitle))}${" ".repeat(Math.max(titlePad, 0))}${badgePart} ${BOX.vertical}`;
39
+ const divider = `${BOX.dividerLeft}${BOX.dividerHorizontal.repeat(innerWidth + 2)}${BOX.dividerRight}`;
40
+ const bottomBorder = `${BOX.bottomLeft}${BOX.horizontal.repeat(innerWidth + 2)}${BOX.bottomRight}`;
41
+
42
+ const expandedLines = bodyLines.flatMap((line) => line.split("\n"));
43
+ const borderedBody = expandedLines.map((line) => {
44
+ const visibleWidth = stripAnsi(line).length;
45
+ const pad = Math.max(innerWidth - visibleWidth, 0);
46
+ return `${BOX.vertical} ${line}${" ".repeat(pad)} ${BOX.vertical}`;
47
+ });
48
+
49
+ return [topBorder, titleLine, divider, ...borderedBody, bottomBorder].join(
50
+ "\n",
51
+ );
52
+ }
53
+
54
+ function resolveChalkColor(
55
+ colorName: string | undefined,
56
+ ): (text: string) => string {
57
+ if (!colorName) return (text) => text;
58
+ const fn = chalk[colorName as keyof typeof chalk];
59
+ return typeof fn === "function"
60
+ ? (fn as (text: string) => string)
61
+ : (text) => text;
62
+ }
63
+
64
+ /**
65
+ * Strip ANSI escape codes from a string to measure visible width.
66
+ */
67
+ function stripAnsi(text: string): string {
68
+ return text.replace(
69
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escapes
70
+ /\x1b\[[0-9;]*m/g,
71
+ "",
72
+ );
73
+ }
@@ -0,0 +1 @@
1
+ export { default as buildCard } from "./Card.js";
@@ -0,0 +1,12 @@
1
+ import type React from "react";
2
+
3
+ export interface CardProps {
4
+ /** Card title displayed in the header bar. */
5
+ readonly title: string;
6
+ /** Domain name — indexes into DOMAIN_COLORS for the title color. */
7
+ readonly domain: string;
8
+ /** Optional badge text displayed right-aligned in the header (e.g., "[1 of 2]"). */
9
+ readonly badge?: string;
10
+ /** Card body content. */
11
+ readonly children: React.ReactNode;
12
+ }
@@ -0,0 +1,37 @@
1
+ import { Box, Text } from "ink";
2
+ import { BOX } from "../../constants.js";
3
+ import { syntaxColor } from "../../helpers/index.js";
4
+ import { useTerminalSize } from "../../hooks/index.js";
5
+ import type { CodeBlockProps } from "./types.js";
6
+
7
+ /**
8
+ * A bordered code block with lightweight syntax coloring.
9
+ *
10
+ * Renders code inside a single-line bordered sub-box using thin
11
+ * box-drawing characters. Applies keyword-level colorization via
12
+ * the syntaxColor helper.
13
+ */
14
+ export default function CodeBlock({ code, language }: CodeBlockProps) {
15
+ const { columns: termWidth } = useTerminalSize();
16
+ const innerWidth = Math.max(termWidth - 8, 20);
17
+
18
+ const colored = syntaxColor(code, language);
19
+ const lines = colored.split("\n");
20
+
21
+ const topBorder = ` ${BOX.thinVertical === "│" ? "┌" : BOX.thinVertical}${"─".repeat(innerWidth)}${"┐"}`;
22
+ const bottomBorder = ` ${"└"}${"─".repeat(innerWidth)}${"┘"}`;
23
+
24
+ return (
25
+ <Box flexDirection="column">
26
+ <Text dimColor>{topBorder}</Text>
27
+ {lines.map((line, i) => (
28
+ // biome-ignore lint/suspicious/noArrayIndexKey: static code lines
29
+ <Text key={i}>
30
+ {" │ "}
31
+ {line}
32
+ </Text>
33
+ ))}
34
+ <Text dimColor>{bottomBorder}</Text>
35
+ </Box>
36
+ );
37
+ }
@@ -0,0 +1,2 @@
1
+ export { default as CodeBlock } from "./CodeBlock.js";
2
+ export type * from "./types.js";