@adobe/design-data-spec 4.8.0 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/conformance/README.md +32 -9
  2. package/conformance/invalid/SPEC-051/dataset.json +9 -3
  3. package/conformance/invalid/SPEC-052/dataset.json +14 -4
  4. package/conformance/invalid/SPEC-053/dataset.json +16 -4
  5. package/conformance/invalid/SPEC-054/dataset.json +18 -5
  6. package/conformance/invalid/SPEC-055/dataset.json +9 -3
  7. package/conformance/invalid/SPEC-057/dataset.json +5 -2
  8. package/conformance/invalid/SPEC-058/dataset.json +8 -3
  9. package/conformance/invalid/SPEC-059/dataset.json +28 -0
  10. package/conformance/invalid/SPEC-059/expected-errors.json +10 -0
  11. package/conformance/invalid/SPEC-060/dataset.json +44 -0
  12. package/conformance/invalid/SPEC-060/expected-errors.json +10 -0
  13. package/conformance/invalid/SPEC-061/dataset.json +63 -0
  14. package/conformance/invalid/SPEC-061/expected-errors.json +15 -0
  15. package/conformance/invalid/SPEC-062/dataset.json +50 -0
  16. package/conformance/invalid/SPEC-062/expected-errors.json +10 -0
  17. package/conformance/invalid/SPEC-063/dataset.json +57 -0
  18. package/conformance/invalid/SPEC-063/expected-errors.json +10 -0
  19. package/conformance/invalid/SPEC-064/dataset.json +53 -0
  20. package/conformance/invalid/SPEC-064/expected-errors.json +15 -0
  21. package/conformance/invalid/SPEC-065/dataset.json +78 -0
  22. package/conformance/invalid/SPEC-065/expected-errors.json +10 -0
  23. package/conformance/invalid/SPEC-066/dataset.json +37 -0
  24. package/conformance/invalid/SPEC-066/expected-errors.json +10 -0
  25. package/conformance/invalid/SPEC-067/dataset.json +60 -0
  26. package/conformance/invalid/SPEC-067/expected-errors.json +10 -0
  27. package/conformance/invalid/SPEC-068/dataset.json +37 -0
  28. package/conformance/invalid/SPEC-068/expected-errors.json +10 -0
  29. package/conformance/invalid/SPEC-069/dataset.json +63 -0
  30. package/conformance/invalid/SPEC-069/expected-errors.json +10 -0
  31. package/conformance/invalid/SPEC-070/dataset.json +71 -0
  32. package/conformance/invalid/SPEC-070/expected-errors.json +15 -0
  33. package/conformance/invalid/SPEC-071/dataset.json +77 -0
  34. package/conformance/invalid/SPEC-071/expected-errors.json +10 -0
  35. package/conformance/invalid/SPEC-072/dataset.json +52 -0
  36. package/conformance/invalid/SPEC-072/expected-errors.json +10 -0
  37. package/conformance/invalid/SPEC-073/dataset.json +59 -0
  38. package/conformance/invalid/SPEC-073/expected-errors.json +10 -0
  39. package/conformance/invalid/SPEC-074/dataset.json +36 -0
  40. package/conformance/invalid/SPEC-074/expected-errors.json +10 -0
  41. package/conformance/invalid/SPEC-075/dataset.json +53 -0
  42. package/conformance/invalid/SPEC-075/expected-errors.json +10 -0
  43. package/conformance/invalid/SPEC-076/dataset.json +62 -0
  44. package/conformance/invalid/SPEC-076/expected-errors.json +10 -0
  45. package/conformance/invalid/SPEC-076/manifest.json +6 -0
  46. package/conformance/invalid/SPEC-079/dataset.json +62 -0
  47. package/conformance/invalid/SPEC-079/expected-errors.json +10 -0
  48. package/conformance/invalid/SPEC-079/manifest.json +58 -0
  49. package/conformance/invalid/SPEC-080/dataset.json +62 -0
  50. package/conformance/invalid/SPEC-080/expected-errors.json +10 -0
  51. package/conformance/invalid/SPEC-080/manifest.json +32 -0
  52. package/conformance/invalid/SPEC-081/dataset.json +62 -0
  53. package/conformance/invalid/SPEC-081/expected-errors.json +10 -0
  54. package/conformance/invalid/SPEC-081/manifest.json +7 -0
  55. package/conformance/invalid/SPEC-082/dataset.json +62 -0
  56. package/conformance/invalid/SPEC-082/expected-errors.json +10 -0
  57. package/conformance/invalid/SPEC-082/manifest.json +14 -0
  58. package/conformance/invalid/SPEC-083/dataset.json +47 -0
  59. package/conformance/invalid/SPEC-083/expected-errors.json +10 -0
  60. package/conformance/invalid/SPEC-084/dataset.json +16 -0
  61. package/conformance/invalid/SPEC-084/expected-errors.json +10 -0
  62. package/conformance/valid/SPEC-025/dataset.json +1 -1
  63. package/conformance/valid/SPEC-049/dataset.json +1 -1
  64. package/conformance/valid/SPEC-051/dataset.json +40 -9
  65. package/conformance/valid/SPEC-064/dataset.json +64 -0
  66. package/conformance/valid/SPEC-064/expected-errors.json +4 -0
  67. package/conformance/valid/SPEC-065/dataset.json +89 -0
  68. package/conformance/valid/SPEC-065/expected-errors.json +4 -0
  69. package/conformance/valid/SPEC-079/dataset.json +62 -0
  70. package/conformance/valid/SPEC-079/manifest.json +58 -0
  71. package/package.json +1 -1
  72. package/rules/rules.yaml +373 -0
  73. package/schemas/anatomy-part.schema.json +11 -0
  74. package/schemas/component-changelog.schema.json +306 -0
  75. package/schemas/component-spec-sheet.schema.json +215 -0
  76. package/schemas/component-visual.schema.json +287 -0
  77. package/schemas/component.schema.json +80 -0
  78. package/schemas/manifest.schema.json +5 -0
  79. package/schemas/relationship.schema.json +8 -6
  80. package/spec/anatomy-format.md +97 -10
  81. package/spec/artifact-moves.md +157 -0
  82. package/spec/component-changelog.md +49 -0
  83. package/spec/component-format.md +4 -0
  84. package/spec/component-visuals.md +210 -0
  85. package/spec/index.md +5 -0
  86. package/spec/manifest.md +46 -29
  87. package/spec/option-constraints.md +87 -0
  88. package/spec/relationship-format.md +14 -11
@@ -0,0 +1,157 @@
1
+ # Artifact moves
2
+
3
+ **Spec version:** `1.0.0-draft` (see [Overview](index.md))
4
+
5
+ This document defines how one artifact moves between a platform manifest and the foundation: **promotion** (platform to foundation) and **demotion** (foundation to one platform). Moving a whole platform out of this repo is a different operation; see the ejection contract in [`platforms/README.md`](../../../platforms/README.md#the-ejection-contract).
6
+
7
+ **Principle.** A move is a data operation that changes no one's output unless they choose it. The artifact keeps its identity, has one owner at a time, and other platforms lose nothing without notice.
8
+
9
+ This chapter is separate from [Manifest](manifest.md) because a move happens over time, like the deprecation policy in [Evolution](evolution.md), while the manifest describes one static cascade.
10
+
11
+ ## What can move
12
+
13
+ Identity decides what can move. A move keeps the artifact's identity, so it needs a stable one.
14
+
15
+ | Artifact | Identity | Moves? | Notes |
16
+ | ------------------------------------ | ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
17
+ | Token | `uuid` | Yes | |
18
+ | Relationship (CTR) | `uuid` | Yes | Moves with its component, or on its own when the component is foundation-owned. |
19
+ | Component | `name` | Yes | Moves as a unit: anatomy, states, relationships, visual recipe and guidelines move with it. |
20
+ | Mode set | `name` | Yes | Promotion adds the set, or its extra modes, to the foundation set of that name. A mode set gets an optional `lifecycle` so a move is visible on the set itself (the schema change is tracked separately; see [Open questions](#open-questions)). |
21
+ | Visual recipe (planned) | component `name` | Yes | A file move between `extensions/visuals/` and the foundation `visuals/`. It has no `uuid`. A recipe of a platform-only component moves with the component and its relationships. See below. |
22
+ | Guideline | `name` | Only with its component | A guideline is not moved on its own. It follows the component it describes. |
23
+ | Field declaration | `name` | No | Fields are the shared naming vocabulary. A platform asks the foundation team to add a value or field. They stay foundation-owned. |
24
+ | Implementation mapping | n/a | No | Always platform-owned by definition (`extensions/implementations/`). |
25
+ | Platform extension (term annotation) | `termId` | No | It annotates a foundation term for one platform. It has no foundation form. |
26
+
27
+ A component is identified by `name`. Its move record says where it came from and when; the identity is separate and does not change.
28
+
29
+ **NORMATIVE (when platform recipes are implemented):** A recipe moves in the same change as its component. If a manifest `componentAliases` entry points at the component, the alias is manifest data, not an artifact: it is removed in that same change, and the impact check lists it. A recipe never moves alone when its component is platform-only. A recipe that replaces a foundation recipe of a foundation-owned component moves as a plain file move. The verify step (`validate --platform <id> --visuals-path`) then also covers platform-owned recipes (SPEC-079).
30
+
31
+ **NORMATIVE:** Things a moved component only *uses* (tokens, mode sets) do not move with it. Each is checked separately in the impact check; one that is still needed in the old place stays there, or is promoted in its own move.
32
+
33
+ ## Who decides
34
+
35
+ **NORMATIVE:** The foundation team decides every move. The platform team that owns the manifest **SHOULD** be part of the discussion.
36
+
37
+ A proposal is a tracked issue (a bead or a GitHub issue) that states:
38
+
39
+ 1. What moves, and in which direction.
40
+ 2. Why.
41
+ 3. Evidence from two or more platforms (promotion), or the design decision (promotion with one platform).
42
+ 4. For a promotion, the foundation values. The foundation team sets them; see [Foundation defaults](#foundation-defaults).
43
+
44
+ The issue's id is recorded in `lifecycle.moves[].proposal` when the move lands.
45
+
46
+ ## Promotion criteria
47
+
48
+ **NORMATIVE:** An artifact **MAY** be promoted when either holds:
49
+
50
+ * Two or more platforms need it.
51
+ * One platform needs it and the foundation team records a design decision that it belongs in the foundation.
52
+
53
+ One platform alone is not enough. An artifact stays platform-local until there is evidence.
54
+
55
+ Demotion has no evidence threshold. The foundation team demotes an artifact when it turns out only one platform uses it, or when the design decision behind it is reversed.
56
+
57
+ ## The six steps
58
+
59
+ The steps are the same in both directions. Where they differ, each direction is written out.
60
+
61
+ 1. **Proposal.** Open and agree the issue above.
62
+ 2. **Impact check.** List every platform and product that resolves, overrides or references the artifact. List everything the artifact references that is not moving with it. List product-layer overrides ([Product context](product-context.md)) in a separate, clearly marked list. A move cannot fix them, since neither the foundation nor a platform owns them, so the check names them as affected consumers to notify. They do not block the move. The planned `design-data platform move --dry-run` reports this.
63
+ 3. **Move.** Put the artifact in its new place, with the same identity, and remove it from the old place **in the same change**. Never leave a state where both places own it.
64
+ 4. **Compatibility.** Keep other platforms' output unchanged. See [Other platforms](#other-platforms).
65
+ 5. **Verify.** Run dataset validation, snapshot and legacy-output parity, and SPEC-076 for each affected platform (`validate --platform <id> --visuals-path`).
66
+ 6. **Record provenance.** Add a `lifecycle.moves` entry.
67
+
68
+ ### Promotion
69
+
70
+ * **3.** Move the file content from `platforms/<id>/extensions/<kind>/` into the matching foundation directory under `packages/design-data/`. Delete the extension file.
71
+ * **Mode added to a set.** **NORMATIVE:** A promotion that adds a mode to a foundation mode set **MUST** declare a fallback mode for it, and validation **MUST** check that the fallback exists in the set. A token that omits a mode field applies under the set's default, which for `colorScheme` is `light`, a poor stand-in for `dark-elevated`. Without a declared fallback, platforms with no values for the new mode would silently resolve to the wrong thing. SPEC-005 already checks peer-mode coverage; the fallback check is proposed as SPEC-078 below.
72
+ * **4.** The foundation record carries the values the foundation team set. A platform whose values differ keeps them as an `overrides` entry (tokens) or a platform replacement (mode sets, components). Its output does not change.
73
+ * **Version.** **NORMATIVE:** A promotion adds foundation content, so it is a **minor** change ([Evolution: Change classification](evolution.md#change-classification)). The release that contains it is the `version` recorded in the move.
74
+
75
+ ### Demotion
76
+
77
+ * **3.** Move the record into `platforms/<id>/extensions/<kind>/` with the same identity. In the foundation, replace it with a **deprecated stub**: the same identity and value, `lifecycle.deprecatedIn` set to the current foundation version, a `deprecatedComment` naming the platform, and the `lifecycle.moves` entry. The stub is the announcement.
78
+ * **4.** Other platforms keep resolving the stub for the whole notice period, so their output does not change.
79
+ * **Notice period.** **NORMATIVE:** The stub **MUST** stay in the published dataset for at least the window in [Evolution: Migration windows](evolution.md#migration-windows): two minor versions after `deprecatedIn`, or a later `plannedRemoval`. A demotion is a deprecation, not a removal, so the stub is a **minor** change. Removing the stub afterward is the breaking step. [Evolution](evolution.md) lists removal as a major change but also permits it two minors after `deprecatedIn`. These do not conflict: removal after the window is allowed in a minor release because the deprecation already gave notice, and removal before the window ends needs a major release. A demotion does not use `replacedBy`, because the successor has the *same* identity and lives outside the foundation.
80
+ * A platform that wants to keep the artifact **MUST** copy it into its own `extensions/` before the stub is removed. The `lifecycle.moves` entry names which platform received it.
81
+
82
+ ## Other platforms
83
+
84
+ Overrides and extensions in other platforms are kept, not rewritten.
85
+
86
+ * **Promotion.** An existing override still replaces the foundation record for that platform ([Manifest: overrides](manifest.md#overrides)). Its `overriddenFrom` now holds the **new** foundation value, because `overriddenFrom` is computed from whatever the foundation holds at load time. Platforms that already carried a copy of the artifact in `extensions/` **MUST** remove it in the same change, or SPEC-077 (below) would flag the duplicate.
87
+ * **Demotion.** An override of the demoted token keeps working while the stub exists, because the stub has the same `uuid`. When the stub is removed the override has no target. The impact check lists these overrides so their owners can act during the notice period.
88
+ * `overriddenBy` and `overriddenFrom` stay in-memory only. They say who is overriding a record now. They are not history; `lifecycle.moves` is.
89
+
90
+ ### Foundation defaults
91
+
92
+ **NORMATIVE:** When platforms disagree on a promoted artifact's values, the foundation team chooses the foundation values in the proposal. This specification does not pick a default.
93
+
94
+ ## Recording a move
95
+
96
+ **NORMATIVE:** Every move **MUST** add one entry to `lifecycle.moves` on the artifact (the foundation record, or the platform record after a demotion), keeping earlier entries. The array is a history, oldest first.
97
+
98
+ | Field | Type | Required | Meaning |
99
+ | ----------- | --------------------- | -------- | ------------------------------------------- |
100
+ | `direction` | `promote` or `demote` | Yes | Which way it moved. |
101
+ | `from` | place | Yes | Where it was. |
102
+ | `to` | place | Yes | Where it is now. |
103
+ | `version` | string (version) | Yes | Foundation version that contains the move. |
104
+ | `proposal` | string | Yes | Issue id of the proposal. |
105
+ | `date` | string (date) | No | Date the change landed. |
106
+ | `decidedBy` | string | No | Who decided, for example `foundation team`. |
107
+ | `evidence` | array of string | No | Platform ids that needed it (promotion). |
108
+ | `comment` | string | No | Free text. |
109
+
110
+ A **place** is `{ "foundation": true }` or `{ "platform": "<id>", "platformVersion": "<version>" }`. `platformVersion` is optional.
111
+
112
+ ```json
113
+ "lifecycle": {
114
+ "moves": [{
115
+ "direction": "promote",
116
+ "from": { "platform": "ios", "platformVersion": "0.3.0" },
117
+ "to": { "foundation": true },
118
+ "version": "3.8.0",
119
+ "proposal": "spectrum-design-data-p6ga.8.19",
120
+ "date": "2026-11-12",
121
+ "decidedBy": "foundation team",
122
+ "evidence": ["ios", "android"]
123
+ }]
124
+ }
125
+ ```
126
+
127
+ How these fields relate to the existing ones:
128
+
129
+ | Field | Where it lives | Relationship |
130
+ | -------------------------------- | ------------------------------ | ------------------------------------------------------- |
131
+ | `lifecycle.moves` | Authored, in the data file | Permanent history of moves. |
132
+ | `lifecycle.deprecatedIn` | Authored, on the demotion stub | Starts the notice period. A promotion does not set it. |
133
+ | `lifecycle.replacedBy` | Authored | Not used by moves; the successor has the same identity. |
134
+ | `overriddenBy`, `overriddenFrom` | In memory only | Current override state. Never written, never history. |
135
+
136
+ ## Detecting a half-finished move
137
+
138
+ **PROPOSED, not implemented.** Two rules.
139
+
140
+ `SPEC-077` (`artifact-single-owner`, error): when a dataset is loaded with a platform manifest, the same identity (token or relationship `uuid`, or component, mode set or guideline `name`) **MUST NOT** be *defined* in both the foundation and the platform's `extensions/`, with one exception. During a demotion window the artifact exists in both places on purpose: the foundation record is a deprecated stub (it has `lifecycle.deprecatedIn` and a `lifecycle.moves` entry whose `from` is the foundation) and the platform copy overrides it. The rule fires when both records are active and not deprecated, or when the stub outlives its window. A manifest `overrides` entry is not a definition and is always allowed.
141
+
142
+ `SPEC-078` (`promoted-mode-fallback`, error): a mode added to a foundation mode set by a promotion **MUST** have a declared fallback mode that exists in the same set. The field that declares it is decided in its implementation bead.
143
+
144
+ ## Open questions
145
+
146
+ Settled in this chapter: version bumps, the fallback for a promoted mode, the `lifecycle.moves` shape for components, the stub-and-copy overlap during a demotion window, and product-layer overrides in the impact check.
147
+
148
+ 1. **Mode-set schema.** This chapter says mode sets carry an optional `lifecycle`. The mode-set schema has none and allows no extra properties, so adding it is a published schema change with its own changeset. Tracked as a follow-up bead.
149
+ 2. **Fallback field.** Where the fallback mode is declared (on the mode set, or per mode) is not decided.
150
+ 3. **`platformVersion` on `overriddenBy`.** Deferred in p6ga.8.9. Add it only if diff tooling or the iOS Figma work needs it.
151
+
152
+ ## References
153
+
154
+ * [Manifest](manifest.md): capability matrix, overrides and `extensions/`.
155
+ * [Evolution](evolution.md): deprecation lifecycle and migration windows.
156
+ * [Token format: Lifecycle and metadata](token-format.md#lifecycle-and-metadata).
157
+ * [Cascade](cascade.md): layer precedence.
@@ -0,0 +1,49 @@
1
+ # Component changelog
2
+
3
+ **Spec version:** `1.0.0-draft` (see [Overview](index.md))
4
+
5
+ A **component changelog** is the generated, committed history of one component: what changed in its records between releases, why (source), and the component version that follows. `design-data changelog` writes `changelog/<component>.json` next to the dataset; the format is `schemas/component-changelog.schema.json`.
6
+
7
+ ## Records compared
8
+
9
+ Two dataset revisions are compared record by record. Formatting and key order never produce a change.
10
+
11
+ | Layer | Identity |
12
+ | -------------- | ---------------------------------------------------------------------------------------------------------- |
13
+ | Tokens | The [semantic diff](diff.md) (uuid, then name object, then replacement link) |
14
+ | Relationships | `uuid`; then scope + context + target; a leftover pair with the same scope and context is a **re-binding** |
15
+ | Components | Options, option values, anatomy parts, states, slots by name; document blocks, implementations, lifecycle |
16
+ | Visual recipes | The component's recipe file and its parts |
17
+
18
+ A removed and an added option, state, anatomy part or slot that differ only by name are one **rename**.
19
+
20
+ ## Attribution
21
+
22
+ Each change attaches to a component, to the relationship context (the spec-sheet block title) it appears under, and to a **source**: the PR number in the squash subject (`... (#123)`), otherwise the commit sha and subject. A global token reaches a component through the relationships that bind it. A token a component binds whose own record is unchanged but whose alias-resolved value moved is an **indirect** change. Registry and mode-set changes are not fanned out to components.
23
+
24
+ A record's own `rationale` is copied into the entry when present. History comes from the diff; the source is the fallback reason.
25
+
26
+ ## Classes
27
+
28
+ | Class | Meaning |
29
+ | --------------- | ------------------------------------------------------------------------------------------------------ |
30
+ | `visual` | How the component looks or behaves: token values, relationship bindings, visual recipes |
31
+ | `api` | Options, option values, states, anatomy, slots, constraints, relationships added or removed, lifecycle |
32
+ | `documentation` | Descriptions, document blocks, rationale, relationship context text |
33
+ | `metadata` | Legacy keys, ids, ordering, implementations, spec bookkeeping (summarized, not listed) |
34
+
35
+ **Metadata-class changes are not entries.** Each commit records them as `metadata: {count, fields}`: how many changes, and which field paths changed (a record added or removed is named `<kind> <change>`, e.g. `implementation added`). They count as a patch.
36
+
37
+ In the committed file every entry, source and metadata summary is written on one line.
38
+
39
+ A token entry is a **Values updated** change; a relationship entry that changes only its target is a **Bindings changed** change. They are never merged.
40
+
41
+ ## Component version
42
+
43
+ The version is derived from the entries of each release; it is never authored. A component's first release is `1.0.0`. Each release applies the highest bump among its entries:
44
+
45
+ * **major**: an option, option value, state, anatomy part, slot or relationship removed; an option, state, anatomy part or slot renamed; an option `type` changed; the component deprecated or removed.
46
+ * **minor**: anything added, including a visual recipe (metadata-class additions are patch).
47
+ * **patch**: value changes (direct or indirect), documentation, metadata, rationale.
48
+
49
+ Each release lists its changes per commit, each commit with its source. Releases are append-only: a release already recorded is never rewritten, and generating the same release again changes nothing. The exception is the pending release named `unreleased`, which holds changes not yet in a package release: the next release takes over its commits (adding newer ones) and the version is worked out again from the version before it.
@@ -208,6 +208,10 @@ Example with a deprecated option value:
208
208
  }
209
209
  ```
210
210
 
211
+ ### Constraints
212
+
213
+ A component MAY declare `constraints`, an array of if-then rules restricting which option value combinations are valid. Each rule has `when`, `require`, and `severity` (`error` or `advisory`), and optionally `description` and `source`. Layer 1 checks shape only. See [Option constraints](option-constraints.md).
214
+
211
215
  ## Slots
212
216
 
213
217
  The `slots` block declares the component's named **content injection points** — the positions where consumers place child content. Slot declarations are derived from the cross-platform audit in [`audits/slots.audit.md`](../audits/slots.audit.md).
@@ -0,0 +1,210 @@
1
+ # Component visuals
2
+
3
+ **Spec version:** `1.0.0-draft` (see [Overview](index.md))
4
+
5
+ This document defines the **component visual recipe**: a renderer-neutral description of how a component looks, bound to design tokens. A recipe lets tooling produce SVG, PNG, and Figma output, plus anatomy and spec overlays, from one source. It is not tied to Figma, React, or any other platform.
6
+
7
+ Status: **draft / RFC**. The schema is [`component-visual.schema.json`](../schemas/component-visual.schema.json). A pilot recipe lives at `packages/design-data/visuals/action-button.json`.
8
+
9
+ ## Goals and non-goals
10
+
11
+ **Goals**
12
+
13
+ * One data source drives three views of a component: a variant × state matrix (usage), anatomy callouts, and token-labelled measurements (specs).
14
+ * Every geometric or paint value is token-bound, so overlays can name the token behind each measurement.
15
+ * Output targets are plural: SVG first, then PNG, then Figma.
16
+
17
+ **Non-goals**
18
+
19
+ * Pixel parity with every platform implementation. Recipes describe the Spectrum design spec, not a browser's rendering of it.
20
+ * Replacing component declarations. Options, states, slots and anatomy stay in [component format](component-format.md).
21
+ * Describing interaction, motion or accessibility behavior.
22
+
23
+ ## Layers
24
+
25
+ | Layer | Owns | Where |
26
+ | --------------------- | ------------------------------------------------------------------------- | -------------------------------------- |
27
+ | Component declaration | Options, states, slots, anatomy names | `components/<name>.json` |
28
+ | **Visual recipe** | Part tree, layout, shape, paint, text, token bindings | `visuals/<name>.json` (this spec) |
29
+ | Render request | Which component, option values, states and modes to draw; matrices | Tool input (not stored in the dataset) |
30
+ | Scene graph | Resolved rects, paths, text, groups, measurements with part and token ids | Renderer output; backends consume it |
31
+
32
+ **NORMATIVE:** A recipe **MUST** name exactly one component through `component`, and that component **MUST** be declared. *Planned Layer 2 rule; not yet enforced.*
33
+
34
+ **NORMATIVE:** Every key of `parts` **MUST** be a declared anatomy part of the component. *Planned Layer 2 rule; not yet enforced.* Recipes therefore require components to declare anatomy.
35
+
36
+ ## Recipe object
37
+
38
+ | Field | Type | Required | Description |
39
+ | ------------- | ------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | `specVersion` | string | RECOMMENDED | Spec version label. |
41
+ | `component` | string | REQUIRED | Component `name`. |
42
+ | `root` | string | REQUIRED | Key of the root part in `parts`. |
43
+ | `sizeScale` | object | OPTIONAL | Maps each `size` option value to a numeric scale index (e.g. `m` → `100`). Enables `{sizeIndex}` in selectors. A component with no `size` option declares one entry, the size renderers draw. |
44
+ | `notVisual` | array | OPTIONAL | Component options and states that deliberately have no visual in this recipe (for example `["isReadOnly"]`). SPEC-066. |
45
+ | `parts` | object | REQUIRED | Map of anatomy part name → part definition. |
46
+
47
+ ### Part
48
+
49
+ | Field | Type | Description |
50
+ | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51
+ | `kind` | enum | `box`, `text`, `icon`, `ring`. `ring` is an outline drawn outside the parent box (focus indicator). |
52
+ | `children` | array of strings | Ordered child part names. Child order is layout order. Children **MUST** be declared parts. |
53
+ | `visibleWhen` | condition | The part is drawn only when the condition holds. Absent means always drawn. |
54
+ | `placement` | object | For a child that is positioned rather than flowed: `anchor` (`top-start`, `top-end`, `bottom-start`, `bottom-end`, `center`) and optional `inset` value. `start`/`end` are logical and flip under RTL. |
55
+ | `around` | string | For a `ring`: the part it outlines. Defaults to the root. |
56
+ | `glyph` | enum | For an `icon` nested in a box: the built-in glyph to draw (`checkmark`, `dash`, `chevron`). Unset draws a placeholder. A `chevron` points to the end; `rotation` turns it. |
57
+ | `pressTarget` | string | For the root: the part the pressed scale applies to (for example a switch handle). Parts nested inside it scale with it (a checkbox `box` scales with its mark). Defaults to the whole scene. |
58
+ | `props` | object | Map of **visual property** → value source or conditional list. |
59
+
60
+ ### Visual properties
61
+
62
+ | Property | Applies to | Meaning |
63
+ | ---------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ |
64
+ | `direction`, `align`, `justify` | box | Root layout: `direction` `row` (default) or `column`; `align` and `justify` `start`, `center` (default) or `end`. |
65
+ | `gap` | box | Space between flowed children. |
66
+ | `paddingInlineStart`, `paddingInlineEnd`, `paddingBlockStart`, `paddingBlockEnd` | box | Inner padding. |
67
+ | `height`, `minWidth`, `width` | box, icon | Dimensions. Box width defaults to content. |
68
+ | `cornerRadius` | box, ring | Corner radius. |
69
+ | `cornerRadiusStartStart`, `cornerRadiusStartEnd`, `cornerRadiusEndStart`, `cornerRadiusEndEnd` | box | Per-corner radius (block-inline). Each falls back to `cornerRadius`. Renderers draw left-to-right, so start-start is top-left. |
70
+ | `fill`, `stroke`, `strokeWidth`, `opacity` | box, ring | Paint. |
71
+ | `color` | text, icon | Content color. |
72
+ | `trackColor` | progress | Color of the progress track behind the arc. |
73
+ | `fontFamily`, `fontSize`, `fontWeight`, `lineHeight` | text | Text style. `lineHeight` sets the line box height. |
74
+ | `gapToParent` | ring | Distance between the ring and the parent box. |
75
+ | `rotation` | icon | Clockwise rotation in degrees about the icon's center (a `chevron` is 90 when open). |
76
+
77
+ **Text height.** A `text` part is as high as its line box: a `lineHeight` ending in `px` is that height, a unitless `lineHeight` multiplies `fontSize`, and with no `lineHeight` it is 1.3 times `fontSize`. A bound `fontFamily` leads the SVG font stack, so a family the viewer lacks falls back to the default stack.
78
+
79
+ **Opacity.** Renderers read `opacity` only on the root part. It dims the whole component as one group (SVG `<g opacity>`), so overlapping parts do not show through each other, the way iOS dims a disabled control. On any other part `opacity` is ignored (SPEC-083).
80
+
81
+ **Height.** A root `box` with no `height` is 32 high, unless the recipe binds `paddingBlockStart` or `paddingBlockEnd`: then its height is the tallest flowed child plus the block padding, and the children sit between the paddings. An explicit `height` always wins and the block padding then only offsets the children.
82
+
83
+ **NORMATIVE:** A renderer **MUST** clamp a uniform `cornerRadius` that is larger than half the shorter side of its box to that value, on all four corners, so the corner stays circular (like CSS `border-radius`).
84
+
85
+ **Row boxes.** A flowed `box` that binds `direction: "row"` lays its `children` out in a row instead of insetting them: `icon`, `text` and `box` children in order, `gap` apart, between its `paddingInlineStart` and `paddingInlineEnd`, centered across (`align` moves them). Without a `width` it is as wide as its children plus padding; without a `height` it is as tall as the tallest child plus its block padding. A `text` child may bind its own padding props, which add to its cell. Only `row` is supported.
86
+
87
+ **Column stretch.** In a column root, a flowed `box` with no `width` and no row content fills the column's inner width (a divider or a content region). A flowed `box` with no `height` and no children is as tall as its block padding.
88
+
89
+ **Nested boxes.** A flowed `box` child paints itself (`fill`, `stroke`, `strokeWidth`, `cornerRadius`) and may list its own `children`, which are boxes laid out inside it. A nested child is centered across the parent. Along the inline axis its `justify` prop (`start`, `center`, `end`) places it, with an inset equal to the cross-axis gap. A nested child with no `width` or `height` fills the parent. A `cornerRadius` of `0.5` with `relativeTo: "height"` draws a pill, or a circle for a square box. The switch uses this for its track and handle. A nested `icon` is centered on both axes and draws its `glyph`; the checkbox uses this for its checkmark and dash.
90
+
91
+ ### Value sources
92
+
93
+ A property value is either one **value source** or an ordered list of **conditional value sources**. A value source is exactly one of:
94
+
95
+ * `{"value": <literal>}`: a literal. Use only for geometry that no token describes (e.g. a glyph path).
96
+ * `{"token": {<name-object fields>}}`: a token selected by name-object fields, resolved with the [cascade](cascade.md).
97
+ * `{"relationship": {"property": "...", "context": "..."}}`: a token chosen through the component's [relationships](relationship-format.md). `property` and `context` match the relationship's `scope.property` and `context`.
98
+
99
+ Selector strings **MAY** contain `{size}` (the request's `size` option value, e.g. `m`), `{sizeIndex}` (that size's entry in `sizeScale`) and `{<option>}` for any other option the component declares (for example `{style}`). A `{<option>}` placeholder expands to each value the option declares, and a candidate found for one value applies only when the request's option equals that value, so one selector serves every value. A brace pair whose content is not a plain identifier (letters and digits) is literal text. A scalar `token` selector value matches an array-valued name field that contains it (e.g. `"state": "disabled"` matches `["disabled"]`).
100
+
101
+ **NORMATIVE:** A relationship may pin component options in its `scope.options` (a value, or an array of values that match when any listed value matches). For a `relationship` selector the renderer **MUST** keep only the matching relationships whose pinned options all match the request, and **MUST** prefer the ones that pin the most options. A relationship that pins no options is the default: it applies only when no relationship that pins options matches. Keys that are not declared options of the component, and the reserved keys `state`, `scale` and `colorScheme`, are not pinned options. A relationship's `scope.options.state` adds states to those of the token it binds, so a hover color that is a plain palette token can still be selected by state. This lets the component data, not the recipe, say which token applies under which options (for example a badge label is white by default, black for the light variants, and gray for subtle).
102
+
103
+ **NORMATIVE:** `token` and `relationship` selectors yield a set of candidate tokens. The renderer **MUST** narrow the candidates with the request's mode context (for example `colorScheme`, `scale`) using the [cascade](cascade.md), and **MUST** report a diagnostic if zero or more than one candidate remains.
104
+
105
+ A conditional entry is a value source plus `when`. A condition has optional `options` (option name → value, or array of values), `states` (state names that must all be active) and `modes` (mode-set name → mode). An array matches when any listed value matches, and counts as one condition.
106
+
107
+ **NORMATIVE:** When several entries match a request, the renderer **MUST** choose the one with the most conditions (options + states + modes). Ties go to the later entry in document order. An entry without `when` is the default. An entry **MAY** set an integer `priority` (default 0); a higher priority wins before condition count is compared. Use it for states that override every other combination, such as `disabled`.
108
+
109
+ An entry on `cornerRadius` **MAY** set `relativeTo: "height"`, making its numeric value a ratio of the container height (a pill is `0.5`). A `stroke` on a `box` is drawn inside its bounds, so `strokeWidth` does not change the layout size.
110
+
111
+ **NORMATIVE:** A renderer **MUST NOT** substitute a literal for a value source it cannot resolve. It **MUST** emit a diagnostic naming the part, property and selector, and omit that property.
112
+
113
+ ### Interim: relationship `context` strings
114
+
115
+ Relationship `context` is free text today (for example `Background (selected, emphasized)`). Recipes bind to those strings so the pilot can ship without changing relationship data. This is brittle: a reworded context breaks a recipe, and SPEC-062 reports a `relationship` selector that matches no relationship. **RECOMMENDED:** a follow-up adds structured `options` to relationships so recipes can select by option and state instead of by label.
116
+
117
+ ## Validation
118
+
119
+ The recipe schema checks one file's structure (Layer 1). A JSON Schema cannot read other files, so cross-file checks against component and relationship data are Layer 2 rules. They run only when the recipe catalog is loaded (`design-data validate --visuals-path`) and are skipped when no component catalog is loaded.
120
+
121
+ | Rule | Checks |
122
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
123
+ | SPEC-060 | `component` is declared; `parts` keys are anatomy parts; `root` is a top-level anatomy part; `children` appear in the anatomy part's `contains`; `around` and `pressTarget` resolve |
124
+ | SPEC-061 | `when` and `visibleWhen` options exist with allowed values and types; states exist on the component; a `{<option>}` placeholder in a selector names a declared option that lists its values |
125
+ | SPEC-062 | `relationship` selectors resolve to a record scoped to the component; `token` selectors match a token (`{size}` and `{sizeIndex}` expand over `sizeScale`, and `{<option>}` over the option's values, first) |
126
+ | SPEC-063 | `sizeScale` keys equal the component's `size` option values (one entry when it has none) |
127
+ | SPEC-066 | Warning. Every option and state the component declares drives the recipe or is listed in `notVisual`; `notVisual` names are declared and unused |
128
+ | SPEC-067 | Warning. A `token` selector does not bind a token that one of the component's relationships already binds |
129
+ | SPEC-068 | Warning. A literal does not supply a color or a non-zero size (layout keywords, `transparent`, zero and ratio radii are fine) |
130
+ | SPEC-069 | Warning. A `token` or `relationship` selector does not resolve only to deprecated tokens (extends SPEC-058 to recipes) |
131
+ | SPEC-070 | Warning. Recipes name the focus ring part (`kind: "ring"`) alike |
132
+ | SPEC-071 | Info. Counts relationship contexts of the component that the recipe never binds |
133
+ | SPEC-075 | Error. A part's `visibleWhen.states` agrees with the `presentIn` of its anatomy part (info when either lists more than one state; see [Anatomy format](anatomy-format.md#presentin)) |
134
+ | SPEC-076 | Warning. Runs only with a platform manifest (`validate --platform <id> --visuals-path`). Every binding that resolves on the foundation also resolves once the manifest's cascade is applied |
135
+ | SPEC-079 | Warning. A platform-owned recipe renders over the request grid with no scene diagnostic. Planned to become an error in a later minor release |
136
+ | SPEC-080 | Info. A platform recipe replaces the foundation recipe of the same component name |
137
+ | SPEC-083 | Warning. A part binds only props the renderer reads for it (the root, its `kind`, or a `placement`); the schema accepts more, and the renderer ignores the rest |
138
+
139
+ SPEC-066 to SPEC-070 are warnings, so `validate` stays green while recipes are migrated. A ratchet keeps the debt from growing: `sdk/core/tests/recipe_lint_baseline.rs` compares each (rule, recipe) warning count with `sdk/core/tests/fixtures/recipe-lint-baseline.json`. A new warning fails the test, an improvement must lower the baseline, and a recipe that is not in the baseline must have none.
140
+
141
+ ## Platform-owned recipes
142
+
143
+ Implemented for `render`, `site` and the Rust core (see [Proposal 014](../../../docs/proposals/014-platform-owned-visual-recipes.md)). `render` and `site` load the foundation recipes into the graph, then apply the platform's `extensions/visuals/` over them, and look recipes up through `componentAliases`.
144
+
145
+ * **Location.** **NORMATIVE (when implemented):** A platform recipe is `extensions/visuals/<component>.json`, one recipe per file, in the platform's extensions directory (honoring `extensionsDir`, see [Manifest](manifest.md#extensions-directory)). The file name **MUST** equal `component`. It validates against the same recipe schema as a foundation recipe, so promoting or demoting it is a file move ([Artifact moves](artifact-moves.md)).
146
+ * **Precedence.** A platform recipe **replaces** the foundation recipe of the same component name as a whole file. There is no layering. A foundation recipe stays the default for platforms that have none. A replaced recipe is a copy that can drift; SPEC-080 reports the pair.
147
+ * **Resolution.** A recipe's selectors resolve against the platform-merged graph (the manifest's tokens, relationships, components and anatomy). A recipe replaces only the look; anatomy stays neutral, so a recipe may use only the parts the platform-merged component declares (SPEC-060).
148
+ * **Validation.** `validate --platform <id>` runs SPEC-060 to SPEC-075 and SPEC-083 over platform-owned recipes against the platform-merged graph, plus SPEC-079 (every request in the grid renders). SPEC-076 keeps its foundation comparison and covers only foundation recipes the platform did not replace.
149
+
150
+ ## Authoring conventions
151
+
152
+ These are the conventions the lints above enforce, plus the decisions made while adding the first five recipes. Follow them in a new recipe; a change to an existing recipe that breaks one needs a reason.
153
+
154
+ 1. **Bind through relationships.** A recipe takes a value from the component's relationships by default (SPEC-067). Use a `token` selector only when no relationship covers the value, and a literal only for layout keywords, `transparent`, zero, ratio radii or geometry no token describes (SPEC-068). If the token you need is missing, file the gap; do not hide it behind a literal. Put option-dependent choices in the relationships, not in the recipe: pin `scope.options` on the relationship records (a default with no options, overrides that pin them) and bind the role once, so a variant added later is a data change. **Exception: platform-owned components.** A recipe for a component that a platform manifest declares (for example `button-ios`) MAY bind platform tokens directly with `token` selectors, because the platform's own code binds tokens by name and has no relationship layer. This applies to platform-owned components only; foundation components stay relationship-first. SPEC-062 and, for platform recipes, SPEC-079 keep checking that every selected token resolves. Watch the recipe size: the mapping from option to token then lives in the recipe, so name the platform's tokens so that selectors can template the option values, and measure the recipe's line count (see [Proposal 014](../../../docs/proposals/014-platform-owned-visual-recipes.md)).
155
+ 2. **Bind current tokens.** Do not bind a deprecated token (SPEC-069; SPEC-058 covers relationships). When the only token is deprecated, bind it and record the gap, so the lint keeps showing it.
156
+ 3. **Selection is an option, not a state.** Model "selected" as the `isSelected` option and scope relationships with `options.isSelected: true`. The renderer treats `isSelected` as the `selected` state and `isDisabled` as `disabled` when choosing between tokens that carry a state.
157
+ 4. **Cover the component.** Every option and state the component declares either changes the recipe or is listed in `notVisual` (SPEC-066). `notVisual` records a deliberate absence, for example `isReadOnly` (it only suppresses hover and press) or `icon` until a shared icon registry exists.
158
+ 5. **Press scales the part the design scales.** Set `pressTarget` to the part the design source scales when pressed: the checkbox scales its `box` (with its mark), the switch scales its `handle`. Parts nested inside the target scale with it, and the label does not.
159
+ 6. **Priorities.** Disabled overrides interaction states with `priority: 10` (11 where a more specific disabled case must win). Error uses `priority: 5`.
160
+ 7. **Relationship contexts are the design token group label,** with no `S1` or `S2` prefix, and the same label names the same concept in every component (for example `Control (disabled)`).
161
+ 8. **Name the focus ring part alike** across recipes (SPEC-070).
162
+ 9. **Prove a change.** `sdk/core/tests/recipe_snapshots.rs` renders every valid option, state, size, color scheme and scale combination of every recipe and compares a hash per option set with `tests/fixtures/recipe-snapshots.json`. It also fails on any render diagnostic. After a recipe, relationship, token or renderer change, review the option sets it reports with `design-data render`, then regenerate with `UPDATE_RECIPE_SNAPSHOTS=1`. A new recipe is covered automatically.
163
+
164
+ New recipes **MUST** be free of SPEC-066 to SPEC-070 warnings. Existing recipes are held at their baseline until they are migrated.
165
+
166
+ ## Render request and scene graph
167
+
168
+ A render request is tool input, not dataset content: `{component, options, states, modes, overlays}`. A **matrix** is a list of requests plus a layout (rows × columns by option or state).
169
+
170
+ The scene graph is the contract between resolution and output:
171
+
172
+ * Nodes: `group`, `rect` (with corner radii), `path`, `text`, `ring`.
173
+ * Every node carries `part` (anatomy name) and, for each token-bound property, `tokens` (property → token name or uuid and resolved value).
174
+ * Measurements (height, padding, gap, radius) are recorded as dimension annotations with their token. Overlay backends draw dimension lines and callouts from these; recipes do not author overlays.
175
+
176
+ **NORMATIVE:** A backend **MUST** produce the same scene graph input for the same request and dataset version (deterministic output).
177
+
178
+ ### Compiled recipes
179
+
180
+ A build step **MAY** pre-resolve every `token` and `relationship` binding per `size|colorScheme|scale` into a ranked list of candidates (`{value, token, states}`), so a renderer needs no token graph. Only state-dependent selection remains at render time: take the first candidate whose `states` all apply, else the first. Backends that consume compiled recipes (for example the dependency-free JS runtime emitted by `design-data site`) **MUST** produce the same scene as the Rust reference renderer; the reference renderer is the source of truth and parity is tested.
181
+
182
+ ## Backend and tooling decisions (research outcomes)
183
+
184
+ These record the research behind this draft and are not normative.
185
+
186
+ **R1 — Playground (Domino) fit: reference only for now.** Domino models component *instances* (`componentType`, props, `items`/`itemsOrder`, slots). It has no geometry, paint, layout or token-binding concepts, no JSON Schema for the node envelope, and no document-format versioning. Its `0react-spectrum/s2` schemas are generated from React Spectrum, and prop names (`isQuiet`, `isDisabled`, `staticColor`) mostly match ours. The repository is private (`"private": true`; MIT metadata but no LICENSE file), so a public spec must not depend on its schemas. Its Figma plugin maps Figma → Domino, the opposite of our Figma output. Decision: keep recipes independent. A render request can later be converted to or from a Domino instance by an adapter, because option names align. Revisit when Domino publishes a versioned envelope schema.
187
+
188
+ **R2 — Text measurement: approximate metrics with Source Sans 3.** Adobe Clean is proprietary and cannot be redistributed in this repository. Source Sans 3 (SIL OFL) is the open fallback. Renderers ship a width table for the fallback font and emit SVG `<text>` with the stack `adobe-clean-spectrum-vf, Adobe Clean, Source Sans 3, sans-serif`, so viewers with Adobe Clean available see it. Overlay positions use the measured width and may differ slightly from the viewer's font. SVG output maps font-weight names from tokens (`medium`, `bold`, and so on) to numeric weights. Exact shaping (rustybuzz or similar) is deferred until a licensed-font story exists. Outlining text to paths is **not** used for SVG, since it would require the proprietary font. **RECOMMENDED:** a page that embeds the SVG inline loads Adobe Clean through its own web font (for example the Typekit kit the docs site uses, family `adobe-clean-spectrum-vf`). Standalone `.svg` files, SVG used as an `<img>` or CSS background, and rasterized PNG output cannot load web fonts. They use the installed font or fall back to Source Sans 3, so exports **MAY** look slightly different from the live view. Outlining text at export time with a locally licensed font is a possible later option.
189
+
190
+ **R3 — Layout: minimal flex first.** The pilot box model needs row direction, centering, gap, padding and one absolutely placed child. A purpose-built layout of that size avoids a dependency and keeps WASM small. Revisit `taffy` when text-field (wrapping, stacked label and help text) is piloted.
191
+
192
+ **R4 — Icons.** Glyph geometry is sourced as SVG path data keyed by icon name (`workflow-icon`) and by the pilot's fixed glyphs (hold-icon corner triangle, checkmark). Literal paths are the one allowed use of `{"value": ...}`. A shared icon registry is follow-up work.
193
+
194
+ **R5 — Figma.** The Figma backend consumes the scene graph and emits node JSON for a plugin to create frames bound to Figma Variables by token id, reusing the Variable mapping in `sdk/plugins/figma`. It requires no Figma-specific data in recipes.
195
+
196
+ ## Open questions
197
+
198
+ * **Structured relationships.** Newer relationships already carry a `part` field and structured `options` (for example `state`, `size`, `scale`). Recipes should prefer them over `context` strings as more components adopt them; action-button's relationships still use labels only.
199
+
200
+ * **Deprecated tokens.** The pilot's icon sizes bind to `workflow-icon-size-*` tokens that validation (SPEC-058) reports as deprecated in 14.5.0 with no `replacedBy`. The recipe follows the relationships until a replacement is mapped.
201
+
202
+ * **Switch.** The switch recipe covers default, hover, down, keyboard-focus, and disabled, across selected and emphasized. Its track and handle colors come from relationships added for it, taken from the Figma token spec. They await design sign-off.
203
+
204
+ * **Pilot coverage.** The action-button recipe covers default, hover, down, focus, disabled, and static black/white. Quiet, selected, and emphasized fills are not yet authored.
205
+
206
+ * Should render requests (presets, matrices) be stored in the dataset for the Hub site, or stay tool input?
207
+
208
+ * Which gaps in component-scoped tokens block recipes beyond the pilot? action-button binds only through relationships; many components have none.
209
+
210
+ * Does `visibleWhen` need boolean composition (any/not), or are single conditions enough?
package/spec/index.md CHANGED
@@ -25,10 +25,12 @@ The specification defines:
25
25
  11. **Document blocks** — typed prose blocks attachable to tokens, components, and anatomy parts; makes design guidance machine-readable and agent-queryable ([Document blocks](document-blocks.md)).
26
26
  12. **Agent-readable surface** — operations and transport contracts (CLI, MCP server, Agent Skill) for AI agents consuming spec-conformant design data; covers session primer, token resolution, validation, query, and component description ([Agent-readable surface](agent-surface.md)).
27
27
  13. **Evolution** — deprecation lifecycle, migration windows, change classification, and legacy format contract ([Evolution](evolution.md)).
28
+ * **Artifact moves** — promoting and demoting one artifact between a platform and the foundation ([Artifact moves](artifact-moves.md)).
28
29
  14. **Dataset layout** — normative directory structure of a dataset (`tokens/`, `components/`, `fields/`, `mode-sets/`, `registry/`, `guidelines/`), discovery algorithm, and optional root descriptor ([Dataset layout](dataset-layout.md)).
29
30
  15. **Guideline documents** — standalone JSON files representing non-component design guidance pages (color, typography, motion, spacing, principles, etc.); each wraps page metadata and a `documentBlocks` body ([Guideline format](guideline-format.md)).
30
31
  16. **Authoring workflow** — normative contract for authoring design data via tooling: authoritative source location, lifecycle operations (create / edit / deprecate / rename / alias-rewire / mode-set management), taxonomy-aware decomposition requirement, and output-generation obligations ([Authoring workflow](authoring-workflow.md)).
31
32
  17. **Relationship format** — Component/Token Relationship (CTR): the anonymous-token entity unifying `tokenBindings` and token name-object scope fields, explicit relationship context, options validation, and interim legacy-compatibility fields ([Relationship format](relationship-format.md)).
33
+ 18. **Component visuals** — renderer-neutral visual recipes (part tree, layout, token-bound paint) that drive SVG, PNG, and Figma output ([Component visuals](component-visuals.md); draft RFC).
32
34
 
33
35
  This specification intentionally defines the Design Data-owned contract and links to external standards for platform-native implementation semantics instead of restating those contracts as normative local rules. The repository owns taxonomy, validation, lifecycle, and schema rules; the authoritative source for native format behavior remains the external standard it describes.
34
36
 
@@ -70,6 +72,7 @@ Full governance (compatibility tiers, migration, CLI `--spec-version`) is discus
70
72
  | [Taxonomy](taxonomy.md) | Concept categories, vocabulary, formatting, anatomy vs objects. |
71
73
  | [Registry](registry.md) | Named value collections for vocabulary validation; three-registry boundary (anatomy, token objects, categories) and packaging. |
72
74
  | [Component format](component-format.md) | Component declaration: options, slots, anatomy (→ anatomy-format.md), states (→ state-model.md), lifecycle. |
75
+ | [Option constraints](option-constraints.md) | If-then rules restricting valid option combinations on a component. |
73
76
  | [Anatomy format](anatomy-format.md) | Anatomy part declarations: field constraints, canonical vocabulary, SPEC-020/SPEC-023/SPEC-024/SPEC-025. |
74
77
  | [State model](state-model.md) | State declarations: trigger semantics, precedence algorithm, canonical vocabulary, SPEC-022/SPEC-026. |
75
78
  | [Cascade](cascade.md) | Layers, specificity, resolution algorithm. |
@@ -83,10 +86,12 @@ Full governance (compatibility tiers, migration, CLI `--spec-version`) is discus
83
86
  | [Document blocks](document-blocks.md) | Typed prose blocks (purpose, guideline, accessibility, do-dont, examples) attachable to any entity. |
84
87
  | [Agent-readable surface](agent-surface.md) | Transport contracts (CLI, MCP, Agent Skill) and operation catalog for AI agents consuming spec-conformant design data. |
85
88
  | [Evolution](evolution.md) | Deprecation lifecycle, migration windows, change classification. |
89
+ | [Artifact moves](artifact-moves.md) | Promote and demote one artifact between a platform manifest and the foundation: criteria, steps, notice period, provenance. |
86
90
  | [Dataset layout](dataset-layout.md) | Normative dataset directory structure, discovery algorithm, optional root descriptor, and SPEC-044 structural pre-check. |
87
91
  | [Guideline format](guideline-format.md) | Standalone guideline document shape: metadata fields, `category` enum, `related` cross-references, and required `documentBlocks` body (SPEC-045/046). |
88
92
  | [Authoring workflow](authoring-workflow.md) | Normative authoring contract: authoritative source location, lifecycle operations, taxonomy-aware decomposition, and output-generation obligations. |
89
93
  | [Relationship format](relationship-format.md) | Component/Token Relationship (CTR): shape, explicit relationship context, options validation, and interim legacy-compatibility fields (SPEC-051–057). |
94
+ | [Component visuals](component-visuals.md) | Draft RFC: visual recipe shape, value sources and conditions, scene graph, and renderer decisions. |
90
95
 
91
96
  ## JSON Schema `$id` and versioning
92
97