design-constraint-validator 2.1.0 → 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 +17 -6
- 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 +33 -16
- package/cli/commands/graph.ts +28 -15
- 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 +5 -17
- package/cli/commands/validate.ts +10 -18
- 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 +144 -178
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +25 -5
- package/cli/config-schema.ts +27 -5
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +53 -15
- package/cli/constraint-registry.ts +53 -18
- 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 +23 -1
- package/cli/dcv.ts +23 -1
- package/cli/types.d.ts +19 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +23 -10
- package/cli/validate-api.d.ts.map +1 -1
- package/cli/validate-api.js +6 -1
- package/cli/validate-api.ts +6 -1
- 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.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.js +1 -1
- package/core/constraints/wcag.ts +1 -1
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +8 -0
- package/core/flatten.ts +9 -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/mcp/contracts.d.ts +1456 -13
- package/mcp/contracts.d.ts.map +1 -1
- package/mcp/contracts.js +45 -1
- package/mcp/contracts.ts +55 -1
- package/mcp/index.d.ts +6 -4
- package/mcp/index.d.ts.map +1 -1
- package/mcp/index.js +6 -3
- package/mcp/index.ts +28 -1
- 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 +14 -3
- package/mcp/tools.d.ts.map +1 -1
- package/mcp/tools.js +133 -6
- package/mcp/tools.ts +188 -11
- package/package.json +1 -6
- package/server.json +2 -2
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 { join } from 'node:path';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
8
7
|
import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
|
|
9
8
|
import { printVersionBanner } from '../version-banner.js';
|
|
9
|
+
import { loadThemeTokens } from './utils.js';
|
|
10
10
|
export async function validateCommand(_options) {
|
|
11
11
|
// Show version banner (subtle, dimmed)
|
|
12
12
|
printVersionBanner({ quiet: _options.format === 'json' });
|
|
@@ -31,11 +31,8 @@ export async function validateCommand(_options) {
|
|
|
31
31
|
}
|
|
32
32
|
const tokensPath = flagTokens ?? posTokens;
|
|
33
33
|
const constraintsDir = _options['constraints-dir'] ?? 'themes';
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
|
|
37
|
-
const sumIdx = argv.indexOf('--summary');
|
|
38
|
-
const summaryFmt = _options.summary ?? (sumIdx >= 0 ? argv[sumIdx + 1] : 'none');
|
|
34
|
+
const failOn = (_options['fail-on'] ?? _options.failOn) ?? 'error';
|
|
35
|
+
const summaryFmt = _options.summary ?? 'none';
|
|
39
36
|
const outputFormat = _options.format ?? 'text';
|
|
40
37
|
// Collect all issues for JSON output
|
|
41
38
|
const allErrors = [];
|
|
@@ -72,16 +69,7 @@ export async function validateCommand(_options) {
|
|
|
72
69
|
let tokens = loadTokensWithBreakpoint(bp, tokensPath);
|
|
73
70
|
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
74
71
|
if (_options.theme) {
|
|
75
|
-
|
|
76
|
-
if (existsSync(themePath)) {
|
|
77
|
-
try {
|
|
78
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
79
|
-
tokens = mergeTokens(tokens, themeTokens);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// If theme file is invalid JSON, ignore and proceed with base tokens
|
|
83
|
-
}
|
|
84
|
-
}
|
|
72
|
+
tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
|
|
85
73
|
}
|
|
86
74
|
// Create engine with flattened tokens
|
|
87
75
|
const { flat, edges } = flattenTokens(tokens);
|
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 { join } from 'node:path';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
10
9
|
import { setupConstraints, collectReferencedIds } from '../constraint-registry.js';
|
|
11
10
|
import { printVersionBanner } from '../version-banner.js';
|
|
11
|
+
import { loadThemeTokens } from './utils.js';
|
|
12
12
|
|
|
13
13
|
export async function validateCommand(_options: ValidateOptions): Promise<void> {
|
|
14
14
|
// Show version banner (subtle, dimmed)
|
|
@@ -32,13 +32,13 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
|
|
|
32
32
|
}
|
|
33
33
|
const tokensPath = flagTokens ?? posTokens;
|
|
34
34
|
const constraintsDir = _options['constraints-dir'] ?? 'themes';
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Read both the kebab key (CLI; camel-case-expansion is off, and yargs sets the
|
|
36
|
+
// default there) and the camelCase key (programmatic callers). The old argv-scan
|
|
37
|
+
// workaround is no longer needed.
|
|
37
38
|
type FailOn = 'off' | 'warn' | 'error';
|
|
38
|
-
const failOn: FailOn =
|
|
39
|
-
const sumIdx = argv.indexOf('--summary');
|
|
39
|
+
const failOn: FailOn = ((_options['fail-on'] ?? _options.failOn) as FailOn) ?? 'error';
|
|
40
40
|
type SummaryFmt = 'table' | 'json' | 'none';
|
|
41
|
-
const summaryFmt: SummaryFmt = _options.summary
|
|
41
|
+
const summaryFmt: SummaryFmt = (_options.summary as SummaryFmt) ?? 'none';
|
|
42
42
|
const outputFormat = _options.format ?? 'text';
|
|
43
43
|
|
|
44
44
|
// Collect all issues for JSON output
|
|
@@ -72,15 +72,7 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
|
|
|
72
72
|
let tokens: TokenNode = loadTokensWithBreakpoint(bp, tokensPath);
|
|
73
73
|
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
74
74
|
if (_options.theme) {
|
|
75
|
-
|
|
76
|
-
if (existsSync(themePath)) {
|
|
77
|
-
try {
|
|
78
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
79
|
-
tokens = mergeTokens(tokens, themeTokens);
|
|
80
|
-
} catch {
|
|
81
|
-
// If theme file is invalid JSON, ignore and proceed with base tokens
|
|
82
|
-
}
|
|
83
|
-
}
|
|
75
|
+
tokens = mergeTokens(tokens, loadThemeTokens(_options.theme));
|
|
84
76
|
}
|
|
85
77
|
// Create engine with flattened tokens
|
|
86
78
|
const { flat, edges } = flattenTokens(tokens);
|
|
@@ -183,8 +175,8 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
|
|
|
183
175
|
}
|
|
184
176
|
let code = anyErrors ? 1 : 0;
|
|
185
177
|
// Budget checks (do not override fail-on semantics unless budgets add failures)
|
|
186
|
-
const budgetTotal =
|
|
187
|
-
const budgetPerBp =
|
|
178
|
+
const budgetTotal = _options['budget-total-ms'] ?? _options.budgetTotalMs;
|
|
179
|
+
const budgetPerBp = _options['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
|
|
188
180
|
let budgetFailed = false;
|
|
189
181
|
if (budgetTotal != null && totalMs > budgetTotal) {
|
|
190
182
|
console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
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;
|