@canonical/pragma-cli 0.38.0 → 0.42.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 (365) hide show
  1. package/README.md +3 -3
  2. package/dist/package.json +16 -16
  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/colophon/markdownTerminal.js.map +1 -1
  17. package/dist/src/capabilities/config/get.verb.js +2 -0
  18. package/dist/src/capabilities/config/get.verb.js.map +1 -1
  19. package/dist/src/capabilities/config/runField.js +2 -2
  20. package/dist/src/capabilities/config/runField.js.map +1 -1
  21. package/dist/src/capabilities/config/set.verb.js +2 -0
  22. package/dist/src/capabilities/config/set.verb.js.map +1 -1
  23. package/dist/src/capabilities/config/show.render.js.map +1 -1
  24. package/dist/src/capabilities/config/show.verb.js +1 -0
  25. package/dist/src/capabilities/config/show.verb.js.map +1 -1
  26. package/dist/src/capabilities/config/unset.verb.js +2 -0
  27. package/dist/src/capabilities/config/unset.verb.js.map +1 -1
  28. package/dist/src/capabilities/create/constants.js +8 -5
  29. package/dist/src/capabilities/create/constants.js.map +1 -1
  30. package/dist/src/capabilities/create/create.render.js.map +1 -1
  31. package/dist/src/capabilities/create/create.verb.js +36 -7
  32. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  33. package/dist/src/capabilities/create/createSurface.generated.js +22 -0
  34. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  35. package/dist/src/capabilities/create/index.js +1 -1
  36. package/dist/src/capabilities/create/pickGenerator.js +1 -0
  37. package/dist/src/capabilities/create/pickGenerator.js.map +1 -1
  38. package/dist/src/capabilities/distribution.js +3 -1
  39. package/dist/src/capabilities/distribution.js.map +1 -1
  40. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +10 -2
  41. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  42. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  43. package/dist/src/capabilities/doctor/doctor.verb.js +2 -0
  44. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  45. package/dist/src/capabilities/graph/connect.verb.js +2 -0
  46. package/dist/src/capabilities/graph/connect.verb.js.map +1 -1
  47. package/dist/src/capabilities/graph/inspect.render.js +7 -1
  48. package/dist/src/capabilities/graph/inspect.render.js.map +1 -1
  49. package/dist/src/capabilities/graph/inspect.verb.js +2 -0
  50. package/dist/src/capabilities/graph/inspect.verb.js.map +1 -1
  51. package/dist/src/capabilities/graph/query.render.js +37 -5
  52. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  53. package/dist/src/capabilities/graph/query.verb.js +2 -0
  54. package/dist/src/capabilities/graph/query.verb.js.map +1 -1
  55. package/dist/src/capabilities/graph/queryText.js +28 -0
  56. package/dist/src/capabilities/graph/queryText.js.map +1 -1
  57. package/dist/src/capabilities/graph/runConnect.js +3 -3
  58. package/dist/src/capabilities/graph/runConnect.js.map +1 -1
  59. package/dist/src/capabilities/graph/runQuery.js +8 -6
  60. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  61. package/dist/src/capabilities/info/info.verb.js +2 -0
  62. package/dist/src/capabilities/info/info.verb.js.map +1 -1
  63. package/dist/src/capabilities/meta/mcp.verb.js +1 -0
  64. package/dist/src/capabilities/meta/mcp.verb.js.map +1 -1
  65. package/dist/src/capabilities/ontology/verbs.js +7 -10
  66. package/dist/src/capabilities/ontology/verbs.js.map +1 -1
  67. package/dist/src/capabilities/prompt/verbs.js +5 -4
  68. package/dist/src/capabilities/prompt/verbs.js.map +1 -1
  69. package/dist/src/capabilities/resources/provider.js +6 -3
  70. package/dist/src/capabilities/resources/provider.js.map +1 -1
  71. package/dist/src/capabilities/setup/operations/writability.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  73. package/dist/src/capabilities/setup/setup.verb.js +13 -5
  74. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  75. package/dist/src/capabilities/shared/calls.js +16 -0
  76. package/dist/src/capabilities/shared/calls.js.map +1 -0
  77. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  78. package/dist/src/capabilities/skill/discover.js.map +1 -1
  79. package/dist/src/capabilities/skill/render.js +7 -5
  80. package/dist/src/capabilities/skill/render.js.map +1 -1
  81. package/dist/src/capabilities/skill/verbs.js +5 -4
  82. package/dist/src/capabilities/skill/verbs.js.map +1 -1
  83. package/dist/src/capabilities/sources/collectStatus.js +6 -1
  84. package/dist/src/capabilities/sources/collectStatus.js.map +1 -1
  85. package/dist/src/capabilities/sources/index.js +4 -3
  86. package/dist/src/capabilities/sources/index.js.map +1 -1
  87. package/dist/src/capabilities/sources/reset.render.js +37 -0
  88. package/dist/src/capabilities/sources/reset.render.js.map +1 -0
  89. package/dist/src/capabilities/sources/reset.verb.js +51 -0
  90. package/dist/src/capabilities/sources/reset.verb.js.map +1 -0
  91. package/dist/src/capabilities/sources/runReset.js +59 -0
  92. package/dist/src/capabilities/sources/runReset.js.map +1 -0
  93. package/dist/src/capabilities/sources/runUpdate.js +7 -6
  94. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  95. package/dist/src/capabilities/sources/status.render.js +24 -8
  96. package/dist/src/capabilities/sources/status.render.js.map +1 -1
  97. package/dist/src/capabilities/sources/status.verb.js +1 -0
  98. package/dist/src/capabilities/sources/status.verb.js.map +1 -1
  99. package/dist/src/capabilities/sources/update.verb.js +2 -0
  100. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  101. package/dist/src/capabilities/upgrade/upgrade.verb.js +1 -0
  102. package/dist/src/capabilities/upgrade/upgrade.verb.js.map +1 -1
  103. package/dist/src/capabilities/version/version.verb.js +1 -0
  104. package/dist/src/capabilities/version/version.verb.js.map +1 -1
  105. package/dist/src/constants.js +4 -3
  106. package/dist/src/constants.js.map +1 -1
  107. package/dist/src/kernel/completion/complete.js.map +1 -1
  108. package/dist/src/kernel/completion/emitScripts.js.map +1 -1
  109. package/dist/src/kernel/completion/parse.js.map +1 -1
  110. package/dist/src/kernel/completion/rank.js.map +1 -1
  111. package/dist/src/kernel/completion/resolve.js.map +1 -1
  112. package/dist/src/kernel/config/readConfig.js.map +1 -1
  113. package/dist/src/kernel/error/PragmaError.js.map +1 -1
  114. package/dist/src/kernel/error/index.js +1 -1
  115. package/dist/src/kernel/error/index.js.map +1 -1
  116. package/dist/src/kernel/error/recovery.js +24 -42
  117. package/dist/src/kernel/error/recovery.js.map +1 -1
  118. package/dist/src/kernel/packs/collect.js +123 -9
  119. package/dist/src/kernel/packs/collect.js.map +1 -1
  120. package/dist/src/kernel/packs/compile.js +74 -15
  121. package/dist/src/kernel/packs/compile.js.map +1 -1
  122. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js +3 -3
  123. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js.map +1 -1
  124. package/dist/src/kernel/packs/iri.js +2 -4
  125. package/dist/src/kernel/packs/iri.js.map +1 -1
  126. package/dist/src/kernel/packs/renderPack.js +90 -50
  127. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  128. package/dist/src/kernel/packs/resolveEntity.js +202 -90
  129. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  130. package/dist/src/kernel/packs/runBodies.js +89 -30
  131. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  132. package/dist/src/kernel/packs/schema.js +66 -12
  133. package/dist/src/kernel/packs/schema.js.map +1 -1
  134. package/dist/src/kernel/packs/sparql/buildListQuery.js +80 -13
  135. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -1
  136. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +41 -13
  137. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  138. package/dist/src/kernel/packs/sparql/filterValues.js +32 -23
  139. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -1
  140. package/dist/src/kernel/packs/sparql/runSelect.js +3 -5
  141. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  142. package/dist/src/kernel/packs/storyRules.js +31 -4
  143. package/dist/src/kernel/packs/storyRules.js.map +1 -1
  144. package/dist/src/kernel/packs/tierScope.js +3 -3
  145. package/dist/src/kernel/packs/tierScope.js.map +1 -1
  146. package/dist/src/kernel/packs/types.js +4 -0
  147. package/dist/src/kernel/packs/types.js.map +1 -1
  148. package/dist/src/kernel/project/cli/buildProgram.js +4 -0
  149. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  150. package/dist/src/kernel/project/cli/dispatch.js +32 -8
  151. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  152. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  153. package/dist/src/kernel/project/cli/suggestNames.js +44 -1
  154. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  155. package/dist/src/kernel/project/cli/verbHelp.js +17 -18
  156. package/dist/src/kernel/project/cli/verbHelp.js.map +1 -1
  157. package/dist/src/kernel/project/mcp/buildServer.js +11 -4
  158. package/dist/src/kernel/project/mcp/buildServer.js.map +1 -1
  159. package/dist/src/kernel/project/mcp/envelope.js +3 -2
  160. package/dist/src/kernel/project/mcp/envelope.js.map +1 -1
  161. package/dist/src/kernel/project/mcp/instructions.js +101 -31
  162. package/dist/src/kernel/project/mcp/instructions.js.map +1 -1
  163. package/dist/src/kernel/project/mcp/registerVerb.js +59 -23
  164. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  165. package/dist/src/kernel/render/envelope.js.map +1 -1
  166. package/dist/src/kernel/render/renderers.js +3 -3
  167. package/dist/src/kernel/render/renderers.js.map +1 -1
  168. package/dist/src/kernel/render/turtle.js +21 -4
  169. package/dist/src/kernel/render/turtle.js.map +1 -1
  170. package/dist/src/kernel/runtime/boot.js.map +1 -1
  171. package/dist/src/kernel/runtime/graphpack/build.js +54 -1
  172. package/dist/src/kernel/runtime/graphpack/build.js.map +1 -1
  173. package/dist/src/kernel/runtime/graphpack/manifest.js +14 -0
  174. package/dist/src/kernel/runtime/graphpack/manifest.js.map +1 -1
  175. package/dist/src/kernel/runtime/graphpack/read.js +3 -5
  176. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  177. package/dist/src/kernel/runtime/graphpack/schemas.js +1 -0
  178. package/dist/src/kernel/runtime/graphpack/schemas.js.map +1 -1
  179. package/dist/src/kernel/runtime/loadSession.js +8 -1
  180. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  181. package/dist/src/kernel/runtime/packVersion.js +75 -0
  182. package/dist/src/kernel/runtime/packVersion.js.map +1 -0
  183. package/dist/src/kernel/runtime/readEntity.js +43 -4
  184. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  185. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  186. package/dist/src/kernel/runtime/resolveSources.js +61 -9
  187. package/dist/src/kernel/runtime/resolveSources.js.map +1 -1
  188. package/dist/src/kernel/runtime/storeReadiness.js +3 -2
  189. package/dist/src/kernel/runtime/storeReadiness.js.map +1 -1
  190. package/dist/src/kernel/spec/call.js +167 -0
  191. package/dist/src/kernel/spec/call.js.map +1 -0
  192. package/dist/src/kernel/spec/emitReference.js +9 -13
  193. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  194. package/dist/src/kernel/spec/emitSurface.js +8 -1
  195. package/dist/src/kernel/spec/emitSurface.js.map +1 -1
  196. package/dist/src/kernel/spec/guidance.js +63 -0
  197. package/dist/src/kernel/spec/guidance.js.map +1 -0
  198. package/dist/src/kernel/spec/wireType.js +36 -0
  199. package/dist/src/kernel/spec/wireType.js.map +1 -0
  200. package/package.json +16 -16
  201. package/pragma.conf.ts +294 -177
  202. package/src/capabilities/block.parity.test.ts +1 -1
  203. package/src/capabilities/callRule.test.ts +147 -0
  204. package/src/capabilities/capabilities/capabilities.test.ts +47 -19
  205. package/src/capabilities/capabilities/capabilities.verb.ts +2 -0
  206. package/src/capabilities/capabilities/catalog.ts +60 -55
  207. package/src/capabilities/capabilities/index.ts +2 -0
  208. package/src/capabilities/capabilities/types.ts +16 -19
  209. package/src/capabilities/colophon/colophon.render.ts +9 -5
  210. package/src/capabilities/colophon/colophon.verb.ts +1 -0
  211. package/src/capabilities/config/get.verb.ts +2 -0
  212. package/src/capabilities/config/runField.ts +3 -3
  213. package/src/capabilities/config/set.verb.ts +3 -0
  214. package/src/capabilities/config/show.verb.ts +2 -0
  215. package/src/capabilities/config/unset.verb.ts +2 -0
  216. package/src/capabilities/create/constants.ts +8 -5
  217. package/src/capabilities/create/create.mcp.test.ts +173 -1
  218. package/src/capabilities/create/create.test.ts +4 -2
  219. package/src/capabilities/create/create.verb.ts +45 -3
  220. package/src/capabilities/create/createGrammar.test.ts +41 -5
  221. package/src/capabilities/create/createSurface.generated.ts +22 -0
  222. package/src/capabilities/create/crossCli.subprocess.test.ts +53 -14
  223. package/src/capabilities/create/index.ts +1 -1
  224. package/src/capabilities/create/pickGenerator.ts +1 -0
  225. package/src/capabilities/create/shippedCreate.subprocess.test.ts +28 -7
  226. package/src/capabilities/distribution.test.ts +6 -3
  227. package/src/capabilities/distribution.ts +6 -0
  228. package/src/capabilities/doctor/checks/checkPackageRefs.ts +14 -2
  229. package/src/capabilities/doctor/doctor.test.ts +38 -1
  230. package/src/capabilities/doctor/doctor.verb.ts +2 -0
  231. package/src/capabilities/emptyRecovery.test.ts +5 -0
  232. package/src/capabilities/graph/connect.verb.ts +2 -0
  233. package/src/capabilities/graph/inspect.render.ts +9 -1
  234. package/src/capabilities/graph/inspect.test.ts +73 -0
  235. package/src/capabilities/graph/inspect.verb.ts +2 -0
  236. package/src/capabilities/graph/query.render.test.ts +35 -0
  237. package/src/capabilities/graph/query.render.ts +45 -7
  238. package/src/capabilities/graph/query.shipped.exec.test.ts +44 -0
  239. package/src/capabilities/graph/query.verb.ts +2 -0
  240. package/src/capabilities/graph/queryText.test.ts +55 -1
  241. package/src/capabilities/graph/queryText.ts +47 -0
  242. package/src/capabilities/graph/runConnect.ts +4 -5
  243. package/src/capabilities/graph/runQuery.ts +23 -9
  244. package/src/capabilities/info/info.verb.ts +2 -0
  245. package/src/capabilities/links.shipped.exec.test.ts +398 -0
  246. package/src/capabilities/listBudget.shipped.exec.test.ts +2 -2
  247. package/src/capabilities/listLookup.shipped.exec.test.ts +122 -13
  248. package/src/capabilities/listQuery.shipped.exec.test.ts +39 -16
  249. package/src/capabilities/lookupBudget.shipped.exec.test.ts +208 -0
  250. package/src/capabilities/meta/mcp.verb.ts +2 -0
  251. package/src/capabilities/ontology/ontology.test.ts +24 -0
  252. package/src/capabilities/ontology/verbs.ts +16 -12
  253. package/src/capabilities/prompt/verbs.ts +10 -4
  254. package/src/capabilities/resources/provider.ts +7 -3
  255. package/src/capabilities/resources/resources.test.ts +2 -1
  256. package/src/capabilities/setup/setup.verb.ts +16 -0
  257. package/src/capabilities/shared/calls.ts +20 -0
  258. package/src/capabilities/skill/render.ts +9 -6
  259. package/src/capabilities/skill/verbs.ts +8 -4
  260. package/src/capabilities/sources/collectStatus.ts +6 -1
  261. package/src/capabilities/sources/index.ts +4 -3
  262. package/src/capabilities/sources/reset.render.test.ts +33 -0
  263. package/src/capabilities/sources/reset.render.ts +43 -0
  264. package/src/capabilities/sources/reset.verb.ts +59 -0
  265. package/src/capabilities/sources/runReset.ts +65 -0
  266. package/src/capabilities/sources/runUpdate.ts +12 -11
  267. package/src/capabilities/sources/sources.test.ts +158 -3
  268. package/src/capabilities/sources/status.render.test.ts +63 -1
  269. package/src/capabilities/sources/status.render.ts +25 -8
  270. package/src/capabilities/sources/status.verb.ts +2 -0
  271. package/src/capabilities/sources/types.ts +26 -0
  272. package/src/capabilities/sources/update.verb.ts +3 -0
  273. package/src/capabilities/surface.test.ts +29 -4
  274. package/src/capabilities/token.parity.test.ts +3 -2
  275. package/src/capabilities/tokenGraph.shipped.exec.test.ts +14 -2
  276. package/src/capabilities/upgrade/upgrade.verb.ts +1 -0
  277. package/src/capabilities/variable.parity.test.ts +2 -1
  278. package/src/capabilities/version/version.verb.ts +1 -0
  279. package/src/capabilities/wireContract.test.ts +110 -0
  280. package/src/constants.ts +4 -3
  281. package/src/kernel/completion/shellDrive.test.ts +2 -2
  282. package/src/kernel/copy.test.ts +1 -1
  283. package/src/kernel/error/PragmaError.test.ts +7 -3
  284. package/src/kernel/error/index.ts +1 -1
  285. package/src/kernel/error/recovery.test.ts +52 -18
  286. package/src/kernel/error/recovery.ts +25 -46
  287. package/src/kernel/error/types.ts +1 -1
  288. package/src/kernel/packs/collect.test.ts +79 -0
  289. package/src/kernel/packs/collect.ts +151 -11
  290. package/src/kernel/packs/compile.test.ts +107 -10
  291. package/src/kernel/packs/compile.ts +100 -11
  292. package/src/kernel/packs/filteredEmpty.test.ts +22 -5
  293. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +5 -6
  294. package/src/kernel/packs/graphqlSource.test.ts +42 -0
  295. package/src/kernel/packs/iri.ts +5 -4
  296. package/src/kernel/packs/listNotice.test.ts +1 -1
  297. package/src/kernel/packs/nounLink.test.ts +385 -0
  298. package/src/kernel/packs/renderPack.ts +137 -57
  299. package/src/kernel/packs/resolveEntity.test.ts +170 -1
  300. package/src/kernel/packs/resolveEntity.ts +281 -101
  301. package/src/kernel/packs/runBodies.ts +133 -31
  302. package/src/kernel/packs/schema.ts +77 -12
  303. package/src/kernel/packs/sparql/buildListQuery.test.ts +78 -1
  304. package/src/kernel/packs/sparql/buildListQuery.ts +106 -16
  305. package/src/kernel/packs/sparql/buildLookupQuery.ts +44 -13
  306. package/src/kernel/packs/sparql/filterValues.test.ts +36 -4
  307. package/src/kernel/packs/sparql/filterValues.ts +50 -31
  308. package/src/kernel/packs/sparql/runSelect.ts +4 -6
  309. package/src/kernel/packs/storyRules.ts +37 -4
  310. package/src/kernel/packs/tierScope.test.ts +120 -6
  311. package/src/kernel/packs/tierScope.ts +5 -7
  312. package/src/kernel/packs/toolDescription.test.ts +59 -74
  313. package/src/kernel/packs/types.ts +93 -11
  314. package/src/kernel/project/cli/buildProgram.ts +4 -0
  315. package/src/kernel/project/cli/dispatch.ts +34 -8
  316. package/src/kernel/project/cli/suggest.test.ts +77 -0
  317. package/src/kernel/project/cli/suggestNames.ts +47 -1
  318. package/src/kernel/project/cli/verbHelp.ts +16 -19
  319. package/src/kernel/project/mcp/buildServer.ts +15 -4
  320. package/src/kernel/project/mcp/envelope.ts +3 -2
  321. package/src/kernel/project/mcp/instructions.ts +123 -35
  322. package/src/kernel/project/mcp/prompts/prompts.test.ts +136 -20
  323. package/src/kernel/project/mcp/registerVerb.test.ts +28 -0
  324. package/src/kernel/project/mcp/registerVerb.ts +67 -29
  325. package/src/kernel/render/__snapshots__/renderers.test.ts.snap +1 -1
  326. package/src/kernel/render/renderers.test.ts +2 -2
  327. package/src/kernel/render/renderers.ts +3 -3
  328. package/src/kernel/render/turtle.ts +24 -4
  329. package/src/kernel/runtime/graphpack/build.ts +58 -1
  330. package/src/kernel/runtime/graphpack/graphpack.test.ts +20 -0
  331. package/src/kernel/runtime/graphpack/manifest.ts +15 -0
  332. package/src/kernel/runtime/graphpack/read.ts +4 -6
  333. package/src/kernel/runtime/graphpack/schemas.ts +1 -0
  334. package/src/kernel/runtime/graphpack/types.ts +13 -0
  335. package/src/kernel/runtime/loadSession.ts +8 -1
  336. package/src/kernel/runtime/packVersion.test.ts +61 -0
  337. package/src/kernel/runtime/packVersion.ts +84 -0
  338. package/src/kernel/runtime/readEntity.ts +61 -6
  339. package/src/kernel/runtime/resolveSources.test.ts +90 -7
  340. package/src/kernel/runtime/resolveSources.ts +91 -10
  341. package/src/kernel/runtime/storeReadiness.ts +4 -4
  342. package/src/kernel/runtime/types.ts +12 -0
  343. package/src/kernel/spec/call.test.ts +95 -0
  344. package/src/kernel/spec/call.ts +186 -0
  345. package/src/kernel/spec/emitReference.ts +9 -13
  346. package/src/kernel/spec/emitSurface.ts +9 -1
  347. package/src/kernel/spec/guidance.test.ts +49 -0
  348. package/src/kernel/spec/guidance.ts +68 -0
  349. package/src/kernel/spec/index.ts +4 -0
  350. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  351. package/src/kernel/spec/types.ts +64 -7
  352. package/src/kernel/spec/wireType.ts +53 -0
  353. package/src/testing/behavioral/README.md +1 -1
  354. package/src/testing/behavioral/errorMatrix.mcp.test.ts +2 -1
  355. package/src/testing/behavioral/journeys.firstInstall.test.ts +72 -2
  356. package/src/testing/behavioral/parity.test.ts +40 -0
  357. package/src/testing/eval/__snapshots__/eval.test.ts.snap +1 -1
  358. package/src/testing/eval/cases/assembly.ts +4 -3
  359. package/src/testing/eval/cases/stable.ts +2 -3
  360. package/src/testing/helpers/callSchema.ts +40 -0
  361. package/src/testing/helpers/parity.ts +35 -2
  362. package/src/testing/setupCallChecking.ts +30 -0
  363. package/dist/src/capabilities/capabilities/hints.js +0 -223
  364. package/dist/src/capabilities/capabilities/hints.js.map +0 -1
  365. package/src/capabilities/capabilities/hints.ts +0 -266
@@ -10,7 +10,7 @@
10
10
  * (lazy), so these factories carry no heavy static import.
11
11
  */
12
12
 
13
- import { cliRecovery, PragmaError } from "../error/index.js";
13
+ import { PragmaError } from "../error/index.js";
14
14
  import type { PragmaRuntime } from "../runtime/index.js";
15
15
  import { encodeCursor, pageFingerprint, readCursor } from "./cursor.js";
16
16
  import { resolvePackDetail } from "./disclosure.js";
@@ -20,10 +20,17 @@ import {
20
20
  type LookupOutput,
21
21
  type LookupScope,
22
22
  listEntityNames,
23
+ listRecovery,
24
+ resolveEntityIris,
23
25
  resolveLookup,
24
26
  } from "./resolveEntity.js";
25
27
  import { parseSampleCount, pickRandom } from "./sample.js";
26
- import { buildListQuery } from "./sparql/buildListQuery.js";
28
+ import {
29
+ buildListQuery,
30
+ buildTierCountQuery,
31
+ type ListPredicate,
32
+ type ListTierScope,
33
+ } from "./sparql/buildListQuery.js";
27
34
  import {
28
35
  type FilterVocabularies,
29
36
  resolveFilterPredicates,
@@ -31,6 +38,7 @@ import {
31
38
  import { runSelect } from "./sparql/runSelect.js";
32
39
  import { readSearchTerm } from "./sparql/searchTerm.js";
33
40
  import {
41
+ localName,
34
42
  resolveReadScope,
35
43
  scopeIris,
36
44
  scopeLabel,
@@ -38,34 +46,21 @@ import {
38
46
  } from "./tierScope.js";
39
47
  import {
40
48
  ENTITY_VARIABLE,
49
+ type NounLookups,
41
50
  type PackAppliedFilter,
42
51
  type PackFilter,
43
52
  type PackList,
44
53
  type PackLookup,
45
54
  type PackPage,
46
55
  type PackTierScope,
56
+ type PageTierScope,
47
57
  type StorySource,
58
+ UNTIERED_KEY,
48
59
  } from "./types.js";
49
60
 
50
61
  /** The highest canonical level — sample fetches everything for shape discovery. */
51
62
  const HIGHEST_LEVEL = "detailed";
52
63
 
53
- /**
54
- * The recovery every lookup-shaped miss carries: browse the noun's list.
55
- *
56
- * `mcp.params` is populated rather than left off. A bare tool NAME is only half
57
- * an instruction to an agent — it still has to guess an argument bag, and a
58
- * guess that misses returns `-32602 Invalid arguments`, which reads like the
59
- * recovery itself was wrong. `{}` is the concrete, valid call: every compiled
60
- * list verb's params are optional, so the recovery is now copy-pasteable.
61
- */
62
- function listRecovery(noun: string) {
63
- return cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
64
- tool: `${noun}_list`,
65
- params: {},
66
- });
67
- }
68
-
69
64
  /** Facts a list-shaped run body needs beyond its `shape`. */
70
65
  export interface ListRunMeta {
71
66
  readonly source: StorySource;
@@ -75,6 +70,10 @@ export interface ListRunMeta {
75
70
  * are. Absent leaves the read exactly as it was.
76
71
  */
77
72
  readonly tierScope?: PackTierScope;
73
+ /** The prefix map a prefixed IRI handed to a noun filter expands against. */
74
+ readonly prefixes: Readonly<Record<string, string>>;
75
+ /** The other stories' lookups, for a filter that names a noun. */
76
+ readonly nouns?: NounLookups;
78
77
  }
79
78
 
80
79
  /**
@@ -104,12 +103,20 @@ export function makeListRun(
104
103
  params,
105
104
  meta.source,
106
105
  );
107
- const predicates = resolveFilterPredicates(
108
- shape.filters,
109
- params,
110
- vocabularies,
111
- meta.source.label,
112
- );
106
+ const predicates = [
107
+ ...resolveFilterPredicates(
108
+ shape.filters?.filter((filter) => filter.noun === undefined),
109
+ params,
110
+ vocabularies,
111
+ meta.source.label,
112
+ ),
113
+ ...(await resolveNounPredicates(rt, shape.filters, params, meta)),
114
+ ];
115
+ // Left out of the rows: over the shipped pack the IRIs a noun filter
116
+ // constrains took the largest list past its payload budget (108 KB).
117
+ const omit = (shape.filters ?? [])
118
+ .flatMap((filter) => (filter.entity ? [filter.entity] : []))
119
+ .filter((name) => !shape.columns.some((column) => column.field === name));
113
120
  const search = readSearchTerm(shape.search, params);
114
121
  const scope = await resolveReadScope(
115
122
  rt,
@@ -142,18 +149,31 @@ export function makeListRun(
142
149
  ...(listScope ? [JSON.stringify(tiers)] : []),
143
150
  ]);
144
151
  const offset = readCursor(params.after, fingerprint);
152
+ const read = {
153
+ query: shape.query,
154
+ predicates,
155
+ ...(search ? { search } : {}),
156
+ label: meta.source.label,
157
+ };
145
158
  const rows = await runSelect(
146
159
  rt,
147
160
  buildListQuery({
148
- query: shape.query,
149
- predicates,
150
- ...(search ? { search } : {}),
161
+ ...read,
151
162
  ...(listScope ? { scope: listScope } : {}),
163
+ omit,
152
164
  window: { limit: limit + 1, offset },
153
- label: meta.source.label,
154
165
  }),
155
166
  meta.source,
156
167
  );
168
+ // First page only, and never the reason a list fails.
169
+ const counts =
170
+ scope?.kind === "tiers" && listScope && params.after === undefined
171
+ ? await readTierCounts(
172
+ rt,
173
+ { ...read, scope: listScope },
174
+ meta.source,
175
+ ).catch(() => undefined)
176
+ : undefined;
157
177
  const hasMore = rows.length > limit;
158
178
  const applied = appliedFilters(shape, params, search?.term, scope);
159
179
  return {
@@ -163,13 +183,45 @@ export function makeListRun(
163
183
  : {}),
164
184
  ...(applied.length > 0 ? { filters: applied } : {}),
165
185
  ...(scope?.kind === "tiers"
166
- ? { scope: { tiers: scope.tiers.map((tier) => tier.local) } }
186
+ ? {
187
+ scope: {
188
+ tiers: scope.tiers.map((tier) => tier.local),
189
+ ...(counts ? { counts } : {}),
190
+ },
191
+ }
167
192
  : {}),
168
193
  limit,
169
194
  };
170
195
  };
171
196
  }
172
197
 
198
+ /**
199
+ * Count the whole filtered answer's rows per tier: every in-scope tier (0 when
200
+ * it holds none), then the out-of-scope tiers that hold some, then
201
+ * {@link UNTIERED_KEY} when any row's entity is in no tier.
202
+ */
203
+ async function readTierCounts(
204
+ rt: PragmaRuntime,
205
+ read: Parameters<typeof buildTierCountQuery>[0] & { scope: ListTierScope },
206
+ source: StorySource,
207
+ ): Promise<NonNullable<PageTierScope["counts"]>> {
208
+ const query = buildTierCountQuery(read);
209
+ const byIri = query.read(await runSelect(rt, query.text, source));
210
+ const inScope = read.scope.tiers.map(
211
+ (iri) => [localName(iri), byIri.get(iri) ?? 0] as const,
212
+ );
213
+ const outside = [...byIri]
214
+ .filter(([iri]) => iri !== "" && !read.scope.tiers.includes(iri))
215
+ .map(([iri, count]) => [localName(iri), count] as const)
216
+ .sort(([a], [b]) => a.localeCompare(b));
217
+ const untiered = byIri.get("");
218
+ return Object.fromEntries([
219
+ ...inScope,
220
+ ...outside,
221
+ ...(untiered ? [[UNTIERED_KEY, untiered] as const] : []),
222
+ ]);
223
+ }
224
+
173
225
  /**
174
226
  * The filters this read was narrowed by, in the spelling the caller used.
175
227
  *
@@ -259,6 +311,52 @@ async function readFilterVocabularies(
259
311
  return resolved;
260
312
  }
261
313
 
314
+ /**
315
+ * Turn the values a caller supplied for each NOUN filter into a constraint on
316
+ * the entities they name.
317
+ *
318
+ * @returns One `"iri"` predicate per noun filter the caller used, over the
319
+ * IRIs its values reached. Against a store holding none of that noun's
320
+ * entities the predicate names no IRI and the list answers empty.
321
+ * @throws PragmaError INVALID_INPUT when a value reaches no entity of the noun;
322
+ * CONFIG_ERROR when no story known to this one declares a lookup for it.
323
+ */
324
+ async function resolveNounPredicates(
325
+ rt: PragmaRuntime,
326
+ filters: readonly PackFilter[] | undefined,
327
+ params: Record<string, unknown>,
328
+ meta: ListRunMeta,
329
+ ): Promise<ListPredicate[]> {
330
+ const predicates: ListPredicate[] = [];
331
+ for (const filter of filters ?? []) {
332
+ const provided = params[filter.param];
333
+ if (!filter.noun || !filter.entity || provided === undefined) continue;
334
+ const occurrences = Array.isArray(provided) ? provided : [provided];
335
+ if (occurrences.length === 0) continue;
336
+ const lookup = meta.nouns?.(filter.noun);
337
+ if (!lookup) {
338
+ throw PragmaError.configError(
339
+ `Filter "--${filter.param}" in ${meta.source.label} names the noun "${filter.noun}", and no story declares a lookup for it.`,
340
+ );
341
+ }
342
+ const terms = await resolveEntityIris(
343
+ rt,
344
+ lookup,
345
+ filter.noun,
346
+ occurrences.map((value) => String(value).trim().normalize("NFC")),
347
+ meta.source,
348
+ meta.prefixes,
349
+ );
350
+ predicates.push({
351
+ variable: filter.entity,
352
+ match: "iri",
353
+ terms,
354
+ ...(filter.via ? { via: filter.via } : {}),
355
+ });
356
+ }
357
+ return predicates;
358
+ }
359
+
262
360
  /**
263
361
  * Build the run body for a lookup verb (variadic names → resolved entities).
264
362
  *
@@ -274,6 +372,7 @@ export function makeLookupRun(
274
372
  noun: string,
275
373
  source: StorySource,
276
374
  prefixes: Readonly<Record<string, string>>,
375
+ hasList: boolean,
277
376
  tierScope?: PackTierScope,
278
377
  ): (
279
378
  params: Record<string, unknown>,
@@ -291,6 +390,7 @@ export function makeLookupRun(
291
390
  source,
292
391
  prefixes,
293
392
  level,
393
+ hasList,
294
394
  lookupScope(tierScope, scope),
295
395
  );
296
396
  // A total miss (single or all-miss) exits non-zero; a partial batch renders
@@ -302,7 +402,7 @@ export function makeLookupRun(
302
402
  code: first.code as PragmaError["code"],
303
403
  message: first.message,
304
404
  suggestions: first.suggestions ? [...first.suggestions] : undefined,
305
- recovery: listRecovery(noun),
405
+ recovery: listRecovery(noun, hasList),
306
406
  });
307
407
  }
308
408
  }
@@ -346,6 +446,7 @@ export function makeSampleRun(
346
446
  source: StorySource,
347
447
  prefixes: Readonly<Record<string, string>>,
348
448
  defaultCount: number,
449
+ hasList: boolean,
349
450
  ): (
350
451
  params: Record<string, unknown>,
351
452
  rt: PragmaRuntime,
@@ -358,7 +459,7 @@ export function makeSampleRun(
358
459
  if (names.length === 0) {
359
460
  throw PragmaError.emptyResults(noun, {
360
461
  message: `No ${noun} entries to sample.`,
361
- recovery: listRecovery(noun),
462
+ recovery: listRecovery(noun, hasList),
362
463
  });
363
464
  }
364
465
  const selected = pickRandom(names, count);
@@ -370,6 +471,7 @@ export function makeSampleRun(
370
471
  source,
371
472
  prefixes,
372
473
  HIGHEST_LEVEL,
474
+ hasList,
373
475
  );
374
476
  return {
375
477
  samples: output.results,
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { z } from "zod";
28
- import { DETAIL_LEVELS, RECOVERY_CLI_PREFIX } from "../../constants.js";
28
+ import { DETAIL_LEVELS } from "../../constants.js";
29
29
  import { PragmaError } from "../error/index.js";
30
30
  import { listShapeIssues, tierScopeIssues } from "./storyRules.js";
31
31
  import {
@@ -33,6 +33,7 @@ import {
33
33
  type PackList,
34
34
  type PackTierScope,
35
35
  RESERVED_STORY_PARAMS,
36
+ VERB_PATH_PATTERN,
36
37
  } from "./types.js";
37
38
 
38
39
  const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
@@ -96,8 +97,14 @@ const term = z.string().regex(TERM_PATTERN, "must be a prefixed name or IRI");
96
97
  const graphqlName = z.string().regex(GRAPHQL_NAME_PATTERN);
97
98
  const fieldName = z.string().regex(FIELD_PATTERN);
98
99
 
100
+ const nounName = z.string().regex(NOUN_PATTERN, NOUN_MESSAGE);
101
+
99
102
  const columnSchema = z
100
- .object({ field: fieldName, label: z.string().optional() })
103
+ .object({
104
+ field: fieldName,
105
+ label: z.string().optional(),
106
+ noun: nounName.optional(),
107
+ })
101
108
  .strict();
102
109
 
103
110
  const vocabularySchema = z
@@ -114,12 +121,22 @@ const filterSchema = z
114
121
  values: z.array(z.string()).min(1).optional(),
115
122
  match: z.enum(["exact", "set"]).optional(),
116
123
  vocabulary: vocabularySchema.optional(),
124
+ noun: nounName.optional(),
125
+ entity: fieldName.optional(),
126
+ via: term.optional(),
117
127
  description: z.string().optional(),
118
128
  })
119
129
  .strict()
120
130
  .refine((f) => !RESERVED_PARAMS.has(f.param), {
121
131
  message: "filter param is a reserved name",
122
132
  })
133
+ .refine((f) => !(f.noun && (f.values || f.vocabulary || f.match)), {
134
+ message:
135
+ '"noun" is mutually exclusive with "values", "vocabulary" and "match" — the named noun admits the values and the rows are matched by IRI',
136
+ })
137
+ .refine((f) => f.noun !== undefined || !(f.entity || f.via), {
138
+ message: '"entity" and "via" belong to a filter that names a "noun"',
139
+ })
123
140
  // A declared `values` set IS the vocabulary — the filter projects it as an
124
141
  // enum and canonicalizes against it. A second, graph-read one alongside would
125
142
  // be a silent no-op at best and a disagreement at worst.
@@ -144,25 +161,44 @@ const searchSchema = z
144
161
  })
145
162
  .strict();
146
163
 
164
+ // A call names a VERB PATH (`sources update`) and a param bag — never a command
165
+ // line. A story is portable only if it names no binary and spells no flags: the
166
+ // consuming distribution's renderer does both, for the surface it prints on.
167
+ const callSchema = z
168
+ .object({
169
+ verb: z
170
+ .string()
171
+ .regex(
172
+ VERB_PATH_PATTERN,
173
+ 'a call names a verb path — write "sources update", not a command line',
174
+ ),
175
+ params: z.record(z.string(), z.unknown()).optional(),
176
+ })
177
+ .strict();
178
+
147
179
  const emptyRecoverySchema = z
148
180
  .object({
149
181
  message: z.string().min(1),
150
- // The command WITHOUT a binary name — the renderer prepends the CONSUMING
151
- // distribution's. A story is portable only if it does not name a binary, so
152
- // the old prefixed form is rejected rather than accepted and doubled. Named
153
- // loudly, in the `packages` → `packs` tradition, because a third-party pack
154
- // written against the old grammar is data, not a typo.
182
+ call: callSchema.optional(),
183
+ // The retired spelling, named loudly in the `packages` → `packs` tradition:
184
+ // a third-party pack written against the old grammar is data, not a typo.
155
185
  cli: z
156
- .string()
157
- .min(1)
186
+ .unknown()
158
187
  .refine(
159
- (value) => !value.startsWith(RECOVERY_CLI_PREFIX),
160
- `emptyRecovery.cli is now the command WITHOUT the binary name — write "sources update", not "${RECOVERY_CLI_PREFIX}sources update"`,
188
+ (value) => value === undefined,
189
+ 'emptyRecovery.cli is now emptyRecovery.call — write call: { verb: "sources update" }, not cli: "sources update"',
161
190
  )
162
191
  .optional(),
163
192
  })
164
193
  .strict();
165
194
 
195
+ // What a story half tells a caller choosing between tools. Optional: the schema
196
+ // is strict and third-party packs predate these.
197
+ const guidanceShape = {
198
+ useWhen: z.string().min(1).optional(),
199
+ example: z.record(z.string(), z.unknown()).optional(),
200
+ };
201
+
166
202
  const listShape = {
167
203
  query: z.string().min(1),
168
204
  columns: z.array(columnSchema).min(1),
@@ -178,6 +214,7 @@ const verbSchema = z
178
214
  verb: z.string().regex(NOUN_PATTERN, NOUN_MESSAGE),
179
215
  description: z.string().optional(),
180
216
  toolDescription: z.string().optional(),
217
+ ...guidanceShape,
181
218
  })
182
219
  .strict();
183
220
 
@@ -188,6 +225,7 @@ const fieldSchema = z
188
225
  label: z.string().optional(),
189
226
  graphqlField: graphqlName.optional(),
190
227
  level: z.string().optional(),
228
+ noun: nounName.optional(),
191
229
  })
192
230
  .strict();
193
231
 
@@ -223,8 +261,14 @@ const expandFieldSchema = z
223
261
  label: z.string().optional(),
224
262
  graphqlField: graphqlName.optional(),
225
263
  blankWhenSelf: z.literal(true).optional(),
264
+ many: z.literal(true).optional(),
265
+ noun: nounName.optional(),
226
266
  })
227
- .strict();
267
+ .strict()
268
+ .refine((f) => !(f.many && f.blankWhenSelf), {
269
+ message:
270
+ '"many" and "blankWhenSelf" are mutually exclusive — a set of values has no single one to compare with the entity\'s own',
271
+ });
228
272
 
229
273
  const expandSchema = z
230
274
  .object({
@@ -276,6 +320,7 @@ const sampleSchema = z.union([
276
320
  fixedCount: z.boolean().optional(),
277
321
  description: z.string().optional(),
278
322
  toolDescription: z.string().optional(),
323
+ ...guidanceShape,
279
324
  })
280
325
  .strict(),
281
326
  ]);
@@ -328,6 +373,7 @@ const lookupSchema = z
328
373
  type: term.optional(),
329
374
  description: z.string().optional(),
330
375
  toolDescription: z.string().optional(),
376
+ ...guidanceShape,
331
377
  types: z.array(term).min(1).optional(),
332
378
  weights: z.record(term, z.number().min(0).max(1)).optional(),
333
379
  scopeWeight: scopeWeightSchema.optional(),
@@ -346,6 +392,7 @@ const definitionSchema = z
346
392
  noun: z.string().regex(NOUN_PATTERN, NOUN_MESSAGE),
347
393
  description: z.string().optional(),
348
394
  toolDescription: z.string().optional(),
395
+ ...guidanceShape,
349
396
  list: listSchema.optional(),
350
397
  verbs: z.array(verbSchema).min(1).optional(),
351
398
  lookup: lookupSchema.optional(),
@@ -599,6 +646,24 @@ function refineLookup(
599
646
  path: ["lookup"],
600
647
  });
601
648
  }
649
+ if ("many" in entry && entry.many && expandSource !== "sparql") {
650
+ ctx.addIssue({
651
+ code: z.ZodIssueCode.custom,
652
+ message: `expand "${expand.name}" field "${entry.name}" sets "many", which only the SPARQL lane can express — set the expand's "source" to "sparql".`,
653
+ path: ["lookup"],
654
+ });
655
+ }
656
+ if (
657
+ "many" in entry &&
658
+ entry.many &&
659
+ expand.orderBy?.includes(entry.name)
660
+ ) {
661
+ ctx.addIssue({
662
+ code: z.ZodIssueCode.custom,
663
+ message: `expand "${expand.name}" orders by "${entry.name}", a "many" field — a set of values has no order to sort on.`,
664
+ path: ["lookup"],
665
+ });
666
+ }
602
667
  }
603
668
  if (expand.orderBy !== undefined) {
604
669
  if (expandSource !== "sparql") {
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { describe, expect, it } from "vitest";
14
14
  import { PragmaError } from "../../error/index.js";
15
- import { buildListQuery } from "./buildListQuery.js";
15
+ import { buildListQuery, buildTierCountQuery } from "./buildListQuery.js";
16
16
 
17
17
  /** The author query every case below wraps: aggregate, grouped, ordered. */
18
18
  const AUTHORED = [
@@ -342,3 +342,80 @@ describe("buildListQuery — a story it cannot serve", () => {
342
342
  );
343
343
  });
344
344
  });
345
+
346
+ describe("buildTierCountQuery — the whole filtered answer, per tier", () => {
347
+ const scope = {
348
+ entity: "uri",
349
+ via: "ds:tier",
350
+ tiers: ["https://ds.canonical.com/global"],
351
+ };
352
+ const build = (query = AUTHORED) =>
353
+ buildTierCountQuery({
354
+ query,
355
+ predicates: [{ variable: "category", match: "exact", terms: ["react"] }],
356
+ search: { variables: ["name"], term: "props" },
357
+ scope,
358
+ label,
359
+ }).text;
360
+
361
+ it("wraps the author query under the page's own filter and search clauses", () => {
362
+ const page = buildListQuery({
363
+ query: AUTHORED,
364
+ predicates: [{ variable: "category", match: "exact", terms: ["react"] }],
365
+ search: { variables: ["name"], term: "props" },
366
+ window,
367
+ label,
368
+ });
369
+ const clauses = page
370
+ .split("\n")
371
+ .filter((line) => line.startsWith(" FILTER EXISTS"));
372
+ expect(clauses).toHaveLength(2);
373
+ for (const clause of clauses) expect(build()).toContain(clause);
374
+ // The author's text is inside it verbatim, as it is inside the page.
375
+ expect(build()).toContain(AUTHORED);
376
+ });
377
+
378
+ it("groups by the row's tier, which is OPTIONAL so an untiered row counts", () => {
379
+ expect(build()).toMatch(
380
+ /SELECT \?(\w+) \(COUNT\(\*\) AS \?\w+\)[\s\S]*OPTIONAL \{ \?uri ds:tier \?\1 \}\n\}\nGROUP BY \?\1$/,
381
+ );
382
+ });
383
+
384
+ it("reads its rows as tier → row count, the unbound tier as the empty key", () => {
385
+ const query = buildTierCountQuery({
386
+ query: AUTHORED,
387
+ predicates: [],
388
+ scope,
389
+ label,
390
+ });
391
+ const [, tier, count] =
392
+ /SELECT \?(\w+) \(COUNT\(\*\) AS \?(\w+)\)/.exec(query.text) ?? [];
393
+ const rows = [
394
+ {
395
+ [tier as string]: "https://ds.canonical.com/apps",
396
+ [count as string]: "3",
397
+ },
398
+ { [count as string]: "1" },
399
+ ];
400
+ expect([...query.read(rows)]).toEqual([
401
+ ["https://ds.canonical.com/apps", 3],
402
+ ["", 1],
403
+ ]);
404
+ });
405
+
406
+ it("carries neither the page nor the scope it is the breakdown OF", () => {
407
+ expect(build()).not.toMatch(/\bLIMIT\b|\bOFFSET\b/);
408
+ expect(build()).not.toContain("https://ds.canonical.com/global");
409
+ });
410
+
411
+ it("refuses an author query already using the generated prefix, filtered or not", () => {
412
+ expect(() =>
413
+ buildTierCountQuery({
414
+ query: "SELECT ?uri ?__pragmaX WHERE { ?uri a ds:Thing }",
415
+ predicates: [],
416
+ scope,
417
+ label,
418
+ }),
419
+ ).toThrow(PragmaError);
420
+ });
421
+ });