@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
@@ -23,6 +23,9 @@ The Anatomy DSL represents design system components as YAML trees with:
23
23
  - **Anonymous nodes**: Structural elements with roles (`content wrapper`)
24
24
  - **Edges**: Parent-child relationships with cardinality and slot names
25
25
  - **Styles**: Platform-agnostic properties using CTI-inspired keys
26
+ - **Projections**: Graph-data bindings — which entity type a subtree is a view over, and which field each position renders
27
+ - **Props**: Pinned prop values fixing a referenced component's configuration at one position (the icon case)
28
+ - **Interaction states**: Style values scoped to a state via an `@state` key suffix (`appearance.background@hover`)
26
29
 
27
30
  This skill helps you author these specifications correctly and consistently.
28
31
 
@@ -50,7 +53,27 @@ Decision gates — in collaboration, pause at each and resolve it WITH the perso
50
53
  The full specification ships beside this file as `ANATOMY_DSL_SPEC.md` in the
51
54
  installed skill folder — `pragma skill lookup anatomy-author` renders only this
52
55
  SKILL.md, so open the sibling file directly when you need the complete spec (the
53
- appendix below carries its type system).
56
+ appendix below carries its type system). That file is a verbatim copy of
57
+ `docs/api-reference.md` in canonical/anatomy-dsl, which is the normative source for
58
+ the NOTATION: the grammar, the type system, what parses and what does not. Where this
59
+ SKILL.md and the sibling spec disagree about the notation, the spec wins, and the
60
+ disagreement is a bug to report. Reach for it for the parts too long to restate
61
+ here — the worked projections gallery, the derived-fragment contract, and the
62
+ full example set.
63
+
64
+ The spec's reach stops at the grammar, and one boundary is worth stating because it
65
+ reads like a contradiction otherwise. The spec's Deferred Items and Out of Scope
66
+ tables list what the GRAMMAR does not provide — a construct, a keyword, a schema. They
67
+ do not rule on how an author models something the grammar has no construct for. Where a
68
+ modelling convention is what is at issue — a pseudo-element drawn by the stylesheet, a
69
+ modifier family a component supports — this SKILL.md is the authority, and it says so
70
+ at each of those places. The authored corpus follows this file.
71
+
72
+ **And this file, in the design-system repository, is the authority over the copy
73
+ `pragma skill lookup anatomy-author` prints.** That copy is bundled into a pragma
74
+ release and lags the repository until the next one, so a sentence you find there and
75
+ cannot find here is old text, not a second opinion. Read the repository file when the
76
+ two differ.
54
77
 
55
78
  ## Opening move: ask, or offer the tutorial
56
79
 
@@ -163,18 +186,113 @@ pragma block lookup Card # full spec: anatomy, modifiers,
163
186
  pragma graph inspect ds:global.component.card # every triple on the entity
164
187
  ```
165
188
 
166
- A bare name can match blocks in several tiers, and `block lookup` silently picks one —
167
- it resolves `ds:name` globally and cannot be steered to a tier.
168
- Confirm the tier the lookup picked from its own `- Tier:` line; the name query below lists every tier that carries the name.
169
- The name query in Workflow §1 is the one that lists them, with each block's IRI. If the
170
- lookup picked the wrong tier's block, read the block you want with
171
- `pragma graph inspect <IRI from that row>`, so the lookup step and the inspect step
172
- describe the same block. If the name has no row at all, the block may be a Group —
173
- `block list` covers no groups; the group pre-step in Workflow §1 lists them.
189
+ A bare name resolves `ds:name` globally, so where several tiers carry the name the
190
+ lookup answers with EVERY one of them: the full writeups concatenated, each under its
191
+ own `## <Name>` heading with its own `- Tier:` line. `pragma block lookup Chip` returns
192
+ the global Chip and the launchpad Chip in one answer, and they are DIFFERENT blocks —
193
+ separately authored, with different properties and different anatomies — not repeats of
194
+ one. So the read is not "confirm the tier it picked": scroll the whole answer, read
195
+ every `- Tier:` line, count the headings, and pick the block you were asked to write.
196
+ Stopping at the first heading is how a second, unrelated component's anatomy gets
197
+ transcribed by mistake.
198
+
199
+ `block lookup` also takes a `ds:` IRI, which answers with exactly one block and skips
200
+ the disambiguation: `pragma block lookup ds:global.component.chip`. The bare dotted
201
+ name is NOT a key — a `block list` row prints `ds:apps_lxd.component.meter`, and
202
+ dropping the `ds:` prefix comes back `ENTITY_NOT_FOUND`. A glob (`*chip*`) fans out the
203
+ same way a shared display name does, one block per tier.
204
+
205
+ The name query in Workflow §1 lists every tier carrying the name with each block's IRI,
206
+ which is the read when you want the list rather than the writeups. If the name has no
207
+ row at all, the block may be a Group — `block list` covers no groups; the group
208
+ pre-step in Workflow §1 lists them.
209
+
210
+ ### The token graph answers whether a symbol exists
211
+
212
+ Every symbol you are about to write is queryable, and guessing is not authoring. Two
213
+ questions come up while transcribing a stylesheet — does this symbol exist, and what
214
+ does this variable stand for — and three commands answer them:
215
+
216
+ ```bash
217
+ pragma token lookup color.text # one symbol in full: type, description, every
218
+ # definition behind it, the families that may
219
+ # rebind it, and what it resolves to
220
+ pragma token list --search focus # which symbols exist around a word
221
+ pragma variable lookup modifier-color-text # what a CSS variable stands for
222
+ ```
223
+
224
+ `token lookup` answers with the symbol's `- Type:`, its `- Description:`, a
225
+ `### Definitions` list, a `### Covered by` list of the families that may rebind it,
226
+ and a `### Values` list. A name nothing declares comes back as `ENTITY_NOT_FOUND`,
227
+ and that is the answer: it is not a value to write. `token list --search motion`
228
+ answers `## Token (0)` today, which is why no anatomy binds a `motion.*` symbol.
229
+
230
+ `token list` is the read for "does this family exist under some other name", and it
231
+ needs a filter to be trusted. It pages at 300 rows sorted by name, so a bare
232
+ `pragma token list` returns colour tokens and nothing else — every `dimension.`,
233
+ `number.`, `typography.` and `fontWeight` symbol is off the first page. The output says
234
+ so, in a heading ending "and more exist" and a closing line offering an `--after`
235
+ cursor, but a bare list piped through `grep` swallows both and reads as an absence that
236
+ is not there. Narrow instead: `--search <word>` over name and description,
237
+ `--type <type>` for one type's population, `--channel-of <symbol>` for the channels
238
+ provisioning one symbol, and `--limit` raised past 300 when the whole population is
239
+ what you want.
240
+
241
+ `variable lookup` is the other direction, and it is the one that reads a stylesheet.
242
+ Give it the custom property WITHOUT its leading dashes. It has THREE outcomes, and only
243
+ the first two are usually anticipated:
244
+
245
+ 1. **It resolves and carries a `- Symbol:` line.** That symbol is what the variable
246
+ stands for, and it is what the anatomy binds. The answer also carries a
247
+ `### Declarations` list — one row per declaration, each naming its `selector`, its
248
+ `inAtRule` layer, what it `emits` and the `file:line` it is declared at.
249
+ `modifier-color-text` answers with one row per modifier, which is the whole family's
250
+ coverage in one read.
251
+ 2. **It resolves and carries NO `- Symbol:` line at all.** The variable is a DERIVED
252
+ one — a state product or a delta, autogenerated from another variable rather than
253
+ authored — and it stands for no symbol, so there is nothing to bind.
254
+ `hover--color-foreground-secondary` is one: it resolves, its tier is
255
+ `dt:tier.derived`, its `emits` is an `oklch(from …)` expression, and no `- Symbol:`
256
+ row appears. Do not read the missing line as a failed lookup and do not bind the
257
+ name. Ask what it reaches, and carry on down the implementation's own fallback chain
258
+ to the next variable, which is usually where the real symbol is:
259
+
260
+ ```bash
261
+ pragma variable chain --variable hover--color-foreground-secondary
262
+ ```
263
+
264
+ 3. **`ENTITY_NOT_FOUND`.** The graph has never heard of the name — `--button-gap`,
265
+ `--icon-size` and `--focus-outline-width` all answer this way, because they are
266
+ component-local properties and not design tokens. That is the case for a `#` comment
267
+ on the line rather than a binding.
268
+
269
+ **A component-local variable is rarely the end of the chain — follow every hop.** The
270
+ `ENTITY_NOT_FOUND` says the token graph does not declare the NAME; it says nothing about
271
+ what the stylesheet declares the name AS. A component's own `:root` block routinely
272
+ aliases a declared variable behind a local one, and the alias can be several hops deep:
273
+
274
+ ```css
275
+ :root {
276
+ --chip-color-background: var(--color-foreground-secondary);
277
+ }
278
+ ```
279
+
280
+ `chip-color-background` is `ENTITY_NOT_FOUND`, and `color-foreground-secondary`
281
+ resolves to `color.foreground.secondary`. Skipping the hop loses a real binding. So for
282
+ every `ENTITY_NOT_FOUND` on a `--<component>-*` name, go back to the component's own
283
+ stylesheet, read what that property is declared as, and look THAT name up — repeating
284
+ until you reach a declared variable (bind its symbol) or a literal (write the literal
285
+ where the key admits one, and comment it where it does not). Only a chain that ends in
286
+ neither is a `#` comment saying what the implementation reads.
287
+
288
+ The same verbs are MCP tools under the names `token_lookup`, `token_list`,
289
+ `variable_lookup` and `variable_chain`, with the same arguments; use whichever surface
290
+ you are on, and take the current catalog from `pragma capabilities`.
174
291
 
175
292
  ### Discovery Queries
176
293
 
177
- Common prefixes (`ds:`, `cs:`) are applied automatically — no PREFIX preamble needed.
294
+ Common prefixes (`ds:`, `cs:`, `dt:`) are applied automatically — no PREFIX preamble
295
+ needed.
178
296
 
179
297
  **Find components by tier:**
180
298
  ```bash
@@ -190,16 +308,19 @@ pragma graph query "SELECT ?component ?name WHERE {
190
308
  pragma graph query "SELECT ?sub ?name WHERE {
191
309
  ?sub a ds:Subcomponent ;
192
310
  ds:name ?name ;
193
- ds:parentComponent <https://ds.canonical.com/global.component.card> .
311
+ ds:parentComponent ds:global.component.card .
194
312
  }"
195
313
  ```
196
314
 
197
- Inside a SPARQL body, a local name with MORE THAN ONE dot (`ds:global.component.card`)
198
- does not parse — use the full IRI there: join `https://ds.canonical.com/` with the
199
- dotted local name (giving `<https://ds.canonical.com/global.component.card>`), or copy
200
- one from `pragma graph query` output, which prints absolute IRIs. Prefixed names with
201
- no dot (`ds:global`, `ds:Component`) or a single dot (`ds:tag.needsdefinition`) work
202
- as-is.
315
+ A prefixed name carrying dots or slashes parses inside a SPARQL body, so write the name
316
+ the graph prints: `ds:global.component.card` and `dt:s4/web/cond/layer-ds-states/universal`
317
+ both resolve, as do the dot-free (`ds:Component`) and single-dot
318
+ (`ds:tag.needsdefinition`) forms. The full IRI
319
+ (`<https://ds.canonical.com/global.component.card>`) is accepted too and answers
320
+ identically — it is what `pragma graph query` prints back, so a copied row is always
321
+ safe. What is NOT accepted is the prefixed name inside angle brackets, `<ds:…>`: that
322
+ returns an EMPTY table rather than an error, which reads as "no such thing" when the
323
+ thing is there.
203
324
 
204
325
  ### The empty-anatomy worklist
205
326
 
@@ -212,8 +333,58 @@ pragma graph query "SELECT ?b ?name WHERE { ?b ds:anatomyDsl ?a ; ds:name ?name
212
333
 
213
334
  The worklist rows point at blocks whose data lives under `data/` — never hand-edit
214
335
  that tree: it is regenerated destructively from Coda by CI, and hand edits are
215
- overwritten by the next sync. An authored DSL lands in the block's spec draft under
216
- `specs/` (see `specs/README.md`) or is pasted into Coda by a human.
336
+ overwritten by the next sync. An authored anatomy lands in a file of its own, and
337
+ "Where the anatomy lands" below is the whole path from that file to the document.
338
+
339
+ ### Where the anatomy lands
340
+
341
+ An anatomy is written by hand, from the component's implementation stylesheet, into
342
+ one file in the design-system repository:
343
+
344
+ anatomies/authored/<tier>/<uri>.yaml
345
+
346
+ The file name IS the dotted uri (`anatomies/authored/global/global.component.button.yaml`),
347
+ which is how the write finds the row it belongs to, and the text of the file is the
348
+ text of the `anatomy_dsl` cell verbatim. Open the file with a comment saying where it
349
+ was read from — the package, the stylesheet and the selector — because the next reader
350
+ has to be able to check it:
351
+
352
+ ```yaml
353
+ ---
354
+ # Authored 2026-09-13 from @canonical/react-ds-global src/lib/component/Button/styles.css on .ds.button
355
+ node:
356
+ uri: global.component.button
357
+ ```
358
+
359
+ Then, in order:
360
+
361
+ 1. **Check it offline.** `bun src/cli.ts anatomies validate --authored` reads every
362
+ authored file and runs the law over it, with no network and no document. It answers
363
+ with the count it read, any warnings, and a verdict — `✓ 19 warning(s), 0 findings
364
+ — every file is lawful` — and exits 0 when the corpus is lawful. Findings are what
365
+ you fix; the `AT11` state-differs-from-base lint is a warning and is usually the
366
+ implementation telling the truth.
367
+ 2. **Review it as a file.** The file goes through a pull request like any other
368
+ change, which is the point of authoring in files: the node boundaries, the
369
+ comments and the symbol choices are all readable in a diff.
370
+ 3. **Write it to the document.** `anatomies write` sends the authored files to the
371
+ `anatomy_dsl` cells, and it is dry by default — run it with no flags first and read
372
+ the plan, then one anatomy as a canary with `--only <uri> --apply` and look at the
373
+ cell, then `--apply` for the rest. It snapshots every cell it is about to touch
374
+ before its first write, and `anatomies restore <snapshot>` puts them back. The
375
+ repository README's anatomies section is the authority on the gates that step runs
376
+ behind; do not improvise around them.
377
+
378
+ **After the write, the document is the source of record.** `data/` is regenerated from
379
+ it by the pull sync, the pack is built from `data/`, and that is the chain a
380
+ `pragma block lookup` answer comes down. Until an authored anatomy has been written
381
+ and the pack rebuilt, `block lookup`'s `### Anatomy (DSL)` section still answers with
382
+ whatever the cell said before — today, for most blocks, the retired notation. A
383
+ `color/text/muted` in that output is history, not a house style to copy.
384
+
385
+ There is no derivation step, no `references.yaml` and no register category for a
386
+ stylesheet fact: reading an implementation and choosing the node, the key and the
387
+ symbol is judgement, which is why it happens in a file under review.
217
388
 
218
389
  ## DSL Reference
219
390
 
@@ -234,15 +405,22 @@ node:
234
405
  AnatomySpec
235
406
  -> node: NamedNode (root must be named)
236
407
 
408
+ Style keys may carry an @state suffix ("appearance.background@hover");
409
+ the unmarked key is the default state.
410
+
237
411
  NamedNode
238
412
  -> uri: string ("tier.type.name")
239
- -> styles?: Record<string, TokenPath | TokenPath[] | PrimitiveValue>
413
+ -> projection?: Projection
414
+ -> props?: Record<string, string | number | boolean>
415
+ -> styles?: Record<StyleKey, Symbol | [...Symbol[], Primitive?] | Primitive>
240
416
  -> edges?: Edge[]
241
417
 
242
418
  AnonymousNode
243
419
  -> role: string ("content wrapper")
244
- -> styles?: Record<string, TokenPath | TokenPath[] | PrimitiveValue>
420
+ -> projection?: Projection
421
+ -> styles?: Record<StyleKey, Symbol | [...Symbol[], Primitive?] | Primitive>
245
422
  -> edges?: Edge[]
423
+ (no props — anonymous nodes have no prop surface)
246
424
 
247
425
  Edge
248
426
  -> node?: NamedNode | AnonymousNode
@@ -256,11 +434,19 @@ Switch
256
434
  SwitchCase
257
435
  -> uri?: string (shorthand)
258
436
  -> node?: Node (full form)
259
- -> default?: boolean (marks the default case)
437
+ (exactly one of the two; there is NO `default` marker)
260
438
 
261
439
  Relation
262
440
  -> cardinality: string ("1", "0..1", "0..*", "1..*")
263
441
  -> slotName?: string ("default", "header", "icon")
442
+ -> projection?: RelationProjection
443
+
444
+ Projection (on a node — at least one of the two)
445
+ -> on?: string GraphQL type condition ("Component")
446
+ -> field?: string dot-delimited field path ("_meta.title")
447
+
448
+ RelationProjection (on a relation — the traversal filling the slot)
449
+ -> field: string required; never carries `on`
264
450
  ```
265
451
 
266
452
  ### URI Encoding Convention
@@ -317,6 +503,43 @@ Subcomponents must reference their parent in the name:
317
503
 
318
504
  **Rule of thumb:** Ask "Can/should a user write `<ComponentName>` in their code?" If yes, use `uri`. If no, use `role`.
319
505
 
506
+ **A part the stylesheet draws with `::before` or `::after` is a node.** It is visible,
507
+ it takes paint of its own, and it is not something a consumer fills — so it is an
508
+ anonymous `role:` node with a comment saying how it is drawn, and it has no
509
+ `slotName`. The live checkbox glyph is the case:
510
+
511
+ ```yaml
512
+ edges:
513
+ - node:
514
+ # drawn as ::before
515
+ role: selection glyph
516
+ styles:
517
+ layout.position: absolute
518
+ appearance.background@selected: [surface.color.foreground.checkbox.checkmark, color.foreground.checkbox.checkmark]
519
+ relation:
520
+ cardinality: "1"
521
+ ```
522
+
523
+ The DSL has no pseudo-element construct, and it does not need one: the node above is an
524
+ ordinary anonymous node, and the `# drawn as ::before` comment is what records which
525
+ pseudo-element the implementation used. The spec's Deferred Items row on pseudo-elements
526
+ is about that missing GRAMMAR — a first-class pseudo-element type — not about whether
527
+ the part is modelled. It is modelled, by the convention above, and the authored corpus
528
+ does it this way. So a `::before` separator, glyph or overlay is never left out of a
529
+ tree because the grammar has no keyword for it.
530
+
531
+ **A native control's own chrome is turned off, not modelled.** Where the
532
+ implementation writes `appearance: none` over a native `<input>` or `<select>` and
533
+ draws the control itself, the anatomy says so on the node that IS the control:
534
+
535
+ ```yaml
536
+ node:
537
+ uri: global.subcomponent.checkbox_input
538
+ styles:
539
+ appearance.native: none
540
+ appearance.border.color: color.border
541
+ ```
542
+
320
543
  ### DRY Principle
321
544
 
322
545
  When a node has a URI (is not anonymous), it references its own DSL file. Do not inline the full tree—reference the URI only. A named child MAY carry `styles:` that override its own anatomy in this context — live, `apps_landscape.component.password_constraints` sizes down its `global.component.icon` child — but never a copy of its subtree.
@@ -343,12 +566,29 @@ edges:
343
566
  - node:
344
567
  uri: global.component.icon
345
568
  styles:
346
- size.width: size/icon/small
347
- size.height: size/icon/small
569
+ size.width: dimension.300
570
+ size.height: dimension.300
348
571
  relation:
349
572
  cardinality: "1"
350
573
  ```
351
574
 
575
+ **Which blocks a `uri:` may reach.** A reference points at a block in the same tier or
576
+ in `global`, and never the other way: a `global` anatomy that reached into an app
577
+ tier would make the global block depend on one product. Where the part is real but the
578
+ block it would name does not exist, write an anonymous `role:` node and say so in a
579
+ comment — the reference is a claim about the graph, and a claim about a block nobody
580
+ has created is one an anatomy must not make:
581
+
582
+ ```yaml
583
+ edges:
584
+ - node:
585
+ # The list is the Landscape table's own, and no block names it yet.
586
+ role: result list
587
+ relation:
588
+ cardinality: "1"
589
+ slotName: default
590
+ ```
591
+
352
592
  ### Cardinality Notation
353
593
 
354
594
  | Notation | Meaning | Example Use |
@@ -371,7 +611,6 @@ edges:
371
611
  on: <discriminator>
372
612
  cases:
373
613
  - uri: <component-uri>
374
- default: true # optional, marks default case
375
614
  - uri: <component-uri>
376
615
  - uri: $custom # reserved URI for user-provided components
377
616
  relation:
@@ -391,6 +630,11 @@ This enum is NORMATIVE. Live anatomies that predate it may carry qualified
391
630
  discriminators (`props/<prop>`) or a `with:` map in exploratory drafts — do not
392
631
  copy those into new anatomies.
393
632
 
633
+ A case carries `uri` or `node`, and nothing else. There is no `default:` marker
634
+ in the DSL: `SwitchCase` is `additionalProperties: false`, so a case carrying one
635
+ FAILS schema validation. Which alternative is the fallback is the component's
636
+ behaviour, not its anatomy — say it in a YAML comment if it matters.
637
+
394
638
  #### Shorthand Expansion
395
639
 
396
640
  The shorthand `- uri: X` expands to `- node: { uri: X }`. This allows cases to be expressed concisely when no additional metadata is needed:
@@ -414,7 +658,7 @@ cases:
414
658
  uri: global.subcomponent.textarea_input
415
659
  styles:
416
660
  size.height: hug
417
- size.min.height: size/input/multiline
661
+ size.min.height: 6rem
418
662
  ```
419
663
 
420
664
  #### Reserved URI: `$custom`
@@ -482,6 +726,104 @@ node:
482
726
  cardinality: "1..*"
483
727
  ```
484
728
 
729
+ ### Projections
730
+
731
+ A projection binds a position in the tree to graph data, after the Relay
732
+ fragment-colocation pattern: the tree carries its own data requirements. Two
733
+ places take one, and they mean different things.
734
+
735
+ ```yaml
736
+ ---
737
+ node:
738
+ uri: global.component.entity-card
739
+ projection:
740
+ on: Component # type condition — this tree is a view over one Component
741
+ edges:
742
+ - node:
743
+ uri: global.subcomponent.entity-card-header
744
+ projection:
745
+ field: _meta.title # this node RENDERS the entity's title
746
+ relation:
747
+ cardinality: "1"
748
+ slotName: header
749
+ - node:
750
+ uri: global.component.chip
751
+ projection:
752
+ field: _meta.title # relative to the TRAVERSED entity, not the root
753
+ relation:
754
+ cardinality: "0..*"
755
+ slotName: tags
756
+ projection:
757
+ field: documentationStages # traversal populating the slot
758
+ ```
759
+
760
+ | Position | Field | Means |
761
+ |----------|-------|-------|
762
+ | Root node | `on` | Establishes the data context — the anatomy is a view over one entity of that type. Children inherit it. |
763
+ | Child node | `on` | Narrows the traversed entity's type — the analog of an inline fragment. |
764
+ | Any node | `field` | This node renders that field's value. |
765
+ | Relation | `field` | Traversal: the slot is populated from that field of the current context. Required, and never carries `on`. |
766
+
767
+ A node projection needs at least one of `on` / `field`. Both are schema names:
768
+ `on` matches `^[A-Z][A-Za-z0-9_]*$`, `field` is dot-delimited
769
+ (`^[_A-Za-z][_A-Za-z0-9]*(\.[_A-Za-z][_A-Za-z0-9]*)*$`).
770
+
771
+ Two rules a checker cannot enforce from the anatomy alone — hold them yourself:
772
+
773
+ - **Cardinality decomposes against the schema.** The upper bound claims
774
+ multiplicity (`..1` an object or scalar field, `..*` a connection or list);
775
+ the lower bound claims nullability (`0..` tolerates `null`, `1..` requires the
776
+ provider to always have the value). So `field: _meta.title` may sit under
777
+ `cardinality: "1"`, but a nullable `summary` must sit under `0..1`. `1..*`
778
+ asserts a non-empty list — stronger than GraphQL can express, checkable only
779
+ at runtime.
780
+ - **Mechanism-blindness.** Whether a plural field is a Relay connection or a
781
+ plain list is a provider mechanism, not an anatomy fact. NEVER write an
782
+ unwrapping path like `edges.node` — consumers discover the shape from the SDL.
783
+ This is what lets an anatomy survive a provider promoting a list to a
784
+ connection unchanged.
785
+
786
+ Projections are optional. A structure-only anatomy stays valid, and unprojected
787
+ positions simply contribute nothing to the derived fragment. Add them when the
788
+ component's whole point is rendering a known entity; leave them off for generic
789
+ containers. The committed provider SDL is the naming authority — check field
790
+ names against it rather than guessing.
791
+
792
+ ### Pinned Props
793
+
794
+ A named node can PIN props of the component it references, fixing a value at
795
+ one tree position:
796
+
797
+ ```yaml
798
+ node:
799
+ uri: global.component.icon
800
+ props:
801
+ icon: chevron-down
802
+ ```
803
+
804
+ | Aspect | Rule |
805
+ |--------|------|
806
+ | Keys | Prop names as defined on the component in the DS ontology (camelCase). |
807
+ | Values | Scalars only — no token paths, no fallback arrays. Pins are values, not styles. |
808
+ | Nodes | Named nodes ONLY. `props` under a `role` node is rejected by the parser and the schema. |
809
+
810
+ The DSL never DEFINES a prop surface — names, types and optionality live in the
811
+ DS ontology (`ds:hasProperty`). A pin asserts one value at one position. Whether
812
+ the prop exists, and whether the value is admissible, are checks against the
813
+ graph: `pragma block lookup <Component>` lists the properties.
814
+
815
+ **Icons are the canonical case**, and need no icon-specific construct. The icon
816
+ is a component whose glyph is a required prop, so icon usage splits in two:
817
+
818
+ | Case | Authoring |
819
+ |------|-----------|
820
+ | Consumer-filled icon slot (Button's `slotName: icon`) | Icon-component edge with a slot and NO pin — the consumer chooses the glyph, and the anatomy correctly says nothing. |
821
+ | Component-intrinsic icon (accordion chevron, modal close ×, status glyph) | Icon-component node with `props: { icon: … }` — the spec fixes the glyph. |
822
+
823
+ **Pins are static by design.** A data-driven value is a projection
824
+ (`projection: { field: … }`); a state-driven one is a `switch` whose cases pin
825
+ different values. If a value varies at runtime, it is not a pin.
826
+
485
827
  ### Slot Names
486
828
 
487
829
  Common slot conventions:
@@ -498,71 +840,297 @@ Common slot conventions:
498
840
 
499
841
  **Convention:** `slotName: default` implies the main children slot (`children` in React, default slot in Vue).
500
842
 
501
- ### Style Key Categories
843
+ ### A style value is the symbol consumed
502
844
 
503
- Styles use CTI-inspired dot-notation: `category.type[.item]`
845
+ A style value is **the name of a token symbol**, written in the symbol's own dotted
846
+ spelling — or **an ordered list of symbol names**, which is the fallback order the
847
+ implementation reads, ending in the one literal it falls back to last.
504
848
 
505
- #### Layout (typically invariant)
506
849
  ```yaml
507
- layout.type: stack | flow | grid
508
- layout.direction: horizontal | vertical
509
- layout.align: start | center | end | stretch
510
- layout.justify: start | center | end | space-between | space-around
511
- layout.wrap: true | false
512
- layout.display: block | flex | grid | none
513
- layout.flex: 1 | auto
514
- layout.overflow: hidden | scroll | visible
850
+ styles:
851
+ # One symbol.
852
+ typography.weight: typography.weight.medium
853
+ # A fallback order: the channel first, then the semantic token behind it. This is
854
+ # the implementation's own `var()` chain, transcribed.
855
+ typography.color: [modifier.color.text, color.text]
856
+ # A chain that ends in a literal keeps the literal: an implementation regenerated
857
+ # from this anatomy has to be able to write the whole chain.
858
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing, currentColor]
859
+ # A primitive is a literal the implementation writes, and resolves against nothing.
860
+ layout.type: inline-flex
515
861
  ```
516
862
 
517
- #### Spacing (invariant if literal, themeable if token)
863
+ Three things that are **retired** and rejected by the parser:
864
+
865
+ | Retired | Write instead |
866
+ |---|---|
867
+ | a slash path — `color/text/muted` | the symbol's dotted name — `color.text.muted` |
868
+ | a trailing `?` marker — `shadow/card?` | nothing: a name no stratum declares is not written at all, and a `#` comment on the line records it |
869
+ | `root` or `$root` inside a value | the dotted name without it — `color.text` |
870
+
871
+ **Consume the channel where the implementation reads the channel.** A modifier or
872
+ surface family reaches a component through a channel variable, and the anatomy says so
873
+ by consuming the channel by name: `modifier.color.text` is the channel of `color.text`,
874
+ and it is a symbol of its own. Where the implementation reads the semantic token
875
+ directly, the anatomy consumes the semantic token. Nothing is inferred either way.
876
+
877
+ **This is also how a component's SUPPORT for a modifier family is expressed — and it is
878
+ the only way.** There is no key that names a family, no `@state` for one, and no
879
+ `switch` discriminator over one: a family is never written into an anatomy by name.
880
+ What an anatomy says is which channels it reads, and a channel is where a family lands.
881
+ So `typography.color: [modifier.color.text, color.text]` is the statement "whatever
882
+ family covers `color.text` reaches this node's text colour", and
883
+ `appearance.background: [surface.color.background, color.background]` is the same
884
+ statement for the surface side. Which families those are is a question for the token
885
+ graph, not the anatomy — `pragma token lookup color.text` lists them under
886
+ `### Covered by`, and `pragma token values --symbol modifier.color.text` shows the
887
+ routing per modifier. Button is the worked chain: it carries the Anticipation family,
888
+ its text colour consumes `modifier.color.text`, and that channel routes
889
+ `anticipation.caution` to `color.text.warning` and `anticipation.destructive` to
890
+ `color.text.destructive` — while Button's own anatomy says the word "Anticipation"
891
+ nowhere. The family is in the channel, not in the tree.
892
+
893
+ Two consequences worth holding on to. A component whose spec claims a family and whose
894
+ anatomy consumes no channel of the symbols that family covers does not in fact support
895
+ it — that mismatch is a finding, and the anatomy is where it shows. And the reverse:
896
+ where the stylesheet reads the plain semantic token with no channel in front of it, the
897
+ anatomy binds the plain token, and the component is correctly saying it is NOT
898
+ family-sensitive at that key. Do not add a channel to express a family the
899
+ implementation does not read.
900
+
901
+ **A name no stratum declares is never written. The comment is the record.** An
902
+ implementation reads plenty of variables that stand for no symbol — a component-local
903
+ `--button-gap`, a state variable the stylesheet computes, a family that has not
904
+ landed. The rule is to bind what does resolve and say in a `#` comment what the
905
+ implementation reads first, on the line it belongs to:
906
+
518
907
  ```yaml
519
- spacing.internal: spacing/medium # padding (themeable)
520
- spacing.external: spacing/large # margin (themeable)
521
- spacing.gap: spacing/small # gap between children (themeable)
522
- spacing.margin.bottom: spacing/small # specific margin
908
+ styles:
909
+ # reads --modifier-color-text-disabled first; no symbol declares it.
910
+ typography.color@disabled: color.text.disabled
911
+ # The transition reads --motion-duration-fast and --motion-easing-standard; both
912
+ # bindings are dropped because no symbol declares either name and neither var()
913
+ # carries a literal fallback.
914
+ motion.property: "background-color, color"
523
915
  ```
524
916
 
525
- #### Appearance (typically themeable)
917
+ The comment is where a reader meets the fact, and it travels with the value — there is
918
+ no register row for an authored file, and nothing is substituted merely to make a name
919
+ resolve. `pragma token lookup <symbol>` settles whether a name resolves before you
920
+ write it; `pragma variable lookup <name>` settles what a variable in the stylesheet
921
+ stands for (see Discovery Flow above).
922
+
923
+ **A list is the implementation's fallback chain, and the first element wins.** The head
924
+ is what the slot reads, each later element is what it falls back to, and a chain may
925
+ end in the one literal the implementation writes last. A list of one element is written
926
+ as the scalar instead, and a primitive anywhere but last is a parse error.
927
+
928
+ ### The style keys are a closed roster
929
+
930
+ There are 111 style keys and they are the DSL's own vocabulary, published as
931
+ `@canonical/anatomy-dsl`'s `definitions/style-keys.yaml`. That file is the authority
932
+ on every question of the form "what does this key take" — it is installed, so read it
933
+ rather than reasoning from the examples here:
934
+
935
+ ```bash
936
+ less node_modules/@canonical/anatomy-dsl/definitions/style-keys.yaml
937
+ ```
938
+
939
+ Each key carries a `valueKind` (`token`, `primitive` or `either`) and, where it takes a
940
+ token, the one `namespace` whose symbols it admits. Those two fields settle it: if a key
941
+ says `either`, a literal is legal there whatever the examples below happen to show, and
942
+ if it names a namespace, a symbol outside that namespace is not legal there however
943
+ well it reads. The roster's own header says how each value was measured, which is worth
944
+ reading once — a key exists because an implementation binds the property, and
945
+ `valueKind` is what the measurement found, not a preference.
946
+
947
+ A key outside the roster is rejected, and each key states what it admits:
948
+
949
+ - **token** — a symbol, always. `appearance.border.color`, `size.inline`.
950
+ - **primitive** — a literal, always. `layout.type`, `appearance.border.style`.
951
+ - **either** — a symbol or a literal, because the corpus binds both.
952
+ `appearance.background`, `spacing.gap`.
953
+
954
+ And each token-taking key admits ONE namespace, in three spellings — the family, its
955
+ modifier channel and its surface channel:
956
+
957
+ | Key family | Namespace it admits |
958
+ |---|---|
959
+ | `appearance.background`, `appearance.border.*.color`, `appearance.outline.color`, `typography.color` | `color.`, `modifier.color.`, `surface.color.` |
960
+ | `appearance.border.width`, `appearance.outline.*`, `appearance.radius`, `layout.flex.basis`, `layout.offset.*`, `size.*`, `typography.letterSpacing`, `typography.size`, `typography.decoration.*` | `dimension.`, `modifier.dimension.`, `surface.dimension.` |
961
+ | `spacing.gap*`, `spacing.internal.*`, `spacing.external.*` | `spacing.`, `modifier.spacing.`, `surface.spacing.` |
962
+ | `typography.font`, `typography.fontFamily`, `typography.lineHeight`, `typography.weight` | `typography.`, `modifier.typography.`, `surface.typography.` |
963
+ | `motion.duration`, `motion.easing` | `motion.`, `modifier.motion.`, `surface.motion.` |
964
+
965
+ Read the table as a summary of the roster, not as a substitute for it: where the two
966
+ differ the roster is right and this table is stale. Note in particular that several
967
+ keys in the `dimension.` row are `either`, not `token` — `appearance.radius` is one, so
968
+ `appearance.radius: 1rem` is a lawful binding where the implementation writes `1rem`
969
+ literally and declares no variable. The namespace constrains which SYMBOL may go on a
970
+ key; it does not turn an `either` key into a token-only one.
971
+
972
+ **Spacing keys admit `dimension.*` too, for now.** Not one of the twelve `spacing.*`
973
+ roles resolves to the same dimension in every product context, and the implementations
974
+ read `--dimension-*` directly for padding and gaps. So where the stylesheet reads
975
+ `--dimension-200`, the anatomy writes `dimension.200` on the spacing key, and the
976
+ second namespace is in the roster for exactly as long as that is true — it comes back
977
+ out once a semantic spacing namespace lands. Write the symbol the implementation reads,
978
+ not the role you would have chosen.
979
+
980
+ **Not every CSS property has a key.** `box-shadow`, `animation`, a vendor prefix, a
981
+ mask: no key in the roster takes them, so an anatomy does not bind what the
982
+ implementation consumes through them, and a `#` comment on the node says what was
983
+ left unbound. Do not invent a key for a property that has none.
984
+
985
+ ### The keys, by family
986
+
987
+ Every key below is in the roster, and the comment says what it admits. Where a key
988
+ takes `either`, the choice is not taste: bind the symbol where the implementation
989
+ reads a variable, and write the literal where it writes a literal.
990
+
991
+ #### Layout — primitives throughout
992
+ ```yaml
993
+ layout.type: inline-flex # the display type, as CSS spells it
994
+ layout.direction: column
995
+ layout.align: center # align-items, on the node that is the container
996
+ layout.alignSelf: center # align-self, on the child that overrides it
997
+ layout.alignContent: center # align-content, on the container
998
+ layout.justify: space-between
999
+ layout.wrap: wrap
1000
+ layout.overflow: hidden
1001
+ layout.flex.grow: 1
1002
+ layout.flex.basis: 20rem # either: a dimension symbol or a literal
1003
+ layout.position: relative
1004
+ layout.grid.columns: repeat(2, 1fr)
1005
+ layout.offset.top: dimension.100 # either: dimension.*
1006
+ ```
1007
+
1008
+ **`layout.align` is `align-items` and nothing else.** The roster carries three keys
1009
+ where CSS has three properties, so there is nothing to disambiguate by context: a
1010
+ child's `align-self: center` is `layout.alignSelf: center` on that child, and
1011
+ `align-content` is `layout.alignContent`. Writing `layout.align` on a leaf that
1012
+ overrides its parent's alignment says the wrong thing — it reads as "this node aligns
1013
+ ITS children" — and the gate will not catch it, because both keys are in the roster and
1014
+ both take a primitive. Take the CSS property from the stylesheet and map it to the key
1015
+ of the same name.
1016
+
1017
+ #### Spacing — a `spacing.*` role, or the `dimension.*` the stylesheet reads
1018
+ ```yaml
1019
+ spacing.internal.inline.start: spacing.inset.action.inline # padding, one side
1020
+ spacing.internal.block.start: spacing.inset.surface.block
1021
+ spacing.external.block.end: spacing.inset.surface.block # margin, one side
1022
+ spacing.gap: spacing.gap.mark.inline # gap between children
1023
+ spacing.gap.block: spacing.gap.group.block # token only
1024
+ spacing.internal.inline.end: dimension.200 # --dimension-200, read directly
1025
+ ```
1026
+
1027
+ **A symmetric CSS shorthand becomes both keys, written out.** There is no key for
1028
+ `padding-block` or `padding-inline` as a pair, so a stylesheet's
1029
+ `padding-block: var(--spacing-x)` is TWO bindings — `spacing.internal.block.start` and
1030
+ `spacing.internal.block.end`, each carrying the same value — and `padding-inline`
1031
+ likewise on `.inline.start` and `.inline.end`. The same holds for `margin-block` and
1032
+ `margin-inline` on the `spacing.external.*` keys. Both sides are written; neither is
1033
+ left implicit:
1034
+
526
1035
  ```yaml
527
- appearance.background: color/surface/card
528
- appearance.border: border/style/default
529
- appearance.border.top: border/style/divider
530
- appearance.shadow: shadow/elevated/medium
531
- appearance.radius: shape/rounded/full
1036
+ # padding-inline: var(--spacing-inset-action-inline)
1037
+ spacing.internal.inline.start: spacing.inset.action.inline
1038
+ spacing.internal.inline.end: spacing.inset.action.inline
532
1039
  ```
533
1040
 
534
- #### Size (invariant if structural, themeable if token)
1041
+ #### Appearance — colour and dimension
535
1042
  ```yaml
536
- size.width: fill | hug | 100% # invariant
537
- size.width: size/card/width # themeable
538
- size.max.width: size/container/max
539
- size.max.height: size/media/max
540
- size.min.height: 48px
1043
+ appearance.background: [modifier.color.foreground.primary, color.foreground.primary]
1044
+ appearance.border.color: [modifier.color.border, color.border]
1045
+ appearance.border.width: dimension.stroke.thickness.medium
1046
+ appearance.border.style: solid # primitive
1047
+ appearance.radius: dimension.radius.medium
1048
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing, currentColor]
1049
+ appearance.outline.width@focus: dimension.stroke.thickness.large
1050
+ appearance.opacity: 0.5 # primitive
541
1051
  ```
542
1052
 
543
- #### Typography (typically themeable)
1053
+ #### Size — a dimension symbol, or a literal where the size is structural
544
1054
  ```yaml
545
- typography.size: font/size/body
546
- typography.weight: font/weight/bold
547
- typography.color: color/text/primary
548
- typography.align: center | left | right # can be invariant
549
- typography.line.height: 1.5
1055
+ size.width: 100% # either: a literal is right here
1056
+ size.max.width: 60rem
1057
+ size.min.height: dimension.900 # token only
1058
+ size.inline: dimension.400 # token only
550
1059
  ```
551
1060
 
552
- #### Interaction (typically invariant)
1061
+ #### Typography
553
1062
  ```yaml
554
- interaction.cursor: pointer | default
555
- interaction.transition.property: background
556
- interaction.transition.duration: transition/duration # token = themeable
557
- interaction.transition.timing: ease
1063
+ typography.font: typography.text.primary # the composite
1064
+ typography.size: dimension.size.fontSize.200
1065
+ typography.weight: typography.weight.medium
1066
+ typography.lineHeight: typography.text.primary
1067
+ typography.color: [modifier.color.text, color.text]
1068
+ typography.letterSpacing: dimension.letterSpacing.wide
1069
+ typography.align: center # primitive
558
1070
  ```
559
1071
 
560
- #### Object (for media)
1072
+ #### Interaction and motion
561
1073
  ```yaml
562
- object.fit: cover | contain | fill
563
- object.position: center | top
1074
+ interaction.cursor: pointer # primitive
1075
+ interaction.select: none # primitive
1076
+ motion.property: background-color # primitive
1077
+ # No stratum declares a motion.* symbol yet, so a duration or an easing the
1078
+ # stylesheet reads through a variable is not bound at all — a `#` comment on the
1079
+ # node says what it reads. A literal the stylesheet writes literally is written.
1080
+ motion.easing: ease-in-out # either: a literal until the family lands
564
1081
  ```
565
1082
 
1083
+ ### Interaction States (`@state`)
1084
+
1085
+ A style key MAY take an `@state` suffix scoping its value to one interaction
1086
+ state. The unmarked key IS the default state — `@default` is invalid.
1087
+
1088
+ ```yaml
1089
+ styles:
1090
+ interaction.cursor: pointer
1091
+ interaction.cursor@disabled: not-allowed
1092
+ appearance.background: color.foreground.primary
1093
+ appearance.background@hover: color.foreground.primary.hover
1094
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing] # a channel may exist only in a state
1095
+ ```
1096
+
1097
+ **Vocabulary — closed, registry-governed.** Eight states, and only these:
1098
+
1099
+ | State | Meaning | What the implementation keys off |
1100
+ |-------|---------|----------------------------------|
1101
+ | `hover` | Pointer over the element | `:hover` |
1102
+ | `active` | Being pressed or activated (MD3 *pressed*, Spectrum *down*) | `:active` |
1103
+ | `disabled` | Interaction unavailable | `:disabled`, `aria-disabled` |
1104
+ | `focus` | Keyboard focus indication | `:focus-visible` on the web |
1105
+ | `selected` | Chosen within a set | `aria-selected`, and `:checked` where the control is one of a set |
1106
+ | `expanded` | A disclosure is open | `[open]`, `aria-expanded` |
1107
+ | `indeterminate` | A checkbox or a progress is mixed, on neither setting | `:indeterminate`, `aria-checked="mixed"` |
1108
+ | `invalid` | Validation has failed | `:invalid`, `aria-invalid` |
1109
+
1110
+ The last three are the newest, and how they arrived is the rule: candidates
1111
+ (`checked`, `visited`, `dragged`, `pending`, `error`, `read-only`) enter through
1112
+ the registry with a definition and a per-platform mapping — NEVER by loosening the
1113
+ schema locally. If an anatomy seems to need a ninth state, that is a registry
1114
+ proposal, not a local decision. `open` is not a second spelling of `expanded`.
1115
+
1116
+ **States re-value channels; they never add structure.** This is the line to
1117
+ hold:
1118
+
1119
+ - A state that changes the TREE is a `switch`, not a state. The test is whether
1120
+ the implementation RE-VALUES a channel in the state or changes what exists: a
1121
+ disclosure header that takes a different background while open is `@expanded`;
1122
+ the panel that exists only while open is a `switch`, and the same disclosure
1123
+ carries both.
1124
+ - The gate and the appearance are separate questions.
1125
+ `props: { disabled: true }` PUTS a node in the disabled state; `…@disabled`
1126
+ styles say how it LOOKS there.
1127
+ - The canonical state token is the base token path plus a state leaf
1128
+ (`color.foreground.primary.hover`), matching the token tree. Where a state-scoped
1129
+ value is a token path whose leaf is a state name, the leaf must agree with
1130
+ the key's `@state`.
1131
+ - One `@` per key in this version. Compound states (`@selected@hover`) are
1132
+ reserved, not available.
1133
+
566
1134
  ## Templates
567
1135
 
568
1136
  ### Basic Component Template
@@ -572,12 +1140,12 @@ node:
572
1140
  uri: {tier}.component.{name}
573
1141
  styles:
574
1142
  # Structural (invariant)
575
- layout.type: stack
576
- layout.direction: vertical
1143
+ layout.type: flex
1144
+ layout.direction: column
577
1145
 
578
1146
  # Themeable
579
- appearance.background: color/surface/{name}
580
- spacing.internal: spacing/medium
1147
+ appearance.background: color.background
1148
+ spacing.internal.inline.start: spacing.inset.surface.block
581
1149
 
582
1150
  edges:
583
1151
  - node:
@@ -593,11 +1161,12 @@ node:
593
1161
  node:
594
1162
  uri: global.component.card
595
1163
  styles:
596
- layout.type: stack
597
- layout.direction: vertical
598
- appearance.background: color/surface/card
599
- appearance.shadow: shadow/card
600
- appearance.radius: shape/rounded/full
1164
+ layout.type: flex
1165
+ layout.direction: column
1166
+ appearance.background: color.background
1167
+ # A shadow has no key: `box-shadow` is one of the properties the roster
1168
+ # leaves unmapped, so it is not bound and this comment is the record.
1169
+ appearance.radius: dimension.radius.full
601
1170
 
602
1171
  edges:
603
1172
  # Named children reference their URI only (DRY Principle above) — each
@@ -627,9 +1196,9 @@ node:
627
1196
  node:
628
1197
  uri: global.component.accordion
629
1198
  styles:
630
- layout.type: stack
631
- layout.direction: vertical
632
- appearance.border: border/style/accordion
1199
+ layout.type: flex
1200
+ layout.direction: column
1201
+ appearance.border.style: solid
633
1202
 
634
1203
  edges:
635
1204
  # The repeating child is NAMED — reference its URI only (DRY Principle
@@ -650,22 +1219,22 @@ anonymous roles:
650
1219
  node:
651
1220
  uri: global.subcomponent.accordion-item
652
1221
  styles:
653
- layout.type: stack
654
- layout.direction: vertical
1222
+ layout.type: flex
1223
+ layout.direction: column
655
1224
  edges:
656
1225
  - node:
657
1226
  role: header tab
658
1227
  styles:
659
- layout.type: flow
660
- layout.direction: horizontal
1228
+ layout.type: flex
1229
+ layout.direction: row
661
1230
  layout.align: center
662
1231
  interaction.cursor: pointer
663
1232
  edges:
664
1233
  - node:
665
1234
  role: control
666
1235
  styles:
667
- size.width: size/icon/small
668
- size.height: size/icon/small
1236
+ size.width: dimension.300
1237
+ size.height: dimension.300
669
1238
  relation:
670
1239
  cardinality: "1"
671
1240
  - node:
@@ -696,9 +1265,9 @@ Transcribed from the live `global.group.keyboard_keys`:
696
1265
  node:
697
1266
  uri: global.group.keyboard_keys
698
1267
  styles:
699
- layout.display: inline-flex
1268
+ layout.type: inline-flex
700
1269
  layout.align: center
701
- spacing.gap: spacing/horizontal/xsmall
1270
+ spacing.gap: spacing.gap.mark.inline
702
1271
  edges:
703
1272
  - node:
704
1273
  uri: global.component.keyboard_key
@@ -715,15 +1284,15 @@ Use when you need a structural element without design system identity:
715
1284
  node:
716
1285
  uri: global.pattern.modal
717
1286
  styles:
718
- layout.type: stack
719
- appearance.background: color/surface/modal
1287
+ layout.type: flex
1288
+ appearance.background: color.background
720
1289
 
721
1290
  edges:
722
1291
  - node:
723
1292
  role: backdrop overlay # anonymous - no uri
724
1293
  styles:
725
- layout.display: block
726
- appearance.background: color/overlay/dark
1294
+ layout.type: block
1295
+ appearance.background: color.background.contrasted
727
1296
  interaction.cursor: pointer
728
1297
  relation:
729
1298
  cardinality: "1..1"
@@ -731,9 +1300,9 @@ node:
731
1300
  - node:
732
1301
  role: content container # anonymous wrapper
733
1302
  styles:
734
- layout.type: stack
735
- size.max.width: size/modal/max
736
- spacing.internal: spacing/large
1303
+ layout.type: flex
1304
+ size.max.width: 40rem
1305
+ spacing.internal.inline.start: spacing.inset.surface.inline
737
1306
  edges:
738
1307
  - node:
739
1308
  uri: global.subcomponent.modal-header
@@ -760,29 +1329,30 @@ node:
760
1329
  uri: global.component.button
761
1330
  styles:
762
1331
  # Structural
763
- layout.type: flow
764
- layout.direction: horizontal
1332
+ layout.type: flex
1333
+ layout.direction: row
765
1334
  layout.align: center
766
1335
  layout.justify: center
767
1336
 
768
1337
  # Themeable
769
- spacing.internal: spacing/button
770
- spacing.gap: spacing/small
771
- appearance.background: color/action/primary
772
- appearance.radius: shape/rounded/button
773
- typography.weight: font/weight/medium
1338
+ spacing.internal.inline.start: spacing.inset.action.inline
1339
+ spacing.gap: spacing.gap.mark.inline
1340
+ appearance.background: color.foreground.primary
1341
+ appearance.radius: dimension.radius.medium
1342
+ typography.weight: typography.weight.medium
774
1343
 
775
1344
  # Interaction
776
1345
  interaction.cursor: pointer
777
- interaction.transition.property: background, transform
778
- interaction.transition.duration: transition/fast
1346
+ motion.property: background, transform
1347
+ # The transition also reads --motion-duration-fast, which no symbol declares,
1348
+ # so the duration is not bound.
779
1349
 
780
1350
  edges:
781
1351
  - node:
782
1352
  role: icon
783
1353
  styles:
784
- size.width: size/icon/small
785
- size.height: size/icon/small
1354
+ size.width: dimension.300
1355
+ size.height: dimension.300
786
1356
  relation:
787
1357
  cardinality: "0..1"
788
1358
  slotName: icon
@@ -790,8 +1360,8 @@ node:
790
1360
  - node:
791
1361
  role: label
792
1362
  styles:
793
- typography.size: font/size/button
794
- typography.color: color/text/on-action
1363
+ typography.size: dimension.size.fontSize.200
1364
+ typography.color: color.text.onForegroundPrimary
795
1365
  relation:
796
1366
  cardinality: "0..1"
797
1367
  slotName: default
@@ -848,6 +1418,27 @@ Ask:
848
1418
  - Can any parts repeat?
849
1419
  - Are there anonymous structural wrappers needed?
850
1420
 
1421
+ Two sources answer these, and they disagree more often than you would expect: the
1422
+ block's `ds:hasProperty` rows in the graph, and the implementation you are authoring
1423
+ from. The graph records what was DOCUMENTED; the implementation ships what was BUILT,
1424
+ and the two run ahead of each other in both directions. Chip is the live case — the
1425
+ graph lists `icon` and `badge` as properties that the current `ChipProps` does not
1426
+ have, and marks `value` as required where the code renders it conditionally.
1427
+
1428
+ **An anatomy transcribes the implementation, so where they disagree the implementation
1429
+ wins — and the disagreement is recorded, not smoothed over.** Write the tree the
1430
+ stylesheet and the component file support, put a `#` comment on the node (or in the
1431
+ file header) saying which graph row it departs from and why, and raise the mismatch:
1432
+ a property in the graph that the code cannot reach is a documentation gap, and a part
1433
+ in the code that the graph does not list is a specification gap. Neither is fixed by
1434
+ inventing a node for a property nothing renders, or by marking a node required because
1435
+ a row said `optional: "false"`.
1436
+
1437
+ The one exception is a block you were asked to author from its SPEC rather than from an
1438
+ implementation — a component not built yet. There the graph is all there is, and the
1439
+ anatomy follows it; say so in the file header, because the next reader cannot tell the
1440
+ two situations apart from the file alone.
1441
+
851
1442
  ### 3. Determine Tier and Type
852
1443
 
853
1444
  The URI is `{tier}.{type}.{snake_name}` (see URI Encoding Convention above). The tier
@@ -882,21 +1473,88 @@ For each node, consider:
882
1473
  - **Interaction**: Cursor, transitions (for interactive nodes)
883
1474
 
884
1475
  **Rule of thumb**:
885
- - Structural values (stack, center, fill) = invariant
886
- - Token references (spacing/medium) = themeable
887
-
888
- ### 6. Validate
1476
+ - Structural values (`flex`, `center`, `100%`) = invariant
1477
+ - A symbol (spacing.inset.surface.block) = themeable
1478
+
1479
+ ### 6. Bind Data and Pin Props
1480
+
1481
+ Both are optional, and both are decision gates — bring them to the person
1482
+ rather than deciding alone.
1483
+
1484
+ **Projections** — ask whether this component is a view over a known entity. If
1485
+ it is, give the root an `on`, then for each position ask: does it RENDER a field
1486
+ (`projection.field` on the node), or is it FILLED BY a traversal
1487
+ (`projection.field` on the relation)? Check every name against the provider SDL,
1488
+ and re-check that each relation's cardinality matches the field's nullability
1489
+ and multiplicity. Generic containers project nothing — that is a real answer.
1490
+
1491
+ **Pins** — for each named child, ask what the spec FIXES versus what it leaves
1492
+ to the consumer. An intrinsic icon is pinned (`props: { icon: … }`); a
1493
+ consumer-filled icon slot is not. Confirm each prop name against
1494
+ `pragma block lookup <Component>`; a pin naming a prop the component does not
1495
+ have is invisible to schema validation and will only fail later.
1496
+
1497
+ ### 7. Validate
1498
+
1499
+ Two checks, and they cover different ground. Know which is which before you read an
1500
+ exit code as approval.
1501
+
1502
+ **What the gate checks.** `anatomies validate --authored` runs the law over every
1503
+ authored file: that the document parses, that the root is named, that every style key
1504
+ is in the roster, that every value has a lawful SHAPE, that every symbol-shaped element
1505
+ RESOLVES in a stratum, and that each symbol sits inside the namespace its key admits.
1506
+ That is a great deal, and it is all mechanical.
1507
+
1508
+ **What the gate does not check is whether the anatomy is TRUE of the component.** It
1509
+ cannot: it never reads the stylesheet. So every one of these passes the gate silently —
1510
+ a lawful key that is the wrong key for the CSS property (`layout.align` where the
1511
+ stylesheet wrote `align-self`), a resolving symbol in the right namespace that is the
1512
+ wrong symbol for the part, a node boundary drawn in the wrong place, a cardinality that
1513
+ contradicts the component's own props, a missing node for a part the stylesheet draws.
1514
+ Exit 0 with no findings means the file is LAWFUL. It does not mean the judgment calls
1515
+ were right, and it is not a second opinion on them. The check that catches those is the
1516
+ pull-request review by someone who can open the same stylesheet — which is why the file
1517
+ goes through review at all.
1518
+
1519
+ So read the list below as the author's own pass, and keep its items honest about which
1520
+ kind they are: the ones the gate will confirm, and the ones only a reader can.
889
1521
 
890
1522
  Check your anatomy against:
891
1523
  - [ ] Root node is named (has `uri`)
892
1524
  - [ ] All cardinalities are valid notation
893
- - [ ] Style keys follow CTI convention
894
- - [ ] Token paths use `/` delimiter
1525
+ - [ ] Every style key is in the roster — the gate confirms this one
1526
+ - [ ] Each key is the key for the CSS property the stylesheet actually wrote (judgment: `layout.align` for `align-items`, `layout.alignSelf` for `align-self`; the gate accepts either)
1527
+ - [ ] Every style value is one dotted symbol, a fallback chain with at most one primitive LAST, or a primitive — no slash path, no `?` marker, no `root`/`$root` segment
1528
+ - [ ] Every symbol resolves (`pragma token lookup <symbol>`) and sits in its key's namespace; a name that does not resolve is a `#` comment, never a value — the gate confirms both
1529
+ - [ ] Every component-local variable that came back `ENTITY_NOT_FOUND` was followed through the component's own `:root` to a declared variable or a literal before it was written off
1530
+ - [ ] `layout.type` and `layout.direction` are spelled as CSS spells them (`flex`, `grid`, `inline-flex`, `block`; `row`, `column`)
895
1531
  - [ ] Anonymous nodes have `role`, not `uri`
896
1532
  - [ ] Multi-word names encode word boundaries with `_`; `-` appears only where the name carries a dot (per the URI Encoding Convention table)
897
1533
  - [ ] Every non-root `uri:` either resolves (`pragma graph inspect ds:<uri>`) or is `$custom`, a template placeholder, or a new block or child this same spec introduces (§3)
898
1534
  - [ ] Named (`uri:`) children carry no copy of their own subtree — contextual style overrides only
899
- - [ ] Nested components make semantic sense
1535
+ - [ ] Nested components make semantic sense (judgment — nothing checks this but a reader)
1536
+ - [ ] Every symbol is the right symbol for the part, not merely one that resolves in the key's namespace (judgment — the gate checks resolution and namespace, never fit)
1537
+ - [ ] Switch cases carry `uri` or `node` and nothing else — no `default:`
1538
+ - [ ] Every `@state` marker is one of hover, active, focus, disabled, selected, expanded, indeterminate, invalid — one `@` per key, never `@default`
1539
+ - [ ] No `@state` stands in for a structural difference — that is a `switch`
1540
+ - [ ] Every node `projection` has at least one of `on` / `field`; every relation `projection` has `field` and no `on`
1541
+ - [ ] No projection spells an unwrapping path (`edges.node`), and every projected field name exists in the provider SDL
1542
+ - [ ] Each projected relation's cardinality matches the field's nullability and multiplicity
1543
+ - [ ] `props` appears only on named nodes, holds scalars only, and every pinned prop name exists on that component (`pragma block lookup`)
1544
+ - [ ] A part the stylesheet draws as `::before`/`::after` is an anonymous `role:` node with a comment saying so, and no `slotName`
1545
+ - [ ] Every `uri:` names a block in this tier or in `global`, and a `global` anatomy reaches into no app tier
1546
+
1547
+ Then run the law over the file, which is the check no list can stand in for:
1548
+
1549
+ ```bash
1550
+ bun src/cli.ts anatomies validate --authored
1551
+ ```
1552
+
1553
+ Exit 0 with `0 findings` is the verdict on lawfulness; a finding names the file, the uri
1554
+ and the rule it broke, and a warning — `AT11`, a state binding differing from its base —
1555
+ is usually the implementation telling the truth. What exit 0 does NOT settle is
1556
+ anything in the paragraphs above: the semantic calls travel to review, so say in the
1557
+ pull request which ones you made and what you read to make them.
900
1558
 
901
1559
  ## Response Format
902
1560
 
@@ -924,9 +1582,12 @@ When creating an anatomy, respond with:
924
1582
  - {Design decision 2}
925
1583
 
926
1584
  ### Next Steps
927
- 1. Land the DSL in the block's spec draft under `specs/` (see `specs/README.md`) —
928
- as step 7 of a specify flow that is the step-6 spec file; never under `data/`
929
- 2. A human enters the content into Coda, the database of record
1585
+ 1. Land the file at `anatomies/authored/{tier}/{uri}.yaml` in the design-system
1586
+ repository — never under `data/`, which the pull sync regenerates
1587
+ 2. `bun src/cli.ts anatomies validate --authored` — exit 0, and no findings
1588
+ 3. Review it as a file, in a pull request
1589
+ 4. `anatomies write` — dry, then a `--only <uri> --apply` canary, then `--apply`;
1590
+ after that the document is the source of record
930
1591
  ```
931
1592
 
932
1593
  ## Tips
@@ -1004,19 +1665,25 @@ The children one-to-many relation follows inspiration from the [Relay connection
1004
1665
  * - Invariant (structural): layout.type, layout.direction, size.width: "fill"
1005
1666
  * - Themeable (brand/theme): appearance.*, spacing.* (when using tokens)
1006
1667
  */
1007
- type Styles = Record<string, TokenPath | TokenPath[] | PrimitiveValue>;
1668
+ /**
1669
+ * Keys may carry an "@state" suffix scoping the value to one interaction
1670
+ * state: "appearance.background@hover". The unmarked key is the default
1671
+ * state; "@default" is invalid. Vocabulary is closed: hover, active, focus,
1672
+ * disabled, selected, expanded, indeterminate, invalid.
1673
+ */
1674
+ type Styles = Record<StyleKey, Symbol | [...Symbol[], Primitive?] | Primitive>;
1008
1675
 
1009
1676
  /**
1010
- * TokenPath: Forward-slash delimited path to a design token.
1011
- * Examples: "spacing/medium", "color/surface/primary", "font/size/heading/1"
1677
+ * Symbol: a token symbol's own dotted name, as the token graph declares it.
1678
+ * Examples: "spacing.inset.surface.block", "color.background", "typography.heading.1"
1012
1679
  */
1013
- type TokenPath = string;
1680
+ type Symbol = string;
1014
1681
 
1015
1682
  /**
1016
- * PrimitiveValue: Direct values not resolved from tokens.
1017
- * Examples: "stack", "center", "fill", 1.5, true
1683
+ * Primitive: a literal the implementation writes, resolved against nothing.
1684
+ * Examples: "flex", "center", "100%", 1.5, true
1018
1685
  */
1019
- type PrimitiveValue = string | number | boolean;
1686
+ type Primitive = string | number | boolean;
1020
1687
 
1021
1688
  /* Main types */
1022
1689
 
@@ -1036,12 +1703,48 @@ interface Relation {
1036
1703
  * Common values: "default", "header", "footer", "icon", "label"
1037
1704
  */
1038
1705
  slotName?: string;
1706
+
1707
+ /** The traversal populating this slot from the current data context. */
1708
+ projection?: RelationProjection;
1709
+ }
1710
+
1711
+ /**
1712
+ * Fragment-style binding of a node to graph data, after the Relay
1713
+ * fragment-colocation pattern. `on` is a GraphQL type condition, which
1714
+ * establishes the data context on the root and narrows it on a child;
1715
+ * `field` is the field the node renders, relative to the enclosing
1716
+ * context. At least one of the two is required.
1717
+ */
1718
+ type Projection =
1719
+ | { on: string; field?: string }
1720
+ | { on?: string; field: string };
1721
+
1722
+ /**
1723
+ * A traversal populating a slot, carried by the reified Relation: the
1724
+ * field of the current data context whose value(s) fill this position.
1725
+ * Names the field ONLY — never an unwrapping path like `edges.node`,
1726
+ * since connection-vs-list is a provider mechanism. Type narrowing lives
1727
+ * on the child node's own `on`, never here.
1728
+ */
1729
+ interface RelationProjection {
1730
+ field: string;
1039
1731
  }
1040
1732
 
1733
+ /**
1734
+ * Pinned prop values: the anatomy FIXES props of the referenced component
1735
+ * at this position. Keys are props defined on the component in the design
1736
+ * system ontology; the DSL never defines the prop surface itself. Values
1737
+ * are scalars — no token paths, no fallback arrays. Named nodes only.
1738
+ */
1739
+ type Props = Record<string, string | number | boolean>;
1740
+
1041
1741
  /**
1042
1742
  * Base interface for all nodes in the anatomy tree.
1043
1743
  */
1044
1744
  interface BaseNode {
1745
+ /** Graph-data binding for this position. */
1746
+ projection?: Projection;
1747
+
1045
1748
  /**
1046
1749
  * Style properties using CTI-inspired keys.
1047
1750
  *
@@ -1069,6 +1772,9 @@ interface NamedNode extends BaseNode {
1069
1772
  * Examples: "global.component.button", "apps.layout.application_layout"
1070
1773
  */
1071
1774
  uri: string;
1775
+
1776
+ /** Pinned prop values, e.g. { icon: "chevron-down" }. */
1777
+ props?: Props;
1072
1778
  }
1073
1779
 
1074
1780
  /**
@@ -1105,9 +1811,6 @@ interface Switch {
1105
1811
  interface SwitchCase {
1106
1812
  uri?: string;
1107
1813
  node?: Node;
1108
-
1109
- /** Marks the default case. */
1110
- default?: boolean;
1111
1814
  }
1112
1815
 
1113
1816
  interface Edge {
@@ -1148,11 +1851,11 @@ node:
1148
1851
  uri: global.component.accordion
1149
1852
  styles:
1150
1853
  # Structural (invariant)
1151
- layout.type: stack
1152
- layout.direction: vertical
1854
+ layout.type: flex
1855
+ layout.direction: column
1153
1856
 
1154
1857
  # Themeable
1155
- appearance.border: border/style/accordion
1858
+ appearance.border.style: solid
1156
1859
 
1157
1860
  edges:
1158
1861
  # The repeating child is NAMED — reference its URI only (DRY Principle);
@@ -1171,22 +1874,22 @@ The child's own anatomy — a separate spec on `global.subcomponent.accordion-it
1171
1874
  node:
1172
1875
  uri: global.subcomponent.accordion-item
1173
1876
  styles:
1174
- layout.type: stack
1175
- layout.direction: vertical
1877
+ layout.type: flex
1878
+ layout.direction: column
1176
1879
  edges:
1177
1880
  - node:
1178
1881
  role: header tab
1179
1882
  styles:
1180
- layout.type: flow
1181
- layout.direction: horizontal
1883
+ layout.type: flex
1884
+ layout.direction: row
1182
1885
  layout.align: center
1183
1886
  interaction.cursor: pointer
1184
1887
  edges:
1185
1888
  - node:
1186
1889
  role: control
1187
1890
  styles:
1188
- size.width: size/icon/small
1189
- size.height: size/icon/small
1891
+ size.width: dimension.300
1892
+ size.height: dimension.300
1190
1893
  relation:
1191
1894
  cardinality: "1"
1192
1895
  - node: