@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
@@ -16,7 +16,7 @@
16
16
  import { mkdtempSync, writeFileSync } from "node:fs";
17
17
  import { tmpdir } from "node:os";
18
18
  import { join } from "node:path";
19
- import { describe, expect, it } from "vitest";
19
+ import { describe, expect, it, vi } from "vitest";
20
20
  import { storyModules } from "../../capabilities/distribution.js";
21
21
  import { checkPackageRefs } from "../../capabilities/doctor/checks/checkPackageRefs.js";
22
22
  import { promptListVerb } from "../../capabilities/prompt/verbs.js";
@@ -27,6 +27,14 @@ import { bootRuntime } from "../../kernel/runtime/boot.js";
27
27
  import type { GlobalFlags } from "../../kernel/runtime/types.js";
28
28
  import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
29
29
 
30
+ // Every case here boots its own runtime — a fresh install is what is under test —
31
+ // and the first read in the process parses the embedded pack cold: 65,000 triples,
32
+ // three seconds on an idle machine and more when every package's suite runs at
33
+ // once, which is how the release workflow runs them. The budget is a fact about
34
+ // the pack's size, not about any assertion, and it belongs to the file because
35
+ // whichever case runs first is the one that pays it.
36
+ vi.setConfig({ testTimeout: 60_000 });
37
+
30
38
  const JSON_FLAGS: GlobalFlags = {
31
39
  llm: false,
32
40
  autoLlm: false,
@@ -113,10 +121,67 @@ describe("first install — an empty cwd answers real reads offline", () => {
113
121
  });
114
122
 
115
123
  describe("first install — empty results are honest, not papered over", () => {
116
- it("token list exits calmly with no rows (the graph carries no ds:Token)", async () => {
117
- expect(
118
- await readData(verbOf(tokenModule, "token list"), emptyCwd()),
119
- ).toEqual([]);
124
+ // `token list` used to be this describe's first case, asserting no rows
125
+ // "because the graph carries no ds:Token". That is no longer true and the
126
+ // change is the point: the noun addressed a class no shipped graph asserted,
127
+ // it now addresses the token symbols, and the embedded pack carries them with
128
+ // the name literal the story keys on — so a first install ANSWERS. The case
129
+ // moved rather than being deleted, and it moved in both directions.
130
+ // ONE case and ONE store boot for the whole token noun, deliberately. Each
131
+ // `readData` here boots its own runtime — which is the point of this file,
132
+ // since a fresh install is what is under test — and the embedded pack is the
133
+ // expensive part of that. Three separate cases meant three boots of a
134
+ // 53,467-triple store for three claims about the same install, which is a
135
+ // cost this file pays under every parallel run for no extra coverage.
136
+ it("the token noun answers a first install: rows, a named symbol, and honest emptiness", async () => {
137
+ const cwd = emptyCwd();
138
+ const runtime = bootRuntime(JSON_FLAGS, cwd);
139
+
140
+ const read = async (
141
+ verb: VerbSpec,
142
+ params: Record<string, unknown> = {},
143
+ ): Promise<unknown> => {
144
+ const outcome = await executeVerb(verb, params, NO_MUTATION, runtime);
145
+ return (JSON.parse(outcome.stdout as string) as { data: unknown }).data;
146
+ };
147
+
148
+ // The symbols resolve offline, from the embedded pack, with no build.
149
+ const rows = (await read(verbOf(tokenModule, "token list"))) as {
150
+ name: string;
151
+ }[];
152
+ expect(rows.length).toBeGreaterThan(0);
153
+ for (const row of rows) expect(row.name).toBeTruthy();
154
+
155
+ // Membership, never a count — but asserted through a FILTER rather than
156
+ // through the unfiltered page, because this is the first population large
157
+ // enough for the page to matter: 436 symbols sort before `color.text`, so
158
+ // it is not on the first page and an unfiltered `toContain` would be
159
+ // testing the alphabet. A filter compiles INTO the query, so it answers
160
+ // from the whole population however the rows happen to page.
161
+ const matched = (await read(verbOf(tokenModule, "token list"), {
162
+ search: "color.text",
163
+ })) as { name: string }[];
164
+ expect(matched.map((row) => row.name)).toContain("color.text");
165
+
166
+ // The bindings between blocks and symbols now ride the pack too — they
167
+ // are derived from the component anatomies the embedded snapshot carries —
168
+ // so this verb ANSWERS on a first install where it used to be the honest
169
+ // emptiness this describe was written around. The claim moved rather than
170
+ // being deleted: what a first install owes the reader is a usable answer,
171
+ // and the columns that make a row usable are asserted here.
172
+ const consumers = (await read(verbOf(tokenModule, "token consumers"))) as {
173
+ block: string;
174
+ via?: string;
175
+ symbol: string;
176
+ }[];
177
+ expect(consumers.length).toBeGreaterThan(0);
178
+ for (const row of consumers) {
179
+ // The CONSUMING block, which is the fact the verb exists for, and the
180
+ // via only where the binding came from another block's tree.
181
+ expect(row.block).toBeTruthy();
182
+ expect(row.symbol).toBeTruthy();
183
+ if (row.via) expect(row.via).not.toBe(row.block);
184
+ }
120
185
  });
121
186
 
122
187
  it("prompt list exits calmly with no prompts (the graph carries no ds:Prompt)", async () => {
@@ -104,9 +104,10 @@ describe("MCP lifecycle — cold store recovers after sources_update (E2, C1)",
104
104
  const names = (warm.data as { name: string }[])
105
105
  .map((row) => row.name)
106
106
  .sort();
107
- // The whole pack — `block_list` is the declared, unfiltered list since
108
- // L-OPEN-9, so no channel hides Beta Badge and no tier join hides the
109
- // untiered Orphan Widget.
107
+ // The whole pack: no channel hides Beta Badge, no tier join hides the
108
+ // untiered Orphan Widget, and the tier scope admits the rest because every
109
+ // tier this fixture declares is top-level. Add a nested one upstream and
110
+ // this set becomes the default scope's, not the pack's.
110
111
  expect(names).toEqual(["Beta Badge", "Button", "Card", "Orphan Widget"]);
111
112
  });
112
113
  });
@@ -22,6 +22,7 @@ import { runChecks } from "../../capabilities/doctor/runChecks.js";
22
22
  import { capabilities } from "../../capabilities/index.js";
23
23
  import { buildUpdateTask } from "../../capabilities/sources/runUpdate.js";
24
24
  import { loadEffectiveModules } from "../../kernel/packs/collect.js";
25
+ import type { PackPage } from "../../kernel/packs/types.js";
25
26
  import { packDir, readActivePack } from "../../kernel/runtime/paths.js";
26
27
  import type { VerbSpec } from "../../kernel/spec/types.js";
27
28
  import {
@@ -103,7 +104,7 @@ describe("a package-declared noun answers reads (in-process)", () => {
103
104
  const list = recipe?.verbs.find(
104
105
  (verb) => verb.path.join(" ") === "recipe list",
105
106
  ) as VerbSpec;
106
- const rows = (await list.run({}, graph.runtime)) as { name: string }[];
107
+ const { rows } = (await list.run({}, graph.runtime)) as PackPage;
107
108
  expect(rows.map((row) => row.name)).toEqual(["Soup", "Stew"]);
108
109
  });
109
110
 
@@ -114,31 +114,34 @@ describe("every MCP tool is callable and returns a well-formed envelope (B4)", (
114
114
  expect(everyExposedVerb.length).toBeGreaterThan(0);
115
115
  });
116
116
 
117
- it.each(
118
- everyExposedVerb,
119
- )("$tool: callable, well-formed envelope, annotations match capability.mutates", async (v) => {
120
- const tool = v.tool as string;
121
- const args = representativeArgs(v);
122
- const result = await mcp.callTool(tool, args);
117
+ it.each(everyExposedVerb)(
118
+ "$tool: callable, well-formed envelope, annotations match capability.mutates",
119
+ async (v) => {
120
+ const tool = v.tool as string;
121
+ const args = representativeArgs(v);
122
+ const result = await mcp.callTool(tool, args);
123
123
 
124
- expect(typeof result.ok).toBe("boolean");
125
- if (result.ok) {
126
- expect("data" in result).toBe(true);
127
- expect(typeof result.meta).toBe("object");
128
- } else {
129
- const error = result.error as { code?: unknown; message?: unknown };
130
- expect(typeof error.code).toBe("string");
131
- expect(typeof error.message).toBe("string");
132
- }
124
+ expect(typeof result.ok).toBe("boolean");
125
+ if (result.ok) {
126
+ expect("data" in result).toBe(true);
127
+ expect(typeof result.meta).toBe("object");
128
+ } else {
129
+ const error = result.error as { code?: unknown; message?: unknown };
130
+ expect(typeof error.code).toBe("string");
131
+ expect(typeof error.message).toBe("string");
132
+ }
133
133
 
134
- const registered = registeredTools.find((t) => t.name === tool);
135
- expect(registered).toBeDefined();
136
- const annotations = registered?.annotations as
137
- | { readOnlyHint?: boolean; destructiveHint?: boolean }
138
- | undefined;
139
- expect(annotations?.readOnlyHint).toBe(!v.mutates);
140
- if (v.spec.capability.destructive !== undefined) {
141
- expect(annotations?.destructiveHint).toBe(v.spec.capability.destructive);
142
- }
143
- });
134
+ const registered = registeredTools.find((t) => t.name === tool);
135
+ expect(registered).toBeDefined();
136
+ const annotations = registered?.annotations as
137
+ | { readOnlyHint?: boolean; destructiveHint?: boolean }
138
+ | undefined;
139
+ expect(annotations?.readOnlyHint).toBe(!v.mutates);
140
+ if (v.spec.capability.destructive !== undefined) {
141
+ expect(annotations?.destructiveHint).toBe(
142
+ v.spec.capability.destructive,
143
+ );
144
+ }
145
+ },
146
+ );
144
147
  });
@@ -197,28 +197,30 @@ describe("read-noun parity — every list/lookup/extra verb (B5)", () => {
197
197
  });
198
198
  });
199
199
 
200
- it.each(
201
- lookupVerbs,
202
- )("$noun lookup: CLI --format json == MCP, for a known name", async (v) => {
203
- const name = firstNameByNoun.get(v.noun);
204
- if (!name) return;
205
- await assertCliMcpParity({
206
- modules: capabilities,
207
- verb: v.spec,
208
- tool: v.tool as string,
209
- cwd: fixture.cwd,
210
- params: { name: [name] },
211
- });
212
- });
200
+ it.each(lookupVerbs)(
201
+ "$noun lookup: CLI --format json == MCP, for a known name",
202
+ async (v) => {
203
+ const name = firstNameByNoun.get(v.noun);
204
+ if (!name) return;
205
+ await assertCliMcpParity({
206
+ modules: capabilities,
207
+ verb: v.spec,
208
+ tool: v.tool as string,
209
+ cwd: fixture.cwd,
210
+ params: { name: [name] },
211
+ });
212
+ },
213
+ );
213
214
 
214
- it.each(
215
- extraListShapedVerbs,
216
- )("$noun $verb: CLI --format json == MCP", async (v) => {
217
- await assertCliMcpParity({
218
- modules: capabilities,
219
- verb: v.spec,
220
- tool: v.tool as string,
221
- cwd: fixture.cwd,
222
- });
223
- });
215
+ it.each(extraListShapedVerbs)(
216
+ "$noun $verb: CLI --format json == MCP",
217
+ async (v) => {
218
+ await assertCliMcpParity({
219
+ modules: capabilities,
220
+ verb: v.spec,
221
+ tool: v.tool as string,
222
+ cwd: fixture.cwd,
223
+ });
224
+ },
225
+ );
224
226
  });
@@ -202,11 +202,21 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
202
202
  "kind": "tool",
203
203
  "passed": true,
204
204
  },
205
+ {
206
+ "id": "tool-token-consumers-answers-or-recovers",
207
+ "kind": "tool",
208
+ "passed": true,
209
+ },
205
210
  {
206
211
  "id": "tool-token-lookup-addresses-only-what-list-publishes",
207
212
  "kind": "tool",
208
213
  "passed": true,
209
214
  },
215
+ {
216
+ "id": "tool-token-lookup-dotted-name-resolves-with-its-definitions",
217
+ "kind": "tool",
218
+ "passed": true,
219
+ },
210
220
  {
211
221
  "id": "tool-token-noun-is-read-only",
212
222
  "kind": "tool",
@@ -217,10 +227,20 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
217
227
  "kind": "tool",
218
228
  "passed": true,
219
229
  },
230
+ {
231
+ "id": "tool-token-values-narrows-to-one-symbol",
232
+ "kind": "tool",
233
+ "passed": true,
234
+ },
220
235
  {
221
236
  "id": "tool-upgrade-is-plan-first",
222
237
  "kind": "tool",
223
238
  "passed": true,
224
239
  },
240
+ {
241
+ "id": "tool-variable-lookup-resolves-without-the-leading-dashes",
242
+ "kind": "tool",
243
+ "passed": true,
244
+ },
225
245
  ]
226
246
  `;
@@ -186,7 +186,13 @@ export const effectfulEvalCases: readonly EvalCase[] = [
186
186
  assert.ok(!tools.includes("token_add-config"));
187
187
  assert.deepEqual(
188
188
  tools.filter((name) => name.startsWith("token")).sort(),
189
- ["token_list", "token_lookup", "token_sample"],
189
+ [
190
+ "token_consumers",
191
+ "token_list",
192
+ "token_lookup",
193
+ "token_sample",
194
+ "token_values",
195
+ ],
190
196
  );
191
197
  });
192
198
  },
@@ -165,7 +165,7 @@ export const readNounEvalCases: readonly EvalCase[] = [
165
165
  id: "tool-token-lookup-addresses-only-what-list-publishes",
166
166
  kind: "tool",
167
167
  input:
168
- "a ds:Token carrying no ds:tokenId appears in no token_list row, resolves through no token_lookup name, and is drawn by no token_sample.",
168
+ "a dt:TokenSymbol carrying no rdfs:label appears in no token_list row, resolves through no token_lookup name, and is drawn by no token_sample.",
169
169
  async expect() {
170
170
  await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
171
171
  const list = await mcp.callTool("token_list");
@@ -175,11 +175,11 @@ export const readNounEvalCases: readonly EvalCase[] = [
175
175
  );
176
176
  assert.ok(published.size > 0, "expected token_list to publish rows");
177
177
  // The IRI-derived spellings the fallback would have minted for
178
- // `ds:token.legacy.borderRadius` (local name, dots as slashes).
179
- for (const derived of [
180
- "token.legacy.borderRadius",
181
- "token/legacy/borderRadius",
182
- ]) {
178
+ // `dt:legacy.borderRadius` — the local name, and the same local name
179
+ // with its dots published as slashes, which is what the kernel's
180
+ // derivation actually does and the reason the token noun declares no
181
+ // fallback at all.
182
+ for (const derived of ["legacy.borderRadius", "legacy/borderRadius"]) {
183
183
  assert.ok(
184
184
  !published.has(derived),
185
185
  `token_list must not publish ${derived}`,
@@ -265,15 +265,19 @@ export const readNounEvalCases: readonly EvalCase[] = [
265
265
  id: "content-canonical-graph-has-4-components",
266
266
  kind: "content",
267
267
  input:
268
- "the canonical fixture graph carries 4 ds:Component individuals (Button, Modal, LXD Panel, Beta Widget); block_list also surfaces the untiered Button Icon subcomponent (A2).",
268
+ 'the canonical fixture graph carries 4 ds:Component individuals (Button, Modal, LXD Panel, Beta Widget); block_list {tier:"all"} also surfaces the untiered Button Icon subcomponent (A2).',
269
269
  async expect() {
270
270
  await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
271
- const result = await mcp.callTool("block_list");
271
+ // `tier: "all"` because the claim is about the POPULATION, and the
272
+ // default tier scope answers from the top-level tiers alone — LXD Panel
273
+ // is two levels down. The scope's own narrowing is asserted where that
274
+ // is the claim (`behavioral/journeys.cli.test.ts`).
275
+ const result = await mcp.callTool("block_list", { tier: "all" });
272
276
  const names = (result.data as { name: string }[])
273
277
  .map((r) => r.name)
274
278
  .sort();
275
- // The declared list takes no arguments and filters nothing: the 4 tiered
276
- // components plus the untiered Button Icon subcomponent (A2).
279
+ // Every tier, and nothing else filtered: the 4 tiered components plus
280
+ // the untiered Button Icon subcomponent (A2).
277
281
  assert.deepEqual(names, [
278
282
  "Beta Widget",
279
283
  "Button",
@@ -422,7 +426,10 @@ export const readNounEvalCases: readonly EvalCase[] = [
422
426
  const normal = await namesUnder(CANONICAL_CONFIG);
423
427
  const prerelease = await namesUnder(ALL_VISIBLE_CONFIG);
424
428
  // The signed-off consequence: an experimental block is visible to
425
- // everyone, on every channel, until filtering returns in declared form.
429
+ // everyone, on every channel. The TIER scope narrows these reads (both
430
+ // configs set no tier, so both answer from the top-level tiers) and the
431
+ // channel narrows neither — which is exactly the asymmetry this case
432
+ // holds: the two answers are equal, and the beta block is in both.
426
433
  assert.ok(normal.includes("Beta Widget"));
427
434
  assert.deepEqual(normal, prerelease);
428
435
  },
@@ -456,6 +463,88 @@ export const readNounEvalCases: readonly EvalCase[] = [
456
463
  });
457
464
  },
458
465
  },
466
+ // The token-graph nouns run against the SHARED env rather than a fixture:
467
+ // the canonical fixture carries no token strata, and what these cases are
468
+ // for is that the SHIPPED pack answers them — the name literals both nouns
469
+ // key on ship with the pack, so a fixture would prove nothing about that.
470
+ {
471
+ id: "tool-token-lookup-dotted-name-resolves-with-its-definitions",
472
+ kind: "tool",
473
+ input:
474
+ "token_lookup {name:[color.text]} resolves the dotted name and carries definitions, coverage and values.",
475
+ async expect({ mcp }) {
476
+ const result = await mcp.callTool("token_lookup", {
477
+ name: ["color.text"],
478
+ });
479
+ assert.equal(result.ok, true);
480
+ const entity = (result.data as { results: Record<string, unknown>[] })
481
+ .results[0];
482
+ assert.equal(entity?.name, "color.text");
483
+ for (const section of ["definitions", "coverage", "values"]) {
484
+ assert.ok(
485
+ Array.isArray(entity?.[section]) &&
486
+ (entity[section] as unknown[]).length > 0,
487
+ `expected a non-empty ${section} array`,
488
+ );
489
+ }
490
+ },
491
+ },
492
+ {
493
+ id: "tool-variable-lookup-resolves-without-the-leading-dashes",
494
+ kind: "tool",
495
+ input:
496
+ "variable_lookup {name:[color-text]} resolves the dash-stripped CSS name and names the symbol it stands for.",
497
+ async expect({ mcp }) {
498
+ const result = await mcp.callTool("variable_lookup", {
499
+ name: ["color-text"],
500
+ });
501
+ assert.equal(result.ok, true);
502
+ const entity = (result.data as { results: Record<string, unknown>[] })
503
+ .results[0];
504
+ assert.equal(entity?.name, "color-text");
505
+ assert.equal(entity?.symbol, "color.text");
506
+ assert.ok(
507
+ Array.isArray(entity?.declarations) &&
508
+ (entity.declarations as unknown[]).length > 0,
509
+ "expected a non-empty declarations array",
510
+ );
511
+ },
512
+ },
513
+ {
514
+ id: "tool-token-values-narrows-to-one-symbol",
515
+ kind: "tool",
516
+ input:
517
+ "token_values {symbol:color.text} returns only that symbol's materialised positions, each with a value or a derivation.",
518
+ async expect({ mcp }) {
519
+ const result = await mcp.callTool("token_values", {
520
+ symbol: "color.text",
521
+ });
522
+ assert.equal(result.ok, true);
523
+ const rows = result.data as Record<string, string>[];
524
+ assert.ok(rows.length > 0, "expected at least one resolved value");
525
+ for (const row of rows) {
526
+ assert.equal(row.symbol, "color.text");
527
+ assert.ok(
528
+ row.value !== undefined || row.derivedFrom !== undefined,
529
+ "every row carries a value or a derivation",
530
+ );
531
+ }
532
+ },
533
+ },
534
+ {
535
+ id: "tool-token-consumers-answers-or-recovers",
536
+ kind: "tool",
537
+ input:
538
+ "token_consumers {} either returns binding rows or reports a recoverable empty result — never an error.",
539
+ async expect({ mcp }) {
540
+ // Deliberately not asserting emptiness: the design-system packs will
541
+ // record bindings, and a case pinning the empty answer would fail on
542
+ // that correct change. What must hold either way is that the call
543
+ // SUCCEEDS.
544
+ const result = await mcp.callTool("token_consumers", {});
545
+ assert.equal(result.ok, true);
546
+ },
547
+ },
459
548
  {
460
549
  id: "prompt-block-question-maps-to-block-lookup",
461
550
  kind: "prompt",
@@ -32,6 +32,12 @@ export const BLOCK_PREFIXES: Readonly<Record<string, string>> = {
32
32
  owl: "http://www.w3.org/2002/07/owl#",
33
33
  rdfs: "http://www.w3.org/2000/01/rdf-schema#",
34
34
  xsd: "http://www.w3.org/2001/XMLSchema#",
35
+ // The token-binding record spans three vocabularies exactly as the live
36
+ // graph does: the design system owns the record and its edges, the token
37
+ // ontology the symbol it consumes, and the anatomy DSL the style key and
38
+ // state it applies at.
39
+ anatomy: "https://anatomy.canonical.com/",
40
+ dt: "https://dt.canonical.com/",
35
41
  };
36
42
 
37
43
  /**
@@ -52,6 +58,8 @@ export const BLOCK_TTL = `
52
58
  @prefix owl: <http://www.w3.org/2002/07/owl#> .
53
59
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
54
60
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
61
+ @prefix anatomy: <https://anatomy.canonical.com/> .
62
+ @prefix dt: <https://dt.canonical.com/> .
55
63
 
56
64
  # ---- Ontology (TBox) ----
57
65
  ds:UIBlock a owl:Class .
@@ -87,6 +95,29 @@ ds:optional a owl:DatatypeProperty ; rdfs:domain ds:BlockProperty ; rdfs:range x
87
95
  # Domain is ds:Component (not ds:UIBlock) — reached via subtype scoping.
88
96
  ds:hasSubcomponent a owl:ObjectProperty ; rdfs:domain ds:Component ; rdfs:range ds:Subcomponent .
89
97
 
98
+ # The token-binding record, modelled with the live graph's split ownership —
99
+ # which is what makes the block lookup's \`tokens\` expand a SPARQL-lane one.
100
+ #
101
+ # The five ds: edges carry a domain and a range, so ke-graphql derives a field
102
+ # for each and a document could read them. \`anatomy:styleKey\` and
103
+ # \`anatomy:styleState\` carry NO rdfs:domain, because they belong to the anatomy
104
+ # vocabulary and apply to every styled thing rather than to this record — so no
105
+ # field is derived for either, and a generated document drops them in SILENCE.
106
+ # They are two of the six columns that IDENTIFY a binding, so dropping them
107
+ # leaves rows a reader cannot tell apart. Preserved here deliberately: change
108
+ # the fixture to declare a domain and the suite stops testing the condition the
109
+ # lane exists for.
110
+ ds:TokenBinding a owl:Class .
111
+ ds:hasTokenBinding a owl:ObjectProperty ; rdfs:domain ds:UIBlock ; rdfs:range ds:TokenBinding .
112
+ ds:consumesSymbol a owl:ObjectProperty, owl:FunctionalProperty ;
113
+ rdfs:domain ds:TokenBinding ; rdfs:range dt:TokenSymbol .
114
+ ds:viaBlock a owl:ObjectProperty ; rdfs:domain ds:TokenBinding ; rdfs:range ds:UIBlock .
115
+ ds:rank a owl:DatatypeProperty ; rdfs:domain ds:TokenBinding ; rdfs:range xsd:integer .
116
+ ds:node a owl:DatatypeProperty ; rdfs:domain ds:TokenBinding ; rdfs:range xsd:string .
117
+ dt:TokenSymbol a owl:Class .
118
+ anatomy:styleKey a owl:DatatypeProperty ; rdfs:range xsd:string .
119
+ anatomy:styleState a owl:DatatypeProperty ; rdfs:range xsd:string .
120
+
90
121
  # ---- Individuals (ABox) ----
91
122
  ds:global a ds:Tier ; ds:name "global" .
92
123
 
@@ -101,7 +132,9 @@ ds:button a ds:Component ;
101
132
  ds:figmaLink "https://figma.com/design/example/Button" ;
102
133
  ds:hasModifierFamily ds:family.importance, ds:family.density ;
103
134
  ds:hasProperty ds:button.prop.disabled ;
104
- ds:hasSubcomponent ds:button.icon .
135
+ ds:hasSubcomponent ds:button.icon ;
136
+ ds:hasTokenBinding ds:binding.button.bg1, ds:binding.button.bg2,
137
+ ds:binding.button.text, ds:binding.button.icon .
105
138
 
106
139
  ds:modal a ds:Component ;
107
140
  ds:name "Modal" ;
@@ -111,10 +144,49 @@ ds:modal a ds:Component ;
111
144
  ds:guidelines "Always provide an explicit close affordance." ;
112
145
  ds:anatomyDsl "root: dialog; children: header, body, footer" ;
113
146
  ds:hasModifierFamily ds:family.size ;
114
- ds:hasProperty ds:modal.prop.open .
147
+ ds:hasProperty ds:modal.prop.open ;
148
+ ds:hasTokenBinding ds:binding.modal.button .
115
149
 
116
150
  ds:button.icon a ds:Subcomponent ; ds:name "Button Icon" .
117
151
 
152
+ # The symbols the bindings consume, addressed by the dotted name the token
153
+ # noun publishes — which is what the expand's \`ds:consumesSymbol/rdfs:label\`
154
+ # path reads, so a row here is an address \`token lookup\` takes.
155
+ dt:color.background a dt:TokenSymbol ; rdfs:label "color.background" .
156
+ dt:color.background.hover a dt:TokenSymbol ; rdfs:label "color.background.hover" .
157
+ dt:color.text a dt:TokenSymbol ; rdfs:label "color.text" .
158
+ dt:color.icon a dt:TokenSymbol ; rdfs:label "color.icon" .
159
+
160
+ # Button's own tree. Two records share a node and a key and differ ONLY in
161
+ # state and rank — the pair that proves the two columns the GraphQL lane cannot
162
+ # see are the ones that tell the rows apart. The declaration order here is
163
+ # deliberately NOT the reading order (node, then key, then rank), so the
164
+ # expand's own ORDER BY is what puts them right.
165
+ ds:binding.button.bg2 a ds:TokenBinding ;
166
+ ds:consumesSymbol dt:color.background.hover ;
167
+ anatomy:styleKey "appearance.background" ; anatomy:styleState "hover" ;
168
+ ds:rank 2 ; ds:viaBlock ds:button ; ds:node "$root" .
169
+ ds:binding.button.bg1 a ds:TokenBinding ;
170
+ ds:consumesSymbol dt:color.background ;
171
+ anatomy:styleKey "appearance.background" ; anatomy:styleState "default" ;
172
+ ds:rank 1 ; ds:viaBlock ds:button ; ds:node "$root" .
173
+ ds:binding.button.text a ds:TokenBinding ;
174
+ ds:consumesSymbol dt:color.text ;
175
+ anatomy:styleKey "typography.color" ; anatomy:styleState "default" ;
176
+ ds:rank 1 ; ds:viaBlock ds:button ; ds:node "$root" .
177
+ # Reached through the subcomponent Button's anatomy embeds, so its via DIFFERS
178
+ # from the block and must print.
179
+ ds:binding.button.icon a ds:TokenBinding ;
180
+ ds:consumesSymbol dt:color.icon ;
181
+ anatomy:styleKey "appearance.background" ; anatomy:styleState "default" ;
182
+ ds:rank 1 ; ds:viaBlock ds:button.icon ; ds:node "$root/icon" .
183
+ # Modal consumes nothing of its own: its one record is Button's, reached
184
+ # through the Button its anatomy embeds.
185
+ ds:binding.modal.button a ds:TokenBinding ;
186
+ ds:consumesSymbol dt:color.background ;
187
+ anatomy:styleKey "appearance.background" ; anatomy:styleState "default" ;
188
+ ds:rank 1 ; ds:viaBlock ds:button ; ds:node "$root/footer" .
189
+
118
190
  ds:button.prop.disabled a ds:BlockProperty ;
119
191
  ds:name "disabled" ; ds:propertyType "boolean" ; ds:optional true .
120
192
  ds:modal.prop.open a ds:BlockProperty ;
@@ -176,10 +248,17 @@ ${ROSTER_MEMBERS}
176
248
  * parity, ontology and GraphQL-engine suites assert over the base graph and
177
249
  * must keep seeing exactly two components.
178
250
  *
179
- * It models the three shapes the addressing path gets wrong: two components
251
+ * It models the four shapes the addressing path gets wrong: two components
180
252
  * sharing one `ds:name` (declared zeta-first so the store's enumeration order
181
- * and IRI order DISAGREE), and a component carrying no `ds:name` at all
182
- * (reachable only by IRI, the way 131 of the 144 live code standards are).
253
+ * and IRI order DISAGREE), a component carrying no `ds:name` at all (reachable
254
+ * only by IRI, the way 131 of the 144 live code standards are), and a component
255
+ * whose `ds:name` is PADDED with a trailing space.
256
+ *
257
+ * That last one is copied from the live graph, not invented: 66 shipped names
258
+ * end in a space, carried across by the transform that reads them out of the
259
+ * source document, and `block lookup Timeline` used to answer
260
+ * ENTITY_NOT_FOUND while suggesting "Timeline " back. It is spelled with the
261
+ * live name so the fixture reads as the report it came from.
183
262
  *
184
263
  * The shared name is a SHAPE probe, not the live ambiguity: neither chip's tier
185
264
  * outranks the other, so this fixture can only prove that both are returned in a
@@ -203,4 +282,9 @@ ds:alpha.chip a ds:Component ;
203
282
  ds:nameless.widget a ds:Component ;
204
283
  ds:tier ds:global ;
205
284
  ds:summary "Carries no ds:name; addressable only by IRI." .
285
+
286
+ ds:padded.timeline a ds:Component ;
287
+ ds:name "Timeline " ;
288
+ ds:tier ds:global ;
289
+ ds:summary "Its ds:name carries the source document's trailing space." .
206
290
  `;