@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
@@ -23,6 +23,7 @@ import type {
23
23
  } from "../../kernel/config/types.js";
24
24
  import { PragmaError } from "../../kernel/error/PragmaError.js";
25
25
  import { executeVerb } from "../../kernel/project/cli/dispatch.js";
26
+ import { styleFor } from "../../kernel/render/style.js";
26
27
  import { bootRuntime } from "../../kernel/runtime/boot.js";
27
28
  import { createQueryFacade } from "../../kernel/runtime/facade.js";
28
29
  import { readManifest } from "../../kernel/runtime/graphpack/manifest.js";
@@ -44,6 +45,7 @@ import { discoverSkills } from "../skill/discover.js";
44
45
  import { collectStatus } from "./collectStatus.js";
45
46
  import { sourcesModule } from "./index.js";
46
47
  import { buildUpdateTask } from "./runUpdate.js";
48
+ import { renderSourcesStatusPlain } from "./status.render.js";
47
49
 
48
50
  const FLAGS: GlobalFlags = {
49
51
  llm: false,
@@ -69,15 +71,25 @@ const tmp = (prefix: string): string => {
69
71
  return dir;
70
72
  };
71
73
 
72
- /** A runtime whose config is the given pack list (no config files needed). */
73
- function runtimeFor(cwd: string, packs: PackDeclaration[]): PragmaRuntime {
74
+ /**
75
+ * A runtime whose config is the given pack list (no config files needed).
76
+ *
77
+ * `packsOrigin` is the knob the boot decision reads to tell "this project pinned
78
+ * its own packs" from "the distribution's defaults" — `"project"` by default,
79
+ * since that is what a test that BUILDS a pack is describing.
80
+ */
81
+ function runtimeFor(
82
+ cwd: string,
83
+ packs: PackDeclaration[],
84
+ packsOrigin: "default" | "project" = "project",
85
+ ): PragmaRuntime {
74
86
  const layers: ConfigLayers = {
75
87
  config: { channel: "normal", packs },
76
88
  origins: {
77
89
  tier: "default",
78
90
  channel: "default",
79
91
  detail: "default",
80
- packs: "project",
92
+ packs: packsOrigin,
81
93
  stories: "default",
82
94
  prefixes: "default",
83
95
  },
@@ -562,6 +574,9 @@ describe("sources update — data-failure classification (U6)", () => {
562
574
  expect(err.recovery?.message ?? "").not.toContain("report this issue");
563
575
  // The recovery points the user at a runnable, useful next step.
564
576
  expect(err.recovery?.cli).toBe("pragma sources update --verbose");
577
+ // `--verbose` is a global CLI flag with no MCP counterpart, and a hint
578
+ // never carries `confirm`: the plan the tool returns says how to proceed.
579
+ expect(err.recovery?.mcp).toEqual({ tool: "sources_update", params: {} });
565
580
  // Nothing was pointed at on failure.
566
581
  expect(readActivePack(cwd)).toBeUndefined();
567
582
  });
@@ -1255,3 +1270,143 @@ describe("sources status CLI-json == MCP tool (PROTECTED)", () => {
1255
1270
  expect((cliEnvelope.data as { store: string }).store).toBe("embedded");
1256
1271
  });
1257
1272
  });
1273
+
1274
+ describe("a pack an older CLI built (upgrade healing)", () => {
1275
+ const resetVerb = sourcesModule.verbs[2] as VerbSpec;
1276
+ const REAL = { dryRun: false, undo: false, yes: true };
1277
+ /** Any version below the running CLI's — what an older build recorded. */
1278
+ const OLDER = "0.1.0";
1279
+
1280
+ /** Build a real pack for `cwd`, then rewrite the builder version it records. */
1281
+ async function builtWithVersion(
1282
+ cwd: string,
1283
+ version: string,
1284
+ ): Promise<string> {
1285
+ const pkg = filePackage();
1286
+ const runtime = runtimeFor(cwd, [
1287
+ { name: "pkg-old", source: `file://${pkg}` },
1288
+ ]);
1289
+ const result = await runTask(await buildUpdateTask(runtime));
1290
+ const path = join(packDir(result.contentHash), "manifest.json");
1291
+ const manifest = JSON.parse(readFileSync(path, "utf-8"));
1292
+ writeFileSync(path, JSON.stringify({ ...manifest, version }));
1293
+ return result.contentHash;
1294
+ }
1295
+
1296
+ it("status reports the snapshot answering, and names the pack passed over", async () => {
1297
+ const cwd = tmp("pragma-proj-");
1298
+ const hash = await builtWithVersion(cwd, OLDER);
1299
+
1300
+ // The reported install: a pointer to a pack an older CLI built, and a config
1301
+ // that pins no packs of its own (the distribution's defaults).
1302
+ const status = await collectStatus(runtimeFor(cwd, [], "default"));
1303
+ expect(status.store).toBe("embedded");
1304
+ expect(status.ignoredPack).toEqual({
1305
+ contentHash: hash,
1306
+ builtBy: OLDER,
1307
+ builtAt: expect.any(String),
1308
+ });
1309
+ expect(renderSourcesStatusPlain(status, styleFor(false))).toContain(
1310
+ `a pack built by pragma ${OLDER} on`,
1311
+ );
1312
+ });
1313
+
1314
+ it("a project with its OWN packs keeps reading its pack, stale or not", async () => {
1315
+ const cwd = tmp("pragma-proj-");
1316
+ const hash = await builtWithVersion(cwd, OLDER);
1317
+
1318
+ // Byte-identical to the behaviour before the upgrade row existed: its own
1319
+ // packs are its own graph, and the snapshot is not a substitute for them.
1320
+ const status = await collectStatus(
1321
+ runtimeFor(cwd, [{ name: "pkg-old", source: "file:///x" }], "project"),
1322
+ );
1323
+ expect(status.store).toBe("built");
1324
+ expect(status.contentHash).toBe(hash);
1325
+ expect(status.ignoredPack).toBeNull();
1326
+ });
1327
+
1328
+ it("re-running the update makes the pack answer again, and the notice goes", async () => {
1329
+ const cwd = tmp("pragma-proj-");
1330
+ const hash = await builtWithVersion(cwd, OLDER);
1331
+ const pkg = filePackage();
1332
+
1333
+ // The STRICTEST form of this case: the sources have not moved, so the update
1334
+ // reuses the very same content-addressed directory rather than building a
1335
+ // new one. Reuse has to refresh the recorded builder version, or the update
1336
+ // the notice recommends would change nothing and the project would be stuck.
1337
+ const rebuilt = await runTask(
1338
+ await buildUpdateTask(
1339
+ runtimeFor(cwd, [{ name: "pkg-old", source: `file://${pkg}` }]),
1340
+ ),
1341
+ );
1342
+ expect(rebuilt.contentHash).toBe(hash);
1343
+ expect(rebuilt.reused).toBe(true);
1344
+ expect(readManifest(packDir(hash))?.version).toBe(VERSION);
1345
+
1346
+ const status = await collectStatus(runtimeFor(cwd, [], "default"));
1347
+ expect(status.store).toBe("built");
1348
+ expect(status.ignoredPack).toBeNull();
1349
+ });
1350
+
1351
+ it("`sources reset` drops the pointer, and the snapshot answers", async () => {
1352
+ const cwd = tmp("pragma-proj-");
1353
+ const hash = await builtWithVersion(cwd, OLDER);
1354
+
1355
+ const outcome = await executeVerb(
1356
+ resetVerb,
1357
+ {},
1358
+ REAL,
1359
+ bootRuntime(FLAGS, cwd),
1360
+ );
1361
+ expect(outcome.exitCode).toBe(0);
1362
+ expect(outcome.stdout).toContain(hash.slice(0, 12));
1363
+ expect(outcome.stdout).toContain("snapshot shipped with the CLI");
1364
+ // The pointer is gone, so the boot has nothing to pass over any more.
1365
+ expect(readActivePack(cwd)).toBeUndefined();
1366
+ const status = await collectStatus(bootRuntime(FLAGS, cwd));
1367
+ expect(status.store).toBe("embedded");
1368
+ expect(status.ignoredPack).toBeNull();
1369
+ // The pack's own files are left alone: the cache is content-addressed and
1370
+ // shared, so another project reading the same hash is untouched, and the
1371
+ // next update reuses it instead of rebuilding.
1372
+ expect(existsSync(packDir(hash))).toBe(true);
1373
+ });
1374
+
1375
+ it("a second reset is a calm no-op, not a failure", async () => {
1376
+ const cwd = tmp("pragma-proj-");
1377
+ await builtWithVersion(cwd, OLDER);
1378
+ await executeVerb(resetVerb, {}, REAL, bootRuntime(FLAGS, cwd));
1379
+
1380
+ const again = await executeVerb(
1381
+ resetVerb,
1382
+ {},
1383
+ REAL,
1384
+ bootRuntime(FLAGS, cwd),
1385
+ );
1386
+ expect(again.exitCode).toBe(0);
1387
+ expect(again.stdout).toContain("nothing to reset");
1388
+ });
1389
+
1390
+ it("MCP sources_reset without confirm plans only, and removes nothing", async () => {
1391
+ const cwd = tmp("pragma-proj-");
1392
+ const hash = await builtWithVersion(cwd, OLDER);
1393
+
1394
+ const mcp = await projectMcp([sourcesModule], cwd);
1395
+ const envelope = await mcp.callTool("sources_reset"); // no confirm
1396
+ expect(envelope.ok).toBe(true);
1397
+ expect(envelope.meta).toMatchObject({
1398
+ planOnly: true,
1399
+ confirmRequired: true,
1400
+ });
1401
+ const plan = (envelope.data as { plan: string[] }).plan;
1402
+ expect(plan.some((line) => line.includes(activePackPath(cwd)))).toBe(true);
1403
+ expect(readActivePack(cwd)).toBe(hash);
1404
+
1405
+ // And with `confirm`, the same call performs it.
1406
+ const done = await mcp.callTool("sources_reset", { confirm: true });
1407
+ await mcp.cleanup();
1408
+ expect(done.ok).toBe(true);
1409
+ expect((done.data as { removed: boolean }).removed).toBe(true);
1410
+ expect(readActivePack(cwd)).toBeUndefined();
1411
+ });
1412
+ });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { type RenderStyle, styleFor } from "../../kernel/render/style.js";
3
- import { renderSourcesStatusPlain } from "./status.render.js";
3
+ import { renderSourcesStatusPlain, statusFormatters } from "./status.render.js";
4
4
  import type { SourcesStatusData } from "./types.js";
5
5
 
6
6
  /** A project reading from its own built pack, with two configured sources. */
@@ -11,6 +11,7 @@ const BUILT: SourcesStatusData = {
11
11
  sourceRef: "ds,extra",
12
12
  builtAt: "2026-01-01",
13
13
  entityCount: 42,
14
+ ignoredPack: null,
14
15
  sources: [
15
16
  { name: "ds", ref: "git+https://example.test/ds.git#main" },
16
17
  { name: "extra", ref: "extra@1" },
@@ -84,6 +85,45 @@ describe("renderSourcesStatusPlain", () => {
84
85
  expect(out).not.toContain(" pack: ");
85
86
  });
86
87
 
88
+ it("names the built pack the snapshot is answering in place of", () => {
89
+ // The upgrade state: this project HAS a pack, an older CLI built it, and the
90
+ // snapshot is answering. The headline still reports what answers (the
91
+ // snapshot); the extra line is the pack that is being passed over, and both
92
+ // ways out of it.
93
+ const out = renderSourcesStatusPlain(
94
+ {
95
+ ...BUILT,
96
+ store: "embedded",
97
+ ignoredPack: {
98
+ contentHash: "0e82d35c66687b8a",
99
+ builtBy: "0.37.0",
100
+ builtAt: "2026-09-10T21:49:00.411Z",
101
+ },
102
+ },
103
+ styleFor(false),
104
+ );
105
+ expect(out).toContain("Store: embedded snapshot");
106
+ expect(out).toContain(
107
+ " 0e82d35c6668: a pack built by pragma 0.37.0 on 2026-09-10 is ignored — run `pragma sources update` to rebuild it or `pragma sources reset` to remove it",
108
+ );
109
+ });
110
+
111
+ it("the llm form names the ignored pack and both ways out", () => {
112
+ const out = statusFormatters.llm({
113
+ ...BUILT,
114
+ store: "embedded",
115
+ ignoredPack: {
116
+ contentHash: "0e82d35c66687b8a",
117
+ builtBy: "0.37.0",
118
+ builtAt: "2026-09-10T21:49:00.411Z",
119
+ },
120
+ });
121
+ expect(out).toContain(
122
+ "- Ignored: a pack built by pragma 0.37.0 on 2026-09-10 is ignored",
123
+ );
124
+ expect(out).toContain("`pragma sources reset`");
125
+ });
126
+
87
127
  it("says so when no packs are configured", () => {
88
128
  const out = renderSourcesStatusPlain(
89
129
  { ...BUILT, sources: [] },
@@ -92,3 +132,25 @@ describe("renderSourcesStatusPlain", () => {
92
132
  expect(out).toContain(" (none configured)");
93
133
  });
94
134
  });
135
+
136
+ describe("sources status — the condensed form keeps the next step", () => {
137
+ it("says what to run when the store is not built, and nothing extra when it is", () => {
138
+ const unavailable: SourcesStatusData = {
139
+ ...BUILT,
140
+ store: "unavailable",
141
+ contentHash: null,
142
+ sourceRef: null,
143
+ builtAt: null,
144
+ entityCount: null,
145
+ };
146
+ expect(statusFormatters.llm(unavailable)).toContain(
147
+ "- Store: not built (run `pragma sources update`)",
148
+ );
149
+ expect(
150
+ statusFormatters.llm({ ...unavailable, store: "embedded" }),
151
+ ).toContain(
152
+ "- Store: embedded snapshot (run `pragma sources update` to build from the configured packs)",
153
+ );
154
+ expect(statusFormatters.llm(BUILT)).toContain("- Store: ready");
155
+ });
156
+ });
@@ -2,9 +2,11 @@
2
2
  * Formatters for `pragma sources status` — plain, llm, json.
3
3
  */
4
4
 
5
- import { BIN_NAME } from "../../constants.js";
6
5
  import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
6
+ import { describeIgnoredPack } from "../../kernel/runtime/resolveSources.js";
7
+ import { renderCall } from "../../kernel/spec/call.js";
7
8
  import type { Formatters } from "../../kernel/spec/index.js";
9
+ import { BUILD_STORE_CALL } from "../shared/calls.js";
8
10
  import type { SourcesStatusData } from "./types.js";
9
11
 
10
12
  /**
@@ -12,11 +14,13 @@ import type { SourcesStatusData } from "./types.js";
12
14
  * explicitly: the distribution's snapshot answers reads, but it is a snapshot,
13
15
  * so reporting it as "up to date" would be a lie.
14
16
  */
15
- const STORE_HEADLINE: Record<SourcesStatusData["store"], string> = {
16
- built: "ready",
17
- embedded: `embedded snapshot (run \`${BIN_NAME} sources update\` to build from the configured packs)`,
18
- unavailable: `not built (run \`${BIN_NAME} sources update\`)`,
19
- };
17
+ function storeHeadline(store: SourcesStatusData["store"]): string {
18
+ const update = `run \`${renderCall(BUILD_STORE_CALL, "cli")}\``;
19
+ if (store === "built") return "ready";
20
+ return store === "embedded"
21
+ ? `embedded snapshot (${update} to build from the configured packs)`
22
+ : `not built (${update})`;
23
+ }
20
24
 
21
25
  /**
22
26
  * Render `sources status` as plain text.
@@ -32,13 +36,22 @@ export function renderSourcesStatusPlain(
32
36
  data: SourcesStatusData,
33
37
  style: RenderStyle = defaultStyle(),
34
38
  ): string {
35
- const lines = [`Store: ${STORE_HEADLINE[data.store]}`];
39
+ const lines = [`Store: ${storeHeadline(data.store)}`];
36
40
  if (data.contentHash !== null) {
37
41
  lines.push(
38
42
  ` pack: ${data.contentHash.slice(0, 12)} — ${data.entityCount ?? "?"} entities, built ${data.builtAt ?? "?"}`,
39
43
  ` from: ${data.sourceRef ?? "?"}`,
40
44
  );
41
45
  }
46
+ // The pack this project built and is NOT reading. It says so on its own line
47
+ // rather than in the headline: the headline answers "what is answering", and
48
+ // the answer (the snapshot) is unchanged — this is the extra fact that the
49
+ // directory has a pack and it is being passed over.
50
+ if (data.ignoredPack !== null) {
51
+ lines.push(
52
+ ` ${data.ignoredPack.contentHash.slice(0, 12)}: ${describeIgnoredPack(data.ignoredPack)}`,
53
+ );
54
+ }
42
55
  lines.push("", style.enabled ? style.bold("Sources:") : "Sources:");
43
56
  if (data.sources.length === 0) {
44
57
  lines.push(" (none configured)");
@@ -62,7 +75,11 @@ export const statusFormatters: Formatters<SourcesStatusData> = {
62
75
  },
63
76
 
64
77
  llm(data) {
65
- const lines = [`# sources`, `- Store: ${data.store}`];
78
+ // The next step rides the condensed form too: an agent reading `embedded`
79
+ // or `unavailable` bare was told a state and not what to do about it.
80
+ const lines = [`# sources`, `- Store: ${storeHeadline(data.store)}`];
81
+ if (data.ignoredPack !== null)
82
+ lines.push(`- Ignored: ${describeIgnoredPack(data.ignoredPack)}`);
66
83
  if (data.sourceRef !== null) lines.push(`- From: ${data.sourceRef}`);
67
84
  if (data.entityCount !== null)
68
85
  lines.push(`- Entities: ${data.entityCount}`);
@@ -21,6 +21,8 @@ export const statusVerb: VerbSpec<
21
21
  path: ["sources", "status"],
22
22
  summary: "Report which pack answers reads, and the packs it was built from.",
23
23
  doc: "Storeless — reads config and the pack cache without booting the store, so it works even when the store is cold. Reports whether reads are answered by a locally built pack, by the embedded snapshot, or not at all.",
24
+ useWhen:
25
+ "when results look empty or stale, to see which data pack is answering",
24
26
  params: [],
25
27
  output: { formatters: statusFormatters },
26
28
  examples: [
@@ -3,6 +3,8 @@
3
3
  * (the Task that resolves, builds, and points the project at a pack).
4
4
  */
5
5
 
6
+ import type { IgnoredPack } from "../../kernel/runtime/resolveSources.js";
7
+
6
8
  /** The `sources status` payload — assembled without booting the store. */
7
9
  export interface SourcesStatusData {
8
10
  readonly cwd: string;
@@ -19,6 +21,15 @@ export interface SourcesStatusData {
19
21
  readonly builtAt: string | null;
20
22
  /** Total indexed entity count from the answering pack, or null. */
21
23
  readonly entityCount: number | null;
24
+ /**
25
+ * The project's own built pack when the boot passed over it — built by an
26
+ * older CLI, in a project that declares no packs of its own, so the embedded
27
+ * snapshot answers instead. `store` is `"embedded"` in that case (it reports
28
+ * which pack ANSWERS, and the snapshot does), and this is how status says the
29
+ * pack is still there and why it is not being read. Null whenever there is
30
+ * nothing passed over, which is every other state.
31
+ */
32
+ readonly ignoredPack: IgnoredPack | null;
22
33
  /** The configured pack declarations, as written in the config. */
23
34
  readonly sources: readonly {
24
35
  readonly name: string;
@@ -26,6 +37,21 @@ export interface SourcesStatusData {
26
37
  }[];
27
38
  }
28
39
 
40
+ /** The `sources reset` result payload. */
41
+ export interface SourcesResetData {
42
+ /** Whether a pointer was there to remove (false = nothing was built). */
43
+ readonly removed: boolean;
44
+ /** The content hash the removed pointer named, or null when there was none. */
45
+ readonly contentHash: string | null;
46
+ /**
47
+ * What answers this project's reads now that the pointer is gone — the
48
+ * embedded snapshot, or nothing at all because the project declares its own
49
+ * packs and must build them. Resolved from config in the run body, so the
50
+ * result can say which of the two it left behind instead of guessing.
51
+ */
52
+ readonly answers: "embedded" | "unavailable";
53
+ }
54
+
29
55
  /** One resolved source in the `sources update` result. */
30
56
  export interface UpdatedSource {
31
57
  readonly name: string;
@@ -26,6 +26,9 @@ export const updateVerb: VerbSpec<
26
26
  path: ["sources", "update"],
27
27
  summary: "Resolve configured packs and build the local store from them.",
28
28
  doc: "Resolves each configured pack (git, file, or npm) and builds one local pack from them, which every later run reads without touching the network. Put a commit SHA in a pack source ref to pin it to that revision.",
29
+ useWhen:
30
+ "when asked to fetch or rebuild the design-system data, or when a read reports that the store is unavailable",
31
+ example: { skipInvalid: true },
29
32
  params: [
30
33
  {
31
34
  kind: "boolean",
@@ -378,14 +378,14 @@ describe("surface COMPLETE — emitted == covenant (PROTECTED)", () => {
378
378
  // The CLOSING direction: assertConforms already proves emitted ⊆ covenant;
379
379
  // this proves covenant ⊆ emitted, so together the tool sets are EQUAL — the
380
380
  // surface-complete milestone. After PR7, every covenant tool is realized.
381
- it("emits every covenant tool (all 49) — set equality with the covenant", () => {
381
+ it("emits every covenant tool (all 50) — set equality with the covenant", () => {
382
382
  const emittedTools = new Set(emitted.mcpSurface.tools);
383
383
  const missing = golden.mcpSurface.tools.filter((t) => !emittedTools.has(t));
384
384
  expect(missing).toEqual([]);
385
385
  expect([...emitted.mcpSurface.tools].sort()).toEqual(
386
386
  [...golden.mcpSurface.tools].sort(),
387
387
  );
388
- expect(emitted.mcpSurface.tools).toHaveLength(49);
388
+ expect(emitted.mcpSurface.tools).toHaveLength(50);
389
389
  });
390
390
 
391
391
  // The covenant edit: the non-tool MCP surface is frozen too.
@@ -34,7 +34,7 @@ import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
34
34
  import type { PragmaRuntime } from "../kernel/runtime/types.js";
35
35
  import type { VerbSpec } from "../kernel/spec/types.js";
36
36
  import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
37
- import { declaredStories } from "./distribution.js";
37
+ import { declaredLookups, declaredStories } from "./distribution.js";
38
38
 
39
39
  const tokenPack = declaredStories.get("token");
40
40
  if (!tokenPack) {
@@ -269,6 +269,7 @@ const verbs = () =>
269
269
  tokenPack,
270
270
  distributionSource("pragma.conf.ts"),
271
271
  DEFAULT_PREFIX_MAP,
272
+ declaredLookups,
272
273
  );
273
274
  const verb = (label: string) =>
274
275
  verbs().find((v) => verbKey(v.path) === `token ${label}`) as VerbSpec;
@@ -624,7 +625,7 @@ describe("token consumers — the binding tuple, either spelling (PROTECTED)", (
624
625
  answered as never,
625
626
  );
626
627
  expect(notice).toContain("matches `--variable button-color-background`.");
627
- expect(notice).toContain("Either no component uses this token");
628
+ expect(notice).toContain("stands for no token");
628
629
  });
629
630
 
630
631
  it("--key and --state narrow to one tuple each", async () => {
@@ -44,7 +44,7 @@ import { bootRuntime } from "../kernel/runtime/boot.js";
44
44
  import type { PragmaRuntime } from "../kernel/runtime/types.js";
45
45
  import type { CapabilityModule, VerbSpec } from "../kernel/spec/types.js";
46
46
  import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
47
- import { declaredStories } from "./distribution.js";
47
+ import { declaredLookups, declaredStories } from "./distribution.js";
48
48
 
49
49
  const SOURCE = distributionSource("pragma.conf.ts");
50
50
 
@@ -55,7 +55,7 @@ const modules = new Map<string, CapabilityModule>(
55
55
  NOUNS.map((noun) => {
56
56
  const story = declaredStories.get(noun);
57
57
  if (!story) throw new Error(`pragma.conf.ts declares no "${noun}" story`);
58
- return [noun, compileStoryModule(story, SOURCE, {})];
58
+ return [noun, compileStoryModule(story, SOURCE, {}, declaredLookups)];
59
59
  }),
60
60
  );
61
61
 
@@ -536,6 +536,18 @@ describe("the shipped nouns answer, end to end (PROTECTED)", () => {
536
536
  }
537
537
  });
538
538
 
539
+ it("several mistyped symbols each get a share of the suggestions", async () => {
540
+ const refusal = await rows("token", "values", {
541
+ symbol: ["color.tex", "color.bordr"],
542
+ }).catch((error: unknown) => error);
543
+ expect(refusal).toMatchObject({
544
+ code: "INVALID_INPUT",
545
+ message: 'No token is named "color.tex", "color.bordr".',
546
+ });
547
+ const { suggestions } = refusal as { suggestions: string[] };
548
+ expect(suggestions.slice(0, 2)).toEqual(["color.text", "color.border"]);
549
+ });
550
+
539
551
  it("a dash-stripped variable name resolves, with its declarations", async () => {
540
552
  const variable = await lookup("variable", "color-text");
541
553
  expect(variable).toMatchObject({
@@ -24,6 +24,7 @@ const upgradeVerb: VerbSpec<Record<string, unknown>, UpgradeData> = {
24
24
  path: ["upgrade"],
25
25
  summary: `Upgrade the ${BIN_NAME} CLI to the latest version.`,
26
26
  doc: "Checks the registry for the active channel's latest release and runs your package manager's global-update command. Preview the update before applying it.",
27
+ useWhen: "when asked to update the CLI itself to the latest release",
27
28
  params: [],
28
29
  output: { formatters: upgradeFormatters },
29
30
  examples: [
@@ -34,7 +34,7 @@ import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
34
34
  import type { PragmaRuntime } from "../kernel/runtime/types.js";
35
35
  import type { VerbSpec } from "../kernel/spec/types.js";
36
36
  import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
37
- import { declaredStories } from "./distribution.js";
37
+ import { declaredLookups, declaredStories } from "./distribution.js";
38
38
 
39
39
  const variablePack = declaredStories.get("variable");
40
40
  if (!variablePack) {
@@ -183,6 +183,7 @@ const verbs = () =>
183
183
  variablePack,
184
184
  distributionSource("pragma.conf.ts"),
185
185
  DEFAULT_PREFIX_MAP,
186
+ declaredLookups,
186
187
  );
187
188
  const verb = (label: string) =>
188
189
  verbs().find((v) => verbKey(v.path) === `variable ${label}`) as VerbSpec;
@@ -15,6 +15,7 @@ const versionVerb: VerbSpec<Record<string, unknown>, string> = {
15
15
  path: ["version"],
16
16
  summary: "Print the CLI version.",
17
17
  doc: "Prints the version `--version` prints — one value, two spellings of the same read.",
18
+ useWhen: "when asked only for the installed version number",
18
19
  params: [],
19
20
  output: {
20
21
  formatters: {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The wire contract of list-valued parameters, derived from the registry.
3
+ *
4
+ * Two recorded failures are the reason this file exists. A weak model asked
5
+ * for twenty tokens' variables with twenty `variable_list` calls, because over
6
+ * MCP a filter took ONE value while the CLI took the flag repeated; a stronger
7
+ * one passed `name: "color.text"` to `token_lookup` and was refused by schema
8
+ * validation over a pair of brackets. So: every parameter that takes several
9
+ * values ADVERTISES an array, and accepts one bare value as a list of one.
10
+ *
11
+ * Derived rather than listed — a story declared tomorrow is held to the rule
12
+ * by the act of declaring it.
13
+ */
14
+
15
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
16
+ import { z } from "zod";
17
+ import { buildToolShape } from "../kernel/project/mcp/registerVerb.js";
18
+ import { toolName } from "../kernel/spec/index.js";
19
+ import type { ParamSpec, VerbSpec } from "../kernel/spec/types.js";
20
+ import { type McpHarness, projectMcp } from "../testing/helpers/projectMcp.js";
21
+ import { capabilities } from "./index.js";
22
+
23
+ const exposed: readonly VerbSpec[] = capabilities
24
+ .flatMap((module) => module.verbs)
25
+ .filter((verb) => !verb.hidden && verb.capability.mcp.expose !== false);
26
+
27
+ /** Every `[tool, param]` pair whose param satisfies `test`. */
28
+ function paramsWhere(
29
+ test: (param: ParamSpec) => boolean,
30
+ ): (readonly [string, string, VerbSpec, ParamSpec])[] {
31
+ return exposed.flatMap((verb) =>
32
+ verb.params
33
+ .filter(test)
34
+ .map((param) => [toolName(verb.path), param.name, verb, param] as const),
35
+ );
36
+ }
37
+
38
+ /** One admissible value for a param: its first enum value, else any word. */
39
+ const sample = (param: ParamSpec): string =>
40
+ param.kind === "enum" ? (param.values.at(0) ?? "") : "anything";
41
+
42
+ /** Parse ONE field of a tool's input shape, leaving its other params aside. */
43
+ function parseField(verb: VerbSpec, param: ParamSpec, value: unknown) {
44
+ const field = buildToolShape(verb)[param.name];
45
+ if (!field) throw new Error(`no "${param.name}" in the tool shape`);
46
+ return z.object({ [param.name]: field }).safeParse({ [param.name]: value });
47
+ }
48
+
49
+ const variadic = paramsWhere((param) => param.kind === "string[]");
50
+ const repeatable = paramsWhere(
51
+ (param) => param.kind !== "string[]" && param.repeatable === true,
52
+ );
53
+
54
+ describe("a parameter that takes several values takes one, too", () => {
55
+ it("the registry has both kinds (the derivation is not vacuous)", () => {
56
+ expect(variadic.length).toBeGreaterThan(0);
57
+ expect(repeatable.length).toBeGreaterThan(0);
58
+ });
59
+
60
+ it.each(variadic)(
61
+ "%s { %s }: a bare string is a list of one",
62
+ (_tool, name, verb, param) => {
63
+ expect(parseField(verb, param, "color.text")).toEqual({
64
+ success: true,
65
+ data: { [name]: ["color.text"] },
66
+ });
67
+ expect(parseField(verb, param, ["a", "b"]).success).toBe(true);
68
+ },
69
+ );
70
+
71
+ it.each(repeatable)(
72
+ "%s { %s }: an array and a bare string are both accepted",
73
+ (_tool, name, verb, param) => {
74
+ const value = sample(param);
75
+ expect(parseField(verb, param, [value, value]).success).toBe(true);
76
+ expect(parseField(verb, param, value)).toEqual({
77
+ success: true,
78
+ data: { [name]: [value] },
79
+ });
80
+ },
81
+ );
82
+ });
83
+
84
+ describe("what is advertised is a plain array", () => {
85
+ let harness: McpHarness;
86
+ let schemas: Map<string, Record<string, Record<string, unknown>>>;
87
+ beforeAll(async () => {
88
+ harness = await projectMcp(capabilities);
89
+ schemas = new Map(
90
+ (await harness.listTools()).map((tool) => [
91
+ tool.name,
92
+ ((tool.inputSchema as { properties?: unknown }).properties ??
93
+ {}) as Record<string, Record<string, unknown>>,
94
+ ]),
95
+ );
96
+ });
97
+ afterAll(() => harness.cleanup());
98
+
99
+ // `anyOf` is what a `string | string[]` union would emit, and it is the
100
+ // shape the weaker models read badly — so the coercion must not leak into
101
+ // the advertised schema.
102
+ it.each([...variadic, ...repeatable])(
103
+ "%s { %s } is `type: array`, never `anyOf`",
104
+ (tool, name) => {
105
+ const advertised = schemas.get(tool)?.[name];
106
+ expect(advertised?.type).toBe("array");
107
+ expect(advertised).not.toHaveProperty("anyOf");
108
+ },
109
+ );
110
+ });
package/src/constants.ts CHANGED
@@ -61,9 +61,10 @@ const PROJECT_CONFIG_FILENAME = `${identity.name}.config.ts`;
61
61
  /**
62
62
  * The distribution's name plus a space: the prefix a `recovery.cli` hint carries
63
63
  * so it quotes a command the installed binary answers to (D5). Kernel hints are
64
- * authored through `cliRecovery`, which prepends it; `packs/schema.ts` REJECTS
65
- * it in a user-authored pack's `emptyRecovery.cli`, because the CONSUMING
66
- * distribution's renderer supplies its own. Use {@link BIN_NAME} for prose —
64
+ * authored through `callRecovery`, whose call renderer prepends it; a pack's
65
+ * `emptyRecovery.call` names a verb path and never a binary (`packs/schema.ts`
66
+ * rejects a command line there), because the CONSUMING distribution's renderer
67
+ * supplies its own. Use {@link BIN_NAME} for prose —
67
68
  * this constant's trailing space belongs to the recovery invariant, not to
68
69
  * sentences.
69
70
  */