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/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/design-constraint-validator.svg)](https://www.npmjs.com/package/design-constraint-validator)
7
7
  [![CI](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/ci.yml)
8
8
  [![SBOM](https://img.shields.io/badge/SBOM-CycloneDX-brightgreen)](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/sbom.yml)
9
+ [![Supply Chain Security](https://img.shields.io/badge/security-hardened-blue)](SECURITY.md)
9
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18.x-339933.svg)](#)
11
12
 
@@ -35,27 +36,53 @@ npx dcv --help
35
36
 
36
37
  ## Quick Start
37
38
 
39
+ DCV validates **your** tokens against **your** constraints. From an empty directory:
40
+
38
41
  ```bash
39
- # Validate tokens with default constraints
40
- npx dcv validate ./tokens/tokens.json
42
+ # 1. Your design tokens (DTCG-style "$value")
43
+ cat > tokens.json <<'JSON'
44
+ {
45
+ "color": {
46
+ "text": { "$value": "#888888" },
47
+ "bg": { "$value": "#999999" }
48
+ }
49
+ }
50
+ JSON
51
+
52
+ # 2. Your constraints — auto-discovered as dcv.config.json in the cwd
53
+ cat > dcv.config.json <<'JSON'
54
+ {
55
+ "constraints": {
56
+ "enableBuiltInWcagDefaults": false,
57
+ "enableBuiltInThreshold": false,
58
+ "wcag": [
59
+ { "foreground": "color.text", "background": "color.bg", "ratio": 4.5, "description": "Body text on background" }
60
+ ]
61
+ }
62
+ }
63
+ JSON
41
64
 
42
- # Explain failures
43
- npx dcv why --format table
65
+ # 3. Validate (positional path or --tokens; exits non-zero on violations)
66
+ npx dcv validate tokens.json --summary table
44
67
 
45
- # Export dependency graph
46
- npx dcv graph --format mermaid > graph.mmd
68
+ # Explain one token (the tokenId is required)
69
+ npx dcv why color.text --tokens tokens.json --format table
70
+
71
+ # Export the dependency graph
72
+ npx dcv graph --tokens tokens.json --format mermaid > graph.mmd
47
73
  ```
48
74
 
49
- **Example Output:**
75
+ **Example output** (`validate`):
50
76
 
77
+ ```text
78
+ validate: 1 error(s), 0 warning(s)
79
+ ERROR wcag-contrast color.text|color.bg @ Body text on background — Contrast 1.24:1 < 4.5:1
80
+ scope rules warnings errors
81
+ ------ ----- -------- ------
82
+ global 1 0 1
51
83
  ```
52
- Constraint Status Details
53
- ──────────────────────────── ────── ─────────────────────────────────────────────
54
- WCAG Contrast ≥ 4.5:1 FAIL text.primary(#5A5A5A) on bg.body(#F5F5F5) ⇒ 3.8
55
- Typography monotonic scale FAIL h3(22) < body(18) < h2(21) < h1(34) ✖ out-of-order: h2<h3
56
- Cross-axis (weight vs size) PASS all headings satisfy min weight for size bucket
57
- Exit code: 1 (violations found)
58
- ```
84
+
85
+ Exit code is `1` when violations are found, `0` when clean (use `--fail-on off` to always exit `0`). The built-in WCAG/threshold defaults target the bundled example token ids, so disable them (as above) when validating your own token names.
59
86
 
60
87
  ---
61
88
 
@@ -64,14 +91,15 @@ Exit code: 1 (violations found)
64
91
  ```ts
65
92
  import { validate } from 'design-constraint-validator';
66
93
 
67
- const result = await validate({
68
- tokensPath: './tokens/tokens.json',
69
- policyPath: './themes/policies/aa.json'
94
+ // Synchronous. Point at files, or pass `tokens` / `constraints` inline.
95
+ const result = validate({
96
+ tokensPath: './tokens.json',
97
+ configPath: './dcv.config.json', // omit to auto-discover dcv.config.json in the cwd
70
98
  });
71
99
 
72
100
  if (!result.ok) {
73
101
  for (const v of result.violations) {
74
- console.log(`[${v.kind}] ${v.message}`, v.context);
102
+ console.log(`[${v.ruleId}] ${v.message}`);
75
103
  }
76
104
  process.exitCode = 1;
77
105
  }
@@ -81,6 +109,31 @@ See **[API Reference](docs/API.md)** for complete programmatic usage.
81
109
 
82
110
  ---
83
111
 
112
+ ## Use from AI agents (MCP)
113
+
114
+ DCV ships a second binary, `dcv-mcp`, that exposes the validator over MCP stdio for agent clients. Add it to a Claude Desktop or generic MCP client config like this:
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "dcv": {
120
+ "command": "npx",
121
+ "args": ["-y", "--package", "design-constraint-validator", "dcv-mcp"]
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ The server exposes exactly three JSON-returning tools:
128
+
129
+ - `validate` - validate inline `tokens` or a `tokensPath` against inline `constraints` or a config file.
130
+ - `why` - explain provenance, aliases, dependencies, dependents, and alias chain for one token id.
131
+ - `graph` - return token dependency `nodes` and `edges`.
132
+
133
+ Tool failures are returned as structured JSON: `{ "ok": false, "error": { "code": "...", "message": "..." } }`.
134
+
135
+ ---
136
+
84
137
  ## Documentation
85
138
 
86
139
  ### For Everyone
@@ -103,6 +156,7 @@ See **[API Reference](docs/API.md)** for complete programmatic usage.
103
156
  - **[Prior Art / Method](docs/prior-art/)** - Design rationale (Decision Themes, receipts)
104
157
  - **[AI Guide](docs/AI-GUIDE.md)** - Using DCV with ChatGPT/Claude/Copilot
105
158
  - **[Contributing](CONTRIBUTING.md)** - Contribution guidelines
159
+ - **[Security](SECURITY.md)** - Supply chain security measures
106
160
 
107
161
  ---
108
162
 
@@ -140,7 +194,7 @@ Adapters normalize common ecosystems:
140
194
 
141
195
  - **Style Dictionary** - See [examples/style-dictionary/](examples/style-dictionary/)
142
196
  - **Tokens Studio JSON** - See [examples/tokens-studio/](examples/tokens-studio/)
143
- - **DTCG** (Design Tokens Community Group) - See [examples/dtcg/](examples/dtcg/)
197
+ - **DTCG** (Design Tokens Community Group) — reads the **2025.10 stable spec** (structured sRGB colors, structured dimensions, `{alias}` references, `$extensions` passthrough; non-sRGB spaces warn rather than mis-calculate; composite types out of scope). See [examples/dtcg/](examples/dtcg/)
144
198
 
145
199
  Full adapter documentation: **[Adapters](docs/Adapters.md)**
146
200
 
@@ -180,7 +234,8 @@ These documents keep the method openly implementable and prevent patent lock-up.
180
234
  DCV generates CycloneDX-compliant SBOMs for supply chain transparency:
181
235
 
182
236
  - **CI Builds:** SBOM artifacts on every CI run (90-day retention)
183
- - **Releases:** SBOM files (JSON + XML) attached to GitHub releases
237
+ - **Version tags:** SBOM artifacts for release tags
238
+ - **GitHub Releases:** SBOM files (JSON + XML) attached when a GitHub Release is created
184
239
  - **Manual:** Run `npx @cyclonedx/cyclonedx-npm` in project root
185
240
 
186
241
  **Download:**
@@ -194,7 +249,7 @@ DCV generates CycloneDX-compliant SBOMs for supply chain transparency:
194
249
  - Plugin API for **custom constraints**
195
250
  - **VS Code** diagnostics (inline explain)
196
251
  - **Cross-axis packs** (typography × weight × contrast)
197
- - **Receipts & provenance** (hashes, signable reports)
252
+ - **Signed / attestable receipts** — `dcv validate --receipt` already emits environment + input content hashes today; cryptographic **signing** is the roadmap part
198
253
  - UI graph explorer (node inspector, violations focus)
199
254
 
200
255
  ---
@@ -6,7 +6,7 @@ import { emitJSON } from '../../adapters/json.js';
6
6
  import { emitJS } from '../../adapters/js.js';
7
7
  export async function buildCommand(options) {
8
8
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
9
- const tokens = loadTokensWithBreakpoint();
9
+ const tokens = loadTokensWithBreakpoint(undefined, options.tokens);
10
10
  const { flat } = flattenTokens(tokens);
11
11
  let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, t.value]));
12
12
  if (options.theme) {
@@ -8,7 +8,7 @@ import type { BuildOptions } from '../types.js';
8
8
 
9
9
  export async function buildCommand(options: BuildOptions & { [k: string]: any }): Promise<void> {
10
10
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
11
- const tokens = loadTokensWithBreakpoint();
11
+ const tokens = loadTokensWithBreakpoint(undefined, options.tokens);
12
12
  const { flat } = flattenTokens(tokens);
13
13
  let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, (t as FlatToken).value]));
14
14
  if (options.theme) {
@@ -100,7 +100,7 @@ export async function graphCommand(options) {
100
100
  let edgeLabels;
101
101
  if (onlyViolations || highlightViolations || labelViolations) {
102
102
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
103
- const tokens = loadTokensWithBreakpoint(breakpoint);
103
+ const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
104
104
  const { flattenTokens } = await import('../../core/flatten.js');
105
105
  const { Engine } = await import('../../core/engine.js');
106
106
  const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
@@ -211,7 +211,7 @@ export async function graphCommand(options) {
211
211
  }
212
212
  for (const breakpoint of plan) {
213
213
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
214
- const tokens = loadTokensWithBreakpoint(breakpoint);
214
+ const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
215
215
  const { edges } = flattenTokens(tokens);
216
216
  let filteredEdges = edges;
217
217
  if (options.filter) {
@@ -69,7 +69,7 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
69
69
  let highlight: { nodes: Set<string>; edges: Set<string>; color?: string } | undefined; let edgeLabels: Map<string,string> | undefined;
70
70
  if (onlyViolations || highlightViolations || labelViolations) {
71
71
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
72
- const tokens = loadTokensWithBreakpoint(breakpoint);
72
+ const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
73
73
  const { flattenTokens } = await import('../../core/flatten.js');
74
74
  const { Engine } = await import('../../core/engine.js');
75
75
  const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
@@ -170,7 +170,7 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
170
170
  return; }
171
171
  for (const breakpoint of plan) {
172
172
  const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
173
- const tokens = loadTokensWithBreakpoint(breakpoint); const { edges } = flattenTokens(tokens);
173
+ const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens); const { edges } = flattenTokens(tokens);
174
174
  let filteredEdges = edges;
175
175
  if (options.filter) { const filterRegex = new RegExp(options.filter); filteredEdges = edges.filter(([from,to]) => filterRegex.test(from) || filterRegex.test(to)); }
176
176
  const format = options.format || 'json'; const graph = generateDependencyGraph(filteredEdges, 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,CAoM9E"}
@@ -5,7 +5,7 @@ import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens } from '../../c
5
5
  import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
6
6
  import { readFileSync, existsSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { setupConstraints } from '../constraint-registry.js';
8
+ import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
9
9
  import { printVersionBanner } from '../version-banner.js';
10
10
  export async function validateCommand(_options) {
11
11
  // Show version banner (subtle, dimmed)
@@ -17,6 +17,20 @@ export async function validateCommand(_options) {
17
17
  let anyErrors = false;
18
18
  let totalErrors = 0;
19
19
  let totalWarnings = 0;
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';
20
34
  const argv = process.argv.slice(2);
21
35
  const failOnIdx = argv.indexOf('--fail-on');
22
36
  const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
@@ -55,7 +69,7 @@ export async function validateCommand(_options) {
55
69
  const tStartTotal = globalThis.performance.now();
56
70
  for (const bp of plan) {
57
71
  const tStart = globalThis.performance.now();
58
- let tokens = loadTokensWithBreakpoint(bp);
72
+ let tokens = loadTokensWithBreakpoint(bp, tokensPath);
59
73
  // Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
60
74
  if (_options.theme) {
61
75
  const themePath = join('tokens/themes', `${_options.theme}.json`);
@@ -78,7 +92,20 @@ export async function validateCommand(_options) {
78
92
  const engine = new Engine(init, edges);
79
93
  const knownIds = new Set(Object.keys(init));
80
94
  // Discover and attach all constraints via centralized registry
81
- setupConstraints(engine, { config, bp, constraintsDir: 'themes' }, { knownIds, crossAxisDebug });
95
+ const sources = setupConstraints(engine, { config, bp, constraintsDir }, { knownIds, crossAxisDebug });
96
+ // Track whether any active constraint actually references a token in this file.
97
+ const coverage = collectReferencedIds(sources);
98
+ if (!coverage.coverageKnown)
99
+ coverageKnownAll = false;
100
+ tokenCount = Math.max(tokenCount, knownIds.size);
101
+ if (!anyConstraintMatched) {
102
+ for (const id of coverage.ids) {
103
+ if (knownIds.has(id)) {
104
+ anyConstraintMatched = true;
105
+ break;
106
+ }
107
+ }
108
+ }
82
109
  const allIds = new Set(Object.keys(init));
83
110
  const issues = engine.evaluate(allIds);
84
111
  const errs = issues.filter((i) => i.level === 'error');
@@ -104,6 +131,14 @@ export async function validateCommand(_options) {
104
131
  }
105
132
  }
106
133
  const totalMs = globalThis.performance.now() - tStartTotal;
134
+ // No-match note: tokens were validated but no active constraint referenced any
135
+ // of them. Surfaced loudly (stderr + JSON `note`) so a foreign file can never
136
+ // silently report "0 errors" when nothing was actually checked.
137
+ const noMatchNote = (tokenCount > 0 && coverageKnownAll && !anyConstraintMatched)
138
+ ? `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.`
139
+ : undefined;
140
+ if (noMatchNote)
141
+ console.error(`note: ${noMatchNote}`);
107
142
  // Append aggregate total row if multiple scopes and not already added
108
143
  if (rows.length > 1) {
109
144
  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 +157,10 @@ export async function validateCommand(_options) {
122
157
  }
123
158
  // Handle JSON output mode
124
159
  if (outputFormat === 'json') {
125
- const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
160
+ const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion, noMatchNote);
126
161
  // If receipt requested, generate full receipt
127
162
  if (_options.receipt) {
128
- const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
129
- const constraintsDir = 'themes';
163
+ const tokensFile = tokensPath ?? 'tokens/tokens.example.json';
130
164
  const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
131
165
  writeJsonOutput(receipt, _options.receipt);
132
166
  }
@@ -7,7 +7,7 @@ import type { ValidateOptions } from '../types.js';
7
7
  import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
8
8
  import { readFileSync, existsSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
- import { setupConstraints } from '../constraint-registry.js';
10
+ import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
11
11
  import { printVersionBanner } from '../version-banner.js';
12
12
 
13
13
  export async function validateCommand(_options: ValidateOptions): Promise<void> {
@@ -19,6 +19,19 @@ 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
+ // 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';
22
35
  const argv = process.argv.slice(2);
23
36
  const failOnIdx = argv.indexOf('--fail-on');
24
37
  type FailOn = 'off' | 'warn' | 'error';
@@ -56,7 +69,7 @@ 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
75
  const themePath = join('tokens/themes', `${_options.theme}.json`);
@@ -79,12 +92,22 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
79
92
  const knownIds = new Set(Object.keys(init));
80
93
 
81
94
  // Discover and attach all constraints via centralized registry
82
- setupConstraints(
95
+ const sources = setupConstraints(
83
96
  engine,
84
- { config, bp, constraintsDir: 'themes' },
97
+ { config, bp, constraintsDir },
85
98
  { knownIds, crossAxisDebug },
86
99
  );
87
100
 
101
+ // Track whether any active constraint actually references a token in this file.
102
+ const coverage = collectReferencedIds(sources);
103
+ if (!coverage.coverageKnown) coverageKnownAll = false;
104
+ tokenCount = Math.max(tokenCount, knownIds.size);
105
+ if (!anyConstraintMatched) {
106
+ for (const id of coverage.ids) {
107
+ if (knownIds.has(id)) { anyConstraintMatched = true; break; }
108
+ }
109
+ }
110
+
88
111
  const allIds = new Set(Object.keys(init));
89
112
  const issues = engine.evaluate(allIds);
90
113
  const errs = issues.filter((i: ConstraintIssue) => i.level === 'error');
@@ -107,6 +130,13 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
107
130
  }
108
131
  }
109
132
  const totalMs = globalThis.performance.now() - tStartTotal;
133
+ // No-match note: tokens were validated but no active constraint referenced any
134
+ // of them. Surfaced loudly (stderr + JSON `note`) so a foreign file can never
135
+ // silently report "0 errors" when nothing was actually checked.
136
+ const noMatchNote = (tokenCount > 0 && coverageKnownAll && !anyConstraintMatched)
137
+ ? `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.`
138
+ : undefined;
139
+ if (noMatchNote) console.error(`note: ${noMatchNote}`);
110
140
  // Append aggregate total row if multiple scopes and not already added
111
141
  if (rows.length > 1) {
112
142
  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 +155,11 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
125
155
 
126
156
  // Handle JSON output mode
127
157
  if (outputFormat === 'json') {
128
- const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
129
-
158
+ const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion, noMatchNote);
159
+
130
160
  // If receipt requested, generate full receipt
131
161
  if (_options.receipt) {
132
- const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
133
- const constraintsDir = 'themes';
162
+ const tokensFile = tokensPath ?? 'tokens/tokens.example.json';
134
163
  const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
135
164
  writeJsonOutput(receipt, _options.receipt);
136
165
  } else {
@@ -4,16 +4,19 @@ export declare const WcagRuleSchema: z.ZodObject<{
4
4
  background: z.ZodString;
5
5
  ratio: z.ZodOptional<z.ZodNumber>;
6
6
  description: z.ZodOptional<z.ZodString>;
7
+ backdrop: z.ZodOptional<z.ZodString>;
7
8
  }, "strip", z.ZodTypeAny, {
8
9
  foreground: string;
9
10
  background: string;
10
11
  description?: string | undefined;
11
12
  ratio?: number | undefined;
13
+ backdrop?: string | undefined;
12
14
  }, {
13
15
  foreground: string;
14
16
  background: string;
15
17
  description?: string | undefined;
16
18
  ratio?: number | undefined;
19
+ backdrop?: string | undefined;
17
20
  }>;
18
21
  export declare const ConstraintsSchema: z.ZodObject<{
19
22
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -21,16 +24,19 @@ export declare const ConstraintsSchema: z.ZodObject<{
21
24
  background: z.ZodString;
22
25
  ratio: z.ZodOptional<z.ZodNumber>;
23
26
  description: z.ZodOptional<z.ZodString>;
27
+ backdrop: z.ZodOptional<z.ZodString>;
24
28
  }, "strip", z.ZodTypeAny, {
25
29
  foreground: string;
26
30
  background: string;
27
31
  description?: string | undefined;
28
32
  ratio?: number | undefined;
33
+ backdrop?: string | undefined;
29
34
  }, {
30
35
  foreground: string;
31
36
  background: string;
32
37
  description?: string | undefined;
33
38
  ratio?: number | undefined;
39
+ backdrop?: string | undefined;
34
40
  }>, "many">>;
35
41
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
36
42
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -38,16 +44,19 @@ export declare const ConstraintsSchema: z.ZodObject<{
38
44
  background: z.ZodString;
39
45
  ratio: z.ZodOptional<z.ZodNumber>;
40
46
  description: z.ZodOptional<z.ZodString>;
47
+ backdrop: z.ZodOptional<z.ZodString>;
41
48
  }, "strip", z.ZodTypeAny, {
42
49
  foreground: string;
43
50
  background: string;
44
51
  description?: string | undefined;
45
52
  ratio?: number | undefined;
53
+ backdrop?: string | undefined;
46
54
  }, {
47
55
  foreground: string;
48
56
  background: string;
49
57
  description?: string | undefined;
50
58
  ratio?: number | undefined;
59
+ backdrop?: string | undefined;
51
60
  }>, "many">>;
52
61
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
53
62
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -55,16 +64,19 @@ export declare const ConstraintsSchema: z.ZodObject<{
55
64
  background: z.ZodString;
56
65
  ratio: z.ZodOptional<z.ZodNumber>;
57
66
  description: z.ZodOptional<z.ZodString>;
67
+ backdrop: z.ZodOptional<z.ZodString>;
58
68
  }, "strip", z.ZodTypeAny, {
59
69
  foreground: string;
60
70
  background: string;
61
71
  description?: string | undefined;
62
72
  ratio?: number | undefined;
73
+ backdrop?: string | undefined;
63
74
  }, {
64
75
  foreground: string;
65
76
  background: string;
66
77
  description?: string | undefined;
67
78
  ratio?: number | undefined;
79
+ backdrop?: string | undefined;
68
80
  }>, "many">>;
69
81
  }, z.ZodTypeAny, "passthrough">>;
70
82
  export declare const DcvConfigSchema: z.ZodObject<{
@@ -75,16 +87,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
75
87
  background: z.ZodString;
76
88
  ratio: z.ZodOptional<z.ZodNumber>;
77
89
  description: z.ZodOptional<z.ZodString>;
90
+ backdrop: z.ZodOptional<z.ZodString>;
78
91
  }, "strip", z.ZodTypeAny, {
79
92
  foreground: string;
80
93
  background: string;
81
94
  description?: string | undefined;
82
95
  ratio?: number | undefined;
96
+ backdrop?: string | undefined;
83
97
  }, {
84
98
  foreground: string;
85
99
  background: string;
86
100
  description?: string | undefined;
87
101
  ratio?: number | undefined;
102
+ backdrop?: string | undefined;
88
103
  }>, "many">>;
89
104
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
90
105
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -92,16 +107,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
92
107
  background: z.ZodString;
93
108
  ratio: z.ZodOptional<z.ZodNumber>;
94
109
  description: z.ZodOptional<z.ZodString>;
110
+ backdrop: z.ZodOptional<z.ZodString>;
95
111
  }, "strip", z.ZodTypeAny, {
96
112
  foreground: string;
97
113
  background: string;
98
114
  description?: string | undefined;
99
115
  ratio?: number | undefined;
116
+ backdrop?: string | undefined;
100
117
  }, {
101
118
  foreground: string;
102
119
  background: string;
103
120
  description?: string | undefined;
104
121
  ratio?: number | undefined;
122
+ backdrop?: string | undefined;
105
123
  }>, "many">>;
106
124
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
107
125
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -109,16 +127,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
109
127
  background: z.ZodString;
110
128
  ratio: z.ZodOptional<z.ZodNumber>;
111
129
  description: z.ZodOptional<z.ZodString>;
130
+ backdrop: z.ZodOptional<z.ZodString>;
112
131
  }, "strip", z.ZodTypeAny, {
113
132
  foreground: string;
114
133
  background: string;
115
134
  description?: string | undefined;
116
135
  ratio?: number | undefined;
136
+ backdrop?: string | undefined;
117
137
  }, {
118
138
  foreground: string;
119
139
  background: string;
120
140
  description?: string | undefined;
121
141
  ratio?: number | undefined;
142
+ backdrop?: string | undefined;
122
143
  }>, "many">>;
123
144
  }, z.ZodTypeAny, "passthrough">>>;
124
145
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
@@ -129,16 +150,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
129
150
  background: z.ZodString;
130
151
  ratio: z.ZodOptional<z.ZodNumber>;
131
152
  description: z.ZodOptional<z.ZodString>;
153
+ backdrop: z.ZodOptional<z.ZodString>;
132
154
  }, "strip", z.ZodTypeAny, {
133
155
  foreground: string;
134
156
  background: string;
135
157
  description?: string | undefined;
136
158
  ratio?: number | undefined;
159
+ backdrop?: string | undefined;
137
160
  }, {
138
161
  foreground: string;
139
162
  background: string;
140
163
  description?: string | undefined;
141
164
  ratio?: number | undefined;
165
+ backdrop?: string | undefined;
142
166
  }>, "many">>;
143
167
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
144
168
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -146,16 +170,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
146
170
  background: z.ZodString;
147
171
  ratio: z.ZodOptional<z.ZodNumber>;
148
172
  description: z.ZodOptional<z.ZodString>;
173
+ backdrop: z.ZodOptional<z.ZodString>;
149
174
  }, "strip", z.ZodTypeAny, {
150
175
  foreground: string;
151
176
  background: string;
152
177
  description?: string | undefined;
153
178
  ratio?: number | undefined;
179
+ backdrop?: string | undefined;
154
180
  }, {
155
181
  foreground: string;
156
182
  background: string;
157
183
  description?: string | undefined;
158
184
  ratio?: number | undefined;
185
+ backdrop?: string | undefined;
159
186
  }>, "many">>;
160
187
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
161
188
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -163,16 +190,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
163
190
  background: z.ZodString;
164
191
  ratio: z.ZodOptional<z.ZodNumber>;
165
192
  description: z.ZodOptional<z.ZodString>;
193
+ backdrop: z.ZodOptional<z.ZodString>;
166
194
  }, "strip", z.ZodTypeAny, {
167
195
  foreground: string;
168
196
  background: string;
169
197
  description?: string | undefined;
170
198
  ratio?: number | undefined;
199
+ backdrop?: string | undefined;
171
200
  }, {
172
201
  foreground: string;
173
202
  background: string;
174
203
  description?: string | undefined;
175
204
  ratio?: number | undefined;
205
+ backdrop?: string | undefined;
176
206
  }>, "many">>;
177
207
  }, z.ZodTypeAny, "passthrough">>>;
178
208
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
@@ -183,16 +213,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
183
213
  background: z.ZodString;
184
214
  ratio: z.ZodOptional<z.ZodNumber>;
185
215
  description: z.ZodOptional<z.ZodString>;
216
+ backdrop: z.ZodOptional<z.ZodString>;
186
217
  }, "strip", z.ZodTypeAny, {
187
218
  foreground: string;
188
219
  background: string;
189
220
  description?: string | undefined;
190
221
  ratio?: number | undefined;
222
+ backdrop?: string | undefined;
191
223
  }, {
192
224
  foreground: string;
193
225
  background: string;
194
226
  description?: string | undefined;
195
227
  ratio?: number | undefined;
228
+ backdrop?: string | undefined;
196
229
  }>, "many">>;
197
230
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
198
231
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -200,16 +233,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
200
233
  background: z.ZodString;
201
234
  ratio: z.ZodOptional<z.ZodNumber>;
202
235
  description: z.ZodOptional<z.ZodString>;
236
+ backdrop: z.ZodOptional<z.ZodString>;
203
237
  }, "strip", z.ZodTypeAny, {
204
238
  foreground: string;
205
239
  background: string;
206
240
  description?: string | undefined;
207
241
  ratio?: number | undefined;
242
+ backdrop?: string | undefined;
208
243
  }, {
209
244
  foreground: string;
210
245
  background: string;
211
246
  description?: string | undefined;
212
247
  ratio?: number | undefined;
248
+ backdrop?: string | undefined;
213
249
  }>, "many">>;
214
250
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
215
251
  wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -217,16 +253,19 @@ export declare const DcvConfigSchema: z.ZodObject<{
217
253
  background: z.ZodString;
218
254
  ratio: z.ZodOptional<z.ZodNumber>;
219
255
  description: z.ZodOptional<z.ZodString>;
256
+ backdrop: z.ZodOptional<z.ZodString>;
220
257
  }, "strip", z.ZodTypeAny, {
221
258
  foreground: string;
222
259
  background: string;
223
260
  description?: string | undefined;
224
261
  ratio?: number | undefined;
262
+ backdrop?: string | undefined;
225
263
  }, {
226
264
  foreground: string;
227
265
  background: string;
228
266
  description?: string | undefined;
229
267
  ratio?: number | undefined;
268
+ backdrop?: string | undefined;
230
269
  }>, "many">>;
231
270
  }, z.ZodTypeAny, "passthrough">>>;
232
271
  }, z.ZodTypeAny, "passthrough">>;