@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
@@ -1,406 +1,1476 @@
1
- # Anatomy DSL Specification
1
+ <!--
2
+ SYNCED FILE — do not edit here.
2
3
 
3
- This specification presents a DSL to represent design system anatomies, providing an accurate platform-agnostic markup primitive to precede implementation.
4
+ Source: canonical/anatomy-dsl · docs/api-reference.md
5
+ Ref: 6e679f2 (feat/namespaces-and-states, 0.6.0)
6
+ Synced: 2026-09-14
4
7
 
5
- ## Rationale
8
+ Two lines diverge from the upstream text, and are marked here rather than left
9
+ silent: the `motion` row of the namespace table (3.7) and the optional-marker row
10
+ of the retired-notation table (12) both pointed at a register row as the record for
11
+ a name no stratum declares. An authored anatomy in this repository has no register
12
+ row — a `#` comment on the line is the record — so both rows say that instead.
13
+ Raise it upstream and the divergence goes away with the next sync.
6
14
 
7
- Design systems need a formal way to describe component structure that:
8
- - Precedes and guides implementation
9
- - Is platform-agnostic (works for React, SwiftUI, Flutter, etc.)
10
- - Supports discussion and review
11
- - Enables automated tooling (auditing, code generation)
15
+ Edit the spec in canonical/anatomy-dsl and re-copy this file wholesale.
16
+ pragma's packages/cli/pragma/bundled-skills/anatomy-author/ is regenerated
17
+ FROM this file by `bun run bundle`, so it needs no separate edit.
18
+ -->
12
19
 
13
- ## Intended Usage
20
+ # Anatomy DSL — API Reference
14
21
 
15
- 1. **Documentation**: Implementation primitive for markup and style bindings
16
- 2. **Specification**: Using the tree-based DSL to identify entities to implement and/or reuse
17
- 3. **Discussion**: Using the DSL to discuss across conversations related to DS (e.g., "I believe these nodes should be siblings, not parents of one another")
18
- 4. **Inference**: Supporting automated inference to audit a codebase against its specifications or to support code generation
22
+ > Consolidated reference merging WD404 (core), WD404.1 (addendum 1), WD404.2 (projections), WD404.3 (prop pinning), and WD404.4 (interaction states).
19
23
 
20
- ## Requirements
24
+ ---
21
25
 
22
- ### General DSL Requirements
26
+ ## Table of Contents
23
27
 
24
- - The DSL MUST optimize for readability
25
- - The DSL MUST be easy to get started with and use a familiar language
26
- - The scope of the DSL MUST be minimized to cover only the strict necessary - additional specification information being covered in the schema
28
+ 1. [Overview](#1-overview)
29
+ 2. [File Format](#2-file-format)
30
+ 3. [Type System](#3-type-system)
31
+ - 3.1 [AnatomySpec (root)](#31-anatomyspec-root)
32
+ - 3.2 [Node](#32-node)
33
+ - 3.3 [NamedNode](#33-namednode)
34
+ - 3.4 [AnonymousNode](#34-anonymousnode)
35
+ - 3.5 [Edge](#35-edge)
36
+ - 3.6 [Relation](#36-relation)
37
+ - 3.7 [Styles](#37-styles)
38
+ - 3.8 [Switch](#38-switch)
39
+ - 3.9 [SwitchCase](#39-switchcase)
40
+ - 3.10 [Projection](#310-projection)
41
+ - 3.11 [Prop](#311-prop)
42
+ - 3.12 [Token binding](#312-token-binding)
43
+ 4. [URI Encoding Convention](#4-uri-encoding-convention)
44
+ 5. [Symbols and the Value Grammar](#5-symbols-and-the-value-grammar)
45
+ 6. [Cardinality Notation](#6-cardinality-notation)
46
+ 7. [Slot Mapping](#7-slot-mapping)
47
+ 8. [Named vs Anonymous Rule](#8-named-vs-anonymous-rule)
48
+ 9. [DRY Principle for Named Nodes](#9-dry-principle-for-named-nodes)
49
+ 10. [Switch Construct](#10-switch-construct)
50
+ - 10.1 [Discriminator Vocabulary](#101-discriminator-vocabulary)
51
+ - 10.2 [Shorthand Expansion](#102-shorthand-expansion)
52
+ - 10.3 [Reserved URI: $custom](#103-reserved-uri-custom)
53
+ 11. [The Fallback List](#11-the-fallback-list)
54
+ 12. [Retired Notation](#12-retired-notation)
55
+ 13. [Examples](#13-examples)
56
+ 14. [TypeScript Definitions](#14-typescript-definitions)
57
+ 15. [Deferred Items](#15-deferred-items)
58
+ 16. [References](#16-references)
27
59
 
28
- ### Modelling Requirements
60
+ ---
29
61
 
30
- - The children of a node MUST be modelled through a reified relation for annotation purposes
31
- - The DSL MUST support anonymous nodes (nodes that are not named components, for instance an unnamed "div"). In this case, the DSL should support role annotations
32
- - The DSL MUST support reified annotations of cardinality
33
- - The DSL MUST support all classes of UI Blocks identified in the ontology (Layout, Pattern, Component, Subcomponent, Group)
62
+ ## 1. Overview
34
63
 
35
- ## Type System
64
+ The Anatomy DSL is a YAML-based language for describing the structural anatomy of design system components. It produces a tree of **nodes** connected by **edges**, where each edge carries a **relation** annotating cardinality and slot assignment.
36
65
 
37
- We structure the children one-to-many relation following inspiration by [the Relay connection pattern](https://relay.dev/graphql/connections.htm) and propose the following typings.
66
+ ### Intended Usage
38
67
 
39
- ```typescript
40
- /* Reusable types */
68
+ | Usage | Description |
69
+ |-----------------|-----------------------------------------------------------------------------------------------------|
70
+ | Documentation | Implementation primitive for markup and style bindings. |
71
+ | Specification | Tree-based DSL to identify entities to implement and/or reuse. |
72
+ | Discussion | Shared vocabulary for design system conversations (e.g. "these nodes should be siblings"). |
73
+ | Inference | Supports automated auditing of a codebase against its specifications, or code generation. |
41
74
 
42
- /**
43
- * Styles use a CTI (Category-Type-Item) inspired flat key structure for
44
- * platform-agnostic UI properties. This allows to avoid using language
45
- * specific keys such as CSS.
46
- *
47
- * KEY NAMING CONVENTION:
48
- * Properties follow a dot-notation pattern: "category.type[.item]"
49
- * - Category: Primary concern (layout, spacing, appearance, typography, size, etc.)
50
- * - Type: Specific aspect within that category
51
- * - Item: Optional further specification
52
- *
53
- * INVARIANT VS THEMEABLE:
54
- * Whether a property is invariant (structural) or themeable depends on its
55
- * semantic meaning and value type, rather than strict rules:
56
- *
57
- * Typically invariant (structural decisions):
58
- * - layout.type: "stack" | "flow" | "grid" // Component structure
59
- * - layout.direction: "horizontal" | "vertical"
60
- * - size.width: "fill" | "hug" | "100%" // Structural sizing
61
- *
62
- * Typically themeable (brand/theme variations):
63
- * - appearance.*: Color, shadow, radius values via tokens
64
- * - spacing.*: When referencing design tokens
65
- * - typography.*: Font properties via tokens
66
- *
67
- * Context-dependent:
68
- * - size.width: "320px" or "size/card/width" // Token = themeable
69
- * - size.width: "fill" or "100%" // Structural = invariant
70
- * - spacing.internal: "16" // Fixed value = invariant
71
- * - spacing.internal: "spacing/medium" // Token = themeable
72
- *
73
- * The distinction is semantic: does this property define structure or appearance?
74
- */
75
- type Styles = Record<string, TokenPath | TokenPath[] | PrimitiveValue>;
75
+ ### Out of Scope
76
76
 
77
- /**
78
- * TokenPath represents a forward-slash delimited path to a design token.
79
- * Follows design token naming conventions referenced by the W3C Design Token Format.
80
- *
81
- * Examples:
82
- * - "spacing/medium"
83
- * - "color/surface/primary"
84
- * - "font/size/heading/1"
85
- * - "border/width/thin"
86
- * - "shadow/elevated/medium"
87
- * - "radius/button/default"
88
- *
89
- * Token paths are resolved at runtime against the active theme.
90
- */
91
- type TokenPath = string;
77
+ The Anatomy DSL describes **structure**, [projected](#310-projection) **graph-data bindings**, and [pinned](#311-prop) **prop values**. The following concerns are explicitly outside its scope:
92
78
 
93
- /**
94
- * PrimitiveValue represents direct values not resolved from tokens.
95
- * Used for structural decisions and fixed values.
96
- *
97
- * Examples:
98
- * - string: "stack", "flow", "center", "fill", "100%", "1fr"
99
- * - number: 1.5 (aspect ratio), 2 (column count), -1 (z-index)
100
- * - boolean: true/false (for wrap, reverse, etc.)
101
- */
102
- type PrimitiveValue = string | number | boolean;
79
+ - **Prop surface definition** — which props a component accepts, their types and optionality. These live in the design system ontology; the DSL only [pins values](#311-prop).
80
+ - **State machines** — transitions, triggers, and interaction logic. Appearance *per* interaction state is in scope (see [Interaction States](#37-styles)); structural state variation is the [Switch construct](#10-switch-construct)'s job.
81
+ - **Modifier definitions** — what a modifier family MEANS, which values it has, and which components declare it. These live in the design system ontology. A component's support for a family is not declared in its anatomy at all: it is CONSUMED, by binding the family's channel symbol on the key the family reaches (`[modifier.color.text, color.text]`, `[surface.color.background, color.background]`), so the family name never appears in the tree. That is the modelling convention, not a gap — see the channel rules in the sibling `SKILL.md`.
103
82
 
104
- /* Main types */
83
+ ### Normative Language
105
84
 
106
- interface Relation {
107
- /**
108
- * Cardinality represents the number of allowed instances of the related node.
109
- * See https://en.wikipedia.org/wiki/Cardinality_(data_modeling) for reference.
110
- *
111
- * Examples:
112
- * - "1" : Exactly one (required)
113
- * - "0..1" : Zero or one (optional)
114
- * - "0..*" : Zero or more
115
- * - "1..*" : One or more
116
- * - "2..5" : Between 2 and 5
117
- */
118
- cardinality: string;
85
+ The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
119
86
 
120
- /**
121
- * Maps content to a specific slot in the parent component.
122
- *
123
- * Common values:
124
- * - "default" : Main content (React children)
125
- * - "header" : Header slot
126
- * - "footer" : Footer slot
127
- * - "icon" : Icon placement
128
- * - "label" : Text label
129
- * - Custom slots as needed
130
- */
131
- slotName?: string;
132
- }
87
+ ### Design Requirements
133
88
 
134
- /**
135
- * Base interface for all nodes in the anatomy tree.
136
- */
137
- interface BaseNode {
138
- /**
139
- * Style properties using CTI-inspired keys.
140
- *
141
- * Common patterns and their platform mappings:
142
- *
143
- * LAYOUT:
144
- * - "layout.type": "stack" | "flow" | "grid"
145
- * -> CSS: display + flexDirection | SwiftUI: VStack/HStack | Flutter: Column/Row
146
- * - "layout.align": "center" | "start" | "end"
147
- * -> CSS: align-items | SwiftUI: alignment | Flutter: crossAxisAlignment
148
- *
149
- * SPACING:
150
- * - "spacing.internal": "spacing/medium"
151
- * -> CSS: padding | SwiftUI: .padding() | Flutter: EdgeInsets
152
- * - "spacing.external": "spacing/large"
153
- * -> CSS: margin | SwiftUI: frame modifiers | Flutter: Container margin
154
- * - "spacing.gap": "spacing/small"
155
- * -> CSS: gap | SwiftUI: spacing parameter | Flutter: SizedBox between
156
- *
157
- * APPEARANCE:
158
- * - "appearance.background": "color/surface/primary"
159
- * -> CSS: background | SwiftUI: .background() | Flutter: Container color
160
- * - "appearance.radius": "radius/medium"
161
- * -> CSS: border-radius | SwiftUI: .cornerRadius() | Flutter: BorderRadius
162
- *
163
- * SIZE:
164
- * - "size.width": "fill" | "hug" | "size/card/width"
165
- * -> CSS: width: 100% | auto | var() | SwiftUI: .frame() | Flutter: constraints
166
- *
167
- * Examples showing invariant vs themeable:
168
- * {
169
- * "layout.type": "grid", // Invariant: structural
170
- * "layout.columns": "1fr 300px 1fr", // Invariant: grid structure
171
- * "spacing.gap": "spacing/grid/gap", // Themeable: token reference
172
- * "appearance.background": "color/surface", // Themeable: always
173
- * "size.width": "fill", // Invariant: structural behavior
174
- * "size.max.width": "size/container/max", // Themeable: token reference
175
- * "typography.align": "center", // Invariant: structural
176
- * "typography.size": "font/size/body", // Themeable: token reference
177
- * }
178
- */
179
- styles?: Styles;
89
+ - The DSL MUST optimize for readability.
90
+ - The DSL MUST be easy to get started with and use a familiar language.
91
+ - The scope of the DSL MUST be minimized to cover only the strict necessary.
92
+ - The children of a node MUST be modelled through a reified relation for annotation purposes.
93
+ - The DSL MUST support anonymous nodes with role annotations.
94
+ - The DSL MUST support reified annotations of cardinality.
95
+ - The DSL MUST support all classes of UI Blocks (Layout, Pattern, Component, Subcomponent).
180
96
 
181
- /**
182
- * Child nodes and their relationships.
183
- * Enables recursive component composition.
184
- */
185
- edges?: Edge[];
186
- }
97
+ ---
187
98
 
188
- /**
189
- * Named nodes represent identifiable components in the design system.
190
- */
191
- interface NamedNode extends BaseNode {
192
- /**
193
- * Unique identifier following "tier.type.name" pattern.
194
- * Maps to URI field on UI Block in the ontology.
195
- *
196
- * Examples:
197
- * - "global.component.button"
198
- * - "global.component.card"
199
- * - "apps.layout.application_layout"
200
- */
201
- uri: string;
202
- }
99
+ ## 2. File Format
203
100
 
204
- /**
205
- * Anonymous nodes represent structural elements without DS identity.
206
- */
207
- interface AnonymousNode extends BaseNode {
208
- /**
209
- * Human-readable description of the node's purpose.
210
- *
211
- * Examples:
212
- * - "content wrapper"
213
- * - "spacer element"
214
- * - "icon container"
215
- * - "layout grid cell"
216
- */
217
- role: string;
218
- }
101
+ All anatomy files MUST use YAML and MUST begin with the YAML document start marker:
219
102
 
220
- /**
221
- * Union type for all possible nodes.
222
- */
223
- type Node = NamedNode | AnonymousNode;
103
+ ```yaml
104
+ ---
105
+ node:
106
+ uri: global.component.button
107
+ # ...
108
+ ```
224
109
 
225
- /**
226
- * A switch fills one position with ONE of several alternatives,
227
- * making polymorphism explicit at the edge level.
228
- * The discriminator says who chooses:
229
- * - "props" : the consumer chooses via component props
230
- * - "internal" : the component chooses from its own state
231
- * - "override" : the consumer may replace the default with a custom component
232
- */
233
- interface Switch {
234
- on: "props" | "internal" | "override";
235
- cases: SwitchCase[];
236
- }
110
+ **File extension**: `.anatomy.yaml`
237
111
 
238
- /**
239
- * One switch alternative — exactly one of `uri` or `node`.
240
- * `uri` is the shorthand for `node: { uri }`; use the full `node` form
241
- * when the case carries styles or edges. The reserved URI `$custom`
242
- * marks a case filled by a user-provided component.
243
- */
244
- interface SwitchCase {
245
- uri?: string;
246
- node?: Node;
112
+ **Language choice rationale**: YAML was chosen for its indentation-based structure and readability. JSON was rejected for verbosity. TOML maps with difficulty to deeply nested nodes. SDLang and KDL are promising but lack wide adoption.
247
113
 
248
- /** Marks the default case. */
249
- default?: boolean;
250
- }
114
+ ---
251
115
 
252
- /**
253
- * Represents a parent-child relationship in the anatomy tree.
254
- */
255
- interface Edge {
256
- /** The child node (Named or Anonymous) — exactly one of `node` or `switch` */
257
- node?: Node;
116
+ ## 3. Type System
258
117
 
259
- /** A polymorphic position (mutually exclusive with node) */
260
- switch?: Switch;
118
+ The children one-to-many relation is structured following inspiration by the [Relay connection pattern](https://relay.dev/graphql/connections.htm), reifying the relationship between parent and child as a first-class **Edge** carrying metadata.
261
119
 
262
- /** Relationship metadata (cardinality, slot) */
263
- relation: Relation;
264
- }
120
+ ### 3.1 AnatomySpec (root)
265
121
 
266
- /**
267
- * Root specification for a component anatomy.
268
- * The top-level node must always be Named.
269
- */
270
- interface AnatomySpec {
271
- node: NamedNode;
272
- }
122
+ The root of every anatomy file. Contains exactly one top-level named node.
123
+
124
+ | Field | Type | Required | Description |
125
+ |--------|-----------|----------|--------------------------------------|
126
+ | `node` | NamedNode | Yes | The root component being described. |
127
+
128
+ ```yaml
129
+ ---
130
+ node:
131
+ uri: global.component.card
132
+ styles: { ... }
133
+ edges: [ ... ]
273
134
  ```
274
135
 
275
- ## Language Choice
136
+ ### 3.2 Node
137
+
138
+ A union type. Every node is either a **NamedNode** or an **AnonymousNode** — never both. Discriminated by the presence of `uri` (named) vs `role` (anonymous).
276
139
 
277
- We propose the use of YAML, a mature indentation-based configuration syntax based on scalars, maps and lists. The main contender we have considered aside from YAML was JSON, rejected for its verbosity. We have also rejected TOML (maps with difficulty deeply nested nodes), SDLang and KDL (promising markup syntaxes without wide adoption at present).
140
+ ```
141
+ Node = NamedNode | AnonymousNode
142
+ ```
278
143
 
279
- ## Examples
144
+ ### 3.3 NamedNode
280
145
 
281
- ### Accordion
146
+ A node representing an identifiable component in the design system — something a consumer can instantiate in code.
147
+
148
+ | Field | Type | Required | Description |
149
+ |----------|--------|----------|-------------------------------------------------------------------|
150
+ | `uri` | string | Yes | Unique identifier. See [URI Encoding Convention](#4-uri-encoding-convention). |
151
+ | `projection` | Projection | No | Graph-data binding. See [Projection](#310-projection). |
152
+ | `props` | Props | No | Pinned prop values. See [Prop](#311-prop). Named nodes only. |
153
+ | `styles` | Styles | No | Platform-agnostic style properties. |
154
+ | `edges` | Edge[] | No | Child nodes and their relationships. |
282
155
 
283
156
  ```yaml
284
157
  node:
285
158
  uri: global.component.accordion
286
159
  styles:
287
- # Structural (implicitly invariant)
288
- layout.type: stack
289
- layout.direction: vertical
290
-
291
- # Themeable
292
- appearance.border: border/style/accordion
293
-
160
+ layout.type: grid
294
161
  edges:
295
- # The repeating child is NAMED — reference its URI only; its subtree
296
- # lives in its own anatomy. The live Accordion does the same.
297
- - node:
298
- uri: global.subcomponent.accordion-item
299
- relation:
300
- cardinality: "1..*"
301
- slotName: default
162
+ - node: { uri: global.subcomponent.accordion-item }
163
+ relation: { cardinality: "1..*" }
302
164
  ```
303
165
 
304
- The child's own anatomy — a separate spec on `global.subcomponent.accordion-item`
305
- (transcribed from the live block) — carries the subtree as anonymous roles:
166
+ ### 3.4 AnonymousNode
167
+
168
+ A structural element without design system identity — not directly instantiated by users.
169
+
170
+ | Field | Type | Required | Description |
171
+ |----------|--------|----------|-------------------------------------------------|
172
+ | `role` | string | Yes | Human-readable description of the node's purpose.|
173
+ | `projection` | Projection | No | Graph-data binding. See [Projection](#310-projection). |
174
+ | `styles` | Styles | No | Platform-agnostic style properties. |
175
+ | `edges` | Edge[] | No | Child nodes and their relationships. |
306
176
 
307
177
  ```yaml
308
178
  node:
309
- uri: global.subcomponent.accordion-item
179
+ role: content wrapper
310
180
  styles:
311
- layout.type: stack
312
- layout.direction: vertical
181
+ layout.type: block
182
+ spacing.external.block.end: spacing.gap.field.block
183
+ ```
184
+
185
+ ### 3.5 Edge
186
+
187
+ Represents a parent-child relationship in the anatomy tree. An edge contains either a `node` or a `switch` (mutually exclusive), plus a `relation`.
188
+
189
+ | Field | Type | Required | Description |
190
+ |------------|----------|----------|---------------------------------------------------------------------|
191
+ | `node` | Node | No* | A single child node (named or anonymous). |
192
+ | `switch` | Switch | No* | A polymorphic position. See [Switch Construct](#10-switch-construct).|
193
+ | `relation` | Relation | Yes | Cardinality and slot metadata. |
194
+
195
+ > \* Exactly one of `node` or `switch` MUST be present.
196
+
197
+ **Shorthand**: When an edge references a named node by URI only, `- uri: X` expands to `- node: { uri: X }`:
198
+
199
+ ```yaml
200
+ # Shorthand
201
+ edges:
202
+ - uri: global.component.button
203
+ relation: { cardinality: "1" }
204
+
205
+ # Expands to
206
+ edges:
207
+ - node:
208
+ uri: global.component.button
209
+ relation: { cardinality: "1" }
210
+ ```
211
+
212
+ ### 3.6 Relation
213
+
214
+ Metadata about the parent-child relationship carried by an edge.
215
+
216
+ | Field | Type | Required | Description |
217
+ |---------------|--------|----------|--------------------------------------------------------------------------------|
218
+ | `cardinality` | string | Yes | Instance count constraint. See [Cardinality Notation](#6-cardinality-notation). |
219
+ | `slotName` | string | No | Target slot in the parent component. See [Slot Mapping](#7-slot-mapping). |
220
+ | `projection` | RelationProjection | No | Traversal populating the slot. See [Projection](#310-projection). |
221
+
222
+ ```yaml
223
+ relation:
224
+ cardinality: "0..1"
225
+ slotName: header
226
+ ```
227
+
228
+ ### 3.7 Styles
229
+
230
+ A flat map of platform-agnostic UI properties. A key is one member of the
231
+ **closed roster** the registry declares; a value is a **symbol**, a
232
+ **primitive**, or a **list** that is the fallback order.
233
+
234
+ ```
235
+ Styles = Record<StyleKey, StyleValue>
236
+ StyleValue = Symbol | Primitive | [Symbol, ...(Symbol | Primitive)]
237
+ ```
238
+
239
+ | Value Type | Example | Description |
240
+ |---|---|---|
241
+ | Symbol | `dimension.100`, `color.text`, `modifier.color.text` | The dotted name of a token symbol, verbatim. See [§5](#5-symbols-and-the-value-grammar). |
242
+ | Primitive | `flow`, `0`, `2px`, `#ccc`, `"1 / -1"` | A literal the implementation writes as it stands. |
243
+ | List | `[modifier.color.text, color.text]` | The fallback order — the implementation's own `var()` chain, terminal literal included. See [§11](#11-the-fallback-list). |
244
+
245
+ **The key roster is closed.** Keys are declared one `anatomy:StyleKey`
246
+ individual each in `definitions/registry.ttl`, generated from the roster in
247
+ `definitions/style-keys.yaml`, and `anatomy:styleKey`'s `sh:in` is projected
248
+ from the same roster. The roster is not designed: it is the right-hand column
249
+ of a table whose left-hand column was every CSS property the reference
250
+ implementations bind, measured over 89 published component stylesheets, so a
251
+ key exists because an implementation binds the property. Both files state
252
+ that provenance, and the measurement itself lives in `canonical/design-system`
253
+ — this package reads no CSS.
254
+
255
+ **Key naming**: `namespace.segment[.segment…]` — the same shape as a symbol, a
256
+ lowercase namespace then one or more segments, camelCase admitted
257
+ (`typography.lineHeight`). The registry tells a key from a symbol, so the
258
+ grammar does not have to.
259
+
260
+ | Namespace | Typical keys | What a value is |
261
+ |---|---|---|
262
+ | `layout` | `layout.type`, `layout.direction`, `layout.align`, `layout.grid.column` | Primitives: structure does not vary by theme |
263
+ | `spacing` | `spacing.gap`, `spacing.internal.inline.start`, `spacing.external.block.end` | `spacing.*` symbols, and `dimension.*` while the implementations still bind it |
264
+ | `appearance` | `appearance.background`, `appearance.radius`, `appearance.border.color`, `appearance.outline.width` | `color.*` or `dimension.*` symbols, channels included |
265
+ | `typography` | `typography.size`, `typography.weight`, `typography.color`, `typography.lineHeight` | `typography.*`, `dimension.*` or `color.*` symbols |
266
+ | `size` | `size.width`, `size.height`, `size.max.width` | `dimension.*` symbols, or a primitive |
267
+ | `interaction` | `interaction.cursor`, `interaction.select` | Primitives |
268
+ | `motion` | `motion.property`, `motion.duration`, `motion.easing` | `motion.*` symbols — a namespace no stratum declares yet, so the binding is not written and a `#` comment on the line records what the implementation reads |
269
+
270
+ **Which of the three a key takes** is `anatomy:valueKind` on the key —
271
+ `token`, `primitive` or `either` — and it was measured from the reference
272
+ rather than asserted: a key whose every reference value read a `var()` is
273
+ `token`, one where a literal appeared too is `either`, one that took no symbol
274
+ at all is `primitive`. **Which symbols** it takes is
275
+ `anatomy:tokenNamespace`, a set of dotted prefixes that includes the
276
+ `modifier.` and `surface.` spellings wherever a channel is legal. A key may
277
+ admit **more than one base namespace**: the roster's `namespace` field takes
278
+ one dotted prefix or a list of them, and each member is projected into its
279
+ own three spellings, grouped by namespace and in the order the roster states
280
+ them. That is how every `spacing.*` key admits `dimension.*` alongside
281
+ `spacing.*` — the implementations read `--dimension-*` directly for padding
282
+ and gaps, and the second namespace comes out of the roster once semantic
283
+ spacing lands. Both are [§3.12](#312-token-binding)'s subject.
284
+
285
+ #### Interaction states (`@state`)
286
+
287
+ A style key MAY take an `@state` suffix scoping its value to an interaction
288
+ state. The unmarked key is the default state; `@default` is invalid (absence
289
+ *is* the default). States re-value channels only — they never add structure.
290
+
291
+ ```yaml
292
+ styles:
293
+ interaction.cursor: pointer
294
+ interaction.cursor@disabled: not-allowed
295
+ appearance.background: [modifier.color.foreground.primary, color.foreground.primary]
296
+ appearance.background@hover: color.foreground.primary.hover
297
+ # A slot may read a channel only in one state.
298
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing, currentColor]
299
+ ```
300
+
301
+ **Vocabulary (closed, registry-governed):**
302
+
303
+ | State | Meaning | Cross-system mapping |
304
+ |-------|---------|----------------------|
305
+ | `hover` | Pointer over the element | universal |
306
+ | `active` | Being pressed/activated | MD3 *pressed*, Spectrum *down*, CSS `:active` |
307
+ | `focus` | Keyboard focus indication | Spectrum *key-focus*; web implementations map to `:focus-visible` |
308
+ | `disabled` | Interaction unavailable | CSS `:disabled`, `aria-disabled` |
309
+ | `selected` | Chosen within a set | `aria-selected`, SLDS/Spectrum *selected* |
310
+ | `expanded` | A disclosure is open | `[open]`, `aria-expanded`, MD3 *expanded* |
311
+ | `indeterminate` | A checkbox or progress is mixed, on neither setting | CSS `:indeterminate`, `aria-checked="mixed"` |
312
+ | `invalid` | Validation has failed | CSS `:invalid`, `aria-invalid` |
313
+
314
+ Every mature system governs states as closed-with-registry (Spectrum's states
315
+ registry, ARIA's list of ten, SLDS's linted grammar). Candidate additions —
316
+ `checked`, `visited`, `dragged`, `pending`, `error`, `read-only` — enter
317
+ through the registry with a definition and per-platform mapping, never by
318
+ loosening the schema. `expanded`, `indeterminate` and `invalid` entered that
319
+ way, and the line they sit on is worth stating: a state is admitted where the
320
+ implementation RE-VALUES a channel in it, and refused where it changes what
321
+ exists. A disclosure header that takes a different background while open is
322
+ the first; the panel that exists only while open is the second, and stays the
323
+ Switch construct's territory — so `expanded` is a state and the panel's
324
+ presence is not. `open` is not admitted as a second spelling of `expanded`.
325
+
326
+ **Rules:**
327
+
328
+ - States hold style values only. A state that changes the tree MUST be
329
+ modelled as a `switch` (e.g. async-button's internal states), not a state.
330
+ - The gate and the appearance are separate questions: `props: { disabled: true }`
331
+ (or the consumer) *puts* a node in the disabled state; `…@disabled` styles
332
+ say how it *looks* there.
333
+ - A state-scoped value that differs from its base state's is REPORTED, never
334
+ rejected — a **lint, not a constraint**. The reference does it: Button's
335
+ `:disabled` reads `color.text.disabled`, a different symbol from its
336
+ resting `color.text`, and the reference is the source. The finding names
337
+ the ranks at which the two differ, and a state key whose base state carries
338
+ no binding at all is not compared (Chip's `dismiss` child has a `:hover`
339
+ background and no unmarked one, which is the standing case). No shape here
340
+ enforces agreement between a value's spelling and the key's `@state`.
341
+ - Compound states are reserved as repeatable markers (`@selected@hover`,
342
+ canonical order: value/control state before user-action state). This
343
+ version permits a single `@` per key.
344
+
345
+ In Turtle, the state is one optional dimension on the Style tuple:
346
+
347
+ ```turtle
348
+ :hasStyle
349
+ [ a :Style ; :styleKey "appearance.background" ;
350
+ :styleValue "[modifier.color.foreground.primary, color.foreground.primary]" ;
351
+ :consumes ( dt:modifier.color.foreground.primary dt:color.foreground.primary ) ] ,
352
+ [ a :Style ; :styleKey "appearance.background" ; :styleState "hover" ;
353
+ :styleValue "color.foreground.primary.hover" ;
354
+ :consumes ( dt:color.foreground.primary.hover ) ]
355
+ ```
356
+
357
+ **Platform mapping examples**:
358
+
359
+ | DSL Key | CSS | SwiftUI | Flutter |
360
+ |--------------------------|---------------------------|-----------------------------------|----------------------------------|
361
+ | `layout.type: flex` | `display: flex` | `VStack` / `HStack` | `Column` / `Row` |
362
+ | `layout.align: center` | `align-items: center` | `alignment: .center` | `crossAxisAlignment: center` |
363
+ | `spacing.internal.inline.start` | `padding-inline-start` | `.padding(.leading)` | `EdgeInsetsDirectional` |
364
+ | `spacing.gap` | `gap` | `spacing` parameter | `SizedBox` between |
365
+ | `appearance.background` | `background` | `.background()` | `Container` color |
366
+ | `appearance.radius` | `border-radius` | `.cornerRadius()` | `BorderRadius` |
367
+ | `size.width: 100%` | `width: 100%` | `.frame(maxWidth: .infinity)` | `double.infinity` |
368
+
369
+ ### 3.8 Switch
370
+
371
+ Models a polymorphic position in the anatomy tree — a place that can be filled by one of several alternatives. Appears at the edge level, parallel to `node`.
372
+
373
+ | Field | Type | Required | Description |
374
+ |---------|---------------------|----------|-------------------------------------------------------------------------------------|
375
+ | `on` | SwitchDiscriminator | Yes | What determines which case applies. See [Discriminator Vocabulary](#101-discriminator-vocabulary). |
376
+ | `cases` | SwitchCase[] | Yes | The available alternatives. |
377
+
378
+ ```yaml
379
+ edges:
380
+ - switch:
381
+ on: props
382
+ cases:
383
+ - uri: global.component.checkbox
384
+ - uri: global.component.radio
385
+ - uri: global.component.text-input
386
+ relation:
387
+ cardinality: "1"
388
+ slotName: input
389
+ ```
390
+
391
+ ### 3.9 SwitchCase
392
+
393
+ An individual case within a switch. Supports both shorthand (`uri`) and full form (`node`).
394
+
395
+ | Field | Type | Required | Description |
396
+ |--------|--------|----------|--------------------------------------|
397
+ | `uri` | string | No* | Shorthand for `node: { uri: ... }`. |
398
+ | `node` | Node | No* | Full node definition with styles/edges. |
399
+
400
+ > \* Exactly one of `uri` or `node` MUST be present.
401
+
402
+ ### 3.10 Projection
403
+
404
+ A fragment-style binding of the anatomy tree to graph data, after the Relay
405
+ colocation pattern: the tree carries its data requirements the way a Relay
406
+ component carries its fragment.
407
+
408
+ **On a node:**
409
+
410
+ | Field | Type | Required | Description |
411
+ |---------|--------|----------|-----------------------------------------------------------------------------|
412
+ | `on` | string | No* | GraphQL type condition — a type name from the provider schema (e.g. `Component`). |
413
+ | `field` | string | No* | Dot-delimited GraphQL field path relative to the enclosing data context (e.g. `_meta.title`). The node renders this field's value. |
414
+
415
+ > \* At least one of `on` or `field` MUST be present.
416
+
417
+ **On a relation:**
418
+
419
+ | Field | Type | Required | Description |
420
+ |---------|--------|----------|-----------------------------------------------------------------|
421
+ | `field` | string | Yes | The traversal populating the slot — a field of the current data context. |
422
+
423
+ A relation projection MUST NOT carry `on`: type narrowing belongs on the child
424
+ node, never on the relation.
425
+
426
+ **Semantics:**
427
+
428
+ - The **root node's** `on` establishes the data context: the anatomy is a
429
+ parameterized view over one entity of that type, like
430
+ `fragment EntityCard on Component`. Children inherit the parent's context.
431
+ - `field` on a **relation** is a traversal: the slot is populated from that
432
+ field of the current context. The DSL names the field only — never
433
+ `edges.node`. Anatomy cardinality on the relation corresponds to
434
+ multiplicity on the graph.
435
+ - `on` on a **child node** narrows the traversed entity's type — the analog of
436
+ an inline fragment. Switch cases carrying different `on` values mirror an
437
+ interface-typed connection resolved through inline fragments.
438
+ - `field` on a **node** (leaf usage) means the node renders that field's value.
439
+
440
+ **Cardinality mapping.** The relation's cardinality decomposes into two claims
441
+ about the projected field, both checkable against the provider SDL:
442
+
443
+ | Cardinality | Claim on the projected field |
444
+ |-------------|------------------------------------------------------------------|
445
+ | `1` | Non-null object or scalar field. |
446
+ | `0..1` | Nullable object or scalar field. |
447
+ | `0..*` | Connection or list, possibly empty. |
448
+ | `1..*` | Connection or list, asserted non-empty — deliberately stronger than GraphQL can express; checkable only at runtime. |
449
+
450
+ The **upper bound** maps to multiplicity (`..1` singular, `..*` plural); the
451
+ **lower bound** maps to nullability (`0..` tolerates `null`, `1..` requires
452
+ the provider to always have the value). A projection under `cardinality: "1"`
453
+ MUST target a field the provider makes total (e.g. `_meta.title`); a nullable
454
+ field like `summary` MUST sit under `0..1`.
455
+
456
+ **Mechanism-blindness.** Whether a plural field is a Relay connection
457
+ (`subcomponents`) or a plain list (`properties`) is a provider mechanism, not
458
+ an anatomy fact. Anatomies MUST NOT spell out unwrapping paths such as
459
+ `edges.node`; consumers discover the field's shape from the SDL and unwrap it
460
+ when needed. An anatomy therefore survives a provider promoting a list to a
461
+ connection unchanged.
462
+
463
+ These two rules constrain projections against a *schema*, so they cannot be
464
+ enforced by this package's SHACL shapes; enforcement belongs to a
465
+ consumer-side checker holding the provider SDL.
466
+
467
+ ```yaml
468
+ node:
469
+ uri: global.component.entity-card
470
+ projection:
471
+ on: Component
313
472
  edges:
314
473
  - node:
315
- role: header tab
316
- styles:
317
- layout.type: flow
318
- layout.direction: horizontal
319
- layout.align: center
320
- interaction.cursor: pointer
321
- edges:
322
- - node:
323
- role: control
324
- styles:
325
- size.width: size/icon/small
326
- size.height: size/icon/small
327
- relation:
328
- cardinality: "1"
329
- - node:
330
- role: heading
331
- relation:
332
- cardinality: "1"
333
- slotName: default
474
+ uri: global.subcomponent.entity-card-header
475
+ projection:
476
+ field: _meta.title
334
477
  relation:
335
478
  cardinality: "1"
336
479
  slotName: header
337
-
338
480
  - node:
339
- role: content panel
340
- styles:
341
- layout.overflow: hidden
481
+ uri: global.component.chip
482
+ projection:
483
+ field: _meta.title # relative to the traversed entity
342
484
  relation:
343
- cardinality: "1"
344
- slotName: default
485
+ cardinality: "0..*"
486
+ slotName: tags
487
+ projection:
488
+ field: documentationStages # a connection field on Component
489
+ ```
490
+
491
+ Field and type names are anchored to the GraphQL schema of the provider the
492
+ consumer runs against; the committed provider SDL is the naming authority.
493
+
494
+ **Name grammar.** Both fields carry schema names, and `ProjectionShape`
495
+ constrains their *syntax* (never their existence):
496
+
497
+ | Field | Pattern | Reads as |
498
+ |---------|--------------------------------------------------------|------------------------------------------------------|
499
+ | `on` | `^[A-Z][A-Za-z0-9_]*$` | A single GraphQL type name (`Component`). |
500
+ | `field` | `^[_A-Za-z][_A-Za-z0-9]*(\.[_A-Za-z][_A-Za-z0-9]*)*$` | Dot-delimited GraphQL field names (`_meta.title`). |
501
+
502
+ Whether the names so formed exist in the provider schema — and whether the
503
+ field's shape matches the relation's cardinality — remains a consumer-side
504
+ check, per the two rules above.
505
+
506
+ **In Turtle**, projections reify like styles do, with an explicit type so
507
+ `ProjectionShape` targets them. The same blank-node tuple serves both
508
+ positions — on a node, and on the reified relation:
509
+
510
+ ```turtle
511
+ # On a node: type condition, field, or both.
512
+ :hasProjection
513
+ [ a :Projection ; :projectionType "Component" ]
514
+
515
+ # On a relation: the traversal populating the slot.
516
+ :hasRelation
517
+ [ a :Relation ;
518
+ :cardinality "0..*" ;
519
+ :slotName "tags" ;
520
+ :hasProjection [ a :Projection ; :projectionField "documentationStages" ] ]
521
+ ```
522
+
523
+ Attaching to the reified `Relation` is what makes slot-level traversal
524
+ annotation possible without changing the `Edge` class.
525
+
526
+ #### Derived fragment
527
+
528
+ A fully projected anatomy is mechanically equivalent to a GraphQL fragment.
529
+ The derivation below is the consumption contract a renderer implements; it is
530
+ walked on the `entity-card` example (`examples/yaml/entity-card.anatomy.yaml`).
531
+
532
+ 1. The root's `on` becomes the fragment's type condition:
533
+ `fragment EntityCardAnatomy on Component`.
534
+ 2. Each node `field` becomes a selection in the current scope, expanding dot
535
+ paths into nested selections: `_meta.title` → `_meta { title }`.
536
+ 3. Each relation `field` becomes a nested selection that shifts the scope for
537
+ the child subtree. If the SDL types the field as a connection, the
538
+ consumer inserts the `edges { node { … } }` unwrapping — the anatomy never
539
+ spells it (mechanism-blindness); a plain list or object field nests
540
+ directly.
541
+ 4. Each child or switch-case `on` differing from the scope's type becomes an
542
+ inline fragment: `... on Pattern { … }`.
543
+ 5. Nodes and relations without projections contribute no selections —
544
+ structure-only positions are invisible to the fragment.
545
+
546
+ Applied to `entity-card`:
547
+
548
+ ```graphql
549
+ fragment EntityCardAnatomy on Component {
550
+ _meta { title } # header node
551
+ summary # summary text node
552
+ documentationStages { # tags relation (connection per SDL)
553
+ edges { node { _meta { title } } } # chip node, traversed scope
554
+ }
555
+ variants { # switch relation (interface-typed)
556
+ edges { node {
557
+ ... on Component { _meta { title } } # variant case
558
+ ... on Pattern { _meta { title } } # pattern case
559
+ } }
560
+ }
561
+ }
345
562
  ```
346
563
 
347
- ### Card
564
+ The derivation is deterministic, so a consumer can generate fragments from
565
+ anatomies at build time (the Relay-compiler moment) or interpret them at
566
+ runtime; both read the same triples.
567
+
568
+ ### 3.11 Prop
569
+
570
+ A **pinned prop value**: the anatomy fixes one prop of the referenced
571
+ component at this tree position. Authored as a flat map on a named node:
348
572
 
349
573
  ```yaml
350
574
  node:
351
- uri: global.component.card
575
+ uri: global.component.icon
576
+ props:
577
+ icon: chevron-down
578
+ ```
579
+
580
+ | Aspect | Rule |
581
+ |---------|------------------------------------------------------------------------|
582
+ | Keys | Prop names as defined on the component in the design system ontology (camelCase). |
583
+ | Values | Scalars (string, number, boolean), coerced to strings. No token paths, no fallback arrays — pins are values, not styles. |
584
+ | Nodes | Named nodes only. Anonymous nodes have no prop surface; `props` under a `role` node MUST be rejected. |
585
+
586
+ **Definition vs pinning.** The DSL never *defines* a prop surface — a
587
+ component's prop names, types, and optionality live in the design system
588
+ ontology (`ds:hasProperty`). A pin *asserts one value at one position*.
589
+ Whether the pinned prop exists on the component, and whether the value is
590
+ admissible (e.g. a glyph name present in the icon set), are consumer-side
591
+ checks against the design system graph — the same enforcement posture as the
592
+ projection rules in §3.10, which check against the provider SDL.
593
+
594
+ **Icons are the canonical pinning case.** The design system models the icon
595
+ as a component whose glyph is a required prop
596
+ (`ds:global.component.icon` › `ds:hasProperty [ ds:name "icon" ]`), so icon
597
+ usage in anatomies splits into exactly two cases and needs no icon-specific
598
+ construct:
599
+
600
+ | Case | Authoring |
601
+ |------|-----------|
602
+ | Consumer-filled icon slot (e.g. Button's `slotName: icon`) | Icon-component edge with a slot and **no pin** — the consumer chooses the glyph. |
603
+ | Component-intrinsic icon (accordion chevron, modal close ×, status glyphs) | Icon-component node with `props: { icon: … }` — the spec fixes the glyph. |
604
+
605
+ **Static by design.** A data-driven value is a [projection](#310-projection)
606
+ (`projection: { field: … }`); a state-driven one is a
607
+ [switch](#10-switch-construct) whose cases pin different values. Pins never
608
+ vary at runtime.
609
+
610
+ In Turtle, pins reify like styles, with an explicit type so `PropShape`
611
+ targets them:
612
+
613
+ ```turtle
614
+ :hasProp
615
+ [ a :Prop ; :propName "icon" ; :propValue "chevron-down" ]
616
+ ```
617
+
618
+ ### 3.12 Token binding
619
+
620
+ A **consumed symbol**: the anatomy states, per slot and per state, which token
621
+ symbols the implementation reads there and in what order it falls back through
622
+ them. Authored as an ordinary style value — there is no binding construct:
623
+
624
+ ```yaml
625
+ node:
626
+ uri: global.component.button
352
627
  styles:
353
- # Structural (implicitly invariant)
354
- layout.type: stack
355
- layout.direction: vertical
628
+ typography.color: [modifier.color.text, color.text]
629
+ ```
630
+
631
+ | Aspect | Rule |
632
+ |---|---|
633
+ | Keys | One member of the closed roster in `definitions/registry.ttl`. |
634
+ | Values | A symbol, a primitive, or a list that is the fallback order — symbols with at most one primitive, and only as the last element. |
635
+ | Symbols | The `dt:` symbol's own dotted name, verbatim: `color.text`, `dimension.100`, `typography.weight.semiBold`. camelCase segments are the symbol's spelling and are not renamed. |
636
+ | Channels | A channel is a symbol like any other: `modifier.color.text`, `surface.color.background`. The anatomy consumes it by name where the implementation reads the channel, and the semantic token by name where it reads the token. Nothing is inferred from the spelling. |
637
+ | Order | The list is the fallback order: element 1 is the primary symbol, each next is what the implementation falls back to when the one before is undefined. |
638
+ | Literals | A terminal literal the chain ends in (`currentColor`, `0`, `2px`) is kept as the last element. It is not a symbol and is consumed by nothing. |
639
+
640
+ **Definition versus binding.** The DSL never *defines* a token — a symbol's
641
+ value, its coverage and its resolved values at each coordinate live in the
642
+ token graph (`@canonical/token-ontology`, strata S1–S4). A binding *asserts
643
+ which symbols one slot consumes*. Two consequences follow. Whether a symbol
644
+ exists at all is not asked here: a name no stratum declares is kept as
645
+ consumed, with a comment and a row in the corpus's register, because the
646
+ programme represents what the implementations do and takes no new token
647
+ decisions. And whether the symbol is *appropriate* for the key is the
648
+ registry's `anatomy:tokenNamespace`, which is a declaration and not a
649
+ derivation.
650
+
651
+ **Consumer-side validation.** The same posture as [§3.10](#310-projection)'s
652
+ projections and [§3.11](#311-prop)'s pins. This package checks the form: the
653
+ grammar, the closed key roster, the shape of the `anatomy:consumes` list, and
654
+ that the retired notation cannot reach the graph. A consumer holding the token
655
+ graph checks the facts:
656
+
657
+ | Check | Who | Outcome |
658
+ |---|---|---|
659
+ | the value parses, the key is in the roster | this package | parse error, or a SHACL violation |
660
+ | every element of `consumes` is a `dt:` symbol IRI | this package (SHACL) | violation |
661
+ | a token-kind key consumes at least one symbol | this package (SHACL) | violation |
662
+ | the symbol RESOLVES in S1 or S2 | the consumer | a finding, unless the register admits it |
663
+ | the symbol is inside the key's `tokenNamespace` | the consumer | a finding, unless the register admits it |
664
+ | a state's value differs from its base state's | the consumer | a **lint**: reported with its ranks, never rejected |
665
+
666
+ **The worked case.** Button's root reads, in the reference implementation:
667
+
668
+ ```css
669
+ .ds.button {
670
+ color: var(--modifier-color-text, var(--button-color-text));
671
+ }
672
+ :root {
673
+ --button-color-text: var(--color-text);
674
+ }
675
+ ```
676
+
677
+ Two names, one chain: the text *channel* first — through which the anticipation
678
+ and emphasis families reach the component without it knowing their members —
679
+ then the plain text token. The component-local `--button-color-text` is
680
+ followed through its own definition and replaced by what it consumes. So the
681
+ anatomy says:
682
+
683
+ ```yaml
684
+ typography.color: [modifier.color.text, color.text]
685
+ ```
686
+
687
+ and that is correct at every importance, because the channel is what the slot
688
+ reads; which symbol the channel is *worth* under a given importance is the
689
+ platform's business, not the anatomy's. A single symbol per key could not
690
+ carry this, and a fallback declared once on the channel could not either:
691
+ Button's background and Button's text fall back differently.
692
+
693
+ In Turtle, the seam is an ordered `rdf:List` on the style tuple, beside the
694
+ authored spelling it was lifted from:
695
+
696
+ ```turtle
697
+ :hasStyle
698
+ [ a :Style ;
699
+ :styleKey "typography.color" ;
700
+ :styleValue "[modifier.color.text, color.text]" ;
701
+ :consumes ( dt:modifier.color.text dt:color.text ) ]
702
+ ```
703
+
704
+ `:styleValue` keeps the authored spelling verbatim, as the evidence of what
705
+ the reference says — it is where a terminal literal lives, since a literal is
706
+ not a symbol — and `:consumes` is the form a query can walk. The design system
707
+ graph reifies the same fact one record per symbol per rank
708
+ (`ds:TokenBinding`, `ds:consumesSymbol`, `ds:rank`), which is what answers
709
+ "which symbols does this component consume, and which arrive through a
710
+ subcomponent".
711
+
712
+
713
+ ---
714
+
715
+ ## 4. URI Encoding Convention
716
+
717
+ URIs follow Turtle-inspired conventions with dot-separated paths.
718
+
719
+ | Symbol | Meaning | Example |
720
+ |--------|--------------------------------------|-----------------------------------|
721
+ | `.` | Path hierarchy | `global.component.button` |
722
+ | `_` | Word boundary (from PascalCase) | `CardHeader` -> `card_header` |
723
+ | `-` | Compound name (from dot in label) | `Card.Header` -> `card-header` |
724
+
725
+ ### Examples
726
+
727
+ | Component | URI |
728
+ |----------------|--------------------------------------|
729
+ | Button | `global.component.button` |
730
+ | Accordion.Item | `global.subcomponent.accordion-item` |
731
+ | CardHeader | `global.subcomponent.card_header` |
732
+
733
+ ### Parent Reference in Subcomponents
734
+
735
+ Subcomponents MUST reference their parent in the name:
736
+
737
+ | Labelled Name | Turtle URI |
738
+ |----------------------|---------------------------------------------|
739
+ | Timeline.Item | `global.subcomponent.timeline-item` |
740
+ | Accordion.ItemHeader | `global.subcomponent.accordion-item_header` |
741
+
742
+ ---
743
+
744
+ ## 5. Symbols and the Value Grammar
745
+
746
+ A **symbol** is the name of a token in the token graph, written in the
747
+ symbol's own dotted spelling and nothing else: no slash paths, no marker, no
748
+ prefix that means something the reader has to know. The grammar below runs
749
+ over the **parsed document** — a scalar, or a sequence of scalars — and never
750
+ over raw text.
751
+
752
+ ```ebnf
753
+ Binding = Key ("@" State)? Value ; a mapping entry: the key scalar, then the value node
754
+ Key = Path ; the registry closes the roster (§3.7)
755
+ State = "hover" | "active" | "focus" | "disabled" | "selected"
756
+ | "expanded" | "indeterminate" | "invalid"
757
+ Value = Symbol | Primitive | List
758
+ List = Symbol+ (Symbol | Primitive) ; a sequence of two or more scalars, in fallback order
759
+ Symbol = Path
760
+ Path = Namespace ("." Segment)+
761
+ Namespace = [a-z]+ ; color, spacing, dimension, typography, modifier, surface
762
+ Segment = [A-Za-z0-9]+ ; text, focusRing, 100
763
+ Primitive = Keyword | Number | Dimension | Color | Quoted
764
+ Keyword = [a-zA-Z]+ ("-" [a-zA-Z]+)* ; currentColor, inherit, auto, flow, not-allowed, space-between
765
+ Number = "-"? [0-9]+ ("." [0-9]+)? ; 0, 1.6, 600
766
+ Dimension = Number ([a-z]+ | "%") ; 2px, 1s, 0.25ch, 0%
767
+ Color = "#" [0-9a-fA-F]{3,8} ; #ccc
768
+ Quoted = any other text ; "1 / -1", "*" — a literal holding a space or a slash
769
+ ```
770
+
771
+ ### Examples
772
+
773
+ | Value | Reads as |
774
+ |---|---|
775
+ | `color.text` | one symbol |
776
+ | `dimension.100` | one symbol; a numeric segment is a segment |
777
+ | `typography.weight.semiBold` | one symbol; camelCase is the symbol's own spelling |
778
+ | `modifier.color.text` | one symbol — a channel, consumed by name |
779
+ | `[modifier.color.text, color.text]` | two symbols, in fallback order |
780
+ | `[modifier.color.icon, modifier.color.text, currentColor]` | two symbols and the literal the chain ends in |
781
+ | `flow` | one primitive, for a key whose `valueKind` admits one |
782
+ | `"1 / -1"` | one primitive: a literal holding a slash is a literal, not a path |
783
+
784
+ **A key and a symbol are one shape.** Both are a `Path`; the registry tells
785
+ them apart, which is why neither needs a sigil.
786
+
787
+ **Classification is by shape, not by quoting.** A scalar satisfying `Path` is
788
+ that symbol however it was written; one satisfying no primitive form either is
789
+ `Quoted` — which is what a literal tail holding a space or a slash needs. So
790
+ `"1 / -1"` is a primitive whether or not YAML required the quotes.
791
+
792
+ **A primitive may stand alone** only for a key the registry says takes one,
793
+ and **may end a list** for any key that takes a token, where it is the
794
+ terminal fallback the implementation writes last. It may not appear anywhere
795
+ else: a primitive before the end of a list is a parse error, because the
796
+ sequence is a fallback order and a literal resolves against nothing.
797
+
798
+ **Rejections.** Each is a typed error naming the value and the rule:
799
+
800
+ | Value | Rule |
801
+ |---|---|
802
+ | `spacing/medium`, `color/focus-ring` | the slash-delimited path is retired (§12) |
803
+ | `color.surface.button?`, `color/surface/button?` | the trailing `?` marker is retired (§12) |
804
+ | `$root`, `color.$root.text`, `color.root.text` | `root` and `$root` are not value segments |
805
+ | `[2px, dimension.stroke.thickness.large]` | a primitive may only end a value |
806
+ | `[color.text]` | a sequence is a fallback order and needs two or more elements |
807
+
808
+ A value that satisfies the grammar may still be a **placeholder**: a segment
809
+ in `definitions/style-keys.yaml`'s `placeholders` list (`sth`, `tbd`, `xxx`,
810
+ `todo`) makes `dimension.radius.sth` parse and then fail the corpus's second
811
+ check, which removes the binding and registers it. The list is data, not a
812
+ rule in the parser.
813
+
814
+ **One lift, shared.** `liftSymbols(value)` returns the symbols in fallback
815
+ order and is exported, so every consumer lifts identically;
816
+ `definitions/lift.fixture.json` is the committed case list both this package
817
+ and design-system assert against.
818
+
819
+ ---
820
+
821
+ ## 6. Cardinality Notation
822
+
823
+ Cardinality values follow standard data modelling notation:
824
+
825
+ ```
826
+ cardinality = exact | range
827
+ exact = digit+
828
+ range = digit+ ".." ( digit+ | "*" )
829
+ ```
830
+
831
+ | Value | Meaning |
832
+ |----------|------------------------|
833
+ | `"1"` | Exactly one (required) |
834
+ | `"0..1"` | Zero or one (optional) |
835
+ | `"0..*"` | Zero or more |
836
+ | `"1..*"` | One or more |
837
+ | `"2..5"` | Between 2 and 5 |
838
+
839
+ Cardinality values MUST be quoted strings in YAML to avoid parsing ambiguity.
840
+
841
+ ---
842
+
843
+ ## 7. Slot Mapping
844
+
845
+ The `slotName` field on a relation maps content to a named slot in the parent component.
846
+
847
+ | Value | Meaning |
848
+ |-------------|-------------------------------------------------------------------------|
849
+ | `"default"` | Main content slot (`children` in React, default slot in Vue). |
850
+ | `"header"` | Header slot. |
851
+ | `"footer"` | Footer slot. |
852
+ | `"icon"` | Icon placement. |
853
+ | `"label"` | Text label. |
854
+ | *(custom)* | Any application-specific slot name. |
855
+
856
+ When `slotName` is omitted, the edge does not target a specific named slot.
857
+
858
+ ---
859
+
860
+ ## 8. Named vs Anonymous Rule
861
+
862
+ | Type | Identifier | Use Case |
863
+ |-----------|------------|-------------------------------------------------------------|
864
+ | Named | `uri` | User-instantiable components (things users compose in code) |
865
+ | Anonymous | `role` | Internal structural elements not directly instantiated |
866
+
867
+ **Rule of thumb**: Ask "Can/should a user write `<ComponentName>` in their code?" If yes, use `uri`. If no, use `role`.
868
+
869
+ ---
870
+
871
+ ## 9. DRY Principle for Named Nodes
872
+
873
+ When a node has a URI, it references its own anatomy file. Do not inline the full tree — reference the URI only. This keeps anatomy files focused and avoids duplication.
874
+
875
+ ```yaml
876
+ # Correct: reference only
877
+ edges:
878
+ - node:
879
+ uri: global.component.button
880
+ relation:
881
+ cardinality: "1"
882
+
883
+ # Incorrect: inlining full definition
884
+ edges:
885
+ - node:
886
+ uri: global.component.button
887
+ styles:
888
+ # ... full button styles duplicated here
889
+ edges:
890
+ # ... full button edges duplicated here
891
+ relation:
892
+ cardinality: "1"
893
+ ```
894
+
895
+ ---
896
+
897
+ ## 10. Switch Construct
898
+
899
+ The `switch` construct models positions in the anatomy tree that can be filled by one of several alternatives. It operates at the edge level, parallel to `node`, making polymorphism explicit in the structure.
900
+
901
+ ### Syntax
902
+
903
+ ```yaml
904
+ edges:
905
+ - switch:
906
+ on: <discriminator>
907
+ cases:
908
+ - uri: <component-uri>
909
+ - uri: <component-uri>
910
+ relation:
911
+ cardinality: <cardinality>
912
+ slotName: <slot>
913
+ ```
914
+
915
+ ### 10.1 Discriminator Vocabulary
916
+
917
+ | Value | Meaning | Use Case |
918
+ |------------|------------------------------------------|------------------------------------------------------------|
919
+ | `props` | Consumer chooses via component props | `<Field type="checkbox">` renders Checkbox vs Radio |
920
+ | `internal` | Component manages choice internally | AsyncButton shows Loading/Success/Error based on state |
921
+ | `override` | Consumer can replace with custom component | Timeline accepts custom Event component via slot |
922
+
923
+ ### 10.2 Shorthand Expansion
924
+
925
+ The shorthand `- uri: X` expands to `- node: { uri: X }`. This allows cases to be expressed concisely when no additional metadata is needed, while permitting full node definitions when case-specific styles or nested edges are required.
926
+
927
+ ```yaml
928
+ # Shorthand (common case)
929
+ cases:
930
+ - uri: global.component.checkbox
931
+
932
+ # Expands to full form
933
+ cases:
934
+ - node:
935
+ uri: global.component.checkbox
936
+ ```
937
+
938
+ When a case requires additional properties:
939
+
940
+ ```yaml
941
+ cases:
942
+ - node:
943
+ uri: global.component.textarea
944
+ styles:
945
+ size.height: hug
946
+ size.min.height: dimension.1600
947
+ ```
948
+
949
+ ### 10.3 Reserved URI: $custom
950
+
951
+ The `$custom` URI is a reserved value indicating that the position accepts a user-provided component. This keeps case entries uniform under the `uri` key while signalling extensibility points.
952
+
953
+ ```yaml
954
+ cases:
955
+ - uri: global.subcomponent.timeline-event
956
+ - uri: $custom
957
+ ```
958
+
959
+ ---
960
+
961
+ ## 11. The Fallback List
962
+
963
+ **The list is the value form, not a special case of it.** A style value is a
964
+ symbol, a primitive, or a list; where it is a list, the list *is* the fallback
965
+ order — a one-to-one transcription of the implementation's own `var()` chain.
966
+
967
+ ```yaml
968
+ styles:
969
+ typography.color: [modifier.color.text, color.text]
970
+ ```
971
+
972
+ which the reference writes as
973
+ `color: var(--modifier-color-text, var(--color-text))`. Element 1 is the
974
+ primary symbol; each next is what the implementation falls back to when the
975
+ one before it is undefined. A terminal literal stays as the last element:
976
+
977
+ ```yaml
978
+ typography.color: [modifier.color.icon, modifier.color.text, currentColor]
979
+ ```
980
+
981
+ Three reasons the list is the form rather than an option:
982
+
983
+ 1. **The fallback is a per-slot choice.** Button's background falls back
984
+ through the primary-foreground channel to the primary foreground token; its
985
+ text falls back through the text channel to the plain text token; its
986
+ border falls back to a *highlighted* border that itself falls back to the
987
+ plain one. No rule declared once on a symbol can carry that, because the
988
+ slots differ.
989
+ 2. **Some slots consume a channel and some the token.** Button's root colour
990
+ reads the text channel; its `.link` variant reads no channel at all.
991
+ Whether a slot subscribes to a family is a fact about the slot, and only
992
+ the anatomy can state it.
993
+ 3. **The chain is the information.** An implementation generated or checked
994
+ from the anatomy must be able to write
995
+ `var(--modifier-color-text, var(--color-text))` from what the anatomy says,
996
+ `currentColor` included where the reference has it.
997
+
998
+ In the graph the list becomes `anatomy:consumes`, an ordered `rdf:List` whose
999
+ head is the primary symbol; `anatomy:styleValue` keeps the authored spelling
1000
+ beside it. See [§3.12](#312-token-binding).
1001
+
1002
+ ---
356
1003
 
357
- # Themeable
358
- appearance.background: color/surface/card
359
- appearance.shadow: shadow/card
360
- appearance.radius: shape/rounded/full
361
- spacing.gap: spacing/small
1004
+ ## 12. Retired Notation
362
1005
 
1006
+ Three notations this reference described before 0.4.0 are **retired**, and
1007
+ each is now a parse error and a SHACL violation. They are listed so a reader
1008
+ of an older anatomy knows what they are looking at.
1009
+
1010
+ | Retired | Example | What to write | Why |
1011
+ |---|---|---|---|
1012
+ | The slash path | `spacing/medium` | the symbol's dotted name, `spacing.gap.field.block` | The paths named no symbols. Checked against the token graph, `color/fill/default` matched nothing, `spacing/small` matched nothing, `radius/button` matched nothing: the anatomy spoke a parallel vocabulary and nothing caught it. |
1013
+ | The optional marker | `color/surface/button?` | the symbol the implementation reads; a name no stratum declares is not written at all, and a `#` comment on the line records it | A sigil that means "may be undefined in some themes" is a rule the reader has to memorise, and it stated an exception in a place no validator could reconcile with the list of exceptions. |
1014
+ | The `$root` segment | `color/surface/$root` | the dotted name without it | It was an artefact of the token files' own shape, not part of a symbol's name. |
1015
+
1016
+ **The kebab-to-dotted bridge.** An older value migrates by splitting on `/`,
1017
+ dropping a trailing `root` or `$root`, stripping a trailing `?`, camelCasing
1018
+ each kebab segment (`focus-ring` → `focusRing`) and joining with `.`. Where
1019
+ the result is a symbol the token graph declares, that is the value; where it
1020
+ is not, a correspondence table decides, and nothing is substituted merely to
1021
+ make a name resolve.
1022
+
1023
+ **What replaces the marker.** The question the `?` tried to answer — "is this
1024
+ value guaranteed to exist?" — is a **query over the token graph**, not an
1025
+ annotation on the value. A symbol's coverage is S2's (`dt:covers`: which
1026
+ families provision it, at which coordinates) and its resolved values are S3's
1027
+ (one row per symbol per coordinate per mode). So:
1028
+
1029
+ - *does this symbol exist?* — is it a `dt:TokenSymbol` in S1, or a channel
1030
+ minted in S2;
1031
+ - *is it worth anything under this modifier?* — has S3 a resolved value for
1032
+ it at that coordinate;
1033
+ - *which families reach this slot?* — follow the consumed channel's
1034
+ `dt:channelOf` to its base symbol, then S2's coverage.
1035
+
1036
+ None of those is a fact the anatomy could have carried, and all of them are
1037
+ answerable now that the value names a symbol. Where a name resolves nowhere at
1038
+ all it is kept as consumed, with a comment and a row in the corpus's committed
1039
+ register — regenerated by a validator from what fails to resolve, so the
1040
+ exceptions and the document that lists them cannot drift apart without a diff
1041
+ saying so.
1042
+
1043
+ ---
1044
+
1045
+ ## 13. Examples
1046
+
1047
+ The nine examples ship with the package, as a YAML file and the Turtle it
1048
+ transforms to:
1049
+
1050
+ ```
1051
+ examples/yaml/<name>.anatomy.yaml the anatomy
1052
+ examples/turtle/<name>.ttl the emission, regenerated by `bun run generate goldens`
1053
+ ```
1054
+
1055
+ They are **corpus, not decoration**: the round-trip test reads them
1056
+ byte-for-byte, the SHACL run validates them against the shapes and the
1057
+ registry, and every symbol they consume is asserted to resolve in the token
1058
+ graph's S1 or S2. That is also why they are not copied into this document —
1059
+ the pair is generated, and a third copy in prose would be a second statement
1060
+ of one notation, which is the drift this release exists to end. Read them in
1061
+ the repository; the worked case below is the one exception, because
1062
+ [§3.12](#312-token-binding) argues from it.
1063
+
1064
+ | Example | Reference | Demonstrates |
1065
+ |---|---|---|
1066
+ | `accordion` | `react/ds-global` `Accordion` | nested named nodes at three depths, a repeating child (`1..*`), a surface fallback, `motion.*` bindings |
1067
+ | `card` | `react/ds-global` `Card` | four subgrid sections, a three-slot `border` split into width, style and colour, a quoted grid-line literal, an anonymous node |
1068
+ | `stateful-button` | `react/ds-global` `Button` | `@state` keys across default/hover/active/focus/disabled, three-rank chains, two channels then `currentColor` |
1069
+ | `field` | — | a `props` switch with URI-shorthand cases |
1070
+ | `async-button` | — | an `internal` switch with four cases |
1071
+ | `timeline` | — | an `override` switch with the reserved `$custom` |
1072
+ | `input-group` | `react/ds-global-form` `TextArea` | a switch with mixed cases, and a token-kind key that admits no bare primitive |
1073
+ | `entity-card` | — | projections on nodes, relations and switch cases, beside style bindings |
1074
+ | `status-header` | — | pinned props (intrinsic icons), a status switch, an unpinned consumer-filled slot |
1075
+
1076
+ ### The worked case: Button's root and icon
1077
+
1078
+ The anatomy (`examples/yaml/stateful-button.anatomy.yaml`, abridged):
1079
+
1080
+ ```yaml
1081
+ ---
1082
+ node:
1083
+ uri: global.component.button
1084
+ styles:
1085
+ layout.type: inline-flex
1086
+ interaction.cursor: pointer
1087
+ interaction.cursor@disabled: not-allowed
1088
+ appearance.background: [modifier.color.foreground.primary, color.foreground.primary]
1089
+ appearance.border.color: [modifier.color.border, color.border.highlighted, color.border]
1090
+ typography.color: [modifier.color.text, color.text]
1091
+ typography.color@disabled: color.text.disabled
1092
+ spacing.internal.inline.start: spacing.inset.action.inline
1093
+ appearance.outline.color@focus: [modifier.color.focusRing, color.focusRing, currentColor]
363
1094
  edges:
364
- # Named children reference their URI only (each carries its own styles
365
- # in its own anatomy); anonymous nodes carry their styles inline. A
366
- # named child may carry contextual style overrides — never a copy of
367
- # its subtree. The live Card models it the same way.
368
1095
  - node:
369
- uri: global.subcomponent.card-header
1096
+ uri: global.component.icon
1097
+ styles:
1098
+ typography.color: [modifier.color.icon, modifier.color.text, currentColor]
370
1099
  relation:
371
1100
  cardinality: "0..1"
372
- slotName: header
1101
+ slotName: icon
1102
+ ```
373
1103
 
374
- - node:
375
- uri: global.subcomponent.card-image
376
- relation:
377
- cardinality: "0..1"
378
- slotName: media
1104
+ and what it emits, for the root's colour and the icon's:
379
1105
 
380
- - node:
381
- uri: global.subcomponent.card-content
382
- relation:
383
- cardinality: "1..1"
384
- slotName: default
1106
+ ```turtle
1107
+ @prefix : <https://anatomy.canonical.com/> .
1108
+ @prefix dt: <https://dt.canonical.com/> .
1109
+
1110
+ [ a :Style ; :styleKey "typography.color" ;
1111
+ :styleValue "[modifier.color.text, color.text]" ;
1112
+ :consumes ( dt:modifier.color.text dt:color.text ) ]
1113
+
1114
+ [ a :Style ; :styleKey "typography.color" ;
1115
+ :styleValue "[modifier.color.icon, modifier.color.text, currentColor]" ;
1116
+ :consumes ( dt:modifier.color.icon dt:modifier.color.text ) ]
1117
+ ```
1118
+
1119
+ The icon's terminal `currentColor` is in the spelling and in neither of the
1120
+ consumed symbols, which is the division of labour the two terms exist for.
1121
+
1122
+ ### Projections gallery
1123
+
1124
+ Each entry shows the authored YAML, the emitted Turtle for the interesting
1125
+ subtree (produced by `anatomyToTTL`, elided to the relevant blank node), and
1126
+ the fragment selection it derives. Type and field names reference the prism
1127
+ provider schema.
1128
+
1129
+ #### Leaf field
1130
+
1131
+ The header renders the entity's total title; `cardinality: "1"` is sound
1132
+ because `_meta.title` is total in the contract (see the cardinality mapping
1133
+ in §3.10).
1134
+
1135
+ ```yaml
1136
+ - node:
1137
+ uri: global.subcomponent.entity-card-header
1138
+ projection:
1139
+ field: _meta.title
1140
+ relation:
1141
+ cardinality: "1"
1142
+ slotName: header
1143
+ ```
1144
+
1145
+ ```turtle
1146
+ :edgeTarget [
1147
+ a :NamedNode ;
1148
+ :uri "global.subcomponent.entity-card-header" ;
1149
+ :hasProjection [ a :Projection ; :projectionField "_meta.title" ]
1150
+ ] ;
1151
+ :hasRelation [ a :Relation ; :cardinality "1" ; :slotName "header" ]
1152
+ ```
1153
+
1154
+ Derives: `_meta { title }`.
1155
+
1156
+ #### Type condition and field together
1157
+
1158
+ The node renders a field *and* asserts the type of what it renders — the
1159
+ combined form every switch case uses.
1160
+
1161
+ ```yaml
1162
+ - node:
1163
+ uri: global.component.badge
1164
+ projection:
1165
+ on: Tier
1166
+ field: tier
1167
+ relation:
1168
+ cardinality: "0..1"
1169
+ slotName: badge
1170
+ ```
1171
+
1172
+ ```turtle
1173
+ :hasProjection [ a :Projection ; :projectionType "Tier" ; :projectionField "tier" ]
1174
+ ```
1175
+
1176
+ Derives: `tier { … }` with the child scope typed `Tier`. Note `0..1`, not
1177
+ `1`: `Component.tier` is nullable, and the lower bound maps to nullability.
1178
+
1179
+ #### Connection traversal filling a slot
1180
+
1181
+ The relation names the connection field; each traversed entity binds one
1182
+ chip. The anatomy never spells `edges.node`.
1183
+
1184
+ ```yaml
1185
+ - node:
1186
+ uri: global.component.chip
1187
+ projection:
1188
+ field: _meta.title # relative to the traversed Tag
1189
+ relation:
1190
+ cardinality: "0..*"
1191
+ slotName: tags
1192
+ projection:
1193
+ field: documentationStages
1194
+ ```
1195
+
1196
+ ```turtle
1197
+ :hasRelation [
1198
+ a :Relation ;
1199
+ :cardinality "0..*" ;
1200
+ :slotName "tags" ;
1201
+ :hasProjection [ a :Projection ; :projectionField "documentationStages" ]
1202
+ ]
1203
+ ```
1204
+
1205
+ Derives: `documentationStages { edges { node { _meta { title } } } }` — the
1206
+ unwrapping comes from the SDL typing the field as a connection, not from the
1207
+ anatomy.
385
1208
 
1209
+ #### Singular traversal
1210
+
1211
+ An object field, not a connection: same construct, singular cardinality.
1212
+
1213
+ ```yaml
1214
+ - node:
1215
+ uri: global.component.link
1216
+ projection:
1217
+ field: _meta.title
1218
+ relation:
1219
+ cardinality: "0..1"
1220
+ slotName: parent
1221
+ projection:
1222
+ field: superclass
1223
+ ```
1224
+
1225
+ Derives: `superclass { _meta { title } }`.
1226
+
1227
+ #### Plain-list traversal
1228
+
1229
+ Identical authoring to the connection case — `Component.properties` is
1230
+ `[Property!]!` in the prism SDL, so the consumer nests directly with no
1231
+ unwrapping. The anatomy is unchanged if the provider later promotes the list
1232
+ to a connection (mechanism-blindness, §3.10).
1233
+
1234
+ ```yaml
1235
+ - node:
1236
+ role: property row
1237
+ projection:
1238
+ field: _meta.title
1239
+ relation:
1240
+ cardinality: "0..*"
1241
+ projection:
1242
+ field: properties
1243
+ ```
1244
+
1245
+ Derives: `properties { _meta { title } }`.
1246
+
1247
+ #### Nested traversals
1248
+
1249
+ A child inherits the traversed scope, narrows it, and traverses again — a
1250
+ recursive view over the class hierarchy.
1251
+
1252
+ ```yaml
1253
+ node:
1254
+ uri: global.component.class-tree
1255
+ projection:
1256
+ on: OntologyClass
1257
+ edges:
386
1258
  - node:
387
- uri: global.subcomponent.card-footer
1259
+ uri: global.subcomponent.class-tree-item
1260
+ projection:
1261
+ on: OntologyClass
1262
+ field: _meta.title
1263
+ edges:
1264
+ - node:
1265
+ role: instance row
1266
+ projection:
1267
+ field: _meta.title
1268
+ relation:
1269
+ cardinality: "0..*"
1270
+ projection:
1271
+ field: instances
388
1272
  relation:
389
- cardinality: "0..1"
390
- slotName: footer
1273
+ cardinality: "0..*"
1274
+ slotName: children
1275
+ projection:
1276
+ field: subclasses
1277
+ ```
1278
+
1279
+ Derives:
1280
+
1281
+ ```graphql
1282
+ fragment ClassTreeAnatomy on OntologyClass {
1283
+ subclasses {
1284
+ _meta { title }
1285
+ instances { edges { node { _meta { title } } } }
1286
+ }
1287
+ }
1288
+ ```
1289
+
1290
+ For switch cases narrowing an interface-typed traversal into inline
1291
+ fragments, see `examples/yaml/entity-card.anatomy.yaml` and step 4 of the
1292
+ derivation in §3.10.
1293
+
1294
+ ---
1295
+
1296
+ ## 14. TypeScript Definitions
1297
+
1298
+ The complete type system expressed as TypeScript interfaces.
1299
+
1300
+ ```typescript
1301
+ // ─── Value Types ────────────────────────────────────────────────
1302
+
1303
+ /**
1304
+ * The dotted name of a token symbol, verbatim: "color.text",
1305
+ * "dimension.100", "modifier.color.text". A channel is a symbol like any
1306
+ * other. Whether the token graph declares it is a consumer-side check.
1307
+ */
1308
+ type Symbol = string;
1309
+
1310
+ /**
1311
+ * A literal the implementation writes as it stands: a CSS-wide keyword, a
1312
+ * number, a dimension, a colour, or any other text. It resolves against
1313
+ * nothing by design.
1314
+ */
1315
+ type Primitive = string | number;
1316
+
1317
+ /**
1318
+ * One member of the closed roster in definitions/registry.ttl. Same shape as
1319
+ * a symbol — a lowercase namespace then one or more segments — because the
1320
+ * registry is what tells the two apart.
1321
+ */
1322
+ type StyleKey = string;
1323
+
1324
+ /**
1325
+ * A symbol, a primitive, or a sequence that is the fallback order: symbols
1326
+ * with at most one primitive, and only as the last element.
1327
+ */
1328
+ type StyleValue = Symbol | Primitive | (Symbol | Primitive)[];
1329
+
1330
+ /**
1331
+ * Platform-agnostic style properties. A key MAY carry an interaction-state
1332
+ * marker: "appearance.background@hover". The unmarked key is the default
1333
+ * state.
1334
+ */
1335
+ type Styles = Record<StyleKey, StyleValue>;
1336
+
1337
+ // ─── Projection ─────────────────────────────────────────────────
1338
+
1339
+ /**
1340
+ * Fragment-style binding of a node to graph data.
1341
+ * At least one of `on` / `field` is required.
1342
+ */
1343
+ type Projection =
1344
+ | { on: string; field?: string }
1345
+ | { on?: string; field: string };
1346
+
1347
+ /**
1348
+ * Traversal populating a slot. The field is required; target typing
1349
+ * lives on the child node's own `on`, never on the relation.
1350
+ */
1351
+ interface RelationProjection {
1352
+ field: string;
1353
+ }
1354
+
1355
+ // ─── Relation ───────────────────────────────────────────────────
1356
+
1357
+ interface Relation {
1358
+ /** Instance count constraint (e.g. "1", "0..1", "1..*"). */
1359
+ cardinality: string;
1360
+
1361
+ /** Target slot in the parent component. */
1362
+ slotName?: string;
1363
+
1364
+ /** Traversal populating the slot. */
1365
+ projection?: RelationProjection;
1366
+ }
1367
+
1368
+ // ─── Nodes ──────────────────────────────────────────────────────
1369
+
1370
+ interface BaseNode {
1371
+ /** Graph-data binding. */
1372
+ projection?: Projection;
1373
+
1374
+ /** Style properties using CTI-inspired keys. */
1375
+ styles?: Styles;
1376
+
1377
+ /** Child nodes and their relationships. */
1378
+ edges?: Edge[];
1379
+ }
1380
+
1381
+ /**
1382
+ * Pinned prop values: the anatomy fixes props of the referenced component
1383
+ * at this position. Keys are prop names defined on the component in the
1384
+ * design system ontology; values are scalars. Named nodes only.
1385
+ */
1386
+ type Props = Record<string, string | number | boolean>;
1387
+
1388
+ /** Identifiable component in the design system. */
1389
+ interface NamedNode extends BaseNode {
1390
+ /** Unique identifier (e.g. "global.component.button"). */
1391
+ uri: string;
1392
+
1393
+ /** Pinned prop values (e.g. { icon: "chevron-down" }). */
1394
+ props?: Props;
1395
+ }
1396
+
1397
+ /** Structural element without design system identity. */
1398
+ interface AnonymousNode extends BaseNode {
1399
+ /** Human-readable description of the node's purpose. */
1400
+ role: string;
1401
+ }
1402
+
1403
+ type Node = NamedNode | AnonymousNode;
1404
+
1405
+ // ─── Switch ─────────────────────────────────────────────────────
1406
+
1407
+ /**
1408
+ * Discriminator types for switch construct.
1409
+ * - props: Consumer chooses via component props.
1410
+ * - internal: Component manages choice internally.
1411
+ * - override: Consumer can replace with custom component.
1412
+ */
1413
+ type SwitchDiscriminator = "props" | "internal" | "override";
1414
+
1415
+ /** A case in a switch construct. */
1416
+ interface SwitchCase {
1417
+ /** Shorthand for node.uri. */
1418
+ uri?: string;
1419
+
1420
+ /** Full node definition (expanded form). */
1421
+ node?: Node;
1422
+ }
1423
+
1424
+ /** Polymorphic position in the anatomy tree. */
1425
+ interface Switch {
1426
+ /** What determines which case applies. */
1427
+ on: SwitchDiscriminator;
1428
+
1429
+ /** Available alternatives. */
1430
+ cases: SwitchCase[];
1431
+ }
1432
+
1433
+ // ─── Edge ───────────────────────────────────────────────────────
1434
+
1435
+ interface Edge {
1436
+ /** Single node (mutually exclusive with switch). */
1437
+ node?: Node;
1438
+
1439
+ /** Polymorphic alternatives (mutually exclusive with node). */
1440
+ switch?: Switch;
1441
+
1442
+ /** Relationship metadata. */
1443
+ relation: Relation;
1444
+ }
1445
+
1446
+ // ─── Root ───────────────────────────────────────────────────────
1447
+
1448
+ /** Root of an anatomy file. Top-level node must be named. */
1449
+ interface AnatomySpec {
1450
+ node: NamedNode;
1451
+ }
391
1452
  ```
392
1453
 
393
- ## Further Work
1454
+ ---
394
1455
 
395
- The following features would require additional work to this specification:
1456
+ ## 15. Deferred Items
396
1457
 
397
- - Representation of related nodes displayed in portals
398
- - Representation of conditional display
399
- - Inheritance reference for styles (e.g., "NetworkCard" inherits the styles from "Card")
1458
+ | Item | Description |
1459
+ |----------------------|------------------------------------------------------------------------------------|
1460
+ | A JSON Schema for the YAML | `schemas/anatomy-dsl.schema.json` was DELETED in 0.4.0: it documented the retired notation — the slash path, the `?` marker — and an open key vocabulary, so the package would have shipped two contradictory statements of one notation. Generating a replacement from §5's grammar and the registry's roster is deferred until a consumer needs one. |
1461
+ | Pseudo elements | No grammar construct: there is no pseudo-element type, and no field that records which pseudo-element a node is. The part itself is NOT deferred — it is modelled, by convention, as an anonymous role node carrying a `# drawn as ::before` note, and the authored corpus does it that way. What a first-class construct would add is machine-readability of that note. |
1462
+ | Portal nodes | Representation of related nodes displayed in portals. |
1463
+ | Conditional display | Representation of conditionality in display. |
1464
+ | Style inheritance | Inheritance reference for styles (e.g. "NetworkCard" inherits styles from "Card"). |
400
1465
 
401
- ## References
1466
+ ---
402
1467
 
403
- Two main references have supported our reflection:
1468
+ ## 16. References
404
1469
 
405
- 1. The work of the [Open UI W3C Working Group](https://open-ui.org/), which takes interest in a code-based anatomy for their specifications ([Example](https://open-ui.org/components/accordion.explainer/))
406
- 2. The work of Nathan Curtis, exploring a similar syntax in his article "[Components as Data](https://medium.com/@nathanacurtis/components-as-data-2be178777f21)"
1470
+ - [Relay GraphQL Connection Specification](https://relay.dev/graphql/connections.htm) — inspiration for the reified edge/connection pattern
1471
+ - [Relay Fragment Colocation](https://relay.dev/docs/guides/fragment-composition/) — inspiration for the projection construct
1472
+ - [Open UI W3C Working Group](https://open-ui.org/) — code-based anatomy for component specifications ([example](https://open-ui.org/components/select/))
1473
+ - Nathan Curtis — ["Components as Data"](https://medium.com/eightshapes-llc/components-as-data-6e5fc295e30d)
1474
+ - [W3C Design Token Format](https://design-tokens.github.io/community-group/format/) — token naming conventions
1475
+ - [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) — normative language keywords (MUST, SHOULD, MAY)
1476
+ - [Wikipedia: Cardinality (data modeling)](https://en.wikipedia.org/wiki/Cardinality_(data_modeling))