@adobe/design-data-spec 3.1.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.
Files changed (58) hide show
  1. package/conformance/diff/cross-format/expected.json +2 -2
  2. package/conformance/diff/deprecated-new-token/new/tokens.tokens.json +1 -1
  3. package/conformance/diff/matched-gaining-deprecated/expected.json +1 -1
  4. package/conformance/diff/matched-gaining-deprecated/new/tokens.tokens.json +1 -1
  5. package/conformance/diff/replaced-by-pairing/expected.json +8 -8
  6. package/conformance/diff/replaced-by-pairing/old/tokens.tokens.json +5 -3
  7. package/conformance/diff/reverted-token/old/tokens.tokens.json +1 -1
  8. package/conformance/generation/ctr-legacy-key/expected/button.json +8 -0
  9. package/conformance/generation/ctr-legacy-key/expected/tokens.json +7 -0
  10. package/conformance/generation/ctr-legacy-key/input/tokens.tokens.json +8 -0
  11. package/conformance/generation/ctr-legacy-key/relationships/button.json +14 -0
  12. package/conformance/generation/deprecated-token/input/tokens.tokens.json +5 -3
  13. package/conformance/generation/mode-set-edit/input/tokens.tokens.json +8 -4
  14. package/conformance/generation/renamed-token/input/tokens.tokens.json +4 -2
  15. package/conformance/invalid/SPEC-010/dataset.json +4 -2
  16. package/conformance/invalid/SPEC-010/expected-errors.json +1 -1
  17. package/conformance/invalid/SPEC-010/tokens.tokens.json +4 -2
  18. package/conformance/invalid/SPEC-011/dataset.json +7 -5
  19. package/conformance/invalid/SPEC-011/expected-errors.json +1 -1
  20. package/conformance/invalid/SPEC-011/tokens.tokens.json +7 -5
  21. package/conformance/invalid/SPEC-012/dataset.json +1 -1
  22. package/conformance/invalid/SPEC-012/expected-errors.json +1 -1
  23. package/conformance/invalid/SPEC-012/tokens.tokens.json +1 -1
  24. package/conformance/invalid/SPEC-013/dataset.json +1 -1
  25. package/conformance/invalid/SPEC-013/tokens.tokens.json +1 -1
  26. package/conformance/invalid/SPEC-014/dataset.json +1 -1
  27. package/conformance/invalid/SPEC-014/tokens.tokens.json +1 -1
  28. package/conformance/invalid/SPEC-036/dataset.json +1 -1
  29. package/conformance/invalid/SPEC-037/dataset.json +3 -3
  30. package/conformance/invalid/SPEC-051/dataset.json +16 -0
  31. package/conformance/invalid/SPEC-051/expected-errors.json +10 -0
  32. package/conformance/invalid/SPEC-052/dataset.json +24 -0
  33. package/conformance/invalid/SPEC-052/expected-errors.json +10 -0
  34. package/conformance/invalid/SPEC-053/dataset.json +26 -0
  35. package/conformance/invalid/SPEC-053/expected-errors.json +10 -0
  36. package/conformance/invalid/SPEC-054/dataset.json +24 -0
  37. package/conformance/invalid/SPEC-054/expected-errors.json +10 -0
  38. package/conformance/invalid/SPEC-055/dataset.json +16 -0
  39. package/conformance/invalid/SPEC-055/expected-errors.json +10 -0
  40. package/conformance/invalid/SPEC-056/dataset.json +22 -0
  41. package/conformance/invalid/SPEC-056/expected-errors.json +10 -0
  42. package/conformance/invalid/SPEC-057/dataset.json +21 -0
  43. package/conformance/invalid/SPEC-057/expected-errors.json +10 -0
  44. package/conformance/valid/SPEC-014/dataset.json +1 -1
  45. package/conformance/valid/SPEC-036/dataset.json +2 -2
  46. package/conformance/valid/SPEC-037/dataset.json +6 -6
  47. package/conformance/valid/SPEC-051/dataset.json +42 -0
  48. package/conformance/valid/lifecycle-with-last-modified.json +1 -1
  49. package/package.json +1 -1
  50. package/rules/rules.yaml +98 -2
  51. package/schemas/component.schema.json +4 -4
  52. package/schemas/relationship.schema.json +228 -0
  53. package/schemas/token.schema.json +38 -48
  54. package/spec/authoring-workflow.md +19 -19
  55. package/spec/evolution.md +20 -20
  56. package/spec/index.md +2 -0
  57. package/spec/relationship-format.md +103 -0
  58. package/spec/token-format.md +39 -27
@@ -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 | Required fields |
60
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
61
- | **create** | Introduce a new token to the dataset. Assign a fresh UUID. Populate name object, value or alias `$ref`, `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 `deprecated` to the current spec version string; optionally set `replaced_by` and `deprecated_comment`. | `deprecated`, `uuid` |
64
- | **rename** | Assign a new name object. Introduce a `replaced_by` pointer on the old token (or retire it). Preserve UUID on the renamed token. | Name-object fields, `replaced_by` |
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 |
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`, `replaced_by`, and external consumers to reference tokens across versions.
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 | 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
- | `introduced` | RECOMMENDED | Spec version string of the active dataset at creation time |
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 `deprecated` field. |
19
- | **Deprecated** | Token is no longer recommended. `deprecated` records the version. Consumers receive warnings. `replaced_by` points to the successor token(s) when a direct replacement exists. |
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 `replaced_by` guarantees
27
+ ### What `lifecycle.replacedBy` guarantees
28
28
 
29
- When a token carries `replaced_by`:
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 `deprecated_comment` **MUST** explain which replacement applies in which context.
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 `deprecated` before it may be removed.
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 `deprecated` version).
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 `deprecated` field)
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 | Legacy format |
83
- | -------------------------------------- | ------------------------------------- |
84
- | `deprecated: "3.2.0"` (version string) | `deprecated: true` (boolean) |
85
- | `replaced_by: "<uuid>"` | `renamed: "<target-token-name>"` |
86
- | `introduced` | Not emitted |
87
- | `plannedRemoval` | Not emitted |
88
- | `deprecated_comment` | `deprecated_comment` (passed through) |
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. |
@@ -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
- The following OPTIONAL fields implement the token lifecycle model described in [#623](https://github.com/adobe/spectrum-design-data/discussions/623) and the evolution policy in [Evolution](evolution.md). Inspired by Swift's `@available` attribute, Kotlin's `@Deprecated`, and OpenAPI 3.3's deprecation model.
138
-
139
- | Field | Type | Description |
140
- | -------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
141
- | `uuid` | string (UUID) | Stable unique id for rename tracking and diffs. |
142
- | `introduced` | string (version) | Spec version when the token was first added (e.g. `"1.0.0"`). |
143
- | `deprecated` | string (version) | Spec version when the token was deprecated (e.g. `"3.2.0"`). Truthy = deprecated. |
144
- | `deprecated_comment` | string | Human-readable deprecation explanation and migration guidance. |
145
- | `replaced_by` | 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. |
146
- | `plannedRemoval` | string (version) | Spec version when the token will be removed. If omitted, defaults to the next major version after `deprecated`. |
147
- | `private` | boolean | Not part of public API surface. |
148
- | `rationale` | string | Why this token has its current value. Intended for product-layer authoring context; see [Product context](product-context.md). |
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
- "introduced": "1.0.0",
158
- "deprecated": "3.2.0",
159
- "deprecated_comment": "Use action-background-default instead.",
160
- "replaced_by": "bbbbbbbb-0002-4000-8000-000000000001",
161
- "plannedRemoval": "4.0.0"
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 `deprecated` is present, `deprecated_comment` **SHOULD** be present.
176
+ **NORMATIVE:** If `lifecycle.deprecatedIn` is present, `lifecycle.deprecatedComment` **SHOULD** be present.
168
177
 
169
- **NORMATIVE:** If `replaced_by` is an array, `deprecated_comment` is **REQUIRED** — the comment **MUST** explain which replacement applies in which context.
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 `replaced_by` is present, `deprecated` **MUST** be present.
180
+ **NORMATIVE:** If `lifecycle.replacedBy` is present, `lifecycle.deprecatedIn` **MUST** be present.
172
181
 
173
- **NORMATIVE:** If `plannedRemoval` is present, `deprecated` **MUST** be present, and the `plannedRemoval` version **MUST NOT** precede the `deprecated` version.
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 `replaced_by` **MUST** resolve to an existing token in the dataset (see rule `SPEC-010`).
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
- * `deprecated: "3.2.0"` maps to `deprecated: true`
182
- * `replaced_by: "<uuid>"` maps to `renamed: "<target-token-name>"` (resolved via UUID lookup)
183
- * `introduced` and `plannedRemoval` have no legacy equivalent and are not emitted
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 `deprecated: "unknown"` (authors should backfill the actual version)
188
- * `renamed: "<name>"` maps to `replaced_by: "<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 — `replaced_by` must be set manually in that case
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