@canonical/pragma-cli 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +3 -2
  2. package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
  3. package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
  4. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
  5. package/bundled-skills/anatomy-author/SKILL.md +840 -137
  6. package/bundled-skills/design-auditor/SKILL.md +133 -9
  7. package/bundled-skills/implement-component/SKILL.md +192 -23
  8. package/bundled-skills/specify-component/SKILL.md +124 -12
  9. package/bundled-skills/specify-pattern/SKILL.md +47 -8
  10. package/dist/package.json +23 -20
  11. package/dist/pragma.conf.js +963 -40
  12. package/dist/pragma.conf.js.map +1 -1
  13. package/dist/src/capabilities/capabilities/catalog.js +14 -5
  14. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  15. package/dist/src/capabilities/capabilities/hints.js +33 -5
  16. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  17. package/dist/src/capabilities/config/show.render.js +30 -2
  18. package/dist/src/capabilities/config/show.render.js.map +1 -1
  19. package/dist/src/capabilities/create/create.verb.js +2 -1
  20. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  21. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  22. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  24. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  28. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
  30. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  31. package/dist/src/capabilities/graph/connect.render.js +101 -0
  32. package/dist/src/capabilities/graph/connect.render.js.map +1 -0
  33. package/dist/src/capabilities/graph/connect.types.js +21 -0
  34. package/dist/src/capabilities/graph/connect.types.js.map +1 -0
  35. package/dist/src/capabilities/graph/connect.verb.js +147 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
  37. package/dist/src/capabilities/graph/index.js +5 -3
  38. package/dist/src/capabilities/graph/index.js.map +1 -1
  39. package/dist/src/capabilities/graph/queryText.js +206 -0
  40. package/dist/src/capabilities/graph/queryText.js.map +1 -0
  41. package/dist/src/capabilities/graph/relationIndex.js +302 -0
  42. package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
  43. package/dist/src/capabilities/graph/runConnect.js +456 -0
  44. package/dist/src/capabilities/graph/runConnect.js.map +1 -0
  45. package/dist/src/capabilities/graph/runQuery.js +21 -4
  46. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  47. package/dist/src/capabilities/setup/buildPlan.js +6 -2
  48. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/index.js +7 -2
  50. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  52. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  54. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
  56. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  57. package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
  58. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
  60. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/writability.js +114 -0
  64. package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
  65. package/dist/src/capabilities/setup/plan.js.map +1 -1
  66. package/dist/src/capabilities/setup/setup.verb.js +80 -31
  67. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  68. package/dist/src/capabilities/setup/targets.js +124 -42
  69. package/dist/src/capabilities/setup/targets.js.map +1 -1
  70. package/dist/src/capabilities/shared/index.js +1 -1
  71. package/dist/src/capabilities/shared/index.js.map +1 -1
  72. package/dist/src/constants.js +7 -1
  73. package/dist/src/constants.js.map +1 -1
  74. package/dist/src/kernel/error/renderError.js +3 -2
  75. package/dist/src/kernel/error/renderError.js.map +1 -1
  76. package/dist/src/kernel/error/validOptions.js +41 -0
  77. package/dist/src/kernel/error/validOptions.js.map +1 -0
  78. package/dist/src/kernel/packs/compile.js +90 -6
  79. package/dist/src/kernel/packs/compile.js.map +1 -1
  80. package/dist/src/kernel/packs/cursor.js +135 -0
  81. package/dist/src/kernel/packs/cursor.js.map +1 -0
  82. package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
  83. package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
  84. package/dist/src/kernel/packs/paging.js +108 -0
  85. package/dist/src/kernel/packs/paging.js.map +1 -0
  86. package/dist/src/kernel/packs/renderPack.js +154 -5
  87. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  88. package/dist/src/kernel/packs/resolveEntity.js +118 -19
  89. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  90. package/dist/src/kernel/packs/runBodies.js +156 -18
  91. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  92. package/dist/src/kernel/packs/schema.js +160 -12
  93. package/dist/src/kernel/packs/schema.js.map +1 -1
  94. package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
  95. package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
  96. package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
  97. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
  98. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  100. package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
  101. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
  102. package/dist/src/kernel/packs/sparql/index.js +24 -14
  103. package/dist/src/kernel/packs/sparql/index.js.map +1 -1
  104. package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
  105. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  106. package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
  107. package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
  108. package/dist/src/kernel/packs/storyRules.js +151 -0
  109. package/dist/src/kernel/packs/storyRules.js.map +1 -0
  110. package/dist/src/kernel/packs/tierScope.js +310 -0
  111. package/dist/src/kernel/packs/tierScope.js.map +1 -0
  112. package/dist/src/kernel/packs/types.js +80 -0
  113. package/dist/src/kernel/packs/types.js.map +1 -1
  114. package/dist/src/kernel/project/cli/buildProgram.js +10 -1
  115. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  116. package/dist/src/kernel/project/cli/dispatch.js +104 -3
  117. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  118. package/dist/src/kernel/project/cli/rootHelp.js +8 -5
  119. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  120. package/dist/src/kernel/project/cli/suggestNames.js +31 -7
  121. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  122. package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
  123. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  124. package/dist/src/kernel/render/renderers.js +34 -2
  125. package/dist/src/kernel/render/renderers.js.map +1 -1
  126. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  127. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  128. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  129. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  130. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  131. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  132. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  133. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  134. package/dist/src/kernel/runtime/graphpack/read.js +17 -10
  135. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  136. package/dist/src/kernel/runtime/loadSession.js +3 -1
  137. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  138. package/dist/src/kernel/runtime/probe.js +1 -1
  139. package/dist/src/kernel/runtime/probe.js.map +1 -1
  140. package/dist/src/kernel/runtime/readEntity.js +19 -4
  141. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  142. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  143. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  144. package/dist/src/kernel/runtime/refs/resolve.js +38 -5
  145. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  146. package/dist/src/kernel/spec/emitReference.js +1 -1
  147. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  148. package/package.json +23 -20
  149. package/pragma.conf.ts +983 -40
  150. package/src/capabilities/block.parity.test.ts +207 -27
  151. package/src/capabilities/block.shipped.exec.test.ts +156 -14
  152. package/src/capabilities/block.tierRank.exec.test.ts +46 -9
  153. package/src/capabilities/capabilities/catalog.ts +14 -5
  154. package/src/capabilities/capabilities/hints.ts +40 -5
  155. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  156. package/src/capabilities/config/set.test.ts +24 -25
  157. package/src/capabilities/config/show.render.test.ts +9 -4
  158. package/src/capabilities/config/show.render.ts +30 -2
  159. package/src/capabilities/create/byteEquality.test.ts +28 -1
  160. package/src/capabilities/create/create.test.ts +1 -0
  161. package/src/capabilities/create/create.verb.ts +2 -1
  162. package/src/capabilities/create/createGrammar.test.ts +4 -1
  163. package/src/capabilities/create/createSurface.generated.ts +23 -1
  164. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  165. package/src/capabilities/distribution.parity.test.ts +12 -11
  166. package/src/capabilities/distribution.test.ts +77 -7
  167. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  168. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  169. package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
  170. package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
  171. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  172. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  173. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  174. package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
  175. package/src/capabilities/doctor/doctor.test.ts +327 -36
  176. package/src/capabilities/emptyRecovery.test.ts +84 -28
  177. package/src/capabilities/graph/connect.render.ts +141 -0
  178. package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
  179. package/src/capabilities/graph/connect.test.ts +361 -0
  180. package/src/capabilities/graph/connect.types.ts +136 -0
  181. package/src/capabilities/graph/connect.verb.ts +165 -0
  182. package/src/capabilities/graph/index.ts +5 -3
  183. package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
  184. package/src/capabilities/graph/query.test.ts +17 -0
  185. package/src/capabilities/graph/queryText.test.ts +121 -0
  186. package/src/capabilities/graph/queryText.ts +240 -0
  187. package/src/capabilities/graph/relationIndex.ts +372 -0
  188. package/src/capabilities/graph/runConnect.ts +577 -0
  189. package/src/capabilities/graph/runQuery.ts +21 -6
  190. package/src/capabilities/lazy.test.ts +32 -2
  191. package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
  192. package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
  193. package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
  194. package/src/capabilities/resources/resources.test.ts +38 -2
  195. package/src/capabilities/setup/buildPlan.ts +7 -1
  196. package/src/capabilities/setup/operations/index.ts +14 -0
  197. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  198. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  199. package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
  200. package/src/capabilities/setup/operations/setupLsp.ts +544 -85
  201. package/src/capabilities/setup/operations/setupMcp.ts +212 -4
  202. package/src/capabilities/setup/operations/setupSkills.ts +7 -2
  203. package/src/capabilities/setup/operations/writability.test.ts +151 -0
  204. package/src/capabilities/setup/operations/writability.ts +135 -0
  205. package/src/capabilities/setup/plan.ts +11 -0
  206. package/src/capabilities/setup/setup.test.ts +992 -16
  207. package/src/capabilities/setup/setup.verb.ts +84 -36
  208. package/src/capabilities/setup/targets.ts +152 -48
  209. package/src/capabilities/setup/types.ts +41 -5
  210. package/src/capabilities/shared/index.ts +1 -0
  211. package/src/capabilities/sources/sources.test.ts +55 -0
  212. package/src/capabilities/surface.test.ts +59 -11
  213. package/src/capabilities/tier.parity.test.ts +2 -2
  214. package/src/capabilities/token.parity.test.ts +784 -0
  215. package/src/capabilities/tokenGraph.cli.test.ts +279 -0
  216. package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
  217. package/src/capabilities/variable.parity.test.ts +448 -0
  218. package/src/constants.ts +8 -0
  219. package/src/kernel/completion/shellDrive.test.ts +32 -30
  220. package/src/kernel/config/defaults.test.ts +21 -2
  221. package/src/kernel/config/readConfig.test.ts +1 -1
  222. package/src/kernel/error/renderError.ts +3 -2
  223. package/src/kernel/error/validOptions.test.ts +57 -0
  224. package/src/kernel/error/validOptions.ts +45 -0
  225. package/src/kernel/packs/collect.test.ts +2 -0
  226. package/src/kernel/packs/compile.test.ts +188 -15
  227. package/src/kernel/packs/compile.ts +109 -14
  228. package/src/kernel/packs/cursor.ts +135 -0
  229. package/src/kernel/packs/filteredEmpty.test.ts +213 -0
  230. package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
  231. package/src/kernel/packs/graphqlSource.test.ts +152 -0
  232. package/src/kernel/packs/listNotice.test.ts +156 -0
  233. package/src/kernel/packs/paging.ts +110 -0
  234. package/src/kernel/packs/renderPack.ts +186 -13
  235. package/src/kernel/packs/resolveEntity.test.ts +62 -3
  236. package/src/kernel/packs/resolveEntity.ts +186 -26
  237. package/src/kernel/packs/runBodies.ts +196 -26
  238. package/src/kernel/packs/schema.ts +180 -13
  239. package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
  240. package/src/kernel/packs/sparql/authorQuery.ts +257 -0
  241. package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
  242. package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
  243. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
  244. package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
  245. package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
  246. package/src/kernel/packs/sparql/filterValues.ts +182 -0
  247. package/src/kernel/packs/sparql/index.ts +30 -14
  248. package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
  249. package/src/kernel/packs/sparql/runSelect.ts +42 -2
  250. package/src/kernel/packs/sparql/searchTerm.ts +29 -0
  251. package/src/kernel/packs/storyRules.ts +180 -0
  252. package/src/kernel/packs/tierScope.test.ts +487 -0
  253. package/src/kernel/packs/tierScope.ts +409 -0
  254. package/src/kernel/packs/toolDescription.test.ts +13 -10
  255. package/src/kernel/packs/types.ts +311 -9
  256. package/src/kernel/project/cli/buildProgram.ts +17 -1
  257. package/src/kernel/project/cli/dispatch.test.ts +87 -0
  258. package/src/kernel/project/cli/dispatch.ts +114 -4
  259. package/src/kernel/project/cli/rootHelp.ts +8 -5
  260. package/src/kernel/project/cli/suggest.test.ts +35 -0
  261. package/src/kernel/project/cli/suggestNames.ts +34 -7
  262. package/src/kernel/project/mcp/registerVerb.ts +5 -1
  263. package/src/kernel/render/contracts.ts +2 -0
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.test.ts +43 -0
  266. package/src/kernel/render/renderers.ts +34 -1
  267. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  268. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  269. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  270. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  271. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  272. package/src/kernel/runtime/graphpack/read.ts +17 -13
  273. package/src/kernel/runtime/loadSession.ts +3 -1
  274. package/src/kernel/runtime/probe.ts +1 -1
  275. package/src/kernel/runtime/readEntity.ts +19 -4
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +39 -5
  279. package/src/kernel/runtime/types.ts +30 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  282. package/src/kernel/spec/types.ts +17 -0
  283. package/src/testing/PARITY_GAPS.ts +4 -4
  284. package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
  285. package/src/testing/behavioral/completion.test.ts +7 -6
  286. package/src/testing/behavioral/disclosure.test.ts +39 -37
  287. package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
  288. package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
  289. package/src/testing/behavioral/journeys.cli.test.ts +71 -32
  290. package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
  291. package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
  292. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
  293. package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
  294. package/src/testing/behavioral/mcpSurface.test.ts +28 -25
  295. package/src/testing/behavioral/parity.test.ts +25 -23
  296. package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
  297. package/src/testing/eval/cases/effectful.ts +7 -1
  298. package/src/testing/eval/cases/readNouns.ts +100 -11
  299. package/src/testing/fixtures/blockGraph.ts +89 -5
  300. package/src/testing/fixtures/connectGraph.ts +159 -0
  301. package/src/testing/fixtures/graph/canonical.ts +128 -32
  302. package/src/testing/helpers/fsProbe.ts +88 -0
  303. package/src/testing/helpers/runCli.test.ts +44 -0
  304. package/src/testing/helpers/runCli.ts +27 -3
  305. package/src/testing/perf/budgets.test.ts +3 -1
  306. package/src/testing/perf/budgets.ts +138 -54
  307. package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
  308. package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
  309. package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
  310. package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
  311. package/src/kernel/packs/sparql/applyFilters.ts +0 -203
  312. package/src/kernel/packs/sparql/applySearch.ts +0 -33
@@ -0,0 +1,653 @@
1
+ /**
2
+ * The `token` and `variable` stories, EXECUTED against the shipped pack
3
+ * (PROTECTED).
4
+ *
5
+ * `token.parity.test.ts` and `variable.parity.test.ts` assert the stories'
6
+ * semantics over fixture graphs this repository authors. That is a real check,
7
+ * but a fixture obligingly asserts whatever the story reads — it can prove the
8
+ * story agrees with the fixture and nothing about the corpus a release actually
9
+ * ships. So this file boots the shipped graph and asserts the two things a
10
+ * fixture structurally cannot.
11
+ *
12
+ * FIRST, that every graph term the two stories name is DEFINED by the shipped
13
+ * ontology. Defined, not asserted, and the difference is the whole point: a
14
+ * term the ontology never defines is a story bug that can never render, on any
15
+ * entity, on any install — the defect that let `block lookup` publish no usage
16
+ * narrative for every block on every install, silently, because a name that
17
+ * maps onto no property is exactly what OPTIONAL parity swallows. A term the
18
+ * ontology defines that no instance asserts yet is a content gap and not this
19
+ * suite's business.
20
+ *
21
+ * SECOND, the corpus INVARIANTS the two nouns' addressability rests on: that
22
+ * every symbol and every variable carries exactly one name literal, that the
23
+ * dash-stripping which makes a variable typable is injective and collides with
24
+ * no symbol name, and that a resolved value carries exactly one of a chain or a
25
+ * derivation. Each of those is a property of the whole population, so each is
26
+ * asserted as a count of VIOLATIONS being zero rather than as a row count the
27
+ * next upstream release would move. The populations themselves are deliberately
28
+ * not pinned: upstream adds symbols, and a test that failed because it did
29
+ * would be a test of the wrong thing.
30
+ */
31
+
32
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
33
+ import { compileStoryModule } from "../kernel/packs/compile.js";
34
+ import { MAX_LIST_WINDOW } from "../kernel/packs/paging.js";
35
+ import {
36
+ distributionSource,
37
+ isNestedExpand,
38
+ type PackExpandSelect,
39
+ type PackLookup,
40
+ type PackPage,
41
+ } from "../kernel/packs/types.js";
42
+ import { verbKey } from "../kernel/packs/uniqueness.js";
43
+ import { bootRuntime } from "../kernel/runtime/boot.js";
44
+ import type { PragmaRuntime } from "../kernel/runtime/types.js";
45
+ import type { CapabilityModule, VerbSpec } from "../kernel/spec/types.js";
46
+ import { TEST_FLAGS } from "../testing/helpers/projectCli.js";
47
+ import { declaredStories } from "./distribution.js";
48
+
49
+ const SOURCE = distributionSource("pragma.conf.ts");
50
+
51
+ /** The two stories this file owns, and their compiled modules. */
52
+ const NOUNS = ["token", "variable"] as const;
53
+
54
+ const modules = new Map<string, CapabilityModule>(
55
+ NOUNS.map((noun) => {
56
+ const story = declaredStories.get(noun);
57
+ if (!story) throw new Error(`pragma.conf.ts declares no "${noun}" story`);
58
+ return [noun, compileStoryModule(story, SOURCE, {})];
59
+ }),
60
+ );
61
+
62
+ const lookups = new Map<string, PackLookup>(
63
+ NOUNS.map((noun) => {
64
+ const lookup = declaredStories.get(noun)?.lookup;
65
+ if (!lookup) throw new Error(`the "${noun}" story declares no lookup`);
66
+ return [noun, lookup];
67
+ }),
68
+ );
69
+
70
+ function verb(noun: string, name: string): VerbSpec {
71
+ const found = modules
72
+ .get(noun)
73
+ ?.verbs.find((v) => verbKey(v.path) === `${noun} ${name}`);
74
+ if (!found) throw new Error(`no compiled "${noun} ${name}" verb`);
75
+ return found;
76
+ }
77
+
78
+ /** One graph term a story names, paired with the site that declares it. */
79
+ interface DeclaredTerm {
80
+ readonly what: string;
81
+ readonly term: string;
82
+ }
83
+
84
+ /**
85
+ * Standard RDF vocabulary, which no domain ontology declares and every graph
86
+ * carries. The distribution's own vocabulary declaration leaves `rdfs:label`
87
+ * and `rdfs:comment` out for exactly this reason — "the kernel treats standard
88
+ * vocabulary as universal" — and the list walk that reads an `rdf:List` is the
89
+ * same case: `rdf:first` and `rdf:rest` are the list's own structure, not terms
90
+ * a token ontology could define.
91
+ *
92
+ * Skipped here rather than admitted by a looser ASK, so the sweep below stays a
93
+ * hard requirement on every DOMAIN term and this exemption is a readable list
94
+ * rather than a widened predicate.
95
+ */
96
+ const STANDARD_VOCABULARY = new Set([
97
+ "rdf:first",
98
+ "rdf:rest",
99
+ "rdfs:label",
100
+ "rdfs:comment",
101
+ ]);
102
+
103
+ /**
104
+ * The individual property steps of a declared term.
105
+ *
106
+ * A declared term may be a property PATH, and the shipped stories use three
107
+ * shapes of one: an inverse step (`^dt:symbol`, from a symbol back to the
108
+ * definitions that point at it), a sequence (`dt:under/dt-web:selector`), and a
109
+ * list walk, whose middle step carries a zero-or-more marker. An ASK over the path
110
+ * itself is not the question — the question is whether each STEP names a
111
+ * property the ontology defines — and a path spliced into an ASK's subject
112
+ * position would not even parse.
113
+ */
114
+ function steps(term: string): string[] {
115
+ return term
116
+ .split("/")
117
+ .map((step) => step.replace(/^\^/, "").replace(/[*+?]$/, ""))
118
+ .filter((step) => step !== "" && !STANDARD_VOCABULARY.has(step));
119
+ }
120
+
121
+ /** Every term an `expand.select` names, to whatever depth it nests. */
122
+ function selectedTerms(
123
+ site: string,
124
+ select: readonly PackExpandSelect[],
125
+ ): readonly DeclaredTerm[] {
126
+ return select.flatMap((entry) =>
127
+ isNestedExpand(entry)
128
+ ? [
129
+ { what: `${site} → nested "${entry.name}"`, term: entry.relation },
130
+ ...selectedTerms(`${site} → "${entry.name}"`, entry.select),
131
+ ]
132
+ : [{ what: `${site} → field "${entry.name}"`, term: entry.property }],
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Every property the two stories name in the graph, read from the declaration
138
+ * rather than listed — a term added to any field, section or expand is covered
139
+ * the moment it is declared, by the author who declares it.
140
+ *
141
+ * The LOOKUP half only. A list query is author TEXT the kernel does not
142
+ * compose, so its terms cannot be enumerated from the declaration; what guards
143
+ * those is that the list must answer rows at all, which the cross-noun
144
+ * round-trip sweep asserts for every noun with both halves.
145
+ */
146
+ const DECLARED_TERMS: readonly DeclaredTerm[] = NOUNS.flatMap((noun) => {
147
+ const lookup = lookups.get(noun) as PackLookup;
148
+ return [
149
+ { what: `${noun} lookup \`by\``, term: lookup.by },
150
+ ...(lookup.fields ?? []).map((f) => ({
151
+ what: `${noun} field "${f.name}"`,
152
+ term: f.property,
153
+ })),
154
+ ...(lookup.sections ?? []).map((s) => ({
155
+ what: `${noun} section "${s.name}"`,
156
+ term: s.property,
157
+ })),
158
+ ...(lookup.expand ?? []).flatMap((e) => [
159
+ { what: `${noun} expand "${e.name}"`, term: e.relation },
160
+ ...selectedTerms(`${noun} expand "${e.name}"`, e.select),
161
+ ]),
162
+ ];
163
+ });
164
+
165
+ /** One case per (declaration site, property step). */
166
+ const DECLARED_STEPS: readonly { what: string; property: string }[] =
167
+ DECLARED_TERMS.flatMap(({ what, term }) =>
168
+ steps(term).map((property) => ({ what, property })),
169
+ );
170
+
171
+ // ONE store for the file — booting the shipped pack is the expensive part, and
172
+ // this package already contends under parallel runs.
173
+ let rt: PragmaRuntime;
174
+ beforeAll(async () => {
175
+ rt = bootRuntime(TEST_FLAGS);
176
+ await rt.store.get();
177
+ }, 60_000);
178
+ afterAll(async () => {
179
+ (await rt.store.get()).store.dispose();
180
+ });
181
+
182
+ /** One scalar from the shipped graph, queried directly. */
183
+ async function count(query: string): Promise<number> {
184
+ const result = await rt.query.sparql(query);
185
+ if (result.type !== "select") throw new Error("expected a SELECT");
186
+ return Number(Object.values(result.bindings[0] ?? {})[0] ?? Number.NaN);
187
+ }
188
+
189
+ /** The rows of a list-shaped verb over the shipped pack. */
190
+ async function rows(
191
+ noun: string,
192
+ name: string,
193
+ params: Record<string, unknown> = {},
194
+ ): Promise<Record<string, string>[]> {
195
+ const page = (await verb(noun, name).run(params, rt)) as PackPage;
196
+ return [...page.rows];
197
+ }
198
+
199
+ /** One looked-up entity from the shipped pack. */
200
+ async function lookup(
201
+ noun: string,
202
+ name: string,
203
+ ): Promise<Record<string, unknown>> {
204
+ const out = (await verb(noun, "lookup").run({ name: [name] }, rt)) as {
205
+ errors: unknown[];
206
+ results: Record<string, unknown>[];
207
+ };
208
+ expect(out.errors).toEqual([]);
209
+ return out.results[0] as Record<string, unknown>;
210
+ }
211
+
212
+ describe("every declared term is DEFINED by the shipped ontology (PROTECTED)", () => {
213
+ it("derives the sweep from the declarations rather than a list", () => {
214
+ // A guard, not a no-op: an empty derivation would make every case below
215
+ // vacuous. Both nouns declare a `by`, fields and expands, so the floor is
216
+ // comfortably above zero without pinning a number that moves whenever a
217
+ // field is added.
218
+ expect(DECLARED_STEPS.length).toBeGreaterThanOrEqual(15);
219
+ expect(DECLARED_TERMS.map((t) => t.what)).toContain("token lookup `by`");
220
+ expect(DECLARED_TERMS.map((t) => t.what)).toContain("variable lookup `by`");
221
+ });
222
+
223
+ it.each(DECLARED_STEPS)(
224
+ "$what reads $property, which the shipped ontology defines",
225
+ async ({ what, property }) => {
226
+ const result = await rt.query.sparql(
227
+ [
228
+ "ASK {",
229
+ ` ${property} a ?kind .`,
230
+ " VALUES ?kind { owl:DatatypeProperty owl:ObjectProperty }",
231
+ "}",
232
+ ].join("\n"),
233
+ );
234
+ expect(result.type).toBe("ask");
235
+ expect(
236
+ result.type === "ask" ? result.result : false,
237
+ `${what} reads ${property}, which the shipped ontology does not define — it can never render, for any entity, on any install`,
238
+ ).toBe(true);
239
+ },
240
+ );
241
+ });
242
+
243
+ describe("the name literal both nouns key on (PROTECTED)", () => {
244
+ it("every symbol carries exactly one label", async () => {
245
+ // Both halves of the grammar REQUIRE it and neither declares a fallback,
246
+ // so a symbol without one is published by neither and a symbol with two
247
+ // would be published twice under different names.
248
+ expect(
249
+ await count(
250
+ `SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE {
251
+ ?s a dt:TokenSymbol .
252
+ FILTER NOT EXISTS { ?s rdfs:label ?l }
253
+ }`,
254
+ ),
255
+ ).toBe(0);
256
+ expect(
257
+ await count(
258
+ `SELECT (COUNT(?s) AS ?n) WHERE {
259
+ SELECT ?s (COUNT(DISTINCT ?l) AS ?labels) WHERE {
260
+ ?s a dt:TokenSymbol ; rdfs:label ?l .
261
+ } GROUP BY ?s HAVING (COUNT(DISTINCT ?l) > 1)
262
+ }`,
263
+ ),
264
+ ).toBe(0);
265
+ });
266
+
267
+ it("every variable carries exactly one label", async () => {
268
+ expect(
269
+ await count(
270
+ `SELECT (COUNT(DISTINCT ?v) AS ?n) WHERE {
271
+ ?v a dt:Variable .
272
+ FILTER NOT EXISTS { ?v rdfs:label ?l }
273
+ }`,
274
+ ),
275
+ ).toBe(0);
276
+ expect(
277
+ await count(
278
+ `SELECT (COUNT(?v) AS ?n) WHERE {
279
+ SELECT ?v (COUNT(DISTINCT ?l) AS ?labels) WHERE {
280
+ ?v a dt:Variable ; rdfs:label ?l .
281
+ } GROUP BY ?v HAVING (COUNT(DISTINCT ?l) > 1)
282
+ }`,
283
+ ),
284
+ ).toBe(0);
285
+ });
286
+
287
+ it("a variable's label IS its CSS name with the leading dashes stripped", async () => {
288
+ // This is what makes the noun addressable at all — a `--`-prefixed
289
+ // positional cannot be typed — and it is upstream's promise rather than
290
+ // this repository's derivation. Asserted as zero violations, so it holds
291
+ // however many variables upstream ships.
292
+ expect(
293
+ await count(
294
+ `SELECT (COUNT(?v) AS ?n) WHERE {
295
+ ?v a dt:Variable ; rdfs:label ?l .
296
+ BIND(REPLACE(REPLACE(STR(?v), "^.*[/#]", ""), "^--", "") AS ?stripped)
297
+ FILTER(?l != ?stripped)
298
+ }`,
299
+ ),
300
+ ).toBe(0);
301
+ });
302
+
303
+ it("a symbol's label IS its dotted local name, dots intact", async () => {
304
+ // The reason no IRI-derived fallback is declared: the kernel's derivation
305
+ // publishes these dots as SLASHES, which would disagree with the notation
306
+ // the label uses and the anatomy spells. Pinning the agreement is what
307
+ // makes that a considered omission rather than a guess.
308
+ expect(
309
+ await count(
310
+ `SELECT (COUNT(?s) AS ?n) WHERE {
311
+ ?s a dt:TokenSymbol ; rdfs:label ?l .
312
+ BIND(REPLACE(STR(?s), "^.*[/#]", "") AS ?derived)
313
+ FILTER(?l != ?derived)
314
+ }`,
315
+ ),
316
+ ).toBe(0);
317
+ });
318
+
319
+ it("the stripping is INJECTIVE — no two variables answer to one name", async () => {
320
+ // If two ever collided, a lookup would answer one and silently hide the
321
+ // other, which is the shape of defect the round-trip guard exists for.
322
+ const variables = await count(
323
+ "SELECT (COUNT(DISTINCT ?v) AS ?n) WHERE { ?v a dt:Variable }",
324
+ );
325
+ const names = await count(
326
+ "SELECT (COUNT(DISTINCT ?l) AS ?n) WHERE { ?v a dt:Variable ; rdfs:label ?l }",
327
+ );
328
+ expect(names).toBe(variables);
329
+ });
330
+
331
+ it("no variable name collides with a symbol name", async () => {
332
+ // The two nouns share one flat namespace as far as a caller is concerned,
333
+ // so a shared name would make `token lookup X` and `variable lookup X`
334
+ // answer different entities under one spelling.
335
+ expect(
336
+ await count(
337
+ `SELECT (COUNT(*) AS ?n) WHERE {
338
+ ?v a dt:Variable ; rdfs:label ?name .
339
+ ?s a dt:TokenSymbol ; rdfs:label ?name .
340
+ }`,
341
+ ),
342
+ ).toBe(0);
343
+ });
344
+ });
345
+
346
+ describe("a resolved value carries a chain XOR a derivation (PROTECTED)", () => {
347
+ it("never both, and never neither", async () => {
348
+ // The shape's own rule, and the reason both S3 surfaces select both: a
349
+ // surface reading only the chain renders every derived routing blank, and
350
+ // one reading only the derivation renders every ordinary value blank.
351
+ expect(
352
+ await count(
353
+ `SELECT (COUNT(?r) AS ?n) WHERE {
354
+ ?r a dt:ResolvedValue .
355
+ OPTIONAL { ?r dt:resolutionChain ?c }
356
+ OPTIONAL { ?r dt:derivedFrom ?d }
357
+ FILTER((BOUND(?c) && BOUND(?d)) || (!BOUND(?c) && !BOUND(?d)))
358
+ }`,
359
+ ),
360
+ ).toBe(0);
361
+ });
362
+
363
+ it("a derived routing carries NO value of its own", async () => {
364
+ expect(
365
+ await count(
366
+ `SELECT (COUNT(?r) AS ?n) WHERE {
367
+ ?r a dt:ResolvedValue ; dt:derivedFrom ?d ; dt:resolvesTo ?v .
368
+ }`,
369
+ ),
370
+ ).toBe(0);
371
+ });
372
+
373
+ it("both kinds are present, so neither branch is untested here", async () => {
374
+ // Without this the two assertions above would pass vacuously on a corpus
375
+ // that had lost one kind entirely.
376
+ expect(
377
+ await count(
378
+ "SELECT (COUNT(?r) AS ?n) WHERE { ?r dt:resolutionChain ?c }",
379
+ ),
380
+ ).toBeGreaterThan(0);
381
+ expect(
382
+ await count("SELECT (COUNT(?r) AS ?n) WHERE { ?r dt:derivedFrom ?d }"),
383
+ ).toBeGreaterThan(0);
384
+ });
385
+ });
386
+
387
+ describe("a symbol's own definition is its base chain's HEAD (PROTECTED)", () => {
388
+ // What `token list`'s two definition-level columns rest on, and what a
389
+ // fixture cannot vouch for. The story reads the head of the symbol's BASE
390
+ // resolved value — the one with no `dt:coordinate` — so three things have to
391
+ // hold over the corpus: that there is at most one such value per symbol, that
392
+ // its head really is a definition OF that symbol, and that the heads of a
393
+ // symbol's OTHER values never name a different type or description, because
394
+ // `token lookup` reads the same pair as a property path and a path cannot
395
+ // spell the base restriction.
396
+ it("no symbol has two base resolved values", async () => {
397
+ // If one ever did, the list would publish the symbol twice — or, with the
398
+ // `DISTINCT` that guards it, silently drop whichever head sorted second.
399
+ expect(
400
+ await count(
401
+ `SELECT (COUNT(*) AS ?n) WHERE {
402
+ SELECT ?s WHERE {
403
+ ?v a dt:ResolvedValue ; dt:forSymbol ?s .
404
+ FILTER NOT EXISTS { ?v dt:coordinate ?c }
405
+ } GROUP BY ?s HAVING (COUNT(?v) > 1)
406
+ }`,
407
+ ),
408
+ ).toBe(0);
409
+ });
410
+
411
+ it("a chain head is a definition OF the symbol the value is for", async () => {
412
+ // The claim that makes "its own definition" true rather than merely
413
+ // convenient: the head is reached through the value, not through
414
+ // `dt:symbol`, so nothing in the path itself guarantees it points back.
415
+ expect(
416
+ await count(
417
+ `SELECT (COUNT(*) AS ?n) WHERE {
418
+ ?v a dt:ResolvedValue ;
419
+ dt:forSymbol ?s ;
420
+ dt:resolutionChain/rdf:first ?head .
421
+ FILTER NOT EXISTS { ?head dt:symbol ?s }
422
+ }`,
423
+ ),
424
+ ).toBe(0);
425
+ });
426
+
427
+ it("a symbol's chain heads never name two types, or two descriptions", async () => {
428
+ // This is what licenses the lookup's unrestricted path. A symbol with
429
+ // values at several positions has several heads (354 of them do today); as
430
+ // long as they agree on these two fields, the path is one value repeated
431
+ // and the first row the resolver keeps is the same answer the list gives.
432
+ for (const property of [
433
+ "dt:tokenType/rdfs:label",
434
+ "w3c-tokens:description",
435
+ ]) {
436
+ expect(
437
+ await count(
438
+ `SELECT (COUNT(*) AS ?n) WHERE {
439
+ SELECT ?s WHERE {
440
+ ?v dt:forSymbol ?s ; dt:resolutionChain/rdf:first/${property} ?o .
441
+ } GROUP BY ?s HAVING (COUNT(DISTINCT ?o) > 1)
442
+ }`,
443
+ ),
444
+ property,
445
+ ).toBe(0);
446
+ }
447
+ });
448
+
449
+ it("the population is there, so none of the three passes vacuously", async () => {
450
+ expect(
451
+ await count(
452
+ `SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE {
453
+ ?v a dt:ResolvedValue ;
454
+ dt:forSymbol ?s ;
455
+ dt:resolutionChain/rdf:first ?head .
456
+ FILTER NOT EXISTS { ?v dt:coordinate ?c }
457
+ }`,
458
+ ),
459
+ ).toBeGreaterThan(0);
460
+ expect(
461
+ await count(
462
+ `SELECT (COUNT(*) AS ?n) WHERE {
463
+ SELECT ?s WHERE { ?v dt:forSymbol ?s ; dt:resolutionChain ?ch }
464
+ GROUP BY ?s HAVING (COUNT(?v) > 1)
465
+ }`,
466
+ ),
467
+ ).toBeGreaterThan(0);
468
+ });
469
+
470
+ it("token list and token lookup publish the SAME pair for one symbol", async () => {
471
+ // One rule, two spellings — the list's own SPARQL and the lookup's
472
+ // property path — so this is the seam a divergence would open at.
473
+ const listed = (
474
+ (await verb("token", "list").run(
475
+ { search: "color.text" },
476
+ rt,
477
+ )) as PackPage
478
+ ).rows.find((row) => row.name === "color.text");
479
+ const looked = await lookup("token", "color.text");
480
+ expect(listed?.type).toBeTruthy();
481
+ expect(listed?.description).toBeTruthy();
482
+ expect(looked.type).toBe(listed?.type);
483
+ expect(looked.description).toBe(listed?.description);
484
+ });
485
+ });
486
+
487
+ describe("the shipped nouns answer, end to end (PROTECTED)", () => {
488
+ it("token list publishes rows, and every row is named and typed or blank", async () => {
489
+ const page = (await verb("token", "list").run(
490
+ { limit: 50 },
491
+ rt,
492
+ )) as PackPage;
493
+ expect(page.rows.length).toBeGreaterThan(0);
494
+ for (const row of page.rows) {
495
+ expect(row.name, JSON.stringify(row)).toBeTruthy();
496
+ // The column's contract: `type` is the label its own definition carries,
497
+ // or nothing at all. It is never a partial guess, and never an IRI.
498
+ expect(row.type ?? "").not.toContain("://");
499
+ }
500
+ });
501
+
502
+ it("a dotted symbol name resolves, with its definitions shown apart", async () => {
503
+ const symbol = await lookup("token", "color.text");
504
+ expect(symbol).toMatchObject({ name: "color.text" });
505
+ const definitions = symbol.definitions as Record<string, string>[];
506
+ // Many definitions is the fact that makes "the symbol's own" a real
507
+ // choice, and this expand is the only surface that shows them apart.
508
+ // Asserted as a floor, not a count: upstream adds modifier contexts.
509
+ expect(definitions.length).toBeGreaterThan(1);
510
+ expect(new Set(definitions.map((d) => d.type))).toEqual(new Set(["color"]));
511
+ // Coverage reads the family end of the relation; values read the other.
512
+ expect((symbol.coverage as unknown[]).length).toBeGreaterThan(0);
513
+ expect((symbol.values as unknown[]).length).toBeGreaterThan(0);
514
+ });
515
+
516
+ it("a channel names the symbol it provisions and resolves by derivation", async () => {
517
+ const channel = await lookup("token", "modifier.color.text");
518
+ expect(channel).toMatchObject({ channelOf: "color.text" });
519
+ const values = channel.values as Record<string, string>[];
520
+ expect(values.length).toBeGreaterThan(0);
521
+ for (const value of values) {
522
+ expect(value.derivedFrom, JSON.stringify(value)).toBeTruthy();
523
+ expect(value.value).toBeUndefined();
524
+ }
525
+ });
526
+
527
+ it("token values answers a symbol's materialised positions", async () => {
528
+ const answered = await rows("token", "values", { symbol: "color.text" });
529
+ expect(answered.length).toBeGreaterThan(0);
530
+ for (const row of answered) expect(row.symbol).toBe("color.text");
531
+ // The default position is a row with no coordinate; the mode coordinate is
532
+ // one the resolver materialises. Both spellings, neither an IRI.
533
+ expect(answered.some((row) => row.position === undefined)).toBe(true);
534
+ for (const row of answered) {
535
+ expect(row.position ?? "").not.toContain("://");
536
+ }
537
+ });
538
+
539
+ it("a dash-stripped variable name resolves, with its declarations", async () => {
540
+ const variable = await lookup("variable", "color-text");
541
+ expect(variable).toMatchObject({
542
+ name: "color-text",
543
+ symbol: "color.text",
544
+ });
545
+ const declarations = variable.declarations as Record<string, string>[];
546
+ expect(declarations.length).toBeGreaterThan(0);
547
+ // The web's own bridge fields, which nothing read before this story: the
548
+ // selector a declaration sits under and the at-rule stack around it.
549
+ expect(declarations.some((d) => d.selector !== undefined)).toBe(true);
550
+ expect(declarations.some((d) => d.inAtRule !== undefined)).toBe(true);
551
+ expect(declarations.some((d) => d.emits !== undefined)).toBe(true);
552
+ });
553
+
554
+ it("two spellings of one symbol each answer their own row", async () => {
555
+ const out = (await verb("variable", "lookup").run(
556
+ { name: ["typography-weight-semiBold", "typography-weight-semi-bold"] },
557
+ rt,
558
+ )) as { errors: unknown[]; results: Record<string, unknown>[] };
559
+ expect(out.errors).toEqual([]);
560
+ expect(out.results).toHaveLength(2);
561
+ for (const result of out.results) {
562
+ expect(result.symbol).toBe("typography.weight.semiBold");
563
+ }
564
+ });
565
+
566
+ it("variable chain walks transitively, through variables standing for nothing", async () => {
567
+ // The walk's whole value: the chain passes through hops that stand for no
568
+ // symbol, so a one-hop join would answer nothing for these.
569
+ const walk = await rows("variable", "chain", {
570
+ variable: "disabled--color-text",
571
+ });
572
+ expect(walk.length).toBeGreaterThan(1);
573
+ for (const row of walk) {
574
+ expect(row.variable).toBe("disabled--color-text");
575
+ expect(row.symbol).toBeTruthy();
576
+ }
577
+ // The starting variable stands for nothing itself — which is exactly why
578
+ // it is not reachable through the token noun.
579
+ expect(
580
+ await count(
581
+ `SELECT (COUNT(*) AS ?n) WHERE {
582
+ ?v rdfs:label "disabled--color-text" ; dt:ofSymbol ?s .
583
+ }`,
584
+ ),
585
+ ).toBe(0);
586
+ });
587
+
588
+ it("token consumers answers rows with every identity column, or its recovery", async () => {
589
+ // Deliberately NOT "is empty": the design-system packs will record
590
+ // bindings, and a test pinning the emptiness would fail on that correct
591
+ // change. What is invariant is the SHAPE of the two legal answers.
592
+ const page = (await verb("token", "consumers").run({}, rt)) as PackPage;
593
+ if (page.rows.length === 0) {
594
+ const notice = verb("token", "consumers").output.formatters.notice?.(
595
+ page as never,
596
+ );
597
+ expect(notice).toContain(
598
+ "no component is recorded as using any token yet",
599
+ );
600
+ expect(notice).toContain("sources update");
601
+ return;
602
+ }
603
+ for (const row of page.rows) {
604
+ // All seven identity columns, because two bindings differing only in
605
+ // state or rank are two different facts — a narrower row would publish
606
+ // rows a caller cannot tell apart.
607
+ expect(Object.keys(row).sort()).toEqual(
608
+ expect.arrayContaining(["block", "symbol", "uri"]),
609
+ );
610
+ expect(row.block).toBeTruthy();
611
+ expect(row.symbol).toBeTruthy();
612
+ // The via is the block whose anatomy the binding was authored in, and it
613
+ // is BLANK whenever that is the consuming block itself — which the data
614
+ // asserts on every own-tree record. A via equal to the block would mean
615
+ // the two columns had collapsed back into one, which is the defect that
616
+ // printed six identical `Button` rows for one symbol.
617
+ if (row.via) expect(row.via).not.toBe(row.block);
618
+ }
619
+ });
620
+
621
+ it("token consumers names the CONSUMING block, not the tree it came from", async () => {
622
+ // The corpus invariant a fixture cannot reach: over the WHOLE population,
623
+ // every row whose record is inherited must name a block DIFFERENT from the
624
+ // via, and every own-tree row must leave the via empty. Asserted as a
625
+ // count of violations being zero rather than as row counts upstream moves.
626
+ //
627
+ // Read straight from the graph, because the story's own answer is what is
628
+ // under test: `ds:viaBlock` is present on every record and the term's
629
+ // definition says it equals the block on the block's own tree, so a column
630
+ // that coalesced the two printed a block's inherited rows under the name
631
+ // of the block they came from.
632
+ const inherited = await count(
633
+ `SELECT (COUNT(*) AS ?n) WHERE {
634
+ ?block ds:hasTokenBinding ?r .
635
+ ?r ds:viaBlock ?via .
636
+ FILTER(?via != ?block)
637
+ }`,
638
+ );
639
+ // The WHOLE population in one page, which is what makes this an invariant
640
+ // over the corpus rather than over whatever the default page held.
641
+ const page = (await verb("token", "consumers").run(
642
+ { limit: MAX_LIST_WINDOW },
643
+ rt,
644
+ )) as PackPage;
645
+ if (page.rows.length === 0) return;
646
+
647
+ const withVia = page.rows.filter((row) => Boolean(row.via));
648
+ expect(withVia).toHaveLength(inherited);
649
+ // And the consuming block is never the via — the whole point of splitting
650
+ // the column.
651
+ expect(withVia.filter((row) => row.via === row.block)).toEqual([]);
652
+ });
653
+ });