design-constraint-validator 2.1.0 → 2.2.1

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 (78) hide show
  1. package/README.md +17 -6
  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 +33 -16
  7. package/cli/commands/graph.ts +28 -15
  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 +5 -17
  20. package/cli/commands/validate.ts +10 -18
  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 +144 -178
  25. package/cli/config-schema.d.ts.map +1 -1
  26. package/cli/config-schema.js +25 -5
  27. package/cli/config-schema.ts +27 -5
  28. package/cli/constraint-registry.d.ts.map +1 -1
  29. package/cli/constraint-registry.js +53 -15
  30. package/cli/constraint-registry.ts +53 -18
  31. package/cli/cross-axis-loader.d.ts +62 -0
  32. package/cli/cross-axis-loader.d.ts.map +1 -1
  33. package/cli/cross-axis-loader.js +186 -31
  34. package/cli/cross-axis-loader.ts +199 -24
  35. package/cli/dcv.js +23 -1
  36. package/cli/dcv.ts +23 -1
  37. package/cli/types.d.ts +19 -9
  38. package/cli/types.d.ts.map +1 -1
  39. package/cli/types.ts +23 -10
  40. package/cli/validate-api.d.ts.map +1 -1
  41. package/cli/validate-api.js +6 -1
  42. package/cli/validate-api.ts +6 -1
  43. package/core/constraints/cross-axis.d.ts.map +1 -1
  44. package/core/constraints/cross-axis.js +37 -9
  45. package/core/constraints/cross-axis.ts +37 -9
  46. package/core/constraints/monotonic.d.ts.map +1 -1
  47. package/core/constraints/monotonic.js +32 -8
  48. package/core/constraints/monotonic.ts +29 -8
  49. package/core/constraints/threshold.d.ts.map +1 -1
  50. package/core/constraints/threshold.js +24 -4
  51. package/core/constraints/threshold.ts +23 -4
  52. package/core/constraints/wcag.js +1 -1
  53. package/core/constraints/wcag.ts +1 -1
  54. package/core/flatten.d.ts.map +1 -1
  55. package/core/flatten.js +8 -0
  56. package/core/flatten.ts +9 -0
  57. package/core/poset.d.ts +6 -1
  58. package/core/poset.d.ts.map +1 -1
  59. package/core/poset.js +7 -2
  60. package/core/poset.ts +7 -2
  61. package/mcp/contracts.d.ts +1456 -13
  62. package/mcp/contracts.d.ts.map +1 -1
  63. package/mcp/contracts.js +45 -1
  64. package/mcp/contracts.ts +55 -1
  65. package/mcp/index.d.ts +6 -4
  66. package/mcp/index.d.ts.map +1 -1
  67. package/mcp/index.js +6 -3
  68. package/mcp/index.ts +28 -1
  69. package/mcp/insights.d.ts +94 -0
  70. package/mcp/insights.d.ts.map +1 -0
  71. package/mcp/insights.js +445 -0
  72. package/mcp/insights.ts +541 -0
  73. package/mcp/tools.d.ts +14 -3
  74. package/mcp/tools.d.ts.map +1 -1
  75. package/mcp/tools.js +133 -6
  76. package/mcp/tools.ts +188 -11
  77. package/package.json +2 -7
  78. package/server.json +3 -3
@@ -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,CAoM9E"}
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';
6
+ import { readFileSync } from 'node:fs';
8
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' });
@@ -31,11 +31,8 @@ export async function validateCommand(_options) {
31
31
  }
32
32
  const tokensPath = flagTokens ?? posTokens;
33
33
  const constraintsDir = _options['constraints-dir'] ?? 'themes';
34
- const argv = process.argv.slice(2);
35
- const failOnIdx = argv.indexOf('--fail-on');
36
- const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
37
- const sumIdx = argv.indexOf('--summary');
38
- const summaryFmt = _options.summary ?? (sumIdx >= 0 ? argv[sumIdx + 1] : 'none');
34
+ const failOn = (_options['fail-on'] ?? _options.failOn) ?? 'error';
35
+ const summaryFmt = _options.summary ?? 'none';
39
36
  const outputFormat = _options.format ?? 'text';
40
37
  // Collect all issues for JSON output
41
38
  const allErrors = [];
@@ -72,16 +69,7 @@ export async function validateCommand(_options) {
72
69
  let tokens = loadTokensWithBreakpoint(bp, tokensPath);
73
70
  // Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
74
71
  if (_options.theme) {
75
- const themePath = join('tokens/themes', `${_options.theme}.json`);
76
- if (existsSync(themePath)) {
77
- try {
78
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
79
- tokens = mergeTokens(tokens, themeTokens);
80
- }
81
- catch {
82
- // If theme file is invalid JSON, ignore and proceed with base tokens
83
- }
84
- }
72
+ tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
85
73
  }
86
74
  // Create engine with flattened tokens
87
75
  const { flat, edges } = flattenTokens(tokens);
@@ -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';
8
+ import { readFileSync } from 'node:fs';
10
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)
@@ -32,13 +32,13 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
32
32
  }
33
33
  const tokensPath = flagTokens ?? posTokens;
34
34
  const constraintsDir = _options['constraints-dir'] ?? 'themes';
35
- const argv = process.argv.slice(2);
36
- const failOnIdx = argv.indexOf('--fail-on');
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.
37
38
  type FailOn = 'off' | 'warn' | 'error';
38
- const failOn: FailOn = _options.failOn ?? (failOnIdx >= 0 ? (argv[failOnIdx + 1] as FailOn) : 'error');
39
- const sumIdx = argv.indexOf('--summary');
39
+ const failOn: FailOn = ((_options['fail-on'] ?? _options.failOn) as FailOn) ?? 'error';
40
40
  type SummaryFmt = 'table' | 'json' | 'none';
41
- const summaryFmt: SummaryFmt = _options.summary ?? (sumIdx >= 0 ? (argv[sumIdx + 1] as SummaryFmt) : 'none');
41
+ const summaryFmt: SummaryFmt = (_options.summary as SummaryFmt) ?? 'none';
42
42
  const outputFormat = _options.format ?? 'text';
43
43
 
44
44
  // Collect all issues for JSON output
@@ -72,15 +72,7 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
72
72
  let tokens: TokenNode = loadTokensWithBreakpoint(bp, tokensPath);
73
73
  // Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
74
74
  if (_options.theme) {
75
- const themePath = join('tokens/themes', `${_options.theme}.json`);
76
- if (existsSync(themePath)) {
77
- try {
78
- const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
79
- tokens = mergeTokens(tokens, themeTokens);
80
- } catch {
81
- // If theme file is invalid JSON, ignore and proceed with base tokens
82
- }
83
- }
75
+ tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
84
76
  }
85
77
  // Create engine with flattened tokens
86
78
  const { flat, edges } = flattenTokens(tokens);
@@ -183,8 +175,8 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
183
175
  }
184
176
  let code = anyErrors ? 1 : 0;
185
177
  // Budget checks (do not override fail-on semantics unless budgets add failures)
186
- const budgetTotal = (_options as any)['budget-total-ms'] ?? _options.budgetTotalMs;
187
- 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;
188
180
  let budgetFailed = false;
189
181
  if (budgetTotal != null && totalMs > budgetTotal) {
190
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;