@adobe/design-data-spec 3.0.0 → 3.2.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/conformance/diff/cross-format/expected.json +2 -2
- package/conformance/diff/deprecated-new-token/new/tokens.tokens.json +1 -1
- package/conformance/diff/matched-gaining-deprecated/expected.json +1 -1
- package/conformance/diff/matched-gaining-deprecated/new/tokens.tokens.json +1 -1
- package/conformance/diff/replaced-by-pairing/expected.json +8 -8
- package/conformance/diff/replaced-by-pairing/old/tokens.tokens.json +5 -3
- package/conformance/diff/reverted-token/old/tokens.tokens.json +1 -1
- package/conformance/generation/ctr-legacy-key/expected/button.json +8 -0
- package/conformance/generation/ctr-legacy-key/expected/tokens.json +7 -0
- package/conformance/generation/ctr-legacy-key/input/tokens.tokens.json +8 -0
- package/conformance/generation/ctr-legacy-key/relationships/button.json +14 -0
- package/conformance/generation/deprecated-token/input/tokens.tokens.json +5 -3
- package/conformance/generation/mode-set-edit/input/tokens.tokens.json +8 -4
- package/conformance/generation/renamed-token/input/tokens.tokens.json +4 -2
- package/conformance/invalid/SPEC-010/dataset.json +4 -2
- package/conformance/invalid/SPEC-010/expected-errors.json +1 -1
- package/conformance/invalid/SPEC-010/tokens.tokens.json +4 -2
- package/conformance/invalid/SPEC-011/dataset.json +7 -5
- package/conformance/invalid/SPEC-011/expected-errors.json +1 -1
- package/conformance/invalid/SPEC-011/tokens.tokens.json +7 -5
- package/conformance/invalid/SPEC-012/dataset.json +1 -1
- package/conformance/invalid/SPEC-012/expected-errors.json +1 -1
- package/conformance/invalid/SPEC-012/tokens.tokens.json +1 -1
- package/conformance/invalid/SPEC-013/dataset.json +1 -1
- package/conformance/invalid/SPEC-013/tokens.tokens.json +1 -1
- package/conformance/invalid/SPEC-014/dataset.json +1 -1
- package/conformance/invalid/SPEC-014/tokens.tokens.json +1 -1
- package/conformance/invalid/SPEC-036/dataset.json +1 -1
- package/conformance/invalid/SPEC-037/dataset.json +3 -3
- package/conformance/invalid/SPEC-050/dataset.json +11 -0
- package/conformance/invalid/SPEC-050/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-051/dataset.json +16 -0
- package/conformance/invalid/SPEC-051/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-052/dataset.json +24 -0
- package/conformance/invalid/SPEC-052/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-053/dataset.json +26 -0
- package/conformance/invalid/SPEC-053/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-054/dataset.json +24 -0
- package/conformance/invalid/SPEC-054/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-055/dataset.json +16 -0
- package/conformance/invalid/SPEC-055/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-056/dataset.json +22 -0
- package/conformance/invalid/SPEC-056/expected-errors.json +10 -0
- package/conformance/invalid/SPEC-057/dataset.json +21 -0
- package/conformance/invalid/SPEC-057/expected-errors.json +10 -0
- package/conformance/valid/SPEC-014/dataset.json +1 -1
- package/conformance/valid/SPEC-036/dataset.json +2 -2
- package/conformance/valid/SPEC-037/dataset.json +6 -6
- package/conformance/valid/SPEC-051/dataset.json +42 -0
- package/conformance/valid/lifecycle-with-last-modified.json +1 -1
- package/package.json +1 -1
- package/rules/rules.yaml +114 -2
- package/schemas/component.schema.json +4 -4
- package/schemas/relationship.schema.json +228 -0
- package/schemas/token.schema.json +38 -48
- package/spec/authoring-workflow.md +19 -19
- package/spec/evolution.md +20 -20
- package/spec/index.md +2 -0
- package/spec/relationship-format.md +103 -0
- package/spec/token-format.md +39 -27
|
@@ -114,6 +114,40 @@
|
|
|
114
114
|
"type": "string"
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
|
+
"lifecycle": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"description": "Version lifecycle metadata, mirroring the component lifecycle pattern (see component.schema.json).",
|
|
120
|
+
"properties": {
|
|
121
|
+
"introduced": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Spec version when this token was added (e.g. \"1.0.0-draft\")."
|
|
124
|
+
},
|
|
125
|
+
"deprecatedIn": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Spec version when the token was deprecated (e.g. \"3.2.0\"). Truthy = deprecated."
|
|
128
|
+
},
|
|
129
|
+
"deprecatedComment": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Human-readable deprecation explanation and migration guidance."
|
|
132
|
+
},
|
|
133
|
+
"replacedBy": {
|
|
134
|
+
"oneOf": [
|
|
135
|
+
{ "type": "string", "format": "uuid" },
|
|
136
|
+
{
|
|
137
|
+
"type": "array",
|
|
138
|
+
"items": { "type": "string", "format": "uuid" },
|
|
139
|
+
"minItems": 1
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"description": "UUID(s) of the replacement token(s). Array form requires deprecatedComment."
|
|
143
|
+
},
|
|
144
|
+
"plannedRemoval": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Spec version when token will be removed."
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"additionalProperties": false
|
|
150
|
+
},
|
|
117
151
|
"tokenWithValue": {
|
|
118
152
|
"type": "object",
|
|
119
153
|
"required": ["name", "value"],
|
|
@@ -166,30 +200,8 @@
|
|
|
166
200
|
"type": "string",
|
|
167
201
|
"format": "uuid"
|
|
168
202
|
},
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
},
|
|
172
|
-
"deprecated": {
|
|
173
|
-
"type": "string",
|
|
174
|
-
"description": "Spec version when token was deprecated (e.g. \"3.2.0\"). Truthy = deprecated."
|
|
175
|
-
},
|
|
176
|
-
"deprecated_comment": {
|
|
177
|
-
"type": "string"
|
|
178
|
-
},
|
|
179
|
-
"replaced_by": {
|
|
180
|
-
"oneOf": [
|
|
181
|
-
{ "type": "string", "format": "uuid" },
|
|
182
|
-
{
|
|
183
|
-
"type": "array",
|
|
184
|
-
"items": { "type": "string", "format": "uuid" },
|
|
185
|
-
"minItems": 1
|
|
186
|
-
}
|
|
187
|
-
],
|
|
188
|
-
"description": "UUID(s) of the replacement token(s). Array form requires deprecated_comment."
|
|
189
|
-
},
|
|
190
|
-
"plannedRemoval": {
|
|
191
|
-
"type": "string",
|
|
192
|
-
"description": "Spec version when token will be removed."
|
|
203
|
+
"lifecycle": {
|
|
204
|
+
"$ref": "#/$defs/lifecycle"
|
|
193
205
|
},
|
|
194
206
|
"private": {
|
|
195
207
|
"type": "boolean"
|
|
@@ -250,30 +262,8 @@
|
|
|
250
262
|
"type": "string",
|
|
251
263
|
"format": "uuid"
|
|
252
264
|
},
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
},
|
|
256
|
-
"deprecated": {
|
|
257
|
-
"type": "string",
|
|
258
|
-
"description": "Spec version when token was deprecated (e.g. \"3.2.0\"). Truthy = deprecated."
|
|
259
|
-
},
|
|
260
|
-
"deprecated_comment": {
|
|
261
|
-
"type": "string"
|
|
262
|
-
},
|
|
263
|
-
"replaced_by": {
|
|
264
|
-
"oneOf": [
|
|
265
|
-
{ "type": "string", "format": "uuid" },
|
|
266
|
-
{
|
|
267
|
-
"type": "array",
|
|
268
|
-
"items": { "type": "string", "format": "uuid" },
|
|
269
|
-
"minItems": 1
|
|
270
|
-
}
|
|
271
|
-
],
|
|
272
|
-
"description": "UUID(s) of the replacement token(s). Array form requires deprecated_comment."
|
|
273
|
-
},
|
|
274
|
-
"plannedRemoval": {
|
|
275
|
-
"type": "string",
|
|
276
|
-
"description": "Spec version when token will be removed."
|
|
265
|
+
"lifecycle": {
|
|
266
|
+
"$ref": "#/$defs/lifecycle"
|
|
277
267
|
},
|
|
278
268
|
"private": {
|
|
279
269
|
"type": "boolean"
|
|
@@ -56,19 +56,19 @@ The following table defines the **target token-authoring contract**: the complet
|
|
|
56
56
|
|
|
57
57
|
### Token lifecycle operations
|
|
58
58
|
|
|
59
|
-
| Operation | Description
|
|
60
|
-
| ----------------------- |
|
|
61
|
-
| **create** | Introduce a new token to the dataset. Assign a fresh UUID. Populate name object, value or alias `$ref`, `introduced` version.
|
|
62
|
-
| **edit** | Update a token's value, alias target, rationale, or name-object fields. Preserve UUID.
|
|
63
|
-
| **deprecate** | Set `
|
|
64
|
-
| **rename** | Assign a new name object. Introduce a `
|
|
65
|
-
| **alias-rewire** | Change the `$ref` target UUID of an alias token. Verify the new target resolves in the cascade.
|
|
66
|
-
| **mode-set management** | Add, rename, or remove a mode-set entry. Update affected `mode` fields across tokens.
|
|
67
|
-
| **remove** | Delete a token that has passed its migration window. Verify no `$ref` in the dataset resolves to the removed UUID.
|
|
59
|
+
| Operation | Description | Required fields |
|
|
60
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
61
|
+
| **create** | Introduce a new token to the dataset. Assign a fresh UUID. Populate name object, value or alias `$ref`, `lifecycle.introduced` version. | `uuid`, `name`, `value` or `$ref` |
|
|
62
|
+
| **edit** | Update a token's value, alias target, rationale, or name-object fields. Preserve UUID. | `uuid` (for identity lookup) |
|
|
63
|
+
| **deprecate** | Set `lifecycle.deprecatedIn` to the current spec version string; optionally set `lifecycle.replacedBy` and `lifecycle.deprecatedComment`. | `lifecycle.deprecatedIn`, `uuid` |
|
|
64
|
+
| **rename** | Assign a new name object. Introduce a `lifecycle.replacedBy` pointer on the old token (or retire it). Preserve UUID on the renamed token. | Name-object fields, `lifecycle.replacedBy` |
|
|
65
|
+
| **alias-rewire** | Change the `$ref` target UUID of an alias token. Verify the new target resolves in the cascade. | `$ref` |
|
|
66
|
+
| **mode-set management** | Add, rename, or remove a mode-set entry. Update affected `mode` fields across tokens. | Mode-set file + affected token entries |
|
|
67
|
+
| **remove** | Delete a token that has passed its migration window. Verify no `$ref` in the dataset resolves to the removed UUID. | n/a |
|
|
68
68
|
|
|
69
|
-
**NORMATIVE:** A conforming authoring tool MUST assign UUIDs at creation time and MUST NOT change the UUID of an existing token during any edit, deprecate, rename, or alias-rewire operation. UUID stability is the identity contract that allows `$ref`, `
|
|
69
|
+
**NORMATIVE:** A conforming authoring tool MUST assign UUIDs at creation time and MUST NOT change the UUID of an existing token during any edit, deprecate, rename, or alias-rewire operation. UUID stability is the identity contract that allows `$ref`, `lifecycle.replacedBy`, and external consumers to reference tokens across versions.
|
|
70
70
|
|
|
71
|
-
**NORMATIVE:** A conforming authoring tool MUST write a valid `introduced` version on create, using the declared spec version of the active dataset (`specVersion` field in `dataset.json` or `.design-data.toml`).
|
|
71
|
+
**NORMATIVE:** A conforming authoring tool MUST write a valid `lifecycle.introduced` version on create, using the declared spec version of the active dataset (`specVersion` field in `dataset.json` or `.design-data.toml`).
|
|
72
72
|
|
|
73
73
|
**NORMATIVE:** A conforming authoring tool MUST validate the written dataset against Layer 1 (JSON Schema) and Layer 2 (semantic rules) before persisting the write. A write operation that produces a Layer 1 error MUST be rejected.
|
|
74
74
|
|
|
@@ -95,13 +95,13 @@ Tokens are authored in `tokens/**/*.tokens.json`. Files may be nested arbitraril
|
|
|
95
95
|
|
|
96
96
|
**Required at creation:** A conforming authoring tool MUST write:
|
|
97
97
|
|
|
98
|
-
| Field
|
|
99
|
-
|
|
|
100
|
-
| `name`
|
|
101
|
-
| `uuid`
|
|
102
|
-
| `value` or `$ref`
|
|
103
|
-
| `$schema`
|
|
104
|
-
| `introduced`
|
|
98
|
+
| Field | Requirement | Notes |
|
|
99
|
+
| ---------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `name` | REQUIRED | Name object with at least `property`; MUST be decomposed via the `fields/` catalog — see [Taxonomy-aware authoring](#taxonomy-aware-authoring) |
|
|
101
|
+
| `uuid` | REQUIRED | Fresh UUID v4 assigned by the tool; MUST NOT change for the lifetime of the token |
|
|
102
|
+
| `value` or `$ref` | REQUIRED (exactly one) | Literal value or alias reference to another token's UUID |
|
|
103
|
+
| `$schema` | RECOMMENDED | Token-type schema URI (e.g. `color.json`, `typography.json`) from `packages/design-data-spec/schemas/` |
|
|
104
|
+
| `lifecycle.introduced` | RECOMMENDED | Spec version string of the active dataset at creation time |
|
|
105
105
|
|
|
106
106
|
**Validation gate:** SPEC-001–017 (name, value, lifecycle, tech-debt), SPEC-041 (mode-set
|
|
107
107
|
conformance), SPEC-042 (field-scope), SPEC-043 (domain-required-fields).
|
|
@@ -261,7 +261,7 @@ authoring is Phase C scope; vocabulary additions today follow the manual process
|
|
|
261
261
|
|
|
262
262
|
1. Write all authored artifacts to the dataset root's registered directories (not to legacy output files).
|
|
263
263
|
2. Assign UUIDs at creation time and preserve them across all subsequent operations.
|
|
264
|
-
3. Write a valid `introduced` version on all created artifacts.
|
|
264
|
+
3. Write a valid `lifecycle.introduced` version on all created artifacts.
|
|
265
265
|
4. Validate written artifacts against Layer 1 (JSON Schema) and Layer 2 (semantic rules) before persisting.
|
|
266
266
|
5. Support structured name-object field decomposition using the `fields/` catalog for token creation.
|
|
267
267
|
|
package/spec/evolution.md
CHANGED
|
@@ -12,36 +12,36 @@ Tokens progress through the following lifecycle stages:
|
|
|
12
12
|
introduced → active → deprecated → planned removal → removed
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
| Stage | Token state
|
|
16
|
-
| ------------------- |
|
|
17
|
-
| **Introduced** | Token first appears in the dataset. `introduced` field records the version.
|
|
18
|
-
| **Active** | Token is current and recommended for use. No `
|
|
19
|
-
| **Deprecated** | Token is no longer recommended. `
|
|
20
|
-
| **Planned removal** | `plannedRemoval` records the target version. The token remains in the dataset but consumers should complete migration.
|
|
21
|
-
| **Removed** | Token is deleted from the dataset. Consumers that still reference it will break.
|
|
15
|
+
| Stage | Token state |
|
|
16
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| **Introduced** | Token first appears in the dataset. `lifecycle.introduced` field records the version. |
|
|
18
|
+
| **Active** | Token is current and recommended for use. No `lifecycle` field (or no `deprecatedIn` within it). |
|
|
19
|
+
| **Deprecated** | Token is no longer recommended. `lifecycle.deprecatedIn` records the version. Consumers receive warnings. `lifecycle.replacedBy` points to the successor token(s) when a direct replacement exists. |
|
|
20
|
+
| **Planned removal** | `lifecycle.plannedRemoval` records the target version. The token remains in the dataset but consumers should complete migration. |
|
|
21
|
+
| **Removed** | Token is deleted from the dataset. Consumers that still reference it will break. |
|
|
22
22
|
|
|
23
23
|
### Lifecycle fields
|
|
24
24
|
|
|
25
25
|
See [Token format — Lifecycle and metadata](token-format.md#lifecycle-and-metadata) for the full field definitions and normative rules.
|
|
26
26
|
|
|
27
|
-
### What `
|
|
27
|
+
### What `lifecycle.replacedBy` guarantees
|
|
28
28
|
|
|
29
|
-
When a token carries `
|
|
29
|
+
When a token carries `lifecycle.replacedBy`:
|
|
30
30
|
|
|
31
31
|
* Each target UUID **MUST** resolve to an existing token in the dataset (rule `SPEC-010`).
|
|
32
32
|
* The target token **SHOULD NOT** itself be deprecated. Chains of replacements (A replaced by B, B replaced by C) are discouraged; authors should point directly to the final replacement.
|
|
33
33
|
* For a single UUID (1:1 replacement), consumers can mechanically rewrite references.
|
|
34
|
-
* For an array of UUIDs (one-to-many split), the `
|
|
34
|
+
* For an array of UUIDs (one-to-many split), the `lifecycle.deprecatedComment` **MUST** explain which replacement applies in which context.
|
|
35
35
|
|
|
36
36
|
## Migration windows
|
|
37
37
|
|
|
38
|
-
**NORMATIVE:** A deprecated token **MUST** remain in the published dataset for at least **two minor versions** after the version recorded in `
|
|
38
|
+
**NORMATIVE:** A deprecated token **MUST** remain in the published dataset for at least **two minor versions** after the version recorded in `lifecycle.deprecatedIn` before it may be removed.
|
|
39
39
|
|
|
40
40
|
**EXAMPLE:** A token deprecated in `3.2.0` may not be removed until `3.4.0` at the earliest. Removal in a major version (e.g. `4.0.0`) is always permitted regardless of how recently the token was deprecated.
|
|
41
41
|
|
|
42
42
|
**RATIONALE:** Two minor versions gives consumers at least two release cycles to migrate. The major-version escape hatch allows accumulated deprecations to be cleaned up in a coordinated breaking release.
|
|
43
43
|
|
|
44
|
-
If `plannedRemoval` is set, it overrides the default window — the token will be removed in the specified version (which must not precede the `
|
|
44
|
+
If `lifecycle.plannedRemoval` is set, it overrides the default window — the token will be removed in the specified version (which must not precede the `deprecatedIn` version).
|
|
45
45
|
|
|
46
46
|
## Change classification
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ The specification follows [Semantic Versioning](https://semver.org/) for its pub
|
|
|
52
52
|
* New tokens added to the dataset
|
|
53
53
|
* New optional fields added to token schema
|
|
54
54
|
* New validation rules added to the rule catalog
|
|
55
|
-
* Tokens deprecated (with `
|
|
55
|
+
* Tokens deprecated (with `lifecycle.deprecatedIn` field)
|
|
56
56
|
* Rule severity relaxed (error → warning)
|
|
57
57
|
* New enum values added to registries
|
|
58
58
|
|
|
@@ -79,13 +79,13 @@ The `@adobe/spectrum-tokens` package continues to publish tokens in the **legacy
|
|
|
79
79
|
|
|
80
80
|
### Lifecycle field mapping
|
|
81
81
|
|
|
82
|
-
| Cascade format
|
|
83
|
-
|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `introduced`
|
|
87
|
-
| `plannedRemoval`
|
|
88
|
-
| `
|
|
82
|
+
| Cascade format | Legacy format |
|
|
83
|
+
| -------------------------------------------------- | ------------------------------------- |
|
|
84
|
+
| `lifecycle.deprecatedIn: "3.2.0"` (version string) | `deprecated: true` (boolean) |
|
|
85
|
+
| `lifecycle.replacedBy: "<uuid>"` | `renamed: "<target-token-name>"` |
|
|
86
|
+
| `lifecycle.introduced` | Not emitted |
|
|
87
|
+
| `lifecycle.plannedRemoval` | Not emitted |
|
|
88
|
+
| `lifecycle.deprecatedComment` | `deprecated_comment` (passed through) |
|
|
89
89
|
|
|
90
90
|
### Output-generator determinism
|
|
91
91
|
|
package/spec/index.md
CHANGED
|
@@ -28,6 +28,7 @@ The specification defines:
|
|
|
28
28
|
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
29
|
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
30
|
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
|
+
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)).
|
|
31
32
|
|
|
32
33
|
## Conformance
|
|
33
34
|
|
|
@@ -83,6 +84,7 @@ Full governance (compatibility tiers, migration, CLI `--spec-version`) is discus
|
|
|
83
84
|
| [Dataset layout](dataset-layout.md) | Normative dataset directory structure, discovery algorithm, optional root descriptor, and SPEC-044 structural pre-check. |
|
|
84
85
|
| [Guideline format](guideline-format.md) | Standalone guideline document shape: metadata fields, `category` enum, `related` cross-references, and required `documentBlocks` body (SPEC-045/046). |
|
|
85
86
|
| [Authoring workflow](authoring-workflow.md) | Normative authoring contract: authoritative source location, lifecycle operations, taxonomy-aware decomposition, and output-generation obligations. |
|
|
87
|
+
| [Relationship format](relationship-format.md) | Component/Token Relationship (CTR): shape, explicit relationship context, options validation, and interim legacy-compatibility fields (SPEC-051–057). |
|
|
86
88
|
|
|
87
89
|
## JSON Schema `$id` and versioning
|
|
88
90
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Relationship format (Component/Token Relationship — CTR)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A Component/Token Relationship (CTR) is an anonymous token that unifies the two mechanisms that today independently connect components and tokens:
|
|
6
|
+
|
|
7
|
+
1. **`tokenBindings`** on a component declaration (component-declares-usage direction; see [component-format.md#token-bindings](component-format.md#token-bindings)).
|
|
8
|
+
2. **Name-object scope fields** on a token (`component`, `variant`, `anatomy`, `state`, and arbitrary option keys; token-declares-scope direction, validated today by SPEC-018, SPEC-019, SPEC-020, SPEC-022, and SPEC-040).
|
|
9
|
+
|
|
10
|
+
Rather than encoding component/part/option context inside a token's `name` string, a CTR carries its value (or `$ref` alias) alongside an explicit `scope` object naming the component, part, property, and option scope it belongs to, plus an optional `context` string carrying a human-readable label (the same role `tokenBindings[].context` plays today — e.g. mapping to a Figma Token Group label). CTR entries live under `relationships/*.json`, each file a JSON array of CTR objects (see [`relationship.schema.json`](../schemas/relationship.schema.json)).
|
|
11
|
+
|
|
12
|
+
**NORMATIVE:** This document defines the CTR **foundation** — shape, validation rules, and interim legacy-compatibility fields. It does not migrate existing token or component data; `tokenBindings` and `componentBindings` remain in the schemas and are **NOT** removed by this chapter (deferred to a future migration phase).
|
|
13
|
+
|
|
14
|
+
## Shape
|
|
15
|
+
|
|
16
|
+
Each CTR is one of two subtypes, mirroring the token `oneOf` split (see [token-format.md](token-format.md)):
|
|
17
|
+
|
|
18
|
+
* **Value-owning CTR** (`ctrWithValue`) — carries a literal `value`, like a token.
|
|
19
|
+
* **Relationship-only CTR** (`ctrWithRef`) — carries a `$ref` alias to another token or CTR `uuid`, instead of an owned value.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
"scope": {
|
|
25
|
+
"component": "button",
|
|
26
|
+
"part": "label",
|
|
27
|
+
"property": "color",
|
|
28
|
+
"options": { "variant": "accent", "state": ["hover"] }
|
|
29
|
+
},
|
|
30
|
+
"context": "Label color, accent variant, hover",
|
|
31
|
+
"value": "#0060e0",
|
|
32
|
+
"uuid": "5b1a8e2e-9e2f-4b7a-9b2e-1a2b3c4d5e6f"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"scope": {
|
|
36
|
+
"component": "checkbox",
|
|
37
|
+
"property": "corner-radius"
|
|
38
|
+
},
|
|
39
|
+
"$ref": "5b1a8e2e-9e2f-4b7a-9b2e-1a2b3c4d5e6f"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Scope fields
|
|
45
|
+
|
|
46
|
+
| Field | Required | Type | Description |
|
|
47
|
+
| ----------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| `scope.component` | yes | string | Component name (kebab-case). **MUST** match the `name` of a declared component in the dataset (rule SPEC-051, mirrors SPEC-018). |
|
|
49
|
+
| `scope.part` | no | string | Anatomy part name. **MUST** match a declared anatomy part on the referenced component when present (rule SPEC-052, mirrors SPEC-020). |
|
|
50
|
+
| `scope.property` | yes | string | The stylistic attribute this relationship describes (e.g. `color`, `width`, `padding`). |
|
|
51
|
+
| `scope.options` | no | object | Option-scoped narrowing. Each `options.<key>` value **MUST** validate against the referenced component's `options.<key>.values` (rule SPEC-053 — the headline CTR rule, stricter than the token-side SPEC-040 warning). `options.state` is the exception: an ordered array of atomic state ids, validated against the component's declared states (rule SPEC-054, mirrors SPEC-022). |
|
|
52
|
+
| `context` | no | string | Human-readable label for how this CTR is used (mirrors `tokenBindings[].context`; maps to a Figma Token Group label). Purely descriptive — not validated. |
|
|
53
|
+
|
|
54
|
+
Layer 1 (`relationship.schema.json`) only enforces shape: `scope.options` is an object, `scope.options.state` (when present) is an array of strings, and other option values are typically strings. It does **not** enforce which option values are valid — that is a Layer 2 (Rust, `sdk/core`) relational rule, because it requires cross-referencing the component catalog.
|
|
55
|
+
|
|
56
|
+
## Value-owning vs. relationship-only
|
|
57
|
+
|
|
58
|
+
| Field | Subtype | Description |
|
|
59
|
+
| ------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
60
|
+
| `value` | `ctrWithValue` | Literal value owned by this CTR, same value-type narrowing as a token's `value` (`$valueType`). |
|
|
61
|
+
| `$ref` | `ctrWithRef` | Alias target. **MUST** resolve to a token or CTR `uuid` in the dataset (rule SPEC-055, mirrors SPEC-001/SPEC-027). |
|
|
62
|
+
|
|
63
|
+
A CTR **MUST NOT** declare both `value` and `$ref` (enforced at Layer 1 via `not: { required: [...] }`, mirroring `token.schema.json`).
|
|
64
|
+
|
|
65
|
+
## Identity
|
|
66
|
+
|
|
67
|
+
| Field | Required | Type | Description |
|
|
68
|
+
| ------ | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `uuid` | no | string (uuid) | **MUST** be globally unique across all relationship entries within the same cascade layer (rule SPEC-056, mirrors SPEC-004). Required for a CTR to be a valid `$ref` target. |
|
|
70
|
+
|
|
71
|
+
## Legacy fields (interim)
|
|
72
|
+
|
|
73
|
+
To keep `packages/tokens/src` (the legacy generated output, produced by `design-data migrate legacy-output` / `sdk/core/src/legacy.rs`) byte-identical while this foundation lands, a CTR that has a legacy counterpart carries transitional fields so the existing legacy generator can still reproduce that output:
|
|
74
|
+
|
|
75
|
+
| Field | Required | Type | Description |
|
|
76
|
+
| ----------- | -------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| `legacyKey` | no | string | Pins the exact flat key written to legacy output, mirroring the token name-object `legacyKey` escape hatch. |
|
|
78
|
+
| `setUuid` | no | string (uuid) | Identifies the legacy color-set entry this CTR belongs to. Only meaningful alongside `legacyKey`. |
|
|
79
|
+
| `setSchema` | no | string | Names the legacy color-set schema this CTR's set entry conforms to. Only meaningful alongside `setUuid`. |
|
|
80
|
+
|
|
81
|
+
**NORMATIVE:** A CTR that has no legacy counterpart **SHOULD NOT** carry `legacyKey`/`setUuid`/`setSchema` — these fields exist solely to bridge the interim period before the legacy generator is retired or rewritten to consume CTRs directly. Rule SPEC-057 (`ctr-legacykey-present-if-legacy`, warning) is an advisory placeholder documenting this intent; it does not attempt to detect "should have a legacy counterpart" automatically, since that determination belongs to the (future) generator-side migration work, not this foundation.
|
|
82
|
+
|
|
83
|
+
These fields are transitional. Once the legacy generator (or its replacement) reads CTRs directly, `legacyKey`/`setUuid`/`setSchema` are expected to be removed in a later spec version.
|
|
84
|
+
|
|
85
|
+
## Relationship to `tokenBindings` / `componentBindings`
|
|
86
|
+
|
|
87
|
+
CTRs are designed to eventually supersede both `tokenBindings` (on components, see [component-format.md#token-bindings](component-format.md#token-bindings)) and the token-side `componentBindings` reverse index (see [token-format.md#component-bindings](token-format.md#component-bindings)), by combining both directions — and the token name-object scope fields — into a single first-class relationship record with an explicit `context`.
|
|
88
|
+
|
|
89
|
+
**NORMATIVE:** This chapter does **NOT** remove or deprecate `tokenBindings` or `componentBindings`. Both remain valid and required as-is. Migrating existing data onto CTRs, and any eventual removal of the superseded mechanisms, is out of scope for this foundation and deferred to a separate migration phase.
|
|
90
|
+
|
|
91
|
+
## SPEC rules
|
|
92
|
+
|
|
93
|
+
The following rules are added to the Layer 2 rule catalog (`rules/rules.yaml`) by this chapter. New CTR rules start at SPEC-051 to avoid collision with the existing catalog (SPEC-001–SPEC-050).
|
|
94
|
+
|
|
95
|
+
| Rule ID | Name | Severity | Assert |
|
|
96
|
+
| -------- | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| SPEC-051 | `ctr-component-exists` | error | CTR `scope.component` value **MUST** match the `name` of a declared component in the dataset. |
|
|
98
|
+
| SPEC-052 | `ctr-part-valid` | error | CTR `scope.part` value **MUST** match the `name` of a declared anatomy part on the referenced component (when present). |
|
|
99
|
+
| SPEC-053 | `ctr-option-valid` | error | CTR `scope.options.<key>` value **MUST** match a value in the declared `options.<key>.values` list for the referenced component (when that list exists). |
|
|
100
|
+
| SPEC-054 | `ctr-state-valid` | error | CTR `scope.options.state` array elements **MUST** match the `name` of a declared state on the referenced component (when state declarations are present). |
|
|
101
|
+
| SPEC-055 | `ctr-ref-resolves` | error | CTR `$ref` value **MUST** resolve to a declared token or relationship `uuid` in the dataset. |
|
|
102
|
+
| SPEC-056 | `ctr-uuid-unique` | error | CTR `uuid` values **MUST** be unique across all relationship entries within the same cascade layer. |
|
|
103
|
+
| SPEC-057 | `ctr-legacykey-present-if-legacy` | warning | Advisory placeholder: a CTR that round-trips through the legacy generator **SHOULD** carry `legacyKey`. Generator-side "should have a legacy counterpart" detection is deferred; this rule documents intent rather than enforcing it structurally. |
|
package/spec/token-format.md
CHANGED
|
@@ -134,18 +134,25 @@ When **`value`** is present, it **MUST** conform to the declared value type for
|
|
|
134
134
|
|
|
135
135
|
### Lifecycle and metadata
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
137
|
+
Version lifecycle fields are grouped under an OPTIONAL nested `lifecycle` object, mirroring the
|
|
138
|
+
component schema's `lifecycle` pattern (see [Component format](component-format.md)). This
|
|
139
|
+
implements the token lifecycle model described in
|
|
140
|
+
[#623](https://github.com/adobe/spectrum-design-data/discussions/623) and the evolution policy in
|
|
141
|
+
[Evolution](evolution.md). Inspired by Swift's `@available` attribute, Kotlin's `@Deprecated`, and
|
|
142
|
+
OpenAPI 3.3's deprecation model.
|
|
143
|
+
|
|
144
|
+
| Field | Type | Description |
|
|
145
|
+
| ----------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
146
|
+
| `uuid` | string (UUID) | Stable unique id for rename tracking and diffs. |
|
|
147
|
+
| `lifecycle.introduced` | string (version) | Spec version when the token was first added (e.g. `"1.0.0"`). |
|
|
148
|
+
| `lifecycle.deprecatedIn` | string (version) | Spec version when the token was deprecated (e.g. `"3.2.0"`). Truthy = deprecated. |
|
|
149
|
+
| `lifecycle.deprecatedComment` | string | Human-readable deprecation explanation and migration guidance. |
|
|
150
|
+
| `lifecycle.replacedBy` | string (UUID) or array of string (UUID) | UUID(s) of the replacement token(s). Single string for 1:1 replacement; array for one-to-many splits. |
|
|
151
|
+
| `lifecycle.plannedRemoval` | string (version) | Spec version when the token will be removed. If omitted, defaults to the next major version after `deprecatedIn`. |
|
|
152
|
+
| `private` | boolean | Not part of public API surface. |
|
|
153
|
+
| `rationale` | string | Why this token has its current value. Intended for product-layer authoring context; see [Product context](product-context.md). |
|
|
154
|
+
|
|
155
|
+
`private` and `rationale` stay flat (not version-lifecycle concepts).
|
|
149
156
|
|
|
150
157
|
#### Lifecycle example
|
|
151
158
|
|
|
@@ -154,38 +161,43 @@ The following OPTIONAL fields implement the token lifecycle model described in [
|
|
|
154
161
|
"name": { "component": "button", "object": "background", "property": "color", "variant": "primary" },
|
|
155
162
|
"value": "#0265dc",
|
|
156
163
|
"uuid": "aaaaaaaa-0001-4000-8000-000000000001",
|
|
157
|
-
"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
"lifecycle": {
|
|
165
|
+
"introduced": "1.0.0",
|
|
166
|
+
"deprecatedIn": "3.2.0",
|
|
167
|
+
"deprecatedComment": "Use action-background-default instead.",
|
|
168
|
+
"replacedBy": "bbbbbbbb-0002-4000-8000-000000000001",
|
|
169
|
+
"plannedRemoval": "4.0.0"
|
|
170
|
+
}
|
|
162
171
|
}
|
|
163
172
|
```
|
|
164
173
|
|
|
165
174
|
#### Normative rules
|
|
166
175
|
|
|
167
|
-
**NORMATIVE:** If `
|
|
176
|
+
**NORMATIVE:** If `lifecycle.deprecatedIn` is present, `lifecycle.deprecatedComment` **SHOULD** be present.
|
|
168
177
|
|
|
169
|
-
**NORMATIVE:** If `
|
|
178
|
+
**NORMATIVE:** If `lifecycle.replacedBy` is an array, `lifecycle.deprecatedComment` is **REQUIRED** — the comment **MUST** explain which replacement applies in which context.
|
|
170
179
|
|
|
171
|
-
**NORMATIVE:** If `
|
|
180
|
+
**NORMATIVE:** If `lifecycle.replacedBy` is present, `lifecycle.deprecatedIn` **MUST** be present.
|
|
172
181
|
|
|
173
|
-
**NORMATIVE:** If `plannedRemoval` is present, `
|
|
182
|
+
**NORMATIVE:** If `lifecycle.plannedRemoval` is present, `lifecycle.deprecatedIn` **MUST** be present, and the `plannedRemoval` version **MUST NOT** precede the `deprecatedIn` version.
|
|
174
183
|
|
|
175
|
-
**NORMATIVE:** Each UUID in `
|
|
184
|
+
**NORMATIVE:** Each UUID in `lifecycle.replacedBy` **MUST** resolve to an existing token in the dataset (see rule `SPEC-010`).
|
|
176
185
|
|
|
177
186
|
#### Legacy format mapping
|
|
178
187
|
|
|
179
188
|
When generating legacy-format output from cascade tokens:
|
|
180
189
|
|
|
181
|
-
* `
|
|
182
|
-
* `
|
|
183
|
-
* `
|
|
190
|
+
* `lifecycle.deprecatedIn: "3.2.0"` (or any truthy value) maps to flat `deprecated: true`
|
|
191
|
+
* `lifecycle.deprecatedComment` maps to flat `deprecated_comment` (copied verbatim)
|
|
192
|
+
* `lifecycle.replacedBy: "<uuid>"` maps to flat `renamed: "<target-token-name>"` (resolved via UUID lookup)
|
|
193
|
+
* `lifecycle.plannedRemoval` and `lifecycle.introduced` have no legacy equivalent and are not emitted
|
|
194
|
+
* The `lifecycle` wrapper itself is never emitted — legacy format has no nested wrapper
|
|
184
195
|
|
|
185
196
|
When migrating legacy-format tokens to cascade:
|
|
186
197
|
|
|
187
|
-
* `deprecated: true` maps to `
|
|
188
|
-
* `
|
|
198
|
+
* `deprecated: true` maps to `lifecycle.deprecatedIn: "unknown"` (authors should backfill the actual version)
|
|
199
|
+
* `deprecated_comment` maps to `lifecycle.deprecatedComment` (copied verbatim)
|
|
200
|
+
* `renamed: "<name>"` maps to `lifecycle.replacedBy: "<uuid>"` (resolved via name lookup across all files in the migrated input set). If the rename target is not found in the scanned corpus, the field is dropped — `lifecycle.replacedBy` must be set manually in that case
|
|
189
201
|
|
|
190
202
|
### `specVersion`
|
|
191
203
|
|