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