@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
package/pragma.conf.ts CHANGED
@@ -55,8 +55,10 @@ over GraphQL or raw SPARQL.
55
55
  ## How it fits together
56
56
 
57
57
  - **Tiers** are a hierarchy (\`global\` > \`apps\` > \`apps/lxd\`): a lower tier
58
- inherits and overrides the blocks of its ancestors, so scoping a query to a
59
- tier walks that chain.
58
+ inherits and overrides the blocks of its ancestors, so scoping a read to a
59
+ tier walks that chain. Reads of a tiered entity ARE scoped — a list answers
60
+ from the top-level tiers, \`--tier apps_lxd\` from that tier and its
61
+ ancestors, \`--tier all\` from every tier.
60
62
  - **Channels** (\`normal\`, \`experimental\`, \`prerelease\`) gate visibility, so an
61
63
  in-progress block never leaks into a stable answer.
62
64
 
@@ -72,7 +74,15 @@ exactly the domain that was published.`;
72
74
 
73
75
  /**
74
76
  * The read stories the design-system pack supplies — `block`, `token`,
75
- * `modifier` and `tier` as declared data rather than code.
77
+ * `variable`, `modifier` and `tier` as declared data rather than code.
78
+ *
79
+ * `token` and `variable` read the TOKEN-ONTOLOGY pack's four strata rather than
80
+ * this one's graph, and they are declared here anyway, deliberately: `token
81
+ * consumers` joins a symbol to the design-system records that consume it, so
82
+ * the pair spans both packs and there is no single pack whose `stories/*.json`
83
+ * could carry them. The day the strata are addressable without the design
84
+ * system — or the day either pack ships its own stories — is the day to split
85
+ * them out, and the note below about deletion applies to whichever half moves.
76
86
  *
77
87
  * The association with the pack is the point: when `@canonical/design-system`
78
88
  * starts shipping its own `stories/*.json`, this block is deleted and the
@@ -82,11 +92,16 @@ exactly the domain that was published.`;
82
92
  * layer does not know the pack grammar (`parsePackDefinition` does).
83
93
  */
84
94
  const designSystemStories: readonly PackDefinition[] = [
85
- // `block list` is declared content (L-OPEN-9): one unfiltered SELECT over
86
- // the four UIBlock classes, listing ALL blocks — experimental and alpha ones
87
- // included, for everyone — until filtering returns in declared form. The
88
- // hand-written tier-chain/channel filtering (and its `--all-tiers` escape)
89
- // is removed with the code, an owner-signed consequence. Display parity
95
+ // `block list` is declared content: one SELECT over the four UIBlock
96
+ // classes, listing every block the TIER SCOPE admits — experimental and
97
+ // alpha ones included, for everyone, since no read filters by channel.
98
+ //
99
+ // Tier filtering did come back in declared form, and in the form the story
100
+ // declares rather than the form the query text does: `tierScope` below names
101
+ // the hierarchy, and the kernel compiles the scope into this query as a
102
+ // constraint on `?uri` (`--tier all` for the unscoped list this used to be).
103
+ // The retired `--all-tiers` escape is not back — `--tier all` answers the
104
+ // same question with the argument a caller already holds. Display parity
90
105
  // lives IN the query: `VALUES ?class` closes the type set, so the BINDs
91
106
  // that derive `name` (declared name, else the IRI's local name), `type`
92
107
  // (lowercased class local name), and `tier` (tier IRI's local name) operate
@@ -135,6 +150,30 @@ const designSystemStories: readonly PackDefinition[] = [
135
150
  toolDescription:
136
151
  "List all design system blocks with their type, tier, and modifier families. Use when browsing which blocks exist. Example: block_list {}.",
137
152
  colophon: DESIGN_SYSTEM_COLOPHON,
153
+ // Blocks are TIERED, so every read of them is SCOPED: `block list` answers
154
+ // from the top-level tiers, `--tier apps_lxd` from that tier and its
155
+ // ancestors, `--tier all` from every tier (kernel/packs/tierScope.ts states
156
+ // the rule once, for every noun that declares this).
157
+ //
158
+ // The four terms are the ones the graph already carries, and three of them
159
+ // this story already declared for its RANKING (`scopeWeight` below): the
160
+ // hierarchy is the `/` in a tier's own `ds:name`, because the shipped graph
161
+ // asserts no parent edge to read instead — `ds:apps_lxd` is a `ds:Tier`
162
+ // with a name and nothing else. Reading the hierarchy from one place is
163
+ // what keeps the scope and the lookup ranking from disagreeing about which
164
+ // tier is above which.
165
+ //
166
+ // `base` is the editorial half, declared here for the reason `weights` is:
167
+ // nothing in the graph says `global` is the tier the others build on — the
168
+ // colophon above says it, and the 119 global blocks every product screen
169
+ // is assembled from say it. Without `base`, `--tier apps_lxd` would answer
170
+ // with LXD's nine blocks and hide the Buttons and Inputs they sit in.
171
+ tierScope: {
172
+ type: "ds:Tier",
173
+ via: "ds:tier",
174
+ by: "ds:name",
175
+ base: "ds:global",
176
+ },
138
177
  list: {
139
178
  query: [
140
179
  "SELECT ?uri ?name ?type ?tier",
@@ -268,6 +307,10 @@ const designSystemStories: readonly PackDefinition[] = [
268
307
  label: "Anatomy (DSL)",
269
308
  kind: "code",
270
309
  level: "detailed",
310
+ // The one piece of the notation that cannot be guessed from reading
311
+ // it: a bracketed value looks like a set of alternatives with no
312
+ // stated order of preference, and it is an ORDERED chain.
313
+ note: "A list value is the fallback chain the implementation reads, first wins.",
271
314
  },
272
315
  {
273
316
  name: "anatomyClassic",
@@ -314,6 +357,69 @@ const designSystemStories: readonly PackDefinition[] = [
314
357
  { name: "uri", property: "ds:name", graphqlField: "uri" },
315
358
  ],
316
359
  },
360
+ // Which symbols this block consumes, at which style key and rank — the
361
+ // mirror of `token consumers`, read from the block's end. It was
362
+ // DELIBERATELY ABSENT while none of the terms existed: the record the
363
+ // whole tuple hangs off is the design system's to mint, and until it
364
+ // did, `ds:hasTokenBinding`, `ds:consumesSymbol`, `ds:rank`,
365
+ // `ds:viaBlock` and `ds:node` were not declared properties at all. All
366
+ // five are now, with domains and ranges, and `ds:TokenBinding` is a
367
+ // class — so the declaration lands as the note then predicted, all six
368
+ // identity columns of it.
369
+ //
370
+ // SPARQL lane, and NOT this lookup's own GraphQL one, which is the only
371
+ // part of that prediction that had to change. Two of the six columns
372
+ // are unreachable through a generated document: `anatomy:styleKey` and
373
+ // `anatomy:styleState` carry no `rdfs:domain` — they are the anatomy
374
+ // vocabulary's, shared by every styled thing — so ke-graphql generates
375
+ // no field for them on `TokenBinding`, and a derived name the schema
376
+ // does not carry drops out of the document in silence. Measured on the
377
+ // shipped pack: Button rendered 24 rows with no key and no state, two
378
+ // of which (`spacing.internal.inline.start` and `…end`) were then
379
+ // byte-identical. A table whose rows a reader cannot tell apart is the
380
+ // defect this story refuses elsewhere, so the expand takes the lane
381
+ // that can read every term instead. The modifier families above cannot
382
+ // follow it — their nested `values` is a second hop only a document can
383
+ // make — which is why the lane is per-expand.
384
+ {
385
+ name: "tokens",
386
+ heading: "Tokens",
387
+ kind: "table",
388
+ relation: "ds:hasTokenBinding",
389
+ source: "sparql",
390
+ level: "detailed",
391
+ // Ordered by the READING order of the thing described: a node's
392
+ // styles, one key at a time, each key's fallback chain in the order
393
+ // the implementation walks it. The store's own scan order is
394
+ // undefined and put a node's two spacing keys either side of an
395
+ // unrelated one.
396
+ orderBy: ["node", "key", "rank"],
397
+ // The same sentence `Anatomy (DSL)` carries, because it is the same
398
+ // notation read from the other side: the anatomy's bracketed list is
399
+ // the chain, and `rank` is the position in it. A reader who has the
400
+ // rank column but not that sentence has no way to know that rank 2 is
401
+ // reached only when rank 1 does not resolve.
402
+ note: "A list value in the anatomy is the fallback chain the implementation reads, first wins; rank is the position in that chain.",
403
+ select: [
404
+ // The dotted name, not the symbol's IRI: it is the address
405
+ // `token lookup` and `token consumers --symbol` both take, so a row
406
+ // here is a query a reader can run.
407
+ { name: "symbol", property: "ds:consumesSymbol/rdfs:label" },
408
+ { name: "key", property: "anatomy:styleKey" },
409
+ { name: "state", property: "anatomy:styleState" },
410
+ { name: "rank", property: "ds:rank" },
411
+ // Blank on the block's OWN tree. `ds:viaBlock` is present on every
412
+ // record and its own definition says it equals the block in the
413
+ // own-tree case, so printing it raw put "Button" in all 24 of
414
+ // Button's rows — a cell that is only ever news when it differs.
415
+ {
416
+ name: "via",
417
+ property: "ds:viaBlock/ds:name",
418
+ blankWhenSelf: true,
419
+ },
420
+ { name: "node", property: "ds:node" },
421
+ ],
422
+ },
317
423
  ],
318
424
  disclosure: {
319
425
  levels: ["summary", "standard", "detailed"],
@@ -327,59 +433,845 @@ const designSystemStories: readonly PackDefinition[] = [
327
433
  },
328
434
  },
329
435
 
330
- // Design tokens: SPARQL-sourced on both verbs. There is no `ds:Token` GraphQL
331
- // type to project against when the graph ships no tokens, and the lookup reads
332
- // a property path (`ds:tokenType/rdfs:label`) only SPARQL can express. The
333
- // `emptyRecovery` install hint is the story users see on an empty store. The
334
- // noun is now purely declarative: `token add-config` wrote a starter file, and
335
- // L-OPEN-9 removed it rather than growing the read grammar a mutation verb.
436
+ // The design-token SYMBOLS — S1 of the four token strata, keyed on the name
437
+ // literal the binding programme publishes.
438
+ //
439
+ // REPOINTED from `ds:Token`, a class no graph asserts. The shipped pack
440
+ // carries 0 instances of it and 745 `dt:TokenSymbol`, so every `token list`
441
+ // on every install answered an empty table while the population sat one
442
+ // namespace over — the same silent shape as reading a retired `ds:whenToUse`,
443
+ // and caught the same way (`listBudget.shipped.exec.test.ts` measured this
444
+ // story at 0 rows / 2 bytes). `ds:valueLight`/`ds:valueDark` go with it: a
445
+ // symbol's value is not a pair of theme columns but one row per POSITION in
446
+ // the coordinate space, which is what `token values` answers.
447
+ //
448
+ // NO `nameFallback`, deliberately. The kernel's IRI derivation publishes a
449
+ // dotted local name with SLASHES (`dt:color.text` → `color/text`), which
450
+ // contradicts the dotted notation ruled for symbol names and would make this
451
+ // answer disagree with the anatomy's spelling of the same symbol. The
452
+ // consequence is visible rather than hidden: until the pack ships
453
+ // `rdfs:label` on the symbols, `token list` publishes no rows and
454
+ // `token lookup` resolves no name. An empty answer that says so beats a
455
+ // populated one whose names nothing else recognises — and it is why `token`
456
+ // is still in `EMPTY_CORPUS_TODAY`.
336
457
  {
337
458
  noun: "token",
338
- description: "List all design tokens.",
459
+ description: "List the design-token symbols.",
339
460
  toolDescription:
340
- "List all design tokens with their type. Use when browsing which tokens exist under the active scope. Example: token_list {}.",
461
+ 'List the design-token SYMBOLS — logical dotted names (`color.text`), with the type and description from the symbol\'s OWN definition, and the symbol a channel provisions. The CSS custom-property names a stylesheet declares are variable_list. Example: token_list { type: "color" }.',
341
462
  list: {
463
+ // Type and description are DEFINITION-level facts, and 393 of the 745
464
+ // symbols have more than one definition (`color.text` has 20). The one
465
+ // that speaks FOR the symbol is its own, and the graph already carries a
466
+ // path to it: every symbol the resolver reached has a BASE
467
+ // `dt:ResolvedValue` — the one with no `dt:coordinate` — and the HEAD of
468
+ // that value's `dt:resolutionChain` is the definition the value was
469
+ // authored in at the all-defaults position. Its `dt:tokenType` and its
470
+ // `w3c-tokens:description` are this row's two cells.
471
+ //
472
+ // The base restriction is SPELLED OUT rather than left to `DISTINCT`.
473
+ // 354 of the 519 values at a coordinate carry a chain too, and their
474
+ // heads are the definitions authored for those positions — a different
475
+ // definition, correctly so, and not this column's. Over the shipped
476
+ // corpus those heads happen to agree with the base head on both fields,
477
+ // so the unrestricted walk answers the same 745 rows; `FILTER NOT EXISTS`
478
+ // says WHICH value is meant instead of resting on that coincidence.
479
+ //
480
+ // Measured over the 745 symbols: 718 publish both fields, 27 blank both.
481
+ // The 27 are the 25 minted channels, which have no definition anywhere,
482
+ // plus `typography.heading.display` and its `.bold` twin, which have
483
+ // definitions but no resolved value to authorise one of them. Nothing
484
+ // publishes one field and blanks the other — a chain head carries both,
485
+ // or the symbol has no chain head.
486
+ //
487
+ // This REPLACED an agreement rule (publish what every definition agrees
488
+ // on, blank it when they disagree), and the two never disagree where the
489
+ // old one spoke: 0 of the 745 symbols have two definitions naming
490
+ // different types, and the 40 whose descriptions "disagreed" disagree
491
+ // only with a `semantic/modifier/**` definition — one position's wording,
492
+ // which the old rule let outvote the symbol's own. Those 40 now read
493
+ // their own description instead of a blank. What went with the old rule
494
+ // is its cost: a `COUNT(DISTINCT ...)` sub-select over 1,311 definitions
495
+ // made this the slowest body in the distribution. The paged walk that
496
+ // `listQuery.shipped.exec.test.ts` runs over it took 190 s, past its own
497
+ // 60-second timeout; over this query it takes 1.8 s, and the whole
498
+ // population comes back in 71 ms rather than 1.1 s a page.
342
499
  query: [
343
- "SELECT ?uri ?name ?category WHERE {",
344
- " ?uri a ds:Token ;",
345
- " ds:tokenId ?name .",
500
+ "SELECT DISTINCT ?uri ?name ?type ?description ?channelOf",
501
+ "WHERE {",
502
+ " ?uri a dt:TokenSymbol ;",
503
+ " rdfs:label ?name .",
504
+ // A channel IS a symbol after the binding programme, so the relation to
505
+ // the symbol it provisions is a column on this population rather than a
506
+ // noun of its own. Bound to the base symbol's LABEL, not its IRI: the
507
+ // filter column and the displayed column are one column, and an
508
+ // unbound IRI cell renders as a full IRI.
509
+ " OPTIONAL { ?uri dt:channelOf/rdfs:label ?channelOf }",
346
510
  " OPTIONAL {",
347
- " ?uri ds:tokenType ?type .",
348
- " ?type rdfs:label ?category .",
511
+ " ?resolved dt:forSymbol ?uri ;",
512
+ " dt:resolutionChain/rdf:first ?definition .",
513
+ " FILTER NOT EXISTS { ?resolved dt:coordinate ?coordinate }",
514
+ " OPTIONAL { ?definition dt:tokenType/rdfs:label ?type }",
515
+ " OPTIONAL { ?definition w3c-tokens:description ?description }",
349
516
  " }",
350
517
  "}",
351
518
  "ORDER BY ?name",
352
519
  ].join("\n"),
353
520
  columns: [
521
+ { field: "name", label: "Name" },
522
+ { field: "type", label: "Type" },
523
+ { field: "channelOf", label: "Channel of" },
524
+ { field: "description", label: "Description" },
354
525
  { field: "uri", label: "IRI" },
526
+ ],
527
+ filters: [
528
+ {
529
+ param: "type",
530
+ variable: "type",
531
+ // The seven typed members of the definition's type class, which is
532
+ // the same term the `type` column reads off the symbol's own
533
+ // definition. A type the ontology declares that no symbol is filed
534
+ // under is a calm empty list, not a bad argument.
535
+ vocabulary: {
536
+ query: [
537
+ "SELECT DISTINCT ?type WHERE {",
538
+ " ?kind a w3c-tokens:TokenType ;",
539
+ " rdfs:label ?type .",
540
+ "}",
541
+ ].join("\n"),
542
+ },
543
+ description: "Filter by type.",
544
+ },
545
+ {
546
+ param: "channelOf",
547
+ variable: "channelOf",
548
+ // The dimension is "which SYMBOL", so the roster is EVERY symbol —
549
+ // not the far smaller set that happens to have a channel today.
550
+ // Asking for a symbol nothing provisions is the documented calm empty
551
+ // list, which is the whole reason a vocabulary is read from the graph
552
+ // rather than from the rows a page returned.
553
+ //
554
+ // Keyed on `rdfs:label`, the same term the column binds and the same
555
+ // term the lookup resolves by. All three move together or not at all.
556
+ vocabulary: {
557
+ query: [
558
+ "SELECT DISTINCT ?channelOf WHERE {",
559
+ " ?symbol a dt:TokenSymbol ;",
560
+ " rdfs:label ?channelOf .",
561
+ "}",
562
+ ].join("\n"),
563
+ },
564
+ description: "Filter to one symbol's channels.",
565
+ },
566
+ ],
567
+ search: {
568
+ variables: ["name", "description"],
569
+ description: "Search name and description.",
570
+ },
571
+ // No `emptyRecovery`: this list is empty only on an install whose store
572
+ // holds no token pack at all, and the kernel's generic hint already names
573
+ // both fixes for that (broaden the filter, or build the store). Anything a
574
+ // story added here would be a sentence about an empty store printed to
575
+ // readers who have 745 symbols and only missed with a filter.
576
+ },
577
+ verbs: [
578
+ {
579
+ verb: "values",
580
+ description:
581
+ "List the value each symbol resolves to at each position, with its chain or its derivation.",
582
+ toolDescription:
583
+ 'List the resolved token VALUES — one row per (symbol, position) the graph materialises, with the value and either its resolution chain or the symbol it derives from. Only MATERIALISED positions appear, not the permutation space: a position with no row falls through to the base symbol. A derived row carries a derivation and no value cell. Example: token_values { symbol: "color.text" }.',
584
+ // The resolved-value shape admits exactly one of a chain or a
585
+ // derivation, so BOTH are selected wherever a value is projected. A
586
+ // surface that selected only the chain would show a blank row for every
587
+ // one of the 165 channel routings — and channel values are precisely
588
+ // what the anatomy validator and the editor's completion ask about.
589
+ //
590
+ // Both are rdf:LISTs, not literals: `dt:resolutionChain` is a list of
591
+ // definition IRIs, so it needs `/rdf:rest*/rdf:first` and an aggregate
592
+ // to become one cell. The chain items are trimmed to the path inside
593
+ // the token files, which is the form the resolution chain is quoted in.
594
+ //
595
+ // The cell is a SET, and its order is NOT the list's. `GROUP_CONCAT`
596
+ // has no defined order in SPARQL and takes no `ORDER BY`, so the links
597
+ // of a multi-link chain may come back either way round — which a test
598
+ // pinning one spelling of a two-link chain found by flaking. Reading
599
+ // the cell as "which definitions this value passed through" is
600
+ // therefore right and reading it as "in what order" is not. The
601
+ // ORDERED anchor a caller usually wants is the chain's HEAD, the
602
+ // definition the value was authored in, and that is what the lookup's
603
+ // `values` expand projects with `rdf:first`.
604
+ query: [
605
+ "SELECT ?symbol ?position ?value ?derivedFrom",
606
+ ' (GROUP_CONCAT(DISTINCT ?chainItem; SEPARATOR=" ") AS ?chain)',
607
+ "WHERE {",
608
+ " ?resolved a dt:ResolvedValue ;",
609
+ " dt:forSymbol ?symbolUri .",
610
+ " ?symbolUri rdfs:label ?symbol .",
611
+ " OPTIONAL { ?resolved dt:coordinate ?coordinate }",
612
+ " OPTIONAL { ?resolved dt:resolvesTo ?value }",
613
+ " OPTIONAL { ?resolved dt:derivedFrom/rdfs:label ?derivedFrom }",
614
+ " OPTIONAL {",
615
+ " ?resolved dt:resolutionChain/rdf:rest*/rdf:first ?chainUri .",
616
+ ' BIND(REPLACE(STR(?chainUri), "^.*/file/", "") AS ?chainItem)',
617
+ " }",
618
+ // The coordinate's own dotted name, with the class prefix its IRI
619
+ // carries dropped: `dt:coordinate.mode.dark` displays and filters as
620
+ // `mode.dark`, which is how a position is written everywhere else.
621
+ // A value at the DEFAULT position carries no coordinate at all, and
622
+ // its cell is empty rather than a full IRI.
623
+ ' BIND(REPLACE(REPLACE(STR(?coordinate), "^.*[/#]", ""), "^coordinate[.]", "") AS ?position)',
624
+ "}",
625
+ "GROUP BY ?resolved ?symbol ?position ?value ?derivedFrom",
626
+ "ORDER BY ?symbol ?position",
627
+ ].join("\n"),
628
+ columns: [
629
+ { field: "symbol", label: "Symbol" },
630
+ { field: "position", label: "Position" },
631
+ { field: "value", label: "Value" },
632
+ { field: "chain", label: "Chain" },
633
+ { field: "derivedFrom", label: "Derived from" },
634
+ ],
635
+ filters: [
636
+ {
637
+ param: "symbol",
638
+ variable: "symbol",
639
+ vocabulary: {
640
+ query: [
641
+ "SELECT DISTINCT ?symbol WHERE {",
642
+ " ?s a dt:TokenSymbol ;",
643
+ " rdfs:label ?symbol .",
644
+ "}",
645
+ ].join("\n"),
646
+ },
647
+ description: "Filter to one symbol.",
648
+ },
649
+ {
650
+ param: "position",
651
+ variable: "position",
652
+ // `set`, because ONE row can sit at several positions at once: a
653
+ // value asserted `dt:alsoAt` a second coordinate is one row
654
+ // belonging to both, and a filter comparing only the whole cell
655
+ // would answer such a row for neither.
656
+ match: "set",
657
+ vocabulary: {
658
+ query: [
659
+ "SELECT DISTINCT ?position WHERE {",
660
+ " ?c a dt:Coordinate .",
661
+ ' BIND(REPLACE(REPLACE(STR(?c), "^.*[/#]", ""), "^coordinate[.]", "") AS ?position)',
662
+ "}",
663
+ ].join("\n"),
664
+ },
665
+ description: "Filter to one position.",
666
+ },
667
+ ],
668
+ search: {
669
+ variables: ["symbol", "value", "derivedFrom"],
670
+ description: "Search symbol, value, derivation.",
671
+ },
672
+ emptyRecovery: {
673
+ message:
674
+ "Resolved token values come from the @canonical/token-ontology pack, and a value is only addressable by symbol once that pack publishes its name literals — a store built without that pack carries none.",
675
+ cli: "sources update",
676
+ },
677
+ },
678
+ {
679
+ verb: "consumers",
680
+ description:
681
+ "List which blocks consume which token symbol, at which style key, state and rank.",
682
+ toolDescription:
683
+ 'List the token BINDINGS the design system records — which block consumes which symbol, at which style key, state, rank and node. Every column is identity: two bindings differing only in state are different facts. The block is the CONSUMING block; via names the block whose anatomy the binding was authored in, and is blank when that is the consuming block itself. Name the symbol by its dotted name (symbol) or by a CSS variable standing for it (variable). Answers empty until the packs record bindings. Example: token_consumers { variable: "color-text" }.',
684
+ // Seven identity columns, and not one of them is decoration: a binding
685
+ // is identified by the whole tuple, so dropping `rank` or `node` would
686
+ // publish rows a caller cannot tell apart — which is worse than a wide
687
+ // table, because a deduplicating consumer would silently lose facts.
688
+ //
689
+ // `ds:hasTokenBinding` and its four siblings are DEFINED BY the
690
+ // design-system ontology only from the binding programme's records
691
+ // onward; today the shipped ontology defines none of them, and the `ds:`
692
+ // prefix IS bound, so this answers a calm empty list with the recovery
693
+ // below rather than failing. It starts answering with no code change.
694
+ //
695
+ // `?block` is the CONSUMING block and `?via` the block whose anatomy
696
+ // the binding was authored in, and the two are separate columns because
697
+ // they are separate facts. They were one: `?block` used to COALESCE the
698
+ // via name over the consuming block's, so a record Accordion carries
699
+ // because its anatomy embeds `Accordion.Item` printed as
700
+ // `Accordion.Item` — byte-identical to Accordion.Item's OWN record.
701
+ // `--symbol color.text` answered six rows reading `Button`, one per
702
+ // block that embeds a Button, and no reader could tell which component
703
+ // would change if the symbol did. Which component consumes the symbol
704
+ // is the fact this verb exists for, so it is the column the row leads
705
+ // with; the via is BLANK on a record sitting on the block's own tree —
706
+ // where the data asserts `ds:viaBlock` equal to the block itself —
707
+ // because "Button via Button" is not news and 24 of Button's rows
708
+ // would say it.
709
+ query: [
710
+ "SELECT ?block ?via ?symbol ?key ?state ?rank ?node ?uri",
711
+ ' (GROUP_CONCAT(DISTINCT ?variableName; SEPARATOR=" ") AS ?variable)',
712
+ "WHERE {",
713
+ " ?blockUri ds:hasTokenBinding ?uri .",
714
+ " ?uri ds:consumesSymbol ?symbolUri .",
715
+ " ?symbolUri rdfs:label ?symbol .",
716
+ " OPTIONAL { ?uri anatomy:styleKey ?key }",
717
+ " OPTIONAL { ?uri anatomy:styleState ?state }",
718
+ " OPTIONAL { ?uri ds:rank ?rank }",
719
+ " OPTIONAL { ?uri ds:node ?node }",
720
+ " OPTIONAL { ?uri ds:viaBlock ?viaUri . OPTIONAL { ?viaUri ds:name ?viaName } }",
721
+ " OPTIONAL { ?blockUri ds:name ?blockName }",
722
+ // Every platform spelling of the consumed symbol, as a SET rather
723
+ // than a join that multiplies the row: 204 symbols carry both a
724
+ // kebab and a camelCase variable, and binding one row per variable
725
+ // would publish each binding twice under names a caller cannot tell
726
+ // apart. Aggregated, both spellings land in one cell and `--variable`
727
+ // set-matches either — which is also why the two spellings of one
728
+ // symbol return the SAME set rather than partitioning it.
729
+ " OPTIONAL {",
730
+ " ?variableUri dt:ofSymbol ?symbolUri ;",
731
+ " rdfs:label ?variableName .",
732
+ " }",
733
+ ' BIND(COALESCE(?blockName, REPLACE(STR(?blockUri), "^.*[/#]", "")) AS ?block)',
734
+ // `IF` and not `COALESCE`: the blank says something a MISSING value
735
+ // cannot, because the via is present on every record and equals the
736
+ // block on its own tree. SPARQL evaluates only the taken arm of an
737
+ // `IF`, so the local-name fallback is never asked to name an unbound
738
+ // via.
739
+ " BIND(IF(BOUND(?viaUri) && ?viaUri != ?blockUri,",
740
+ ' COALESCE(?viaName, REPLACE(STR(?viaUri), "^.*[/#]", "")),',
741
+ ' "") AS ?via)',
742
+ "}",
743
+ "GROUP BY ?block ?via ?symbol ?key ?state ?rank ?node ?uri",
744
+ // `?via` second, so a block's OWN bindings (blank via, which sorts
745
+ // first) come before the ones it inherits, and the inherited ones
746
+ // stay grouped by the block they come from.
747
+ "ORDER BY ?block ?via ?symbol ?key ?state ?rank ?node",
748
+ ].join("\n"),
749
+ // No `uri` COLUMN, though the record's IRI is still SELECTed: it is the
750
+ // row's subject and what keeps two bindings differing only in state
751
+ // apart, so `--format json` carries it exactly as before. What it is
752
+ // not is a cell worth a reader's eye. A binding record is a BLANK NODE,
753
+ // so it is not addressable — `graph inspect _:dda768d3…` answers
754
+ // INVALID_INPUT — and a blank node's label is re-minted on every store
755
+ // load, so the same row printed a different id on every run. And the
756
+ // `llm` renderer hoists a `uri` column to the HEAD of the line, ahead
757
+ // of every other cell, which is the slot the consuming block belongs
758
+ // in; a story cannot reorder that, so the column goes rather than the
759
+ // block staying second to an unusable id.
760
+ columns: [
761
+ { field: "block", label: "Block" },
762
+ { field: "via", label: "Via" },
763
+ { field: "symbol", label: "Symbol" },
764
+ { field: "variable", label: "Variables" },
765
+ { field: "key", label: "Style key" },
766
+ { field: "state", label: "State" },
767
+ { field: "rank", label: "Rank" },
768
+ { field: "node", label: "Node" },
769
+ ],
770
+ filters: [
771
+ {
772
+ param: "symbol",
773
+ variable: "symbol",
774
+ vocabulary: {
775
+ query: [
776
+ "SELECT DISTINCT ?symbol WHERE {",
777
+ " ?s a dt:TokenSymbol ;",
778
+ " rdfs:label ?symbol .",
779
+ "}",
780
+ ].join("\n"),
781
+ },
782
+ description: "Filter to one symbol.",
783
+ },
784
+ {
785
+ param: "variable",
786
+ variable: "variable",
787
+ // The SAME constraint as `--symbol`, named by the other spelling.
788
+ // A web implementer holds a CSS custom-property name, not a dotted
789
+ // symbol, so answering "what breaks if I change --color-text" used
790
+ // to take two calls and a spelling the caller did not start with.
791
+ // The join is in the query instead.
792
+ //
793
+ // `set`, because the cell is every platform spelling of the
794
+ // consumed symbol and one row legitimately belongs to all of them.
795
+ match: "set",
796
+ // The variable labels, the same source `variable lookup` resolves
797
+ // against — so a name that works there works here.
798
+ vocabulary: {
799
+ query: [
800
+ "SELECT DISTINCT ?variable WHERE {",
801
+ " ?v a dt:Variable ;",
802
+ " rdfs:label ?variable .",
803
+ "}",
804
+ ].join("\n"),
805
+ },
806
+ // Two things a caller will otherwise assume, both wrong.
807
+ //
808
+ // A CHANNEL variable and its semantic sibling are DIFFERENT
809
+ // constraints: `--variable modifier-color-text` finds the blocks
810
+ // that bind the channel, `--variable color-text` those that bind
811
+ // the symbol, and neither includes the other. Whether a block's
812
+ // resolution eventually reaches the other through a fallback is a
813
+ // question about the consumed list, not about this single-symbol
814
+ // join, and blurring them here would answer a question nobody
815
+ // asked with rows nobody can check.
816
+ //
817
+ // And a variable standing for NO symbol has no answer down this
818
+ // path at all — 236 of them do — which is an empty answer for a
819
+ // reason the recovery below names, not evidence that nothing
820
+ // consumes it.
821
+ description:
822
+ "A CSS variable name for the consumed symbol — the other spelling of the symbol parameter. A channel variable and its semantic sibling differ.",
823
+ },
824
+ {
825
+ param: "key",
826
+ variable: "key",
827
+ // The style-key REGISTRY, which the anatomy pack publishes as 111
828
+ // `anatomy:StyleKey` individuals. Their identity is the IRI and
829
+ // they carry no key literal, so the admissible spelling is the
830
+ // IRI's local name with the class prefix its minting adds dropped:
831
+ // `anatomy:key.appearance.background` is the key
832
+ // `appearance.background`, which is how the vocabulary's own
833
+ // examples write it.
834
+ //
835
+ // Read from the REGISTRY rather than from the records, and that is
836
+ // the point: no block has a binding record yet, so a roster read
837
+ // from records would be empty and would refuse every key a caller
838
+ // could legitimately ask about. The registry is the graph's
839
+ // statement of what a key may be; the records are the population.
840
+ vocabulary: {
841
+ query: [
842
+ "SELECT DISTINCT ?key WHERE {",
843
+ " ?styleKey a anatomy:StyleKey .",
844
+ ' BIND(REPLACE(REPLACE(STR(?styleKey), "^.*[/#]", ""), "^key[.]", "") AS ?key)',
845
+ "}",
846
+ ].join("\n"),
847
+ },
848
+ description: "Filter to one style key.",
849
+ },
850
+ {
851
+ param: "state",
852
+ variable: "state",
853
+ // The closed interaction-state vocabulary, read from the shape that
854
+ // closes it rather than transcribed here: the anatomy pack's shapes
855
+ // constrain `anatomy:styleState` with an `sh:in` list, so the five
856
+ // admissible states are a query over that list. Hard-coding them
857
+ // would be the "don't hard-code the slugs" defect with an extra
858
+ // step — the graph already says it.
859
+ vocabulary: {
860
+ query: [
861
+ "SELECT DISTINCT ?state WHERE {",
862
+ " ?shape sh:path anatomy:styleState ;",
863
+ " sh:in/rdf:rest*/rdf:first ?state .",
864
+ "}",
865
+ ].join("\n"),
866
+ },
867
+ description: "Filter to one interaction state.",
868
+ },
869
+ ],
870
+ search: {
871
+ variables: ["block", "via", "symbol", "key", "state", "node"],
872
+ description: "Search block, via, symbol, key, state and node.",
873
+ },
874
+ // Deliberately `sources update`: unlike `standard list`, this story's
875
+ // data does NOT ride the embedded snapshot — the binding records are
876
+ // written by the design-system packs, so an empty answer here can be a
877
+ // store that predates them.
878
+ //
879
+ // CAN be, not is — which is why the message opens on the two cases
880
+ // rather than asserting one. This text prints under a filtered miss too
881
+ // (`--symbol color.text`), where "nothing is recorded at all" would be a
882
+ // claim the answer cannot support, so it names both readings and hands
883
+ // the reader the one command that separates them.
884
+ emptyRecovery: {
885
+ message:
886
+ "Either no component uses this token, or no component is recorded as using any token yet — which components use which tokens comes from the component anatomies in the design-system document, and a copy of the graph built before any anatomy named its tokens is empty for every token. Run the same command without `--symbol` to tell the two apart: an empty unfiltered answer means the graph predates the anatomies and fills in once they are written and the graph is rebuilt. If you asked about a CSS variable rather than a token: some variables are computed from others (the `--hover--…` and `--disabled--…` ones, for example) and stand for no token, so nothing is ever recorded as using them; `pragma variable chain --variable <name>` shows what such a variable ends up as.",
887
+ cli: "sources update",
888
+ },
889
+ },
890
+ ],
891
+ lookup: {
892
+ source: "sparql",
893
+ // `rdfs:label`, matching the `name` the list publishes — the two-step
894
+ // grammar is that a row's `name` goes VERBATIM to lookup. Both halves
895
+ // REQUIRE the literal, so both are empty together rather than one
896
+ // publishing names the other cannot resolve.
897
+ by: "rdfs:label",
898
+ type: "dt:TokenSymbol",
899
+ description:
900
+ "Look up one or more token symbols by dotted name, IRI, or glob.",
901
+ toolDescription:
902
+ 'Get one design-token symbol in full: its own type and description, every definition behind it with that definition\'s own type and description, the modifier families that may rebind it, and the value it resolves to at each position. Address it by the dotted name token_list publishes (`color.text`), by prefixed name, by IRI, or by a glob. Example: token_lookup { name: ["color.text"] }.',
903
+ fields: [
904
+ // Single-valued: a channel provisions exactly one symbol.
905
+ {
906
+ name: "channelOf",
907
+ property: "dt:channelOf/rdfs:label",
908
+ label: "Channel of",
909
+ },
910
+ // The same pair `token list` publishes, reached the same way: the head
911
+ // of a resolved value's chain is the definition that value was authored
912
+ // in, so these two read the symbol's OWN definition rather than
913
+ // sampling one of the twenty behind `color.text`.
914
+ //
915
+ // A lookup field is a property PATH, so it cannot spell the
916
+ // `FILTER NOT EXISTS` that restricts the list's walk to the BASE value,
917
+ // and here it does not have to. 354 symbols have more than one chain
918
+ // head — one per materialised position — and over the shipped corpus no
919
+ // symbol's heads name two types or two descriptions, so the path is
920
+ // multi-valued with one value repeated and `firstRowPerEntity` keeping
921
+ // the first is a choice between identical answers. Were a position's
922
+ // head ever to differ, the `definitions` expand is where the difference
923
+ // is readable, because it shows every definition APART.
924
+ {
925
+ name: "type",
926
+ property:
927
+ "^dt:forSymbol/dt:resolutionChain/rdf:first/dt:tokenType/rdfs:label",
928
+ label: "Type",
929
+ },
930
+ {
931
+ name: "description",
932
+ property:
933
+ "^dt:forSymbol/dt:resolutionChain/rdf:first/w3c-tokens:description",
934
+ label: "Description",
935
+ },
936
+ ],
937
+ expand: [
938
+ {
939
+ name: "definitions",
940
+ heading: "Definitions",
941
+ kind: "table",
942
+ // The inverse edge: definitions point AT the symbol
943
+ // (`?definition dt:symbol ?uri`), so the relation from the symbol is
944
+ // `^dt:symbol`. 1,311 definitions stand behind 745 symbols.
945
+ relation: "^dt:symbol",
946
+ select: [
947
+ { name: "file", property: "w3c-tokens:inFile/w3c-tokens:path" },
948
+ { name: "type", property: "dt:tokenType/rdfs:label" },
949
+ { name: "description", property: "w3c-tokens:description" },
950
+ ],
951
+ },
952
+ {
953
+ name: "coverage",
954
+ heading: "Covered by",
955
+ // Coverage hangs on the FAMILY, not on the symbol
956
+ // (`?family dt:covers ?symbol`), so this is the inverse too. The
957
+ // family is a `ds:` entity, so its name comes from the
958
+ // design-system pack rather than the token ontology.
959
+ relation: "^dt:covers",
960
+ select: [{ name: "family", property: "ds:name" }],
961
+ },
962
+ {
963
+ name: "values",
964
+ heading: "Values",
965
+ kind: "table",
966
+ relation: "^dt:forSymbol",
967
+ // BOTH the chain and the derivation, for the reason `token values`
968
+ // states: the shape admits exactly one of them, and selecting only
969
+ // the chain would blank every channel routing.
970
+ //
971
+ // `rdf:first` and NOT `/rdf:rest*/rdf:first`, which is the walk
972
+ // `token values` uses. An expand's child field is a plain triple with
973
+ // no aggregate available, so a multi-hop chain multiplies the CHILD:
974
+ // 204 of the 1,072 values carry a chain of 2 or 3 links, and
975
+ // `color.background.container` rendered four "Values" rows for its
976
+ // two positions, each repeating the same value beside a different
977
+ // link. A caller counting positions would have read that as four.
978
+ // The head of the list is the definition the value was AUTHORED in
979
+ // (the tail is what it aliased through to a primitive), so one link
980
+ // per row is both correct and the most useful one; the full walk is
981
+ // `token values`, which can GROUP_CONCAT it into a single cell.
982
+ select: [
983
+ { name: "position", property: "dt:coordinate" },
984
+ { name: "value", property: "dt:resolvesTo" },
985
+ { name: "chain", property: "dt:resolutionChain/rdf:first" },
986
+ { name: "derivedFrom", property: "dt:derivedFrom/rdfs:label" },
987
+ ],
988
+ },
989
+ ],
990
+ sample: {
991
+ fixedCount: true,
992
+ toolDescription:
993
+ "Return random complete design-token symbols — definitions, coverage and resolved values — as exemplars. Use BEFORE writing queries to see real data shapes. Example: token_sample {}.",
994
+ },
995
+ },
996
+ },
997
+
998
+ // The platform VARIABLES — S4, the stylesheet's own names for the values.
999
+ //
1000
+ // A SECOND noun rather than a flag on `token`, and one measurement settles
1001
+ // it: 236 of the 1,156 variables stand for NO symbol at all — legacy twins
1002
+ // the ontology itself names, plus the computed states and resets.
1003
+ // `--disabled--color-text` and `--modifier-color-text` are nobody's symbol, so
1004
+ // a surface keyed on symbols cannot address them. The cut is by STRATUM ROLE,
1005
+ // not by platform: the platform is read out of the IRI base as an ordinary
1006
+ // filter, so a second platform's catalogue arrives as one more value of
1007
+ // `--platform` and not as a noun of its own.
1008
+ //
1009
+ // The published name is the label with its leading `--` STRIPPED, and that is
1010
+ // what makes the noun usable at all: `--color-text` cannot be typed as a
1011
+ // positional argument — the parser answers `unknown option '--color-text'`
1012
+ // and a glob is no escape. Upstream publishes the stripped form as
1013
+ // `rdfs:label`, and the stripping is measurably safe: over the 1,156
1014
+ // variables it produces 1,156 distinct names and collides with no symbol
1015
+ // name. `variable.parity.test.ts` pins both halves.
1016
+ {
1017
+ noun: "variable",
1018
+ description:
1019
+ "List the platform variables the design tokens are emitted as.",
1020
+ toolDescription:
1021
+ 'List the platform VARIABLES a stylesheet declares as CSS custom properties, with the symbol each stands for, its tier, visibility and the coordinates it is selected at. 236 stand for no symbol, so token_list cannot reach them. Address one WITHOUT its leading dashes (`color-text`). Example: variable_list { symbol: "color.text" }.',
1022
+ list: {
1023
+ query: [
1024
+ "SELECT ?uri ?name ?platform ?symbol ?tier ?visibility",
1025
+ ' (GROUP_CONCAT(DISTINCT ?coordinateName; SEPARATOR=" ") AS ?coordinate)',
1026
+ "WHERE {",
1027
+ " ?uri a dt:Variable ;",
1028
+ " rdfs:label ?name .",
1029
+ " OPTIONAL { ?uri dt:ofSymbol/rdfs:label ?symbol }",
1030
+ " OPTIONAL { ?uri dt:tier ?tierUri }",
1031
+ " OPTIONAL { ?uri dt:visibility ?visibilityUri }",
1032
+ // A variable is selected at a coordinate two ways, and both count: the
1033
+ // CONDITION its declaration sits under selects one
1034
+ // (`.success` → `criticality.success`), and a declaration may assert a
1035
+ // second directly (`dt:alsoAt`, which is `mode.dark` for all 261 of
1036
+ // them). 305 of the 1,156 variables reach at least one coordinate.
1037
+ " OPTIONAL {",
1038
+ " { ?uri dt:declaredAt/dt:under/dt:selectsCoordinate ?coordinateUri }",
1039
+ " UNION",
1040
+ " { ?uri dt:declaredAt/dt:alsoAt ?coordinateUri }",
1041
+ ' BIND(REPLACE(REPLACE(STR(?coordinateUri), "^.*[/#]", ""), "^coordinate[.]", "") AS ?coordinateName)',
1042
+ " }",
1043
+ // The platform, read out of the IRI base rather than asserted: every
1044
+ // variable is minted under `…/s4/<platform>/`. STRAFTER/STRBEFORE
1045
+ // rather than a regex so a variable whose IRI carries no such segment
1046
+ // answers an EMPTY cell instead of its own full IRI, which is what a
1047
+ // failed REPLACE returns.
1048
+ ' BIND(STRBEFORE(STRAFTER(STR(?uri), "/s4/"), "/") AS ?platform)',
1049
+ ' BIND(REPLACE(REPLACE(STR(?tierUri), "^.*[/#]", ""), "^tier[.]", "") AS ?tier)',
1050
+ ' BIND(REPLACE(REPLACE(STR(?visibilityUri), "^.*[/#]", ""), "^visibility[.]", "") AS ?visibility)',
1051
+ "}",
1052
+ "GROUP BY ?uri ?name ?platform ?symbol ?tier ?visibility",
1053
+ "ORDER BY ?name",
1054
+ ].join("\n"),
1055
+ columns: [
355
1056
  { field: "name", label: "Name" },
356
- { field: "category", label: "Type" },
1057
+ { field: "symbol", label: "Symbol" },
1058
+ { field: "tier", label: "Tier" },
1059
+ { field: "visibility", label: "Visibility" },
1060
+ { field: "platform", label: "Platform" },
1061
+ { field: "coordinate", label: "Coordinates" },
1062
+ { field: "uri", label: "IRI" },
1063
+ ],
1064
+ filters: [
1065
+ {
1066
+ param: "platform",
1067
+ variable: "platform",
1068
+ // The vocabulary is a query over the IRI base, exactly as the column
1069
+ // is — the platform is not asserted anywhere, so the file identity
1070
+ // in the IRI is the only thing that states it. One value today
1071
+ // (`web`); a second platform's catalogue adds itself here with no
1072
+ // edit.
1073
+ vocabulary: {
1074
+ query: [
1075
+ "SELECT DISTINCT ?platform WHERE {",
1076
+ " ?v a dt:Variable .",
1077
+ ' BIND(STRBEFORE(STRAFTER(STR(?v), "/s4/"), "/") AS ?platform)',
1078
+ "}",
1079
+ ].join("\n"),
1080
+ },
1081
+ description: "Filter by platform.",
1082
+ },
1083
+ {
1084
+ param: "symbol",
1085
+ variable: "symbol",
1086
+ // The symbol roster, read the same way `token list`'s `channelOf`
1087
+ // filter reads it: every symbol, keyed on `rdfs:label`.
1088
+ vocabulary: {
1089
+ query: [
1090
+ "SELECT DISTINCT ?symbol WHERE {",
1091
+ " ?s a dt:TokenSymbol ;",
1092
+ " rdfs:label ?symbol .",
1093
+ "}",
1094
+ ].join("\n"),
1095
+ },
1096
+ description: "Filter to one symbol.",
1097
+ },
1098
+ {
1099
+ param: "tier",
1100
+ variable: "tier",
1101
+ vocabulary: {
1102
+ query: [
1103
+ "SELECT DISTINCT ?tier WHERE {",
1104
+ " ?t a dt:Tier .",
1105
+ ' BIND(REPLACE(REPLACE(STR(?t), "^.*[/#]", ""), "^tier[.]", "") AS ?tier)',
1106
+ "}",
1107
+ ].join("\n"),
1108
+ },
1109
+ description: "Filter by tier.",
1110
+ },
1111
+ {
1112
+ param: "visibility",
1113
+ variable: "visibility",
1114
+ vocabulary: {
1115
+ query: [
1116
+ "SELECT DISTINCT ?visibility WHERE {",
1117
+ " ?v a dt:Visibility .",
1118
+ ' BIND(REPLACE(REPLACE(STR(?v), "^.*[/#]", ""), "^visibility[.]", "") AS ?visibility)',
1119
+ "}",
1120
+ ].join("\n"),
1121
+ },
1122
+ description: "Filter by visibility.",
1123
+ },
1124
+ {
1125
+ param: "coordinate",
1126
+ variable: "coordinate",
1127
+ // `set`, and it has to be: `--modifier-color-text` is selected at 17
1128
+ // coordinates at once, so its cell is the whole set its declarations
1129
+ // reach and a filter comparing the whole cell would answer for none
1130
+ // of them. The cell is a `GROUP_CONCAT`, so this predicate can only
1131
+ // run after aggregation — which is why the page wraps the query.
1132
+ match: "set",
1133
+ vocabulary: {
1134
+ query: [
1135
+ "SELECT DISTINCT ?coordinate WHERE {",
1136
+ " ?c a dt:Coordinate .",
1137
+ ' BIND(REPLACE(REPLACE(STR(?c), "^.*[/#]", ""), "^coordinate[.]", "") AS ?coordinate)',
1138
+ "}",
1139
+ ].join("\n"),
1140
+ },
1141
+ description: "Filter to one coordinate.",
1142
+ },
357
1143
  ],
1144
+ search: {
1145
+ variables: ["name", "symbol"],
1146
+ description: "Search name and symbol.",
1147
+ },
358
1148
  emptyRecovery: {
359
1149
  message:
360
- "No tokens in the store. Build it from the configured design-system packs.",
1150
+ "Platform variables come from the @canonical/token-ontology pack, and a variable is only addressable once that pack publishes its name literals — a store built without that pack carries none.",
361
1151
  cli: "sources update",
362
1152
  },
363
1153
  },
1154
+ verbs: [
1155
+ {
1156
+ verb: "chain",
1157
+ description:
1158
+ "List the walk from a variable to every symbol it reaches, through every variable in between.",
1159
+ toolDescription:
1160
+ 'List the resolution WALK: every (variable, symbol) pair a variable reaches through what its declarations reference, transitively — what a variable finally means. The closure runs over every declaration of every hop, so one variable can reach dozens of pairs. Example: variable_chain { variable: "modifier-color-text" }.',
1161
+ // ONE property path carries the whole walk, which is why this is a
1162
+ // query and not a traversal in code. `dt:references` is an rdf:List of
1163
+ // the variables a declaration's value reads, so each hop is
1164
+ // `dt:declaredAt/dt:references/rdf:rest*/rdf:first`, and the `+` makes
1165
+ // the whole thing transitive. The terminal is a variable that stands
1166
+ // for a symbol, which is what `dt:ofSymbol` reads.
1167
+ query: [
1168
+ "SELECT ?variable ?reaches ?symbol",
1169
+ "WHERE {",
1170
+ " ?uri a dt:Variable ;",
1171
+ " rdfs:label ?variable .",
1172
+ " ?uri (dt:declaredAt/dt:references/rdf:rest*/rdf:first)+ ?reachedUri .",
1173
+ " ?reachedUri dt:ofSymbol ?symbolUri ;",
1174
+ " rdfs:label ?reaches .",
1175
+ " ?symbolUri rdfs:label ?symbol .",
1176
+ "}",
1177
+ "ORDER BY ?variable ?reaches ?symbol",
1178
+ ].join("\n"),
1179
+ columns: [
1180
+ { field: "variable", label: "Variable" },
1181
+ { field: "reaches", label: "Reaches" },
1182
+ { field: "symbol", label: "Symbol" },
1183
+ ],
1184
+ filters: [
1185
+ {
1186
+ param: "variable",
1187
+ variable: "variable",
1188
+ // The variable roster, keyed on the same `rdfs:label` the column
1189
+ // binds and the lookup resolves by — the CSS name with its leading
1190
+ // `--` already stripped, which is the only form typable as a
1191
+ // positional. `variable.parity.test.ts` pins that the stripping is
1192
+ // injective and collides with no symbol name.
1193
+ vocabulary: {
1194
+ query: [
1195
+ "SELECT DISTINCT ?variable WHERE {",
1196
+ " ?v a dt:Variable ;",
1197
+ " rdfs:label ?variable .",
1198
+ "}",
1199
+ ].join("\n"),
1200
+ },
1201
+ description: "Filter to one variable.",
1202
+ },
1203
+ {
1204
+ param: "symbol",
1205
+ variable: "symbol",
1206
+ vocabulary: {
1207
+ query: [
1208
+ "SELECT DISTINCT ?symbol WHERE {",
1209
+ " ?s a dt:TokenSymbol ;",
1210
+ " rdfs:label ?symbol .",
1211
+ "}",
1212
+ ].join("\n"),
1213
+ },
1214
+ description: "Filter to one symbol.",
1215
+ },
1216
+ ],
1217
+ emptyRecovery: {
1218
+ message:
1219
+ "A resolution walk follows the declarations the @canonical/token-ontology pack provides, and the walk is only addressable by name once that pack publishes its name literals — a store built without that pack carries none.",
1220
+ cli: "sources update",
1221
+ },
1222
+ },
1223
+ ],
364
1224
  lookup: {
365
1225
  source: "sparql",
366
- by: "ds:tokenId",
367
- type: "ds:Token",
1226
+ // `rdfs:label`, matching the `name` the list publishes, and with NO
1227
+ // `nameFallback` for the same reason `token` has none: the kernel's IRI
1228
+ // derivation would publish `--color-text` with its dashes intact and its
1229
+ // dots as slashes, and a `--`-prefixed positional cannot be typed at all.
1230
+ by: "rdfs:label",
1231
+ type: "dt:Variable",
1232
+ description:
1233
+ "Look up one or more platform variables by name (without the leading dashes), IRI, or glob.",
368
1234
  toolDescription:
369
- 'Get type and theme values for one or more design tokens by name. Use when resolving specific tokens\' light/dark values. Example: token_lookup { name: ["color.primary"] }.',
1235
+ 'Get one platform variable in full: its symbol, tier, visibility, and EVERY place it is declared — the selector and at-rule stack, the emitted value, the source location, the coordinate it also applies at, and the derivation. Address it by the CSS name WITHOUT its leading dashes (`color-text`). Example: variable_lookup { name: ["color-text"] }.',
370
1236
  fields: [
1237
+ { name: "symbol", property: "dt:ofSymbol/rdfs:label", label: "Symbol" },
1238
+ { name: "tier", property: "dt:tier", label: "Tier" },
1239
+ { name: "visibility", property: "dt:visibility", label: "Visibility" },
1240
+ ],
1241
+ expand: [
371
1242
  {
372
- name: "category",
373
- property: "ds:tokenType/rdfs:label",
374
- label: "Type",
1243
+ name: "declarations",
1244
+ heading: "Declarations",
1245
+ kind: "table",
1246
+ relation: "dt:declaredAt",
1247
+ // 927 variables are declared once; the rest up to 17 times, which is
1248
+ // why this is an expand and not a set of fields.
1249
+ select: [
1250
+ { name: "under", property: "dt:under" },
1251
+ { name: "selector", property: "dt:under/dt-web:selector" },
1252
+ // The at-rule STACK, not one at-rule: `dt-web:inAtRule` is an
1253
+ // rdf:List, so the walk is `/rdf:rest*/rdf:first`. One condition of
1254
+ // the 31 nests two levels deep and so contributes two rows for one
1255
+ // declaration — that is the stack, read outermost-in, rather than a
1256
+ // duplicate.
1257
+ {
1258
+ name: "inAtRule",
1259
+ property: "dt:under/dt-web:inAtRule/rdf:rest*/rdf:first",
1260
+ },
1261
+ { name: "emits", property: "dt:emits" },
1262
+ // `dt:at` is the SOURCE LOCATION (`modifiers.theme.css:322`), not a
1263
+ // coordinate; `dt:alsoAt` is the coordinate. The two read alike and
1264
+ // mean different things, which is why both are labelled.
1265
+ { name: "at", property: "dt:at", label: "source" },
1266
+ { name: "alsoAt", property: "dt:alsoAt", label: "alsoAt" },
1267
+ { name: "derives", property: "dt:derives" },
1268
+ ],
375
1269
  },
376
- { name: "valueLight", property: "ds:valueLight", label: "Light value" },
377
- { name: "valueDark", property: "ds:valueDark", label: "Dark value" },
378
1270
  ],
379
1271
  sample: {
380
1272
  fixedCount: true,
381
1273
  toolDescription:
382
- "Return randomly selected complete design tokens (with theme values) as exemplars. Use BEFORE writing queries to see actual data shapes. Example: token_sample {}.",
1274
+ "Return random complete platform variables — symbol, tier, visibility and every declaration — as exemplars. Use BEFORE writing queries to see real data shapes. Example: variable_sample {}.",
383
1275
  },
384
1276
  },
385
1277
  },
@@ -394,6 +1286,16 @@ const designSystemStories: readonly PackDefinition[] = [
394
1286
  description: "List all modifier families.",
395
1287
  toolDescription:
396
1288
  "List all modifier families with their values. Use when browsing which modifier families exist and the values each allows. Example: modifier_list {}.",
1289
+ // Tiered like blocks — 11 of the modifier families carry a `ds:tier` — so
1290
+ // the same scope, declared with the same four terms. The scope is per NOUN
1291
+ // because being tiered is a fact about the entities, and the design
1292
+ // system's token symbols and code standards carry nothing of the kind.
1293
+ tierScope: {
1294
+ type: "ds:Tier",
1295
+ via: "ds:tier",
1296
+ by: "ds:name",
1297
+ base: "ds:global",
1298
+ },
397
1299
  list: {
398
1300
  query: [
399
1301
  "SELECT ?uri ?name",
@@ -454,7 +1356,7 @@ const designSystemStories: readonly PackDefinition[] = [
454
1356
  noun: "tier",
455
1357
  description: "List all tiers in the design system ontology.",
456
1358
  toolDescription:
457
- "List all tiers in the design-system ontology. Use when understanding the tier hierarchy before setting a tier filter. Example: tier_list {}.",
1359
+ "List all tiers in the design-system ontology. Use when picking the tier to read with: the `tier` parameter scopes a read to one tier plus its ancestors, and this list is never scoped itself. Example: tier_list {}.",
458
1360
  list: {
459
1361
  query: [
460
1362
  "SELECT ?uri ?name WHERE {",
@@ -498,6 +1400,16 @@ const conceptStory: PackDefinition = {
498
1400
  description: "List design-system concepts.",
499
1401
  toolDescription:
500
1402
  'List design-system concepts — long-form foundations, how-to guides, and decision guides not bound to a single UI block. Optionally filter by type or search. Example: concept_list { type: "Explanation" }.',
1403
+ // Concepts carry a `ds:tier` too (4 of them do), so they are scoped on the
1404
+ // same terms as blocks and modifier families. Its `list` publishes no tier
1405
+ // COLUMN and does not need to: the scope constrains the entity through
1406
+ // `ds:tier`, not a projected cell.
1407
+ tierScope: {
1408
+ type: "ds:Tier",
1409
+ via: "ds:tier",
1410
+ by: "ds:name",
1411
+ base: "ds:global",
1412
+ },
501
1413
  list: {
502
1414
  query: [
503
1415
  "SELECT ?uri ?name ?type ?summary",
@@ -542,7 +1454,7 @@ const conceptStory: PackDefinition = {
542
1454
  },
543
1455
  emptyRecovery: {
544
1456
  message:
545
- "No concepts in the store. The @canonical/design-system pack provides them; refresh the local store.",
1457
+ "Concepts come from the @canonical/design-system pack, and a local store built without it carries none — refresh it.",
546
1458
  cli: "sources update",
547
1459
  },
548
1460
  },
@@ -671,7 +1583,7 @@ const implementationStory: PackDefinition = {
671
1583
  },
672
1584
  emptyRecovery: {
673
1585
  message:
674
- "No implementations in the store. The @canonical/ds-implementations pack provides them; refresh the local store.",
1586
+ "Implementations come from the @canonical/ds-implementations pack, and a local store built without it carries none — refresh it.",
675
1587
  cli: "sources update",
676
1588
  },
677
1589
  },
@@ -871,10 +1783,23 @@ const codeStandardsStories: readonly PackDefinition[] = [
871
1783
  // because a pack is data — a future release that drops a label should
872
1784
  // degrade to the IRI-derived name, not become unaddressable.
873
1785
  //
874
- // Deliberately NOT declared on `block`/`token`/`tier`/`concept`: each of
875
- // those lists REQUIRES its `by` property, so an entity without one is a
876
- // row they never publish, and making it addressable (or sampleable) here
877
- // would be the mirror of the defect this repairs.
1786
+ // Deliberately NOT declared on `token`/`modifier`/`tier`/`concept`:
1787
+ // each of those lists REQUIRES its `by` property, so an entity without
1788
+ // one is a row they never publish, and making it addressable (or
1789
+ // sampleable) here would be the mirror of the defect this repairs.
1790
+ //
1791
+ // `block` is NOT in that set, though an earlier version of this comment
1792
+ // claimed it was: its list COALESCEs an OPTIONAL `ds:name` with an
1793
+ // IRI-derived fallback, so a block without a `ds:name` WOULD be
1794
+ // published under a name its lookup cannot resolve. Every shipped block
1795
+ // carries `ds:name` today, so the divergence is latent — and pinned:
1796
+ // `listLookup.shipped.exec.test.ts` runs the whole corpus through both
1797
+ // halves on every test run, and goes red the moment upstream ships an
1798
+ // unnamed block. Fix it THEN, by choosing deliberately between requiring
1799
+ // the name in the list and declaring a fallback pair whose derivations
1800
+ // actually agree (the list keeps the IRI's dots; the shared
1801
+ // `nameFallback` derivation converts them to slashes — declaring the
1802
+ // fallback alone would trade one mismatch for another).
878
1803
  nameFallback: "iri",
879
1804
  type: "cs:CodeStandard",
880
1805
  description:
@@ -982,7 +1907,20 @@ Made by the Canonical Webteam — https://canonical.com.`,
982
1907
  name: "@canonical/anatomy-dsl",
983
1908
  source: "git+https://github.com/canonical/anatomy-dsl.git#main",
984
1909
  },
985
- // Pinned to a TAG, and the only source here that is. The standards are the
1910
+ // The design-token semantic model: the authored `dt:` / `w3c-tokens:`
1911
+ // ontology in `definitions/` plus the generated strata in `data/`.
1912
+ //
1913
+ // The only source here carrying a SUBDIRECTORY, and it needs one: this
1914
+ // package lives inside a monorepo, at `packages/token-ontology`, while a
1915
+ // git source names a repository. `readTtlSources` scans
1916
+ // `<root>/definitions` and `<root>/data`, which the design-tokens repo
1917
+ // root does not have — so without the subdirectory this would clone
1918
+ // cleanly and contribute nothing.
1919
+ {
1920
+ name: "@canonical/token-ontology",
1921
+ source:
1922
+ "git+https://github.com/canonical/design-tokens.git#main:packages/token-ontology",
1923
+ },
986
1924
  // pack an agent is most likely to quote back at a human as policy, so which
987
1925
  // revision answered a query has to be recoverable — a floating ref makes
988
1926
  // "the CLI told me this was the rule" unfalsifiable. The other three still
@@ -1023,6 +1961,11 @@ Made by the Canonical Webteam — https://canonical.com.`,
1023
1961
  // `cs:` is listed even though nothing rebinds it upstream.
1024
1962
  prefixes: {
1025
1963
  ds: "https://ds.canonical.com/",
1964
+ dt: "https://dt.canonical.com/",
1965
+ // Nested UNDER `dt:`, and both must be bound: `compactUri` takes the
1966
+ // longest match, so binding only the parent would render every term of
1967
+ // this vocabulary as `dt:w3c-tokens/…`.
1968
+ "w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
1026
1969
  cs: "http://pragma.canonical.com/codestandards#",
1027
1970
  },
1028
1971
  channel: "normal",