@adobe/design-data-spec 0.2.0 → 0.3.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 (37) hide show
  1. package/README.md +3 -3
  2. package/components/button.json +70 -0
  3. package/conformance/README.md +26 -26
  4. package/conformance/invalid/SPEC-014/expected-errors.json +10 -0
  5. package/conformance/invalid/SPEC-014/tokens.tokens.json +9 -0
  6. package/conformance/invalid/SPEC-018/dataset.json +9 -0
  7. package/conformance/invalid/SPEC-018/expected-errors.json +10 -0
  8. package/conformance/invalid/SPEC-019/dataset.json +29 -0
  9. package/conformance/invalid/SPEC-019/expected-errors.json +10 -0
  10. package/conformance/invalid/SPEC-020/dataset.json +27 -0
  11. package/conformance/invalid/SPEC-020/expected-errors.json +10 -0
  12. package/conformance/invalid/SPEC-021/dataset.json +18 -0
  13. package/conformance/invalid/SPEC-021/expected-errors.json +10 -0
  14. package/conformance/invalid/SPEC-022/dataset.json +33 -0
  15. package/conformance/invalid/SPEC-022/expected-errors.json +10 -0
  16. package/conformance/invalid/SPEC-023/dataset.json +18 -0
  17. package/conformance/invalid/SPEC-023/expected-errors.json +10 -0
  18. package/conformance/invalid/SPEC-024/dataset.json +18 -0
  19. package/conformance/invalid/SPEC-024/expected-errors.json +10 -0
  20. package/conformance/valid/component-refs/dataset.json +63 -0
  21. package/conformance/valid/lifecycle-with-last-modified.json +12 -0
  22. package/package.json +18 -6
  23. package/rules/rules.yaml +81 -0
  24. package/schemas/anatomy-part.schema.json +35 -0
  25. package/schemas/component.schema.json +267 -0
  26. package/schemas/state-declaration.schema.json +36 -0
  27. package/spec/agent-surface.md +116 -0
  28. package/spec/anatomy-format.md +167 -0
  29. package/spec/component-format.md +326 -0
  30. package/spec/evolution.md +32 -32
  31. package/spec/index.md +27 -21
  32. package/spec/manifest.md +3 -1
  33. package/spec/state-model.md +245 -0
  34. package/spec/taxonomy.md +17 -1
  35. package/spec/token-format.md +1 -1
  36. package/src/canonical.js +61 -0
  37. package/src/validate.js +166 -0
@@ -0,0 +1,245 @@
1
+ # State model
2
+
3
+ **Spec version:** `1.0.0-draft` (see [Overview](index.md))
4
+
5
+ This document defines the normative **state declaration** object: the named conditions that affect a component's visual appearance or token resolution, declared in the `states` array of a component declaration. State declarations complete the machine-readable contract introduced by the component declaration (see [Component format — States stub](component-format.md#states-stub)) and enable cross-reference validation between tokens and component surfaces.
6
+
7
+ Scoped under [RFC-A — Component Contract in Design Data Spec](https://github.com/adobe/spectrum-design-data/discussions/832). See also [Component format](component-format.md).
8
+
9
+ ## Introduction
10
+
11
+ A **component state** is a named condition under which a component's visual presentation differs from its baseline. States drive token resolution: when a component is in a given state, the design system selects tokens scoped to that state name rather than (or layered on top of) the baseline tokens.
12
+
13
+ State names appear in the `state` field of token name objects (see [Token format — Name object](token-format.md#name-object)). A token with `"state": "hover"` applies only when the component is in the `hover` state. For this cross-reference to be machine-enforceable, state declarations **MUST** be present on the component declaration.
14
+
15
+ States fall into two trigger types:
16
+
17
+ * **`prop`** — set by a persistent component API property (e.g. `isDisabled`, `isSelected`). The state remains active until the property value changes.
18
+ * **`interaction`** — set by runtime user interaction (hover, focus, pressed, dragging). The state is transient and is cleared when the interaction ends.
19
+
20
+ Full normative rules for the `states` array within a component declaration are in this document. The component declaration format is defined in [`spec/component-format.md`](component-format.md).
21
+
22
+ ## State declaration object
23
+
24
+ A state declaration is a **JSON object** that appears as an element of a component declaration's `states` array. Each state declaration object **MUST** validate against the standalone schema [`state-declaration.schema.json`](../schemas/state-declaration.schema.json) (canonical `$id`: `https://opensource.adobe.com/spectrum-design-data/schemas/v0/state-declaration.schema.json`).
25
+
26
+ **NORMATIVE:** `states` **MUST** be a JSON array within the component declaration. Each element **MUST** be a state declaration object.
27
+
28
+ ### Fields
29
+
30
+ | Field | Type | Required | Description |
31
+ | ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
32
+ | `name` | string | REQUIRED | Kebab-case state identifier. **MUST** match the pattern `^[a-z][a-z0-9-]*$`. Used as the value of the `state` field in token name objects. |
33
+ | `description` | string | OPTIONAL | Plain-text description of the state's semantics and the conditions under which it is active. |
34
+ | `trigger` | string | OPTIONAL | `"prop"` for persistent prop-driven states; `"interaction"` for runtime interaction states. See [Trigger semantics](#trigger-semantics). |
35
+ | `precedence` | integer | OPTIONAL | Resolution precedence; higher value wins when multiple non-layered states are active simultaneously. Defaults to `0` if omitted. |
36
+ | `layered` | boolean | OPTIONAL | When `true`, this state composes on top of the winning non-layered state rather than competing with it. Default: `false`. |
37
+
38
+ **NORMATIVE:** No properties beyond those listed above are permitted in a state declaration object. Additional fields **MUST** cause a Layer 1 schema error.
39
+
40
+ ### `name`
41
+
42
+ **NORMATIVE:** `name` **MUST** match the pattern `^[a-z][a-z0-9-]*$` — lower-case kebab-case, non-empty.
43
+
44
+ **NORMATIVE:** `name` **MUST** be unique within the `states` array of a single component declaration. No two state declaration objects in the same component may share a `name` value.
45
+
46
+ **NORMATIVE:** Token name-object `state` field values referencing a component **MUST** match the `name` of a declared state on that component, when state declarations are present (rule SPEC-022). An undeclared `state` value is a validation error.
47
+
48
+ ### `description`
49
+
50
+ **OPTIONAL.** A plain-text description of the state's semantics and the conditions that activate it (e.g. `"Applied while the pointer is positioned over the component."`).
51
+
52
+ **RECOMMENDED:** Custom state names (those outside the [canonical state vocabulary](#canonical-state-vocabulary)) **SHOULD** include a `description` to document intent (rule SPEC-026 fires a warning for undocumented custom names).
53
+
54
+ ### `trigger`
55
+
56
+ **OPTIONAL.** One of two string values:
57
+
58
+ * `"prop"` — the state is driven by a persistent component API property. The state is active as long as the property holds a truthy or specific value (e.g. `isDisabled: true`).
59
+ * `"interaction"` — the state is driven by transient user interaction. The state activates when the interaction begins and clears when it ends.
60
+
61
+ When `trigger` is omitted, the trigger type is unspecified. Validators **MAY** warn for state declarations without a `trigger` when the component has states listed in the canonical vocabulary with a known trigger type.
62
+
63
+ ### `precedence`
64
+
65
+ **OPTIONAL.** A non-negative integer indicating this state's weight in the resolution algorithm. When multiple non-layered states are simultaneously active, the state with the highest `precedence` integer wins for token selection.
66
+
67
+ When `precedence` is omitted, it is treated as `0`.
68
+
69
+ **RECOMMENDED:** Components **SHOULD** declare explicit `precedence` values when two or more states could be active simultaneously, to avoid relying on declaration-order tie-breaking.
70
+
71
+ ### `layered`
72
+
73
+ **OPTIONAL.** A boolean indicating whether this state composes on top of the winning non-layered state instead of competing with it. Default: `false`.
74
+
75
+ When `layered: true`, the state does not participate in the non-layered precedence competition. Instead, after the winning non-layered state is determined, all active `layered: true` states are applied on top of it in order of their `precedence` values (higher precedence layered states apply last / outermost).
76
+
77
+ Typical use: focus ring states (`focus`, `focus-visible`) that must be visible regardless of whether the component is also in a `hover` or `selected` state.
78
+
79
+ ## Trigger semantics
80
+
81
+ ### `prop` trigger
82
+
83
+ A `prop` state is driven by a component API property. The state is persistent: it is active for the full duration that the property holds its activating value and is cleared only when the property changes.
84
+
85
+ Examples:
86
+
87
+ * `isDisabled: true` activates the `disabled` state.
88
+ * `isSelected: true` activates the `selected` state.
89
+ * `isIndeterminate: true` activates the `indeterminate` state.
90
+ * `isReadOnly: true` activates the `read-only` state.
91
+
92
+ `prop` states are typically mutually exclusive with each other in practice (a component is either disabled or selected, rarely both), but the spec permits multiple `prop` states to be simultaneously active; the precedence algorithm resolves which token set applies.
93
+
94
+ ### `interaction` trigger
95
+
96
+ An `interaction` state is driven by transient user input. The state is active only while the interaction is occurring and is automatically cleared when the interaction ends.
97
+
98
+ Examples:
99
+
100
+ * `hover` — active while a pointer device is positioned over the component's interactive area.
101
+ * `focus` — active while the component holds keyboard focus.
102
+ * `focus-visible` — active when focus was received via keyboard navigation (not pointer).
103
+ * `active` / `pressed` — active while a pointer button or key is held down.
104
+ * `dragging` — active while a drag-and-drop gesture originating from the component is in progress.
105
+
106
+ `interaction` states may overlay `prop` states (e.g. a disabled button may still receive a hover event on some platforms). When both are active, the precedence algorithm determines which token set wins, or, if the interaction state is `layered: true`, the interaction state's tokens are applied on top.
107
+
108
+ ## Precedence and resolution algorithm
109
+
110
+ The following algorithm is **NORMATIVE** for conforming validators and renderers that implement state-aware token resolution.
111
+
112
+ **Inputs:** A set of currently active state names for a given component instance.
113
+
114
+ **Algorithm:**
115
+
116
+ 1. Partition the active states into two groups:
117
+ * **Non-layered states:** states with `layered: false` (or `layered` omitted).
118
+ * **Layered states:** states with `layered: true`.
119
+
120
+ 2. Among non-layered states, select the **winning state**:
121
+ * Compare `precedence` values. The state with the highest `precedence` integer wins.
122
+ * If `precedence` is omitted, treat it as `0`.
123
+ * If two non-layered states tie on `precedence`, the state that appears **earlier** in the component's `states` array wins. **MUST** be treated as a tie; implementations **SHOULD** emit a warning (see SPEC-006 for the analogous cascade tie rule).
124
+
125
+ 3. Resolve tokens for the winning non-layered state. If no non-layered state is active, resolve the `default` state (or baseline tokens when no `default` state is declared).
126
+
127
+ 4. For each active layered state, in ascending `precedence` order (lowest first, so the highest-precedence layered state is applied last and sits outermost):
128
+ * Apply the layered state's tokens on top of the tokens resolved in step 3. Tokens explicitly declared for the layered state override the corresponding tokens from the non-layered resolution.
129
+
130
+ 5. The resulting merged token set is the resolved appearance for the component instance in its current state combination.
131
+
132
+ **Example:** A checkbox is simultaneously `selected` (precedence 80) and `hover` (precedence 50). `focus` (precedence 60, layered) is also active.
133
+
134
+ * Non-layered active states: `selected` (80), `hover` (50). Winner: `selected`.
135
+ * Layered active state: `focus`. Applied on top of `selected`.
136
+ * Resolution: `selected` tokens, with `focus` tokens composited over them.
137
+
138
+ ## Canonical state vocabulary
139
+
140
+ The following state names are defined by the cross-platform design audit and **SHOULD** be used in preference to custom names when their semantics match. Using canonical names enables cross-component tooling, documentation generation, and token audits.
141
+
142
+ | Name | Trigger | Precedence | Layered | Semantics |
143
+ | --------------- | ----------- | ---------- | ------- | ------------------------------------------------------------------------- |
144
+ | `default` | — | 0 | false | No special state; baseline component appearance. |
145
+ | `hover` | interaction | 50 | false | Pointer device is positioned over the component's interactive area. |
146
+ | `dragging` | interaction | 55 | false | A drag-and-drop gesture originating from this component is in progress. |
147
+ | `focus` | interaction | 60 | true | Component holds keyboard or programmatic focus. |
148
+ | `focus-visible` | interaction | 65 | true | Focus received via keyboard navigation; used for visible focus ring only. |
149
+ | `active` | interaction | 70 | false | Pointer button or activation key is currently held down. |
150
+ | `pressed` | interaction | 70 | false | Alias for `active`; prefer `active` for new declarations. |
151
+ | `invalid` | prop | 75 | false | Component value has failed validation. |
152
+ | `valid` | prop | 75 | false | Component value has passed validation. |
153
+ | `selected` | prop | 80 | false | Component is in a selected state (checkbox checked, tab active, etc.). |
154
+ | `indeterminate` | prop | 85 | false | Component has partial selection (tri-state checkbox mixed state). |
155
+ | `read-only` | prop | 90 | false | Component value is visible but not editable by the user. |
156
+ | `disabled` | prop | 100 | false | Component is non-interactive; all user input is suppressed. |
157
+
158
+ Custom state names are permitted. When a custom name is used, the state declaration object **SHOULD** include a `description` field explaining its semantics (rule SPEC-026).
159
+
160
+ **NORMATIVE:** The values in the canonical vocabulary table (trigger type, precedence, layered) are **RECOMMENDED defaults**. A component declaration MAY override any of these values for a canonical state name by explicitly declaring a different value in the state declaration object. When overriding a canonical default, the `description` **SHOULD** explain the deviation.
161
+
162
+ ## Cross-reference with token name objects
163
+
164
+ Token name objects use a `state` field to scope a token to a specific component state. The `state` field value must correspond to a state declared in the component's `states` array.
165
+
166
+ **NORMATIVE:** A token name-object `state` field value **MUST** match the `name` of a declared state on the component identified by the token's `component` field, when state declarations are present on that component (rule SPEC-022). A `state` value that does not match any declared state name is a validation error.
167
+
168
+ See [Token format — Name object](token-format.md#name-object) for the full name object field catalog.
169
+
170
+ **NORMATIVE:** The `state` field in a token name object **MUST NOT** be present unless the token's `component` field is also present. States are always scoped to a component.
171
+
172
+ ```json
173
+ {
174
+ "name": {
175
+ "component": "checkbox",
176
+ "anatomy": "checkmark",
177
+ "property": "color",
178
+ "state": "selected"
179
+ },
180
+ "value": "#0265dc"
181
+ }
182
+ ```
183
+
184
+ ## SPEC rules
185
+
186
+ The following rules in the Layer 2 rule catalog (`rules/rules.yaml`) apply to state declarations. SPEC-022 was introduced in Phase 6.1 (component-format); SPEC-026 is introduced by this chapter.
187
+
188
+ | Rule ID | Name | Severity | Assert |
189
+ | -------- | ------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
190
+ | SPEC-022 | `component-state-valid` | error | Token `state` field value **MUST** match the `name` of a declared state on the referenced component (when state declarations are present). |
191
+ | SPEC-026 | `state-custom-name-documented` | warning | State declarations with a `name` outside the canonical state vocabulary **SHOULD** include a `description` field documenting the state's semantics. |
192
+
193
+ ## Full example
194
+
195
+ A complete `states` array for a checkbox component, demonstrating prop states (`selected`, `indeterminate`, `disabled`), interaction states (`hover`, `focus` with `layered: true`, `pressed`), and explicit precedence values:
196
+
197
+ ```json
198
+ "states": [
199
+ {
200
+ "name": "default",
201
+ "description": "Baseline unchecked checkbox appearance.",
202
+ "trigger": "prop",
203
+ "precedence": 0
204
+ },
205
+ {
206
+ "name": "hover",
207
+ "description": "Applied while a pointer device is positioned over the checkbox.",
208
+ "trigger": "interaction",
209
+ "precedence": 50
210
+ },
211
+ {
212
+ "name": "focus",
213
+ "description": "Applied while the checkbox holds keyboard or programmatic focus. Composes on top of the active non-layered state.",
214
+ "trigger": "interaction",
215
+ "precedence": 60,
216
+ "layered": true
217
+ },
218
+ {
219
+ "name": "pressed",
220
+ "description": "Applied while the pointer button or Space key is held down during activation.",
221
+ "trigger": "interaction",
222
+ "precedence": 70
223
+ },
224
+ {
225
+ "name": "selected",
226
+ "description": "Applied when isSelected is true (checkbox checked).",
227
+ "trigger": "prop",
228
+ "precedence": 80
229
+ },
230
+ {
231
+ "name": "indeterminate",
232
+ "description": "Applied when isIndeterminate is true (tri-state mixed selection).",
233
+ "trigger": "prop",
234
+ "precedence": 85
235
+ },
236
+ {
237
+ "name": "disabled",
238
+ "description": "Applied when isDisabled is true. Suppresses all user input.",
239
+ "trigger": "prop",
240
+ "precedence": 100
241
+ }
242
+ ]
243
+ ```
244
+
245
+ In this example, if the checkbox is simultaneously `selected` (80) and `hover` (50) with `focus` (60, layered) active, the resolution is: `selected` wins the non-layered competition; `focus` tokens are then applied on top of the `selected` tokens.
package/spec/taxonomy.md CHANGED
@@ -77,7 +77,23 @@ Additional categories for variant and state are inherited from the existing name
77
77
  | Variant | `variant` | Variant within a component (e.g. accent, negative, primary). |
78
78
  | State | `state` | Interactive or semantic state (e.g. hover, focus, disabled). |
79
79
 
80
- **NOTE:** The categories above are filtered for semantic and layout token taxonomies. Additional categories do and will exist for other token types (e.g. color, typography). The taxonomy is built to scale as new concepts and terms are identified.
80
+ **NOTE:** The categories above are filtered for semantic and layout token taxonomies. Additional taxonomies for other token types (color, typography, motion) will be defined in future spec versions — see the open follow-up RFC discussion [#806](https://github.com/adobe/spectrum-design-data/discussions/806) (Q3, taxonomy scoping). The taxonomy is built to scale as new concepts and terms are identified.
81
+
82
+ ### Structure vs. component — when does the line move?
83
+
84
+ The `structure` and `component` fields both answer "What?" but apply at different scopes. A useful rule of thumb:
85
+
86
+ * Use `component` when the token belongs to a specific component's surface (e.g. `button-background-color` — the background color of the Button component).
87
+ * Use `structure` when the token belongs to a reusable visual pattern that recurs **across** components (e.g. `container-padding` — padding for any container-shaped surface, regardless of which component owns it).
88
+
89
+ **Worked example — `card`:**
90
+
91
+ * As a `structure`: when "card" describes a layout primitive used inside many components (e.g. `card-padding-medium` on a list item, a popover body, or a modal header), the token is structure-scoped.
92
+ * As a `component`: when "card" describes the dedicated Card component's own surfaces (e.g. `card-background-color` on the Card root), the token is component-scoped.
93
+
94
+ The same word can validly appear in both fields across the dataset; they are independent. Authors choose based on whether the token's *meaning* generalizes across many components (structure) or is specific to one component's identity (component).
95
+
96
+ Source: Nate Baldwin, "Naming conventions & shared taxonomy" — Design Data & Platforms onsite, April 1, 2026.
81
97
 
82
98
  ## Component anatomy vs. token objects
83
99
 
@@ -27,7 +27,7 @@ A token's `name` field **MAY** be a non-empty plain string instead of a name obj
27
27
  {
28
28
  "name": "focus-ring-color-key-focus",
29
29
  "value": "#0265dc",
30
- "uuid": "aaaaaaaa-0012-4000-8000-000000000001"
30
+ "uuid": "aaaaaaaa-0011-4000-8000-000000000001"
31
31
  }
32
32
  ```
33
33
 
@@ -0,0 +1,61 @@
1
+ /*
2
+ Copyright 2026 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ // Canonical vocabulary sets derived from spec chapters.
14
+ // Sources: spec/component-format.md, spec/anatomy-format.md, spec/state-model.md
15
+
16
+ export const CANONICAL_SLOTS = new Set([
17
+ "default",
18
+ "icon",
19
+ "label",
20
+ "help-text",
21
+ "negative-help-text",
22
+ "action",
23
+ "heading",
24
+ "description",
25
+ "hero",
26
+ "footer",
27
+ "tooltip",
28
+ ]);
29
+
30
+ export const CANONICAL_ANATOMY_PARTS = new Set([
31
+ "body",
32
+ "checkmark",
33
+ "disclosure-triangle",
34
+ "field",
35
+ "handle",
36
+ "header",
37
+ "icon",
38
+ "label",
39
+ "picker",
40
+ "progress-bar",
41
+ "swatch",
42
+ "thumbnail",
43
+ "track",
44
+ "value",
45
+ ]);
46
+
47
+ export const CANONICAL_STATES = new Set([
48
+ "default",
49
+ "hover",
50
+ "focus",
51
+ "focus-visible",
52
+ "active",
53
+ "pressed",
54
+ "selected",
55
+ "indeterminate",
56
+ "disabled",
57
+ "read-only",
58
+ "invalid",
59
+ "valid",
60
+ "dragging",
61
+ ]);
@@ -0,0 +1,166 @@
1
+ /*
2
+ Copyright 2026 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Layer 2 cross-reference validator for design-data-spec.
15
+ *
16
+ * Implements SPEC-018 through SPEC-024: semantic rules that validate token
17
+ * name-object fields against component declarations, and validate component
18
+ * declarations internally.
19
+ *
20
+ * @see spec/component-format.md#spec-rules
21
+ * @see spec/anatomy-format.md#spec-rules
22
+ * @see spec/state-model.md#spec-rules
23
+ */
24
+
25
+ import {
26
+ CANONICAL_SLOTS,
27
+ CANONICAL_ANATOMY_PARTS,
28
+ CANONICAL_STATES,
29
+ } from "./canonical.js";
30
+
31
+ /**
32
+ * @typedef {{ ruleId: string, severity: 'error'|'warning', message: string, tokenName?: string, componentName?: string }} Diagnostic
33
+ * @typedef {{ name: string|object, [key: string]: unknown }} Token
34
+ * @typedef {{ name: string, options?: object, anatomy?: Array<{name:string,description?:string}>, slots?: Array<{name:string,description?:string}>, states?: Array<{name:string,trigger?:string,precedence?:number,layered?:boolean,description?:string}> }} ComponentDeclaration
35
+ * @typedef {{ tokens?: Token[], components?: ComponentDeclaration[] }} Dataset
36
+ */
37
+
38
+ /**
39
+ * Validate a dataset for SPEC-018 through SPEC-024 compliance.
40
+ *
41
+ * @param {Dataset} dataset
42
+ * @returns {Diagnostic[]}
43
+ */
44
+ export function validateDataset(dataset) {
45
+ const tokens = dataset.tokens ?? [];
46
+ const components = dataset.components ?? [];
47
+
48
+ // Build component lookup map keyed by name.
49
+ const componentMap = new Map(components.map((c) => [c.name, c]));
50
+
51
+ const diagnostics = [];
52
+
53
+ // --- Token cross-reference rules ---
54
+ for (const token of tokens) {
55
+ const name = token.name;
56
+ // String names (SPEC-017 escape hatch) skip cross-reference checks.
57
+ if (typeof name !== "object" || name === null) continue;
58
+
59
+ const tokenLabel = JSON.stringify(name);
60
+
61
+ if (name.component != null) {
62
+ // SPEC-018: component name must be declared
63
+ if (!componentMap.has(name.component)) {
64
+ diagnostics.push({
65
+ ruleId: "SPEC-018",
66
+ severity: "error",
67
+ message: `Token '${tokenLabel}' references undeclared component '${name.component}'`,
68
+ tokenName: tokenLabel,
69
+ });
70
+ // Can't validate further fields without a component declaration.
71
+ continue;
72
+ }
73
+
74
+ const component = componentMap.get(name.component);
75
+
76
+ // SPEC-019: variant must be in component's variant option enum
77
+ if (name.variant != null) {
78
+ const variantEnum = component.options?.variant?.enum;
79
+ if (Array.isArray(variantEnum) && !variantEnum.includes(name.variant)) {
80
+ diagnostics.push({
81
+ ruleId: "SPEC-019",
82
+ severity: "error",
83
+ message: `Token '${tokenLabel}' has variant '${name.variant}' which is not declared on component '${name.component}'`,
84
+ tokenName: tokenLabel,
85
+ componentName: name.component,
86
+ });
87
+ }
88
+ }
89
+
90
+ // SPEC-020: anatomy must match a declared anatomy part name
91
+ if (name.anatomy != null) {
92
+ const declaredParts = new Set(
93
+ (component.anatomy ?? []).map((p) => p.name),
94
+ );
95
+ if (declaredParts.size > 0 && !declaredParts.has(name.anatomy)) {
96
+ diagnostics.push({
97
+ ruleId: "SPEC-020",
98
+ severity: "error",
99
+ message: `Token '${tokenLabel}' references undeclared anatomy part '${name.anatomy}' on component '${name.component}'`,
100
+ tokenName: tokenLabel,
101
+ componentName: name.component,
102
+ });
103
+ }
104
+ }
105
+
106
+ // SPEC-022: state must match a declared state name (only when states are declared)
107
+ if (name.state != null) {
108
+ const declaredStates = new Set(
109
+ (component.states ?? []).map((s) => s.name),
110
+ );
111
+ if (declaredStates.size > 0 && !declaredStates.has(name.state)) {
112
+ diagnostics.push({
113
+ ruleId: "SPEC-022",
114
+ severity: "error",
115
+ message: `Token '${tokenLabel}' references undeclared state '${name.state}' on component '${name.component}'`,
116
+ tokenName: tokenLabel,
117
+ componentName: name.component,
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // --- Component declaration internal rules ---
125
+ for (const component of components) {
126
+ const cName = component.name;
127
+
128
+ // SPEC-021: custom slot names should have descriptions
129
+ for (const slot of component.slots ?? []) {
130
+ if (!CANONICAL_SLOTS.has(slot.name) && !slot.description) {
131
+ diagnostics.push({
132
+ ruleId: "SPEC-021",
133
+ severity: "warning",
134
+ message: `Component '${cName}' has custom slot '${slot.name}' with no description — add a description or use a canonical slot name`,
135
+ componentName: cName,
136
+ });
137
+ }
138
+ }
139
+
140
+ // SPEC-023: custom anatomy part names should have descriptions
141
+ for (const part of component.anatomy ?? []) {
142
+ if (!CANONICAL_ANATOMY_PARTS.has(part.name) && !part.description) {
143
+ diagnostics.push({
144
+ ruleId: "SPEC-023",
145
+ severity: "warning",
146
+ message: `Component '${cName}' has custom anatomy part '${part.name}' with no description`,
147
+ componentName: cName,
148
+ });
149
+ }
150
+ }
151
+
152
+ // SPEC-024: custom state names should have descriptions
153
+ for (const state of component.states ?? []) {
154
+ if (!CANONICAL_STATES.has(state.name) && !state.description) {
155
+ diagnostics.push({
156
+ ruleId: "SPEC-024",
157
+ severity: "warning",
158
+ message: `Component '${cName}' has custom state '${state.name}' with no description`,
159
+ componentName: cName,
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ return diagnostics;
166
+ }