design-constraint-validator 2.0.1 → 2.1.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 (82) hide show
  1. package/README.md +76 -21
  2. package/cli/commands/build.js +1 -1
  3. package/cli/commands/build.ts +1 -1
  4. package/cli/commands/graph.js +2 -2
  5. package/cli/commands/graph.ts +2 -2
  6. package/cli/commands/validate.d.ts.map +1 -1
  7. package/cli/commands/validate.js +40 -6
  8. package/cli/commands/validate.ts +37 -8
  9. package/cli/config-schema.d.ts +39 -0
  10. package/cli/config-schema.d.ts.map +1 -1
  11. package/cli/config-schema.js +4 -2
  12. package/cli/config-schema.ts +4 -2
  13. package/cli/config.d.ts.map +1 -1
  14. package/cli/config.js +8 -2
  15. package/cli/config.ts +8 -2
  16. package/cli/constraint-registry.d.ts +16 -0
  17. package/cli/constraint-registry.d.ts.map +1 -1
  18. package/cli/constraint-registry.js +64 -31
  19. package/cli/constraint-registry.ts +67 -31
  20. package/cli/dcv.js +8 -24
  21. package/cli/dcv.ts +8 -20
  22. package/cli/json-output.d.ts +3 -1
  23. package/cli/json-output.d.ts.map +1 -1
  24. package/cli/json-output.js +11 -4
  25. package/cli/json-output.ts +13 -4
  26. package/cli/types.d.ts +2 -0
  27. package/cli/types.d.ts.map +1 -1
  28. package/cli/types.ts +2 -0
  29. package/cli/validate-api.d.ts +40 -0
  30. package/cli/validate-api.d.ts.map +1 -0
  31. package/cli/validate-api.js +85 -0
  32. package/cli/validate-api.ts +126 -0
  33. package/core/breakpoints.d.ts +8 -2
  34. package/core/breakpoints.d.ts.map +1 -1
  35. package/core/breakpoints.js +24 -3
  36. package/core/breakpoints.ts +22 -3
  37. package/core/color.js +4 -4
  38. package/core/color.ts +4 -4
  39. package/core/constraints/monotonic-lightness.d.ts.map +1 -1
  40. package/core/constraints/monotonic-lightness.js +9 -5
  41. package/core/constraints/monotonic-lightness.ts +9 -4
  42. package/core/constraints/wcag.d.ts.map +1 -1
  43. package/core/constraints/wcag.js +6 -0
  44. package/core/constraints/wcag.ts +6 -0
  45. package/core/dtcg.d.ts +38 -0
  46. package/core/dtcg.d.ts.map +1 -0
  47. package/core/dtcg.js +88 -0
  48. package/core/dtcg.ts +102 -0
  49. package/core/engine.d.ts +6 -0
  50. package/core/engine.d.ts.map +1 -1
  51. package/core/engine.ts +7 -0
  52. package/core/flatten.d.ts +5 -3
  53. package/core/flatten.d.ts.map +1 -1
  54. package/core/flatten.js +24 -10
  55. package/core/flatten.ts +39 -16
  56. package/core/image-export.d.ts.map +1 -1
  57. package/core/image-export.js +10 -7
  58. package/core/image-export.ts +9 -6
  59. package/core/index.d.ts +2 -0
  60. package/core/index.d.ts.map +1 -1
  61. package/core/index.js +4 -0
  62. package/core/index.ts +6 -0
  63. package/core/why.d.ts +1 -1
  64. package/core/why.d.ts.map +1 -1
  65. package/core/why.ts +1 -1
  66. package/mcp/contracts.d.ts +118 -0
  67. package/mcp/contracts.d.ts.map +1 -0
  68. package/mcp/contracts.js +30 -0
  69. package/mcp/contracts.ts +51 -0
  70. package/mcp/index.d.ts +9 -0
  71. package/mcp/index.d.ts.map +1 -0
  72. package/mcp/index.js +32 -0
  73. package/mcp/index.ts +70 -0
  74. package/mcp/tools.d.ts +52 -0
  75. package/mcp/tools.d.ts.map +1 -0
  76. package/mcp/tools.js +172 -0
  77. package/mcp/tools.ts +254 -0
  78. package/package.json +41 -26
  79. package/server.json +21 -0
  80. package/cli/constraints-loader.d.ts.map +0 -1
  81. package/cli/engine-helpers.d.ts.map +0 -1
  82. package/core/cross-axis-config.d.ts.map +0 -1
package/core/dtcg.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * DTCG 2025.10 stable-spec value normalization.
3
+ *
4
+ * The stable spec (2025-10-28) made color and dimension tokens **structured
5
+ * objects** rather than CSS strings — e.g. Figma's native export emits
6
+ * { "colorSpace": "srgb", "components": [0.53, 0.53, 0.53], "alpha": 1, "hex": "#888888" }
7
+ * and
8
+ * { "value": 16, "unit": "px" }.
9
+ *
10
+ * The engine and constraint plugins (WCAG, threshold, monotonic) consume the
11
+ * legacy string/number forms. We normalize structured values to those forms in
12
+ * ONE place — the flatten boundary — so `core/color.ts`'s verified math stays
13
+ * untouched and string aliases (`"{dot.path}"`) keep flowing through unchanged.
14
+ *
15
+ * Non-sRGB color spaces are deliberately NOT coerced into sRGB math: we refuse
16
+ * to treat e.g. display-p3 components as sRGB (that would silently corrupt
17
+ * contrast). Without a `hex` fallback they normalize to a sentinel string that
18
+ * the color parser rejects, producing an explicit "Unparseable color(s)"
19
+ * warning naming the color space — never a wrong-but-silent ratio.
20
+ */
21
+ function toByte(c) {
22
+ return Math.max(0, Math.min(255, Math.round(c * 255)));
23
+ }
24
+ function srgbComponentsToCss(c) {
25
+ if (!Array.isArray(c.components) || c.components.length < 3)
26
+ return null;
27
+ const [r, g, b] = c.components;
28
+ if (![r, g, b].every((n) => typeof n === 'number' && Number.isFinite(n)))
29
+ return null;
30
+ const a = typeof c.alpha === 'number' ? c.alpha : 1;
31
+ const R = toByte(r);
32
+ const G = toByte(g);
33
+ const B = toByte(b);
34
+ if (a >= 1) {
35
+ const hx = (n) => n.toString(16).padStart(2, '0');
36
+ return `#${hx(R)}${hx(G)}${hx(B)}`;
37
+ }
38
+ return `rgba(${R}, ${G}, ${B}, ${a})`;
39
+ }
40
+ function normalizeColor(v) {
41
+ // Prefer the spec's `hex` convenience field when present (works for any space).
42
+ if (typeof v.hex === 'string' && v.hex.trim())
43
+ return v.hex.trim();
44
+ // Only gamma-encoded sRGB components map 1:1 to the existing color math.
45
+ if ((v.colorSpace ?? '').toLowerCase() === 'srgb') {
46
+ const css = srgbComponentsToCss(v);
47
+ if (css)
48
+ return css;
49
+ }
50
+ // Unsupported space with no hex fallback → sentinel the parser rejects.
51
+ return `<unsupported colorSpace: ${v.colorSpace ?? 'unknown'}>`;
52
+ }
53
+ function normalizeDimension(v) {
54
+ if (typeof v.value !== 'number' || !Number.isFinite(v.value)) {
55
+ return '<invalid dimension>';
56
+ }
57
+ const unit = typeof v.unit === 'string' && v.unit ? v.unit : 'px';
58
+ return `${v.value}${unit}`;
59
+ }
60
+ function isColorObject(obj, type) {
61
+ return ((type ?? '').toLowerCase() === 'color' ||
62
+ 'colorSpace' in obj ||
63
+ 'components' in obj ||
64
+ 'hex' in obj);
65
+ }
66
+ function isDimensionObject(obj, type) {
67
+ return (type ?? '').toLowerCase() === 'dimension' || ('value' in obj && 'unit' in obj);
68
+ }
69
+ /**
70
+ * Normalize a raw DTCG `$value` to the string/number form the engine expects.
71
+ * Strings (including `"{alias}"` references) and numbers pass through untouched.
72
+ * Structured objects are converted; unrecognized objects (composite types such
73
+ * as typography/shadow — out of scope) become a non-crashing sentinel string.
74
+ */
75
+ export function normalizeDtcgValue(raw, type) {
76
+ if (typeof raw === 'string' || typeof raw === 'number')
77
+ return raw;
78
+ if (raw && typeof raw === 'object') {
79
+ const obj = raw;
80
+ if (isColorObject(obj, type))
81
+ return normalizeColor(obj);
82
+ if (isDimensionObject(obj, type))
83
+ return normalizeDimension(obj);
84
+ return `<unsupported $value: ${(type ?? 'object').toLowerCase()}>`;
85
+ }
86
+ // null / undefined / boolean — return an empty string so nothing downstream crashes.
87
+ return '';
88
+ }
package/core/dtcg.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * DTCG 2025.10 stable-spec value normalization.
3
+ *
4
+ * The stable spec (2025-10-28) made color and dimension tokens **structured
5
+ * objects** rather than CSS strings — e.g. Figma's native export emits
6
+ * { "colorSpace": "srgb", "components": [0.53, 0.53, 0.53], "alpha": 1, "hex": "#888888" }
7
+ * and
8
+ * { "value": 16, "unit": "px" }.
9
+ *
10
+ * The engine and constraint plugins (WCAG, threshold, monotonic) consume the
11
+ * legacy string/number forms. We normalize structured values to those forms in
12
+ * ONE place — the flatten boundary — so `core/color.ts`'s verified math stays
13
+ * untouched and string aliases (`"{dot.path}"`) keep flowing through unchanged.
14
+ *
15
+ * Non-sRGB color spaces are deliberately NOT coerced into sRGB math: we refuse
16
+ * to treat e.g. display-p3 components as sRGB (that would silently corrupt
17
+ * contrast). Without a `hex` fallback they normalize to a sentinel string that
18
+ * the color parser rejects, producing an explicit "Unparseable color(s)"
19
+ * warning naming the color space — never a wrong-but-silent ratio.
20
+ */
21
+
22
+ export type DtcgColorValue = {
23
+ colorSpace?: string;
24
+ components?: number[];
25
+ alpha?: number;
26
+ hex?: string;
27
+ };
28
+
29
+ export type DtcgDimensionValue = {
30
+ value?: number;
31
+ unit?: string;
32
+ };
33
+
34
+ function toByte(c: number): number {
35
+ return Math.max(0, Math.min(255, Math.round(c * 255)));
36
+ }
37
+
38
+ function srgbComponentsToCss(c: DtcgColorValue): string | null {
39
+ if (!Array.isArray(c.components) || c.components.length < 3) return null;
40
+ const [r, g, b] = c.components;
41
+ if (![r, g, b].every((n) => typeof n === 'number' && Number.isFinite(n))) return null;
42
+ const a = typeof c.alpha === 'number' ? c.alpha : 1;
43
+ const R = toByte(r);
44
+ const G = toByte(g);
45
+ const B = toByte(b);
46
+ if (a >= 1) {
47
+ const hx = (n: number) => n.toString(16).padStart(2, '0');
48
+ return `#${hx(R)}${hx(G)}${hx(B)}`;
49
+ }
50
+ return `rgba(${R}, ${G}, ${B}, ${a})`;
51
+ }
52
+
53
+ function normalizeColor(v: DtcgColorValue): string {
54
+ // Prefer the spec's `hex` convenience field when present (works for any space).
55
+ if (typeof v.hex === 'string' && v.hex.trim()) return v.hex.trim();
56
+ // Only gamma-encoded sRGB components map 1:1 to the existing color math.
57
+ if ((v.colorSpace ?? '').toLowerCase() === 'srgb') {
58
+ const css = srgbComponentsToCss(v);
59
+ if (css) return css;
60
+ }
61
+ // Unsupported space with no hex fallback → sentinel the parser rejects.
62
+ return `<unsupported colorSpace: ${v.colorSpace ?? 'unknown'}>`;
63
+ }
64
+
65
+ function normalizeDimension(v: DtcgDimensionValue): string {
66
+ if (typeof v.value !== 'number' || !Number.isFinite(v.value)) {
67
+ return '<invalid dimension>';
68
+ }
69
+ const unit = typeof v.unit === 'string' && v.unit ? v.unit : 'px';
70
+ return `${v.value}${unit}`;
71
+ }
72
+
73
+ function isColorObject(obj: Record<string, unknown>, type?: string): boolean {
74
+ return (
75
+ (type ?? '').toLowerCase() === 'color' ||
76
+ 'colorSpace' in obj ||
77
+ 'components' in obj ||
78
+ 'hex' in obj
79
+ );
80
+ }
81
+
82
+ function isDimensionObject(obj: Record<string, unknown>, type?: string): boolean {
83
+ return (type ?? '').toLowerCase() === 'dimension' || ('value' in obj && 'unit' in obj);
84
+ }
85
+
86
+ /**
87
+ * Normalize a raw DTCG `$value` to the string/number form the engine expects.
88
+ * Strings (including `"{alias}"` references) and numbers pass through untouched.
89
+ * Structured objects are converted; unrecognized objects (composite types such
90
+ * as typography/shadow — out of scope) become a non-crashing sentinel string.
91
+ */
92
+ export function normalizeDtcgValue(raw: unknown, type?: string): string | number {
93
+ if (typeof raw === 'string' || typeof raw === 'number') return raw;
94
+ if (raw && typeof raw === 'object') {
95
+ const obj = raw as Record<string, unknown>;
96
+ if (isColorObject(obj, type)) return normalizeColor(obj as DtcgColorValue);
97
+ if (isDimensionObject(obj, type)) return normalizeDimension(obj as DtcgDimensionValue);
98
+ return `<unsupported $value: ${(type ?? 'object').toLowerCase()}>`;
99
+ }
100
+ // null / undefined / boolean — return an empty string so nothing downstream crashes.
101
+ return '';
102
+ }
package/core/engine.d.ts CHANGED
@@ -26,6 +26,12 @@ export type ConstraintIssue = {
26
26
  * this captures that reference edge.
27
27
  */
28
28
  involvedEdges?: Array<[TokenId, TokenId]>;
29
+ /**
30
+ * Structured, rule-specific data for JSON consumers.
31
+ * For example, WCAG contrast issues can expose actual/required ratios here
32
+ * without forcing callers to parse human-readable messages.
33
+ */
34
+ metadata?: Record<string, unknown>;
29
35
  };
30
36
  /**
31
37
  * Constraint plugin interface.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;IAE3B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,eAAe,EAAE,CAAC;CACvE,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,OAAO,CAA0B;gBAE7B,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IASrF,GAAG,CAAC,MAAM,EAAE,gBAAgB;IAE5B,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IACxC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;IAElC;;;;;;;OAOG;IACH,SAAS,IAAI,OAAO,EAAE;IAItB;;;;;;;OAOG;IACH,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC;IAI5C,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAatC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC;IAIjC,2FAA2F;IAC3F,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;;;;;CAQtC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;IAE3B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAE1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,eAAe,EAAE,CAAC;CACvE,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,OAAO,CAA0B;gBAE7B,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IASrF,GAAG,CAAC,MAAM,EAAE,gBAAgB;IAE5B,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IACxC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;IAElC;;;;;;;OAOG;IACH,SAAS,IAAI,OAAO,EAAE;IAItB;;;;;;;OAOG;IACH,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC;IAI5C,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAatC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC;IAIjC,2FAA2F;IAC3F,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;;;;;CAQtC"}
package/core/engine.ts CHANGED
@@ -29,6 +29,13 @@ export type ConstraintIssue = {
29
29
  * this captures that reference edge.
30
30
  */
31
31
  involvedEdges?: Array<[TokenId, TokenId]>;
32
+
33
+ /**
34
+ * Structured, rule-specific data for JSON consumers.
35
+ * For example, WCAG contrast issues can expose actual/required ratios here
36
+ * without forcing callers to parse human-readable messages.
37
+ */
38
+ metadata?: Record<string, unknown>;
32
39
  };
33
40
 
34
41
  /**
package/core/flatten.d.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  export type TokenId = string;
2
2
  export type TokenValue = string | number;
3
+ export type DtcgStructuredValue = Record<string, unknown>;
3
4
  export type TokenNode = {
4
5
  $type?: string;
5
- $value?: TokenValue;
6
- [k: string]: TokenNode | string | number | undefined;
6
+ $value?: TokenValue | DtcgStructuredValue;
7
+ $extensions?: DtcgStructuredValue;
8
+ [k: string]: TokenNode | string | number | DtcgStructuredValue | undefined;
7
9
  };
8
10
  export type FlatToken = {
9
11
  id: TokenId;
10
12
  type: string;
11
13
  value: TokenValue;
12
- raw: TokenValue;
14
+ raw: TokenValue | DtcgStructuredValue;
13
15
  refs: TokenId[];
14
16
  };
15
17
  export type FlattenResult = {
@@ -1 +1 @@
1
- {"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["flatten.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AACzC,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC;IAChB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAIF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CA4F5D"}
1
+ {"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["flatten.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAGzC,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1D,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,GAAG,mBAAmB,CAAC;IAC1C,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC5E,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,UAAU,GAAG,mBAAmB,CAAC;IACtC,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAKF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CA4G5D"}
package/core/flatten.js CHANGED
@@ -1,4 +1,6 @@
1
- const REF_RE = /\{([a-z0-9.-]+)\}/gi;
1
+ import { normalizeDtcgValue } from "./dtcg.js";
2
+ const REF_RE = /\{([a-z0-9_.-]+)\}/gi;
3
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2
4
  export function flattenTokens(root) {
3
5
  const flat = {};
4
6
  const edges = [];
@@ -8,13 +10,17 @@ export function flattenTokens(root) {
8
10
  return;
9
11
  if (Object.prototype.hasOwnProperty.call(node, '$value')) {
10
12
  const id = path.join('.');
11
- const raw = node.$value;
12
- if (raw === undefined)
13
+ if (node.$value === undefined)
13
14
  return; // Skip tokens without values
15
+ // Normalize DTCG 2025.10 structured color/dimension objects to the
16
+ // string/number form the engine + plugins expect (strings, incl. aliases,
17
+ // pass through unchanged). Keeps the color math in core/color.ts untouched.
18
+ const raw = node.$value;
19
+ const normalized = normalizeDtcgValue(raw, node.$type);
14
20
  const refs = [];
15
21
  // Find all references in the value
16
- if (typeof raw === 'string') {
17
- const matches = raw.matchAll(REF_RE);
22
+ if (typeof normalized === 'string') {
23
+ const matches = normalized.matchAll(REF_RE);
18
24
  for (const match of matches) {
19
25
  refs.push(match[1]);
20
26
  }
@@ -22,7 +28,7 @@ export function flattenTokens(root) {
22
28
  flat[id] = {
23
29
  id,
24
30
  type: String(node.$type ?? 'unknown'),
25
- value: raw,
31
+ value: normalized,
26
32
  raw,
27
33
  refs
28
34
  };
@@ -44,7 +50,7 @@ export function flattenTokens(root) {
44
50
  // Second pass: resolve references iteratively
45
51
  let changed = true;
46
52
  let iterations = 0;
47
- const maxIterations = Object.keys(flat).length * 2; // Safety limit
53
+ const maxIterations = Object.keys(flat).length * 2 + 1; // Safety limit (never 0)
48
54
  while (changed && iterations < maxIterations) {
49
55
  changed = false;
50
56
  iterations++;
@@ -63,7 +69,7 @@ export function flattenTokens(root) {
63
69
  break;
64
70
  }
65
71
  // Replace the reference with the resolved value
66
- const refPattern = new RegExp(`\\{${refId}\\}`, 'g');
72
+ const refPattern = new RegExp(`\\{${escapeRegExp(refId)}\\}`, 'g');
67
73
  newValue = newValue.replace(refPattern, String(refToken.value));
68
74
  }
69
75
  if (fullyResolved && newValue !== token.value) {
@@ -73,8 +79,16 @@ export function flattenTokens(root) {
73
79
  }
74
80
  }
75
81
  }
76
- if (iterations >= maxIterations) {
77
- throw new Error('Token resolution exceeded maximum iterations - possible circular reference');
82
+ // A token still carrying an unresolved "{ref}" placeholder after the fixpoint
83
+ // means a genuine cycle (a -> b -> a) or a self-reference. An empty or
84
+ // fully-literal token set resolves cleanly and must never trip this guard —
85
+ // the old `iterations >= maxIterations` check threw a bogus "circular
86
+ // reference" error whenever no tokens were found (maxIterations === 0).
87
+ const unresolved = Object.values(flat).filter((t) => typeof t.value === 'string' && t.value.includes('{'));
88
+ if (unresolved.length > 0) {
89
+ throw new Error(`Token resolution exceeded maximum iterations - possible circular reference (unresolved: ${unresolved
90
+ .map((t) => t.id)
91
+ .join(', ')})`);
78
92
  }
79
93
  return { flat, edges };
80
94
  }
package/core/flatten.ts CHANGED
@@ -1,16 +1,22 @@
1
+ import { normalizeDtcgValue } from "./dtcg.js";
2
+
1
3
  export type TokenId = string; // e.g. "color.palette.brand.600"
2
4
  export type TokenValue = string | number;
3
- export type TokenNode = {
4
- $type?: string;
5
- $value?: TokenValue;
6
- [k: string]: TokenNode | string | number | undefined;
5
+ // $value may also be a DTCG 2025.10 structured object (color / dimension); it is
6
+ // normalized to a TokenValue at ingestion. See normalizeDtcgValue.
7
+ export type DtcgStructuredValue = Record<string, unknown>;
8
+ export type TokenNode = {
9
+ $type?: string;
10
+ $value?: TokenValue | DtcgStructuredValue;
11
+ $extensions?: DtcgStructuredValue; // spec passthrough — preserved, never interpreted
12
+ [k: string]: TokenNode | string | number | DtcgStructuredValue | undefined;
7
13
  };
8
14
 
9
15
  export type FlatToken = {
10
16
  id: TokenId;
11
17
  type: string;
12
18
  value: TokenValue; // resolved (if ref)
13
- raw: TokenValue; // original $value
19
+ raw: TokenValue | DtcgStructuredValue; // original $value
14
20
  refs: TokenId[]; // referenced token IDs found in raw
15
21
  };
16
22
 
@@ -19,7 +25,8 @@ export type FlattenResult = {
19
25
  edges: Array<[from: TokenId, to: TokenId]>; // from ref -> to dependent
20
26
  };
21
27
 
22
- const REF_RE = /\{([a-z0-9.-]+)\}/gi;
28
+ const REF_RE = /\{([a-z0-9_.-]+)\}/gi;
29
+ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
30
 
24
31
  export function flattenTokens(root: TokenNode): FlattenResult {
25
32
  const flat: Record<TokenId, FlatToken> = {};
@@ -31,14 +38,18 @@ export function flattenTokens(root: TokenNode): FlattenResult {
31
38
 
32
39
  if (Object.prototype.hasOwnProperty.call(node, '$value')) {
33
40
  const id = path.join('.');
41
+ if (node.$value === undefined) return; // Skip tokens without values
42
+ // Normalize DTCG 2025.10 structured color/dimension objects to the
43
+ // string/number form the engine + plugins expect (strings, incl. aliases,
44
+ // pass through unchanged). Keeps the color math in core/color.ts untouched.
34
45
  const raw = node.$value;
35
- if (raw === undefined) return; // Skip tokens without values
36
-
46
+ const normalized = normalizeDtcgValue(raw, node.$type);
47
+
37
48
  const refs: TokenId[] = [];
38
49
 
39
50
  // Find all references in the value
40
- if (typeof raw === 'string') {
41
- const matches = raw.matchAll(REF_RE);
51
+ if (typeof normalized === 'string') {
52
+ const matches = normalized.matchAll(REF_RE);
42
53
  for (const match of matches) {
43
54
  refs.push(match[1]);
44
55
  }
@@ -47,7 +58,7 @@ export function flattenTokens(root: TokenNode): FlattenResult {
47
58
  flat[id] = {
48
59
  id,
49
60
  type: String(node.$type ?? 'unknown'),
50
- value: raw,
61
+ value: normalized,
51
62
  raw,
52
63
  refs
53
64
  };
@@ -72,8 +83,8 @@ export function flattenTokens(root: TokenNode): FlattenResult {
72
83
  // Second pass: resolve references iteratively
73
84
  let changed = true;
74
85
  let iterations = 0;
75
- const maxIterations = Object.keys(flat).length * 2; // Safety limit
76
-
86
+ const maxIterations = Object.keys(flat).length * 2 + 1; // Safety limit (never 0)
87
+
77
88
  while (changed && iterations < maxIterations) {
78
89
  changed = false;
79
90
  iterations++;
@@ -96,7 +107,7 @@ export function flattenTokens(root: TokenNode): FlattenResult {
96
107
  }
97
108
 
98
109
  // Replace the reference with the resolved value
99
- const refPattern = new RegExp(`\\{${refId}\\}`, 'g');
110
+ const refPattern = new RegExp(`\\{${escapeRegExp(refId)}\\}`, 'g');
100
111
  newValue = newValue.replace(refPattern, String(refToken.value));
101
112
  }
102
113
 
@@ -108,8 +119,20 @@ export function flattenTokens(root: TokenNode): FlattenResult {
108
119
  }
109
120
  }
110
121
 
111
- if (iterations >= maxIterations) {
112
- throw new Error('Token resolution exceeded maximum iterations - possible circular reference');
122
+ // A token still carrying an unresolved "{ref}" placeholder after the fixpoint
123
+ // means a genuine cycle (a -> b -> a) or a self-reference. An empty or
124
+ // fully-literal token set resolves cleanly and must never trip this guard —
125
+ // the old `iterations >= maxIterations` check threw a bogus "circular
126
+ // reference" error whenever no tokens were found (maxIterations === 0).
127
+ const unresolved = Object.values(flat).filter(
128
+ (t) => typeof t.value === 'string' && t.value.includes('{'),
129
+ );
130
+ if (unresolved.length > 0) {
131
+ throw new Error(
132
+ `Token resolution exceeded maximum iterations - possible circular reference (unresolved: ${unresolved
133
+ .map((t) => t.id)
134
+ .join(', ')})`,
135
+ );
113
136
  }
114
137
 
115
138
  return { flat, edges };
@@ -1 +1 @@
1
- {"version":3,"file":"image-export.d.ts","sourceRoot":"","sources":["image-export.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AACrC,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;AAEzC,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EAAe,6BAA6B;AAC7D,OAAO,EAAE,MAAM,EAAiB,wBAAwB;AACxD,GAAG,EAAE,QAAQ,EAAmB,gBAAgB;AAChD,QAAQ,EAAE,QAAQ,GACjB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBhC"}
1
+ {"version":3,"file":"image-export.d.ts","sourceRoot":"","sources":["image-export.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AACrC,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;AAEzC,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EAAe,6BAA6B;AAC7D,OAAO,EAAE,MAAM,EAAiB,wBAAwB;AACxD,GAAG,EAAE,QAAQ,EAAmB,gBAAgB;AAChD,QAAQ,EAAE,QAAQ,GACjB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBhC"}
@@ -5,13 +5,16 @@ import { spawnSync } from "node:child_process";
5
5
  function which(cmd) {
6
6
  const paths = (process.env.PATH || "").split(path.delimiter);
7
7
  for (const p of paths) {
8
- const full = path.join(p, cmd + (process.platform === "win32" ? ".cmd" : ""));
9
- try {
10
- fs.accessSync(full, fs.constants.X_OK);
11
- return full;
12
- }
13
- catch {
14
- // File not accessible, continue to next path
8
+ const names = process.platform === "win32" ? [cmd + ".cmd", cmd + ".exe", cmd] : [cmd];
9
+ for (const name of names) {
10
+ const full = path.join(p, name);
11
+ try {
12
+ fs.accessSync(full, fs.constants.X_OK);
13
+ return full;
14
+ }
15
+ catch {
16
+ // File not accessible, continue to next path
17
+ }
15
18
  }
16
19
  }
17
20
  return null;
@@ -6,12 +6,15 @@ import { spawnSync } from "node:child_process";
6
6
  function which(cmd: string): string | null {
7
7
  const paths = (process.env.PATH || "").split(path.delimiter);
8
8
  for (const p of paths) {
9
- const full = path.join(p, cmd + (process.platform === "win32" ? ".cmd" : ""));
10
- try {
11
- fs.accessSync(full, fs.constants.X_OK);
12
- return full;
13
- } catch {
14
- // File not accessible, continue to next path
9
+ const names = process.platform === "win32" ? [cmd + ".cmd", cmd + ".exe", cmd] : [cmd];
10
+ for (const name of names) {
11
+ const full = path.join(p, name);
12
+ try {
13
+ fs.accessSync(full, fs.constants.X_OK);
14
+ return full;
15
+ } catch {
16
+ // File not accessible, continue to next path
17
+ }
15
18
  }
16
19
  }
17
20
  return null;
package/core/index.d.ts CHANGED
@@ -7,4 +7,6 @@
7
7
  export type { TokenId, TokenValue } from "./flatten.js";
8
8
  export type { ConstraintIssue, ConstraintPlugin, Graph } from "./engine.js";
9
9
  export { Engine } from "./engine.js";
10
+ export { validate } from "../cli/validate-api.js";
11
+ export type { ValidateInput, ValidateResult } from "../cli/validate-api.js";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAKrC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC"}
package/core/index.js CHANGED
@@ -5,3 +5,7 @@
5
5
  * See engine.ts for full implementation with Phase 3C enhancements.
6
6
  */
7
7
  export { Engine } from "./engine.js";
8
+ // Programmatic convenience API (validate tokens against constraints in one call).
9
+ // Implemented in the CLI layer atop the shared registry; surfaced here as the
10
+ // package's public entry point.
11
+ export { validate } from "../cli/validate-api.js";
package/core/index.ts CHANGED
@@ -8,3 +8,9 @@
8
8
  export type { TokenId, TokenValue } from "./flatten.js";
9
9
  export type { ConstraintIssue, ConstraintPlugin, Graph } from "./engine.js";
10
10
  export { Engine } from "./engine.js";
11
+
12
+ // Programmatic convenience API (validate tokens against constraints in one call).
13
+ // Implemented in the CLI layer atop the shared registry; surfaced here as the
14
+ // package's public entry point.
15
+ export { validate } from "../cli/validate-api.js";
16
+ export type { ValidateInput, ValidateResult } from "../cli/validate-api.js";
package/core/why.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { FlatToken } from "./flatten.js";
2
2
  export type WhyReport = {
3
3
  id: string;
4
4
  value: string | number | undefined;
5
- raw?: string | number;
5
+ raw?: unknown;
6
6
  refs?: string[];
7
7
  provenance: "base" | "theme" | "override" | "unknown";
8
8
  dependsOn: string[];
package/core/why.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAQxF;AAED,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC/B,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,EAC7B,MAAM,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GACpF,SAAS,CAkCX"}
1
+ {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACnC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAQxF;AAED,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC/B,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,EAC7B,MAAM,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GACpF,SAAS,CAkCX"}
package/core/why.ts CHANGED
@@ -3,7 +3,7 @@ import type { FlatToken } from "./flatten.js";
3
3
  export type WhyReport = {
4
4
  id: string;
5
5
  value: string | number | undefined;
6
- raw?: string | number;
6
+ raw?: unknown;
7
7
  refs?: string[];
8
8
  provenance: "base" | "theme" | "override" | "unknown";
9
9
  dependsOn: string[]; // immediate refs (parents)