@canonical/pragma-cli 0.36.0 → 0.38.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 (312) hide show
  1. package/README.md +3 -2
  2. package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
  3. package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
  4. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
  5. package/bundled-skills/anatomy-author/SKILL.md +840 -137
  6. package/bundled-skills/design-auditor/SKILL.md +133 -9
  7. package/bundled-skills/implement-component/SKILL.md +192 -23
  8. package/bundled-skills/specify-component/SKILL.md +124 -12
  9. package/bundled-skills/specify-pattern/SKILL.md +47 -8
  10. package/dist/package.json +23 -20
  11. package/dist/pragma.conf.js +963 -40
  12. package/dist/pragma.conf.js.map +1 -1
  13. package/dist/src/capabilities/capabilities/catalog.js +14 -5
  14. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  15. package/dist/src/capabilities/capabilities/hints.js +33 -5
  16. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  17. package/dist/src/capabilities/config/show.render.js +30 -2
  18. package/dist/src/capabilities/config/show.render.js.map +1 -1
  19. package/dist/src/capabilities/create/create.verb.js +2 -1
  20. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  21. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  22. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  24. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  28. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
  30. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  31. package/dist/src/capabilities/graph/connect.render.js +101 -0
  32. package/dist/src/capabilities/graph/connect.render.js.map +1 -0
  33. package/dist/src/capabilities/graph/connect.types.js +21 -0
  34. package/dist/src/capabilities/graph/connect.types.js.map +1 -0
  35. package/dist/src/capabilities/graph/connect.verb.js +147 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
  37. package/dist/src/capabilities/graph/index.js +5 -3
  38. package/dist/src/capabilities/graph/index.js.map +1 -1
  39. package/dist/src/capabilities/graph/queryText.js +206 -0
  40. package/dist/src/capabilities/graph/queryText.js.map +1 -0
  41. package/dist/src/capabilities/graph/relationIndex.js +302 -0
  42. package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
  43. package/dist/src/capabilities/graph/runConnect.js +456 -0
  44. package/dist/src/capabilities/graph/runConnect.js.map +1 -0
  45. package/dist/src/capabilities/graph/runQuery.js +21 -4
  46. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  47. package/dist/src/capabilities/setup/buildPlan.js +6 -2
  48. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/index.js +7 -2
  50. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  52. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  54. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
  56. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  57. package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
  58. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
  60. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/writability.js +114 -0
  64. package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
  65. package/dist/src/capabilities/setup/plan.js.map +1 -1
  66. package/dist/src/capabilities/setup/setup.verb.js +80 -31
  67. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  68. package/dist/src/capabilities/setup/targets.js +124 -42
  69. package/dist/src/capabilities/setup/targets.js.map +1 -1
  70. package/dist/src/capabilities/shared/index.js +1 -1
  71. package/dist/src/capabilities/shared/index.js.map +1 -1
  72. package/dist/src/constants.js +7 -1
  73. package/dist/src/constants.js.map +1 -1
  74. package/dist/src/kernel/error/renderError.js +3 -2
  75. package/dist/src/kernel/error/renderError.js.map +1 -1
  76. package/dist/src/kernel/error/validOptions.js +41 -0
  77. package/dist/src/kernel/error/validOptions.js.map +1 -0
  78. package/dist/src/kernel/packs/compile.js +90 -6
  79. package/dist/src/kernel/packs/compile.js.map +1 -1
  80. package/dist/src/kernel/packs/cursor.js +135 -0
  81. package/dist/src/kernel/packs/cursor.js.map +1 -0
  82. package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
  83. package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
  84. package/dist/src/kernel/packs/paging.js +108 -0
  85. package/dist/src/kernel/packs/paging.js.map +1 -0
  86. package/dist/src/kernel/packs/renderPack.js +154 -5
  87. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  88. package/dist/src/kernel/packs/resolveEntity.js +118 -19
  89. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  90. package/dist/src/kernel/packs/runBodies.js +156 -18
  91. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  92. package/dist/src/kernel/packs/schema.js +160 -12
  93. package/dist/src/kernel/packs/schema.js.map +1 -1
  94. package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
  95. package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
  96. package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
  97. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
  98. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  100. package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
  101. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
  102. package/dist/src/kernel/packs/sparql/index.js +24 -14
  103. package/dist/src/kernel/packs/sparql/index.js.map +1 -1
  104. package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
  105. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  106. package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
  107. package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
  108. package/dist/src/kernel/packs/storyRules.js +151 -0
  109. package/dist/src/kernel/packs/storyRules.js.map +1 -0
  110. package/dist/src/kernel/packs/tierScope.js +310 -0
  111. package/dist/src/kernel/packs/tierScope.js.map +1 -0
  112. package/dist/src/kernel/packs/types.js +80 -0
  113. package/dist/src/kernel/packs/types.js.map +1 -1
  114. package/dist/src/kernel/project/cli/buildProgram.js +10 -1
  115. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  116. package/dist/src/kernel/project/cli/dispatch.js +104 -3
  117. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  118. package/dist/src/kernel/project/cli/rootHelp.js +8 -5
  119. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  120. package/dist/src/kernel/project/cli/suggestNames.js +31 -7
  121. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  122. package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
  123. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  124. package/dist/src/kernel/render/renderers.js +34 -2
  125. package/dist/src/kernel/render/renderers.js.map +1 -1
  126. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  127. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  128. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  129. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  130. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  131. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  132. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  133. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  134. package/dist/src/kernel/runtime/graphpack/read.js +17 -10
  135. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  136. package/dist/src/kernel/runtime/loadSession.js +3 -1
  137. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  138. package/dist/src/kernel/runtime/probe.js +1 -1
  139. package/dist/src/kernel/runtime/probe.js.map +1 -1
  140. package/dist/src/kernel/runtime/readEntity.js +19 -4
  141. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  142. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  143. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  144. package/dist/src/kernel/runtime/refs/resolve.js +38 -5
  145. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  146. package/dist/src/kernel/spec/emitReference.js +1 -1
  147. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  148. package/package.json +23 -20
  149. package/pragma.conf.ts +983 -40
  150. package/src/capabilities/block.parity.test.ts +207 -27
  151. package/src/capabilities/block.shipped.exec.test.ts +156 -14
  152. package/src/capabilities/block.tierRank.exec.test.ts +46 -9
  153. package/src/capabilities/capabilities/catalog.ts +14 -5
  154. package/src/capabilities/capabilities/hints.ts +40 -5
  155. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  156. package/src/capabilities/config/set.test.ts +24 -25
  157. package/src/capabilities/config/show.render.test.ts +9 -4
  158. package/src/capabilities/config/show.render.ts +30 -2
  159. package/src/capabilities/create/byteEquality.test.ts +28 -1
  160. package/src/capabilities/create/create.test.ts +1 -0
  161. package/src/capabilities/create/create.verb.ts +2 -1
  162. package/src/capabilities/create/createGrammar.test.ts +4 -1
  163. package/src/capabilities/create/createSurface.generated.ts +23 -1
  164. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  165. package/src/capabilities/distribution.parity.test.ts +12 -11
  166. package/src/capabilities/distribution.test.ts +77 -7
  167. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  168. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  169. package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
  170. package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
  171. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  172. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  173. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  174. package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
  175. package/src/capabilities/doctor/doctor.test.ts +327 -36
  176. package/src/capabilities/emptyRecovery.test.ts +84 -28
  177. package/src/capabilities/graph/connect.render.ts +141 -0
  178. package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
  179. package/src/capabilities/graph/connect.test.ts +361 -0
  180. package/src/capabilities/graph/connect.types.ts +136 -0
  181. package/src/capabilities/graph/connect.verb.ts +165 -0
  182. package/src/capabilities/graph/index.ts +5 -3
  183. package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
  184. package/src/capabilities/graph/query.test.ts +17 -0
  185. package/src/capabilities/graph/queryText.test.ts +121 -0
  186. package/src/capabilities/graph/queryText.ts +240 -0
  187. package/src/capabilities/graph/relationIndex.ts +372 -0
  188. package/src/capabilities/graph/runConnect.ts +577 -0
  189. package/src/capabilities/graph/runQuery.ts +21 -6
  190. package/src/capabilities/lazy.test.ts +32 -2
  191. package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
  192. package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
  193. package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
  194. package/src/capabilities/resources/resources.test.ts +38 -2
  195. package/src/capabilities/setup/buildPlan.ts +7 -1
  196. package/src/capabilities/setup/operations/index.ts +14 -0
  197. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  198. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  199. package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
  200. package/src/capabilities/setup/operations/setupLsp.ts +544 -85
  201. package/src/capabilities/setup/operations/setupMcp.ts +212 -4
  202. package/src/capabilities/setup/operations/setupSkills.ts +7 -2
  203. package/src/capabilities/setup/operations/writability.test.ts +151 -0
  204. package/src/capabilities/setup/operations/writability.ts +135 -0
  205. package/src/capabilities/setup/plan.ts +11 -0
  206. package/src/capabilities/setup/setup.test.ts +992 -16
  207. package/src/capabilities/setup/setup.verb.ts +84 -36
  208. package/src/capabilities/setup/targets.ts +152 -48
  209. package/src/capabilities/setup/types.ts +41 -5
  210. package/src/capabilities/shared/index.ts +1 -0
  211. package/src/capabilities/sources/sources.test.ts +55 -0
  212. package/src/capabilities/surface.test.ts +59 -11
  213. package/src/capabilities/tier.parity.test.ts +2 -2
  214. package/src/capabilities/token.parity.test.ts +784 -0
  215. package/src/capabilities/tokenGraph.cli.test.ts +279 -0
  216. package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
  217. package/src/capabilities/variable.parity.test.ts +448 -0
  218. package/src/constants.ts +8 -0
  219. package/src/kernel/completion/shellDrive.test.ts +32 -30
  220. package/src/kernel/config/defaults.test.ts +21 -2
  221. package/src/kernel/config/readConfig.test.ts +1 -1
  222. package/src/kernel/error/renderError.ts +3 -2
  223. package/src/kernel/error/validOptions.test.ts +57 -0
  224. package/src/kernel/error/validOptions.ts +45 -0
  225. package/src/kernel/packs/collect.test.ts +2 -0
  226. package/src/kernel/packs/compile.test.ts +188 -15
  227. package/src/kernel/packs/compile.ts +109 -14
  228. package/src/kernel/packs/cursor.ts +135 -0
  229. package/src/kernel/packs/filteredEmpty.test.ts +213 -0
  230. package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
  231. package/src/kernel/packs/graphqlSource.test.ts +152 -0
  232. package/src/kernel/packs/listNotice.test.ts +156 -0
  233. package/src/kernel/packs/paging.ts +110 -0
  234. package/src/kernel/packs/renderPack.ts +186 -13
  235. package/src/kernel/packs/resolveEntity.test.ts +62 -3
  236. package/src/kernel/packs/resolveEntity.ts +186 -26
  237. package/src/kernel/packs/runBodies.ts +196 -26
  238. package/src/kernel/packs/schema.ts +180 -13
  239. package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
  240. package/src/kernel/packs/sparql/authorQuery.ts +257 -0
  241. package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
  242. package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
  243. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
  244. package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
  245. package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
  246. package/src/kernel/packs/sparql/filterValues.ts +182 -0
  247. package/src/kernel/packs/sparql/index.ts +30 -14
  248. package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
  249. package/src/kernel/packs/sparql/runSelect.ts +42 -2
  250. package/src/kernel/packs/sparql/searchTerm.ts +29 -0
  251. package/src/kernel/packs/storyRules.ts +180 -0
  252. package/src/kernel/packs/tierScope.test.ts +487 -0
  253. package/src/kernel/packs/tierScope.ts +409 -0
  254. package/src/kernel/packs/toolDescription.test.ts +13 -10
  255. package/src/kernel/packs/types.ts +311 -9
  256. package/src/kernel/project/cli/buildProgram.ts +17 -1
  257. package/src/kernel/project/cli/dispatch.test.ts +87 -0
  258. package/src/kernel/project/cli/dispatch.ts +114 -4
  259. package/src/kernel/project/cli/rootHelp.ts +8 -5
  260. package/src/kernel/project/cli/suggest.test.ts +35 -0
  261. package/src/kernel/project/cli/suggestNames.ts +34 -7
  262. package/src/kernel/project/mcp/registerVerb.ts +5 -1
  263. package/src/kernel/render/contracts.ts +2 -0
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.test.ts +43 -0
  266. package/src/kernel/render/renderers.ts +34 -1
  267. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  268. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  269. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  270. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  271. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  272. package/src/kernel/runtime/graphpack/read.ts +17 -13
  273. package/src/kernel/runtime/loadSession.ts +3 -1
  274. package/src/kernel/runtime/probe.ts +1 -1
  275. package/src/kernel/runtime/readEntity.ts +19 -4
  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 +39 -5
  279. package/src/kernel/runtime/types.ts +30 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  282. package/src/kernel/spec/types.ts +17 -0
  283. package/src/testing/PARITY_GAPS.ts +4 -4
  284. package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
  285. package/src/testing/behavioral/completion.test.ts +7 -6
  286. package/src/testing/behavioral/disclosure.test.ts +39 -37
  287. package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
  288. package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
  289. package/src/testing/behavioral/journeys.cli.test.ts +71 -32
  290. package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
  291. package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
  292. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
  293. package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
  294. package/src/testing/behavioral/mcpSurface.test.ts +28 -25
  295. package/src/testing/behavioral/parity.test.ts +25 -23
  296. package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
  297. package/src/testing/eval/cases/effectful.ts +7 -1
  298. package/src/testing/eval/cases/readNouns.ts +100 -11
  299. package/src/testing/fixtures/blockGraph.ts +89 -5
  300. package/src/testing/fixtures/connectGraph.ts +159 -0
  301. package/src/testing/fixtures/graph/canonical.ts +128 -32
  302. package/src/testing/helpers/fsProbe.ts +88 -0
  303. package/src/testing/helpers/runCli.test.ts +44 -0
  304. package/src/testing/helpers/runCli.ts +27 -3
  305. package/src/testing/perf/budgets.test.ts +3 -1
  306. package/src/testing/perf/budgets.ts +138 -54
  307. package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
  308. package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
  309. package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
  310. package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
  311. package/src/kernel/packs/sparql/applyFilters.ts +0 -203
  312. package/src/kernel/packs/sparql/applySearch.ts +0 -33
@@ -0,0 +1,409 @@
1
+ /**
2
+ * The tier scope: which tiers a read of a tiered noun answers from.
3
+ *
4
+ * WHAT IT IS FOR. The design system's tier hierarchy has two levels — five
5
+ * top-level tiers (`global`, `apps`, `sites`, `documentation`, `stores`) and
6
+ * ten second-level ones beneath `apps` and `sites` — and every read used to
7
+ * answer from all fifteen at once. `block list` printed 252 blocks, a third of
8
+ * them belonging to one product's own screens; `block lookup button` printed
9
+ * the global Button AND Launchpad's, concatenated, with nothing saying why
10
+ * there were two. The `tier` config key that was supposed to narrow this was
11
+ * read by nothing. So a read of a tiered noun now has a SCOPE, and the scope is
12
+ * part of the query rather than a predicate over what the query returned.
13
+ *
14
+ * THE RULE, in one place:
15
+ *
16
+ * - DEFAULT — the top-level tiers. A tier is top-level when its path-shaped
17
+ * name has no `/` (see {@link PackTierScope.by}): nothing sits above it.
18
+ * - A CHOSEN tier (`--tier apps_lxd`, or `tier: "apps_lxd"` in config) — that
19
+ * tier, its ancestors, and the declared base tier. `apps_lxd` therefore
20
+ * answers from `global`, `apps` and `apps_lxd`: the blocks an LXD screen is
21
+ * actually built from, which is the chain the design system's own colophon
22
+ * describes ("a lower tier inherits and overrides the blocks of its
23
+ * ancestors").
24
+ * - `all` — no scope at all, the pre-scope behaviour, reachable per call
25
+ * (`--tier all`) or as a setting (`tier: "all"`).
26
+ *
27
+ * PRECEDENCE is the one every other per-call argument has: the flag beats the
28
+ * config, the config beats the default. A per-call `--tier` is not a second
29
+ * narrowing on top of a configured one — it REPLACES it, because a chain is not
30
+ * an intersection of chains and an agent that passes `--tier apps_lxd` on a
31
+ * machine configured to `apps_launchpad` means the first.
32
+ *
33
+ * THE HIERARCHY IS READ FROM THE STORE, ONCE. Nothing here enumerates a tier:
34
+ * the tiers, their names and therefore their parents are read from the pack the
35
+ * session booted, so a tier added upstream tomorrow is scoped correctly with no
36
+ * edit here. The read is memoized per store SESSION (not per process): a
37
+ * `sources update` on the long-lived MCP server invalidates the session, and the
38
+ * next read re-learns the hierarchy from the pack that replaced it.
39
+ */
40
+
41
+ import { cliRecovery, PragmaError } from "../error/index.js";
42
+ import type { PragmaRuntime } from "../runtime/index.js";
43
+ import type { StoreSession } from "../runtime/types.js";
44
+ import { formatTerm } from "./sparql/escape.js";
45
+ import { runSelect } from "./sparql/runSelect.js";
46
+ import { EVERY_TIER, type PackTierScope, type StorySource } from "./types.js";
47
+
48
+ /** One tier, with the hierarchy its own name states. */
49
+ export interface TierNode {
50
+ /** The tier's IRI, as the query filter binds it. */
51
+ readonly iri: string;
52
+ /** The tier's path-shaped name (`"Apps/LXD"`). */
53
+ readonly name: string;
54
+ /**
55
+ * The IRI's local name (`"apps_lxd"`) — the spelling `block list`'s `Tier`
56
+ * column prints and `--tier` accepts, so the scope is reported in the words a
57
+ * reader can type straight back.
58
+ */
59
+ readonly local: string;
60
+ /** How deep the name sits: `"Global"` is 1, `"Apps/LXD"` is 2. */
61
+ readonly depth: number;
62
+ }
63
+
64
+ /** The tier hierarchy one pack declares, as the scope reads it. */
65
+ export interface TierHierarchy {
66
+ /** Every tier the store knows, in the order the read returned them. */
67
+ readonly tiers: readonly TierNode[];
68
+ /** The declared base tier, when the pack declares one and the store has it. */
69
+ readonly base?: TierNode;
70
+ /** Every spelling a caller may address a tier by → that tier. */
71
+ readonly bySpelling: ReadonlyMap<string, TierNode>;
72
+ }
73
+
74
+ /** The tiers a read answers from, or every tier. */
75
+ export type TierScope =
76
+ | { readonly kind: "all" }
77
+ | {
78
+ /** The scope is these tiers, in reporting order (base first, then shallowest). */
79
+ readonly kind: "tiers";
80
+ readonly tiers: readonly TierNode[];
81
+ /** The tier a caller (or their config) asked for, when one was asked for. */
82
+ readonly chosen?: TierNode;
83
+ };
84
+
85
+ /**
86
+ * Every tier read once per store session, per declaration.
87
+ *
88
+ * Keyed on the SESSION so the memo cannot outlive the pack it describes, and
89
+ * nested by declaration so the three nouns that declare the same hierarchy
90
+ * (`block`, `modifier`, `concept`) share ONE query rather than issuing three
91
+ * identical ones per invocation. A `WeakMap` because the session is the owner:
92
+ * when the runtime drops it, this drops with it.
93
+ */
94
+ const SESSIONS = new WeakMap<
95
+ StoreSession,
96
+ Map<string, Promise<TierHierarchy>>
97
+ >();
98
+
99
+ /** The SELECT reading every tier and its path-shaped name. */
100
+ function hierarchyQuery(declaration: PackTierScope): string {
101
+ return [
102
+ "SELECT ?tier ?name WHERE {",
103
+ ` ?tier a ${formatTerm(declaration.type)} .`,
104
+ ` ?tier ${formatTerm(declaration.by)} ?name .`,
105
+ "}",
106
+ "ORDER BY ?name",
107
+ ].join("\n");
108
+ }
109
+
110
+ /**
111
+ * Read (once per session) the tier hierarchy a declaration describes.
112
+ *
113
+ * @param rt - The runtime, for the store and the query facade.
114
+ * @param declaration - The noun's declared tier hierarchy.
115
+ * @param source - The story's provenance, for a query diagnosis.
116
+ * @returns The hierarchy, memoized for this store session.
117
+ */
118
+ export async function readTierHierarchy(
119
+ rt: PragmaRuntime,
120
+ declaration: PackTierScope,
121
+ source: StorySource,
122
+ ): Promise<TierHierarchy> {
123
+ const session = await rt.store.get();
124
+ const key = [
125
+ declaration.type,
126
+ declaration.via,
127
+ declaration.by,
128
+ declaration.base ?? "",
129
+ ].join("|");
130
+ let perDeclaration = SESSIONS.get(session);
131
+ if (!perDeclaration) {
132
+ perDeclaration = new Map();
133
+ SESSIONS.set(session, perDeclaration);
134
+ }
135
+ const memoized = perDeclaration.get(key);
136
+ if (memoized) return memoized;
137
+ const reading = runSelect(rt, hierarchyQuery(declaration), source).then(
138
+ (rows) =>
139
+ buildTierHierarchy(
140
+ rows.map((row) => ({ iri: row.tier ?? "", name: row.name ?? "" })),
141
+ declaration,
142
+ ),
143
+ );
144
+ perDeclaration.set(key, reading);
145
+ return reading;
146
+ }
147
+
148
+ /**
149
+ * The local name of a term: everything after its last `#`, `/` or `:`.
150
+ *
151
+ * The `:` is what makes one function serve both spellings a term arrives in. A
152
+ * tier read out of the store is an absolute IRI
153
+ * (`https://ds.canonical.com/global`), and the declared `base` beside it is a
154
+ * PREFIXED name (`ds:global`) — the grammar admits either, everywhere. Reading
155
+ * only `#` and `/` left `ds:global` as its own local name, so the base tier
156
+ * matched nothing and the base-first reporting order silently became
157
+ * alphabetical.
158
+ */
159
+ export function localName(term: string): string {
160
+ return term.replace(/^.*[#/:]/, "");
161
+ }
162
+
163
+ /**
164
+ * Every spelling of a tier a caller may type.
165
+ *
166
+ * Three, because three are in circulation and all of them are the reader's own
167
+ * words: the local name the `Tier` column prints (`apps_lxd`), the display name
168
+ * `tier list` prints (`Apps/LXD`), and that display name with its separator
169
+ * written the way the IRI writes it (`apps/lxd` — the spelling the config key's
170
+ * own documentation used before anything read it). Refusing two of the three
171
+ * would be refusing a value this distribution itself published.
172
+ */
173
+ function spellings(tier: TierNode): string[] {
174
+ return [
175
+ tier.local.toLowerCase(),
176
+ tier.name.toLowerCase(),
177
+ tier.name.toLowerCase().replaceAll("/", "_"),
178
+ ];
179
+ }
180
+
181
+ /**
182
+ * Build the hierarchy from the rows the store returned.
183
+ *
184
+ * Exported for the unit tests, which state the hierarchy rules over a handful
185
+ * of rows rather than over the shipped pack's fifteen tiers.
186
+ *
187
+ * @param rows - One `{ iri, name }` per tier.
188
+ * @param declaration - The noun's declared tier hierarchy.
189
+ * @returns The hierarchy, with tiers carrying an empty IRI or name dropped —
190
+ * a tier the scope cannot address or place is not a tier it can scope by.
191
+ */
192
+ export function buildTierHierarchy(
193
+ rows: readonly { readonly iri: string; readonly name: string }[],
194
+ declaration: PackTierScope,
195
+ ): TierHierarchy {
196
+ const tiers: TierNode[] = [];
197
+ for (const row of rows) {
198
+ if (row.iri === "" || row.name === "") continue;
199
+ tiers.push({
200
+ iri: row.iri,
201
+ name: row.name,
202
+ local: localName(row.iri),
203
+ depth: row.name.split("/").length,
204
+ });
205
+ }
206
+ const bySpelling = new Map<string, TierNode>();
207
+ for (const tier of tiers) {
208
+ for (const spelling of spellings(tier)) {
209
+ if (!bySpelling.has(spelling)) bySpelling.set(spelling, tier);
210
+ }
211
+ }
212
+ const declaredBase = declaration.base;
213
+ const base =
214
+ declaredBase === undefined
215
+ ? undefined
216
+ : tiers.find(
217
+ (tier) =>
218
+ tier.iri === declaredBase ||
219
+ tier.local.toLowerCase() === localName(declaredBase).toLowerCase(),
220
+ );
221
+ return { tiers, bySpelling, ...(base ? { base } : {}) };
222
+ }
223
+
224
+ /**
225
+ * The reporting order: the base tier first, then shallowest first, then
226
+ * alphabetical.
227
+ *
228
+ * The base leads because it is what the rest inherit from, and the chain then
229
+ * reads root-to-leaf (`global, apps, apps_lxd`) — the direction the hierarchy
230
+ * is written in everywhere else in this distribution.
231
+ */
232
+ function orderTiers(
233
+ tiers: readonly TierNode[],
234
+ base: TierNode | undefined,
235
+ ): TierNode[] {
236
+ return [...tiers].sort((left, right) => {
237
+ if (left.iri === right.iri) return 0;
238
+ if (base) {
239
+ if (left.iri === base.iri) return -1;
240
+ if (right.iri === base.iri) return 1;
241
+ }
242
+ return (
243
+ left.depth - right.depth ||
244
+ left.local.localeCompare(right.local) ||
245
+ left.iri.localeCompare(right.iri)
246
+ );
247
+ });
248
+ }
249
+
250
+ /** The tiers whose name states no parent — the default scope. */
251
+ function topLevel(hierarchy: TierHierarchy): TierNode[] {
252
+ return hierarchy.tiers.filter((tier) => tier.depth === 1);
253
+ }
254
+
255
+ /**
256
+ * The chosen tier's chain: itself, every tier its name sits under, and the base.
257
+ *
258
+ * An ancestor is found by NAME, not by an edge, because the graph asserts no
259
+ * edge (see {@link PackTierScope}). A name segment naming no declared tier
260
+ * contributes nothing — an orphan `Apps/LXD` in a pack with no `Apps` is
261
+ * scoped to itself and the base, which is the honest reading of the only
262
+ * hierarchy it states.
263
+ */
264
+ function chain(hierarchy: TierHierarchy, chosen: TierNode): TierNode[] {
265
+ const segments = chosen.name.split("/");
266
+ const ancestorNames = new Set(
267
+ segments.slice(0, -1).map((_, index) =>
268
+ segments
269
+ .slice(0, index + 1)
270
+ .join("/")
271
+ .toLowerCase(),
272
+ ),
273
+ );
274
+ const selected = hierarchy.tiers.filter(
275
+ (tier) =>
276
+ tier.iri === chosen.iri ||
277
+ ancestorNames.has(tier.name.toLowerCase()) ||
278
+ (hierarchy.base !== undefined && tier.iri === hierarchy.base.iri),
279
+ );
280
+ return orderTiers(selected, hierarchy.base);
281
+ }
282
+
283
+ /** What a caller and their config asked of the scope for one read. */
284
+ export interface TierRequest {
285
+ /** The per-call `--tier` argument (`tier` over MCP), when one was passed. */
286
+ readonly requested?: unknown;
287
+ /** The `tier` config key, when it is set. */
288
+ readonly configured?: string;
289
+ }
290
+
291
+ /**
292
+ * Resolve the scope one read answers under.
293
+ *
294
+ * @param hierarchy - The tiers the store knows.
295
+ * @param request - The per-call argument and the configured default.
296
+ * @returns Every tier (when asked for, or when the store knows no tiers), else
297
+ * the chosen tier's chain, else the top-level tiers.
298
+ * @throws PragmaError INVALID_INPUT when a tier was asked for by a name no tier
299
+ * answers to, carrying the names that do.
300
+ */
301
+ export function resolveTierScope(
302
+ hierarchy: TierHierarchy,
303
+ request: TierRequest,
304
+ ): TierScope {
305
+ // A graph with no tiers in it is not a narrowed graph: there is nothing to
306
+ // scope by, so the read answers as it always did and says nothing about a
307
+ // scope it never applied.
308
+ if (hierarchy.tiers.length === 0) return { kind: "all" };
309
+ const perCall = readRequested(request.requested);
310
+ const asked = perCall ?? request.configured?.trim();
311
+ if (asked === undefined || asked === "") {
312
+ return {
313
+ kind: "tiers",
314
+ tiers: orderTiers(topLevel(hierarchy), hierarchy.base),
315
+ };
316
+ }
317
+ if (asked.toLowerCase() === EVERY_TIER) return { kind: "all" };
318
+ const chosen = hierarchy.bySpelling.get(asked.toLowerCase());
319
+ if (!chosen) {
320
+ const validOptions = [
321
+ EVERY_TIER,
322
+ ...hierarchy.tiers.map((tier) => tier.local).sort(),
323
+ ];
324
+ // The recovery names WHERE the bad value came from, because the two places
325
+ // are fixed differently and only one of them is in the caller's hand: a
326
+ // per-call `--tier` is retyped, a configured one is unset. A reader whose
327
+ // machine is set to a tier this pack does not carry would otherwise be told
328
+ // to retype an argument they never typed.
329
+ throw PragmaError.invalidInput("tier", asked, {
330
+ validOptions,
331
+ recovery:
332
+ perCall === undefined
333
+ ? cliRecovery(
334
+ "config unset tier",
335
+ `The \`tier\` config key names "${asked}", which is not one of this pack's ${hierarchy.tiers.length} tiers. Clear it, or set one that exists.`,
336
+ { tool: "config_unset", params: { key: "tier" } },
337
+ )
338
+ : cliRecovery(
339
+ "tier list",
340
+ `Name one of the ${hierarchy.tiers.length} tiers, or "${EVERY_TIER}" for every tier.`,
341
+ { tool: "tier_list", params: {} },
342
+ ),
343
+ });
344
+ }
345
+ return { kind: "tiers", tiers: chain(hierarchy, chosen), chosen };
346
+ }
347
+
348
+ /**
349
+ * The per-call argument as a tier name.
350
+ *
351
+ * @throws PragmaError INVALID_INPUT when it is not a string — a repeated
352
+ * `--tier` is a caller asking for two chains at once, which is not a chain.
353
+ */
354
+ function readRequested(provided: unknown): string | undefined {
355
+ if (provided === undefined) return undefined;
356
+ if (typeof provided === "string") {
357
+ const trimmed = provided.trim();
358
+ return trimmed === "" ? undefined : trimmed;
359
+ }
360
+ throw PragmaError.invalidInput("tier", String(provided), {
361
+ recovery: {
362
+ message: `Pass one tier name, or "${EVERY_TIER}" for every tier.`,
363
+ },
364
+ });
365
+ }
366
+
367
+ /**
368
+ * The scope one read of a tiered noun answers under, resolved from everything
369
+ * that has a say: the story's declaration, the caller's argument, the config.
370
+ *
371
+ * @param rt - The runtime (store for the hierarchy, config for the default).
372
+ * @param declaration - The noun's declared hierarchy, absent when it is not
373
+ * tiered — in which case there is no scope and nothing is read.
374
+ * @param requested - The per-call `--tier` argument, if any.
375
+ * @param source - The story's provenance, for a query diagnosis.
376
+ * @returns The scope, or `undefined` for an unscoped noun.
377
+ * @throws PragmaError INVALID_INPUT when the tier asked for does not exist.
378
+ */
379
+ export async function resolveReadScope(
380
+ rt: PragmaRuntime,
381
+ declaration: PackTierScope | undefined,
382
+ requested: unknown,
383
+ source: StorySource,
384
+ ): Promise<TierScope | undefined> {
385
+ if (!declaration) return undefined;
386
+ const hierarchy = await readTierHierarchy(rt, declaration, source);
387
+ const { config } = await rt.loadConfig();
388
+ return resolveTierScope(hierarchy, {
389
+ requested,
390
+ ...(config.tier === undefined ? {} : { configured: config.tier }),
391
+ });
392
+ }
393
+
394
+ /**
395
+ * The scope in the words a reader can type back: `global, apps, apps_lxd`.
396
+ *
397
+ * @param scope - The resolved scope.
398
+ * @returns The in-scope tier local names, or `undefined` for every tier — a
399
+ * read that narrowed nothing has no scope to report.
400
+ */
401
+ export function scopeLabel(scope: TierScope): string | undefined {
402
+ if (scope.kind === "all") return undefined;
403
+ return scope.tiers.map((tier) => tier.local).join(", ");
404
+ }
405
+
406
+ /** The in-scope tier IRIs, for the query filter. */
407
+ export function scopeIris(scope: TierScope): readonly string[] {
408
+ return scope.kind === "all" ? [] : scope.tiers.map((tier) => tier.iri);
409
+ }
@@ -36,6 +36,13 @@ const verb = (
36
36
  return found;
37
37
  };
38
38
 
39
+ // The fragments below track the stories' CURRENT prose. They are quoted by
40
+ // FRAGMENT and not by exact string on purpose — tool descriptions are
41
+ // explicitly not frozen, and what this file pins is the WIRING: that a
42
+ // definition-level description reaches the list tool, a lookup-level one
43
+ // reaches the lookup tool, an extra verb's reaches its own, and that the MCP
44
+ // call example never leaks into CLI help. When a story's prose is rewritten
45
+ // these move with it; when a description stops REACHING its tool, they fail.
39
46
  describe("pack toolDescription wiring (PROTECTED)", () => {
40
47
  it("routes the lookup-level toolDescription to MCP, whole (with the call example)", async () => {
41
48
  const mcp = await projectMcp([tokenModule]);
@@ -43,11 +50,9 @@ describe("pack toolDescription wiring (PROTECTED)", () => {
43
50
  (t) => t.name === "token_lookup",
44
51
  )?.description;
45
52
  await mcp.cleanup();
46
- expect(desc).toContain(
47
- "Get type and theme values for one or more design tokens by name.",
48
- );
53
+ expect(desc).toContain("Get one design-token symbol in full");
49
54
  // The authored MCP tool-call example survives on the MCP surface.
50
- expect(desc).toContain('Example: token_lookup { name: ["color.primary"] }');
55
+ expect(desc).toContain('Example: token_lookup { name: ["color.text"] }');
51
56
  });
52
57
 
53
58
  it("routes the definition-level toolDescription to the MCP list tool", async () => {
@@ -58,8 +63,8 @@ describe("pack toolDescription wiring (PROTECTED)", () => {
58
63
  await mcp.cleanup();
59
64
  // The rich description reaches MCP (was previously dropped — only `summary`
60
65
  // reached the tool), including the authored call example.
61
- expect(desc).toContain("List all design tokens with their type.");
62
- expect(desc).toContain("Example: token_list {}");
66
+ expect(desc).toContain("List the design-token SYMBOLS");
67
+ expect(desc).toContain('Example: token_list { type: "color" }');
63
68
  });
64
69
 
65
70
  it("routes an extra-verb toolDescription to its MCP tool", async () => {
@@ -77,15 +82,13 @@ describe("pack toolDescription wiring (PROTECTED)", () => {
77
82
 
78
83
  it("CLI --help shows the rich prose but NEVER the MCP tool-call syntax", () => {
79
84
  const lookupHelp = formatVerbHelp("pragma", verb(tokenModule, "lookup"));
80
- expect(lookupHelp).toContain(
81
- "Get type and theme values for one or more design tokens by name.",
82
- );
85
+ expect(lookupHelp).toContain("Get one design-token symbol in full");
83
86
  // No-leaks: the `token_lookup {…}` MCP call shape must not reach CLI help.
84
87
  expect(lookupHelp).not.toContain("token_lookup {");
85
88
  expect(lookupHelp).not.toContain("Example:");
86
89
 
87
90
  const listHelp = formatVerbHelp("pragma", verb(tokenModule, "list"));
88
- expect(listHelp).toContain("List all design tokens with their type.");
91
+ expect(listHelp).toContain("List the design-token SYMBOLS");
89
92
  expect(listHelp).not.toContain("token_list {");
90
93
  });
91
94
  });