@canonical/pragma-cli 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -485,6 +485,98 @@ describe("weights validation (a weight that can never apply is rejected)", () =>
485
485
  });
486
486
  });
487
487
 
488
+ describe("nameFallback validation (a derived name needs a class to vouch for it)", () => {
489
+ it("accepts the fallback beside a class constraint", () => {
490
+ const definition = {
491
+ noun: "gadget",
492
+ lookup: { by: "ex:name", nameFallback: "iri", type: "ex:Widget" },
493
+ };
494
+ expect(parsePackDefinition(definition, "t")).toEqual(definition);
495
+ });
496
+
497
+ it("REJECTS the fallback with no class constraint", () => {
498
+ // Without one there is no triple bounding `?uri`, so the derived-name
499
+ // population would be scanned over the whole graph — and a derived name is
500
+ // only as trustworthy as the class vouching for the entity it came from.
501
+ expect(() =>
502
+ parsePackDefinition(
503
+ { noun: "gadget", lookup: { by: "ex:name", nameFallback: "iri" } },
504
+ "t",
505
+ ),
506
+ ).toThrow(/nameFallback/);
507
+ });
508
+ });
509
+
510
+ describe("filter vocabulary validation (where a value-free filter's values live)", () => {
511
+ const listShape = {
512
+ query: "SELECT ?uri ?kind WHERE { ?uri a ex:Widget ; ex:kind ?kind }",
513
+ columns: [{ field: "uri" }],
514
+ };
515
+
516
+ it("accepts a vocabulary SELECT on a value-free filter", () => {
517
+ const definition = {
518
+ noun: "widget",
519
+ list: {
520
+ ...listShape,
521
+ filters: [
522
+ {
523
+ param: "kind",
524
+ variable: "kind",
525
+ vocabulary: { query: "SELECT ?kind WHERE { ?k a ex:Kind }" },
526
+ },
527
+ ],
528
+ },
529
+ };
530
+ expect(parsePackDefinition(definition, "t")).toEqual(definition);
531
+ });
532
+
533
+ it("REJECTS a vocabulary beside a declared `values` set", () => {
534
+ // A declared set IS the vocabulary — the filter projects it as an enum and
535
+ // canonicalizes against it. A second, graph-read one alongside is a silent
536
+ // no-op at best and a disagreement at worst.
537
+ expect(() =>
538
+ parsePackDefinition(
539
+ {
540
+ noun: "widget",
541
+ list: {
542
+ ...listShape,
543
+ filters: [
544
+ {
545
+ param: "kind",
546
+ variable: "kind",
547
+ values: ["a", "b"],
548
+ vocabulary: { query: "SELECT ?kind WHERE { ?k a ex:Kind }" },
549
+ },
550
+ ],
551
+ },
552
+ },
553
+ "t",
554
+ ),
555
+ ).toThrow(/mutually exclusive/);
556
+ });
557
+
558
+ it("REJECTS a vocabulary query that is not a SELECT", () => {
559
+ expect(() =>
560
+ parsePackDefinition(
561
+ {
562
+ noun: "widget",
563
+ list: {
564
+ ...listShape,
565
+ filters: [
566
+ {
567
+ param: "kind",
568
+ variable: "kind",
569
+ vocabulary: { query: "ASK { ?k a ex:Kind }" },
570
+ },
571
+ ],
572
+ },
573
+ },
574
+ "t",
575
+ ),
576
+ ).toThrow(/SELECT/);
577
+ });
578
+ });
579
+
488
580
  describe("compileListable — absolute IRIs reach the prefixed contract", () => {
489
581
  it("compacts an absolute lookup type, and its weight key with it", () => {
490
582
  // `PackLookup.type`/`types` accept a prefixed name OR an absolute IRI, but
@@ -55,7 +55,7 @@ export interface RenderMeta {
55
55
  * list on a BUILT store (a cold store would have failed with STORE_UNAVAILABLE
56
56
  * first) means "nothing matched", so point at both possible fixes.
57
57
  */
58
- const DEFAULT_EMPTY_HINT = `Build the store with \`${BIN_NAME} sources update\`, or broaden the filter or channel.`;
58
+ const DEFAULT_EMPTY_HINT = `Either nothing matched — try a wider filter — or the store has nothing in it yet: build it with \`${BIN_NAME} sources update\`.`;
59
59
 
60
60
  /** Build the list formatters for a list-shaped verb (list or an extra verb). */
61
61
  export function listFormatters(
@@ -89,7 +89,7 @@ export function listFormatters(
89
89
  json: (rows) => JSON.stringify(rows, null, 2),
90
90
  // Zero rows: the dispatcher routes this to stderr (exit 0) so the plain
91
91
  // stdout stream stays pure data; llm/json keep their own empty shapes.
92
- emptyNotice: (rows) =>
92
+ notice: (rows) =>
93
93
  rows.length === 0 ? renderListEmptyNotice(options) : undefined,
94
94
  };
95
95
  }
@@ -132,7 +132,7 @@ export function lookupOptions(
132
132
  };
133
133
  }
134
134
 
135
- /** Build the lookup formatters (renders every resolved entity, then any errors). */
135
+ /** Build the lookup formatters (every resolved entity, then errors, then the notice). */
136
136
  export function lookupFormatters(
137
137
  lookup: PackLookup,
138
138
  prefixes: Readonly<Record<string, string>>,
@@ -7,8 +7,18 @@
7
7
  * and nothing else — as accepting a name, a prefixed name, an absolute IRI, or
8
8
  * a glob. These assert that the resolver honours the shape it is handed rather
9
9
  * than the source its pack declares, that an entity is addressable by IRI even
10
- * when it carries no `by` value, and that an ambiguous name resolves to the
11
- * same entity on every store and every machine.
10
+ * when it carries no `by` value, and that an ambiguous name answers with the
11
+ * same entity on every store and every machine — while NAMING the ones it did
12
+ * not answer with.
13
+ *
14
+ * That last clause is a REVERSAL, signed off by the owner. It used to read "an
15
+ * ambiguous name resolves to the SAME entity every time", and the suite proved
16
+ * it: a total `ORDER BY` under a `LIMIT 1` does make the answer reproducible.
17
+ * It just made it reproducibly SILENT — `block lookup button` answered with
18
+ * Launchpad's Button on every machine, because `apps_launchpad…` sorts before
19
+ * `global…`, and nothing in the payload said the global one existed. The arity
20
+ * is unchanged and deliberately so; what the old assertions took for the whole
21
+ * property, determinism, was only half of it.
12
22
  */
13
23
 
14
24
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -20,7 +30,7 @@ import {
20
30
  import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
21
31
  import type { PragmaRuntime } from "../runtime/types.js";
22
32
  import { compilePack } from "./compile.js";
23
- import { lookupOptions } from "./renderPack.js";
33
+ import { lookupFormatters, lookupOptions } from "./renderPack.js";
24
34
  import type { LookupOutput } from "./resolveEntity.js";
25
35
  import type { PackDefinition, PackLookup } from "./types.js";
26
36
  import { distributionSource } from "./types.js";
@@ -79,6 +89,14 @@ describe("pack lookup addressing (PROTECTED)", () => {
79
89
  return verb.run({ name }, rt) as Promise<LookupOutput>;
80
90
  };
81
91
 
92
+ const lookupNoticeVia = (
93
+ definition: PackDefinition,
94
+ out: LookupOutput,
95
+ ): string | undefined =>
96
+ lookupFormatters(definition.lookup as PackLookup, BLOCK_PREFIXES).notice?.(
97
+ out,
98
+ );
99
+
82
100
  const uris = (out: LookupOutput): string[] =>
83
101
  out.results.map((entity) => String(entity.uri));
84
102
 
@@ -182,32 +200,75 @@ describe("pack lookup addressing (PROTECTED)", () => {
182
200
  });
183
201
  });
184
202
 
185
- describe("an ambiguous name resolves to the SAME entity every time", () => {
203
+ describe("an ambiguous name answers with EVERY entity it reaches", () => {
186
204
  // `ds:zeta.chip` is declared BEFORE `ds:alpha.chip` in the fixture, so the
187
- // store enumerates it first while IRI order puts `alpha` first. Without an
188
- // explicit ORDER BY the winner is whichever the store happens to yield —
189
- // exactly the cross-tier `Button` ambiguity in the live graph.
190
- it("picks the lowest IRI on the sparql path", async () => {
205
+ // store enumerates it first while IRI order puts `alpha` first. Neither of
206
+ // these stories declares a ranking, so the order is the total `STR(?uri)`
207
+ // it always was — what changed is that BOTH chips come back. Both paths,
208
+ // because the resolve is generated SPARQL either way and only the field
209
+ // fetch differs.
210
+ it("answers with both, best first, on the sparql path", async () => {
191
211
  const out = await lookupVia(SPQ, "Chip");
192
- expect(uris(out)).toEqual([`${DS}alpha.chip`]);
212
+ expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
193
213
  });
194
214
 
195
- it("picks the lowest IRI on the graphql path", async () => {
215
+ it("answers with both, best first, on the graphql path", async () => {
196
216
  const out = await lookupVia(GQL, "Chip");
197
- expect(uris(out)).toEqual([`${DS}alpha.chip`]);
217
+ expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
198
218
  });
199
219
 
200
220
  it("agrees with itself across repeated resolves", async () => {
221
+ // Determinism was never the property in question — `LIMIT 1` over a total
222
+ // order was already deterministic. It is pinned because the order now
223
+ // carries the ranking's judgement, and an order that varied by store
224
+ // would make the first result mean nothing.
201
225
  const runs = await Promise.all([
202
226
  lookupVia(SPQ, "Chip"),
203
227
  lookupVia(SPQ, "chip"),
204
228
  lookupVia(GQL, "CHIP"),
205
229
  ]);
206
230
  expect(runs.map(uris)).toEqual([
207
- [`${DS}alpha.chip`],
208
- [`${DS}alpha.chip`],
209
- [`${DS}alpha.chip`],
231
+ [`${DS}alpha.chip`, `${DS}zeta.chip`],
232
+ [`${DS}alpha.chip`, `${DS}zeta.chip`],
233
+ [`${DS}alpha.chip`, `${DS}zeta.chip`],
210
234
  ]);
211
235
  });
236
+
237
+ it("answers with ONE entity for an unambiguous name", async () => {
238
+ // The plural case costs the singular one nothing: a unique name still
239
+ // returns an array of one, which is the payload it always had.
240
+ const out = await lookupVia(GQL, "Modal");
241
+ expect(uris(out)).toEqual([`${DS}modal`]);
242
+ });
243
+
244
+ it("answers with ONE entity for an IRI, even a shared name's IRI", async () => {
245
+ // An IRI is one entity by construction, so the IRI form keeps its
246
+ // `LIMIT 1`: there is nothing to rank and nothing else to reach.
247
+ const out = await lookupVia(SPQ, "ds:zeta.chip");
248
+ expect(uris(out)).toEqual([`${DS}zeta.chip`]);
249
+ });
250
+
251
+ it("reaches both entities when a GLOB expands onto the shared name", async () => {
252
+ // The glob population is DISTINCT names, so "Chip" expands to ONE
253
+ // candidate — a glob was never the escape a shared name needs. Live,
254
+ // `block lookup 'Butt*'` listed Launchpad's Button and ButtonLink while
255
+ // the global Button appeared nowhere. Now the shared name reaches both
256
+ // through the glob for the same reason it does through a plain name.
257
+ const out = await lookupVia(SPQ, "Chi*");
258
+ expect(uris(out)).toEqual([`${DS}alpha.chip`, `${DS}zeta.chip`]);
259
+ });
260
+
261
+ it("adds no notice — the payload IS the address", async () => {
262
+ // An earlier draft kept the arity and named the outranked IRIs in a
263
+ // notice. With every entity returned there is nothing set aside to name,
264
+ // and a sentence restating what the payload already carries is noise.
265
+ // The zero-record notice is untouched; only the ambiguity one is gone.
266
+ expect(
267
+ lookupNoticeVia(SPQ, await lookupVia(SPQ, "Chip")),
268
+ ).toBeUndefined();
269
+ expect(
270
+ lookupNoticeVia(SPQ, await lookupVia(SPQ, "Modal")),
271
+ ).toBeUndefined();
272
+ });
212
273
  });
213
274
  });
@@ -42,6 +42,7 @@ import type {
42
42
  PackChildRow,
43
43
  PackEntity,
44
44
  PackLookup,
45
+ PackRow,
45
46
  StorySource,
46
47
  } from "./types.js";
47
48
 
@@ -53,7 +54,15 @@ export interface LookupError {
53
54
  readonly suggestions?: readonly string[];
54
55
  }
55
56
 
56
- /** The result of a (possibly multi-name) lookup. */
57
+ /**
58
+ * The result of a (possibly multi-name) lookup.
59
+ *
60
+ * `results` may be LONGER than the arguments that produced it: a name several
61
+ * entities share answers with all of them, ranked. There is no companion field
62
+ * naming the ones not answered with, because there are none — an earlier draft
63
+ * kept the single-entity arity and carried the rest as IRIs in a notice, which
64
+ * is a sentence about an address where the payload is the address.
65
+ */
57
66
  export interface LookupOutput {
58
67
  readonly results: PackEntity[];
59
68
  readonly errors: LookupError[];
@@ -80,6 +89,9 @@ export async function resolveLookup(
80
89
  throw PragmaError.invalidInput("names", "(empty)", {
81
90
  recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
82
91
  tool: `${noun}_list`,
92
+ // A concrete, valid argument bag rather than a bare tool name: an
93
+ // agent can call the recovery as written instead of guessing one.
94
+ params: {},
83
95
  }),
84
96
  });
85
97
  }
@@ -103,7 +115,7 @@ export async function resolveLookup(
103
115
  const query = expanded.names[index];
104
116
  if (query === undefined) continue;
105
117
  if (outcome.status === "fulfilled") {
106
- results.push(outcome.value);
118
+ results.push(...outcome.value);
107
119
  continue;
108
120
  }
109
121
  const error = outcome.reason;
@@ -186,7 +198,35 @@ async function expandQueries(
186
198
  return { names, globErrors };
187
199
  }
188
200
 
189
- /** Look up one entity, dispatching to the pack's declared fetch source. */
201
+ /**
202
+ * Look up every entity a lookup argument reaches, dispatching to the pack's
203
+ * declared fetch source.
204
+ *
205
+ * One argument, EVERY entity it reaches — ranked, best first.
206
+ *
207
+ * `lookup` is deliberately one tool for the singular and the plural case. An
208
+ * agent that had to decide up front whether a name is unique would pay two
209
+ * round trips to find out, and the answer is not knowable from the name: 25
210
+ * live block names reach two or three blocks apiece, and the caller cannot
211
+ * tell which until it asks. Returning them all costs the caller nothing when a
212
+ * name is unique — the array is one long — and saves a whole exchange when it
213
+ * is not.
214
+ *
215
+ * That is why the `LIMIT 1` had to go rather than be compensated for. With it,
216
+ * `block lookup button` answered with Launchpad's Button and gave no sign the
217
+ * global one existed, because `apps_launchpad…` sorts before `global…`. An
218
+ * earlier draft of this change kept the arity and named the losers in a notice;
219
+ * a notice is a sentence about an address, and the payload is the address.
220
+ *
221
+ * Ranking still matters, and matters more: it is now what ORDERS the answer
222
+ * rather than what silently picks it. The best row leads, so a caller reading
223
+ * only the first gets the entity the ranking believes was meant.
224
+ *
225
+ * The rows are collapsed to one per `?uri` first. The sparql form projects its
226
+ * fields in the same SELECT, so a multi-valued field yields one row per value;
227
+ * that has always been true and `LIMIT 1` merely hid it. Without the collapse
228
+ * one entity would report itself as several.
229
+ */
190
230
  async function lookupOne(
191
231
  rt: LookupRuntime,
192
232
  lookup: PackLookup,
@@ -195,47 +235,71 @@ async function lookupOne(
195
235
  source: StorySource,
196
236
  prefixes: Readonly<Record<string, string>>,
197
237
  level: string | undefined,
198
- ): Promise<PackEntity> {
238
+ ): Promise<PackEntity[]> {
199
239
  const graphqlSourced = lookup.source === "graphql";
200
240
  const rows = await runSelect(
201
241
  rt,
202
242
  buildResolveQuery(lookup, query, prefixes, level),
203
243
  source,
204
244
  );
205
- const base = rows.at(0);
206
- if (!base?.uri) {
245
+ const bases = firstRowPerEntity(rows);
246
+ if (bases.length === 0) {
207
247
  const candidates = await listEntityNames(rt, lookup, source);
208
248
  throw PragmaError.notFound(noun, query, {
209
249
  suggestions: suggestNames(query, candidates),
210
250
  recovery: cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
211
251
  tool: `${noun}_list`,
252
+ // A concrete, valid argument bag rather than a bare tool name: an
253
+ // agent can call the recovery as written instead of guessing one.
254
+ params: {},
212
255
  }),
213
256
  });
214
257
  }
215
258
 
216
259
  if (graphqlSourced) {
217
- return fetchGraphqlLookup(
218
- rt,
219
- lookup,
220
- base.uri,
221
- base.name ?? query,
222
- // The GraphQL lane's only use of the source is CONFIG_ERROR attribution,
223
- // which is right for any origin — so it takes the label, not provenance.
224
- source.label,
225
- prefixes,
226
- level,
260
+ return Promise.all(
261
+ bases.map((base) =>
262
+ fetchGraphqlLookup(
263
+ rt,
264
+ lookup,
265
+ String(base.uri),
266
+ base.name ?? query,
267
+ // The GraphQL lane's only use of the source is CONFIG_ERROR
268
+ // attribution, which is right for any origin — so it takes the label,
269
+ // not provenance.
270
+ source.label,
271
+ prefixes,
272
+ level,
273
+ ),
274
+ ),
227
275
  );
228
276
  }
229
277
 
230
- const entity: PackEntity = { ...base };
231
- for (const expand of activeExpands(lookup, level)) {
232
- entity[expand.name] = (await runSelect(
233
- rt,
234
- buildExpandQuery(expand, base.uri),
235
- source,
236
- )) as readonly PackChildRow[];
278
+ const entities: PackEntity[] = [];
279
+ for (const base of bases) {
280
+ const entity: PackEntity = { ...base };
281
+ for (const expand of activeExpands(lookup, level)) {
282
+ entity[expand.name] = (await runSelect(
283
+ rt,
284
+ buildExpandQuery(expand, String(base.uri)),
285
+ source,
286
+ )) as readonly PackChildRow[];
287
+ }
288
+ entities.push(entity);
289
+ }
290
+ return entities;
291
+ }
292
+
293
+ /** Collapse a ranked resolve to one row per entity, keeping the best-ranked. */
294
+ function firstRowPerEntity(rows: readonly PackRow[]): PackRow[] {
295
+ const seen = new Set<string>();
296
+ const bases: PackRow[] = [];
297
+ for (const row of rows) {
298
+ if (!row.uri || seen.has(row.uri)) continue;
299
+ seen.add(row.uri);
300
+ bases.push(row);
237
301
  }
238
- return entity;
302
+ return bases;
239
303
  }
240
304
 
241
305
  /**
@@ -19,14 +19,39 @@ import {
19
19
  resolveLookup,
20
20
  } from "./resolveEntity.js";
21
21
  import { parseSampleCount, pickRandom } from "./sample.js";
22
- import { applyPackFilters } from "./sparql/applyFilters.js";
22
+ import {
23
+ applyPackFilters,
24
+ type FilterVocabularies,
25
+ } from "./sparql/applyFilters.js";
23
26
  import { applyPackSearch } from "./sparql/applySearch.js";
24
27
  import { runSelect } from "./sparql/runSelect.js";
25
- import type { PackList, PackLookup, PackRow, StorySource } from "./types.js";
28
+ import type {
29
+ PackFilter,
30
+ PackList,
31
+ PackLookup,
32
+ PackRow,
33
+ StorySource,
34
+ } from "./types.js";
26
35
 
27
36
  /** The highest canonical level — sample fetches everything for shape discovery. */
28
37
  const HIGHEST_LEVEL = "detailed";
29
38
 
39
+ /**
40
+ * The recovery every lookup-shaped miss carries: browse the noun's list.
41
+ *
42
+ * `mcp.params` is populated rather than left off. A bare tool NAME is only half
43
+ * an instruction to an agent — it still has to guess an argument bag, and a
44
+ * guess that misses returns `-32602 Invalid arguments`, which reads like the
45
+ * recovery itself was wrong. `{}` is the concrete, valid call: every compiled
46
+ * list verb's params are optional, so the recovery is now copy-pasteable.
47
+ */
48
+ function listRecovery(noun: string) {
49
+ return cliRecovery(`${noun} list`, `List available ${noun} entries.`, {
50
+ tool: `${noun}_list`,
51
+ params: {},
52
+ });
53
+ }
54
+
30
55
  /** Facts a list-shaped run body needs beyond its `shape`. */
31
56
  export interface ListRunMeta {
32
57
  readonly source: StorySource;
@@ -45,16 +70,64 @@ export function makeListRun(
45
70
  shape: PackList,
46
71
  meta: ListRunMeta,
47
72
  ): (params: Record<string, unknown>, rt: PragmaRuntime) => Promise<PackRow[]> {
48
- return async (params, rt) =>
49
- applyPackSearch(
50
- applyPackFilters(
51
- await runSelect(rt, shape.query, meta.source),
52
- shape.filters,
53
- params,
54
- ),
73
+ return async (params, rt) => {
74
+ const rows = await runSelect(rt, shape.query, meta.source);
75
+ const vocabularies = await readFilterVocabularies(
76
+ rt,
77
+ shape.filters,
78
+ params,
79
+ meta.source,
80
+ );
81
+ return applyPackSearch(
82
+ applyPackFilters(rows, shape.filters, params, vocabularies),
55
83
  shape.search,
56
84
  params,
57
85
  );
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Read the declared vocabulary of each value-free filter the caller actually
91
+ * used.
92
+ *
93
+ * WHY a second query rather than the rows already in hand: the rows are a
94
+ * population, not a vocabulary. A category the graph declares with no standards
95
+ * filed under it, or a `ds:ConceptType` no concept uses yet, is a REAL value
96
+ * that appears in no row — and rejecting it as `INVALID_INPUT` contradicts both
97
+ * `standard categories` (which lists it, with count 0) and the documented calm
98
+ * empty list. The query reads the same terms the enumerating surface reads, so
99
+ * "the graph is the vocabulary" stays true of the graph rather than of whatever
100
+ * the list happened to return.
101
+ *
102
+ * Only for a filter that is DECLARED with a vocabulary, carries no `values`
103
+ * (a declared set is already the vocabulary), and was actually PROVIDED — an
104
+ * unfiltered `list` runs exactly the one query it always did.
105
+ */
106
+ async function readFilterVocabularies(
107
+ rt: PragmaRuntime,
108
+ filters: readonly PackFilter[] | undefined,
109
+ params: Record<string, unknown>,
110
+ source: StorySource,
111
+ ): Promise<FilterVocabularies | undefined> {
112
+ const needed = (filters ?? []).filter(
113
+ (filter) =>
114
+ filter.vocabulary !== undefined &&
115
+ filter.values === undefined &&
116
+ params[filter.param] !== undefined,
117
+ );
118
+ if (needed.length === 0) return undefined;
119
+ const resolved = new Map<string, readonly string[]>();
120
+ for (const filter of needed) {
121
+ const vocabulary = filter.vocabulary;
122
+ if (!vocabulary) continue;
123
+ const variable = vocabulary.variable ?? filter.variable;
124
+ const rows = await runSelect(rt, vocabulary.query, source);
125
+ resolved.set(
126
+ filter.param,
127
+ rows.map((row) => row[variable] ?? "").filter((value) => value !== ""),
128
+ );
129
+ }
130
+ return resolved;
58
131
  }
59
132
 
60
133
  /** Build the run body for a lookup verb (variadic names → resolved entities). */
@@ -88,11 +161,7 @@ export function makeLookupRun(
88
161
  code: first.code as PragmaError["code"],
89
162
  message: first.message,
90
163
  suggestions: first.suggestions ? [...first.suggestions] : undefined,
91
- recovery: cliRecovery(
92
- `${noun} list`,
93
- `List available ${noun} entries.`,
94
- { tool: `${noun}_list` },
95
- ),
164
+ recovery: listRecovery(noun),
96
165
  });
97
166
  }
98
167
  }
@@ -119,11 +188,7 @@ export function makeSampleRun(
119
188
  if (names.length === 0) {
120
189
  throw PragmaError.emptyResults(noun, {
121
190
  message: `No ${noun} entries to sample.`,
122
- recovery: cliRecovery(
123
- `${noun} list`,
124
- `List available ${noun} entries.`,
125
- { tool: `${noun}_list` },
126
- ),
191
+ recovery: listRecovery(noun),
127
192
  });
128
193
  }
129
194
  const selected = pickRandom(names, count);