@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
@@ -6,13 +6,19 @@
6
6
  * `lookup` both compile from the declared story, so parity is asserted directly
7
7
  * against the graph. Two halves:
8
8
  *
9
- * - `block list` — the UNFILTERED list. Every block in the store, whatever the
10
- * tier or channel, including the UNTIERED subcomponent the old hand-written
11
- * query's required `ds:tier` join dropped (A2, formerly reachable only under
12
- * `--all-tiers`). The row shape the hand-written verb built in TypeScript —
13
- * the name fallback, the lowercased type, the tier's local name — is now
14
- * derived IN SPARQL by `BIND`/`COALESCE`, and this file is what holds those
15
- * BINDs to the shapes the old `blockList.verb.ts` produced.
9
+ * - `block list` — the TIER-SCOPED list. Every block the scope admits,
10
+ * whatever its channel, including the UNTIERED subcomponent the old
11
+ * hand-written query's required `ds:tier` join dropped (A2, formerly
12
+ * reachable only under `--all-tiers`) — an entity the scope cannot place is
13
+ * not one it may hide, so no scope drops it either. This fixture declares one
14
+ * tier, `global`, so the scope admits everything in it and the row-shape
15
+ * cases below read what they always read; what the scope decides is asserted
16
+ * in `kernel/packs/tierScope.test.ts` and, over the shipped pack's fifteen
17
+ * tiers, in `block.tierRank.exec.test.ts`. The row shape the hand-written
18
+ * verb built in TypeScript — the name fallback, the lowercased type, the
19
+ * tier's local name — is now derived IN SPARQL by `BIND`/`COALESCE`, and this
20
+ * file is what holds those BINDs to the shapes the old `blockList.verb.ts`
21
+ * produced.
16
22
  * - `block lookup` — unchanged GraphQL content parity, moved here from the
17
23
  * deleted `capabilities/block/parity.test.ts`. Button and Modal resolve with
18
24
  * the same content a direct SPARQL oracle returns (summary, guidance,
@@ -26,7 +32,7 @@
26
32
 
27
33
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
28
34
  import type { LookupOutput } from "../kernel/packs/resolveEntity.js";
29
- import type { PackRow } from "../kernel/packs/types.js";
35
+ import type { PackPage } from "../kernel/packs/types.js";
30
36
  import { verbKey } from "../kernel/packs/uniqueness.js";
31
37
  import type { PragmaRuntime } from "../kernel/runtime/types.js";
32
38
  import type { VerbSpec } from "../kernel/spec/types.js";
@@ -66,13 +72,13 @@ async function lookup(name: string): Promise<Record<string, unknown>> {
66
72
  return out.results.at(0) as Record<string, unknown>;
67
73
  }
68
74
 
69
- describe("block list parity — the declared, unfiltered list", () => {
75
+ describe("block list parity — the declared, tier-scoped list", () => {
70
76
  it("lists EVERY block, including the untiered subcomponent (A2)", async () => {
71
77
  // `ds:button.icon` is a `ds:Subcomponent` with NO `ds:tier`. The
72
78
  // hand-written query inner-joined `?c ds:tier ?t` in its default view and
73
79
  // surfaced it only under `--all-tiers`; the declared query's OPTIONAL join
74
80
  // means there is no view it can fall out of.
75
- const rows = (await verb("list").run({}, rt)) as PackRow[];
81
+ const rows = ((await verb("list").run({}, rt)) as PackPage).rows;
76
82
  expect(rows.map((row) => row.name)).toEqual([
77
83
  "Button",
78
84
  "Button Icon",
@@ -81,7 +87,7 @@ describe("block list parity — the declared, unfiltered list", () => {
81
87
  });
82
88
 
83
89
  it("derives the summary row shape in SPARQL, matching the old hand-built row", async () => {
84
- const rows = (await verb("list").run({}, rt)) as PackRow[];
90
+ const rows = ((await verb("list").run({}, rt)) as PackPage).rows;
85
91
  const byName = new Map(rows.map((row) => [row.name, row]));
86
92
  const button = byName.get("Button");
87
93
  // `type`: the LOWERCASED local name of the matched class — the BIND that
@@ -99,30 +105,73 @@ describe("block list parity — the declared, unfiltered list", () => {
99
105
  });
100
106
 
101
107
  it("an untiered block simply carries no tier — it is not dropped or blanked", async () => {
102
- const rows = (await verb("list").run({}, rt)) as PackRow[];
108
+ const rows = ((await verb("list").run({}, rt)) as PackPage).rows;
103
109
  const icon = rows.find((row) => row.name === "Button Icon");
104
110
  expect(icon).toBeDefined();
105
111
  expect(icon?.tier).toBeUndefined();
106
112
  expect(icon?.uri).toBe(`${DS}button.icon`);
107
113
  });
108
114
 
109
- it("is CONFIG-INDEPENDENT: a configured tier and channel change nothing", async () => {
110
- // The signed-off consequence of L-OPEN-9, pinned at the verb: the run body
111
- // never reads `loadConfig()`, so the same rows come back under any scope.
112
- const scoped = {
115
+ it("READS the configured tier as its scope, and the channel not at all", async () => {
116
+ // The `tier` key used to scope nothing: this suite pinned the list as
117
+ // CONFIG-INDEPENDENT, which was the signed-off consequence of the
118
+ // hand-written filtering's removal. The tier scope reverses exactly that
119
+ // half and only that half — `tier` decides which tiers a read answers
120
+ // from, `channel` still decides nothing (it selects an npm dist-tag).
121
+ //
122
+ // This fixture declares ONE tier (`global`, the base), so the scope it
123
+ // resolves to is the same set by either route; what this holds is that the
124
+ // config is READ and REPORTED. Narrowing down a real chain is asserted
125
+ // where a chain exists — `tierScope.test.ts` over a nested fixture, and
126
+ // `block.tierRank.exec.test.ts` over the shipped pack's fifteen tiers.
127
+ const withConfig = (tier: string, channel: "normal" | "experimental") =>
128
+ ({
129
+ ...rt,
130
+ loadConfig: async () => ({
131
+ ...(await rt.loadConfig()),
132
+ config: { ...(await rt.loadConfig()).config, tier, channel },
133
+ }),
134
+ }) as PragmaRuntime;
135
+
136
+ const scoped = (await verb("list").run(
137
+ {},
138
+ withConfig("global", "experimental"),
139
+ )) as PackPage;
140
+ expect(scoped.scope).toEqual({ tiers: ["global"] });
141
+ expect(scoped.rows.map((row) => row.name)).toEqual([
142
+ "Button",
143
+ "Button Icon",
144
+ "Modal",
145
+ ]);
146
+
147
+ // `all` turns the scope off, and the page then reports none — the shape
148
+ // every unscoped story's page has always had.
149
+ const all = (await verb("list").run(
150
+ {},
151
+ withConfig("all", "normal"),
152
+ )) as PackPage;
153
+ expect(all.scope).toBeUndefined();
154
+ expect(all.rows).toEqual(
155
+ ((await verb("list").run({}, rt)) as PackPage).rows,
156
+ );
157
+ });
158
+
159
+ it("refuses a configured tier no tier answers to, naming the ones that do", async () => {
160
+ // A `tier` nothing in the pack carries used to be inert — accepted by the
161
+ // validator and read by nobody. Now it decides a scope, so a value that
162
+ // names no tier is a misconfiguration worth saying out loud rather than
163
+ // silently answering from a scope the reader did not ask for.
164
+ const stale = {
113
165
  ...rt,
114
166
  loadConfig: async () => ({
115
167
  ...(await rt.loadConfig()),
116
- config: {
117
- ...(await rt.loadConfig()).config,
118
- tier: "apps/lxd",
119
- channel: "experimental" as const,
120
- },
168
+ config: { ...(await rt.loadConfig()).config, tier: "apps/lxd" },
121
169
  }),
122
170
  } as PragmaRuntime;
123
- const scopedRows = (await verb("list").run({}, scoped)) as PackRow[];
124
- const plainRows = (await verb("list").run({}, rt)) as PackRow[];
125
- expect(scopedRows).toEqual(plainRows);
171
+ await expect(verb("list").run({}, stale)).rejects.toMatchObject({
172
+ code: "INVALID_INPUT",
173
+ validOptions: ["all", "global"],
174
+ });
126
175
  });
127
176
 
128
177
  it("zero rows is a calm empty list, not an error (the declared-family semantics)", async () => {
@@ -139,12 +188,12 @@ ds:Component a owl:Class .
139
188
  prefixes: BLOCK_PREFIXES,
140
189
  });
141
190
  try {
142
- const rows = (await verb("list").run({}, emptyRt)) as PackRow[];
143
- expect(rows).toEqual([]);
191
+ const page = (await verb("list").run({}, emptyRt)) as PackPage;
192
+ expect(page.rows).toEqual([]);
144
193
  // The same semantics, on the empty-state seam: the calm message + its
145
194
  // runnable hint now reach the user via stderr (`notice`), keeping
146
195
  // plain stdout pure data.
147
- const notice = verb("list").output.formatters.notice?.(rows);
196
+ const notice = verb("list").output.formatters.notice?.(page);
148
197
  expect(notice).toContain("No blocks in the store.");
149
198
  expect(notice).toContain("pragma sources update");
150
199
  } finally {
@@ -208,11 +257,142 @@ describe("block lookup — Button content parity (GraphQL, detailed)", () => {
208
257
  expect(llm).toContain("#### When not to use");
209
258
  expect(llm).not.toContain("\n### When to use\n");
210
259
  expect(llm).toContain("### Anatomy (DSL)");
260
+ // The anatomy is a NOTATION, and its one unguessable rule is stated where
261
+ // the reader is looking: a bracketed value reads as a set of equals and is
262
+ // in fact an ordered fallback chain.
263
+ expect(llm).toContain(
264
+ "### Anatomy (DSL)\nA list value is the fallback chain the implementation reads, first wins.",
265
+ );
211
266
  expect(llm).toContain("### Modifier Families");
212
267
  expect(llm).toContain("primary");
213
268
  });
214
269
  });
215
270
 
271
+ describe("block lookup — the tokens a block consumes (SPARQL-lane expand)", () => {
272
+ it("lists one row per binding, with all six identity columns", async () => {
273
+ const button = await lookup("Button");
274
+ // Six columns and not five: the anatomy's `styleKey` and `styleState`
275
+ // carry no rdfs:domain, so the GraphQL document this lookup otherwise
276
+ // builds derives no field for either and dropped both WITHOUT a word —
277
+ // leaving `appearance.background` rank 1 and rank 2 looking like one fact
278
+ // repeated. That is why this expand declares the SPARQL lane.
279
+ expect(button.tokens).toEqual([
280
+ {
281
+ symbol: "color.background",
282
+ key: "appearance.background",
283
+ state: "default",
284
+ rank: "1",
285
+ node: "$root",
286
+ },
287
+ {
288
+ symbol: "color.background.hover",
289
+ key: "appearance.background",
290
+ state: "hover",
291
+ rank: "2",
292
+ node: "$root",
293
+ },
294
+ {
295
+ symbol: "color.text",
296
+ key: "typography.color",
297
+ state: "default",
298
+ rank: "1",
299
+ node: "$root",
300
+ },
301
+ {
302
+ symbol: "color.icon",
303
+ key: "appearance.background",
304
+ state: "default",
305
+ rank: "1",
306
+ via: "Button Icon",
307
+ node: "$root/icon",
308
+ },
309
+ ]);
310
+ });
311
+
312
+ it("orders the rows by node, then key, then rank", async () => {
313
+ // The fixture declares them in another order on purpose: the store's own
314
+ // scan order is undefined, and a table of a node's styles is only readable
315
+ // one node at a time, one key at a time, each key's fallback chain in the
316
+ // order the implementation walks it.
317
+ const button = await lookup("Button");
318
+ const rows = button.tokens as {
319
+ node: string;
320
+ key: string;
321
+ rank: string;
322
+ }[];
323
+ expect(rows.map((row) => [row.node, row.key, row.rank])).toEqual([
324
+ ["$root", "appearance.background", "1"],
325
+ ["$root", "appearance.background", "2"],
326
+ ["$root", "typography.color", "1"],
327
+ ["$root/icon", "appearance.background", "1"],
328
+ ]);
329
+ });
330
+
331
+ it("blanks the via on the block's own tree and prints it otherwise", async () => {
332
+ // `ds:viaBlock` is asserted on EVERY record and equals the block on the
333
+ // block's own tree, so printing it raw put "Button" in every one of
334
+ // Button's own rows — a cell that is only ever news when it differs.
335
+ const button = await lookup("Button");
336
+ const rows = button.tokens as { via?: string; node: string }[];
337
+ for (const row of rows.filter((row) => row.node === "$root")) {
338
+ expect(row.via).toBeUndefined();
339
+ }
340
+ expect(rows.find((row) => row.node === "$root/icon")?.via).toBe(
341
+ "Button Icon",
342
+ );
343
+
344
+ // Modal consumes nothing of its own: its one row is Button's binding,
345
+ // reached through the Button its anatomy embeds, so the via is the whole
346
+ // point of the row.
347
+ const modal = await lookup("Modal");
348
+ expect(modal.tokens).toEqual([
349
+ {
350
+ symbol: "color.background",
351
+ key: "appearance.background",
352
+ state: "default",
353
+ rank: "1",
354
+ via: "Button",
355
+ node: "$root/footer",
356
+ },
357
+ ]);
358
+ });
359
+
360
+ it("renders the section under its own heading, with the reading note", async () => {
361
+ const llm = verb("lookup").output.formatters.llm(
362
+ (await verb("lookup").run({ name: ["Button"] }, rt)) as never,
363
+ );
364
+ // The same sentence `Anatomy (DSL)` carries, because the rank column is
365
+ // that notation's fallback chain seen from the other side.
366
+ expect(llm).toContain(
367
+ "### Tokens\nA list value in the anatomy is the fallback chain the implementation reads, first wins; rank is the position in that chain.",
368
+ );
369
+ expect(llm).toContain("symbol: color.text | key: typography.color");
370
+ });
371
+
372
+ it("prints no Tokens heading for a block that consumes nothing", async () => {
373
+ // What every other section on this lookup does when it is empty: no
374
+ // heading at all, rather than a heading with nothing under it. The key is
375
+ // still present in the payload as an empty array, so the machine shape
376
+ // stays uniform.
377
+ const icon = await lookup("Button Icon");
378
+ expect(icon.tokens).toEqual([]);
379
+ const llm = verb("lookup").output.formatters.llm(
380
+ (await verb("lookup").run({ name: ["Button Icon"] }, rt)) as never,
381
+ );
382
+ expect(llm).not.toContain("### Tokens");
383
+ });
384
+
385
+ it("is withheld at the summary level with the other detailed expands", async () => {
386
+ const out = (await verb("lookup").run(
387
+ { name: ["Button"] },
388
+ { ...rt, globalFlags: { ...rt.globalFlags, detail: "summary" } },
389
+ )) as LookupOutput;
390
+ expect(
391
+ (out.results.at(0) as Record<string, unknown>).tokens,
392
+ ).toBeUndefined();
393
+ });
394
+ });
395
+
216
396
  describe("block lookup — Modal content parity (GraphQL, detailed)", () => {
217
397
  it("resolves the full Modal spec (a different block, same document engine)", async () => {
218
398
  const modal = await lookup("Modal");
@@ -131,6 +131,44 @@ const DECLARED_PROPERTIES: readonly DeclaredTerm[] = [
131
131
  ]),
132
132
  ];
133
133
 
134
+ /**
135
+ * Standard RDF vocabulary, which no domain ontology declares and every graph
136
+ * carries — the same exemption, for the same reason, that the token stories'
137
+ * shipped sweep makes: the distribution's own vocabulary declaration leaves
138
+ * `rdfs:label` out because "the kernel treats standard vocabulary as
139
+ * universal", and the shipped pack accordingly carries no T-box triple about
140
+ * it at all. Nothing upstream can retire a W3C term, which is the defect this
141
+ * file exists to catch.
142
+ *
143
+ * A readable list rather than a widened ASK, so the sweep stays a hard
144
+ * requirement on every DOMAIN term.
145
+ */
146
+ const STANDARD_VOCABULARY = new Set(["rdfs:label", "rdfs:comment"]);
147
+
148
+ /**
149
+ * The property STEPS one declaration names — more than one when it is a path.
150
+ *
151
+ * A SPARQL-lane declaration may read a grandchild through a path
152
+ * (`ds:consumesSymbol/rdfs:label`), and every domain step of it is a term the
153
+ * ontology has to define: a retired step anywhere along a path renders exactly
154
+ * as silently as a retired single term. Split rather than skipped — a path
155
+ * spliced whole into the ASK's subject position would not even parse, so the
156
+ * check would have raised instead of judging, and skipping paths outright
157
+ * would leave their steps unguarded.
158
+ *
159
+ * An absolute IRI is left whole: a `/` inside `https://…` is not a path
160
+ * separator, which is the grammar's own test for a property path.
161
+ */
162
+ function pathSteps(property: string): readonly string[] {
163
+ if (!property.includes("/") || property.includes("://")) {
164
+ return STANDARD_VOCABULARY.has(property) ? [] : [property];
165
+ }
166
+ return property
167
+ .split("/")
168
+ .map((step) => step.replace(/^\^/, "").replace(/[*+?]$/, ""))
169
+ .filter((step) => step !== "" && !STANDARD_VOCABULARY.has(step));
170
+ }
171
+
134
172
  /** The IRI of the block whose usage narrative is asserted on below. */
135
173
  const BUTTON = "https://ds.canonical.com/global.component.button";
136
174
 
@@ -167,25 +205,129 @@ describe("the `block` story declares vocabulary the shipped ontology defines (PR
167
205
  expect(DECLARED_PROPERTIES.length).toBeGreaterThan(0);
168
206
  });
169
207
 
170
- it.each(
171
- DECLARED_PROPERTIES,
172
- )("$what reads $property, which the shipped ontology defines", async ({
173
- what,
174
- property,
175
- }) => {
176
- const result = await rt.query.sparql(
208
+ it.each(DECLARED_PROPERTIES)(
209
+ "$what reads $property, which the shipped ontology defines",
210
+ async ({ what, property }) => {
211
+ for (const hop of pathSteps(property)) {
212
+ const result = await rt.query.sparql(
213
+ [
214
+ "ASK {",
215
+ ` ${hop} a ?kind .`,
216
+ " VALUES ?kind { owl:DatatypeProperty owl:ObjectProperty }",
217
+ "}",
218
+ ].join("\n"),
219
+ );
220
+ expect(result.type).toBe("ask");
221
+ expect(
222
+ result.type === "ask" ? result.result : false,
223
+ `${what} reads ${property}, whose hop ${hop} the shipped ontology does not define — it can never render, for any block, on any install`,
224
+ ).toBe(true);
225
+ }
226
+ },
227
+ );
228
+ });
229
+
230
+ describe("block lookup lists the tokens a block consumes (PROTECTED)", () => {
231
+ it("renders one row per binding record, with all six identity columns", async () => {
232
+ // The binding records are the design system's, derived from the anatomies,
233
+ // and they ride the embedded snapshot — so the ONE thing a fixture cannot
234
+ // prove is that the shipped corpus reaches this expand at all. Button is
235
+ // asserted on rather than any block: it is the block whose anatomy every
236
+ // other lookup test reads, and it carries bindings on its own tree AND on
237
+ // the tree of the icon it embeds.
238
+ const records = await scalar(
239
+ `SELECT (COUNT(*) AS ?n) WHERE { <${BUTTON}> ds:hasTokenBinding ?r }`,
240
+ );
241
+ expect(Number(records)).toBeGreaterThan(0);
242
+
243
+ const llm = await renderLookup(BUTTON);
244
+ expect(llm).toContain("### Tokens");
245
+ // The note, because a rank column without it is a number with no stated
246
+ // meaning.
247
+ expect(llm).toContain("rank is the position in that chain");
248
+
249
+ // One rendered row per record, and every row carrying the two columns the
250
+ // GraphQL lane cannot derive (neither `anatomy:styleKey` nor
251
+ // `anatomy:styleState` has an rdfs:domain, so no field exists for either
252
+ // on `TokenBinding`). Their absence is what made two of Button's rows
253
+ // byte-identical, so their presence is the assertion.
254
+ const rows = (llm.split("### Tokens")[1] ?? "")
255
+ .split("\n")
256
+ .filter((line) => line.startsWith("- symbol: "));
257
+ expect(rows).toHaveLength(Number(records));
258
+ for (const row of rows) {
259
+ expect(row).toContain(" | key: ");
260
+ expect(row).toContain(" | node: ");
261
+ }
262
+ // And no two rows the same — the defect a dropped identity column causes.
263
+ expect(new Set(rows).size).toBe(rows.length);
264
+ });
265
+
266
+ it("blanks the via on a block's own tree and prints an inherited one", async () => {
267
+ // `ds:viaBlock` is asserted on every record and its own definition says it
268
+ // equals the block in the own-tree case, so an unconditional column put
269
+ // the block's own name in every one of its rows.
270
+ const ownTree = await scalar(
271
+ [
272
+ "SELECT (COUNT(*) AS ?n) WHERE {",
273
+ ` <${BUTTON}> ds:hasTokenBinding ?r .`,
274
+ ` ?r ds:viaBlock <${BUTTON}> .`,
275
+ "}",
276
+ ].join("\n"),
277
+ );
278
+ expect(Number(ownTree)).toBeGreaterThan(0);
279
+
280
+ const llm = await renderLookup(BUTTON);
281
+ const rows = (llm.split("### Tokens")[1] ?? "")
282
+ .split("\n")
283
+ .filter((line) => line.startsWith("- symbol: "));
284
+ expect(rows.filter((row) => !row.includes(" | via: "))).toHaveLength(
285
+ Number(ownTree),
286
+ );
287
+
288
+ // A block whose bindings are ALL inherited, read from the graph rather
289
+ // than named here: every row of its section must carry a via.
290
+ const inheritor = await scalar(
177
291
  [
178
- "ASK {",
179
- ` ${property} a ?kind .`,
180
- " VALUES ?kind { owl:DatatypeProperty owl:ObjectProperty }",
292
+ "SELECT ?b WHERE {",
293
+ ` VALUES ?class { ${TYPE_VALUES} }`,
294
+ " ?b a ?class ; ds:hasTokenBinding ?r .",
295
+ " FILTER NOT EXISTS { ?b ds:hasTokenBinding/ds:viaBlock ?b }",
181
296
  "}",
297
+ "ORDER BY ?b",
298
+ "LIMIT 1",
182
299
  ].join("\n"),
183
300
  );
184
- expect(result.type).toBe("ask");
185
301
  expect(
186
- result.type === "ask" ? result.result : false,
187
- `${what} reads ${property}, which the shipped ontology does not define — it can never render, for any block, on any install`,
188
- ).toBe(true);
302
+ inheritor,
303
+ "no shipped block carries only inherited bindings",
304
+ ).toBeDefined();
305
+ const inheritedRows = (
306
+ (await renderLookup(inheritor as string)).split("### Tokens")[1] ?? ""
307
+ )
308
+ .split("\n")
309
+ .filter((line) => line.startsWith("- symbol: "));
310
+ expect(inheritedRows.length).toBeGreaterThan(0);
311
+ for (const row of inheritedRows) expect(row).toContain(" | via: ");
312
+ });
313
+
314
+ it("prints no Tokens heading for a block that consumes nothing", async () => {
315
+ // The shape every other section on this lookup has when it is empty. Read
316
+ // from the graph, because which blocks have no bindings is upstream's to
317
+ // change.
318
+ const uri = await scalar(
319
+ [
320
+ "SELECT ?b WHERE {",
321
+ ` VALUES ?class { ${TYPE_VALUES} }`,
322
+ " ?b a ?class .",
323
+ " FILTER NOT EXISTS { ?b ds:hasTokenBinding ?r }",
324
+ "}",
325
+ "ORDER BY ?b",
326
+ "LIMIT 1",
327
+ ].join("\n"),
328
+ );
329
+ expect(uri, "every shipped block carries bindings").toBeDefined();
330
+ expect(await renderLookup(uri as string)).not.toContain("### Tokens");
189
331
  });
190
332
  });
191
333
 
@@ -2,7 +2,7 @@
2
2
  * Which block a SHARED name means, EXECUTED against the shipped pack
3
3
  * (PROTECTED).
4
4
  *
5
- * 25 of the design system's block names are carried by two or three blocks
5
+ * 32 of the design system's block names are carried by two or three blocks
6
6
  * apiece, across tiers. `block lookup` used to answer each of them with whichever
7
7
  * IRI sorted first and say nothing about the rest, so `button` returned
8
8
  * Launchpad's Button — `apps_launchpad…` sorts before `global…` — and an agent
@@ -27,6 +27,8 @@
27
27
  */
28
28
 
29
29
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
30
+ import { MAX_LIST_WINDOW } from "../kernel/packs/paging.js";
31
+ import type { PackPage } from "../kernel/packs/types.js";
30
32
  import { verbKey } from "../kernel/packs/uniqueness.js";
31
33
  import { bootRuntime } from "../kernel/runtime/boot.js";
32
34
  import type { PragmaRuntime } from "../kernel/runtime/types.js";
@@ -80,11 +82,23 @@ async function rows(query: string): Promise<Record<string, string>[]> {
80
82
  : [];
81
83
  }
82
84
 
83
- /** One `block lookup <name>`: the IRI it answered with, and the ones it did not. */
85
+ /**
86
+ * One `block lookup <name>`: the IRI it answered with, and the ones it did not.
87
+ *
88
+ * ASKED FOR EVERY TIER, and that is load-bearing for this whole file. The claim
89
+ * under test is about RANKING — every block a shared name reaches comes back,
90
+ * best first — and the ranking orders a population the TIER SCOPE would
91
+ * otherwise cut: `block lookup button` answers with the global Button alone
92
+ * now, because the Launchpad one is out of the default scope. Both rules are
93
+ * about the same 25 collisions and they are not the same rule, so this suite
94
+ * widens the read to the population whose order it is judging. That the scope
95
+ * NARROWS it by default is asserted where that is the claim
96
+ * (`tierScope.test.ts`, and the shipped-pack cases below).
97
+ */
84
98
  async function lookup(
85
99
  name: string,
86
100
  ): Promise<{ chosen: string; others: string[] }> {
87
- const out = (await lookupVerb.run({ name: [name] }, rt)) as {
101
+ const out = (await lookupVerb.run({ name: [name], tier: "all" }, rt)) as {
88
102
  results: { uri?: string }[];
89
103
  };
90
104
  // Every entity the name reached, best first. `chosen` is simply the head —
@@ -97,8 +111,12 @@ async function lookup(
97
111
  * Every block name the shipped graph gives to more than one block, with each
98
112
  * block's IRI, its tier's declared name and whether it is a subcomponent.
99
113
  *
100
- * Read with the SAME `LCASE` the resolve filters by, so a pair that collides
101
- * only for a lookup (`Button` vs `button`) is in this set too.
114
+ * Read with the SAME equality the resolve filters by — case-folded AND
115
+ * end-trimmed — so a pair that collides only for a lookup (`Button` vs
116
+ * `button`, `Timeline` vs the padded `Timeline ` 66 shipped names are spelled
117
+ * like) is in this set too. An oracle that folded less than the resolve would
118
+ * hand the sweep below a name reaching more blocks than the oracle listed, and
119
+ * read the resolve's correct answer as a failure.
102
120
  */
103
121
  async function collisions(): Promise<
104
122
  Map<string, { uri: string; tier: string; part: boolean }[]>
@@ -110,7 +128,7 @@ async function collisions(): Promise<
110
128
  " ?uri a ?class .",
111
129
  ` ?uri ${blockLookup.by} ?name .`,
112
130
  ` OPTIONAL { ?uri ${scope.via} ?tier . ?tier ${scope.by} ?tn . }`,
113
- " BIND(LCASE(STR(?name)) AS ?key)",
131
+ ' BIND(LCASE(REPLACE(STR(?name), "^\\\\s+|\\\\s+$", "")) AS ?key)',
114
132
  ' BIND(COALESCE(?tn, "") AS ?tierName)',
115
133
  " BIND(EXISTS { ?uri a ds:Subcomponent } AS ?part)",
116
134
  "}",
@@ -234,10 +252,29 @@ describe("a shared block name reaches every block that carries it (PROTECTED)",
234
252
  });
235
253
 
236
254
  describe("`block list` orders its ties totally (PROTECTED)", () => {
237
- /** The entity IRIs of one `block list` run, in the order it printed them. */
255
+ /**
256
+ * The entity IRIs of one `block list` run, in the order it printed them —
257
+ * over EVERY tier, like the lookup cases above, and as ONE page over the whole
258
+ * population, because the order under test is the order of the whole
259
+ * population. A total order over the 313-block population is the property; the
260
+ * default scope's rows are a subsequence of it, so judging the order on the
261
+ * wider read judges both. A default-paged read used to be the whole population
262
+ * and quietly stopped being it: `DEFAULT_LIST_LIMIT` is 300, so the tail of
263
+ * the alphabet — `Timestamp`, `Tooltip`, and their launchpad twins — fell off
264
+ * the page and the cases below read a missing row as a lost row. The page is
265
+ * asserted COMPLETE rather than assumed so, so that the next time the
266
+ * population outgrows the window this says which it was.
267
+ */
238
268
  async function listOrder(): Promise<string[]> {
239
- const data = (await listVerb.run({}, rt)) as { uri?: string }[];
240
- return data.map((row) => String(row.uri));
269
+ const page = (await listVerb.run(
270
+ { tier: "all", limit: MAX_LIST_WINDOW },
271
+ rt,
272
+ )) as PackPage;
273
+ expect(
274
+ page.nextAfter,
275
+ `block list did not fit in one ${MAX_LIST_WINDOW}-row page`,
276
+ ).toBeUndefined();
277
+ return page.rows.map((row) => String(row.uri));
241
278
  }
242
279
 
243
280
  it("repeats the same order across runs", async () => {
@@ -31,10 +31,19 @@ import type {
31
31
  * each other. `system` projects the distribution's identity rather than naming
32
32
  * a domain — the live tool catalog the same handshake carries already says
33
33
  * which nouns exist. `querying` is verbatim from the old shell (the SPARQL
34
- * model is still accurate for v2); `model` stopped claiming reads are
35
- * tier/channel-SCOPED when the hand-written filtering was removed (L-OPEN-9)
36
- * — the hierarchy is graph data now, not a query gate; `mutations` is new in
37
- * v2, surfacing the plan-first/confirm gate.
34
+ * model is still accurate for v2); `mutations` is new in v2, surfacing the
35
+ * plan-first/confirm gate.
36
+ *
37
+ * `model` is the one that has had to be rewritten twice, and both times because
38
+ * it was describing a gate that had moved. It stopped claiming reads were
39
+ * tier/channel-scoped when the hand-written filtering was removed (the
40
+ * hierarchy became graph data, not a query gate) — and "Reads are unscoped —
41
+ * every list shows everything" became false the day the tier scope landed. An
42
+ * agent reading it took a scoped `block_list` for the whole design system,
43
+ * which is the exact misreading the sentence existed to prevent. So it now says
44
+ * what is true of each half separately: the TIER hierarchy gates a read of a
45
+ * tiered noun and names the argument that widens it, while the CHANNEL is still
46
+ * data on the entity and gates nothing.
38
47
  */
39
48
  export const CONVENTIONS = {
40
49
  // `help` is authored as a bare phrase (`--help` renders it as one), so the
@@ -43,7 +52,7 @@ export const CONVENTIONS = {
43
52
  // otherwise reads "recipe graph.. A CLI and MCP server…".
44
53
  system: `${BIN_NAME} — ${PROGRAM_DESCRIPTION} (a CLI and MCP server over a knowledge graph).`,
45
54
  model:
46
- "The graph models a tier hierarchy (e.g. global > apps > apps/lxd) and release channels (normal, experimental, prerelease) as DATA on each entity. Reads are unscoped — every list shows everything; filter by inspecting an entity's tier/release fields or with graph_query.",
55
+ 'The tier hierarchy (global > apps > apps_lxd) SCOPES every read of a tiered entity: lists answer from the top-level tiers, lookups prefer them. Pass tier: "<name>" for a tier plus its ancestors, or tier: "all" for every tier; each answer states its scope. Channels scope nothing.',
47
56
  querying:
48
57
  "All queries run against an RDF triple store. Prefixed IRIs (e.g. prefix:name) identify entities. Use ontology_list to discover the active namespaces.",
49
58
  mutations: