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.
- package/README.md +76 -21
- package/cli/commands/build.js +1 -1
- package/cli/commands/build.ts +1 -1
- package/cli/commands/graph.js +2 -2
- package/cli/commands/graph.ts +2 -2
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +40 -6
- package/cli/commands/validate.ts +37 -8
- package/cli/config-schema.d.ts +39 -0
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +4 -2
- package/cli/config-schema.ts +4 -2
- package/cli/config.d.ts.map +1 -1
- package/cli/config.js +8 -2
- package/cli/config.ts +8 -2
- package/cli/constraint-registry.d.ts +16 -0
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +64 -31
- package/cli/constraint-registry.ts +67 -31
- package/cli/dcv.js +8 -24
- package/cli/dcv.ts +8 -20
- package/cli/json-output.d.ts +3 -1
- package/cli/json-output.d.ts.map +1 -1
- package/cli/json-output.js +11 -4
- package/cli/json-output.ts +13 -4
- package/cli/types.d.ts +2 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +2 -0
- package/cli/validate-api.d.ts +40 -0
- package/cli/validate-api.d.ts.map +1 -0
- package/cli/validate-api.js +85 -0
- package/cli/validate-api.ts +126 -0
- package/core/breakpoints.d.ts +8 -2
- package/core/breakpoints.d.ts.map +1 -1
- package/core/breakpoints.js +24 -3
- package/core/breakpoints.ts +22 -3
- package/core/color.js +4 -4
- package/core/color.ts +4 -4
- package/core/constraints/monotonic-lightness.d.ts.map +1 -1
- package/core/constraints/monotonic-lightness.js +9 -5
- package/core/constraints/monotonic-lightness.ts +9 -4
- package/core/constraints/wcag.d.ts.map +1 -1
- package/core/constraints/wcag.js +6 -0
- package/core/constraints/wcag.ts +6 -0
- package/core/dtcg.d.ts +38 -0
- package/core/dtcg.d.ts.map +1 -0
- package/core/dtcg.js +88 -0
- package/core/dtcg.ts +102 -0
- package/core/engine.d.ts +6 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.ts +7 -0
- package/core/flatten.d.ts +5 -3
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +24 -10
- package/core/flatten.ts +39 -16
- package/core/image-export.d.ts.map +1 -1
- package/core/image-export.js +10 -7
- package/core/image-export.ts +9 -6
- package/core/index.d.ts +2 -0
- package/core/index.d.ts.map +1 -1
- package/core/index.js +4 -0
- package/core/index.ts +6 -0
- package/core/why.d.ts +1 -1
- package/core/why.d.ts.map +1 -1
- package/core/why.ts +1 -1
- package/mcp/contracts.d.ts +118 -0
- package/mcp/contracts.d.ts.map +1 -0
- package/mcp/contracts.js +30 -0
- package/mcp/contracts.ts +51 -0
- package/mcp/index.d.ts +9 -0
- package/mcp/index.d.ts.map +1 -0
- package/mcp/index.js +32 -0
- package/mcp/index.ts +70 -0
- package/mcp/tools.d.ts +52 -0
- package/mcp/tools.d.ts.map +1 -0
- package/mcp/tools.js +172 -0
- package/mcp/tools.ts +254 -0
- package/package.json +41 -26
- package/server.json +21 -0
- package/cli/constraints-loader.d.ts.map +0 -1
- package/cli/engine-helpers.d.ts.map +0 -1
- package/core/cross-axis-config.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/design-constraint-validator)
|
|
7
7
|
[](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/ci.yml)
|
|
8
8
|
[](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/sbom.yml)
|
|
9
|
+
[](SECURITY.md)
|
|
9
10
|
[](LICENSE)
|
|
10
11
|
[](#)
|
|
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
|
-
#
|
|
40
|
-
|
|
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
|
-
#
|
|
43
|
-
npx dcv
|
|
65
|
+
# 3. Validate (positional path or --tokens; exits non-zero on violations)
|
|
66
|
+
npx dcv validate tokens.json --summary table
|
|
44
67
|
|
|
45
|
-
#
|
|
46
|
-
npx dcv
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
---
|
package/cli/commands/build.js
CHANGED
|
@@ -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) {
|
package/cli/commands/build.ts
CHANGED
|
@@ -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) {
|
package/cli/commands/graph.js
CHANGED
|
@@ -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) {
|
package/cli/commands/graph.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/cli/commands/validate.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
}
|
package/cli/commands/validate.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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 {
|
package/cli/config-schema.d.ts
CHANGED
|
@@ -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">>;
|