@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.
- package/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -1,406 +1,1476 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
SYNCED FILE — do not edit here.
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
+
# Anatomy DSL — API Reference
|
|
14
21
|
|
|
15
|
-
1.
|
|
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
|
-
|
|
24
|
+
---
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
## Table of Contents
|
|
23
27
|
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
60
|
+
---
|
|
29
61
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
### Intended Usage
|
|
38
67
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
*
|
|
95
|
-
|
|
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
|
-
|
|
83
|
+
### Normative Language
|
|
105
84
|
|
|
106
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
relation: Relation;
|
|
264
|
-
}
|
|
120
|
+
### 3.1 AnatomySpec (root)
|
|
265
121
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
```
|
|
141
|
+
Node = NamedNode | AnonymousNode
|
|
142
|
+
```
|
|
278
143
|
|
|
279
|
-
|
|
144
|
+
### 3.3 NamedNode
|
|
280
145
|
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
179
|
+
role: content wrapper
|
|
310
180
|
styles:
|
|
311
|
-
layout.type:
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
481
|
+
uri: global.component.chip
|
|
482
|
+
projection:
|
|
483
|
+
field: _meta.title # relative to the traversed entity
|
|
342
484
|
relation:
|
|
343
|
-
cardinality: "
|
|
344
|
-
slotName:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
1101
|
+
slotName: icon
|
|
1102
|
+
```
|
|
373
1103
|
|
|
374
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
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
|
|
390
|
-
slotName:
|
|
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
|
-
|
|
1454
|
+
---
|
|
394
1455
|
|
|
395
|
-
|
|
1456
|
+
## 15. Deferred Items
|
|
396
1457
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
1466
|
+
---
|
|
402
1467
|
|
|
403
|
-
|
|
1468
|
+
## 16. References
|
|
404
1469
|
|
|
405
|
-
|
|
406
|
-
|
|
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))
|