design-constraint-validator 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +89 -23
  2. package/cli/commands/build.d.ts.map +1 -1
  3. package/cli/commands/build.js +32 -24
  4. package/cli/commands/build.ts +26 -17
  5. package/cli/commands/graph.d.ts.map +1 -1
  6. package/cli/commands/graph.js +35 -18
  7. package/cli/commands/graph.ts +30 -17
  8. package/cli/commands/patch-apply.d.ts.map +1 -1
  9. package/cli/commands/patch-apply.js +4 -1
  10. package/cli/commands/patch-apply.ts +4 -1
  11. package/cli/commands/set.d.ts.map +1 -1
  12. package/cli/commands/set.js +18 -19
  13. package/cli/commands/set.ts +19 -19
  14. package/cli/commands/utils.d.ts +1 -0
  15. package/cli/commands/utils.d.ts.map +1 -1
  16. package/cli/commands/utils.js +20 -1
  17. package/cli/commands/utils.ts +23 -1
  18. package/cli/commands/validate.d.ts.map +1 -1
  19. package/cli/commands/validate.js +45 -23
  20. package/cli/commands/validate.ts +47 -26
  21. package/cli/commands/why.d.ts.map +1 -1
  22. package/cli/commands/why.js +22 -10
  23. package/cli/commands/why.ts +20 -9
  24. package/cli/config-schema.d.ts +171 -166
  25. package/cli/config-schema.d.ts.map +1 -1
  26. package/cli/config-schema.js +29 -7
  27. package/cli/config-schema.ts +31 -7
  28. package/cli/config.d.ts.map +1 -1
  29. package/cli/config.js +8 -2
  30. package/cli/config.ts +8 -2
  31. package/cli/constraint-registry.d.ts +16 -0
  32. package/cli/constraint-registry.d.ts.map +1 -1
  33. package/cli/constraint-registry.js +115 -44
  34. package/cli/constraint-registry.ts +118 -47
  35. package/cli/cross-axis-loader.d.ts +62 -0
  36. package/cli/cross-axis-loader.d.ts.map +1 -1
  37. package/cli/cross-axis-loader.js +186 -31
  38. package/cli/cross-axis-loader.ts +199 -24
  39. package/cli/dcv.js +31 -25
  40. package/cli/dcv.ts +31 -21
  41. package/cli/json-output.d.ts +3 -1
  42. package/cli/json-output.d.ts.map +1 -1
  43. package/cli/json-output.js +11 -4
  44. package/cli/json-output.ts +13 -4
  45. package/cli/types.d.ts +21 -9
  46. package/cli/types.d.ts.map +1 -1
  47. package/cli/types.ts +25 -10
  48. package/cli/validate-api.d.ts +40 -0
  49. package/cli/validate-api.d.ts.map +1 -0
  50. package/cli/validate-api.js +90 -0
  51. package/cli/validate-api.ts +131 -0
  52. package/core/breakpoints.d.ts +8 -2
  53. package/core/breakpoints.d.ts.map +1 -1
  54. package/core/breakpoints.js +24 -3
  55. package/core/breakpoints.ts +22 -3
  56. package/core/color.js +4 -4
  57. package/core/color.ts +4 -4
  58. package/core/constraints/cross-axis.d.ts.map +1 -1
  59. package/core/constraints/cross-axis.js +37 -9
  60. package/core/constraints/cross-axis.ts +37 -9
  61. package/core/constraints/monotonic-lightness.d.ts.map +1 -1
  62. package/core/constraints/monotonic-lightness.js +9 -5
  63. package/core/constraints/monotonic-lightness.ts +9 -4
  64. package/core/constraints/monotonic.d.ts.map +1 -1
  65. package/core/constraints/monotonic.js +32 -8
  66. package/core/constraints/monotonic.ts +29 -8
  67. package/core/constraints/threshold.d.ts.map +1 -1
  68. package/core/constraints/threshold.js +24 -4
  69. package/core/constraints/threshold.ts +23 -4
  70. package/core/constraints/wcag.d.ts.map +1 -1
  71. package/core/constraints/wcag.js +7 -1
  72. package/core/constraints/wcag.ts +7 -1
  73. package/core/dtcg.d.ts +38 -0
  74. package/core/dtcg.d.ts.map +1 -0
  75. package/core/dtcg.js +88 -0
  76. package/core/dtcg.ts +102 -0
  77. package/core/engine.d.ts +6 -0
  78. package/core/engine.d.ts.map +1 -1
  79. package/core/engine.ts +7 -0
  80. package/core/flatten.d.ts +5 -3
  81. package/core/flatten.d.ts.map +1 -1
  82. package/core/flatten.js +32 -10
  83. package/core/flatten.ts +48 -16
  84. package/core/image-export.d.ts.map +1 -1
  85. package/core/image-export.js +10 -7
  86. package/core/image-export.ts +9 -6
  87. package/core/index.d.ts +2 -0
  88. package/core/index.d.ts.map +1 -1
  89. package/core/index.js +4 -0
  90. package/core/index.ts +6 -0
  91. package/core/poset.d.ts +6 -1
  92. package/core/poset.d.ts.map +1 -1
  93. package/core/poset.js +7 -2
  94. package/core/poset.ts +7 -2
  95. package/core/why.d.ts +1 -1
  96. package/core/why.d.ts.map +1 -1
  97. package/core/why.ts +1 -1
  98. package/mcp/contracts.d.ts +1561 -0
  99. package/mcp/contracts.d.ts.map +1 -0
  100. package/mcp/contracts.js +74 -0
  101. package/mcp/contracts.ts +105 -0
  102. package/mcp/index.d.ts +11 -0
  103. package/mcp/index.d.ts.map +1 -0
  104. package/mcp/index.js +35 -0
  105. package/mcp/index.ts +97 -0
  106. package/mcp/insights.d.ts +94 -0
  107. package/mcp/insights.d.ts.map +1 -0
  108. package/mcp/insights.js +445 -0
  109. package/mcp/insights.ts +541 -0
  110. package/mcp/tools.d.ts +63 -0
  111. package/mcp/tools.d.ts.map +1 -0
  112. package/mcp/tools.js +299 -0
  113. package/mcp/tools.ts +431 -0
  114. package/package.json +36 -26
  115. package/server.json +21 -0
  116. package/cli/constraints-loader.d.ts.map +0 -1
  117. package/cli/engine-helpers.d.ts.map +0 -1
  118. package/core/cross-axis-config.d.ts.map +0 -1
@@ -1,9 +1,8 @@
1
- import { join } from 'node:path';
2
- import { readFileSync, existsSync } from 'node:fs';
3
1
  import { loadConfig } from '../config.js';
4
2
  import { Engine } from '../../core/engine.js';
5
3
  import { flattenTokens } from '../../core/flatten.js';
6
- import { loadTokens, outputResult } from './utils.js';
4
+ import { mergeTokens } from '../../core/breakpoints.js';
5
+ import { loadThemeTokens, loadTokens, outputResult } from './utils.js';
7
6
  import { setupConstraints } from '../constraint-registry.js';
8
7
  // Lightweight suggestion helpers (kept local – why command uses core formatter instead)
9
8
  function levenshtein(a, b) {
@@ -81,7 +80,10 @@ export async function setCommand(options) {
81
80
  process.exit(2);
82
81
  }
83
82
  const config = cfgRes.value;
84
- const tokens = loadTokens(tokensPath);
83
+ let tokens = loadTokens(tokensPath);
84
+ if (options.theme) {
85
+ tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
86
+ }
85
87
  // Create engine with flattened tokens
86
88
  const { flat, edges } = flattenTokens(tokens);
87
89
  const init = {};
@@ -103,18 +105,6 @@ export async function setCommand(options) {
103
105
  process.exit(1);
104
106
  }
105
107
  }
106
- if (options.theme) {
107
- const themePath = join('tokens/themes', `${options.theme}.json`);
108
- if (existsSync(themePath)) {
109
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
110
- for (const [id, value] of Object.entries(themeTokens)) {
111
- engine.commit(id, value);
112
- }
113
- }
114
- else {
115
- console.warn(`Theme file not found: ${themePath}`);
116
- }
117
- }
118
108
  let finalResult = {};
119
109
  function setDeep(obj, parts, v) {
120
110
  let cur = obj;
@@ -210,7 +200,7 @@ export async function setCommand(options) {
210
200
  for (const e of entries)
211
201
  console.log(' ', e);
212
202
  }
213
- const dryRun = process.argv.includes('--dry-run');
203
+ const dryRun = !!(options['dry-run'] ?? options.dryRun);
214
204
  for (const { id, value, unset } of entries) {
215
205
  if (unset) {
216
206
  if (!options.quiet)
@@ -227,7 +217,8 @@ export async function setCommand(options) {
227
217
  }
228
218
  }
229
219
  const format = options.format || 'json';
230
- outputResult(finalResult, format, options.output);
220
+ // Dry-run prints the patch to stdout instead of writing --output (no file writes).
221
+ outputResult(finalResult, format, dryRun ? undefined : options.output);
231
222
  if (options.write && !dryRun) {
232
223
  const path = 'tokens/overrides/local.json';
233
224
  let local = {};
@@ -264,9 +255,17 @@ export async function setCommand(options) {
264
255
  }
265
256
  Object.assign(finalResult, result.patch);
266
257
  }
258
+ const dryRun = !!(options['dry-run'] ?? options.dryRun);
267
259
  const format = options.format || 'json';
268
- outputResult(finalResult, format, options.output);
260
+ // Dry-run prints the patch to stdout instead of writing --output (no file writes).
261
+ outputResult(finalResult, format, dryRun ? undefined : options.output);
269
262
  if (options.write || (options.unset && options.unset.length)) {
263
+ // --dry-run must not touch the filesystem (it previously persisted the
264
+ // override file on the positional path; the batch path already guarded it).
265
+ if (dryRun) {
266
+ console.log('Dry-run: changes not written');
267
+ return;
268
+ }
270
269
  const fs = await import('node:fs');
271
270
  const path = 'tokens/overrides/local.json';
272
271
  let local = {};
@@ -1,10 +1,9 @@
1
- import { join } from 'node:path';
2
- import { readFileSync, existsSync } from 'node:fs';
3
1
  import { loadConfig } from '../config.js';
4
2
  import { Engine } from '../../core/engine.js';
5
3
  import { flattenTokens, type FlatToken } from '../../core/flatten.js';
4
+ import { mergeTokens } from '../../core/breakpoints.js';
6
5
  import type { OverridesTree, SetOptions, ValuesPatch } from '../types.js';
7
- import { loadTokens, outputResult } from './utils.js';
6
+ import { loadThemeTokens, loadTokens, outputResult } from './utils.js';
8
7
  import { setupConstraints } from '../constraint-registry.js';
9
8
 
10
9
  // Lightweight suggestion helpers (kept local – why command uses core formatter instead)
@@ -76,7 +75,11 @@ export async function setCommand(options: SetOptions): Promise<void> {
76
75
  const cfgRes = loadConfig(options.config);
77
76
  if (!cfgRes.ok) { console.error(cfgRes.error); process.exit(2); }
78
77
  const config = cfgRes.value;
79
- const tokens = loadTokens(tokensPath);
78
+ let tokens = loadTokens(tokensPath);
79
+
80
+ if (options.theme) {
81
+ tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
82
+ }
80
83
 
81
84
  // Create engine with flattened tokens
82
85
  const { flat, edges } = flattenTokens(tokens);
@@ -105,18 +108,6 @@ export async function setCommand(options: SetOptions): Promise<void> {
105
108
  }
106
109
  }
107
110
 
108
- if (options.theme) {
109
- const themePath = join('tokens/themes', `${options.theme}.json`);
110
- if (existsSync(themePath)) {
111
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
112
- for (const [id, value] of Object.entries(themeTokens)) {
113
- engine.commit(id, value as string | number);
114
- }
115
- } else {
116
- console.warn(`Theme file not found: ${themePath}`);
117
- }
118
- }
119
-
120
111
  let finalResult: ValuesPatch = {};
121
112
 
122
113
  function setDeep(obj: OverridesTree, parts: string[], v: unknown) {
@@ -183,7 +174,7 @@ export async function setCommand(options: SetOptions): Promise<void> {
183
174
  for (const ent of entries) ensureKnownOrSuggest(ent.id);
184
175
  const debug = process.argv.includes('--debug-set') || process.env.DCV_DEBUG_SET === '1';
185
176
  if (debug) { console.log('[set:batch] parsed entries:'); for (const e of entries) console.log(' ', e); }
186
- const dryRun = process.argv.includes('--dry-run');
177
+ const dryRun = !!(options['dry-run'] ?? options.dryRun);
187
178
  for (const { id, value, unset } of entries) {
188
179
  if (unset) { if (!options.quiet) console.log(`preview: unset ${id}`); }
189
180
  else {
@@ -193,7 +184,8 @@ export async function setCommand(options: SetOptions): Promise<void> {
193
184
  }
194
185
  }
195
186
  const format = options.format || 'json';
196
- outputResult(finalResult, format, options.output);
187
+ // Dry-run prints the patch to stdout instead of writing --output (no file writes).
188
+ outputResult(finalResult, format, dryRun ? undefined : options.output);
197
189
  if (options.write && !dryRun) {
198
190
  const path = 'tokens/overrides/local.json';
199
191
  let local: OverridesTree = {} as OverridesTree;
@@ -218,9 +210,17 @@ export async function setCommand(options: SetOptions): Promise<void> {
218
210
  }
219
211
  Object.assign(finalResult, result.patch);
220
212
  }
213
+ const dryRun = !!(options['dry-run'] ?? options.dryRun);
221
214
  const format = options.format || 'json';
222
- outputResult(finalResult, format, options.output);
215
+ // Dry-run prints the patch to stdout instead of writing --output (no file writes).
216
+ outputResult(finalResult, format, dryRun ? undefined : options.output);
223
217
  if (options.write || (options.unset && options.unset.length)) {
218
+ // --dry-run must not touch the filesystem (it previously persisted the
219
+ // override file on the positional path; the batch path already guarded it).
220
+ if (dryRun) {
221
+ console.log('Dry-run: changes not written');
222
+ return;
223
+ }
224
224
  const fs = await import('node:fs');
225
225
  const path = 'tokens/overrides/local.json';
226
226
  let local: OverridesTree = {} as OverridesTree;
@@ -1,4 +1,5 @@
1
1
  import type { TokenNode } from '../../core/flatten.js';
2
2
  export declare function loadTokens(tokensPath: string): TokenNode;
3
+ export declare function loadThemeTokens(theme: string): TokenNode;
3
4
  export declare function outputResult(data: unknown, format: string, outputPath?: string): void;
4
5
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAInE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAUxD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CA8BrF"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAInE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAUxD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAoBxD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CA8BrF"}
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { resolve, dirname } from 'node:path';
2
+ import { resolve, dirname, join } from 'node:path';
3
3
  import { valuesToCss } from '../../adapters/css.js';
4
4
  // Shared helpers for command modules
5
5
  export function loadTokens(tokensPath) {
@@ -13,6 +13,25 @@ export function loadTokens(tokensPath) {
13
13
  }
14
14
  return data;
15
15
  }
16
+ export function loadThemeTokens(theme) {
17
+ const themePath = join('tokens/themes', `${theme}.json`);
18
+ if (!existsSync(themePath)) {
19
+ throw new Error(`Theme file not found: ${themePath}`);
20
+ }
21
+ let data;
22
+ try {
23
+ data = JSON.parse(readFileSync(themePath, 'utf8'));
24
+ }
25
+ catch (e) {
26
+ const detail = e instanceof Error ? e.message : String(e);
27
+ throw new Error(`Theme file is not valid JSON: ${themePath} (${detail})`);
28
+ }
29
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
30
+ const got = data === null ? 'null' : Array.isArray(data) ? 'array' : typeof data;
31
+ throw new Error(`Theme file must contain a JSON object: ${themePath} (got ${got})`);
32
+ }
33
+ return data;
34
+ }
16
35
  export function outputResult(data, format, outputPath) {
17
36
  let content;
18
37
  switch (format) {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { resolve, dirname } from 'node:path';
2
+ import { resolve, dirname, join } from 'node:path';
3
3
  import { valuesToCss } from '../../adapters/css.js';
4
4
  import type { TokenNode, TokenValue } from '../../core/flatten.js';
5
5
 
@@ -17,6 +17,28 @@ export function loadTokens(tokensPath: string): TokenNode {
17
17
  return data as TokenNode;
18
18
  }
19
19
 
20
+ export function loadThemeTokens(theme: string): TokenNode {
21
+ const themePath = join('tokens/themes', `${theme}.json`);
22
+ if (!existsSync(themePath)) {
23
+ throw new Error(`Theme file not found: ${themePath}`);
24
+ }
25
+
26
+ let data: unknown;
27
+ try {
28
+ data = JSON.parse(readFileSync(themePath, 'utf8'));
29
+ } catch (e) {
30
+ const detail = e instanceof Error ? e.message : String(e);
31
+ throw new Error(`Theme file is not valid JSON: ${themePath} (${detail})`);
32
+ }
33
+
34
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
35
+ const got = data === null ? 'null' : Array.isArray(data) ? 'array' : typeof data;
36
+ throw new Error(`Theme file must contain a JSON object: ${themePath} (got ${got})`);
37
+ }
38
+
39
+ return data as TokenNode;
40
+ }
41
+
20
42
  export function outputResult(data: unknown, format: string, outputPath?: string): void {
21
43
  let content: string;
22
44
  switch (format) {
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAOnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuK9E"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAOnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA4L9E"}
@@ -3,10 +3,10 @@ import { Engine } from '../../core/engine.js';
3
3
  import { loadConfig } from '../config.js';
4
4
  import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens } from '../../core/breakpoints.js';
5
5
  import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
6
- import { readFileSync, existsSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { setupConstraints } from '../constraint-registry.js';
6
+ import { readFileSync } from 'node:fs';
7
+ import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
9
8
  import { printVersionBanner } from '../version-banner.js';
9
+ import { loadThemeTokens } from './utils.js';
10
10
  export async function validateCommand(_options) {
11
11
  // Show version banner (subtle, dimmed)
12
12
  printVersionBanner({ quiet: _options.format === 'json' });
@@ -17,11 +17,22 @@ export async function validateCommand(_options) {
17
17
  let anyErrors = false;
18
18
  let totalErrors = 0;
19
19
  let totalWarnings = 0;
20
- const argv = process.argv.slice(2);
21
- const failOnIdx = argv.indexOf('--fail-on');
22
- const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
23
- const sumIdx = argv.indexOf('--summary');
24
- const summaryFmt = _options.summary ?? (sumIdx >= 0 ? argv[sumIdx + 1] : 'none');
20
+ // Coverage tracking for the no-match note (never silently "pass" a file whose
21
+ // tokens are referenced by no active constraint).
22
+ let anyConstraintMatched = false;
23
+ let coverageKnownAll = true;
24
+ let tokenCount = 0;
25
+ // Reconcile the tokens path: the positional `[tokens-path]` is an alias for
26
+ // --tokens. The flag wins; warn on a genuine mismatch. undefined => loader default.
27
+ const flagTokens = _options.tokens;
28
+ const posTokens = _options['tokens-path'];
29
+ if (flagTokens && posTokens && flagTokens !== posTokens) {
30
+ console.error(`warning: both --tokens (${flagTokens}) and positional tokens path (${posTokens}) provided; using --tokens`);
31
+ }
32
+ const tokensPath = flagTokens ?? posTokens;
33
+ const constraintsDir = _options['constraints-dir'] ?? 'themes';
34
+ const failOn = (_options['fail-on'] ?? _options.failOn) ?? 'error';
35
+ const summaryFmt = _options.summary ?? 'none';
25
36
  const outputFormat = _options.format ?? 'text';
26
37
  // Collect all issues for JSON output
27
38
  const allErrors = [];
@@ -55,19 +66,10 @@ export async function validateCommand(_options) {
55
66
  const tStartTotal = globalThis.performance.now();
56
67
  for (const bp of plan) {
57
68
  const tStart = globalThis.performance.now();
58
- let tokens = loadTokensWithBreakpoint(bp);
69
+ let tokens = loadTokensWithBreakpoint(bp, tokensPath);
59
70
  // Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
60
71
  if (_options.theme) {
61
- const themePath = join('tokens/themes', `${_options.theme}.json`);
62
- if (existsSync(themePath)) {
63
- try {
64
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
65
- tokens = mergeTokens(tokens, themeTokens);
66
- }
67
- catch {
68
- // If theme file is invalid JSON, ignore and proceed with base tokens
69
- }
70
- }
72
+ tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
71
73
  }
72
74
  // Create engine with flattened tokens
73
75
  const { flat, edges } = flattenTokens(tokens);
@@ -78,7 +80,20 @@ export async function validateCommand(_options) {
78
80
  const engine = new Engine(init, edges);
79
81
  const knownIds = new Set(Object.keys(init));
80
82
  // Discover and attach all constraints via centralized registry
81
- setupConstraints(engine, { config, bp, constraintsDir: 'themes' }, { knownIds, crossAxisDebug });
83
+ const sources = setupConstraints(engine, { config, bp, constraintsDir }, { knownIds, crossAxisDebug });
84
+ // Track whether any active constraint actually references a token in this file.
85
+ const coverage = collectReferencedIds(sources);
86
+ if (!coverage.coverageKnown)
87
+ coverageKnownAll = false;
88
+ tokenCount = Math.max(tokenCount, knownIds.size);
89
+ if (!anyConstraintMatched) {
90
+ for (const id of coverage.ids) {
91
+ if (knownIds.has(id)) {
92
+ anyConstraintMatched = true;
93
+ break;
94
+ }
95
+ }
96
+ }
82
97
  const allIds = new Set(Object.keys(init));
83
98
  const issues = engine.evaluate(allIds);
84
99
  const errs = issues.filter((i) => i.level === 'error');
@@ -104,6 +119,14 @@ export async function validateCommand(_options) {
104
119
  }
105
120
  }
106
121
  const totalMs = globalThis.performance.now() - tStartTotal;
122
+ // No-match note: tokens were validated but no active constraint referenced any
123
+ // of them. Surfaced loudly (stderr + JSON `note`) so a foreign file can never
124
+ // silently report "0 errors" when nothing was actually checked.
125
+ const noMatchNote = (tokenCount > 0 && coverageKnownAll && !anyConstraintMatched)
126
+ ? `No active constraint references any of the ${tokenCount} validated token(s) — nothing was checked. Define constraints in dcv.config.json (constraints.wcag / constraints.thresholds) or point --constraints-dir at your order/cross-axis files.`
127
+ : undefined;
128
+ if (noMatchNote)
129
+ console.error(`note: ${noMatchNote}`);
107
130
  // Append aggregate total row if multiple scopes and not already added
108
131
  if (rows.length > 1) {
109
132
  const agg = rows.reduce((a, b) => ({ rules: a.rules + b.rules, warnings: a.warnings + b.warnings, errors: a.errors + b.errors }), { rules: 0, warnings: 0, errors: 0 });
@@ -122,11 +145,10 @@ export async function validateCommand(_options) {
122
145
  }
123
146
  // Handle JSON output mode
124
147
  if (outputFormat === 'json') {
125
- const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
148
+ const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion, noMatchNote);
126
149
  // If receipt requested, generate full receipt
127
150
  if (_options.receipt) {
128
- const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
129
- const constraintsDir = 'themes';
151
+ const tokensFile = tokensPath ?? 'tokens/tokens.example.json';
130
152
  const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
131
153
  writeJsonOutput(receipt, _options.receipt);
132
154
  }
@@ -5,10 +5,10 @@ import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens, type Breakpoin
5
5
  import type { ConstraintIssue } from '../../core/engine.js';
6
6
  import type { ValidateOptions } from '../types.js';
7
7
  import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
8
- import { readFileSync, existsSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import { setupConstraints } from '../constraint-registry.js';
8
+ import { readFileSync } from 'node:fs';
9
+ import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
11
10
  import { printVersionBanner } from '../version-banner.js';
11
+ import { loadThemeTokens } from './utils.js';
12
12
 
13
13
  export async function validateCommand(_options: ValidateOptions): Promise<void> {
14
14
  // Show version banner (subtle, dimmed)
@@ -19,13 +19,26 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
19
19
  const crossAxisDebug = process.argv.includes('--cross-axis-debug');
20
20
  const plan: (Breakpoint | undefined)[] = bps.length ? bps : [undefined];
21
21
  let anyErrors = false; let totalErrors = 0; let totalWarnings = 0;
22
- const argv = process.argv.slice(2);
23
- const failOnIdx = argv.indexOf('--fail-on');
22
+ // Coverage tracking for the no-match note (never silently "pass" a file whose
23
+ // tokens are referenced by no active constraint).
24
+ let anyConstraintMatched = false; let coverageKnownAll = true; let tokenCount = 0;
25
+
26
+ // Reconcile the tokens path: the positional `[tokens-path]` is an alias for
27
+ // --tokens. The flag wins; warn on a genuine mismatch. undefined => loader default.
28
+ const flagTokens = _options.tokens;
29
+ const posTokens = _options['tokens-path'];
30
+ if (flagTokens && posTokens && flagTokens !== posTokens) {
31
+ console.error(`warning: both --tokens (${flagTokens}) and positional tokens path (${posTokens}) provided; using --tokens`);
32
+ }
33
+ const tokensPath = flagTokens ?? posTokens;
34
+ const constraintsDir = _options['constraints-dir'] ?? 'themes';
35
+ // Read both the kebab key (CLI; camel-case-expansion is off, and yargs sets the
36
+ // default there) and the camelCase key (programmatic callers). The old argv-scan
37
+ // workaround is no longer needed.
24
38
  type FailOn = 'off' | 'warn' | 'error';
25
- const failOn: FailOn = _options.failOn ?? (failOnIdx >= 0 ? (argv[failOnIdx + 1] as FailOn) : 'error');
26
- const sumIdx = argv.indexOf('--summary');
39
+ const failOn: FailOn = ((_options['fail-on'] ?? _options.failOn) as FailOn) ?? 'error';
27
40
  type SummaryFmt = 'table' | 'json' | 'none';
28
- const summaryFmt: SummaryFmt = _options.summary ?? (sumIdx >= 0 ? (argv[sumIdx + 1] as SummaryFmt) : 'none');
41
+ const summaryFmt: SummaryFmt = (_options.summary as SummaryFmt) ?? 'none';
29
42
  const outputFormat = _options.format ?? 'text';
30
43
 
31
44
  // Collect all issues for JSON output
@@ -56,18 +69,10 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
56
69
  const tStartTotal = globalThis.performance.now();
57
70
  for (const bp of plan) {
58
71
  const tStart = globalThis.performance.now();
59
- let tokens: TokenNode = loadTokensWithBreakpoint(bp);
72
+ let tokens: TokenNode = loadTokensWithBreakpoint(bp, tokensPath);
60
73
  // Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
61
74
  if (_options.theme) {
62
- const themePath = join('tokens/themes', `${_options.theme}.json`);
63
- if (existsSync(themePath)) {
64
- try {
65
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
66
- tokens = mergeTokens(tokens, themeTokens);
67
- } catch {
68
- // If theme file is invalid JSON, ignore and proceed with base tokens
69
- }
70
- }
75
+ tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
71
76
  }
72
77
  // Create engine with flattened tokens
73
78
  const { flat, edges } = flattenTokens(tokens);
@@ -79,12 +84,22 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
79
84
  const knownIds = new Set(Object.keys(init));
80
85
 
81
86
  // Discover and attach all constraints via centralized registry
82
- setupConstraints(
87
+ const sources = setupConstraints(
83
88
  engine,
84
- { config, bp, constraintsDir: 'themes' },
89
+ { config, bp, constraintsDir },
85
90
  { knownIds, crossAxisDebug },
86
91
  );
87
92
 
93
+ // Track whether any active constraint actually references a token in this file.
94
+ const coverage = collectReferencedIds(sources);
95
+ if (!coverage.coverageKnown) coverageKnownAll = false;
96
+ tokenCount = Math.max(tokenCount, knownIds.size);
97
+ if (!anyConstraintMatched) {
98
+ for (const id of coverage.ids) {
99
+ if (knownIds.has(id)) { anyConstraintMatched = true; break; }
100
+ }
101
+ }
102
+
88
103
  const allIds = new Set(Object.keys(init));
89
104
  const issues = engine.evaluate(allIds);
90
105
  const errs = issues.filter((i: ConstraintIssue) => i.level === 'error');
@@ -107,6 +122,13 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
107
122
  }
108
123
  }
109
124
  const totalMs = globalThis.performance.now() - tStartTotal;
125
+ // No-match note: tokens were validated but no active constraint referenced any
126
+ // of them. Surfaced loudly (stderr + JSON `note`) so a foreign file can never
127
+ // silently report "0 errors" when nothing was actually checked.
128
+ const noMatchNote = (tokenCount > 0 && coverageKnownAll && !anyConstraintMatched)
129
+ ? `No active constraint references any of the ${tokenCount} validated token(s) — nothing was checked. Define constraints in dcv.config.json (constraints.wcag / constraints.thresholds) or point --constraints-dir at your order/cross-axis files.`
130
+ : undefined;
131
+ if (noMatchNote) console.error(`note: ${noMatchNote}`);
110
132
  // Append aggregate total row if multiple scopes and not already added
111
133
  if (rows.length > 1) {
112
134
  const agg = rows.reduce((a,b)=>({ rules:a.rules+b.rules, warnings:a.warnings+b.warnings, errors:a.errors+b.errors }), { rules:0,warnings:0,errors:0 });
@@ -125,12 +147,11 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
125
147
 
126
148
  // Handle JSON output mode
127
149
  if (outputFormat === 'json') {
128
- const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
129
-
150
+ const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion, noMatchNote);
151
+
130
152
  // If receipt requested, generate full receipt
131
153
  if (_options.receipt) {
132
- const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
133
- const constraintsDir = 'themes';
154
+ const tokensFile = tokensPath ?? 'tokens/tokens.example.json';
134
155
  const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
135
156
  writeJsonOutput(receipt, _options.receipt);
136
157
  } else {
@@ -154,8 +175,8 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
154
175
  }
155
176
  let code = anyErrors ? 1 : 0;
156
177
  // Budget checks (do not override fail-on semantics unless budgets add failures)
157
- const budgetTotal = (_options as any)['budget-total-ms'] ?? _options.budgetTotalMs;
158
- const budgetPerBp = (_options as any)['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
178
+ const budgetTotal = _options['budget-total-ms'] ?? _options.budgetTotalMs;
179
+ const budgetPerBp = _options['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
159
180
  let budgetFailed = false;
160
181
  if (budgetTotal != null && totalMs > budgetTotal) {
161
182
  console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
@@ -1 +1 @@
1
- {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmJnE"}
1
+ {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8JnE"}
@@ -38,17 +38,28 @@ export async function whyCommand(options) {
38
38
  return {};
39
39
  }
40
40
  }
41
+ // Local overrides (written by `dcv set --write`) inform provenance labelling.
42
+ // The legacy `themes/theme.json` hint was dropped: `themes/` now holds constraint
43
+ // policy files, not visual themes (the overlay convention is tokens/themes/<name>.json),
44
+ // so reading it as a theme layer was a wrong-convention silent fallback.
41
45
  const overrides = safeLoad('tokens/overrides/local.json');
42
- const theme = safeLoad('themes/theme.json');
43
46
  const baseReport = explain(target, flat, edges, {
44
47
  overrides: overrides?.overrides ?? overrides,
45
- theme,
46
48
  });
47
49
  // Best-effort constraint summary: which rules currently implicate this token
48
50
  let constraintsSummary;
49
- try {
50
- const cfgRes = loadConfig(options.config);
51
- if (cfgRes.ok) {
51
+ // An explicitly requested --config that fails to load is a hard error (parity
52
+ // with validate); a config discovered from the cwd just enables the best-effort
53
+ // constraint summary when present.
54
+ const cfgRes = loadConfig(options.config);
55
+ if (!cfgRes.ok) {
56
+ if (options.config) {
57
+ console.error(cfgRes.error);
58
+ process.exit(2);
59
+ }
60
+ }
61
+ else {
62
+ try {
52
63
  const config = cfgRes.value;
53
64
  // Create engine with flattened tokens
54
65
  const init = {};
@@ -57,8 +68,9 @@ export async function whyCommand(options) {
57
68
  }
58
69
  const engine = new Engine(init, edges);
59
70
  const knownIds = new Set(Object.keys(init));
60
- // Discover and attach all constraints via centralized registry
61
- setupConstraints(engine, { config, constraintsDir: 'themes' }, { knownIds });
71
+ // Discover and attach all constraints via centralized registry.
72
+ // Honor --constraints-dir, matching `validate` (default: themes).
73
+ setupConstraints(engine, { config, constraintsDir: options['constraints-dir'] ?? 'themes' }, { knownIds });
62
74
  const candidates = new Set([target]);
63
75
  const allIssues = engine.evaluate(candidates);
64
76
  if (allIssues.length) {
@@ -76,9 +88,9 @@ export async function whyCommand(options) {
76
88
  }
77
89
  }
78
90
  }
79
- }
80
- catch {
81
- // If constraint analysis fails, fall back to provenance-only report.
91
+ catch {
92
+ // If constraint analysis fails, fall back to provenance-only report.
93
+ }
82
94
  }
83
95
  const report = constraintsSummary ? { ...baseReport, constraints: constraintsSummary } : baseReport;
84
96
  const format = options.format || 'json';
@@ -42,12 +42,14 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
42
42
  }
43
43
  }
44
44
 
45
+ // Local overrides (written by `dcv set --write`) inform provenance labelling.
46
+ // The legacy `themes/theme.json` hint was dropped: `themes/` now holds constraint
47
+ // policy files, not visual themes (the overlay convention is tokens/themes/<name>.json),
48
+ // so reading it as a theme layer was a wrong-convention silent fallback.
45
49
  const overrides = safeLoad('tokens/overrides/local.json');
46
- const theme = safeLoad('themes/theme.json');
47
50
 
48
51
  const baseReport = explain(target, flat, edges, {
49
52
  overrides: (overrides as any)?.overrides ?? overrides,
50
- theme,
51
53
  });
52
54
 
53
55
  // Best-effort constraint summary: which rules currently implicate this token
@@ -60,9 +62,17 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
60
62
  }[]
61
63
  | undefined;
62
64
 
63
- try {
64
- const cfgRes = loadConfig(options.config);
65
- if (cfgRes.ok) {
65
+ // An explicitly requested --config that fails to load is a hard error (parity
66
+ // with validate); a config discovered from the cwd just enables the best-effort
67
+ // constraint summary when present.
68
+ const cfgRes = loadConfig(options.config);
69
+ if (!cfgRes.ok) {
70
+ if (options.config) {
71
+ console.error(cfgRes.error);
72
+ process.exit(2);
73
+ }
74
+ } else {
75
+ try {
66
76
  const config = cfgRes.value;
67
77
 
68
78
  // Create engine with flattened tokens
@@ -73,10 +83,11 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
73
83
  const engine = new Engine(init, edges);
74
84
  const knownIds = new Set(Object.keys(init));
75
85
 
76
- // Discover and attach all constraints via centralized registry
86
+ // Discover and attach all constraints via centralized registry.
87
+ // Honor --constraints-dir, matching `validate` (default: themes).
77
88
  setupConstraints(
78
89
  engine,
79
- { config, constraintsDir: 'themes' },
90
+ { config, constraintsDir: options['constraints-dir'] ?? 'themes' },
80
91
  { knownIds },
81
92
  );
82
93
 
@@ -96,9 +107,9 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
96
107
  }));
97
108
  }
98
109
  }
110
+ } catch {
111
+ // If constraint analysis fails, fall back to provenance-only report.
99
112
  }
100
- } catch {
101
- // If constraint analysis fails, fall back to provenance-only report.
102
113
  }
103
114
 
104
115
  const report: any = constraintsSummary ? { ...baseReport, constraints: constraintsSummary } : baseReport;