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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
package/cli/types.ts CHANGED
@@ -38,6 +38,8 @@ export interface BuildOptions extends GlobalOptions {
38
38
  export interface ValidateOptions extends GlobalOptions {
39
39
  strict?: boolean;
40
40
  constraints?: string[];
41
+ 'tokens-path'?: string;
42
+ 'constraints-dir'?: string;
41
43
  perf?: boolean;
42
44
  budgetTotalMs?: number;
43
45
  budgetPerBpMs?: number;
@@ -0,0 +1,40 @@
1
+ import { type TokenNode } from '../core/flatten.js';
2
+ import type { Breakpoint } from '../core/breakpoints.js';
3
+ import { type ConstraintViolation } from './json-output.js';
4
+ import type { DcvConfig } from './types.js';
5
+ export interface ValidateInput {
6
+ /** Inline tokens object (DTCG-style `$value`/`$type`). Takes precedence over `tokensPath`. */
7
+ tokens?: TokenNode;
8
+ /** Path to a tokens file. Used when `tokens` is not provided. */
9
+ tokensPath?: string;
10
+ /** Inline constraints config (the `constraints` block of dcv.config.json). Takes precedence over `configPath`. */
11
+ constraints?: DcvConfig['constraints'];
12
+ /** Path to a config file. When neither `constraints` nor `configPath` is given, a dcv.config.* in cwd is discovered. */
13
+ configPath?: string;
14
+ /** Directory holding order / cross-axis constraint files. Defaults to `themes`. */
15
+ constraintsDir?: string;
16
+ /** Optional breakpoint, selecting `<axis>.<bp>.order.json` / cross-axis variants. */
17
+ breakpoint?: Breakpoint;
18
+ }
19
+ export interface ValidateResult {
20
+ /** True when there are no error-level violations. */
21
+ ok: boolean;
22
+ counts: {
23
+ checked: number;
24
+ violations: number;
25
+ warnings: number;
26
+ };
27
+ violations: ConstraintViolation[];
28
+ warnings: ConstraintViolation[];
29
+ /** Set when tokens were validated but no active constraint referenced any of them. */
30
+ note?: string;
31
+ }
32
+ /**
33
+ * Validate a token set against constraints and return structured results.
34
+ *
35
+ * Unlike the CLI, this treats `tokens` / `tokensPath` as the complete token set
36
+ * (no implicit cwd token overrides), so it is safe to call from a server that
37
+ * cannot share a filesystem with the caller.
38
+ */
39
+ export declare function validate(input?: ValidateInput): ValidateResult;
40
+ //# sourceMappingURL=validate-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-api.d.ts","sourceRoot":"","sources":["validate-api.ts"],"names":[],"mappings":"AAYA,OAAO,EAAiB,KAAK,SAAS,EAAkB,MAAM,oBAAoB,CAAC;AAEnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGzD,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kHAAkH;IAClH,WAAW,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IACvC,wHAAwH;IACxH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAyBD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,GAAE,aAAkB,GAAG,cAAc,CAkDlE"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Programmatic validation API.
3
+ *
4
+ * A thin convenience wrapper over the same flatten + engine + constraint-registry
5
+ * machinery the CLI uses, so library consumers (and the MCP server) get one call
6
+ * that takes tokens + constraints and returns structured violations — no argv, no
7
+ * process.exit, no prose.
8
+ *
9
+ * Re-exported from the package root (`design-constraint-validator`) via
10
+ * `core/index.ts`.
11
+ */
12
+ import fs from 'node:fs';
13
+ import { flattenTokens } from '../core/flatten.js';
14
+ import { Engine } from '../core/engine.js';
15
+ import { loadConfig } from './config.js';
16
+ import { setupConstraints, collectReferencedIds } from './constraint-registry.js';
17
+ import { formatViolation } from './json-output.js';
18
+ function readTokensFile(p) {
19
+ if (!fs.existsSync(p)) {
20
+ throw new Error(`Tokens file not found: ${p}`);
21
+ }
22
+ try {
23
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
24
+ }
25
+ catch (e) {
26
+ const detail = e instanceof Error ? e.message : String(e);
27
+ throw new Error(`Tokens file is not valid JSON: ${p} (${detail})`);
28
+ }
29
+ }
30
+ function resolveConfig(input) {
31
+ if (input.constraints !== undefined) {
32
+ return { constraints: input.constraints };
33
+ }
34
+ const res = loadConfig(input.configPath);
35
+ if (!res.ok) {
36
+ throw new Error(res.error);
37
+ }
38
+ return res.value;
39
+ }
40
+ /**
41
+ * Validate a token set against constraints and return structured results.
42
+ *
43
+ * Unlike the CLI, this treats `tokens` / `tokensPath` as the complete token set
44
+ * (no implicit cwd token overrides), so it is safe to call from a server that
45
+ * cannot share a filesystem with the caller.
46
+ */
47
+ export function validate(input = {}) {
48
+ const tokens = input.tokens !== undefined
49
+ ? input.tokens
50
+ : input.tokensPath !== undefined
51
+ ? readTokensFile(input.tokensPath)
52
+ : {};
53
+ const config = resolveConfig(input);
54
+ const { flat, edges } = flattenTokens(tokens);
55
+ const init = {};
56
+ for (const t of Object.values(flat)) {
57
+ init[t.id] = t.value;
58
+ }
59
+ const engine = new Engine(init, edges);
60
+ const knownIds = new Set(Object.keys(init));
61
+ const sources = setupConstraints(engine, { config, bp: input.breakpoint, constraintsDir: input.constraintsDir ?? 'themes' }, { knownIds });
62
+ const issues = engine.evaluate(knownIds);
63
+ const errors = issues.filter((i) => i.level === 'error');
64
+ const warnings = issues.filter((i) => i.level !== 'error');
65
+ // No-match note: validated tokens that no active constraint references.
66
+ const coverage = collectReferencedIds(sources);
67
+ let matched = false;
68
+ for (const id of coverage.ids) {
69
+ if (knownIds.has(id)) {
70
+ matched = true;
71
+ break;
72
+ }
73
+ }
74
+ const note = knownIds.size > 0 && coverage.coverageKnown && !matched
75
+ ? `No active constraint references any of the ${knownIds.size} validated token(s) — nothing was checked. ` +
76
+ `Define constraints (constraints.wcag / constraints.thresholds) or point constraintsDir at your order/cross-axis files.`
77
+ : undefined;
78
+ return {
79
+ ok: errors.length === 0,
80
+ counts: { checked: issues.length, violations: errors.length, warnings: warnings.length },
81
+ violations: errors.map(formatViolation),
82
+ warnings: warnings.map(formatViolation),
83
+ note,
84
+ };
85
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Programmatic validation API.
3
+ *
4
+ * A thin convenience wrapper over the same flatten + engine + constraint-registry
5
+ * machinery the CLI uses, so library consumers (and the MCP server) get one call
6
+ * that takes tokens + constraints and returns structured violations — no argv, no
7
+ * process.exit, no prose.
8
+ *
9
+ * Re-exported from the package root (`design-constraint-validator`) via
10
+ * `core/index.ts`.
11
+ */
12
+ import fs from 'node:fs';
13
+ import { flattenTokens, type TokenNode, type FlatToken } from '../core/flatten.js';
14
+ import { Engine, type ConstraintIssue } from '../core/engine.js';
15
+ import type { Breakpoint } from '../core/breakpoints.js';
16
+ import { loadConfig } from './config.js';
17
+ import { setupConstraints, collectReferencedIds } from './constraint-registry.js';
18
+ import { formatViolation, type ConstraintViolation } from './json-output.js';
19
+ import type { DcvConfig } from './types.js';
20
+
21
+ export interface ValidateInput {
22
+ /** Inline tokens object (DTCG-style `$value`/`$type`). Takes precedence over `tokensPath`. */
23
+ tokens?: TokenNode;
24
+ /** Path to a tokens file. Used when `tokens` is not provided. */
25
+ tokensPath?: string;
26
+ /** Inline constraints config (the `constraints` block of dcv.config.json). Takes precedence over `configPath`. */
27
+ constraints?: DcvConfig['constraints'];
28
+ /** Path to a config file. When neither `constraints` nor `configPath` is given, a dcv.config.* in cwd is discovered. */
29
+ configPath?: string;
30
+ /** Directory holding order / cross-axis constraint files. Defaults to `themes`. */
31
+ constraintsDir?: string;
32
+ /** Optional breakpoint, selecting `<axis>.<bp>.order.json` / cross-axis variants. */
33
+ breakpoint?: Breakpoint;
34
+ }
35
+
36
+ export interface ValidateResult {
37
+ /** True when there are no error-level violations. */
38
+ ok: boolean;
39
+ counts: { checked: number; violations: number; warnings: number };
40
+ violations: ConstraintViolation[];
41
+ warnings: ConstraintViolation[];
42
+ /** Set when tokens were validated but no active constraint referenced any of them. */
43
+ note?: string;
44
+ }
45
+
46
+ function readTokensFile(p: string): TokenNode {
47
+ if (!fs.existsSync(p)) {
48
+ throw new Error(`Tokens file not found: ${p}`);
49
+ }
50
+ try {
51
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as TokenNode;
52
+ } catch (e) {
53
+ const detail = e instanceof Error ? e.message : String(e);
54
+ throw new Error(`Tokens file is not valid JSON: ${p} (${detail})`);
55
+ }
56
+ }
57
+
58
+ function resolveConfig(input: ValidateInput): DcvConfig {
59
+ if (input.constraints !== undefined) {
60
+ return { constraints: input.constraints };
61
+ }
62
+ const res = loadConfig(input.configPath);
63
+ if (!res.ok) {
64
+ throw new Error(res.error);
65
+ }
66
+ return res.value;
67
+ }
68
+
69
+ /**
70
+ * Validate a token set against constraints and return structured results.
71
+ *
72
+ * Unlike the CLI, this treats `tokens` / `tokensPath` as the complete token set
73
+ * (no implicit cwd token overrides), so it is safe to call from a server that
74
+ * cannot share a filesystem with the caller.
75
+ */
76
+ export function validate(input: ValidateInput = {}): ValidateResult {
77
+ const tokens: TokenNode =
78
+ input.tokens !== undefined
79
+ ? input.tokens
80
+ : input.tokensPath !== undefined
81
+ ? readTokensFile(input.tokensPath)
82
+ : {};
83
+
84
+ const config = resolveConfig(input);
85
+
86
+ const { flat, edges } = flattenTokens(tokens);
87
+ const init: Record<string, string | number> = {};
88
+ for (const t of Object.values(flat)) {
89
+ init[(t as FlatToken).id] = (t as FlatToken).value;
90
+ }
91
+ const engine = new Engine(init, edges);
92
+ const knownIds = new Set(Object.keys(init));
93
+
94
+ const sources = setupConstraints(
95
+ engine,
96
+ { config, bp: input.breakpoint, constraintsDir: input.constraintsDir ?? 'themes' },
97
+ { knownIds },
98
+ );
99
+
100
+ const issues = engine.evaluate(knownIds);
101
+ const errors = issues.filter((i: ConstraintIssue) => i.level === 'error');
102
+ const warnings = issues.filter((i: ConstraintIssue) => i.level !== 'error');
103
+
104
+ // No-match note: validated tokens that no active constraint references.
105
+ const coverage = collectReferencedIds(sources);
106
+ let matched = false;
107
+ for (const id of coverage.ids) {
108
+ if (knownIds.has(id)) {
109
+ matched = true;
110
+ break;
111
+ }
112
+ }
113
+ const note =
114
+ knownIds.size > 0 && coverage.coverageKnown && !matched
115
+ ? `No active constraint references any of the ${knownIds.size} validated token(s) — nothing was checked. ` +
116
+ `Define constraints (constraints.wcag / constraints.thresholds) or point constraintsDir at your order/cross-axis files.`
117
+ : undefined;
118
+
119
+ return {
120
+ ok: errors.length === 0,
121
+ counts: { checked: issues.length, violations: errors.length, warnings: warnings.length },
122
+ violations: errors.map(formatViolation),
123
+ warnings: warnings.map(formatViolation),
124
+ note,
125
+ };
126
+ }
@@ -6,7 +6,13 @@ export declare function loadOrders(axis: string, bp?: Breakpoint): [string, "<="
6
6
  export declare function mergeTokens(base: unknown, overlay: unknown): TokenNode;
7
7
  /** Load tokens with optional breakpoint override: base + overrides/<bp>.json */
8
8
  /**
9
- * Load tokens with override precedence: base < local < breakpoint
9
+ * Load tokens with override precedence: base < local < breakpoint.
10
+ *
11
+ * @param bp optional breakpoint overlay (tokens/overrides/<bp>.json).
12
+ * @param tokensPath explicit base tokens file. When provided, a missing or
13
+ * invalid file throws — callers must never silently validate
14
+ * an empty token set. When omitted, defaults to the repo
15
+ * example file and stays lenient (backward-compatible).
10
16
  */
11
- export declare function loadTokensWithBreakpoint(bp?: Breakpoint): TokenNode;
17
+ export declare function loadTokensWithBreakpoint(bp?: Breakpoint, tokensPath?: string): TokenNode;
12
18
  //# sourceMappingURL=breakpoints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"breakpoints.d.ts","sourceRoot":"","sources":["breakpoints.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAM7D;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,CAKzF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,CAQtE;AAED,gFAAgF;AAChF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,CAAC,EAAE,UAAU,GAAG,SAAS,CAKnE"}
1
+ {"version":3,"file":"breakpoints.d.ts","sourceRoot":"","sources":["breakpoints.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAM7D;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,CAKzF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,CAQtE;AAED,gFAAgF;AAChF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAkBxF"}
@@ -38,10 +38,31 @@ export function mergeTokens(base, overlay) {
38
38
  }
39
39
  /** Load tokens with optional breakpoint override: base + overrides/<bp>.json */
40
40
  /**
41
- * Load tokens with override precedence: base < local < breakpoint
41
+ * Load tokens with override precedence: base < local < breakpoint.
42
+ *
43
+ * @param bp optional breakpoint overlay (tokens/overrides/<bp>.json).
44
+ * @param tokensPath explicit base tokens file. When provided, a missing or
45
+ * invalid file throws — callers must never silently validate
46
+ * an empty token set. When omitted, defaults to the repo
47
+ * example file and stays lenient (backward-compatible).
42
48
  */
43
- export function loadTokensWithBreakpoint(bp) {
44
- const base = loadJsonSafe("tokens/tokens.example.json") ?? {};
49
+ export function loadTokensWithBreakpoint(bp, tokensPath) {
50
+ let base;
51
+ if (tokensPath !== undefined) {
52
+ if (!fs.existsSync(tokensPath)) {
53
+ throw new Error(`Tokens file not found: ${tokensPath}`);
54
+ }
55
+ try {
56
+ base = JSON.parse(fs.readFileSync(tokensPath, "utf8"));
57
+ }
58
+ catch (e) {
59
+ const detail = e instanceof Error ? e.message : String(e);
60
+ throw new Error(`Tokens file is not valid JSON: ${tokensPath} (${detail})`);
61
+ }
62
+ }
63
+ else {
64
+ base = loadJsonSafe("tokens/tokens.example.json") ?? {};
65
+ }
45
66
  const local = loadJsonSafe("tokens/overrides/local.json");
46
67
  const ov = bp ? loadJsonSafe(`tokens/overrides/${bp}.json`) : null;
47
68
  return mergeTokens(mergeTokens(base, local), ov);
@@ -40,10 +40,29 @@ export function mergeTokens(base: unknown, overlay: unknown): TokenNode {
40
40
 
41
41
  /** Load tokens with optional breakpoint override: base + overrides/<bp>.json */
42
42
  /**
43
- * Load tokens with override precedence: base < local < breakpoint
43
+ * Load tokens with override precedence: base < local < breakpoint.
44
+ *
45
+ * @param bp optional breakpoint overlay (tokens/overrides/<bp>.json).
46
+ * @param tokensPath explicit base tokens file. When provided, a missing or
47
+ * invalid file throws — callers must never silently validate
48
+ * an empty token set. When omitted, defaults to the repo
49
+ * example file and stays lenient (backward-compatible).
44
50
  */
45
- export function loadTokensWithBreakpoint(bp?: Breakpoint): TokenNode {
46
- const base = loadJsonSafe<TokenNode>("tokens/tokens.example.json") ?? {};
51
+ export function loadTokensWithBreakpoint(bp?: Breakpoint, tokensPath?: string): TokenNode {
52
+ let base: TokenNode;
53
+ if (tokensPath !== undefined) {
54
+ if (!fs.existsSync(tokensPath)) {
55
+ throw new Error(`Tokens file not found: ${tokensPath}`);
56
+ }
57
+ try {
58
+ base = JSON.parse(fs.readFileSync(tokensPath, "utf8")) as TokenNode;
59
+ } catch (e) {
60
+ const detail = e instanceof Error ? e.message : String(e);
61
+ throw new Error(`Tokens file is not valid JSON: ${tokensPath} (${detail})`);
62
+ }
63
+ } else {
64
+ base = loadJsonSafe<TokenNode>("tokens/tokens.example.json") ?? {};
65
+ }
47
66
  const local = loadJsonSafe<TokenNode>("tokens/overrides/local.json");
48
67
  const ov = bp ? loadJsonSafe<TokenNode>(`tokens/overrides/${bp}.json`) : null;
49
68
  return mergeTokens(mergeTokens(base, local), ov);
package/core/color.js CHANGED
@@ -74,7 +74,7 @@ export function parseCssColor(input) {
74
74
  const C = Math.max(0, num(m[2]));
75
75
  const h = ((num(m[3]) % 360) + 360) % 360;
76
76
  const a = m[4] ? (m[4].includes("%") ? pct(m[4]) : num(m[4])) : 1;
77
- const [r, g, b] = oklchToSrgb(L, C, h).map(v => clamp255(v * 255));
77
+ const [r, g, b] = oklchToLinearSrgb(L, C, h).map(v => linToSrgb(v));
78
78
  return { r, g, b, a: clamp01(a) };
79
79
  }
80
80
  // named "transparent"
@@ -103,8 +103,8 @@ function hslToRgb(H, S, L) {
103
103
  const m = L - C / 2;
104
104
  return { r: clamp255((r + m) * 255), g: clamp255((g + m) * 255), b: clamp255((b + m) * 255), a: 1 };
105
105
  }
106
- /* ---------- OKLCH → sRGB (0..1 channels) ---------- */
107
- function oklchToSrgb(L, C, hDeg) {
106
+ /* ---------- OKLCH → linear sRGB (0..1 channels) ---------- */
107
+ function oklchToLinearSrgb(L, C, hDeg) {
108
108
  const h = (hDeg * Math.PI) / 180;
109
109
  // OKLCH -> OKLab
110
110
  const a = C * Math.cos(h);
@@ -118,7 +118,7 @@ function oklchToSrgb(L, C, hDeg) {
118
118
  const R = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
119
119
  const G = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
120
120
  const B = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
121
- // clamp to [0,1] (gamut clip)
121
+ // clamp to [0,1] (gamut clip). Callers gamma-encode to sRGB.
122
122
  return [clamp01(R), clamp01(G), clamp01(B)];
123
123
  }
124
124
  /* ---------- Alpha compositing (linear light) ---------- */
package/core/color.ts CHANGED
@@ -87,7 +87,7 @@ export function parseCssColor(input: string | undefined | null): RGBA | null {
87
87
  const C = Math.max(0, num(m[2]));
88
88
  const h = ((num(m[3]) % 360) + 360) % 360;
89
89
  const a = m[4] ? (m[4].includes("%") ? pct(m[4]) : num(m[4])) : 1;
90
- const [r, g, b] = oklchToSrgb(L, C, h).map(v => clamp255(v * 255));
90
+ const [r, g, b] = oklchToLinearSrgb(L, C, h).map(v => linToSrgb(v));
91
91
  return { r, g, b, a: clamp01(a) };
92
92
  }
93
93
 
@@ -113,8 +113,8 @@ function hslToRgb(H: number, S: number, L: number): RGBA {
113
113
  return { r: clamp255((r + m) * 255), g: clamp255((g + m) * 255), b: clamp255((b + m) * 255), a: 1 };
114
114
  }
115
115
 
116
- /* ---------- OKLCH → sRGB (0..1 channels) ---------- */
117
- function oklchToSrgb(L: number, C: number, hDeg: number): [number, number, number] {
116
+ /* ---------- OKLCH → linear sRGB (0..1 channels) ---------- */
117
+ function oklchToLinearSrgb(L: number, C: number, hDeg: number): [number, number, number] {
118
118
  const h = (hDeg * Math.PI) / 180;
119
119
  // OKLCH -> OKLab
120
120
  const a = C * Math.cos(h);
@@ -128,7 +128,7 @@ function oklchToSrgb(L: number, C: number, hDeg: number): [number, number, numbe
128
128
  const R = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
129
129
  const G = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
130
130
  const B = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
131
- // clamp to [0,1] (gamut clip)
131
+ // clamp to [0,1] (gamut clip). Callers gamma-encode to sRGB.
132
132
  return [clamp01(R), clamp01(G), clamp01(B)];
133
133
  }
134
134
 
@@ -1 +1 @@
1
- {"version":3,"file":"monotonic-lightness.d.ts","sourceRoot":"","sources":["monotonic-lightness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOxD;AAED,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAElD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAuBpE"}
1
+ {"version":3,"file":"monotonic-lightness.d.ts","sourceRoot":"","sources":["monotonic-lightness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYxD;AAED,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAElD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAuBpE"}
@@ -2,11 +2,15 @@ import { parseCssColor, relativeLuminance } from "../color.js";
2
2
  export function parseLightness(v) {
3
3
  if (typeof v !== "string")
4
4
  return null;
5
- const s = v.trim().toLowerCase();
6
- const m = /^oklch\(\s*([0-9.]+%?)\s+/.exec(s);
7
- if (m)
8
- return m[1].includes("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
9
- const rgba = parseCssColor(s);
5
+ // ONE consistent lightness scale for every format. parseCssColor handles
6
+ // hex / rgb / hsl / oklch (oklch→sRGB is the verified TASK-005 pipeline), so we
7
+ // always compare WCAG relative luminance.
8
+ //
9
+ // BUG FIXED (TASK-009): oklch() previously short-circuited to its raw perceptual
10
+ // L coordinate — a DIFFERENT scale from the relative luminance used for hex. A
11
+ // scale mixing the two formats then compared incomparable numbers (e.g. oklch L
12
+ // 0.60 vs hex luminance 0.216 for the same gray), yielding false pass/fail.
13
+ const rgba = parseCssColor(v.trim());
10
14
  return rgba ? relativeLuminance(rgba) : null;
11
15
  }
12
16
  export function MonotonicLightness(orders) {
@@ -3,10 +3,15 @@ import { parseCssColor, relativeLuminance } from "../color.js";
3
3
 
4
4
  export function parseLightness(v: unknown): number | null {
5
5
  if (typeof v !== "string") return null;
6
- const s = v.trim().toLowerCase();
7
- const m = /^oklch\(\s*([0-9.]+%?)\s+/.exec(s);
8
- if (m) return m[1].includes("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
9
- const rgba = parseCssColor(s);
6
+ // ONE consistent lightness scale for every format. parseCssColor handles
7
+ // hex / rgb / hsl / oklch (oklch→sRGB is the verified TASK-005 pipeline), so we
8
+ // always compare WCAG relative luminance.
9
+ //
10
+ // BUG FIXED (TASK-009): oklch() previously short-circuited to its raw perceptual
11
+ // L coordinate — a DIFFERENT scale from the relative luminance used for hex. A
12
+ // scale mixing the two formats then compared incomparable numbers (e.g. oklch L
13
+ // 0.60 vs hex luminance 0.216 for the same gray), yielding false pass/fail.
14
+ const rgba = parseCssColor(v.trim());
10
15
  return rgba ? relativeLuminance(rgba) : null;
11
16
  }
12
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"wcag.d.ts","sourceRoot":"","sources":["wcag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC7B,CAAC;AASF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,gBAAgB,CAiD1E"}
1
+ {"version":3,"file":"wcag.d.ts","sourceRoot":"","sources":["wcag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC7B,CAAC;AASF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,gBAAgB,CAuD1E"}
@@ -26,6 +26,7 @@ export function WcagContrastPlugin(pairs) {
26
26
  rule: "wcag-contrast",
27
27
  level: "warn",
28
28
  where: p.where,
29
+ involvedTokens: [p.fg, p.bg],
29
30
  message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
30
31
  });
31
32
  continue;
@@ -43,6 +44,11 @@ export function WcagContrastPlugin(pairs) {
43
44
  rule: "wcag-contrast",
44
45
  level: "error",
45
46
  where: p.where,
47
+ involvedTokens: [p.fg, p.bg],
48
+ metadata: {
49
+ actual: Number(ratio.toFixed(2)),
50
+ required: p.min
51
+ },
46
52
  message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
47
53
  });
48
54
  }
@@ -40,6 +40,7 @@ export function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin {
40
40
  rule: "wcag-contrast",
41
41
  level: "warn" as const,
42
42
  where: p.where,
43
+ involvedTokens: [p.fg, p.bg],
43
44
  message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
44
45
  });
45
46
  continue;
@@ -60,6 +61,11 @@ export function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin {
60
61
  rule: "wcag-contrast",
61
62
  level: "error" as const,
62
63
  where: p.where,
64
+ involvedTokens: [p.fg, p.bg],
65
+ metadata: {
66
+ actual: Number(ratio.toFixed(2)),
67
+ required: p.min
68
+ },
63
69
  message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
64
70
  });
65
71
  }
package/core/dtcg.d.ts ADDED
@@ -0,0 +1,38 @@
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
+ export type DtcgColorValue = {
22
+ colorSpace?: string;
23
+ components?: number[];
24
+ alpha?: number;
25
+ hex?: string;
26
+ };
27
+ export type DtcgDimensionValue = {
28
+ value?: number;
29
+ unit?: string;
30
+ };
31
+ /**
32
+ * Normalize a raw DTCG `$value` to the string/number form the engine expects.
33
+ * Strings (including `"{alias}"` references) and numbers pass through untouched.
34
+ * Structured objects are converted; unrecognized objects (composite types such
35
+ * as typography/shadow — out of scope) become a non-crashing sentinel string.
36
+ */
37
+ export declare function normalizeDtcgValue(raw: unknown, type?: string): string | number;
38
+ //# sourceMappingURL=dtcg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dtcg.d.ts","sourceRoot":"","sources":["dtcg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAsDF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAU/E"}