@canonical/pragma-cli 0.32.0 → 0.34.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 (295) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  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 +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -10,9 +10,22 @@
10
10
  * arguments. The SDK request schemas are dynamic-imported inside `register`, so
11
11
  * this module — reachable on the capabilities import graph through the prompt
12
12
  * module — never pulls the SDK types onto the `--help`/`__complete` fast path.
13
+ *
14
+ * Both handlers open with a STORELESS cold-store pre-check ({@link
15
+ * checkStoreAvailable}), and the store-backed `get` projects any read failure
16
+ * through {@link mcpErrorFrom}, so the native surface converges with the
17
+ * store-backed `prompt_list` tool: a cold store — or a store that cannot answer
18
+ * the prompt query — surfaces the SAME machine code and recovery here as the
19
+ * tool envelope carries, rather than a silent `[]` (list) or a bogus "not found"
20
+ * (get) that masks it. When the store is available (including the embedded
21
+ * fallback) both handlers behave exactly as before.
13
22
  */
14
23
 
24
+ import { asPragmaError } from "../../../error/fromTaskError.js";
25
+ import { resolveSources } from "../../../runtime/resolveSources.js";
26
+ import { checkStoreAvailable } from "../../../runtime/storeReadiness.js";
15
27
  import type { McpPromptProvider } from "../../../spec/types.js";
28
+ import { mcpErrorFrom } from "../mcpError.js";
16
29
  import { listPromptSummaries, readPrompt } from "./source.js";
17
30
 
18
31
  /** Substitute `{{arg}}` placeholders in a template body with provided values. */
@@ -41,17 +54,48 @@ export const promptProvider: McpPromptProvider = {
41
54
  // of whether the active graph ships prompt entities.
42
55
  server.server.registerCapabilities({ prompts: { listChanged: false } });
43
56
 
44
- server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
45
- prompts: listPromptSummaries(runtime).map((prompt) => ({
46
- name: prompt.name,
47
- ...(prompt.description ? { description: prompt.description } : {}),
48
- })),
49
- }));
57
+ // A STORELESS cold-store pre-check, shared with the store-backed `prompt_list`
58
+ // tool (whose `needsStore` guard throws) and the resource browser: a cold
59
+ // store must present the SAME STORE_UNAVAILABLE + `sources_update` recovery on
60
+ // native `prompts/*` as on the tool surface, not silently list from a stale
61
+ // index (or `[]`) here while the tool errors. Available (incl. the embedded
62
+ // fallback) → the native list stays storeless and the get stays store-backed.
63
+ const guardStore = async (): Promise<void> => {
64
+ const unavailable = checkStoreAvailable(
65
+ await runtime.loadConfig(),
66
+ runtime.cwd,
67
+ );
68
+ if (unavailable) throw mcpErrorFrom(unavailable, { McpError, ErrorCode });
69
+ };
70
+
71
+ server.server.setRequestHandler(ListPromptsRequestSchema, async () => {
72
+ await guardStore();
73
+ return {
74
+ prompts: listPromptSummaries(
75
+ resolveSources(await runtime.loadConfig(), runtime.cwd),
76
+ ).map((prompt) => ({
77
+ name: prompt.name,
78
+ ...(prompt.description ? { description: prompt.description } : {}),
79
+ })),
80
+ };
81
+ });
50
82
 
51
83
  server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
84
+ await guardStore();
52
85
  const { name, arguments: args } = request.params;
53
- const entry = await readPrompt(runtime, name);
86
+ // The read can fail (a store that cannot answer the prompt query), and a
87
+ // native surface signals that by THROWING — so project it the way the
88
+ // resource browser does, carrying the machine code and the recovery into
89
+ // the JSON-RPC `data` instead of letting a bare Error reach the agent.
90
+ let entry: Awaited<ReturnType<typeof readPrompt>>;
91
+ try {
92
+ entry = await readPrompt(runtime, name);
93
+ } catch (error) {
94
+ throw mcpErrorFrom(asPragmaError(error), { McpError, ErrorCode });
95
+ }
54
96
  if (!entry) {
97
+ // Reached only when the store IS available — a genuine miss, not a cold
98
+ // store (which `guardStore` already surfaced as STORE_UNAVAILABLE).
55
99
  throw new McpError(
56
100
  ErrorCode.InvalidParams,
57
101
  `Prompt "${name}" not found`,
@@ -1,27 +1,44 @@
1
1
  /**
2
- * The ONE source of truth for prompt entities — read TWO ways from ONE KG type
3
- * (`ds:Prompt`), so the native MCP `prompts/*` surface and the covenant
4
- * `prompt_list`/`prompt_lookup` content tools never diverge.
2
+ * The ONE source of truth for prompt entities — read TWO ways from ONE KG type,
3
+ * so the native MCP `prompts/*` surface and the covenant `prompt_list`/
4
+ * `prompt_lookup` content tools never diverge.
5
5
  *
6
- * A `ds:Prompt` is authored as:
7
- * ds:prompt.x a ds:Prompt ;
8
- * rdfs:label "x" ; # the prompt NAME (indexed → storeless list)
9
- * rdfs:comment "…" ; # the description (indexed)
10
- * ds:promptBody "…" ; # the template body (store-backed)
11
- * ds:promptArgument [ ds:argName "a" ; rdfs:comment "…" ; ds:argRequired true ] .
6
+ * The type and its four properties are DECLARED by the distribution
7
+ * (`kernel/vocabulary.ts`), not named here — this module knows the shape of a
8
+ * prompt, not whose prompt it is. With `<type>`/`<body>`/`<argument>`/
9
+ * `<argName>`/`<argRequired>` standing for the declared terms, a prompt entity
10
+ * is authored as:
11
+ *
12
+ * ex:prompt.x a <type> ;
13
+ * rdfs:label "x" ; # the prompt NAME (indexed → storeless list)
14
+ * rdfs:comment "…" ; # the description (indexed)
15
+ * <body> "…" ; # the template body (store-backed)
16
+ * <argument> [ <argName> "a" ; rdfs:comment "…" ; <argRequired> true ] .
12
17
  *
13
18
  * Listing is STORELESS (over the pack index — `label`/`description` are indexed),
14
19
  * so `prompts/list` and native discovery cost nothing until a `get`. A `get`
15
- * (body + arguments) is STORE-BACKED through the SPARQL facade — identical
16
- * laziness to the resource browser (list storeless, read store-backed). An empty
17
- * or missing index yields zero prompts (never a store boot on the list path).
20
+ * (body + arguments) is STORE-BACKED through `runSelect` — identical laziness to
21
+ * the resource browser (list storeless, read store-backed). An empty or missing
22
+ * index yields zero prompts (never a store boot on the list path).
23
+ *
24
+ * The store-backed reads route through `runSelect` and DO NOT catch. A cold
25
+ * store is already refused upstream — the tools are `needsStore`, and the native
26
+ * provider calls `guardStore()` first — so a swallowed failure here could only
27
+ * ever hide a real one, reporting a broken query as an empty graph. `runSelect`
28
+ * classifies an unbound prefix as STORE_UNAVAILABLE with its `sources update`
29
+ * recovery and lets everything else propagate.
18
30
  */
19
31
 
20
32
  import { readPackIndex } from "../../../completion/entitySource.js";
33
+ import { runSelect } from "../../../packs/sparql/runSelect.js";
34
+ import type { PackRow } from "../../../packs/types.js";
35
+ import { distributionSource } from "../../../packs/types.js";
36
+ import type { SourcesDecision } from "../../../runtime/resolveSources.js";
21
37
  import type { PragmaRuntime } from "../../../runtime/types.js";
38
+ import { VOCABULARY } from "../../../vocabulary.js";
22
39
 
23
- /** The KG type (prefixed) every prompt entity carries. */
24
- export const PROMPT_TYPE = "ds:Prompt";
40
+ /** The declared prompt terms this module reads the graph with. */
41
+ const PROMPT = VOCABULARY.prompt;
25
42
 
26
43
  /** One declared argument of a prompt template. */
27
44
  export interface PromptArgument {
@@ -42,27 +59,29 @@ export interface PromptEntry extends PromptSummary {
42
59
  readonly arguments: readonly PromptArgument[];
43
60
  }
44
61
 
45
- /** Whether an index entity is a `ds:Prompt` (primary type or any type). */
62
+ /** Whether an index entity carries the prompt type (primary type or any type). */
46
63
  function isPromptEntity(entity: {
47
64
  type: string;
48
65
  types?: readonly string[];
49
66
  }): boolean {
50
67
  return (
51
- entity.type === PROMPT_TYPE ||
52
- (Array.isArray(entity.types) && entity.types.includes(PROMPT_TYPE))
68
+ entity.type === PROMPT.type ||
69
+ (Array.isArray(entity.types) && entity.types.includes(PROMPT.type))
53
70
  );
54
71
  }
55
72
 
56
73
  /**
57
- * List prompt summaries STORELESSLY from the active pack index. Returns `[]`
58
- * when no index is reachable or the index carries no prompts — never boots the
59
- * store.
74
+ * List prompt summaries STORELESSLY from the index of the pack the boot decision
75
+ * names. Returns `[]` when no index is reachable (including an unavailable
76
+ * store) or the index carries no prompts — never boots the store.
60
77
  *
61
- * @param rt - The per-invocation runtime (for `cwd`).
78
+ * @param decision - The boot decision from `resolveSources`.
62
79
  * @returns Prompt summaries (name from the entity label, sorted by name).
63
80
  */
64
- export function listPromptSummaries(rt: PragmaRuntime): PromptSummary[] {
65
- const index = readPackIndex(rt.cwd);
81
+ export function listPromptSummaries(
82
+ decision: SourcesDecision,
83
+ ): PromptSummary[] {
84
+ const index = readPackIndex(decision);
66
85
  if (!index || !Array.isArray(index.entities)) return [];
67
86
  const summaries: PromptSummary[] = [];
68
87
  for (const entity of index.entities) {
@@ -76,21 +95,6 @@ export function listPromptSummaries(rt: PragmaRuntime): PromptSummary[] {
76
95
  return summaries.sort((a, b) => a.name.localeCompare(b.name));
77
96
  }
78
97
 
79
- /**
80
- * Rows the prompt SELECT returns (one per argument; body repeats). The facade's
81
- * SELECT bindings are keyed by variable name with PLAIN string values (an
82
- * unbound OPTIONAL variable is simply absent) — the same shape `runSelect`
83
- * consumes.
84
- */
85
- interface PromptRow {
86
- readonly name?: string;
87
- readonly description?: string;
88
- readonly body?: string;
89
- readonly argName?: string;
90
- readonly argDescription?: string;
91
- readonly argRequired?: string;
92
- }
93
-
94
98
  /** The SPARQL that materializes prompts (all, or one when `label` is bound). */
95
99
  function promptQuery(labelFilter?: string): string {
96
100
  const filter = labelFilter
@@ -98,24 +102,28 @@ function promptQuery(labelFilter?: string): string {
98
102
  : "";
99
103
  return [
100
104
  "SELECT ?name ?description ?body ?argName ?argDescription ?argRequired WHERE {",
101
- " ?prompt a ds:Prompt ;",
105
+ ` ?prompt a ${PROMPT.type} ;`,
102
106
  " rdfs:label ?name .",
103
107
  filter,
104
108
  " OPTIONAL { ?prompt rdfs:comment ?description }",
105
- " OPTIONAL { ?prompt ds:promptBody ?body }",
109
+ ` OPTIONAL { ?prompt ${PROMPT.body} ?body }`,
106
110
  " OPTIONAL {",
107
- " ?prompt ds:promptArgument ?arg .",
108
- " ?arg ds:argName ?argName .",
111
+ ` ?prompt ${PROMPT.argument} ?arg .`,
112
+ ` ?arg ${PROMPT.argName} ?argName .`,
109
113
  " OPTIONAL { ?arg rdfs:comment ?argDescription }",
110
- " OPTIONAL { ?arg ds:argRequired ?argRequired }",
114
+ ` OPTIONAL { ?arg ${PROMPT.argRequired} ?argRequired }`,
111
115
  " }",
112
116
  "}",
113
117
  "ORDER BY ?name ?argName",
114
118
  ].join("\n");
115
119
  }
116
120
 
117
- /** Fold SELECT rows (one per argument) into distinct {@link PromptEntry}s. */
118
- function foldPromptRows(rows: readonly PromptRow[]): PromptEntry[] {
121
+ /**
122
+ * Fold SELECT rows (one per argument; the body repeats) into distinct
123
+ * {@link PromptEntry}s. Rows are keyed by SELECT variable name with plain string
124
+ * values, and an unbound OPTIONAL variable is simply absent.
125
+ */
126
+ function foldPromptRows(rows: readonly PackRow[]): PromptEntry[] {
119
127
  const byName = new Map<
120
128
  string,
121
129
  { description?: string; body: string; args: Map<string, PromptArgument> }
@@ -151,27 +159,19 @@ function foldPromptRows(rows: readonly PromptRow[]): PromptEntry[] {
151
159
  .sort((a, b) => a.name.localeCompare(b.name));
152
160
  }
153
161
 
154
- /** Extract the SELECT binding rows from a facade query result. */
155
- function selectRows(result: unknown): PromptRow[] {
156
- const typed = result as { type?: string; bindings?: unknown };
157
- if (typed.type !== "select" || !Array.isArray(typed.bindings)) return [];
158
- return typed.bindings as PromptRow[];
159
- }
160
-
161
162
  /**
162
- * Read every prompt entity STORE-BACKED (body + arguments). Boots the store via
163
- * the SPARQL facade; returns `[]` on any query failure (graceful degradation).
163
+ * Read every prompt entity STORE-BACKED (body + arguments).
164
164
  *
165
165
  * @param rt - The per-invocation runtime.
166
166
  * @returns The materialized prompt entries, sorted by name.
167
+ * @throws PragmaError STORE_UNAVAILABLE when the store cannot answer; any other
168
+ * query failure propagates, so a broken read is never an empty graph.
169
+ * @note Impure — boots the store through `runSelect`.
167
170
  */
168
171
  export async function readPrompts(rt: PragmaRuntime): Promise<PromptEntry[]> {
169
- try {
170
- const result = await rt.query.sparql(promptQuery());
171
- return foldPromptRows(selectRows(result));
172
- } catch {
173
- return [];
174
- }
172
+ return foldPromptRows(
173
+ await runSelect(rt, promptQuery(), distributionSource("prompt")),
174
+ );
175
175
  }
176
176
 
177
177
  /**
@@ -179,16 +179,17 @@ export async function readPrompts(rt: PragmaRuntime): Promise<PromptEntry[]> {
179
179
  *
180
180
  * @param rt - The per-invocation runtime.
181
181
  * @param name - The prompt name (its `rdfs:label`).
182
- * @returns The materialized prompt, or `undefined` when not found.
182
+ * @returns The materialized prompt, or `undefined` when the graph has no such
183
+ * prompt — a genuine miss, never a failed read.
184
+ * @throws PragmaError STORE_UNAVAILABLE when the store cannot answer; any other
185
+ * query failure propagates.
186
+ * @note Impure — boots the store through `runSelect`.
183
187
  */
184
188
  export async function readPrompt(
185
189
  rt: PragmaRuntime,
186
190
  name: string,
187
191
  ): Promise<PromptEntry | undefined> {
188
- try {
189
- const result = await rt.query.sparql(promptQuery(name));
190
- return foldPromptRows(selectRows(result))[0];
191
- } catch {
192
- return undefined;
193
- }
192
+ return foldPromptRows(
193
+ await runSelect(rt, promptQuery(name), distributionSource("prompt")),
194
+ ).at(0);
194
195
  }
@@ -12,8 +12,8 @@
12
12
 
13
13
  import { statSync } from "node:fs";
14
14
  import { isAbsolute } from "node:path";
15
- import { describeEffect, dryRun, type Task } from "@canonical/task";
16
- import { runTask } from "@canonical/task/node";
15
+ import { describeEffect, type Task } from "@canonical/task";
16
+ import { runPreview, runTask } from "@canonical/task/node";
17
17
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
19
19
  import { z } from "zod";
@@ -199,10 +199,29 @@ function mutateHandler(verb: VerbSpec, runtime: PragmaRuntime) {
199
199
  | Promise<Task<unknown>>,
200
200
  );
201
201
  if (preview) {
202
- const plan = dryRun(task)
203
- .effects.filter((effect) => effect._tag !== "Prompt")
204
- .map(describeEffect);
205
- return toolSuccess({ plan }, { planOnly: true, confirmRequired: true });
202
+ // The HONEST preview (PR7), the same interpreter `--dry-run` uses:
203
+ // reads are real, writes are recorded and never executed. A tool call
204
+ // whose confirmed run would fail now returns that error instead of a
205
+ // confident plan, so plan-first predicts rather than reassures.
206
+ const previewExec = mutationRuntime.exec ?? {};
207
+ try {
208
+ const { effects } = await runPreview(task, {
209
+ cwd: previewExec.cwd,
210
+ onEffectStart: previewExec.onEffectStart,
211
+ onLog: logToStderr,
212
+ });
213
+ const plan = effects
214
+ .filter((effect) => effect._tag !== "Prompt")
215
+ .map(describeEffect);
216
+ return toolSuccess(
217
+ { plan },
218
+ { planOnly: true, confirmRequired: true },
219
+ );
220
+ } finally {
221
+ // No `store.invalidate()`: a preview cannot have changed disk. Only
222
+ // the verb's own teardown (e.g. an interactive session) runs here.
223
+ await previewExec.dispose?.();
224
+ }
206
225
  }
207
226
  // Real execution: spread the verb's runner options (prompt handler,
208
227
  // stamping) into the interpreter; run teardown afterwards.
@@ -15,8 +15,9 @@ import { buildServer } from "./buildServer.js";
15
15
  /**
16
16
  * Build the MCP server and serve it over stdio until the transport closes.
17
17
  *
18
- * The server is a real invocation (not a fast path), so it merges config-declared
19
- * story packs into the static capabilities before projecting the tool surface.
18
+ * The server is a real invocation (not a fast path), so it merges the package-
19
+ * and config-declared story packs into the static capabilities before projecting
20
+ * the tool surface.
20
21
  *
21
22
  * @param modules - The static capability modules to project.
22
23
  * @param cwd - The working directory for the server's runtime.
@@ -28,7 +29,14 @@ export async function serveMcp(
28
29
  ): Promise<void> {
29
30
  const dir = cwd ?? process.cwd();
30
31
  const effective = await loadEffectiveModules(modules, dir);
31
- const server = await buildServer(effective, dir);
32
+ // stderr only — the stdio transport owns stdout, so naming an unusable
33
+ // package story here is safe and reaches the host's server log.
34
+ for (const problem of effective.problems) {
35
+ process.stderr.write(
36
+ `Ignored story ${problem.source}: ${problem.message}\n`,
37
+ );
38
+ }
39
+ const server = await buildServer(effective.modules, dir);
32
40
  const transport = new StdioServerTransport();
33
41
  await server.connect(transport);
34
42
  }
@@ -2,7 +2,7 @@
2
2
  * MCP tool descriptions carry NO CLI-syntax leaks (a PR8 doc→MCP residue).
3
3
  *
4
4
  * A verb's `doc` doubles as its MCP tool description (`registerVerb`), so a doc
5
- * authored in CLI terms leaks flags an agent can't use — `--all-tiers`,
5
+ * authored in CLI terms leaks flags an agent can't use — `--category`,
6
6
  * `--dry-run`/`--yes`, or a `pragma …` shell command — into the agent-facing
7
7
  * catalog. Tool descriptions are NOT frozen in the covenant, so this is the guard
8
8
  * against the leak (and its recurrence). Storeless: listing tools never boots the
@@ -68,7 +68,11 @@ describe("MCP tool descriptions — no CLI-syntax leaks (doc→MCP residue)", ()
68
68
  const byName = new Map(
69
69
  descriptions.map((tool) => [tool.name, tool.description]),
70
70
  );
71
- expect(byName.get("block_list")).not.toContain("--all-tiers");
71
+ // `block_list` was the original leak site (`--all-tiers`); L-OPEN-9 removed
72
+ // the flag AND made the description declared content (`toolDescription` in
73
+ // pragma.conf.ts), so the guard widens to "no CLI flag of any name" — the
74
+ // failure mode a re-authored story description could reintroduce.
75
+ expect(byName.get("block_list")).not.toMatch(/--[a-z]/);
72
76
  expect(byName.get("graph_query")).not.toContain("pragma ontology list");
73
77
  expect(byName.get("setup")).not.toMatch(/--dry-run|--yes/);
74
78
  expect(byName.get("upgrade")).not.toContain("--dry-run");
@@ -4,8 +4,10 @@
4
4
  * A verb renders at one of {@link DETAIL_LEVELS}. The effective level is chosen
5
5
  * from four sources, most specific first: the `--detail` flag, the resolved
6
6
  * config's `detail`, the verb's own `disclosure.default`, and finally the
7
- * built-in `"standard"`. Unknown values (a stale config, a typo) fall back to
8
- * the default rather than erroring, so output never fails on disclosure.
7
+ * built-in `"standard"`. Every source is validated upstream (the flag by
8
+ * `readDetail`, the config field by `z.enum` at load, a spec default by the
9
+ * pack grammar), so the fall-back here is a last belt against a source added
10
+ * later without validation — output never fails on disclosure.
9
11
  */
10
12
 
11
13
  import {
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * The grammar's {@link Formatters} (plain / llm / json — no ink field) is the
5
5
  * per-verb output contract. {@link selectFormatter} picks one by the parsed
6
- * global flags with a fixed precedence: `--format json` wins, then `--llm`,
7
- * then the plain terminal default. The dispatcher uses the plain/llm branches
8
- * directly and wraps the json branch in the machine envelope (D3).
6
+ * global flags with a fixed precedence: `--format json` wins, then the llm mode
7
+ * (`--format llm` or auto-detected on a non-interactive stdout), then the plain
8
+ * terminal default. The dispatcher uses the plain/llm branches directly and
9
+ * wraps the json branch in the machine envelope (D3).
9
10
  */
10
11
 
11
12
  import type { GlobalFlags } from "../runtime/types.js";
@@ -14,7 +15,7 @@ import type { Formatters } from "../spec/types.js";
14
15
  /**
15
16
  * Pick the formatter for the active output mode.
16
17
  *
17
- * Precedence: `--format json` > `--llm` > plain.
18
+ * Precedence: `--format json` > llm (`--format llm` / auto-detected) > plain.
18
19
  *
19
20
  * @param flags - The parsed global flags for this invocation.
20
21
  * @param formatters - The verb's three-mode formatter set.
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { embeddedManifest } from "../runtime/graphpack/embedded.js";
3
+ import { DEFAULT_PREFIX_MAP } from "./prefixes.js";
4
+
5
+ describe("DEFAULT_PREFIX_MAP", () => {
6
+ it("is the standard RDF vocabulary plus the distribution's declared namespaces", () => {
7
+ // A literal pin, not a re-derivation: the point is that editing
8
+ // `pragma.conf.ts`'s `prefixes` is what moves this map, and that the
9
+ // standard half stays kernel-owned. A reviewer reads the values here.
10
+ expect(DEFAULT_PREFIX_MAP).toEqual({
11
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
12
+ owl: "http://www.w3.org/2002/07/owl#",
13
+ skos: "http://www.w3.org/2004/02/skos/core#",
14
+ ds: "https://ds.canonical.com/",
15
+ cs: "http://pragma.canonical.com/codestandards#",
16
+ });
17
+ });
18
+
19
+ it("binds every prefix to the same IRI the shipped pack does", () => {
20
+ // Two independently produced maps: this compiled-in constant, and the
21
+ // committed embedded artifact's harvested manifest. Neither derives the
22
+ // other, so agreement is a real claim.
23
+ //
24
+ // It is also the guard against a silent bundler skip. `scripts/bundle.ts`
25
+ // writes nothing when the content hash matches, and that hash covers pack
26
+ // SOURCES only — so declaring a namespace in `pragma.conf.ts` cannot, by
27
+ // itself, make the shipped pack bind it. Without this test that mismatch
28
+ // shows up as entities compacting to nothing; with it, it fails here.
29
+ const shipped = embeddedManifest().prefixes;
30
+ for (const [prefix, namespace] of Object.entries(DEFAULT_PREFIX_MAP)) {
31
+ expect(shipped[prefix], `pack binding for \`${prefix}:\``).toBe(
32
+ namespace,
33
+ );
34
+ }
35
+ });
36
+ });
@@ -1,21 +1,48 @@
1
1
  /**
2
- * The default display-compaction prefix set.
2
+ * The compiled-in prefix map — the CLI's prefixed↔full translation table.
3
3
  *
4
- * A minimal port of the v1 prefix map: only the namespaces the renderers use
5
- * to compact IRIs in output. The store's query prefix map (which layers in
6
- * package- and config-declared prefixes) is a store concern and joins when the
7
- * first store-backed capability lands — this display set is intentionally
8
- * small and static so the render layer stays store-free.
4
+ * NOT only display compaction, despite the name this module used to carry.
5
+ * Three readers make it behavioural, and every one of them predates any store:
6
+ * - `render/compactUri.ts` shortens IRIs for output.
7
+ * - `packs/iri.ts#resolveUri` expands a user-typed prefixed name into the IRI a
8
+ * generated SPARQL read filters on, so a namespace missing here makes
9
+ * `<noun> lookup <prefix>:<local>` fail with "Invalid prefix".
10
+ * - `packs/graphql/fetchGraphqlLookup.ts#expandPrefixed` expands the prefixed
11
+ * values a GraphQL lookup returns, so a namespace missing here silently
12
+ * leaks the *unexpanded* token into a field documented as a full IRI.
13
+ *
14
+ * Two halves, one map. The W3C namespaces below are universal vocabulary, so
15
+ * the kernel names them. The DOMAIN namespaces are the distribution's, so they
16
+ * are read from `pragma.conf.ts`'s `prefixes` — the same declaration that pins
17
+ * what `sources update` builds the store with, so what the CLI compacts and
18
+ * what the graph is indexed under cannot drift apart. `render/prefixes.test.ts`
19
+ * pins that agreement against the shipped pack's own manifest.
20
+ *
21
+ * The asymmetry, stated plainly: a PROJECT layer's `prefixes` reach the pack it
22
+ * builds — hence that pack's store session and index — but never this map. This
23
+ * is a compiled-in presentation default, read on the storeless `--help`/
24
+ * `__complete` fast path where no config layer is reachable at all.
9
25
  */
10
26
 
11
- /**
12
- * Namespaces compacted in list/lookup output. Keys are the prefixes shown to
13
- * the user (`ds:Button`); values are the absolute namespace IRIs matched.
14
- */
15
- export const DEFAULT_PREFIX_MAP: Readonly<Record<string, string>> = {
16
- ds: "https://ds.canonical.com/",
17
- cs: "http://pragma.canonical.com/codestandards#",
27
+ import conf from "../../../pragma.conf.js";
28
+
29
+ /** Standard RDF vocabulary — the same IRIs in every graph, domain or not. */
30
+ const STANDARD_PREFIXES: Readonly<Record<string, string>> = {
18
31
  rdfs: "http://www.w3.org/2000/01/rdf-schema#",
19
32
  owl: "http://www.w3.org/2002/07/owl#",
20
33
  skos: "http://www.w3.org/2004/02/skos/core#",
21
34
  };
35
+
36
+ /**
37
+ * Namespaces compacted and expanded across the CLI. Keys are the prefixes shown
38
+ * to the user (`rdfs:label`); values are the absolute namespace IRIs matched.
39
+ *
40
+ * The standard half is spread LAST so it wins a collision: `rdfs` means the
41
+ * same thing in every graph, and the kernel hardcodes those IRIs elsewhere
42
+ * (`buildIndex`'s label predicates, the prompt SELECT), so letting a
43
+ * declaration rebind them here would split one prefix across two meanings.
44
+ */
45
+ export const DEFAULT_PREFIX_MAP: Readonly<Record<string, string>> = {
46
+ ...conf.prefixes,
47
+ ...STANDARD_PREFIXES,
48
+ };
@@ -14,6 +14,7 @@ import {
14
14
  renderLookupLlm,
15
15
  renderLookupPlain,
16
16
  } from "./renderers.js";
17
+ import { type RenderStyle, styleFor } from "./style.js";
17
18
 
18
19
  interface Widget {
19
20
  readonly uri: string;
@@ -144,11 +145,44 @@ describe("render matrix (list/lookup/empty × plain/llm/json × detail)", () =>
144
145
  });
145
146
  });
146
147
 
148
+ describe("lookup beautify (TTY style seam)", () => {
149
+ const lookupOpts = {
150
+ title: (w: Widget) => w.name,
151
+ fields: fieldsFor("standard"),
152
+ sections: sectionsFor("standard"),
153
+ };
154
+
155
+ it("bolds the title, dims the rule, and cyans field labels on a TTY", () => {
156
+ const tagged: RenderStyle = {
157
+ enabled: true,
158
+ bold: (t) => `B(${t})`,
159
+ dim: (t) => `D(${t})`,
160
+ cyan: (t) => `C(${t})`,
161
+ green: (t) => t,
162
+ yellow: (t) => t,
163
+ };
164
+ const lines = renderLookupPlain(
165
+ widgets[0] as Widget,
166
+ lookupOpts,
167
+ tagged,
168
+ ).split("\n");
169
+ expect(lines.at(0)).toBe("B(Button)");
170
+ expect(lines.at(1)).toBe(`D(${"═".repeat(24)})`);
171
+ expect(lines).toContain(" C(Kind): component");
172
+ });
173
+
174
+ it("is byte-identical to the plain lookup when the styler is disabled", () => {
175
+ expect(
176
+ renderLookupPlain(widgets[0] as Widget, lookupOpts, styleFor(false)),
177
+ ).toBe(`Button\n${"═".repeat(24)}\n\n Kind: component`);
178
+ });
179
+ });
180
+
147
181
  describe("error render matrix (× plain/llm/json)", () => {
148
182
  it("renders each error across the three formats", () => {
149
183
  const notFound = PragmaError.notFound("block", "Buton", {
150
184
  suggestions: ["Button"],
151
- recovery: cliRecovery("pragma block list", "List available blocks."),
185
+ recovery: cliRecovery("block list", "List available blocks."),
152
186
  });
153
187
  const empty = PragmaError.emptyResults("token", {
154
188
  filters: { channel: "stable", tier: "core" },