@adobe/design-data-spec 0.1.1 → 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 (46) hide show
  1. package/README.md +3 -3
  2. package/components/button.json +70 -0
  3. package/conformance/README.md +0 -1
  4. package/conformance/invalid/SPEC-016/expected-errors.json +10 -0
  5. package/conformance/invalid/SPEC-016/tokens.tokens.json +8 -0
  6. package/conformance/invalid/SPEC-017/expected-errors.json +10 -0
  7. package/conformance/invalid/SPEC-017/tokens.tokens.json +7 -0
  8. package/conformance/invalid/SPEC-018/dataset.json +9 -0
  9. package/conformance/invalid/SPEC-018/expected-errors.json +10 -0
  10. package/conformance/invalid/SPEC-019/dataset.json +29 -0
  11. package/conformance/invalid/SPEC-019/expected-errors.json +10 -0
  12. package/conformance/invalid/SPEC-020/dataset.json +27 -0
  13. package/conformance/invalid/SPEC-020/expected-errors.json +10 -0
  14. package/conformance/invalid/SPEC-021/dataset.json +18 -0
  15. package/conformance/invalid/SPEC-021/expected-errors.json +10 -0
  16. package/conformance/invalid/SPEC-022/dataset.json +33 -0
  17. package/conformance/invalid/SPEC-022/expected-errors.json +10 -0
  18. package/conformance/invalid/SPEC-023/dataset.json +18 -0
  19. package/conformance/invalid/SPEC-023/expected-errors.json +10 -0
  20. package/conformance/invalid/SPEC-024/dataset.json +18 -0
  21. package/conformance/invalid/SPEC-024/expected-errors.json +10 -0
  22. package/conformance/valid/component-refs/dataset.json +63 -0
  23. package/conformance/valid/composite-drop-shadow.json +14 -0
  24. package/conformance/valid/composite-typography-scale.json +6 -0
  25. package/conformance/valid/composite-typography.json +12 -0
  26. package/conformance/valid/string-name-escape-hatch.json +7 -0
  27. package/fields/scaleIndex.json +15 -0
  28. package/package.json +18 -6
  29. package/rules/rules.yaml +113 -5
  30. package/schemas/anatomy-part.schema.json +35 -0
  31. package/schemas/component.schema.json +267 -0
  32. package/schemas/field.schema.json +2 -2
  33. package/schemas/state-declaration.schema.json +36 -0
  34. package/schemas/token.schema.json +26 -10
  35. package/schemas/value-types/drop-shadow.schema.json +20 -0
  36. package/schemas/value-types/typography-scale.schema.json +13 -0
  37. package/schemas/value-types/typography.schema.json +16 -0
  38. package/spec/agent-surface.md +116 -0
  39. package/spec/anatomy-format.md +167 -0
  40. package/spec/component-format.md +326 -0
  41. package/spec/evolution.md +0 -1
  42. package/spec/index.md +27 -21
  43. package/spec/state-model.md +245 -0
  44. package/spec/token-format.md +80 -15
  45. package/src/canonical.js +61 -0
  46. package/src/validate.js +166 -0
@@ -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
+ }