@adobe/design-data-spec 4.0.0 → 4.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 (52) hide show
  1. package/conformance/README.md +2 -2
  2. package/conformance/generation/README.md +6 -6
  3. package/conformance/generation/mode-set-edit/input/tokens.tokens.json +3 -3
  4. package/conformance/generation/mode-set-token/input/tokens.tokens.json +2 -2
  5. package/conformance/manifest-extensions/README.md +4 -1
  6. package/conformance/manifest-extensions/invalid/mode-set-missing-default/expected-errors.json +7 -0
  7. package/conformance/manifest-extensions/invalid/mode-set-missing-default/extensions/mode-sets/broken.json +1 -0
  8. package/conformance/manifest-extensions/invalid/mode-set-missing-default/manifest.json +4 -0
  9. package/conformance/manifest-extensions/invalid/mode-set-remove-mode-of-default/expected-errors.json +6 -0
  10. package/conformance/manifest-extensions/invalid/mode-set-remove-mode-of-default/extensions/mode-sets/remove-desktop.json +1 -0
  11. package/conformance/manifest-extensions/invalid/mode-set-remove-mode-of-default/extensions/mode-sets/scale.json +1 -0
  12. package/conformance/manifest-extensions/invalid/mode-set-remove-mode-of-default/manifest.json +1 -0
  13. package/conformance/manifest-extensions/invalid/mode-set-set-default-nonmember/expected-errors.json +6 -0
  14. package/conformance/manifest-extensions/invalid/mode-set-set-default-nonmember/extensions/mode-sets/default-tv.json +1 -0
  15. package/conformance/manifest-extensions/invalid/mode-set-set-default-nonmember/extensions/mode-sets/scale.json +1 -0
  16. package/conformance/manifest-extensions/invalid/mode-set-set-default-nonmember/manifest.json +1 -0
  17. package/conformance/manifest-extensions/invalid/mode-set-target-not-found/expected-errors.json +6 -0
  18. package/conformance/manifest-extensions/invalid/mode-set-target-not-found/extensions/mode-sets/add-tv.json +1 -0
  19. package/conformance/manifest-extensions/invalid/mode-set-target-not-found/manifest.json +1 -0
  20. package/conformance/manifest-extensions/invalid/mode-set-unknown-op/expected-errors.json +6 -0
  21. package/conformance/manifest-extensions/invalid/mode-set-unknown-op/extensions/mode-sets/bogus.json +1 -0
  22. package/conformance/manifest-extensions/invalid/mode-set-unknown-op/extensions/mode-sets/scale.json +1 -0
  23. package/conformance/manifest-extensions/invalid/mode-set-unknown-op/manifest.json +1 -0
  24. package/conformance/manifest-extensions/valid/injects-mode-set/expected.json +1 -0
  25. package/conformance/manifest-extensions/valid/injects-mode-set/extensions/mode-sets/interface-level.json +5 -0
  26. package/conformance/manifest-extensions/valid/injects-mode-set/manifest.json +4 -0
  27. package/conformance/manifest-extensions/valid/mode-set-add-mode/expected.json +8 -0
  28. package/conformance/manifest-extensions/valid/mode-set-add-mode/extensions/mode-sets/add-tv.json +1 -0
  29. package/conformance/manifest-extensions/valid/mode-set-add-mode/extensions/mode-sets/scale.json +1 -0
  30. package/conformance/manifest-extensions/valid/mode-set-add-mode/manifest.json +1 -0
  31. package/conformance/manifest-extensions/valid/mode-set-remove/expected.json +1 -0
  32. package/conformance/manifest-extensions/valid/mode-set-remove/extensions/mode-sets/contrast.json +1 -0
  33. package/conformance/manifest-extensions/valid/mode-set-remove/extensions/mode-sets/remove-contrast.json +1 -0
  34. package/conformance/manifest-extensions/valid/mode-set-remove/manifest.json +1 -0
  35. package/conformance/manifest-extensions/valid/mode-set-remove-mode/expected.json +8 -0
  36. package/conformance/manifest-extensions/valid/mode-set-remove-mode/extensions/mode-sets/color-scheme.json +5 -0
  37. package/conformance/manifest-extensions/valid/mode-set-remove-mode/extensions/mode-sets/remove-wireframe.json +1 -0
  38. package/conformance/manifest-extensions/valid/mode-set-remove-mode/manifest.json +1 -0
  39. package/conformance/manifest-extensions/valid/mode-set-set-default/expected.json +6 -0
  40. package/conformance/manifest-extensions/valid/mode-set-set-default/extensions/mode-sets/default-tv.json +1 -0
  41. package/conformance/manifest-extensions/valid/mode-set-set-default/extensions/mode-sets/scale.json +1 -0
  42. package/conformance/manifest-extensions/valid/mode-set-set-default/manifest.json +1 -0
  43. package/conformance/reference/set-alias-dark/expected.json +1 -1
  44. package/conformance/reference/set-alias-dark/input/tokens.tokens.json +2 -2
  45. package/conformance/reference/set-alias-light/expected.json +1 -1
  46. package/conformance/reference/set-alias-light/input/tokens.tokens.json +2 -2
  47. package/package.json +1 -1
  48. package/rules/rules.yaml +17 -0
  49. package/spec/cascade.md +3 -1
  50. package/spec/manifest.md +61 -18
  51. package/spec/mode-sets.md +10 -0
  52. package/spec/token-format.md +28 -0
@@ -79,14 +79,14 @@ These fixtures verify the field-mapping contract in [Evolution — Legacy format
79
79
  | Folder | Intent |
80
80
  | --------------------------- | -------------------------------------------------------------------------------------------------- |
81
81
  | `generation/flat-token` | A single cascade token with no mode sets maps to a single keyed entry `{ slug: { value, uuid } }`. |
82
- | `generation/mode-set-token` | Multiple cascade tokens sharing a `set_uuid` merge into a single `sets`-keyed entry. |
82
+ | `generation/mode-set-token` | Multiple cascade tokens sharing a `conceptId` merge into a single `sets`-keyed entry. |
83
83
 
84
84
  The Rust SDK drives these fixtures via `design-data migrate legacy-output` (see `sdk/core/src/legacy.rs`).
85
85
  Consumer implementations MUST regenerate each `input/` and diff against `expected/` to claim conformance
86
86
  with the output-generator determinism contract.
87
87
 
88
88
  See [`generation/README.md`](generation/README.md) for the fixture format, the run command, and
89
- documentation of the `set_uuid`/`set_schema` cascade grouping fields used in `mode-set-token`.
89
+ documentation of the `conceptId`/`set_schema` cascade grouping fields used in `mode-set-token`.
90
90
 
91
91
  ***
92
92
 
@@ -38,20 +38,20 @@ design-data migrate legacy-output <case>/input --output <case>/expected
38
38
  | Case | What it exercises |
39
39
  | ------------------ | ----------------------------------------------------------------------------------------------------------- |
40
40
  | `flat-token` | Basic flat token conversion (property key derivation, field passthrough) |
41
- | `mode-set-token` | Color-set reconstruction from per-mode cascade records (`set_uuid`/`set_schema`) |
41
+ | `mode-set-token` | Color-set reconstruction from per-mode cascade records (`conceptId`/`set_schema`) |
42
42
  | `deprecated-token` | `deprecated: "version"` string → `true` boolean; `deprecated_comment` passthrough; `plannedRemoval` dropped |
43
43
  | `renamed-token` | `replaced_by: "<uuid>"` → `renamed: "<property-name>"` via global UUID→name map |
44
44
  | `alias-rewire` | `$ref: "<uuid>"` → `value: "{<property-name>}"` (alias denormalization with UUID resolution) |
45
45
  | `mode-set-edit` | `deprecated`/`renamed` hoisted to outer set level when consistent across all mode entries |
46
46
 
47
- ## Cascade grouping fields (`set_uuid` / `set_schema`)
47
+ ## Cascade grouping fields (`conceptId` / `set_schema`)
48
48
 
49
49
  The `mode-set-token` and `mode-set-edit` fixtures use two cascade-internal grouping fields:
50
50
 
51
- | Field | Type | Role |
52
- | ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
- | `set_uuid` | string | Shared UUID that groups per-mode cascade records into one set-keyed legacy entry. The SDK registers this as `set_uuid → child-key list` in `sdk/core/src/graph.rs` and the legacy generator collapses all records sharing a `set_uuid` into a single `sets`-keyed output object. |
54
- | `set_schema` | string | The `$schema` URI of the aggregate set token emitted at the outer (non-mode-keyed) level in the legacy output (e.g. `color-set.json`). |
51
+ | Field | Type | Role |
52
+ | ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `conceptId` | string | Canonical concept-level UUID shared by every mode row of a design concept. The SDK registers this as `conceptId → child-key list` in `sdk/core/src/graph.rs` and the legacy generator collapses all records sharing a `conceptId` into a single `sets`-keyed output object. |
54
+ | `set_schema` | string | The `$schema` URI of the aggregate set token emitted at the outer (non-mode-keyed) level in the legacy output (e.g. `color-set.json`). |
55
55
 
56
56
  These fields are not yet in the normative token-format field table — they are Rust SDK
57
57
  implementation fields defined in `sdk/core/src/graph.rs` and emitted by the cascade ingest
@@ -9,7 +9,7 @@
9
9
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
10
10
  "value": "rgb(20, 115, 230)",
11
11
  "uuid": "11111111-0001-4000-8000-000000000001",
12
- "set_uuid": "11111111-0003-4000-8000-000000000003",
12
+ "conceptId": "11111111-0003-4000-8000-000000000003",
13
13
  "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json",
14
14
  "lifecycle": {
15
15
  "deprecatedIn": true,
@@ -26,7 +26,7 @@
26
26
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
27
27
  "value": "rgb(56, 145, 254)",
28
28
  "uuid": "11111111-0002-4000-8000-000000000002",
29
- "set_uuid": "11111111-0003-4000-8000-000000000003",
29
+ "conceptId": "11111111-0003-4000-8000-000000000003",
30
30
  "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json",
31
31
  "lifecycle": {
32
32
  "deprecatedIn": true,
@@ -38,7 +38,7 @@
38
38
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
39
39
  "value": "rgb(20, 115, 230)",
40
40
  "uuid": "11111111-0004-4000-8000-000000000004",
41
- "set_uuid": "11111111-0005-4000-8000-000000000005",
41
+ "conceptId": "11111111-0005-4000-8000-000000000005",
42
42
  "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json"
43
43
  }
44
44
  ]
@@ -9,7 +9,7 @@
9
9
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
10
10
  "value": "rgb(245, 249, 255)",
11
11
  "uuid": "bbbbbbbb-0001-4000-8000-000000000001",
12
- "set_uuid": "cccccccc-0001-4000-8000-000000000001",
12
+ "conceptId": "cccccccc-0001-4000-8000-000000000001",
13
13
  "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json",
14
14
  "private": true
15
15
  },
@@ -23,7 +23,7 @@
23
23
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
24
24
  "value": "rgb(14, 23, 63)",
25
25
  "uuid": "bbbbbbbb-0002-4000-8000-000000000002",
26
- "set_uuid": "cccccccc-0001-4000-8000-000000000001",
26
+ "conceptId": "cccccccc-0001-4000-8000-000000000001",
27
27
  "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json",
28
28
  "private": true
29
29
  }
@@ -4,7 +4,7 @@ Exercises the Layer 2 platform manifest's `extensions/` **directory** loader
4
4
  (`packages/design-data-spec/spec/manifest.md#extensions-directory`): a sibling
5
5
  `extensions/` tree (or a custom name via the manifest's `extensionsDir` field)
6
6
  whose category subdirectories (`tokens/`, `components/`, `fields/`,
7
- `guidelines/`, `platform-extensions/`, `relationships/`) are discovered,
7
+ `guidelines/`, `mode-sets/`, `platform-extensions/`, `relationships/`) are discovered,
8
8
  glob+merged in sorted path order, per-fragment schema-validated, and spliced
9
9
  into the manifest before it's applied to the graph.
10
10
 
@@ -23,6 +23,7 @@ into the manifest before it's applied to the graph.
23
23
  },
24
24
  "components": { "present": ["tab-bar-ios"] },
25
25
  "fields": { "present": ["hapticStyle"] },
26
+ "modeSets": { "present": ["interfaceLevel"] },
26
27
  "guidelines": { "present": ["ios-haptics"] },
27
28
  "tokens": { "orderByUuid": ["<uuid-1>", "<uuid-2>"] }
28
29
  }
@@ -42,6 +43,7 @@ Rust SDK drives these fixtures in `sdk/core/src/lib.rs` via the
42
43
  | `valid/injects-platform-extension` | A `platform-extensions/` fragment is injected. |
43
44
  | `valid/injects-field` | A `fields/` fragment is injected. |
44
45
  | `valid/injects-guideline` | A `guidelines/` fragment is injected. |
46
+ | `valid/injects-mode-set` | A `mode-sets/` fragment is injected (declare-or-replace by name). |
45
47
  | `valid/injects-relationship` | A `relationships/` plain-add fragment is injected. |
46
48
  | `valid/override-relationship-by-uuid` | An `op: "override"` entry replaces a plain add sharing its `uuid`, regardless of file sort order. |
47
49
  | `valid/plain-add-uuid-collision-append` | Two plain adds sharing a `uuid` both append rather than one overwriting the other. |
@@ -55,6 +57,7 @@ Rust SDK drives these fixtures in `sdk/core/src/lib.rs` via the
55
57
  | `invalid/component-missing-name` | A `components/` fragment missing `name` fails Layer 1 fragment schema validation. |
56
58
  | `invalid/field-invalid-kind` | A `fields/` fragment with an invalid `kind` enum value fails schema validation. |
57
59
  | `invalid/guideline-invalid-category` | A `guidelines/` fragment with an invalid `category` enum value fails schema validation. |
60
+ | `invalid/mode-set-missing-default` | A `mode-sets/` fragment missing the required `default` field fails Layer 1 fragment schema validation. |
58
61
  | `invalid/platform-extension-missing-extends` | A `platform-extensions/` fragment missing `extends` fails schema validation. |
59
62
  | `invalid/token-invalid-uuid` | A `tokens/*.tokens.json` fragment with a malformed `uuid` fails schema validation. |
60
63
  | `invalid/relationship-missing-scope` | A `relationships/` plain-add fragment missing `scope` fails schema validation. |
@@ -0,0 +1,7 @@
1
+ {
2
+ "errors": [
3
+ { "message_pattern": "broken\\.json" },
4
+ { "message_pattern": "mode-set\\.schema\\.json" },
5
+ { "message_pattern": "\"default\"" }
6
+ ]
7
+ }
@@ -0,0 +1 @@
1
+ { "name": "interfaceLevel", "modes": ["base", "elevated"] }
@@ -0,0 +1,4 @@
1
+ {
2
+ "specVersion": "1.0.0-draft",
3
+ "foundationVersion": "1.0.0"
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors": [
3
+ { "message_pattern": "removeMode" },
4
+ { "message_pattern": "current default" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "op": "removeMode", "mode": "desktop" }
@@ -0,0 +1 @@
1
+ { "name": "scale", "modes": ["desktop", "mobile"], "default": "desktop" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors": [
3
+ { "message_pattern": "setDefault" },
4
+ { "message_pattern": "not in its modes" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "op": "setDefault", "default": "tv" }
@@ -0,0 +1 @@
1
+ { "name": "scale", "modes": ["desktop", "mobile"], "default": "desktop" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors": [
3
+ { "message_pattern": "\"scale\"" },
4
+ { "message_pattern": "does not exist" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "op": "addMode", "mode": "tv" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors": [
3
+ { "message_pattern": "unknown op" },
4
+ { "message_pattern": "\"bogus\"" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "modes": ["desktop", "mobile"], "default": "desktop" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1 @@
1
+ { "modeSets": { "present": ["interfaceLevel"] } }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "interfaceLevel",
3
+ "modes": ["base", "elevated"],
4
+ "default": "base"
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "specVersion": "1.0.0-draft",
3
+ "foundationVersion": "1.0.0"
4
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "modeSets": {
3
+ "present": ["scale"],
4
+ "modes": {
5
+ "scale": { "modes": ["desktop", "mobile", "tv"], "default": "desktop" }
6
+ }
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "op": "addMode", "mode": "tv" }
@@ -0,0 +1 @@
1
+ { "name": "scale", "modes": ["desktop", "mobile"], "default": "desktop" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1 @@
1
+ { "modeSets": { "absent": ["contrast"] } }
@@ -0,0 +1 @@
1
+ { "name": "contrast", "modes": ["regular", "high"], "default": "regular" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1,8 @@
1
+ {
2
+ "modeSets": {
3
+ "present": ["colorScheme"],
4
+ "modes": {
5
+ "colorScheme": { "modes": ["light", "dark"], "default": "light" }
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "colorScheme",
3
+ "modes": ["light", "dark", "wireframe"],
4
+ "default": "light"
5
+ }
@@ -0,0 +1 @@
1
+ { "name": "colorScheme", "op": "removeMode", "mode": "wireframe" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -0,0 +1,6 @@
1
+ {
2
+ "modeSets": {
3
+ "present": ["scale"],
4
+ "modes": { "scale": { "default": "tv" } }
5
+ }
6
+ }
@@ -0,0 +1 @@
1
+ { "name": "scale", "op": "setDefault", "default": "tv" }
@@ -0,0 +1 @@
1
+ { "name": "scale", "modes": ["desktop", "mobile", "tv"], "default": "desktop" }
@@ -0,0 +1 @@
1
+ { "specVersion": "1.0.0-draft", "foundationVersion": "1.0.0" }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "A set-level alias ($ref targeting a set_uuid) MUST resolve to the mode-appropriate child. Under colorScheme=dark, accent-color-900 → blue-900 dark (different value from light).",
2
+ "description": "A set-level alias ($ref targeting a conceptId) MUST resolve to the mode-appropriate child. Under colorScheme=dark, accent-color-900 → blue-900 dark (different value from light).",
3
3
  "resolved": true,
4
4
  "expected_value": "rgb(75, 117, 255)",
5
5
  "expected_chain_length": 3,
@@ -9,7 +9,7 @@
9
9
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
10
10
  "value": "rgb(59, 99, 251)",
11
11
  "uuid": "ref-blue900-light-0000-000000000002",
12
- "set_uuid": "ref-blue900-set-000-0000-000000000002"
12
+ "conceptId": "ref-blue900-set-000-0000-000000000002"
13
13
  },
14
14
  {
15
15
  "name": {
@@ -21,7 +21,7 @@
21
21
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
22
22
  "value": "rgb(75, 117, 255)",
23
23
  "uuid": "ref-blue900-dark-0000-000000000002",
24
- "set_uuid": "ref-blue900-set-000-0000-000000000002"
24
+ "conceptId": "ref-blue900-set-000-0000-000000000002"
25
25
  },
26
26
  {
27
27
  "name": { "property": "accent-color-900" },
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "A set-level alias ($ref targeting a set_uuid) MUST resolve to the mode-appropriate child. Under colorScheme=light, accent-color-900 → blue-900 light.",
2
+ "description": "A set-level alias ($ref targeting a conceptId) MUST resolve to the mode-appropriate child. Under colorScheme=light, accent-color-900 → blue-900 light.",
3
3
  "resolved": true,
4
4
  "expected_value": "rgb(59, 99, 251)",
5
5
  "expected_chain_length": 3,
@@ -9,7 +9,7 @@
9
9
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
10
10
  "value": "rgb(59, 99, 251)",
11
11
  "uuid": "ref-blue900-light-0000-000000000001",
12
- "set_uuid": "ref-blue900-set-000-0000-000000000001"
12
+ "conceptId": "ref-blue900-set-000-0000-000000000001"
13
13
  },
14
14
  {
15
15
  "name": {
@@ -21,7 +21,7 @@
21
21
  "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color.json",
22
22
  "value": "rgb(75, 117, 255)",
23
23
  "uuid": "ref-blue900-dark-0000-000000000001",
24
- "set_uuid": "ref-blue900-set-000-0000-000000000001"
24
+ "conceptId": "ref-blue900-set-000-0000-000000000001"
25
25
  },
26
26
  {
27
27
  "name": { "property": "accent-color-900" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/design-data-spec",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Design Data Specification — prose, JSON Schemas, rule catalog, and conformance fixtures for the design data format",
5
5
  "type": "module",
6
6
  "repository": {
package/rules/rules.yaml CHANGED
@@ -707,3 +707,20 @@ rules:
707
707
  message: "Relationship $ref {ref} resolves to a token deprecated in {deprecatedIn}"
708
708
  spec_ref: spec/relationship-format.md#legacy-fields-interim
709
709
  introduced_in: "1.0.0-draft"
710
+
711
+ - id: SPEC-059
712
+ name: alias-mode-context-compatibility
713
+ severity: warning
714
+ layer: 2
715
+ category: reference-integrity
716
+ note: >
717
+ Mode analogue of SPEC-002. A `$ref` target that is a `conceptId` (see
718
+ token-format.md#concept-identity-conceptid) is context-resolved elsewhere and exempt
719
+ here; only a direct per-value target is checked.
720
+ assert: >
721
+ An alias whose name object sets a mode-set field (e.g. `colorScheme: dark`) SHOULD NOT
722
+ resolve, via its `$ref` chain, to a per-value token that sets the same field to a
723
+ different value. Tokens whose name object omits the field (mode-agnostic) are exempt.
724
+ message: "Alias {token} sets {field}={ownValue} but its $ref chain resolves to a token with {field}={leafValue}"
725
+ spec_ref: spec/cascade.md#alias-resolution
726
+ introduced_in: "1.0.0-draft"
package/spec/cascade.md CHANGED
@@ -57,7 +57,9 @@ Exact matching rules for omitted mode sets are defined alongside mode set declar
57
57
 
58
58
  **NORMATIVE:** If the winning candidate is an alias, its `$ref` chain **MUST** be resolved to a literal value using the standard alias-resolution rules (SPEC-001 through SPEC-003 in `rules/rules.yaml`). The alias target is resolved within the same dataset (i.e. the same merged layer stack), not relative to any single layer. In cascade format, `$ref` holds the **UUID** of the target token (see [`token-format.md` §Alias](token-format.md#alias-ref)); resolution proceeds UUID-first, then direct graph key (legacy slug), then serialised name.
59
59
 
60
- **RATIONALE:** Aliases participate in cascade as opaque references — their target values are not examined during specificity calculation or layer comparison. This keeps resolution deterministic and allows aliases to be valid candidates at any specificity level.
60
+ **NORMATIVE:** When a `$ref` target is a **`conceptId`** (see [`token-format.md` §Concept identity](token-format.md#concept-identity-conceptid)) rather than a per-row `uuid`, resolution **MUST** pick the mode row registered under that `conceptId` whose name-object fields best match the referencing token's own resolution context (greatest count of matching mode-set key/value pairs). Ties **MUST** be broken by the lexicographically smallest `uuid` among equally-scoring rows, so repeated resolution of the same alias always returns the same token. This context-aware step runs in place of, not in addition to, the UUID-first/direct-key/name fallback above — a `conceptId` target either resolves to its best-matching row or (when no rows are registered under it) falls through to the standard chain.
61
+
62
+ **RATIONALE:** Aliases participate in cascade as opaque references — their target values are not examined during specificity calculation or layer comparison. This keeps resolution deterministic and allows aliases to be valid candidates at any specificity level. Resolving a `conceptId` target against the *referencing* token's own context (rather than always the first or an arbitrary row) is what lets one alias correctly track a multi-mode concept (e.g. a semantic color that must stay light/dark-aligned with whatever aliases to it) without one alias per mode.
61
63
 
62
64
  ## Cross-mode-set overrides
63
65
 
package/spec/manifest.md CHANGED
@@ -8,23 +8,25 @@ 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
- | Add relationships/CTRs; override/remove by `uuid` | Yes | `extensions/relationships/` | Relationships (CTRs) |
22
- | Add / remove naming exceptions | Yes | `namingExceptions` | Naming validation |
23
- | Annotate existing terminology (cannot add new ids) | Yes | `extensions/platform-extensions/` | Existing registry ids |
24
- | Restrict allowed mode-set values | Yes | `modeSetRestrictions` | Mode sets |
25
- | Reformat name serialization | Schema-declared only, not yet applied by the reference SDK | `formatting` | Token name strings |
26
- | Override/remove/alias translations | No | — | — |
27
- | 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 |
26
+ | Restrict allowed mode-set values | Yes | `modeSetRestrictions` | Mode sets |
27
+ | Reformat name serialization | Schema-declared only, not yet applied by the reference SDK | `formatting` | Token name strings |
28
+ | Override/remove/alias translations | No | — | — |
29
+ | Override Layer-1 schemas | No (decided; see [spike](manifest-schema-override-spike.md)) | — | — |
28
30
 
29
31
  ## Manifest document
30
32
 
@@ -86,6 +88,7 @@ extensions/
86
88
  tokens/ *.tokens.json cascade-format token files
87
89
  components/ <component>.json one component per file
88
90
  fields/ <field>.json one field declaration per file
91
+ mode-sets/ <mode-set>.json one mode set per file
89
92
  relationships/ <component>.json one CTR set per file
90
93
  guidelines/ <topic>.json one guideline per file
91
94
  platform-extensions/ <platform>-<registry>.json
@@ -103,7 +106,7 @@ or empty subdirectory contributes nothing and is not an error.
103
106
  cascade token file) and **MUST** validate against `cascade-file.schema.json`. Files are
104
107
  **deep-merged** into one tokens object, in sorted path order. Entries **MAY** carry a `$ref`
105
108
  to alias an existing token instead of a literal value.
106
- * **`components/`, `fields/`, `guidelines/`, `platform-extensions/`** — one artifact per file.
109
+ * **`components/`, `fields/`, `guidelines/`, `mode-sets/`, `platform-extensions/`** — one artifact per file.
107
110
  Entries across all files in the subdirectory are concatenated, in sorted path order, and
108
111
  injected into the corresponding catalog **add-or-replace by name** (for
109
112
  `platform-extensions/`, by `termId`; see below). When two files declare the same name,
@@ -115,6 +118,7 @@ load time — this is enforced reference-SDK behavior, not an aspirational goal:
115
118
  * **`components/`** → `component.schema.json`
116
119
  * **`fields/`** → `field.schema.json` (see [`extensions/fields/`](#extensionsfields) below)
117
120
  * **`guidelines/`** → `guideline.schema.json`
121
+ * **`mode-sets/`** → `mode-set.schema.json` (see [`extensions/mode-sets/`](#extensionsmode-sets) below)
118
122
  * **`relationships/`** → `relationship.schema.json` (see the Add/Override/remove rules above)
119
123
  * **`platform-extensions/`** → `platform-extension.json` (see
120
124
  [`extensions/platform-extensions/`](#extensionsplatform-extensions) below)
@@ -143,6 +147,45 @@ declared) references field names by string — a platform that renames or remove
143
147
  also references there is self-inconsistent; that is a manifest-authoring concern, not enforced
144
148
  by the reference SDK.
145
149
 
150
+ #### `extensions/mode-sets/`
151
+
152
+ Platform-local mode-set declarations and edits, injected into the mode-set catalog for this
153
+ platform's resolution. A mode set's `name` is its stable key (there is no separate `uuid`, unlike
154
+ relationships) — every entry, add or op alike, identifies its target by `name`:
155
+
156
+ * **Add / replace:** an entry with no `op` field is a full mode-set object (**NORMATIVE:** MUST
157
+ validate against `mode-set.schema.json`, requiring `name`, `modes`, `default`). A `name` not
158
+ already in the foundation catalog is a clean new platform axis (e.g. `interfaceLevel: [base,
159
+ elevated]`); a `name` matching a foundation mode set replaces it, for this platform only — the
160
+ foundation catalog is untouched.
161
+ * **`"op": "addMode"`** — appends one mode value (`mode`) to an existing set, without restating its
162
+ other modes. **NORMATIVE:** the target set (by `name`) MUST already exist and MUST NOT already
163
+ contain `mode`; either condition failing is a manifest error.
164
+ * **`"op": "removeMode"`** — drops one mode value (`mode`) from an existing set. **NORMATIVE:** the
165
+ target MUST exist, MUST contain `mode`, `mode` MUST NOT be the set's current default (retarget the
166
+ default first with `setDefault`), the set MUST have more than one mode remaining afterward, and no
167
+ token in the resolved graph MAY still carry `mode` as that mode set's value — each violation is a
168
+ manifest error. (This mirrors the reference SDK's authoring-time `remove_mode` guard, which checks
169
+ the same "still referenced" condition against cascade token files.)
170
+ * **`"op": "setDefault"`** — retargets an existing set's default mode (`default`). **NORMATIVE:**
171
+ the target MUST exist and `default` MUST already be one of its modes.
172
+ * **`"op": "remove"`** — drops the whole set (by `name`) from this platform's resolution.
173
+ **NORMATIVE:** the target MUST exist, and no token in the resolved graph MAY still carry this mode
174
+ set's `name` as a key — either violation is a manifest error.
175
+
176
+ Add/replace entries are applied before op entries, regardless of file sort order, so a set declared
177
+ in one file can be edited by an op in another file within the same `extensions/mode-sets/`
178
+ directory. Ops within each group apply in sorted path order. Any other `op` value, or an op entry
179
+ missing its required field (`mode` for `addMode`/`removeMode`, `default` for `setDefault`), is a
180
+ manifest error — the reference SDK rejects the load rather than silently skipping it, consistent
181
+ with `extensions/relationships/`'s override/remove ops above. `default ∈ modes` remains a Layer 2
182
+ concern for the resolved graph as a whole, enforced by SPEC-005 against whichever mode sets end up
183
+ there, foundation or platform-declared alike.
184
+
185
+ Contrast with `modeSetRestrictions` ([Mode Sets — Platform restrictions](mode-sets.md#platform-restrictions)), a top-level manifest field that only *narrows* the allowed values of an
186
+ existing mode set at resolution time — it cannot add, remove, or retarget a mode value, or declare
187
+ a new axis.
188
+
146
189
  #### `extensions/platform-extensions/`
147
190
 
148
191
  Platform terminology annotations layered onto **existing** foundation registry entries (for
package/spec/mode-sets.md CHANGED
@@ -45,6 +45,16 @@ The Spectrum foundation publishes mode set declarations as JSON files under `pac
45
45
 
46
46
  Additional mode sets (e.g. `language`, `motion`) **MAY** be declared in a dataset's mode set catalog. Token name objects **MAY** include keys matching declared mode set names.
47
47
 
48
+ A platform manifest **MAY** also declare or edit mode sets local to that platform, one entry per
49
+ file under `extensions/mode-sets/` (see [Platform manifest — `extensions/`
50
+ directory](manifest.md#extensions-directory)). An entry with no `op` uses **declare-or-replace-by-name** semantics, same as `extensions/fields/`: a new `name` adds a platform-local axis (e.g.
51
+ `interfaceLevel`), while a `name` matching a foundation mode set replaces it for that platform only,
52
+ leaving the foundation catalog untouched. `op: "addMode"` / `"removeMode"` / `"setDefault"` /
53
+ `"remove"` instead edit an existing set in place — adding or dropping a single mode value,
54
+ retargeting its default, or dropping the whole set — without restating its other modes. This is
55
+ distinct from `modeSetRestrictions` (below), which only *narrows* the allowed values of an existing
56
+ mode set at resolution time rather than editing the declared set itself.
57
+
48
58
  ## Defaults and specificity
49
59
 
50
60
  **NORMATIVE:** A token name object **omitting** a mode set field implies the token applies under the mode set's **`default`** mode for specificity and matching purposes unless the spec for that mode set states otherwise.
@@ -128,6 +128,34 @@ When **`$ref`** is present, the token is an **alias**. The value **MUST** be a n
128
128
  "uuid": "f24eb871-6419-4cef-88a2-cca8548ae31e" }
129
129
  ```
130
130
 
131
+ ### Concept identity (`conceptId`)
132
+
133
+ **`conceptId`** is a UUID shared by every mode row of the same design concept — e.g. the light,
134
+ dark, and wireframe rows of one color, or the mobile/desktop rows of one scale. It is additive to
135
+ the per-value `uuid` (which remains the `$ref`/diff/`replacedBy`/sync anchor for one specific mode
136
+ row) and, unlike `uuid`, is stable across the whole concept rather than one row.
137
+
138
+ ```json
139
+ // Two mode rows of the same concept share one conceptId, each with its own uuid.
140
+ { "name": { "property": "accent-color", "colorScheme": "light" },
141
+ "value": "rgb(59, 99, 251)",
142
+ "uuid": "aaaaaaaa-0000-4000-8000-000000000001",
143
+ "conceptId": "cccccccc-0000-4000-8000-000000000099" }
144
+ { "name": { "property": "accent-color", "colorScheme": "dark" },
145
+ "value": "rgb(75, 117, 255)",
146
+ "uuid": "aaaaaaaa-0000-4000-8000-000000000002",
147
+ "conceptId": "cccccccc-0000-4000-8000-000000000099" }
148
+ ```
149
+
150
+ **NORMATIVE:** All mode rows of the same design concept **MUST** carry the same `conceptId`.
151
+
152
+ **NORMATIVE:** A `$ref` target **MAY** be a `conceptId` instead of a per-row `uuid`. When it is,
153
+ resolution **MUST** pick the mode row registered under that `conceptId` whose name-object fields
154
+ best match the referencing token's own resolution context (see
155
+ [Cascade resolution — Alias resolution](cascade.md#alias-resolution)), rather than an arbitrary
156
+ row — this is what makes a single alias correctly track light/dark or mobile/desktop without one
157
+ alias per mode.
158
+
131
159
  ### Literal `value`
132
160
 
133
161
  When **`value`** is present, it **MUST** conform to the declared value type for that token. Value types are defined under `schemas/value-types/` and referenced from the token schema.