design-constraint-validator 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +229 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/decisionthemes.d.ts +44 -0
- package/adapters/decisionthemes.d.ts.map +1 -0
- package/adapters/decisionthemes.js +35 -0
- package/adapters/decisionthemes.ts +59 -0
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +89 -33
- package/cli/commands/validate.ts +180 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +69 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +109 -0
- package/cli/json-output.ts +184 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/cli/version-banner.d.ts +20 -0
- package/cli/version-banner.d.ts.map +1 -0
- package/cli/version-banner.js +49 -0
- package/cli/version-banner.ts +61 -0
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/cli/engine-helpers.d.ts +0 -8
- package/cli/engine-helpers.js +0 -70
- package/cli/engine-helpers.ts +0 -61
- package/core/cross-axis-config.d.ts +0 -5
- package/core/cross-axis-config.js +0 -144
- package/core/cross-axis-config.ts +0 -152
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/cli/commands/validate.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { flattenTokens } from '../../core/flatten.js';
|
|
2
|
-
import {
|
|
2
|
+
import { Engine } from '../../core/engine.js';
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
|
-
import { parseBreakpoints, loadTokensWithBreakpoint } from '../../core/breakpoints.js';
|
|
5
|
-
import {
|
|
4
|
+
import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens } from '../../core/breakpoints.js';
|
|
5
|
+
import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
9
|
+
import { printVersionBanner } from '../version-banner.js';
|
|
6
10
|
export async function validateCommand(_options) {
|
|
11
|
+
// Show version banner (subtle, dimmed)
|
|
12
|
+
printVersionBanner({ quiet: _options.format === 'json' });
|
|
7
13
|
try {
|
|
8
14
|
const bps = parseBreakpoints(process.argv);
|
|
9
15
|
const crossAxisDebug = process.argv.includes('--cross-axis-debug');
|
|
@@ -13,9 +19,13 @@ export async function validateCommand(_options) {
|
|
|
13
19
|
let totalWarnings = 0;
|
|
14
20
|
const argv = process.argv.slice(2);
|
|
15
21
|
const failOnIdx = argv.indexOf('--fail-on');
|
|
16
|
-
const failOn = failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error';
|
|
22
|
+
const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
|
|
17
23
|
const sumIdx = argv.indexOf('--summary');
|
|
18
|
-
const summaryFmt = sumIdx >= 0 ? argv[sumIdx + 1] : 'none';
|
|
24
|
+
const summaryFmt = _options.summary ?? (sumIdx >= 0 ? argv[sumIdx + 1] : 'none');
|
|
25
|
+
const outputFormat = _options.format ?? 'text';
|
|
26
|
+
// Collect all issues for JSON output
|
|
27
|
+
const allErrors = [];
|
|
28
|
+
const allWarnings = [];
|
|
19
29
|
const rows = [];
|
|
20
30
|
function pushRow(bpLabel, stats) { rows.push({ bp: bpLabel, ...stats }); }
|
|
21
31
|
function printSummaryTable(rs) {
|
|
@@ -45,19 +55,31 @@ export async function validateCommand(_options) {
|
|
|
45
55
|
const tStartTotal = globalThis.performance.now();
|
|
46
56
|
for (const bp of plan) {
|
|
47
57
|
const tStart = globalThis.performance.now();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
let tokens = loadTokensWithBreakpoint(bp);
|
|
59
|
+
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
60
|
+
if (_options.theme) {
|
|
61
|
+
const themePath = join('tokens/themes', `${_options.theme}.json`);
|
|
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
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Create engine with flattened tokens
|
|
73
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
74
|
+
const init = {};
|
|
75
|
+
for (const t of Object.values(flat)) {
|
|
76
|
+
init[t.id] = t.value;
|
|
57
77
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
const engine = new Engine(init, edges);
|
|
79
|
+
const knownIds = new Set(Object.keys(init));
|
|
80
|
+
// Discover and attach all constraints via centralized registry
|
|
81
|
+
setupConstraints(engine, { config, bp, constraintsDir: 'themes' }, { knownIds, crossAxisDebug });
|
|
82
|
+
const allIds = new Set(Object.keys(init));
|
|
61
83
|
const issues = engine.evaluate(allIds);
|
|
62
84
|
const errs = issues.filter((i) => i.level === 'error');
|
|
63
85
|
const warns = issues.filter((i) => i.level !== 'error');
|
|
@@ -65,14 +87,20 @@ export async function validateCommand(_options) {
|
|
|
65
87
|
anyErrors = true;
|
|
66
88
|
totalErrors += errs.length;
|
|
67
89
|
totalWarnings += warns.length;
|
|
90
|
+
// Collect for JSON output
|
|
91
|
+
allErrors.push(...errs);
|
|
92
|
+
allWarnings.push(...warns);
|
|
68
93
|
const rulesEvaluated = errs.length + warns.length;
|
|
69
94
|
pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
|
|
70
95
|
const dur = globalThis.performance.now() - tStart;
|
|
71
96
|
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
// Only print text output if not in JSON mode
|
|
98
|
+
if (outputFormat !== 'json') {
|
|
99
|
+
console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
100
|
+
for (const it of issues) {
|
|
101
|
+
const tag = it.level === 'error' ? 'ERROR' : 'WARN ';
|
|
102
|
+
console.log(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`);
|
|
103
|
+
}
|
|
76
104
|
}
|
|
77
105
|
}
|
|
78
106
|
const totalMs = globalThis.performance.now() - tStartTotal;
|
|
@@ -81,20 +109,48 @@ export async function validateCommand(_options) {
|
|
|
81
109
|
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 });
|
|
82
110
|
rows.push({ bp: 'TOTAL', ...agg });
|
|
83
111
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
// Get package version for stats
|
|
113
|
+
let engineVersion = '1.0.0';
|
|
114
|
+
try {
|
|
115
|
+
// eslint-disable-next-line no-undef
|
|
116
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
117
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
118
|
+
engineVersion = pkg.version;
|
|
89
119
|
}
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
93
|
-
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
94
|
-
console.log(JSON.stringify(json, null, 2));
|
|
120
|
+
catch {
|
|
121
|
+
// Ignore package.json read errors, use default version
|
|
95
122
|
}
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
// Handle JSON output mode
|
|
124
|
+
if (outputFormat === 'json') {
|
|
125
|
+
const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
|
|
126
|
+
// If receipt requested, generate full receipt
|
|
127
|
+
if (_options.receipt) {
|
|
128
|
+
const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
|
|
129
|
+
const constraintsDir = 'themes';
|
|
130
|
+
const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
|
|
131
|
+
writeJsonOutput(receipt, _options.receipt);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
writeJsonOutput(result, _options.output);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Text output mode
|
|
139
|
+
if (_options.perf) {
|
|
140
|
+
console.log('[perf] per-breakpoint timings:');
|
|
141
|
+
for (const t of perBpTimings)
|
|
142
|
+
console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
|
|
143
|
+
console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
|
|
144
|
+
}
|
|
145
|
+
if (summaryFmt === 'json') {
|
|
146
|
+
// Provide machine-readable aggregate separate from rows if TOTAL present
|
|
147
|
+
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
148
|
+
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
149
|
+
console.log(JSON.stringify(json, null, 2));
|
|
150
|
+
}
|
|
151
|
+
else if (summaryFmt === 'table') {
|
|
152
|
+
printSummaryTable(rows);
|
|
153
|
+
}
|
|
98
154
|
}
|
|
99
155
|
let code = anyErrors ? 1 : 0;
|
|
100
156
|
// Budget checks (do not override fail-on semantics unless budgets add failures)
|
package/cli/commands/validate.ts
CHANGED
|
@@ -1,115 +1,180 @@
|
|
|
1
|
-
import { flattenTokens } from '../../core/flatten.js';
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig } from '../config.js';
|
|
4
|
-
import { parseBreakpoints, loadTokensWithBreakpoint, type Breakpoint } from '../../core/breakpoints.js';
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
import { flattenTokens, type TokenNode, type FlatToken } from '../../core/flatten.js';
|
|
2
|
+
import { Engine } from '../../core/engine.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens, type Breakpoint } from '../../core/breakpoints.js';
|
|
5
|
+
import type { ConstraintIssue } from '../../core/engine.js';
|
|
6
|
+
import type { ValidateOptions } from '../types.js';
|
|
7
|
+
import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
11
|
+
import { printVersionBanner } from '../version-banner.js';
|
|
12
|
+
|
|
13
|
+
export async function validateCommand(_options: ValidateOptions): Promise<void> {
|
|
14
|
+
// Show version banner (subtle, dimmed)
|
|
15
|
+
printVersionBanner({ quiet: _options.format === 'json' });
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const bps = parseBreakpoints(process.argv);
|
|
19
|
+
const crossAxisDebug = process.argv.includes('--cross-axis-debug');
|
|
20
|
+
const plan: (Breakpoint | undefined)[] = bps.length ? bps : [undefined];
|
|
21
|
+
let anyErrors = false; let totalErrors = 0; let totalWarnings = 0;
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const failOnIdx = argv.indexOf('--fail-on');
|
|
24
|
+
type FailOn = 'off' | 'warn' | 'error';
|
|
25
|
+
const failOn: FailOn = _options.failOn ?? (failOnIdx >= 0 ? (argv[failOnIdx + 1] as FailOn) : 'error');
|
|
26
|
+
const sumIdx = argv.indexOf('--summary');
|
|
27
|
+
type SummaryFmt = 'table' | 'json' | 'none';
|
|
28
|
+
const summaryFmt: SummaryFmt = _options.summary ?? (sumIdx >= 0 ? (argv[sumIdx + 1] as SummaryFmt) : 'none');
|
|
29
|
+
const outputFormat = _options.format ?? 'text';
|
|
30
|
+
|
|
31
|
+
// Collect all issues for JSON output
|
|
32
|
+
const allErrors: ConstraintIssue[] = [];
|
|
33
|
+
const allWarnings: ConstraintIssue[] = [];
|
|
34
|
+
type VRow = { bp: string; rules: number; warnings: number; errors: number };
|
|
35
|
+
const rows: VRow[] = [];
|
|
36
|
+
function pushRow(bpLabel: string, stats: { rules: number; warnings: number; errors: number }) { rows.push({ bp: bpLabel, ...stats }); }
|
|
37
|
+
function printSummaryTable(rs: VRow[]) {
|
|
38
|
+
if (!rs.length) return;
|
|
39
|
+
const showTotalLine = !rs.some(r => r.bp === 'TOTAL');
|
|
40
|
+
const cols = ['scope','rules','warnings','errors'] as const;
|
|
41
|
+
const data = rs.map(r => ({ scope: r.bp, rules: String(r.rules), warnings: String(r.warnings), errors: String(r.errors) }));
|
|
42
|
+
const widths = cols.map(c => Math.max(c.length, ...data.map(d => d[c].length)));
|
|
43
|
+
const line = (vals: string[]) => vals.map((v,i)=>v.padEnd(widths[i])).join(' ');
|
|
44
|
+
console.log(line(cols as unknown as string[]));
|
|
45
|
+
console.log(line(widths.map(w => '-'.repeat(w))));
|
|
46
|
+
for (const d of data) console.log(line(cols.map(c => d[c])));
|
|
47
|
+
if (showTotalLine && rs.length > 1) {
|
|
48
|
+
const tot = rs.reduce((a,b)=>({ rules:a.rules+b.rules, warnings:a.warnings+b.warnings, errors:a.errors+b.errors }), { rules:0,warnings:0,errors:0 });
|
|
49
|
+
console.log(line(['TOTAL', String(tot.rules), String(tot.warnings), String(tot.errors)]));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const cfgRes = loadConfig(_options.config);
|
|
53
|
+
if (!cfgRes.ok) { console.error(cfgRes.error); process.exit(2); }
|
|
54
|
+
const config = cfgRes.value;
|
|
55
|
+
const perBpTimings: Array<{ bp: string; ms: number }> = [];
|
|
56
|
+
const tStartTotal = globalThis.performance.now();
|
|
57
|
+
for (const bp of plan) {
|
|
58
|
+
const tStart = globalThis.performance.now();
|
|
59
|
+
let tokens: TokenNode = loadTokensWithBreakpoint(bp);
|
|
60
|
+
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
61
|
+
if (_options.theme) {
|
|
62
|
+
const themePath = join('tokens/themes', `${_options.theme}.json`);
|
|
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
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Create engine with flattened tokens
|
|
73
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
74
|
+
const init: Record<string, string | number> = {};
|
|
75
|
+
for (const t of Object.values(flat)) {
|
|
76
|
+
init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
77
|
+
}
|
|
78
|
+
const engine = new Engine(init, edges);
|
|
79
|
+
const knownIds = new Set(Object.keys(init));
|
|
80
|
+
|
|
81
|
+
// Discover and attach all constraints via centralized registry
|
|
82
|
+
setupConstraints(
|
|
83
|
+
engine,
|
|
84
|
+
{ config, bp, constraintsDir: 'themes' },
|
|
85
|
+
{ knownIds, crossAxisDebug },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const allIds = new Set(Object.keys(init));
|
|
89
|
+
const issues = engine.evaluate(allIds);
|
|
90
|
+
const errs = issues.filter((i: ConstraintIssue) => i.level === 'error');
|
|
91
|
+
const warns = issues.filter((i: ConstraintIssue) => i.level !== 'error');
|
|
92
|
+
if (errs.length) anyErrors = true;
|
|
93
|
+
totalErrors += errs.length; totalWarnings += warns.length;
|
|
94
|
+
|
|
95
|
+
// Collect for JSON output
|
|
96
|
+
allErrors.push(...errs);
|
|
97
|
+
allWarnings.push(...warns);
|
|
98
|
+
|
|
99
|
+
const rulesEvaluated = errs.length + warns.length; pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
|
|
100
|
+
const dur = globalThis.performance.now() - tStart;
|
|
101
|
+
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
102
|
+
|
|
103
|
+
// Only print text output if not in JSON mode
|
|
104
|
+
if (outputFormat !== 'json') {
|
|
105
|
+
console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
106
|
+
for (const it of issues) { const tag = it.level === 'error' ? 'ERROR' : 'WARN '; console.log(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`); }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const totalMs = globalThis.performance.now() - tStartTotal;
|
|
110
|
+
// Append aggregate total row if multiple scopes and not already added
|
|
111
|
+
if (rows.length > 1) {
|
|
112
|
+
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 });
|
|
113
|
+
rows.push({ bp: 'TOTAL', ...agg });
|
|
114
|
+
}
|
|
115
|
+
// Get package version for stats
|
|
116
|
+
let engineVersion = '1.0.0';
|
|
117
|
+
try {
|
|
118
|
+
// eslint-disable-next-line no-undef
|
|
119
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
120
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
121
|
+
engineVersion = pkg.version;
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore package.json read errors, use default version
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle JSON output mode
|
|
127
|
+
if (outputFormat === 'json') {
|
|
128
|
+
const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
|
|
129
|
+
|
|
130
|
+
// If receipt requested, generate full receipt
|
|
131
|
+
if (_options.receipt) {
|
|
132
|
+
const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
|
|
133
|
+
const constraintsDir = 'themes';
|
|
134
|
+
const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
|
|
135
|
+
writeJsonOutput(receipt, _options.receipt);
|
|
136
|
+
} else {
|
|
137
|
+
writeJsonOutput(result, _options.output);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Text output mode
|
|
141
|
+
if (_options.perf) {
|
|
142
|
+
console.log('[perf] per-breakpoint timings:');
|
|
143
|
+
for (const t of perBpTimings) console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
|
|
144
|
+
console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
|
|
145
|
+
}
|
|
146
|
+
if (summaryFmt === 'json') {
|
|
147
|
+
// Provide machine-readable aggregate separate from rows if TOTAL present
|
|
148
|
+
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
149
|
+
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
150
|
+
console.log(JSON.stringify(json, null, 2));
|
|
151
|
+
} else if (summaryFmt === 'table') {
|
|
152
|
+
printSummaryTable(rows);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let code = anyErrors ? 1 : 0;
|
|
156
|
+
// Budget checks (do not override fail-on semantics unless budgets add failures)
|
|
157
|
+
const budgetTotal = (_options as any)['budget-total-ms'] ?? _options.budgetTotalMs;
|
|
158
|
+
const budgetPerBp = (_options as any)['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
|
|
159
|
+
let budgetFailed = false;
|
|
160
|
+
if (budgetTotal != null && totalMs > budgetTotal) {
|
|
161
|
+
console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
|
|
162
|
+
budgetFailed = true;
|
|
163
|
+
}
|
|
164
|
+
if (budgetPerBp != null) {
|
|
165
|
+
for (const t of perBpTimings) {
|
|
166
|
+
if (t.ms > budgetPerBp) {
|
|
167
|
+
console.error(`[perf] ${t.bp} time ${t.ms.toFixed(2)}ms exceeded per-breakpoint budget ${budgetPerBp}ms`);
|
|
168
|
+
budgetFailed = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (failOn === 'off') code = 0; else if (failOn === 'warn') code = (totalErrors + totalWarnings) > 0 ? 1 : 0; else code = totalErrors > 0 ? 1 : 0;
|
|
173
|
+
if (budgetFailed) code = Math.max(code, 1);
|
|
174
|
+
process.exit(code);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
177
|
+
console.error('validate: failed:', msg);
|
|
178
|
+
process.exit(2);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,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,CAmJnE"}
|
package/cli/commands/why.js
CHANGED
|
@@ -2,61 +2,127 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { flattenTokens } from '../../core/flatten.js';
|
|
3
3
|
import { explain } from '../../core/why.js';
|
|
4
4
|
import { loadTokens } from './utils.js';
|
|
5
|
+
import { Engine } from '../../core/engine.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
5
8
|
export async function whyCommand(options) {
|
|
6
9
|
const tokensPath = options.tokens || 'tokens/tokens.json';
|
|
7
10
|
const tokens = loadTokens(tokensPath);
|
|
8
11
|
const { flat, edges } = flattenTokens(tokens);
|
|
9
12
|
const target = options.tokenId;
|
|
10
13
|
if (!flat[target]) {
|
|
11
|
-
console.error(
|
|
14
|
+
console.error(`Token not found: ${target}`);
|
|
12
15
|
const { suggestIds } = await import('../../core/cli-format.js');
|
|
13
16
|
const suggestions = suggestIds(target, Object.keys(flat));
|
|
14
17
|
if (suggestions.length > 0) {
|
|
15
|
-
console.log(
|
|
16
|
-
suggestions.slice(0, 5).forEach(s => console.log(` ${s.id}`));
|
|
18
|
+
console.log('\nDid you mean:');
|
|
19
|
+
suggestions.slice(0, 5).forEach((s) => console.log(` ${s.id}`));
|
|
17
20
|
}
|
|
18
21
|
else {
|
|
19
|
-
console.log(
|
|
20
|
-
Object.keys(flat)
|
|
21
|
-
|
|
22
|
+
console.log('\nAvailable tokens:');
|
|
23
|
+
Object.keys(flat)
|
|
24
|
+
.sort()
|
|
25
|
+
.slice(0, 10)
|
|
26
|
+
.forEach((id) => console.log(` ${id}`));
|
|
27
|
+
if (Object.keys(flat).length > 10) {
|
|
22
28
|
console.log(` ... and ${Object.keys(flat).length - 10} more`);
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
process.exit(1);
|
|
25
32
|
}
|
|
26
|
-
function safeLoad(p) {
|
|
27
|
-
|
|
33
|
+
function safeLoad(p) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
28
40
|
}
|
|
29
|
-
catch {
|
|
30
|
-
return {};
|
|
31
|
-
} }
|
|
32
41
|
const overrides = safeLoad('tokens/overrides/local.json');
|
|
33
42
|
const theme = safeLoad('themes/theme.json');
|
|
34
|
-
const
|
|
43
|
+
const baseReport = explain(target, flat, edges, {
|
|
44
|
+
overrides: overrides?.overrides ?? overrides,
|
|
45
|
+
theme,
|
|
46
|
+
});
|
|
47
|
+
// Best-effort constraint summary: which rules currently implicate this token
|
|
48
|
+
let constraintsSummary;
|
|
49
|
+
try {
|
|
50
|
+
const cfgRes = loadConfig(options.config);
|
|
51
|
+
if (cfgRes.ok) {
|
|
52
|
+
const config = cfgRes.value;
|
|
53
|
+
// Create engine with flattened tokens
|
|
54
|
+
const init = {};
|
|
55
|
+
for (const t of Object.values(flat)) {
|
|
56
|
+
init[t.id] = t.value;
|
|
57
|
+
}
|
|
58
|
+
const engine = new Engine(init, edges);
|
|
59
|
+
const knownIds = new Set(Object.keys(init));
|
|
60
|
+
// Discover and attach all constraints via centralized registry
|
|
61
|
+
setupConstraints(engine, { config, constraintsDir: 'themes' }, { knownIds });
|
|
62
|
+
const candidates = new Set([target]);
|
|
63
|
+
const allIssues = engine.evaluate(candidates);
|
|
64
|
+
if (allIssues.length) {
|
|
65
|
+
const related = allIssues.filter((issue) => {
|
|
66
|
+
const parts = String(issue.id).split('|');
|
|
67
|
+
return parts.includes(target);
|
|
68
|
+
});
|
|
69
|
+
if (related.length) {
|
|
70
|
+
constraintsSummary = related.map((issue) => ({
|
|
71
|
+
ruleId: issue.rule,
|
|
72
|
+
level: issue.level,
|
|
73
|
+
message: issue.message,
|
|
74
|
+
where: issue.where,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// If constraint analysis fails, fall back to provenance-only report.
|
|
82
|
+
}
|
|
83
|
+
const report = constraintsSummary ? { ...baseReport, constraints: constraintsSummary } : baseReport;
|
|
35
84
|
const format = options.format || 'json';
|
|
36
85
|
if (format === 'table') {
|
|
37
86
|
const { pad, trunc } = await import('../../core/cli-format.js');
|
|
38
87
|
console.log(`\n=== Token Analysis: ${target} ===`);
|
|
39
88
|
console.log(`Value: ${report.value}`);
|
|
40
|
-
console.log(`Raw: ${report.raw
|
|
89
|
+
console.log(`Raw: ${report.raw ?? 'N/A'}`);
|
|
41
90
|
console.log(`Provenance: ${report.provenance}`);
|
|
42
91
|
if (report.dependsOn && report.dependsOn.length > 0) {
|
|
43
92
|
console.log(`\nDependencies (${report.dependsOn.length}):`);
|
|
44
93
|
console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
|
|
45
94
|
console.log('-'.repeat(70));
|
|
46
|
-
report.dependsOn.forEach((depId) => {
|
|
47
|
-
|
|
95
|
+
report.dependsOn.forEach((depId) => {
|
|
96
|
+
const dep = flat[depId];
|
|
97
|
+
if (dep) {
|
|
98
|
+
console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown'));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
48
101
|
}
|
|
49
102
|
if (report.dependents && report.dependents.length > 0) {
|
|
50
103
|
console.log(`\nDependents (${report.dependents.length}):`);
|
|
51
104
|
console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
|
|
52
105
|
console.log('-'.repeat(70));
|
|
53
|
-
report.dependents.forEach((depId) => {
|
|
54
|
-
|
|
106
|
+
report.dependents.forEach((depId) => {
|
|
107
|
+
const dep = flat[depId];
|
|
108
|
+
if (dep) {
|
|
109
|
+
console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown'));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
55
112
|
}
|
|
56
|
-
if (report.refs && report.refs.length > 0)
|
|
113
|
+
if (report.refs && report.refs.length > 0) {
|
|
57
114
|
console.log(`\nReferences: ${report.refs.join(', ')}`);
|
|
58
|
-
|
|
59
|
-
|
|
115
|
+
}
|
|
116
|
+
if (report.chain && report.chain.length > 0) {
|
|
117
|
+
console.log(`\nReference Chain: ${report.chain.join(' -> ')}`);
|
|
118
|
+
}
|
|
119
|
+
if (constraintsSummary && constraintsSummary.length > 0) {
|
|
120
|
+
console.log('\nConstraints (violations involving this token):');
|
|
121
|
+
constraintsSummary.forEach((c) => {
|
|
122
|
+
const where = c.where ? ` @ ${c.where}` : '';
|
|
123
|
+
console.log(`- [${c.level}] ${c.ruleId}${where}: ${c.message}`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
60
126
|
}
|
|
61
127
|
else {
|
|
62
128
|
console.log(JSON.stringify(report, null, 2));
|