@canonical/pragma-cli 0.33.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 (250) hide show
  1. package/README.md +7 -7
  2. package/dist/pragma +0 -0
  3. package/package.json +18 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +13 -4
  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.verb.ts +31 -13
  9. package/src/capabilities/capabilities/catalog.ts +18 -10
  10. package/src/capabilities/capabilities/hints.ts +13 -19
  11. package/src/capabilities/colophon/collectColophon.ts +34 -19
  12. package/src/capabilities/colophon/colophon.verb.ts +8 -4
  13. package/src/capabilities/colophon/types.ts +16 -7
  14. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  15. package/src/capabilities/config/collectConfigShow.ts +41 -4
  16. package/src/capabilities/config/fields.ts +0 -27
  17. package/src/capabilities/config/set.verb.ts +7 -3
  18. package/src/capabilities/config/show.render.test.ts +77 -5
  19. package/src/capabilities/config/show.render.ts +7 -12
  20. package/src/capabilities/config/show.verb.ts +4 -3
  21. package/src/capabilities/config/types.ts +8 -0
  22. package/src/capabilities/create/byteEquality.test.ts +78 -100
  23. package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
  24. package/src/capabilities/create/constants.ts +75 -0
  25. package/src/capabilities/create/create.mcp.test.ts +2 -2
  26. package/src/capabilities/create/create.test.ts +162 -15
  27. package/src/capabilities/create/create.verb.ts +140 -69
  28. package/src/capabilities/create/index.ts +2 -10
  29. package/src/capabilities/create/pickGenerator.ts +34 -28
  30. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  31. package/src/capabilities/create/types.ts +10 -6
  32. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  33. package/src/capabilities/distribution.test.ts +120 -0
  34. package/src/capabilities/distribution.ts +59 -0
  35. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  36. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  37. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
  38. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  39. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  40. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  41. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  42. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  43. package/src/capabilities/doctor/doctor.render.test.ts +6 -6
  44. package/src/capabilities/doctor/doctor.render.ts +2 -1
  45. package/src/capabilities/doctor/doctor.test.ts +37 -8
  46. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  47. package/src/capabilities/doctor/runChecks.ts +5 -5
  48. package/src/capabilities/doctor/types.ts +1 -1
  49. package/src/capabilities/emptyRecovery.test.ts +32 -15
  50. package/src/capabilities/graph/inspect.verb.ts +3 -2
  51. package/src/capabilities/graph/query.verb.ts +3 -2
  52. package/src/capabilities/graph/runQuery.ts +8 -5
  53. package/src/capabilities/index.ts +22 -18
  54. package/src/capabilities/info/collectInfo.ts +10 -6
  55. package/src/capabilities/info/info.render.ts +3 -2
  56. package/src/capabilities/info/info.verb.ts +3 -2
  57. package/src/capabilities/lazy.test.ts +93 -2
  58. package/src/capabilities/ontology/verbs.ts +21 -21
  59. package/src/capabilities/prompt/prompt.test.ts +12 -6
  60. package/src/capabilities/prompt/verbs.ts +24 -11
  61. package/src/capabilities/resources/provider.ts +30 -11
  62. package/src/capabilities/resources/resources.test.ts +85 -29
  63. package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
  64. package/src/capabilities/setup/operations/setupMcp.ts +2 -2
  65. package/src/capabilities/setup/operations/setupSkills.ts +3 -3
  66. package/src/capabilities/setup/setup.verb.ts +12 -5
  67. package/src/capabilities/setup/shell.ts +29 -3
  68. package/src/capabilities/skill/discover.ts +11 -5
  69. package/src/capabilities/skill/render.ts +4 -5
  70. package/src/capabilities/skill/verbs.ts +7 -7
  71. package/src/capabilities/sources/collectStatus.ts +55 -50
  72. package/src/capabilities/sources/index.ts +1 -1
  73. package/src/capabilities/sources/installSkills.ts +2 -2
  74. package/src/capabilities/sources/runUpdate.ts +113 -122
  75. package/src/capabilities/sources/sources.test.ts +111 -117
  76. package/src/capabilities/sources/status.render.test.ts +53 -24
  77. package/src/capabilities/sources/status.render.ts +24 -29
  78. package/src/capabilities/sources/status.verb.ts +15 -8
  79. package/src/capabilities/sources/types.ts +18 -25
  80. package/src/capabilities/sources/update.render.ts +9 -2
  81. package/src/capabilities/sources/update.verb.ts +12 -23
  82. package/src/capabilities/surface.test.ts +33 -10
  83. package/src/capabilities/tier.parity.test.ts +144 -0
  84. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  85. package/src/constants.test.ts +8 -6
  86. package/src/constants.ts +47 -21
  87. package/src/identity.test.ts +333 -0
  88. package/src/index.ts +2 -2
  89. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
  90. package/src/kernel/completion/emitScripts.test.ts +1 -1
  91. package/src/kernel/completion/emitScripts.ts +4 -2
  92. package/src/kernel/completion/entitySource.test.ts +185 -41
  93. package/src/kernel/completion/entitySource.ts +117 -107
  94. package/src/kernel/completion/model.test.ts +1 -1
  95. package/src/kernel/completion/model.ts +11 -0
  96. package/src/kernel/completion/safety.test.ts +82 -36
  97. package/src/kernel/completion/shellDrive.test.ts +592 -0
  98. package/src/kernel/completion/templates/fish.ts +28 -6
  99. package/src/kernel/completion/types.ts +3 -3
  100. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  101. package/src/kernel/config/defaults.test.ts +85 -0
  102. package/src/kernel/config/defaults.ts +12 -23
  103. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  104. package/src/kernel/config/findProjectConfig.ts +4 -3
  105. package/src/kernel/config/firstRun.ts +12 -5
  106. package/src/kernel/config/globalConfig.ts +2 -2
  107. package/src/kernel/config/index.ts +4 -5
  108. package/src/kernel/config/paths.ts +17 -17
  109. package/src/kernel/config/readConfig.test.ts +242 -6
  110. package/src/kernel/config/readConfig.ts +8 -8
  111. package/src/kernel/config/schema.test.ts +50 -0
  112. package/src/kernel/config/schema.ts +97 -10
  113. package/src/kernel/config/types.ts +73 -19
  114. package/src/kernel/copy.test.ts +453 -0
  115. package/src/kernel/error/PragmaError.test.ts +1 -1
  116. package/src/kernel/error/constants.ts +3 -2
  117. package/src/kernel/error/index.ts +1 -1
  118. package/src/kernel/error/recovery.test.ts +22 -45
  119. package/src/kernel/error/recovery.ts +22 -27
  120. package/src/kernel/error/types.ts +5 -4
  121. package/src/kernel/packs/collect.test.ts +195 -11
  122. package/src/kernel/packs/collect.ts +213 -58
  123. package/src/kernel/packs/compile.test.ts +163 -7
  124. package/src/kernel/packs/compile.ts +15 -12
  125. package/src/kernel/packs/disclosure.test.ts +5 -2
  126. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  127. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  128. package/src/kernel/packs/iri.ts +5 -7
  129. package/src/kernel/packs/renderPack.ts +5 -3
  130. package/src/kernel/packs/resolveEntity.ts +21 -17
  131. package/src/kernel/packs/runBodies.ts +15 -14
  132. package/src/kernel/packs/schema.ts +96 -10
  133. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  134. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  135. package/src/kernel/packs/toolDescription.test.ts +10 -2
  136. package/src/kernel/packs/types.ts +39 -4
  137. package/src/kernel/packs/uniqueness.ts +4 -4
  138. package/src/kernel/project/cli/buildProgram.test.ts +9 -4
  139. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  140. package/src/kernel/project/cli/dispatch.ts +34 -12
  141. package/src/kernel/project/cli/helpFormat.ts +1 -1
  142. package/src/kernel/project/cli/rootHelp.ts +105 -105
  143. package/src/kernel/project/mcp/buildServer.ts +11 -6
  144. package/src/kernel/project/mcp/instructions.ts +11 -6
  145. package/src/kernel/project/mcp/parity.test.ts +39 -1
  146. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  147. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  148. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  149. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  150. package/src/kernel/project/mcp/serve.ts +11 -3
  151. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  152. package/src/kernel/render/disclosure.ts +4 -2
  153. package/src/kernel/render/prefixes.test.ts +36 -0
  154. package/src/kernel/render/prefixes.ts +40 -13
  155. package/src/kernel/render/renderers.test.ts +1 -1
  156. package/src/kernel/runtime/graphpack/build.ts +29 -3
  157. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  158. package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
  159. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  160. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  161. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  162. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  163. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  164. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  165. package/src/kernel/runtime/graphpack/index.ts +1 -1
  166. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  167. package/src/kernel/runtime/graphpack/read.ts +4 -9
  168. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  169. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  170. package/src/kernel/runtime/graphpack/types.ts +61 -48
  171. package/src/kernel/runtime/paths.ts +64 -15
  172. package/src/kernel/runtime/probe.test.ts +8 -4
  173. package/src/kernel/runtime/readEntity.ts +6 -5
  174. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  175. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  176. package/src/kernel/runtime/refs/resolve.ts +78 -62
  177. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  178. package/src/kernel/runtime/resolveSources.ts +36 -25
  179. package/src/kernel/runtime/storeReadiness.ts +9 -8
  180. package/src/kernel/runtime/storeless.test.ts +1 -1
  181. package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
  182. package/src/kernel/spec/emitReference.test.ts +3 -3
  183. package/src/kernel/spec/emitReference.ts +144 -11
  184. package/src/kernel/spec/emitSurface.ts +9 -5
  185. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  186. package/src/kernel/spec/surfaceConformance.ts +8 -0
  187. package/src/kernel/spec/types.ts +29 -9
  188. package/src/kernel/spec/validate.ts +2 -1
  189. package/src/kernel/vocabulary.test.ts +164 -0
  190. package/src/kernel/vocabulary.ts +146 -0
  191. package/src/testing/PARITY_GAPS.ts +8 -2
  192. package/src/testing/behavioral/README.md +3 -3
  193. package/src/testing/behavioral/completion.test.ts +19 -6
  194. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  195. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  196. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  197. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  198. package/src/testing/behavioral/journeys.cli.test.ts +116 -75
  199. package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
  200. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  201. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  202. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  203. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  204. package/src/testing/behavioral/mcpSurface.test.ts +9 -4
  205. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  206. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  207. package/src/testing/eval/cases/assembly.ts +13 -6
  208. package/src/testing/eval/cases/effectful.ts +25 -13
  209. package/src/testing/eval/cases/readNouns.ts +22 -20
  210. package/src/testing/eval/cases/stable.ts +5 -1
  211. package/src/testing/eval/eval.test.ts +7 -1
  212. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  213. package/src/testing/fixtures/graph/canonical.ts +6 -3
  214. package/src/testing/fixtures/graph/defaultPack.ts +6 -5
  215. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  216. package/src/testing/helpers/fixtureGraph.ts +33 -16
  217. package/src/testing/helpers/packRuntime.ts +2 -3
  218. package/src/testing/helpers/projectCli.ts +2 -1
  219. package/src/testing/helpers/projectMcp.ts +6 -0
  220. package/src/testing/perf/budgets.test.ts +4 -0
  221. package/src/testing/perf/budgets.ts +22 -7
  222. package/src/testing/perf/globalSetup.ts +121 -6
  223. package/src/types.ts +2 -2
  224. package/src/capabilities/block/blockList.test.ts +0 -79
  225. package/src/capabilities/block/blockList.verb.ts +0 -163
  226. package/src/capabilities/block/index.ts +0 -25
  227. package/src/capabilities/block/pack.ts +0 -166
  228. package/src/capabilities/block/parity.test.ts +0 -187
  229. package/src/capabilities/block/tierChain.ts +0 -78
  230. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  231. package/src/capabilities/modifier/index.ts +0 -15
  232. package/src/capabilities/modifier/pack.ts +0 -64
  233. package/src/capabilities/standard/index.ts +0 -18
  234. package/src/capabilities/standard/pack.ts +0 -128
  235. package/src/capabilities/tier/index.ts +0 -19
  236. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  237. package/src/capabilities/tier/lookup.render.ts +0 -59
  238. package/src/capabilities/tier/lookup.test.ts +0 -83
  239. package/src/capabilities/tier/lookup.verb.ts +0 -44
  240. package/src/capabilities/tier/pack.ts +0 -28
  241. package/src/capabilities/tier/runLookup.ts +0 -87
  242. package/src/capabilities/token/addConfig.render.ts +0 -27
  243. package/src/capabilities/token/addConfig.test.ts +0 -62
  244. package/src/capabilities/token/addConfig.verb.ts +0 -40
  245. package/src/capabilities/token/index.ts +0 -19
  246. package/src/capabilities/token/pack.ts +0 -58
  247. package/src/capabilities/token/runAddConfig.ts +0 -95
  248. package/src/kernel/completion/bashDrive.test.ts +0 -73
  249. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  250. package/src/kernel/runtime/lock.ts +0 -90
@@ -1,13 +1,18 @@
1
1
  import { mkdtempSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
5
  import { afterEach, describe, expect, it } from "vitest";
6
+ import { CONVENTIONS } from "../../../../capabilities/capabilities/catalog.js";
5
7
  import { capabilities } from "../../../../capabilities/index.js";
6
8
  import type { McpHarness } from "../../../../testing/helpers/projectMcp.js";
7
9
  import { projectMcp } from "../../../../testing/helpers/projectMcp.js";
10
+ import type { PragmaRuntime } from "../../../runtime/types.js";
11
+ import { emitSurface } from "../../../spec/emitSurface.js";
8
12
  import type { CapabilityModule } from "../../../spec/types.js";
9
13
  import { buildInstructions, INSTRUCTIONS_MAX_CHARS } from "../instructions.js";
10
14
  import { fillTemplate, promptProvider } from "./provider.js";
15
+ import { readPrompts } from "./source.js";
11
16
 
12
17
  const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-prompts-"));
13
18
 
@@ -39,14 +44,25 @@ describe("instructions — handshake orientation (PROTECTED)", () => {
39
44
  );
40
45
  });
41
46
 
42
- it("derives its conventions from the shared catalog source", () => {
43
- // The system convention string is authored once (capabilities/catalog.ts);
44
- // instructions must carry it verbatim so the two can never diverge.
45
- expect(buildInstructions(capabilities)).toContain(
46
- "design-system knowledge graph",
47
+ it("opens with the shared catalog's conventions, verbatim and once", () => {
48
+ // The conventions are authored once (capabilities/catalog.ts); instructions
49
+ // must OPEN with them, so a second hand-written preamble cannot creep back
50
+ // in and drift from the `capabilities` tool the same handshake carries.
51
+ expect(buildInstructions(capabilities).startsWith(CONVENTIONS.system)).toBe(
52
+ true,
47
53
  );
48
54
  });
49
55
 
56
+ it("quotes the resource templates the MCP surface actually advertises", () => {
57
+ // The `pragma:` scheme is covenant-frozen protocol identity
58
+ // (surface.v2.json). The orientation DERIVES it from the emitted surface
59
+ // instead of hand-copying it, so the two cannot disagree.
60
+ const { resources } = emitSurface(capabilities).mcpSurface;
61
+ expect(resources.length).toBeGreaterThan(0);
62
+ const text = buildInstructions(capabilities);
63
+ for (const template of resources) expect(text).toContain(template);
64
+ });
65
+
50
66
  it("states the plan-first/confirm convention in the orientation (D2)", () => {
51
67
  // The confirm gate must be surfaced at handshake, not discovered by an agent
52
68
  // tripping it. Same single source as the `capabilities` tool's conventions.
@@ -91,6 +107,105 @@ describe("MCP handshake — capabilities advertised (PROTECTED)", () => {
91
107
  });
92
108
  });
93
109
 
110
+ /** A runtime whose SPARQL facade always fails with `message`. */
111
+ function createFailingRuntime(message: string): PragmaRuntime {
112
+ return {
113
+ query: {
114
+ sparql: () => Promise.reject(new Error(message)),
115
+ },
116
+ } as unknown as PragmaRuntime;
117
+ }
118
+
119
+ /** Every field at the default layer — the embedded pack answers, so the store IS available. */
120
+ const DEFAULT_ORIGINS = {
121
+ tier: "default",
122
+ channel: "default",
123
+ detail: "default",
124
+ packs: "default",
125
+ stories: "default",
126
+ prefixes: "default",
127
+ } as const;
128
+
129
+ /**
130
+ * The same failing facade, wearing the shape `promptProvider.register` needs:
131
+ * a cwd whose store is AVAILABLE (so `guardStore` passes and the read is what
132
+ * fails) plus a `loadConfig` the readiness check can resolve.
133
+ */
134
+ function createFailingProviderRuntime(
135
+ cwd: string,
136
+ message: string,
137
+ ): PragmaRuntime {
138
+ return {
139
+ cwd,
140
+ loadConfig: async () => ({
141
+ config: { channel: "normal" as const },
142
+ origins: DEFAULT_ORIGINS,
143
+ global: { path: "/nonexistent", exists: false },
144
+ project: { exists: false },
145
+ }),
146
+ query: { sparql: () => Promise.reject(new Error(message)) },
147
+ } as unknown as PragmaRuntime;
148
+ }
149
+
150
+ describe("native prompts/get — a failed read reaches the agent as one", () => {
151
+ it("carries the machine code and recovery, not a bare Error", async () => {
152
+ // The native surface signals failure by THROWING, so a read that fails must
153
+ // be projected the way the resource browser projects its own — with `code`
154
+ // and `recovery` in the JSON-RPC `data`. Otherwise the store diagnosis is
155
+ // lost between `prompt_lookup` (which envelopes it) and `prompts/get`.
156
+ const handlers = new Map<unknown, (request: unknown) => Promise<unknown>>();
157
+ const stubServer = {
158
+ server: {
159
+ registerCapabilities: () => {},
160
+ setRequestHandler: (
161
+ schema: unknown,
162
+ handler: (request: unknown) => Promise<unknown>,
163
+ ) => {
164
+ handlers.set(schema, handler);
165
+ },
166
+ },
167
+ };
168
+ await promptProvider.register(
169
+ stubServer as never,
170
+ createFailingProviderRuntime(freshCwd(), "Prefix not found: ds"),
171
+ );
172
+
173
+ const get = handlers.get(GetPromptRequestSchema);
174
+ expect(get).toBeDefined();
175
+ await expect(
176
+ get?.({ params: { name: "build-a-block" } }),
177
+ ).rejects.toMatchObject({
178
+ data: {
179
+ code: "STORE_UNAVAILABLE",
180
+ recovery: { mcp: { tool: "sources_update" } },
181
+ },
182
+ });
183
+ });
184
+ });
185
+
186
+ describe("readPrompts — a failed read is never an empty graph", () => {
187
+ it("reports an unbound prefix as a store that cannot answer, not as no prompts", async () => {
188
+ // A pack whose store does not know the declared prompt namespace cannot
189
+ // answer, and the actionable form of that is STORE_UNAVAILABLE with the
190
+ // build command — not a silent zero-prompt listing.
191
+ await expect(
192
+ readPrompts(createFailingRuntime("Prefix not found: ds")),
193
+ ).rejects.toMatchObject({
194
+ code: "STORE_UNAVAILABLE",
195
+ recovery: { mcp: { tool: "sources_update" } },
196
+ });
197
+ });
198
+
199
+ it("propagates a query failure instead of returning no prompts", async () => {
200
+ // The exact defect a bare `catch {}` here caused: a malformed declaration
201
+ // becomes a parse error, the parse error becomes `[]`, and the user is told
202
+ // the distribution ships no prompts. It must surface as a failure.
203
+ await expect(
204
+ readPrompts(createFailingRuntime("SPARQL syntax error at line 2")),
205
+ ).rejects.toThrow(/syntax error/);
206
+ });
207
+ });
208
+
94
209
  describe("fillTemplate — argument substitution", () => {
95
210
  it("replaces {{arg}} placeholders and leaves unknowns intact", () => {
96
211
  expect(
@@ -12,14 +12,17 @@
12
12
  * module — never pulls the SDK types onto the `--help`/`__complete` fast path.
13
13
  *
14
14
  * Both handlers open with a STORELESS cold-store pre-check ({@link
15
- * checkStoreAvailable}) so the native surface converges with the store-backed
16
- * `prompt_list` tool: a cold store surfaces the SAME `STORE_UNAVAILABLE` +
17
- * `sources_update` recovery here as the tool's `needsStore` guard raises, rather
18
- * than a silent `[]` (list) or a bogus "not found" (get) that masks the cold
19
- * store. When the store is available (including the embedded fallback) both
20
- * handlers behave exactly as before.
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.
21
22
  */
22
23
 
24
+ import { asPragmaError } from "../../../error/fromTaskError.js";
25
+ import { resolveSources } from "../../../runtime/resolveSources.js";
23
26
  import { checkStoreAvailable } from "../../../runtime/storeReadiness.js";
24
27
  import type { McpPromptProvider } from "../../../spec/types.js";
25
28
  import { mcpErrorFrom } from "../mcpError.js";
@@ -68,7 +71,9 @@ export const promptProvider: McpPromptProvider = {
68
71
  server.server.setRequestHandler(ListPromptsRequestSchema, async () => {
69
72
  await guardStore();
70
73
  return {
71
- prompts: listPromptSummaries(runtime).map((prompt) => ({
74
+ prompts: listPromptSummaries(
75
+ resolveSources(await runtime.loadConfig(), runtime.cwd),
76
+ ).map((prompt) => ({
72
77
  name: prompt.name,
73
78
  ...(prompt.description ? { description: prompt.description } : {}),
74
79
  })),
@@ -78,7 +83,16 @@ export const promptProvider: McpPromptProvider = {
78
83
  server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
79
84
  await guardStore();
80
85
  const { name, arguments: args } = request.params;
81
- 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
+ }
82
96
  if (!entry) {
83
97
  // Reached only when the store IS available — a genuine miss, not a cold
84
98
  // store (which `guardStore` already surfaced as STORE_UNAVAILABLE).
@@ -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 {
@@ -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
+ };
@@ -182,7 +182,7 @@ describe("error render matrix (× plain/llm/json)", () => {
182
182
  it("renders each error across the three formats", () => {
183
183
  const notFound = PragmaError.notFound("block", "Buton", {
184
184
  suggestions: ["Button"],
185
- recovery: cliRecovery("pragma block list", "List available blocks."),
185
+ recovery: cliRecovery("block list", "List available blocks."),
186
186
  });
187
187
  const empty = PragmaError.emptyResults("token", {
188
188
  filters: { channel: "stable", tier: "core" },