@canonical/pragma-cli 0.38.0 → 0.40.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 (330) hide show
  1. package/README.md +1 -1
  2. package/dist/package.json +14 -14
  3. package/dist/pragma.conf.js +273 -176
  4. package/dist/pragma.conf.js.map +1 -1
  5. package/dist/src/capabilities/capabilities/capabilities.verb.js +2 -0
  6. package/dist/src/capabilities/capabilities/capabilities.verb.js.map +1 -1
  7. package/dist/src/capabilities/capabilities/catalog.js +47 -51
  8. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  9. package/dist/src/capabilities/capabilities/index.js +2 -0
  10. package/dist/src/capabilities/capabilities/index.js.map +1 -1
  11. package/dist/src/capabilities/capabilities/types.js +5 -5
  12. package/dist/src/capabilities/colophon/colophon.render.js +7 -4
  13. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  14. package/dist/src/capabilities/colophon/colophon.verb.js +1 -0
  15. package/dist/src/capabilities/colophon/colophon.verb.js.map +1 -1
  16. package/dist/src/capabilities/config/get.verb.js +2 -0
  17. package/dist/src/capabilities/config/get.verb.js.map +1 -1
  18. package/dist/src/capabilities/config/runField.js +2 -2
  19. package/dist/src/capabilities/config/runField.js.map +1 -1
  20. package/dist/src/capabilities/config/set.verb.js +2 -0
  21. package/dist/src/capabilities/config/set.verb.js.map +1 -1
  22. package/dist/src/capabilities/config/show.verb.js +1 -0
  23. package/dist/src/capabilities/config/show.verb.js.map +1 -1
  24. package/dist/src/capabilities/config/unset.verb.js +2 -0
  25. package/dist/src/capabilities/config/unset.verb.js.map +1 -1
  26. package/dist/src/capabilities/create/constants.js +2 -2
  27. package/dist/src/capabilities/create/create.verb.js +14 -4
  28. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  29. package/dist/src/capabilities/distribution.js +3 -1
  30. package/dist/src/capabilities/distribution.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +10 -2
  32. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  33. package/dist/src/capabilities/doctor/doctor.verb.js +2 -0
  34. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  35. package/dist/src/capabilities/graph/connect.verb.js +2 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -1
  37. package/dist/src/capabilities/graph/inspect.render.js +7 -1
  38. package/dist/src/capabilities/graph/inspect.render.js.map +1 -1
  39. package/dist/src/capabilities/graph/inspect.verb.js +2 -0
  40. package/dist/src/capabilities/graph/inspect.verb.js.map +1 -1
  41. package/dist/src/capabilities/graph/query.render.js +37 -5
  42. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  43. package/dist/src/capabilities/graph/query.verb.js +2 -0
  44. package/dist/src/capabilities/graph/query.verb.js.map +1 -1
  45. package/dist/src/capabilities/graph/queryText.js +28 -0
  46. package/dist/src/capabilities/graph/queryText.js.map +1 -1
  47. package/dist/src/capabilities/graph/runConnect.js +3 -3
  48. package/dist/src/capabilities/graph/runConnect.js.map +1 -1
  49. package/dist/src/capabilities/graph/runQuery.js +8 -6
  50. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  51. package/dist/src/capabilities/info/info.verb.js +2 -0
  52. package/dist/src/capabilities/info/info.verb.js.map +1 -1
  53. package/dist/src/capabilities/meta/mcp.verb.js +1 -0
  54. package/dist/src/capabilities/meta/mcp.verb.js.map +1 -1
  55. package/dist/src/capabilities/ontology/verbs.js +7 -10
  56. package/dist/src/capabilities/ontology/verbs.js.map +1 -1
  57. package/dist/src/capabilities/prompt/verbs.js +5 -4
  58. package/dist/src/capabilities/prompt/verbs.js.map +1 -1
  59. package/dist/src/capabilities/resources/provider.js +6 -3
  60. package/dist/src/capabilities/resources/provider.js.map +1 -1
  61. package/dist/src/capabilities/setup/setup.verb.js +13 -5
  62. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  63. package/dist/src/capabilities/shared/calls.js +16 -0
  64. package/dist/src/capabilities/shared/calls.js.map +1 -0
  65. package/dist/src/capabilities/skill/render.js +7 -5
  66. package/dist/src/capabilities/skill/render.js.map +1 -1
  67. package/dist/src/capabilities/skill/verbs.js +5 -4
  68. package/dist/src/capabilities/skill/verbs.js.map +1 -1
  69. package/dist/src/capabilities/sources/collectStatus.js +6 -1
  70. package/dist/src/capabilities/sources/collectStatus.js.map +1 -1
  71. package/dist/src/capabilities/sources/index.js +4 -3
  72. package/dist/src/capabilities/sources/index.js.map +1 -1
  73. package/dist/src/capabilities/sources/reset.render.js +37 -0
  74. package/dist/src/capabilities/sources/reset.render.js.map +1 -0
  75. package/dist/src/capabilities/sources/reset.verb.js +51 -0
  76. package/dist/src/capabilities/sources/reset.verb.js.map +1 -0
  77. package/dist/src/capabilities/sources/runReset.js +59 -0
  78. package/dist/src/capabilities/sources/runReset.js.map +1 -0
  79. package/dist/src/capabilities/sources/runUpdate.js +7 -6
  80. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  81. package/dist/src/capabilities/sources/status.render.js +24 -8
  82. package/dist/src/capabilities/sources/status.render.js.map +1 -1
  83. package/dist/src/capabilities/sources/status.verb.js +1 -0
  84. package/dist/src/capabilities/sources/status.verb.js.map +1 -1
  85. package/dist/src/capabilities/sources/update.verb.js +2 -0
  86. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  87. package/dist/src/capabilities/upgrade/upgrade.verb.js +1 -0
  88. package/dist/src/capabilities/upgrade/upgrade.verb.js.map +1 -1
  89. package/dist/src/capabilities/version/version.verb.js +1 -0
  90. package/dist/src/capabilities/version/version.verb.js.map +1 -1
  91. package/dist/src/constants.js +4 -3
  92. package/dist/src/constants.js.map +1 -1
  93. package/dist/src/kernel/error/index.js +1 -1
  94. package/dist/src/kernel/error/index.js.map +1 -1
  95. package/dist/src/kernel/error/recovery.js +24 -42
  96. package/dist/src/kernel/error/recovery.js.map +1 -1
  97. package/dist/src/kernel/packs/collect.js +123 -9
  98. package/dist/src/kernel/packs/collect.js.map +1 -1
  99. package/dist/src/kernel/packs/compile.js +74 -15
  100. package/dist/src/kernel/packs/compile.js.map +1 -1
  101. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js +3 -3
  102. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js.map +1 -1
  103. package/dist/src/kernel/packs/iri.js +2 -4
  104. package/dist/src/kernel/packs/iri.js.map +1 -1
  105. package/dist/src/kernel/packs/renderPack.js +90 -50
  106. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  107. package/dist/src/kernel/packs/resolveEntity.js +202 -90
  108. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  109. package/dist/src/kernel/packs/runBodies.js +89 -30
  110. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  111. package/dist/src/kernel/packs/schema.js +66 -12
  112. package/dist/src/kernel/packs/schema.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildListQuery.js +80 -13
  114. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +41 -13
  116. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  117. package/dist/src/kernel/packs/sparql/filterValues.js +32 -23
  118. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -1
  119. package/dist/src/kernel/packs/sparql/runSelect.js +3 -5
  120. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  121. package/dist/src/kernel/packs/storyRules.js +31 -4
  122. package/dist/src/kernel/packs/storyRules.js.map +1 -1
  123. package/dist/src/kernel/packs/tierScope.js +3 -3
  124. package/dist/src/kernel/packs/tierScope.js.map +1 -1
  125. package/dist/src/kernel/packs/types.js +4 -0
  126. package/dist/src/kernel/packs/types.js.map +1 -1
  127. package/dist/src/kernel/project/cli/buildProgram.js +4 -0
  128. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  129. package/dist/src/kernel/project/cli/dispatch.js +32 -8
  130. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  131. package/dist/src/kernel/project/cli/suggestNames.js +44 -1
  132. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  133. package/dist/src/kernel/project/cli/verbHelp.js +17 -18
  134. package/dist/src/kernel/project/cli/verbHelp.js.map +1 -1
  135. package/dist/src/kernel/project/mcp/buildServer.js +11 -4
  136. package/dist/src/kernel/project/mcp/buildServer.js.map +1 -1
  137. package/dist/src/kernel/project/mcp/envelope.js +3 -2
  138. package/dist/src/kernel/project/mcp/envelope.js.map +1 -1
  139. package/dist/src/kernel/project/mcp/instructions.js +101 -31
  140. package/dist/src/kernel/project/mcp/instructions.js.map +1 -1
  141. package/dist/src/kernel/project/mcp/registerVerb.js +59 -23
  142. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  143. package/dist/src/kernel/render/renderers.js +3 -3
  144. package/dist/src/kernel/render/turtle.js +21 -4
  145. package/dist/src/kernel/render/turtle.js.map +1 -1
  146. package/dist/src/kernel/runtime/graphpack/build.js +54 -1
  147. package/dist/src/kernel/runtime/graphpack/build.js.map +1 -1
  148. package/dist/src/kernel/runtime/graphpack/manifest.js +14 -0
  149. package/dist/src/kernel/runtime/graphpack/manifest.js.map +1 -1
  150. package/dist/src/kernel/runtime/graphpack/read.js +3 -5
  151. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  152. package/dist/src/kernel/runtime/graphpack/schemas.js +1 -0
  153. package/dist/src/kernel/runtime/graphpack/schemas.js.map +1 -1
  154. package/dist/src/kernel/runtime/loadSession.js +8 -1
  155. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  156. package/dist/src/kernel/runtime/packVersion.js +75 -0
  157. package/dist/src/kernel/runtime/packVersion.js.map +1 -0
  158. package/dist/src/kernel/runtime/readEntity.js +43 -4
  159. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  160. package/dist/src/kernel/runtime/resolveSources.js +61 -9
  161. package/dist/src/kernel/runtime/resolveSources.js.map +1 -1
  162. package/dist/src/kernel/runtime/storeReadiness.js +3 -2
  163. package/dist/src/kernel/runtime/storeReadiness.js.map +1 -1
  164. package/dist/src/kernel/spec/call.js +167 -0
  165. package/dist/src/kernel/spec/call.js.map +1 -0
  166. package/dist/src/kernel/spec/emitReference.js +9 -13
  167. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  168. package/dist/src/kernel/spec/emitSurface.js +8 -1
  169. package/dist/src/kernel/spec/emitSurface.js.map +1 -1
  170. package/dist/src/kernel/spec/guidance.js +63 -0
  171. package/dist/src/kernel/spec/guidance.js.map +1 -0
  172. package/dist/src/kernel/spec/wireType.js +36 -0
  173. package/dist/src/kernel/spec/wireType.js.map +1 -0
  174. package/package.json +14 -14
  175. package/pragma.conf.ts +294 -177
  176. package/src/capabilities/block.parity.test.ts +1 -1
  177. package/src/capabilities/callRule.test.ts +147 -0
  178. package/src/capabilities/capabilities/capabilities.test.ts +47 -19
  179. package/src/capabilities/capabilities/capabilities.verb.ts +2 -0
  180. package/src/capabilities/capabilities/catalog.ts +60 -55
  181. package/src/capabilities/capabilities/index.ts +2 -0
  182. package/src/capabilities/capabilities/types.ts +16 -19
  183. package/src/capabilities/colophon/colophon.render.ts +9 -5
  184. package/src/capabilities/colophon/colophon.verb.ts +1 -0
  185. package/src/capabilities/config/get.verb.ts +2 -0
  186. package/src/capabilities/config/runField.ts +3 -3
  187. package/src/capabilities/config/set.verb.ts +3 -0
  188. package/src/capabilities/config/show.verb.ts +2 -0
  189. package/src/capabilities/config/unset.verb.ts +2 -0
  190. package/src/capabilities/create/constants.ts +2 -2
  191. package/src/capabilities/create/create.verb.ts +16 -0
  192. package/src/capabilities/create/createGrammar.test.ts +1 -1
  193. package/src/capabilities/distribution.test.ts +6 -3
  194. package/src/capabilities/distribution.ts +6 -0
  195. package/src/capabilities/doctor/checks/checkPackageRefs.ts +14 -2
  196. package/src/capabilities/doctor/doctor.test.ts +38 -1
  197. package/src/capabilities/doctor/doctor.verb.ts +2 -0
  198. package/src/capabilities/emptyRecovery.test.ts +5 -0
  199. package/src/capabilities/graph/connect.verb.ts +2 -0
  200. package/src/capabilities/graph/inspect.render.ts +9 -1
  201. package/src/capabilities/graph/inspect.test.ts +73 -0
  202. package/src/capabilities/graph/inspect.verb.ts +2 -0
  203. package/src/capabilities/graph/query.render.test.ts +35 -0
  204. package/src/capabilities/graph/query.render.ts +45 -7
  205. package/src/capabilities/graph/query.shipped.exec.test.ts +44 -0
  206. package/src/capabilities/graph/query.verb.ts +2 -0
  207. package/src/capabilities/graph/queryText.test.ts +55 -1
  208. package/src/capabilities/graph/queryText.ts +47 -0
  209. package/src/capabilities/graph/runConnect.ts +4 -5
  210. package/src/capabilities/graph/runQuery.ts +23 -9
  211. package/src/capabilities/info/info.verb.ts +2 -0
  212. package/src/capabilities/links.shipped.exec.test.ts +398 -0
  213. package/src/capabilities/listBudget.shipped.exec.test.ts +2 -2
  214. package/src/capabilities/listLookup.shipped.exec.test.ts +122 -13
  215. package/src/capabilities/listQuery.shipped.exec.test.ts +39 -16
  216. package/src/capabilities/lookupBudget.shipped.exec.test.ts +208 -0
  217. package/src/capabilities/meta/mcp.verb.ts +2 -0
  218. package/src/capabilities/ontology/ontology.test.ts +24 -0
  219. package/src/capabilities/ontology/verbs.ts +16 -12
  220. package/src/capabilities/prompt/verbs.ts +10 -4
  221. package/src/capabilities/resources/provider.ts +7 -3
  222. package/src/capabilities/resources/resources.test.ts +2 -1
  223. package/src/capabilities/setup/setup.verb.ts +16 -0
  224. package/src/capabilities/shared/calls.ts +20 -0
  225. package/src/capabilities/skill/render.ts +9 -6
  226. package/src/capabilities/skill/verbs.ts +8 -4
  227. package/src/capabilities/sources/collectStatus.ts +6 -1
  228. package/src/capabilities/sources/index.ts +4 -3
  229. package/src/capabilities/sources/reset.render.test.ts +33 -0
  230. package/src/capabilities/sources/reset.render.ts +43 -0
  231. package/src/capabilities/sources/reset.verb.ts +59 -0
  232. package/src/capabilities/sources/runReset.ts +65 -0
  233. package/src/capabilities/sources/runUpdate.ts +12 -11
  234. package/src/capabilities/sources/sources.test.ts +158 -3
  235. package/src/capabilities/sources/status.render.test.ts +63 -1
  236. package/src/capabilities/sources/status.render.ts +25 -8
  237. package/src/capabilities/sources/status.verb.ts +2 -0
  238. package/src/capabilities/sources/types.ts +26 -0
  239. package/src/capabilities/sources/update.verb.ts +3 -0
  240. package/src/capabilities/surface.test.ts +2 -2
  241. package/src/capabilities/token.parity.test.ts +3 -2
  242. package/src/capabilities/tokenGraph.shipped.exec.test.ts +14 -2
  243. package/src/capabilities/upgrade/upgrade.verb.ts +1 -0
  244. package/src/capabilities/variable.parity.test.ts +2 -1
  245. package/src/capabilities/version/version.verb.ts +1 -0
  246. package/src/capabilities/wireContract.test.ts +110 -0
  247. package/src/constants.ts +4 -3
  248. package/src/kernel/copy.test.ts +1 -1
  249. package/src/kernel/error/PragmaError.test.ts +7 -3
  250. package/src/kernel/error/index.ts +1 -1
  251. package/src/kernel/error/recovery.test.ts +52 -18
  252. package/src/kernel/error/recovery.ts +25 -46
  253. package/src/kernel/error/types.ts +1 -1
  254. package/src/kernel/packs/collect.test.ts +79 -0
  255. package/src/kernel/packs/collect.ts +151 -11
  256. package/src/kernel/packs/compile.test.ts +107 -10
  257. package/src/kernel/packs/compile.ts +100 -11
  258. package/src/kernel/packs/filteredEmpty.test.ts +22 -5
  259. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +5 -6
  260. package/src/kernel/packs/graphqlSource.test.ts +42 -0
  261. package/src/kernel/packs/iri.ts +5 -4
  262. package/src/kernel/packs/listNotice.test.ts +1 -1
  263. package/src/kernel/packs/nounLink.test.ts +385 -0
  264. package/src/kernel/packs/renderPack.ts +137 -57
  265. package/src/kernel/packs/resolveEntity.test.ts +170 -1
  266. package/src/kernel/packs/resolveEntity.ts +281 -101
  267. package/src/kernel/packs/runBodies.ts +133 -31
  268. package/src/kernel/packs/schema.ts +77 -12
  269. package/src/kernel/packs/sparql/buildListQuery.test.ts +78 -1
  270. package/src/kernel/packs/sparql/buildListQuery.ts +106 -16
  271. package/src/kernel/packs/sparql/buildLookupQuery.ts +44 -13
  272. package/src/kernel/packs/sparql/filterValues.test.ts +36 -4
  273. package/src/kernel/packs/sparql/filterValues.ts +50 -31
  274. package/src/kernel/packs/sparql/runSelect.ts +4 -6
  275. package/src/kernel/packs/storyRules.ts +37 -4
  276. package/src/kernel/packs/tierScope.test.ts +120 -6
  277. package/src/kernel/packs/tierScope.ts +5 -7
  278. package/src/kernel/packs/toolDescription.test.ts +59 -74
  279. package/src/kernel/packs/types.ts +93 -11
  280. package/src/kernel/project/cli/buildProgram.ts +4 -0
  281. package/src/kernel/project/cli/dispatch.ts +34 -8
  282. package/src/kernel/project/cli/suggest.test.ts +77 -0
  283. package/src/kernel/project/cli/suggestNames.ts +47 -1
  284. package/src/kernel/project/cli/verbHelp.ts +16 -19
  285. package/src/kernel/project/mcp/buildServer.ts +15 -4
  286. package/src/kernel/project/mcp/envelope.ts +3 -2
  287. package/src/kernel/project/mcp/instructions.ts +123 -35
  288. package/src/kernel/project/mcp/prompts/prompts.test.ts +136 -20
  289. package/src/kernel/project/mcp/registerVerb.test.ts +28 -0
  290. package/src/kernel/project/mcp/registerVerb.ts +67 -29
  291. package/src/kernel/render/__snapshots__/renderers.test.ts.snap +1 -1
  292. package/src/kernel/render/renderers.test.ts +2 -2
  293. package/src/kernel/render/renderers.ts +3 -3
  294. package/src/kernel/render/turtle.ts +24 -4
  295. package/src/kernel/runtime/graphpack/build.ts +58 -1
  296. package/src/kernel/runtime/graphpack/graphpack.test.ts +20 -0
  297. package/src/kernel/runtime/graphpack/manifest.ts +15 -0
  298. package/src/kernel/runtime/graphpack/read.ts +4 -6
  299. package/src/kernel/runtime/graphpack/schemas.ts +1 -0
  300. package/src/kernel/runtime/graphpack/types.ts +13 -0
  301. package/src/kernel/runtime/loadSession.ts +8 -1
  302. package/src/kernel/runtime/packVersion.test.ts +61 -0
  303. package/src/kernel/runtime/packVersion.ts +84 -0
  304. package/src/kernel/runtime/readEntity.ts +61 -6
  305. package/src/kernel/runtime/resolveSources.test.ts +90 -7
  306. package/src/kernel/runtime/resolveSources.ts +91 -10
  307. package/src/kernel/runtime/storeReadiness.ts +4 -4
  308. package/src/kernel/runtime/types.ts +12 -0
  309. package/src/kernel/spec/call.test.ts +95 -0
  310. package/src/kernel/spec/call.ts +186 -0
  311. package/src/kernel/spec/emitReference.ts +9 -13
  312. package/src/kernel/spec/emitSurface.ts +9 -1
  313. package/src/kernel/spec/guidance.test.ts +49 -0
  314. package/src/kernel/spec/guidance.ts +68 -0
  315. package/src/kernel/spec/index.ts +4 -0
  316. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  317. package/src/kernel/spec/types.ts +64 -7
  318. package/src/kernel/spec/wireType.ts +53 -0
  319. package/src/testing/behavioral/errorMatrix.mcp.test.ts +2 -1
  320. package/src/testing/behavioral/journeys.firstInstall.test.ts +72 -2
  321. package/src/testing/behavioral/parity.test.ts +40 -0
  322. package/src/testing/eval/__snapshots__/eval.test.ts.snap +1 -1
  323. package/src/testing/eval/cases/assembly.ts +4 -3
  324. package/src/testing/eval/cases/stable.ts +2 -3
  325. package/src/testing/helpers/callSchema.ts +40 -0
  326. package/src/testing/helpers/parity.ts +35 -2
  327. package/src/testing/setupCallChecking.ts +30 -0
  328. package/dist/src/capabilities/capabilities/hints.js +0 -223
  329. package/dist/src/capabilities/capabilities/hints.js.map +0 -1
  330. package/src/capabilities/capabilities/hints.ts +0 -266
@@ -453,10 +453,12 @@ function createVerb(
453
453
  summary: string,
454
454
  params: ParamSpec[],
455
455
  examples: VerbSpec["examples"],
456
+ guidance: Required<Pick<VerbSpec, "useWhen" | "example">>,
456
457
  ): VerbSpec<Record<string, unknown>, GeneratorResult> {
457
458
  return {
458
459
  path: ["create", kind],
459
460
  summary,
461
+ ...guidance,
460
462
  params,
461
463
  output: CREATE_OUTPUT,
462
464
  examples,
@@ -507,6 +509,11 @@ export const createVerbs: Record<
507
509
  note: "preview the files without writing",
508
510
  },
509
511
  ],
512
+ {
513
+ useWhen:
514
+ "when asked to start a new React, Svelte or Lit component with its tests, stories and styles",
515
+ example: { framework: "react", componentPath: "src/components/Button" },
516
+ },
510
517
  ),
511
518
  package: createVerb(
512
519
  "package",
@@ -520,6 +527,10 @@ export const createVerbs: Record<
520
527
  cmd: `${BIN_NAME} create package --name @canonical/my-tool --no-run-install`,
521
528
  },
522
529
  ],
530
+ {
531
+ useWhen: "when asked to start a new npm package inside the monorepo",
532
+ example: { name: "@canonical/my-lib", type: "library" },
533
+ },
523
534
  ),
524
535
  application: createVerb(
525
536
  "application",
@@ -530,5 +541,10 @@ export const createVerbs: Record<
530
541
  { cmd: `${BIN_NAME} create application react my-app --relay` },
531
542
  { cmd: `${BIN_NAME} create application react my-app --rendering spa` },
532
543
  ],
544
+ {
545
+ useWhen:
546
+ "when asked to start a new React application, server-rendered or client-only",
547
+ example: { appPath: "my-app" },
548
+ },
533
549
  ),
534
550
  };
@@ -286,7 +286,7 @@ describe("the mounted create grammar (subprocess)", () => {
286
286
  expect(matchedEnvelope.error.suggestions).toBeUndefined();
287
287
  // D5's third route, pinned as the INVARIANT and not only the output:
288
288
  // this cli is COMPUTED at the mount (chain[0] = the root program's
289
- // name), bypassing cliRecovery and invisible to copy.test.ts's
289
+ // name), bypassing callRecovery and invisible to copy.test.ts's
290
290
  // quoted-literal position rule — so the expectation is COMPOSED from
291
291
  // the same constant the derivation must land on. A `startsWith` check
292
292
  // behind an exact literal could never fail (the literal throws first
@@ -63,7 +63,7 @@ describe("the distribution's declared stories (PROTECTED)", () => {
63
63
  },
64
64
  );
65
65
 
66
- it('every declared filter names "values" or a "vocabulary"', () => {
66
+ it('every declared filter names "values", a "vocabulary" or a "noun"', () => {
67
67
  // The rule the round-trip above already enforces, said OUT LOUD. It was
68
68
  // only implied there — a story satisfies the whole grammar or it does not,
69
69
  // and a reader looking for this particular property found no assertion
@@ -87,6 +87,7 @@ describe("the distribution's declared stories (PROTECTED)", () => {
87
87
  "implementation list --library",
88
88
  "implementation list --platform",
89
89
  "standard list --category",
90
+ "token consumers --block",
90
91
  "token consumers --key",
91
92
  "token consumers --state",
92
93
  "token consumers --symbol",
@@ -105,8 +106,10 @@ describe("the distribution's declared stories (PROTECTED)", () => {
105
106
  ]);
106
107
  for (const [label, filter] of filters) {
107
108
  expect(
108
- filter.values !== undefined || filter.vocabulary !== undefined,
109
- `${label} declares neither "values" nor a "vocabulary"`,
109
+ filter.values !== undefined ||
110
+ filter.vocabulary !== undefined ||
111
+ filter.noun !== undefined,
112
+ `${label} declares no "values", "vocabulary" or "noun"`,
110
113
  ).toBe(true);
111
114
  // And never both — a declared set IS the vocabulary.
112
115
  expect(
@@ -22,6 +22,7 @@ import conf from "../../pragma.conf.js";
22
22
  import { compileStoryModule } from "../kernel/packs/compile.js";
23
23
  import {
24
24
  distributionSource,
25
+ type NounLookups,
25
26
  type PackDefinition,
26
27
  } from "../kernel/packs/types.js";
27
28
  import { DEFAULT_PREFIX_MAP } from "../kernel/render/index.js";
@@ -34,6 +35,10 @@ export const declaredStories: ReadonlyMap<string, PackDefinition> = new Map(
34
35
  .map((story): readonly [string, PackDefinition] => [story.noun, story]),
35
36
  );
36
37
 
38
+ /** The lookup each declared noun resolves names through, for a filter naming it. */
39
+ export const declaredLookups: NounLookups = (noun) =>
40
+ declaredStories.get(noun)?.lookup;
41
+
37
42
  /**
38
43
  * Those stories compiled to capability modules, by noun.
39
44
  *
@@ -49,6 +54,7 @@ export const storyModules: ReadonlyMap<string, CapabilityModule> = new Map(
49
54
  story,
50
55
  distributionSource("pragma.conf.ts"),
51
56
  DEFAULT_PREFIX_MAP,
57
+ declaredLookups,
52
58
  ),
53
59
  ]),
54
60
  );
@@ -28,7 +28,10 @@ import {
28
28
  import { activeStories } from "../../../kernel/runtime/graphpack/stories.js";
29
29
  import type { PragmaRuntime } from "../../../kernel/runtime/index.js";
30
30
  import type { SourcesDecision } from "../../../kernel/runtime/resolveSources.js";
31
- import { resolveSources } from "../../../kernel/runtime/resolveSources.js";
31
+ import {
32
+ describeIgnoredPack,
33
+ resolveSources,
34
+ } from "../../../kernel/runtime/resolveSources.js";
32
35
  import type { CheckItem, CheckResult } from "../types.js";
33
36
 
34
37
  /**
@@ -148,6 +151,15 @@ export async function checkPackageRefs(
148
151
  const composition = `${packs.length} ${packs.length === 1 ? "pack" : "packs"}, ${entities} entities${suffix}`;
149
152
 
150
153
  if (decision.kind === "embedded") {
154
+ // A pack this project built, passed over because an older CLI built it. The
155
+ // check still PASSES — the snapshot is answering and its answers are the
156
+ // current ones — so the account of the pack goes in the detail, next to the
157
+ // account of what IS answering, and the remedy stays unset: there is
158
+ // nothing to fix, only two ways to change it, and the sentence names both.
159
+ const ignored =
160
+ decision.ignoredPack === undefined
161
+ ? ""
162
+ : ` · ${describeIgnoredPack(decision.ignoredPack)}`;
151
163
  return {
152
164
  name,
153
165
  status: "pass",
@@ -157,7 +169,7 @@ export async function checkPackageRefs(
157
169
  // neither. "shipped with the CLI" rather than "embedded snapshot" — the
158
170
  // reader needs to know where these packs came from, not what the build
159
171
  // calls the artefact.
160
- detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead`,
172
+ detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead${ignored}`,
161
173
  ...(items.length > 0 ? { items } : {}),
162
174
  };
163
175
  }
@@ -16,12 +16,13 @@ import {
16
16
  writeFileSync,
17
17
  } from "node:fs";
18
18
  import { tmpdir } from "node:os";
19
- import { join } from "node:path";
19
+ import { dirname, join } from "node:path";
20
20
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
21
  import type { ConfigLayers } from "../../kernel/config/types.js";
22
22
  import { executeVerb } from "../../kernel/project/cli/dispatch.js";
23
23
  import { bootRuntime } from "../../kernel/runtime/boot.js";
24
24
  import { createQueryFacade } from "../../kernel/runtime/facade.js";
25
+ import { activePackPath, packDir } from "../../kernel/runtime/paths.js";
25
26
  import type {
26
27
  GlobalFlags,
27
28
  LazyStore,
@@ -212,6 +213,42 @@ describe("doctor — the pack-refs check", () => {
212
213
  }
213
214
  });
214
215
 
216
+ it("still passes when a pack an older CLI built is passed over — and names it", async () => {
217
+ // The upgrade state. The check PASSES: the snapshot is answering and its
218
+ // answers are the current ones, so there is nothing to fix — only a pack to
219
+ // account for, and two ways to change it. Hence a detail, not a remedy.
220
+ const cwd = tmp("pragma-proj-");
221
+ const hash = "b".repeat(64);
222
+ const dir = packDir(hash);
223
+ mkdirSync(dir, { recursive: true });
224
+ writeFileSync(join(dir, "data.nq"), "<urn:s> <urn:p> <urn:o> .\n");
225
+ writeFileSync(join(dir, "schema.json"), "{}");
226
+ writeFileSync(join(dir, "index.json"), "{}");
227
+ writeFileSync(join(dir, "stories.json"), "[]");
228
+ writeFileSync(
229
+ join(dir, "manifest.json"),
230
+ JSON.stringify({
231
+ name: "pragma",
232
+ version: "0.1.0",
233
+ sourceRef: "t",
234
+ contentHash: hash,
235
+ prefixes: {},
236
+ createdAt: "2026-09-10T21:49:00.411Z",
237
+ }),
238
+ );
239
+ mkdirSync(dirname(activePackPath(cwd)), { recursive: true });
240
+ writeFileSync(activePackPath(cwd), hash);
241
+
242
+ const pkgRefs = await checkPackageRefs(bootRuntime(FLAGS, cwd));
243
+ expect(pkgRefs.status).toBe("pass");
244
+ expect(pkgRefs.detail).toContain("shipped with the CLI");
245
+ expect(pkgRefs.detail).toContain(
246
+ "a pack built by pragma 0.1.0 on 2026-09-10 is ignored",
247
+ );
248
+ expect(pkgRefs.detail).toContain("pragma sources reset");
249
+ expect(pkgRefs.remedy).toBeUndefined();
250
+ });
251
+
215
252
  it("a project with its OWN packs and nothing built is an attributable fail", async () => {
216
253
  // The install that must never read healthy: `origins.packs` is "project",
217
254
  // so the embedded snapshot is a DIFFERENT graph and every read throws
@@ -27,6 +27,8 @@ const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
27
27
  // (`toolDescriptions.test.ts`), so the inventory's widening is described as
28
28
  // "the verbose global flag" rather than by its spelling.
29
29
  doc: "Reports the environment checks first, then one row per setup target — once for your home directory, once for this project. Each row is pass, fail, available (an optional integration you have not set up yet), or skip (nothing to do here, and the row says why), with the next step printed inline. Every row is named after the setup target that repairs it, except `harnesses`: a listing, per scope, of the AI harnesses found on this machine and whether this CLI's MCP server is registered in each — the ones actually found, or every harness it knows about under the verbose global flag. Needs no store; the store check boots lazily and never fails the run.",
30
+ useWhen: "when something is not working, to find out what",
31
+ category: "diagnostic",
30
32
  params: [],
31
33
  output: { formatters: doctorFormatters },
32
34
  examples: [
@@ -174,6 +174,11 @@ describe("pack list empty-state (U5, PROTECTED)", () => {
174
174
  const notice = formatters.notice?.(empty);
175
175
  expect(notice).toContain(`No ${pack.noun} entries found.`);
176
176
  expect(notice).toMatch(hint);
177
+ // Over MCP the same hint ends in a TOOL CALL: an agent cannot run a
178
+ // command, and `meta.notice` is the only place it reads this.
179
+ const overMcp = formatters.notice?.(empty, "mcp");
180
+ expect(overMcp).toMatch(/Call `[a-z]+_[a-z]+ \{.*\}`\./);
181
+ expect(overMcp).not.toMatch(hint);
177
182
  // JSON is the uniform empty array — unchanged by the message.
178
183
  expect(formatters.json(empty)).toBe("[]");
179
184
  // The llm view stays non-blank too (the `(0)` heading plus the message).
@@ -103,6 +103,8 @@ const connectVerb: VerbSpec<Record<string, unknown>, ConnectResult> = {
103
103
  path: ["graph", "connect"],
104
104
  summary: "Show the shortest relation paths between two entities.",
105
105
  doc: "Finds every shortest path of RELATION edges between two entities, and says which kind of nothing it found when there is no path. An edge counts as a relation only where it does not fan out into a roster — membership of a class, a tier or a family is answered by that noun's list verb, not by a path — so most pairs are correctly reported as unconnected. Address each endpoint by prefixed name (ds:global.component.button) or absolute IRI. Every answer states the hop limit, the fan-in threshold, and the packs searched.",
106
+ useWhen: "when asked whether and how two things are related",
107
+ example: { a: "ds:global.component.button", b: "dt:color.text" },
106
108
  params: [
107
109
  {
108
110
  kind: "string",
@@ -11,6 +11,7 @@ import { toTurtle } from "../../kernel/render/turtle.js";
11
11
  import type {
12
12
  InboundGroup,
13
13
  InspectResult,
14
+ PredicateGroup,
14
15
  ReadTerm,
15
16
  } from "../../kernel/runtime/readEntity.js";
16
17
  import type { Formatters } from "../../kernel/spec/index.js";
@@ -39,6 +40,13 @@ function inboundHeading(group: InboundGroup): string {
39
40
  return `${short(group.predicate)} (${suffix})`;
40
41
  }
41
42
 
43
+ /** `predicate (24 objects, showing 10)` — a cut group says what it was cut from. */
44
+ function outboundHeading(group: PredicateGroup): string {
45
+ return group.truncated
46
+ ? `${short(group.predicate)} (${group.count} objects, showing ${group.objects.length})`
47
+ : short(group.predicate);
48
+ }
49
+
42
50
  export const inspectFormatters: Formatters<InspectResult> = {
43
51
  plain(data) {
44
52
  const title = data.label
@@ -46,7 +54,7 @@ export const inspectFormatters: Formatters<InspectResult> = {
46
54
  : (data.prefixed ?? data.uri);
47
55
  const lines = [title, "═".repeat(Math.max(title.length, 24)), ""];
48
56
  for (const group of data.groups) {
49
- lines.push(` ${short(group.predicate)}:`);
57
+ lines.push(` ${outboundHeading(group)}:`);
50
58
  for (const object of group.objects) lines.push(` ${named(object)}`);
51
59
  }
52
60
  for (const [via, rows] of Object.entries(data.nested)) {
@@ -333,3 +333,76 @@ describe("bounds and losslessness the review surfaced", () => {
333
333
  });
334
334
  });
335
335
  });
336
+
337
+ describe("an outbound predicate is cut below `detailed`, and says so", () => {
338
+ // One subject asserting a wide predicate both ways: 14 named objects, and 12
339
+ // blank ones that inline as records.
340
+ const indexes = Array.from({ length: 14 }, (_, index) => index);
341
+ const WIDE_TTL = [
342
+ "@prefix ds: <https://ds.canonical.com/> .",
343
+ `ds:wide ds:name "Wide" ;`,
344
+ ` ds:uses ${indexes.map((index) => `ds:part.${index}`).join(", ")} ;`,
345
+ ` ds:binds ${indexes
346
+ .slice(0, 12)
347
+ .map((index) => `[ ds:rank ${index} ]`)
348
+ .join(", ")} .`,
349
+ ].join("\n");
350
+
351
+ const readAt = async (
352
+ detail: "summary" | "standard" | "detailed",
353
+ ): Promise<InspectResult> => {
354
+ const fixture = await buildFixtureRuntime({
355
+ ttl: WIDE_TTL,
356
+ prefixes: BLOCK_PREFIXES,
357
+ detail,
358
+ });
359
+ try {
360
+ return (await inspectVerb.run(
361
+ { uri: "ds:wide" },
362
+ fixture.rt,
363
+ )) as InspectResult;
364
+ } finally {
365
+ (await fixture.rt.store.get()).store.dispose();
366
+ }
367
+ };
368
+ const group = (result: InspectResult, local: string) =>
369
+ result.groups.find((g) => g.predicate.value === `${DS}${local}`);
370
+
371
+ it("lists the head and states the true count at standard", async () => {
372
+ const result = await readAt("standard");
373
+ expect(group(result, "uses")).toMatchObject({ truncated: true, count: 14 });
374
+ expect(group(result, "uses")?.objects).toHaveLength(10);
375
+ // The inlined records follow the objects that survived the cut.
376
+ expect(group(result, "binds")?.count).toBe(12);
377
+ expect(result.nested["ds:binds"]).toHaveLength(10);
378
+ // A predicate under the cap carries neither marker.
379
+ expect(group(result, "name")).not.toHaveProperty("truncated");
380
+ expect(group(result, "name")).not.toHaveProperty("count");
381
+ });
382
+
383
+ it("keeps a few at summary rather than none", async () => {
384
+ expect(group(await readAt("summary"), "uses")?.objects).toHaveLength(3);
385
+ });
386
+
387
+ it("cuts nothing at detailed", async () => {
388
+ const result = await readAt("detailed");
389
+ expect(group(result, "uses")?.objects).toHaveLength(14);
390
+ expect(group(result, "uses")).not.toHaveProperty("truncated");
391
+ expect(result.nested["ds:binds"]).toHaveLength(12);
392
+ });
393
+
394
+ it("prints the count in the plain and the Turtle forms", async () => {
395
+ const result = await readAt("standard");
396
+ const formatters = inspectVerb.output.formatters;
397
+ expect(formatters.plain(result)).toContain(
398
+ "ds:uses (14 objects, showing 10):",
399
+ );
400
+ const turtle = formatters.llm(result);
401
+ expect(turtle).toContain(
402
+ " # ds:uses — 14 objects, showing 10\n ds:uses ",
403
+ );
404
+ expect(turtle).toContain(
405
+ " # ds:binds — 12 objects, showing 10\n ds:binds\n",
406
+ );
407
+ });
408
+ });
@@ -25,6 +25,8 @@ const inspectVerb: VerbSpec<Record<string, unknown>, InspectResult> = {
25
25
  summary:
26
26
  "Show every triple where a URI is the subject, grouped by predicate.",
27
27
  doc: "Inspect one entity: all predicate/object pairs asserted on the subject. Address it by prefixed name (ds:global.component.button) or absolute IRI.",
28
+ useWhen: "when asked for everything recorded about one identifier",
29
+ example: { uri: "ds:global.component.button" },
28
30
  params: [
29
31
  {
30
32
  kind: "string",
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { queryFormatters } from "./query.render.js";
3
+
4
+ const EMPTY = { type: "select", bindings: [] } as never;
5
+
6
+ describe("graph query — zero rows names the next call", () => {
7
+ it("says the query ran, and points at the catalogue and the namespaces as commands on the CLI", () => {
8
+ const notice = queryFormatters.notice?.(EMPTY) as string;
9
+ expect(notice).toContain("the query ran and matched nothing");
10
+ expect(notice).toContain("`pragma capabilities`");
11
+ expect(notice).toContain("`pragma ontology list`");
12
+ // The condensed form is one stream, so it carries the same sentence.
13
+ expect(queryFormatters.llm(EMPTY)).toBe(notice);
14
+ });
15
+
16
+ it("spells them as tool calls over MCP, and names no domain tool", () => {
17
+ const notice = queryFormatters.notice?.(EMPTY, "mcp") as string;
18
+ expect(notice).toContain("`capabilities {}`");
19
+ expect(notice).toContain("`ontology_list {}`");
20
+ expect(notice).not.toContain("pragma ");
21
+ // Which tool answers the question is the catalogue's to say, not this
22
+ // renderer's: the only tools it may name are the two it points at.
23
+ const named = [...notice.matchAll(/`([a-z_]+) \{/g)].map((m) => m[1]);
24
+ expect(named.sort()).toEqual(["capabilities", "ontology_list"]);
25
+ });
26
+
27
+ it("says nothing when there are rows", () => {
28
+ expect(
29
+ queryFormatters.notice?.({
30
+ type: "select",
31
+ bindings: [{ s: "ds:x" }],
32
+ } as never),
33
+ ).toBeUndefined();
34
+ });
35
+ });
@@ -9,11 +9,12 @@
9
9
  * - json: the serialized {@link QueryResult}.
10
10
  */
11
11
 
12
- import type { Formatters } from "../../kernel/spec/index.js";
12
+ import { quoteCall } from "../../kernel/spec/call.js";
13
+ import type { Call, Formatters, Surface } from "../../kernel/spec/index.js";
13
14
 
14
15
  // Inline `import("…")` type (no `from`) — keeps the ke types off the static
15
16
  // import graph the lazy-dispatch probe walks (see query.verb.ts).
16
- type QueryResult = import("@canonical/ke").QueryResult;
17
+ type QueryAnswer = import("./runQuery.js").QueryAnswer;
17
18
 
18
19
  /**
19
20
  * Render CONSTRUCT triples as readable `subject\tpredicate\tobject` lines,
@@ -35,13 +36,46 @@ function formatTriples(
35
36
  .join("\n");
36
37
  }
37
38
 
38
- export const queryFormatters: Formatters<QueryResult> = {
39
+ /**
40
+ * Where an empty SELECT points. Deliberately no domain tool: which one answers
41
+ * the caller's question is the catalogue's to say, so the pointer is to the
42
+ * catalogue — and to the namespaces that exist, since an invented prefix parses
43
+ * and matches nothing.
44
+ */
45
+ export const EMPTY_QUERY_CALLS = {
46
+ catalogue: { verb: "capabilities" },
47
+ namespaces: { verb: "ontology list" },
48
+ } as const satisfies Record<string, Call>;
49
+
50
+ /**
51
+ * What zero rows says for itself. "No results." read as a failed tool to an
52
+ * agent with nowhere to go next. The query RAN; when its own text declares a
53
+ * prefix this graph does not have, that is the reason and it is said exactly.
54
+ */
55
+ function describeEmptySelect(result: QueryAnswer, surface: Surface): string {
56
+ const { catalogue, namespaces } = EMPTY_QUERY_CALLS;
57
+ const invented = (result.unknownPrefixes ?? [])
58
+ .map(
59
+ ({ prefix, iri }) =>
60
+ `Prefix \`${prefix}:\` <${iri}> is not a namespace of this graph, so nothing under it can match.`,
61
+ )
62
+ .join(" ");
63
+ return [
64
+ "No results: the query ran and matched nothing.",
65
+ invented,
66
+ `The namespaces that exist are listed by ${quoteCall(namespaces, surface)} (an invented prefix matches nothing). A domain tool may answer this without SPARQL: ${quoteCall(catalogue, surface)} lists the tools by the question each answers.`,
67
+ ]
68
+ .filter(Boolean)
69
+ .join(" ");
70
+ }
71
+
72
+ export const queryFormatters: Formatters<QueryAnswer> = {
39
73
  // Zero rows/triples: plain stdout stays empty — the notice is
40
74
  // `notice`, routed to stderr (exit 0) by the dispatcher so a pipe
41
75
  // reads no prose. ASK always has a result and never goes empty.
42
- notice(result) {
76
+ notice(result, surface = "cli") {
43
77
  if (result.type === "select" && result.bindings.length === 0) {
44
- return "No results.";
78
+ return describeEmptySelect(result, surface);
45
79
  }
46
80
  if (result.type === "construct" && result.triples.length === 0) {
47
81
  return "No triples.";
@@ -65,7 +99,9 @@ export const queryFormatters: Formatters<QueryResult> = {
65
99
 
66
100
  llm(result) {
67
101
  if (result.type === "select") {
68
- if (result.bindings.length === 0) return "_No results._";
102
+ if (result.bindings.length === 0) {
103
+ return describeEmptySelect(result, "cli");
104
+ }
69
105
  const cols = Object.keys(result.bindings[0] ?? {});
70
106
  const header = `| ${cols.join(" | ")} |`;
71
107
  const divider = `| ${cols.map(() => "---").join(" | ")} |`;
@@ -84,6 +120,8 @@ export const queryFormatters: Formatters<QueryResult> = {
84
120
  },
85
121
 
86
122
  json(result) {
87
- return JSON.stringify(result);
123
+ // The payload is the engine's own shape; the prefix finding is the notice's.
124
+ const { unknownPrefixes: _unknownPrefixes, ...data } = result;
125
+ return JSON.stringify(data);
88
126
  },
89
127
  };
@@ -64,3 +64,47 @@ describe("a name pragma printed can be pasted back (PROTECTED)", () => {
64
64
  expect(prefixed).toEqual(absolute);
65
65
  });
66
66
  });
67
+
68
+ describe("an invented namespace is named, not answered with silence", () => {
69
+ // The recorded failure: nine queries against a namespace that does not
70
+ // exist, nine "No results", and a guess. A declared PREFIX always parses, so
71
+ // only a check against the store's own prefix map can say why nothing matched.
72
+ const INVENTED =
73
+ "PREFIX tok: <https://example.invalid/tokens#> SELECT ?c WHERE { ?c tok:uses ?t }";
74
+
75
+ it("says exactly which prefix is not a namespace of this graph, on both surfaces", async () => {
76
+ const result = await queryVerb.run({ sparql: INVENTED }, rt);
77
+ const { notice, json } = queryVerb.output.formatters;
78
+ const cli = notice?.(result, "cli") as string;
79
+ expect(cli).toContain(
80
+ "Prefix `tok:` <https://example.invalid/tokens#> is not a namespace of this graph",
81
+ );
82
+ expect(cli).toContain("`pragma ontology list`");
83
+ expect(notice?.(result, "mcp")).toContain("`ontology_list {}`");
84
+ // The finding explains the emptiness; the payload stays the engine's shape.
85
+ const payload = JSON.parse(json(result));
86
+ expect(payload).toMatchObject({ type: "select", bindings: [] });
87
+ expect(payload).not.toHaveProperty("unknownPrefixes");
88
+ });
89
+
90
+ it("says nothing about prefixes when the query declares only real ones, or has rows", async () => {
91
+ const real = await queryVerb.run(
92
+ {
93
+ sparql:
94
+ "PREFIX ds: <https://ds.canonical.com/> SELECT ?s WHERE { ?s a ds:NoSuchClass }",
95
+ },
96
+ rt,
97
+ );
98
+ const notice = queryVerb.output.formatters.notice?.(real, "cli") as string;
99
+ expect(notice).toContain("the query ran and matched nothing");
100
+ expect(notice).not.toContain("is not a namespace");
101
+ const hit = await queryVerb.run(
102
+ {
103
+ sparql: `${INVENTED.replace("WHERE { ?c tok:uses ?t }", "WHERE { ?c ?p ?t } LIMIT 1")}`,
104
+ },
105
+ rt,
106
+ );
107
+ expect(queryVerb.output.formatters.notice?.(hit, "cli")).toBeUndefined();
108
+ expect(hit).not.toHaveProperty("unknownPrefixes");
109
+ });
110
+ });
@@ -24,6 +24,8 @@ const queryVerb: VerbSpec<Record<string, unknown>, QueryResult> = {
24
24
  path: ["graph", "query"],
25
25
  summary: "Run a raw SPARQL query against the loaded graph.",
26
26
  doc: "Executes an arbitrary SPARQL query (SELECT / ASK / CONSTRUCT) against the store. Prefixes are applied automatically from the pack's namespace map; list the ontology namespaces to discover the available prefixes.",
27
+ useWhen: "only when no other tool answers: SPARQL joins or counts",
28
+ example: { sparql: "SELECT ?s WHERE { ?s a ds:Component } LIMIT 5" },
27
29
  params: [
28
30
  {
29
31
  kind: "string",
@@ -10,7 +10,11 @@
10
10
  */
11
11
 
12
12
  import { describe, expect, it } from "vitest";
13
- import { expandPrefixedNames, trimQueryError } from "./queryText.js";
13
+ import {
14
+ expandPrefixedNames,
15
+ findUnknownPrefixes,
16
+ trimQueryError,
17
+ } from "./queryText.js";
14
18
 
15
19
  const PREFIXES = {
16
20
  ds: "https://ds.canonical.com/",
@@ -119,3 +123,53 @@ describe("trimQueryError — the caller's own position", () => {
119
123
  );
120
124
  });
121
125
  });
126
+
127
+ describe("findUnknownPrefixes — only a prefix that can explain the emptiness", () => {
128
+ const KNOWN = { ds: "https://ds.canonical.com/" };
129
+ const BAD = "https://example.invalid/t#";
130
+
131
+ it("reports a declared, used prefix whose IRI is no namespace of the graph", () => {
132
+ expect(
133
+ findUnknownPrefixes(
134
+ `PREFIX tok: <${BAD}> PREFIX ds: <https://ds.canonical.com/> SELECT ?c WHERE { ?c tok:uses ds:x }`,
135
+ KNOWN,
136
+ ),
137
+ ).toEqual([{ prefix: "tok", iri: BAD }]);
138
+ });
139
+
140
+ it("ignores a PREFIX in a comment or inside a string literal", () => {
141
+ expect(
142
+ findUnknownPrefixes(
143
+ `# PREFIX tok: <${BAD}>\nSELECT ?s WHERE { ?s ds:label "PREFIX tok: <${BAD}> tok:x" }`,
144
+ KNOWN,
145
+ ),
146
+ ).toEqual([]);
147
+ });
148
+
149
+ it("ignores a declared prefix the body never uses", () => {
150
+ expect(
151
+ findUnknownPrefixes(
152
+ `PREFIX tok: <${BAD}> SELECT ?s WHERE { ?s a ds:Component }`,
153
+ KNOWN,
154
+ ),
155
+ ).toEqual([]);
156
+ });
157
+
158
+ it("does not judge a relative IRI, which BASE resolves", () => {
159
+ expect(
160
+ findUnknownPrefixes(
161
+ "BASE <https://ds.canonical.com/> PREFIX rel: <tokens/> SELECT ?s WHERE { ?s rel:p ?o }",
162
+ KNOWN,
163
+ ),
164
+ ).toEqual([]);
165
+ });
166
+
167
+ it("reports the default prefix too, when the body uses it", () => {
168
+ expect(
169
+ findUnknownPrefixes(
170
+ `PREFIX : <${BAD}> SELECT ?s WHERE { ?s :p ?o }`,
171
+ KNOWN,
172
+ ),
173
+ ).toEqual([{ prefix: "", iri: BAD }]);
174
+ });
175
+ });
@@ -238,3 +238,50 @@ function cut(text: string): string {
238
238
  ? text
239
239
  : `${text.slice(0, MAX_PARSER_DETAIL - 1).trimEnd()}…`;
240
240
  }
241
+
242
+ /** A `PREFIX` the caller declared whose IRI is none of the store's namespaces. */
243
+ export interface UnknownPrefix {
244
+ readonly prefix: string;
245
+ readonly iri: string;
246
+ }
247
+
248
+ /**
249
+ * Find the prefixes a query declares, USES, and binds to an IRI the store does
250
+ * not know as a namespace.
251
+ *
252
+ * A declared prefix always parses, so an invented one fails silently: every
253
+ * pattern under it simply matches nothing, and the caller reads "no results"
254
+ * as "no such data". Checked against the store's own prefix map, this is the
255
+ * one emptiness the query's text can explain.
256
+ *
257
+ * Read through the same {@link SCANNER} the expander uses, so a `PREFIX` in a
258
+ * comment or inside a string literal is not a declaration. A declared prefix
259
+ * the body never uses cannot be why nothing matched, and a relative IRI
260
+ * (resolved against `BASE`) cannot be judged from the text, so neither is
261
+ * reported.
262
+ *
263
+ * @param sparql - The query text as the caller wrote it.
264
+ * @param prefixes - The store's prefix map (prefix → namespace IRI).
265
+ * @returns The used declarations whose IRI is not a known namespace, in order.
266
+ */
267
+ export function findUnknownPrefixes(
268
+ sparql: string,
269
+ prefixes: Readonly<Record<string, string>>,
270
+ ): UnknownPrefix[] {
271
+ const known = new Set(Object.values(prefixes));
272
+ const code = sparql.replace(SCANNER, (token) =>
273
+ /^[#"']/.test(token) ? " " : token,
274
+ );
275
+ const body = code.replace(PREFIX_DECLARATION, " ");
276
+ const used = new Set(
277
+ [...body.matchAll(/(?:^|[^\w<:.-])([A-Za-z][\w.-]*)?:(?!\/\/)/g)].map(
278
+ (match) => match[1] ?? "",
279
+ ),
280
+ );
281
+ return [...code.matchAll(PREFIX_DECLARATION)]
282
+ .map((match) => ({ prefix: match[1] ?? "", iri: match[2] ?? "" }))
283
+ .filter(
284
+ ({ prefix, iri }) =>
285
+ /^[a-z][a-z0-9+.-]*:/i.test(iri) && used.has(prefix) && !known.has(iri),
286
+ );
287
+ }