@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
@@ -8,12 +8,14 @@
8
8
  */
9
9
 
10
10
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
11
+ import { RECOVERY_CLI_PREFIX } from "../../constants.js";
11
12
  import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
12
13
  import type { PragmaRuntime } from "../runtime/types.js";
13
14
  import { compilePack } from "./compile.js";
14
15
  import type { LookupOutput } from "./resolveEntity.js";
15
16
  import { parsePackDefinition } from "./schema.js";
16
17
  import type { PackDefinition, PackRow } from "./types.js";
18
+ import { distributionSource } from "./types.js";
17
19
  import { assertUniqueVerbs, verbKey } from "./uniqueness.js";
18
20
 
19
21
  const PREFIXES = {
@@ -92,8 +94,33 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
92
94
  expect(parsePackDefinition(raw, "test:widget")).toEqual(WIDGET_PACK);
93
95
  });
94
96
 
97
+ it("round-trips a fixed-count sample — the field the no-argument samples declare", () => {
98
+ // `sample.fixedCount` is a documented grammar field (packs/types.ts) that
99
+ // three of the distribution's own stories use and the covenant freezes
100
+ // (`block sample` takes no argument). It was missing from the `.strict()`
101
+ // sample schema, so ANY config- or package-declared story using it died
102
+ // with a fatal CONFIG_ERROR before ever reaching the compiler.
103
+ const lookup = WIDGET_PACK.lookup as NonNullable<PackDefinition["lookup"]>;
104
+ const fixed: PackDefinition = {
105
+ ...WIDGET_PACK,
106
+ lookup: { ...lookup, sample: { fixedCount: true } },
107
+ };
108
+ expect(parsePackDefinition(JSON.parse(JSON.stringify(fixed)), "t")).toEqual(
109
+ fixed,
110
+ );
111
+ // …and the compiler honours it: no `[count]` positional on the sample verb.
112
+ const sample = compilePack(fixed, distributionSource("t"), PREFIXES).find(
113
+ (v) => verbKey(v.path) === "widget sample",
114
+ );
115
+ expect(sample?.params).toEqual([]);
116
+ });
117
+
95
118
  it("projects list, lookup, and sample verbs with unique keys", () => {
96
- const verbs = compilePack(WIDGET_PACK, "bundled:widget", PREFIXES);
119
+ const verbs = compilePack(
120
+ WIDGET_PACK,
121
+ distributionSource("bundled:widget"),
122
+ PREFIXES,
123
+ );
97
124
  expect(verbs.map((v) => verbKey(v.path))).toEqual([
98
125
  "widget list",
99
126
  "widget lookup",
@@ -103,7 +130,11 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
103
130
  });
104
131
 
105
132
  it("projects list filters as params (enum) + a search flag", () => {
106
- const [list] = compilePack(WIDGET_PACK, "bundled:widget", PREFIXES);
133
+ const [list] = compilePack(
134
+ WIDGET_PACK,
135
+ distributionSource("bundled:widget"),
136
+ PREFIXES,
137
+ );
107
138
  expect(list?.params.map((p) => p.name)).toEqual(["kind", "search"]);
108
139
  const kind = list?.params.find((p) => p.name === "kind");
109
140
  expect(kind?.kind).toBe("enum");
@@ -112,7 +143,11 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
112
143
  });
113
144
 
114
145
  it("projects the lookup as a variadic name-completing positional + disclosure", () => {
115
- const verbs = compilePack(WIDGET_PACK, "bundled:widget", PREFIXES);
146
+ const verbs = compilePack(
147
+ WIDGET_PACK,
148
+ distributionSource("bundled:widget"),
149
+ PREFIXES,
150
+ );
116
151
  const lookup = verbs.find((v) => verbKey(v.path) === "widget lookup");
117
152
  const name = lookup?.params.at(0);
118
153
  expect(name?.kind).toBe("string[]");
@@ -132,7 +167,7 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
132
167
  PackDefinition["lookup"]
133
168
  >;
134
169
  const nameParam = (def: PackDefinition) =>
135
- compilePack(def, "bundled:widget", PREFIXES)
170
+ compilePack(def, distributionSource("bundled:widget"), PREFIXES)
136
171
  .find((v) => verbKey(v.path) === "widget lookup")
137
172
  ?.params.at(0);
138
173
 
@@ -162,6 +197,125 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
162
197
  });
163
198
  });
164
199
 
200
+ describe("the grammar rejects what the compiler cannot build (PROTECTED)", () => {
201
+ // A definition that compiles to a duplicate `(noun, verb)` key or a duplicate
202
+ // CLI option is not merely odd — it throws out of `assembleEffectiveModules`
203
+ // or out of `buildProgram`, both of which run BEFORE the command tree exists.
204
+ // For a package-shipped story that means every command dies, `doctor` and
205
+ // `sources update` included. The collision is a property of the definition,
206
+ // so it is rejected here, where a package story degrades to a dropped file.
207
+ const listShape = {
208
+ query: "SELECT ?uri WHERE { ?uri a ex:Widget }",
209
+ columns: [{ field: "uri" }],
210
+ };
211
+ const parse = (def: unknown) => () =>
212
+ parsePackDefinition(def, "pkg/stories/widget.json");
213
+
214
+ it("rejects an extra verb that repeats list, lookup, sample, or itself", () => {
215
+ const lookup = { by: "ex:name", type: "ex:Widget", sample: true };
216
+ expect(
217
+ parse({
218
+ noun: "widget",
219
+ list: listShape,
220
+ verbs: [{ ...listShape, verb: "list" }],
221
+ }),
222
+ ).toThrow(/verb "list" is already compiled/);
223
+ expect(
224
+ parse({
225
+ noun: "widget",
226
+ lookup,
227
+ verbs: [{ ...listShape, verb: "lookup" }],
228
+ }),
229
+ ).toThrow(/verb "lookup" is already compiled/);
230
+ expect(
231
+ parse({
232
+ noun: "widget",
233
+ lookup,
234
+ verbs: [{ ...listShape, verb: "sample" }],
235
+ }),
236
+ ).toThrow(/verb "sample" is already compiled/);
237
+ expect(
238
+ parse({
239
+ noun: "widget",
240
+ list: listShape,
241
+ verbs: [
242
+ { ...listShape, verb: "categories" },
243
+ { ...listShape, verb: "categories" },
244
+ ],
245
+ }),
246
+ ).toThrow(/verb "categories" is already compiled/);
247
+ });
248
+
249
+ it("rejects a filter param declared twice, on list or on an extra verb", () => {
250
+ const twice = [
251
+ { param: "kind", variable: "uri" },
252
+ { param: "kind", variable: "uri" },
253
+ ];
254
+ expect(
255
+ parse({ noun: "widget", list: { ...listShape, filters: twice } }),
256
+ ).toThrow(/filter param "kind" is declared twice/);
257
+ expect(
258
+ parse({
259
+ noun: "widget",
260
+ list: listShape,
261
+ verbs: [{ ...listShape, verb: "categories", filters: twice }],
262
+ }),
263
+ ).toThrow(/filter param "kind" is declared twice/);
264
+ });
265
+
266
+ it("still accepts the distinct forms, and they compile to unique keys/params", () => {
267
+ const definition = parsePackDefinition(
268
+ {
269
+ noun: "widget",
270
+ list: {
271
+ ...listShape,
272
+ filters: [
273
+ { param: "kind", variable: "uri" },
274
+ { param: "tier", variable: "uri" },
275
+ ],
276
+ },
277
+ verbs: [{ ...listShape, verb: "categories" }],
278
+ lookup: { by: "ex:name", type: "ex:Widget", sample: true },
279
+ },
280
+ "pkg/stories/widget.json",
281
+ );
282
+ const verbs = compilePack(
283
+ definition,
284
+ { label: "pkg/stories/widget.json", origin: "package" },
285
+ PREFIXES,
286
+ );
287
+ expect(verbs.map((v) => verbKey(v.path))).toEqual([
288
+ "widget list",
289
+ "widget categories",
290
+ "widget lookup",
291
+ "widget sample",
292
+ ]);
293
+ expect(() => assertUniqueVerbs(verbs)).not.toThrow();
294
+ expect(verbs.at(0)?.params.map((p) => p.name)).toEqual(["kind", "tier"]);
295
+ });
296
+
297
+ it("names the shape a noun must have, not the regex", () => {
298
+ expect(parse({ noun: "Bad Noun", list: listShape })).toThrow(
299
+ /noun: must be lowercase kebab-case/,
300
+ );
301
+ });
302
+
303
+ it("rejects an emptyRecovery.cli that names a binary, and names the change", () => {
304
+ // The hint is rendered as `<consuming distribution> <cli>`, so a story
305
+ // carrying the old prefixed form would render `pragma pragma sources
306
+ // update`. Third-party packs are third-party DATA: they get an error that
307
+ // names the grammar change, not a silently doubled string.
308
+ const withCli = (cli: string) => ({
309
+ noun: "widget",
310
+ list: { ...listShape, emptyRecovery: { message: "None.", cli } },
311
+ });
312
+ expect(parse(withCli(`${RECOVERY_CLI_PREFIX}sources update`))).toThrow(
313
+ /WITHOUT the binary name/,
314
+ );
315
+ expect(parse(withCli("sources update"))).not.toThrow();
316
+ });
317
+ });
318
+
165
319
  describe("pack compiler — SPARQL fetch path (PROTECTED)", () => {
166
320
  let rt: PragmaRuntime;
167
321
 
@@ -178,9 +332,11 @@ describe("pack compiler — SPARQL fetch path (PROTECTED)", () => {
178
332
  });
179
333
 
180
334
  const run = <R>(verbLabel: string, params: Record<string, unknown>) => {
181
- const verb = compilePack(WIDGET_PACK, "bundled:widget", PREFIXES).find(
182
- (v) => verbKey(v.path) === `widget ${verbLabel}`,
183
- );
335
+ const verb = compilePack(
336
+ WIDGET_PACK,
337
+ distributionSource("bundled:widget"),
338
+ PREFIXES,
339
+ ).find((v) => verbKey(v.path) === `widget ${verbLabel}`);
184
340
  if (!verb) throw new Error(`no widget ${verbLabel}`);
185
341
  return verb.run(params, rt) as Promise<R>;
186
342
  };
@@ -7,12 +7,14 @@
7
7
  *
8
8
  * Pure, storeless spec-generation: it builds paths, params, formatters, and run
9
9
  * closures without touching the store or zod, so it runs on the
10
- * `--help`/`__complete` fast path (the bundled packs compile at import; the
11
- * dynamic merge is `collect.assembleEffectiveModules`/`loadEffectiveModules`).
10
+ * `--help`/`__complete` fast path (the distribution's declared stories compile
11
+ * at import, in `capabilities/distribution.ts`; the dynamic merge is
12
+ * `collect.assembleEffectiveModules`/`loadEffectiveModules`).
12
13
  * The heavy work is deferred into the run closures (behind the lazy runtime
13
14
  * facade).
14
15
  */
15
16
 
17
+ import { BIN_NAME } from "../../constants.js";
16
18
  import type { PragmaRuntime } from "../runtime/types.js";
17
19
  import { asVerb } from "../spec/asVerb.js";
18
20
  import type { DisclosureSpec, ParamSpec, VerbSpec } from "../spec/types.js";
@@ -35,6 +37,7 @@ import type {
35
37
  PackLookup,
36
38
  PackRow,
37
39
  PackSearch,
40
+ StorySource,
38
41
  } from "./types.js";
39
42
 
40
43
  // The run bodies pull the SPARQL/GraphQL fetch layer; they are dynamic-imported
@@ -62,7 +65,7 @@ const READ_CAPABILITY = {
62
65
  */
63
66
  export function compilePack(
64
67
  definition: PackDefinition,
65
- source: string,
68
+ source: StorySource,
66
69
  prefixes: Readonly<Record<string, string>>,
67
70
  ): VerbSpec[] {
68
71
  const { noun } = definition;
@@ -111,7 +114,7 @@ interface ListVerbMeta {
111
114
  readonly summary: string;
112
115
  /** The authored MCP tool description (from `toolDescription`), if any. */
113
116
  readonly doc?: string;
114
- readonly source: string;
117
+ readonly source: StorySource;
115
118
  readonly prefixes: Readonly<Record<string, string>>;
116
119
  }
117
120
 
@@ -135,17 +138,17 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
135
138
  }),
136
139
  },
137
140
  examples: [
138
- { cmd: `pragma ${meta.noun} ${meta.verb}` },
141
+ { cmd: `${BIN_NAME} ${meta.noun} ${meta.verb}` },
139
142
  ...(filterExample
140
143
  ? [
141
144
  {
142
- cmd: `pragma ${meta.noun} ${meta.verb} --${filterExample.param} ${quoteExample(
145
+ cmd: `${BIN_NAME} ${meta.noun} ${meta.verb} --${filterExample.param} ${quoteExample(
143
146
  filterExample.values?.at(0) ?? "",
144
147
  )}`,
145
148
  },
146
149
  ]
147
150
  : []),
148
- { cmd: `pragma ${meta.noun} ${meta.verb} --format llm` },
151
+ { cmd: `${BIN_NAME} ${meta.noun} ${meta.verb} --format llm` },
149
152
  ],
150
153
  capability: READ_CAPABILITY,
151
154
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
@@ -160,7 +163,7 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
160
163
  function compileLookupVerb(
161
164
  lookup: PackLookup,
162
165
  noun: string,
163
- source: string,
166
+ source: StorySource,
164
167
  prefixes: Readonly<Record<string, string>>,
165
168
  ): VerbSpec {
166
169
  // Derive-by-default: every lookup completes its `<name>` from the pack index
@@ -197,7 +200,7 @@ function compileLookupVerb(
197
200
  ...(lookup.toolDescription ? { doc: lookup.toolDescription } : {}),
198
201
  params: [nameParam],
199
202
  output: { formatters: lookupFormatters(lookup, prefixes) },
200
- examples: [{ cmd: `pragma ${noun} lookup <name>` }],
203
+ examples: [{ cmd: `${BIN_NAME} ${noun} lookup <name>` }],
201
204
  ...(lookup.disclosure
202
205
  ? { disclosure: disclosureSpec(lookup.disclosure) }
203
206
  : {}),
@@ -214,7 +217,7 @@ function compileLookupVerb(
214
217
  function compileSampleVerb(
215
218
  lookup: PackLookup,
216
219
  noun: string,
217
- source: string,
220
+ source: StorySource,
218
221
  prefixes: Readonly<Record<string, string>>,
219
222
  ): VerbSpec {
220
223
  const defaultCount = sampleDefaultCount(lookup);
@@ -241,8 +244,8 @@ function compileSampleVerb(
241
244
  params: countParam,
242
245
  output: { formatters: sampleFormatters(lookup, noun, prefixes) },
243
246
  examples: [
244
- { cmd: `pragma ${noun} sample` },
245
- ...(config?.fixedCount ? [] : [{ cmd: `pragma ${noun} sample 3` }]),
247
+ { cmd: `${BIN_NAME} ${noun} sample` },
248
+ ...(config?.fixedCount ? [] : [{ cmd: `${BIN_NAME} ${noun} sample 3` }]),
246
249
  ],
247
250
  capability: READ_CAPABILITY,
248
251
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
@@ -66,13 +66,16 @@ const PACK: PackDefinition = {
66
66
  };
67
67
 
68
68
  const lookupVerb = () =>
69
- compilePack(PACK, "test:widget", PREFIXES).find(
69
+ compilePack(PACK, { label: "test:widget", origin: "package" }, PREFIXES).find(
70
70
  (v) => verbKey(v.path) === "widget lookup",
71
71
  );
72
72
 
73
73
  async function lookupAt(
74
74
  detail?: "summary" | "standard" | "detailed",
75
- config?: { configDetail: string; detailOrigin: "global" | "project" },
75
+ config?: {
76
+ configDetail: "summary" | "standard" | "detailed";
77
+ detailOrigin: "global" | "project";
78
+ },
76
79
  ): Promise<Record<string, unknown>> {
77
80
  const { rt } = await buildFixtureRuntime({
78
81
  ttl: TTL,
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { PragmaError } from "../../error/PragmaError.js";
18
+ import { cliRecovery } from "../../error/recovery.js";
18
19
  import type { PragmaRuntime } from "../../runtime/types.js";
19
20
  import type { PackChildRow, PackEntity, PackLookup } from "../types.js";
20
21
  import {
@@ -55,12 +56,12 @@ export async function fetchGraphqlLookup(
55
56
  .map((error) => error.message)
56
57
  .join("; ");
57
58
  throw PragmaError.storeUnavailable(`GraphQL lookup failed: ${messages}`, {
58
- recovery: {
59
- message: "Check the loaded ontology and namespaces.",
60
- // `graph inspect` needs a <uri>; `ontology list` is runnable as-is.
61
- cli: "pragma ontology list",
62
- mcp: { tool: "ontology_list" },
63
- },
59
+ // `graph inspect` needs a <uri>; `ontology list` is runnable as-is.
60
+ recovery: cliRecovery(
61
+ "ontology list",
62
+ "Check the loaded ontology and namespaces.",
63
+ { tool: "ontology_list" },
64
+ ),
64
65
  });
65
66
  }
66
67
 
@@ -72,10 +73,10 @@ export async function fetchGraphqlLookup(
72
73
  throw new PragmaError({
73
74
  code: "ENTITY_NOT_FOUND",
74
75
  message: `Could not resolve <${entityUri}> — its rdf:type may be missing from the ontology.`,
75
- recovery: {
76
- message: "Inspect the entity's triples to check its rdf:type.",
77
- cli: `pragma graph inspect ${entityUri}`,
78
- },
76
+ recovery: cliRecovery(
77
+ `graph inspect ${entityUri}`,
78
+ "Inspect the entity's triples to check its rdf:type.",
79
+ ),
79
80
  });
80
81
  }
81
82
 
@@ -17,6 +17,7 @@ import { buildLookupDocument } from "./graphql/buildLookupDocument.js";
17
17
  import type { LookupOutput } from "./resolveEntity.js";
18
18
  import { parsePackDefinition } from "./schema.js";
19
19
  import type { PackDefinition, PackLookup } from "./types.js";
20
+ import { distributionSource } from "./types.js";
20
21
  import { verbKey } from "./uniqueness.js";
21
22
 
22
23
  const DS = "https://ds.canonical.com/";
@@ -292,9 +293,11 @@ describe("GraphQL lookup end to end + shape parity with SPARQL (PROTECTED)", ()
292
293
  });
293
294
 
294
295
  const lookupVia = (definition: PackDefinition, name: string) => {
295
- const verb = compilePack(definition, "t", BLOCK_PREFIXES).find(
296
- (v) => verbKey(v.path) === `${definition.noun} lookup`,
297
- );
296
+ const verb = compilePack(
297
+ definition,
298
+ distributionSource("t"),
299
+ BLOCK_PREFIXES,
300
+ ).find((v) => verbKey(v.path) === `${definition.noun} lookup`);
298
301
  if (!verb) throw new Error("no lookup verb");
299
302
  return verb.run({ name: [name] }, rt) as Promise<LookupOutput>;
300
303
  };
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { PragmaError } from "../error/PragmaError.js";
11
+ import { cliRecovery } from "../error/recovery.js";
11
12
 
12
13
  /** Characters not allowed inside a SPARQL `<IRI>`. */
13
14
  const UNSAFE_IRI_PATTERN = /[<>"{}|\\^`\s]/;
@@ -39,8 +40,7 @@ export function resolveUri(
39
40
  if (colonIdx === -1) {
40
41
  throw PragmaError.invalidInput("uri", uri, {
41
42
  recovery: {
42
- message:
43
- 'Use a prefixed URI (e.g. "ds:global.component.button") or a full URI.',
43
+ message: 'Use a prefixed URI (e.g. "prefix:name") or a full URI.',
44
44
  },
45
45
  });
46
46
  }
@@ -60,11 +60,9 @@ export function resolveUri(
60
60
 
61
61
  throw PragmaError.invalidInput("prefix", prefix, {
62
62
  validOptions: Object.keys(prefixes),
63
- recovery: {
64
- message: "List known ontology prefixes.",
65
- cli: "pragma ontology list",
66
- mcp: { tool: "ontology_list" },
67
- },
63
+ recovery: cliRecovery("ontology list", "List known ontology prefixes.", {
64
+ tool: "ontology_list",
65
+ }),
68
66
  });
69
67
  }
70
68
 
@@ -9,6 +9,7 @@
9
9
  * them (disclosure gates the fetch), never HOW it is laid out.
10
10
  */
11
11
 
12
+ import { BIN_NAME, RECOVERY_CLI_PREFIX } from "../../constants.js";
12
13
  import type {
13
14
  ColumnDef,
14
15
  LookupField,
@@ -52,8 +53,7 @@ export interface RenderMeta {
52
53
  * list on a BUILT store (a cold store would have failed with STORE_UNAVAILABLE
53
54
  * first) means "nothing matched", so point at both possible fixes.
54
55
  */
55
- const DEFAULT_EMPTY_HINT =
56
- "If you haven't built the store yet, run `pragma sources update`; otherwise broaden your filter or channel.";
56
+ const DEFAULT_EMPTY_HINT = `If you haven't built the store yet, run \`${BIN_NAME} sources update\`; otherwise broaden your filter or channel.`;
57
57
 
58
58
  /** Build the list formatters for a list-shaped verb (list or an extra verb). */
59
59
  export function listFormatters(
@@ -69,7 +69,9 @@ export function listFormatters(
69
69
  // `emptyRecovery` becomes the hint; otherwise the generic build/broaden hint.
70
70
  const emptyHint = shape.emptyRecovery
71
71
  ? `${shape.emptyRecovery.message}${
72
- shape.emptyRecovery.cli ? ` Run \`${shape.emptyRecovery.cli}\`.` : ""
72
+ shape.emptyRecovery.cli
73
+ ? ` Run \`${RECOVERY_CLI_PREFIX}${shape.emptyRecovery.cli}\`.`
74
+ : ""
73
75
  }`
74
76
  : DEFAULT_EMPTY_HINT;
75
77
  const options: RenderListOptions<PackRow> = {
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { PragmaError } from "../error/PragmaError.js";
16
+ import { cliRecovery } from "../error/recovery.js";
16
17
  import { suggestNames } from "../project/cli/suggestNames.js";
17
18
  import type { PragmaRuntime } from "../runtime/types.js";
18
19
  import { activeExpands } from "./disclosure.js";
@@ -27,7 +28,12 @@ import {
27
28
  buildNameResolveQuery,
28
29
  } from "./sparql/buildLookupQuery.js";
29
30
  import { runSelect } from "./sparql/runSelect.js";
30
- import type { PackChildRow, PackEntity, PackLookup } from "./types.js";
31
+ import type {
32
+ PackChildRow,
33
+ PackEntity,
34
+ PackLookup,
35
+ StorySource,
36
+ } from "./types.js";
31
37
 
32
38
  /** A structured per-query lookup failure (never rejects the whole batch). */
33
39
  export interface LookupError {
@@ -56,17 +62,15 @@ export async function resolveLookup(
56
62
  lookup: PackLookup,
57
63
  noun: string,
58
64
  queries: readonly string[],
59
- source: string,
65
+ source: StorySource,
60
66
  prefixes: Readonly<Record<string, string>>,
61
67
  level: string | undefined,
62
68
  ): Promise<LookupOutput> {
63
69
  if (queries.length === 0) {
64
70
  throw PragmaError.invalidInput("names", "(empty)", {
65
- recovery: {
66
- message: `List available ${noun} entries.`,
67
- cli: `pragma ${noun} list`,
68
- mcp: { tool: `${noun}_list` },
69
- },
71
+ recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
72
+ tool: `${noun}_list`,
73
+ }),
70
74
  });
71
75
  }
72
76
 
@@ -111,7 +115,7 @@ async function expandQueries(
111
115
  rt: LookupRuntime,
112
116
  lookup: PackLookup,
113
117
  noun: string,
114
- source: string,
118
+ source: StorySource,
115
119
  queries: readonly string[],
116
120
  ): Promise<{ names: string[]; globErrors: LookupError[] }> {
117
121
  if (!queries.some(isGlobPattern))
@@ -144,7 +148,7 @@ async function lookupOne(
144
148
  lookup: PackLookup,
145
149
  noun: string,
146
150
  query: string,
147
- source: string,
151
+ source: StorySource,
148
152
  prefixes: Readonly<Record<string, string>>,
149
153
  level: string | undefined,
150
154
  ): Promise<PackEntity> {
@@ -161,11 +165,9 @@ async function lookupOne(
161
165
  const candidates = await listEntityNames(rt, lookup, source);
162
166
  throw PragmaError.notFound(noun, query, {
163
167
  suggestions: suggestNames(query, candidates),
164
- recovery: {
165
- message: `List available ${noun} entries.`,
166
- cli: `pragma ${noun} list`,
167
- mcp: { tool: `${noun}_list` },
168
- },
168
+ recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
169
+ tool: `${noun}_list`,
170
+ }),
169
171
  });
170
172
  }
171
173
 
@@ -175,7 +177,9 @@ async function lookupOne(
175
177
  lookup,
176
178
  base.uri,
177
179
  base.name ?? query,
178
- source,
180
+ // The GraphQL lane's only use of the source is CONFIG_ERROR attribution,
181
+ // which is right for any origin — so it takes the label, not provenance.
182
+ source.label,
179
183
  prefixes,
180
184
  level,
181
185
  );
@@ -205,7 +209,7 @@ function buildEntityQuery(
205
209
  throw PragmaError.invalidInput("name", query, {
206
210
  recovery: {
207
211
  message:
208
- "Use an absolute IRI (https://…), a prefixed name (ds:thing), or a plain entity name.",
212
+ "Use an absolute IRI (https://…), a prefixed name (prefix:local), or a plain entity name.",
209
213
  },
210
214
  });
211
215
  }
@@ -225,7 +229,7 @@ function looksLikeIri(query: string): boolean {
225
229
  export async function listEntityNames(
226
230
  rt: LookupRuntime,
227
231
  lookup: PackLookup,
228
- source: string,
232
+ source: StorySource,
229
233
  ): Promise<string[]> {
230
234
  const rows = await runSelect(rt, buildLookupNamesQuery(lookup), source);
231
235
  return rows.map((row) => row.name ?? "").filter((name) => name !== "");
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { PragmaError } from "../error/PragmaError.js";
13
+ import { cliRecovery } from "../error/recovery.js";
13
14
  import type { PragmaRuntime } from "../runtime/types.js";
14
15
  import { resolvePackDetail } from "./disclosure.js";
15
16
  import type { SampleOutput } from "./renderPack.js";
@@ -22,14 +23,14 @@ import { parseSampleCount, pickRandom } from "./sample.js";
22
23
  import { applyPackFilters } from "./sparql/applyFilters.js";
23
24
  import { applyPackSearch } from "./sparql/applySearch.js";
24
25
  import { runSelect } from "./sparql/runSelect.js";
25
- import type { PackList, PackLookup, PackRow } from "./types.js";
26
+ import type { PackList, PackLookup, PackRow, StorySource } from "./types.js";
26
27
 
27
28
  /** The highest canonical level — sample fetches everything for shape discovery. */
28
29
  const HIGHEST_LEVEL = "detailed";
29
30
 
30
31
  /** Facts a list-shaped run body needs beyond its `shape`. */
31
32
  export interface ListRunMeta {
32
- readonly source: string;
33
+ readonly source: StorySource;
33
34
  }
34
35
 
35
36
  /**
@@ -61,7 +62,7 @@ export function makeListRun(
61
62
  export function makeLookupRun(
62
63
  lookup: PackLookup,
63
64
  noun: string,
64
- source: string,
65
+ source: StorySource,
65
66
  prefixes: Readonly<Record<string, string>>,
66
67
  ): (
67
68
  params: Record<string, unknown>,
@@ -88,11 +89,11 @@ export function makeLookupRun(
88
89
  code: first.code as PragmaError["code"],
89
90
  message: first.message,
90
91
  suggestions: first.suggestions ? [...first.suggestions] : undefined,
91
- recovery: {
92
- message: `List available ${noun} entries.`,
93
- cli: `pragma ${noun} list`,
94
- mcp: { tool: `${noun}_list` },
95
- },
92
+ recovery: cliRecovery(
93
+ `${noun} list`,
94
+ `List available ${noun} entries.`,
95
+ { tool: `${noun}_list` },
96
+ ),
96
97
  });
97
98
  }
98
99
  }
@@ -104,7 +105,7 @@ export function makeLookupRun(
104
105
  export function makeSampleRun(
105
106
  lookup: PackLookup,
106
107
  noun: string,
107
- source: string,
108
+ source: StorySource,
108
109
  prefixes: Readonly<Record<string, string>>,
109
110
  defaultCount: number,
110
111
  ): (
@@ -119,11 +120,11 @@ export function makeSampleRun(
119
120
  if (names.length === 0) {
120
121
  throw PragmaError.emptyResults(noun, {
121
122
  message: `No ${noun} entries to sample.`,
122
- recovery: {
123
- message: `List available ${noun} entries.`,
124
- cli: `pragma ${noun} list`,
125
- mcp: { tool: `${noun}_list` },
126
- },
123
+ recovery: cliRecovery(
124
+ `${noun} list`,
125
+ `List available ${noun} entries.`,
126
+ { tool: `${noun}_list` },
127
+ ),
127
128
  });
128
129
  }
129
130
  const selected = pickRandom(names, count);