@adobe/design-data-spec 4.8.1 → 4.10.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 (87) 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 +4 -2
  36. package/conformance/invalid/SPEC-073/dataset.json +59 -0
  37. package/conformance/invalid/SPEC-073/expected-errors.json +10 -0
  38. package/conformance/invalid/SPEC-074/dataset.json +36 -0
  39. package/conformance/invalid/SPEC-074/expected-errors.json +10 -0
  40. package/conformance/invalid/SPEC-075/dataset.json +53 -0
  41. package/conformance/invalid/SPEC-075/expected-errors.json +10 -0
  42. package/conformance/invalid/SPEC-076/dataset.json +62 -0
  43. package/conformance/invalid/SPEC-076/expected-errors.json +10 -0
  44. package/conformance/invalid/SPEC-076/manifest.json +6 -0
  45. package/conformance/invalid/SPEC-079/dataset.json +62 -0
  46. package/conformance/invalid/SPEC-079/expected-errors.json +10 -0
  47. package/conformance/invalid/SPEC-079/manifest.json +58 -0
  48. package/conformance/invalid/SPEC-080/dataset.json +62 -0
  49. package/conformance/invalid/SPEC-080/expected-errors.json +10 -0
  50. package/conformance/invalid/SPEC-080/manifest.json +32 -0
  51. package/conformance/invalid/SPEC-081/dataset.json +62 -0
  52. package/conformance/invalid/SPEC-081/expected-errors.json +10 -0
  53. package/conformance/invalid/SPEC-081/manifest.json +7 -0
  54. package/conformance/invalid/SPEC-082/dataset.json +62 -0
  55. package/conformance/invalid/SPEC-082/expected-errors.json +10 -0
  56. package/conformance/invalid/SPEC-082/manifest.json +14 -0
  57. package/conformance/invalid/SPEC-083/dataset.json +47 -0
  58. package/conformance/invalid/SPEC-083/expected-errors.json +10 -0
  59. package/conformance/invalid/SPEC-084/dataset.json +16 -0
  60. package/conformance/invalid/SPEC-084/expected-errors.json +10 -0
  61. package/conformance/valid/SPEC-025/dataset.json +1 -1
  62. package/conformance/valid/SPEC-049/dataset.json +1 -1
  63. package/conformance/valid/SPEC-051/dataset.json +40 -9
  64. package/conformance/valid/SPEC-064/dataset.json +64 -0
  65. package/conformance/valid/SPEC-064/expected-errors.json +4 -0
  66. package/conformance/valid/SPEC-065/dataset.json +89 -0
  67. package/conformance/valid/SPEC-065/expected-errors.json +4 -0
  68. package/conformance/valid/SPEC-079/dataset.json +62 -0
  69. package/conformance/valid/SPEC-079/manifest.json +58 -0
  70. package/package.json +1 -1
  71. package/rules/rules.yaml +358 -0
  72. package/schemas/anatomy-part.schema.json +11 -0
  73. package/schemas/component-changelog.schema.json +306 -0
  74. package/schemas/component-spec-sheet.schema.json +222 -0
  75. package/schemas/component-visual.schema.json +287 -0
  76. package/schemas/component.schema.json +80 -0
  77. package/schemas/manifest.schema.json +5 -0
  78. package/schemas/relationship.schema.json +8 -6
  79. package/spec/anatomy-format.md +97 -10
  80. package/spec/artifact-moves.md +157 -0
  81. package/spec/component-changelog.md +49 -0
  82. package/spec/component-format.md +4 -0
  83. package/spec/component-visuals.md +210 -0
  84. package/spec/index.md +5 -0
  85. package/spec/manifest.md +37 -21
  86. package/spec/option-constraints.md +87 -0
  87. package/spec/relationship-format.md +14 -11
package/spec/manifest.md CHANGED
@@ -8,26 +8,28 @@ This document defines the **platform manifest**: how a platform implementation r
8
8
 
9
9
  The manifest supports a fixed, enumerated set of operations against the foundation — it does **not** allow overriding, aliasing, or removing arbitrary foundation artifacts. Support is concentrated on tokens; translations and schemas have no manifest-level override mechanism at all.
10
10
 
11
- | Operation | Supported? | Field | Applies to |
12
- | --------------------------------------------------- | ------------------------------------------------------------ | --------------------------------- | ------------------------------ |
13
- | Remove / exclude | Yes | `exclude` | Tokens only |
14
- | Include / whitelist | Yes | `include` | Tokens only |
15
- | Override value (type-preserving) | Yes | `overrides[].value` | Tokens only |
16
- | Override → re-alias | Yes | `overrides[].$ref` | Tokens only |
17
- | Add new tokens (may alias via `$ref`) | Yes | `extensions/tokens/` | Tokens |
18
- | Add / replace components | Yes | `extensions/components/` | Components |
19
- | Add / replace field declarations | Yes | `extensions/fields/` | Fields |
20
- | Add / replace guideline documents | Yes | `extensions/guidelines/` | Guidelines |
21
- | Declare / replace platform-local mode set | Yes | `extensions/mode-sets/` | Mode sets |
22
- | Add / remove / retarget a single mode; remove a set | Yes | `extensions/mode-sets/` (`op`) | Mode sets |
23
- | Add relationships/CTRs; override/remove by `uuid` | Yes | `extensions/relationships/` | Relationships (CTRs) |
24
- | Add / remove naming exceptions | Yes | `namingExceptions` | Naming validation |
25
- | Annotate existing terminology (cannot add new ids) | Yes | `extensions/platform-extensions/` | Existing registry ids or modes |
26
- | Upsert / remove own implementation mappings | Yes | `extensions/implementations/` | Component `implementations` |
27
- | Restrict allowed mode-set values | Yes | `modeSetRestrictions` | Mode sets |
28
- | Reformat name serialization | Schema-declared only, not yet applied by the reference SDK | `formatting` | Token name strings |
29
- | Override/remove/alias translations | No | — | — |
30
- | Override Layer-1 schemas | No (decided; see [spike](manifest-schema-override-spike.md)) | — | — |
11
+ | Operation | Supported? | Field | Applies to |
12
+ | --------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------- | -------------------------------------------- |
13
+ | Remove / exclude | Yes | `exclude` | Tokens only |
14
+ | Include / whitelist | Yes | `include` | Tokens only |
15
+ | Override value (type-preserving) | Yes | `overrides[].value` | Tokens only |
16
+ | Override → re-alias | Yes | `overrides[].$ref` | Tokens only |
17
+ | Add new tokens (may alias via `$ref`) | Yes | `extensions/tokens/` | Tokens |
18
+ | Add / replace components | Yes | `extensions/components/` | Components |
19
+ | Add / replace field declarations | Yes | `extensions/fields/` | Fields |
20
+ | Add / replace guideline documents | Yes | `extensions/guidelines/` | Guidelines |
21
+ | Declare / replace platform-local mode set | Yes | `extensions/mode-sets/` | Mode sets |
22
+ | Add / remove / retarget a single mode; remove a set | Yes | `extensions/mode-sets/` (`op`) | Mode sets |
23
+ | Add relationships/CTRs; override/remove by `uuid` | Yes | `extensions/relationships/` | Relationships (CTRs) |
24
+ | Add / remove naming exceptions | Yes | `namingExceptions` | Naming validation |
25
+ | Annotate existing terminology (cannot add new ids) | Yes | `extensions/platform-extensions/` | Existing registry ids or modes |
26
+ | Upsert / remove own implementation mappings | Yes | `extensions/implementations/` | Component `implementations` |
27
+ | Add / replace a component's visual recipe | Yes (loaded into the graph; `render` and `site` read it) | `extensions/visuals/` | Visual recipes |
28
+ | Alias a foundation component name to a platform component | Field and SPEC-081 enforced; renderers apply it | `componentAliases` | `render`, `site`, WASM render, recipe lookup |
29
+ | Restrict allowed mode-set values | Yes | `modeSetRestrictions` | Mode sets |
30
+ | Reformat name serialization | Schema-declared only, not yet applied by the reference SDK | `formatting` | Token name strings |
31
+ | Override/remove/alias translations | No | — | — |
32
+ | Override Layer-1 schemas | No (decided; see [spike](manifest-schema-override-spike.md)) | — | — |
31
33
 
32
34
  ## Manifest document
33
35
 
@@ -53,6 +55,7 @@ A manifest **MUST** conform to [`manifest.schema.json`](../schemas/manifest.sche
53
55
  | `extensionsDir` | string | Path (relative to the manifest) to the platform's `extensions/` directory. Default: `"extensions"`. See [`extensions/` directory](#extensions-directory) below. |
54
56
  | `namingExceptions` | object | Platform-local overlay on the base naming-exceptions set: names to add and/or remove for this platform's naming validation. |
55
57
  | `formatting` | object | Rules for serializing structured name objects into platform-specific token name strings. |
58
+ | `componentAliases` | object | Maps a foundation component name to a component the platform declares. See [`componentAliases`](#componentaliases). |
56
59
  | `modeSetRestrictions` | object | Mode set restrictions for this platform; see [Mode Sets — Platform restrictions](mode-sets.md#platform-restrictions). |
57
60
 
58
61
  ### `include` / `exclude`
@@ -75,7 +78,7 @@ An override **replaces** the foundation record for that platform's graph: the re
75
78
 
76
79
  | Field | Value |
77
80
  | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78
- | `overriddenBy` | `{ "platform": "<manifest platform>" }` when the manifest sets `platform`; otherwise `{ "manifest": "manifest.json" }` (the literal file name; the manifest's real path is not recorded). |
81
+ | `overriddenBy` | `{ "platform": "<manifest platform>" }` when the manifest sets `platform`; otherwise `{ "manifest": "manifest.json" }` (the literal file name; the manifest's real path is not recorded). |
79
82
  | `overriddenFrom` | `{ "value": <foundation value> }` or `{ "$ref": <foundation $ref> }`. The first override's history is kept, so it is always the foundation value, even when a token is overridden more than once (the last override wins for the value). |
80
83
 
81
84
  These markers exist only on the platform's in-memory records. They are not written to token files and are **not** serialized in legacy output.
@@ -115,6 +118,7 @@ extensions/
115
118
  guidelines/ <topic>.json one guideline per file
116
119
  platform-extensions/ <platform>-<registry>.json
117
120
  implementations/ <component>.json one component's implementation rows
121
+ visuals/ <component>.json one visual recipe per file
118
122
  ```
119
123
 
120
124
  **NORMATIVE:** Each subdirectory is discovered by a recursive glob for `*.json` files (for
@@ -268,6 +272,15 @@ a different `implementation` id are never modified or removed.
268
272
  `implementation`, `package`, `importPath`. It deletes every owned row whose values equal every
269
273
  selector field given. A remove that matches no owned row is a manifest error.
270
274
 
275
+ ### `componentAliases`
276
+
277
+ The field, `TokenGraph::canonical_component` and SPEC-081 are implemented; `render`, `site`, WASM render and recipe lookup call it, so a request for the alias key draws the target's recipe ([Proposal 014](../../../docs/proposals/014-platform-owned-visual-recipes.md)). A map from a foundation component name to a component the platform declares in `extensions/components/`, for example `{ "button": "button-ios" }`.
278
+
279
+ * **Applies to** commands that show a component by its user-facing name: `render`, `site`, WASM render and recipe lookup. The name resolves to the target.
280
+ * **Does not apply to** `query`, `validate`, `diff` and `export`, which inspect data under real names. `query` prints a one-line hint when it names an aliased component.
281
+ * **Never rewrites** a token, a relationship scope, a recipe file name or a diagnostic. Both components stay in the graph; diagnostics name the target.
282
+ * SPEC-081 (error): the key **MUST** be a foundation component; the target **MUST** be declared by the platform; aliases **MUST NOT** chain or form a cycle; a key **MUST NOT** also be replaced by `extensions/components/<key>.json`.
283
+
271
284
  ### `namingExceptions`
272
285
 
273
286
  Platform-local overlay on the base naming-exceptions set used by naming validation.
@@ -300,6 +313,8 @@ A platform **MAY** declare formatting rules that control how structured name obj
300
313
 
301
314
  **NORMATIVE:** Manifests **MUST** pass Layer 1 JSON Schema validation.
302
315
 
316
+ **NORMATIVE:** A manifest **MUST** apply to the foundation dataset. A validator that cannot apply it, for example because it extends a mode set that was not loaded, **MUST** report an error (SPEC-082 `platform-manifest-applies`) and not skip the platform rules silently.
317
+
303
318
  **RECOMMENDED:** Validators resolve `foundationVersion` against a registry or lockfile and report mismatches as errors or warnings per product policy.
304
319
 
305
320
  **RECOMMENDED:** Validators confirm the resolved `foundationVersion` provides a cascade-format
@@ -319,4 +334,5 @@ The platform manifest is the Layer 2 context document. For Layer 3 (product-laye
319
334
 
320
335
  * [#715 — Distributed Design Data Architecture](https://github.com/adobe/spectrum-design-data/discussions/715)
321
336
  * [#625 — Token Authoring Workflow](https://github.com/adobe/spectrum-design-data/discussions/625)
337
+ * [Artifact moves](artifact-moves.md) — promoting and demoting one artifact between a platform and the foundation
322
338
  * [Schema-override spike](manifest-schema-override-spike.md) — why the manifest cascade does not allow platform overrides of Layer-1 schemas
@@ -0,0 +1,87 @@
1
+ <!-- Copyright 2026 Adobe. All rights reserved. -->
2
+
3
+ # Option constraints
4
+
5
+ ## Problem
6
+
7
+ A component's `options` block lists the values each option accepts ([Component format](component-format.md#options)). It cannot say which *combinations* are valid. Today that knowledge lives in prose (for example Button's `icon` description: "Required when `isLabelHidden` is true") or in implementation code. Visual rendering exposed three cases for Button:
8
+
9
+ * `style: outline` is only designed for `primary` and `secondary`. Accent and negative outline have no spec.
10
+ * `staticColor` only restyles `primary` and `secondary`. Accent and negative render as usual.
11
+ * `icon` is required when `isLabelHidden` is true.
12
+
13
+ Without a machine-readable form, recipes drift (an accent `fill` rule leaks onto outline buttons), matrices render undefined states, and validators cannot flag either.
14
+
15
+ ## Design
16
+
17
+ An optional `constraints` array on the component object, next to `options`.
18
+
19
+ ```json
20
+ "constraints": [
21
+ {
22
+ "when": { "style": "outline" },
23
+ "require": { "variant": ["primary", "secondary"] },
24
+ "severity": "error",
25
+ "description": "Outline style is only defined for primary and secondary.",
26
+ "source": "react-spectrum S2 Button.tsx"
27
+ },
28
+ {
29
+ "when": { "staticColor": ["white", "black"] },
30
+ "require": { "variant": ["primary", "secondary"] },
31
+ "severity": "advisory",
32
+ "description": "Static color only changes primary and secondary."
33
+ },
34
+ {
35
+ "when": { "isLabelHidden": true },
36
+ "require": { "icon": { "present": true } },
37
+ "severity": "error"
38
+ }
39
+ ]
40
+ ```
41
+
42
+ ### Constraint object
43
+
44
+ | Field | Type | Required | Description |
45
+ | ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
46
+ | `when` | object | REQUIRED | Option conditions that select the combinations the rule applies to. Keys are option names; a value is a literal, or an array meaning any of. Conditions are ANDed. |
47
+ | `require` | object | REQUIRED | What those combinations must satisfy. Keys are option names; a value is an array of permitted values, or `{ "present": true }` for presence. |
48
+ | `severity` | string | REQUIRED | `error` (combination is invalid or undefined) or `advisory` (valid, but no distinct design). |
49
+ | `description` | string | OPTIONAL | Plain-text rationale. |
50
+ | `source` | string | OPTIONAL | Where the rule comes from (design file, implementation, spec page). |
51
+
52
+ `when` uses the same option-matching form as visual-recipe `when` and relationship `scope.options`. A constraint is an "if this, then that" rule. Boolean composition (any/not across options) is out of scope and is answered together with the `visibleWhen` question in [Component visuals](component-visuals.md#open-questions).
53
+
54
+ ### Semantics
55
+
56
+ **NORMATIVE:** A combination of option values that matches a constraint's `when` and violates its `require` is *excluded* at that constraint's severity.
57
+
58
+ * `error`: Tools **MUST NOT** present the combination as a supported state (matrices skip it). Validators **MUST** report recipes, relationships, or tokens that target only excluded combinations.
59
+ * `advisory`: Tools **SHOULD** still render the combination. Validators **SHOULD** warn only when a rule targets it as if it had distinct design.
60
+
61
+ Constraints are additive. They never change which values an option declares; they only restrict combinations.
62
+
63
+ ## Rules
64
+
65
+ | Rule | Layer | Severity | Check |
66
+ | -------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
67
+ | SPEC-064 | 2 | error | Every option name and value used in a constraint's `when` or `require` is declared in the component's `options`. |
68
+ | SPEC-065 | 2 | warning | A visual recipe rule or relationship `scope.options` that matches *only* combinations excluded by an `error` constraint. |
69
+
70
+ SPEC-064 is silent for value checks when an option declares no `values` list. SPEC-065 enumerates the combinations a rule matches; unpinned options range over their declared values (booleans over true and false), and an option with no known domain is never proven excluded. A rule that also matches a valid combination is not reported: a recipe that leaks onto an excluded combination while still serving a valid one is a recipe-scoping problem, not an SPEC-065 finding.
71
+
72
+ Layer 1 (`component.schema.json`) checks the `constraints` shape only. It cannot check that names exist.
73
+
74
+ ## Consumers
75
+
76
+ * **Visual matrix and renderer:** skip combinations excluded by an `error` constraint; render `advisory` ones.
77
+ * **Hub and docs:** show excluded combinations as unavailable in option pickers.
78
+ * **Platform extensions:** no override in v1. If a platform allows a combination another forbids, add a per-platform override later, following the extension layering.
79
+
80
+ ## Decisions
81
+
82
+ * **Name:** `constraints`. Slot constraints, if needed later, can use a separate field.
83
+ * **Severity:** `error` and `advisory` both ship in v1. "Valid but undesigned" combinations (Button accent with a static color) are common and need to be distinguishable from invalid ones.
84
+ * **Composition:** "if this, then that" only. Boolean composition (any/not across options) is deferred and decided together with `visibleWhen` in [Component visuals](component-visuals.md#open-questions).
85
+ * **Overrides:** an option that makes another irrelevant (checkbox `isIndeterminate` over `isSelected`) is an `advisory` constraint that requires the overridden option to hold its default. There is no separate "overrides" kind.
86
+ * **Scope:** foundation component files only. Platform extensions do not declare constraints in v1.
87
+ * **Accessible names:** not option constraints. They belong to [Accessibility](accessibility.md).
@@ -43,15 +43,15 @@ Each CTR is one of two subtypes, mirroring the token `oneOf` split (see [token-f
43
43
 
44
44
  ## Scope fields
45
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. |
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). A boolean option (for example `isSelected`) takes `true` or `false`; use it, not `state`, for conditions the component models as an option. An array value matches when any listed value matches. A relationship with no pinned options is the default for its `(property, context)`; a recipe uses the one that pins the most options matching the request. `scope.options.state` adds states to those of the token the relationship binds. `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
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.
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 strings, booleans (for boolean options) or arrays of either. 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
55
 
56
56
  ## Value-owning vs. relationship-only
57
57
 
@@ -64,9 +64,11 @@ A CTR **MUST NOT** declare both `value` and `$ref` (enforced at Layer 1 via `not
64
64
 
65
65
  ## Identity
66
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. |
67
+ | Field | Required | Type | Description |
68
+ | ------ | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
69
+ | `uuid` | yes | string (uuid) | **MUST** be present (rule SPEC-084) and globally unique across all relationship entries within the same cascade layer (rule SPEC-056, mirrors SPEC-004). It is the stable identity changelogs, platform overrides and `$ref` use, and **MUST NOT** change once assigned. |
70
+
71
+ A platform manifest overrides or removes a foundation relationship by this `uuid`, and a platform-local relationship (an `extensions/relationships/` file or an inline `extensions.relationships` add) carries its own. Authors can generate ids with `packages/design-data/scripts/backfill-relationship-uuid.mjs`, which derives a UUIDv5 from the record's identity and never changes an existing `uuid`.
70
72
 
71
73
  ## Legacy fields (interim)
72
74
 
@@ -101,3 +103,4 @@ The following rules are added to the Layer 2 rule catalog (`rules/rules.yaml`) b
101
103
  | SPEC-055 | `ctr-ref-resolves` | error | CTR `$ref` value **MUST** resolve to a declared token or relationship `uuid` in the dataset. |
102
104
  | SPEC-056 | `ctr-uuid-unique` | error | CTR `uuid` values **MUST** be unique across all relationship entries within the same cascade layer. |
103
105
  | 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. |
106
+ | SPEC-084 | `ctr-uuid-required` | error | Every CTR **MUST** have a `uuid`. |