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