design-constraint-validator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +659 -0
  3. package/adapters/README.md +46 -0
  4. package/adapters/css.d.ts +44 -0
  5. package/adapters/css.d.ts.map +1 -0
  6. package/adapters/css.js +97 -0
  7. package/adapters/css.ts +116 -0
  8. package/adapters/js.d.ts +3 -0
  9. package/adapters/js.d.ts.map +1 -0
  10. package/adapters/js.js +15 -0
  11. package/adapters/js.ts +14 -0
  12. package/adapters/json.d.ts +18 -0
  13. package/adapters/json.d.ts.map +1 -0
  14. package/adapters/json.js +35 -0
  15. package/adapters/json.ts +45 -0
  16. package/cli/build-css.d.ts +2 -0
  17. package/cli/build-css.d.ts.map +1 -0
  18. package/cli/build-css.js +23 -0
  19. package/cli/build-css.ts +32 -0
  20. package/cli/commands/build.d.ts +5 -0
  21. package/cli/commands/build.d.ts.map +1 -0
  22. package/cli/commands/build.js +89 -0
  23. package/cli/commands/build.ts +65 -0
  24. package/cli/commands/graph.d.ts +3 -0
  25. package/cli/commands/graph.d.ts.map +1 -0
  26. package/cli/commands/graph.js +219 -0
  27. package/cli/commands/graph.ts +137 -0
  28. package/cli/commands/index.d.ts +8 -0
  29. package/cli/commands/index.d.ts.map +1 -0
  30. package/cli/commands/index.js +7 -0
  31. package/cli/commands/index.ts +7 -0
  32. package/cli/commands/patch-apply.d.ts +3 -0
  33. package/cli/commands/patch-apply.d.ts.map +1 -0
  34. package/cli/commands/patch-apply.js +75 -0
  35. package/cli/commands/patch-apply.ts +80 -0
  36. package/cli/commands/patch.d.ts +3 -0
  37. package/cli/commands/patch.d.ts.map +1 -0
  38. package/cli/commands/patch.js +21 -0
  39. package/cli/commands/patch.ts +22 -0
  40. package/cli/commands/set.d.ts +3 -0
  41. package/cli/commands/set.d.ts.map +1 -0
  42. package/cli/commands/set.js +286 -0
  43. package/cli/commands/set.ts +225 -0
  44. package/cli/commands/utils.d.ts +4 -0
  45. package/cli/commands/utils.d.ts.map +1 -0
  46. package/cli/commands/utils.js +51 -0
  47. package/cli/commands/utils.ts +50 -0
  48. package/cli/commands/validate.d.ts +3 -0
  49. package/cli/commands/validate.d.ts.map +1 -0
  50. package/cli/commands/validate.js +131 -0
  51. package/cli/commands/validate.ts +115 -0
  52. package/cli/commands/why.d.ts +3 -0
  53. package/cli/commands/why.d.ts.map +1 -0
  54. package/cli/commands/why.js +64 -0
  55. package/cli/commands/why.ts +46 -0
  56. package/cli/config-schema.d.ts +238 -0
  57. package/cli/config-schema.d.ts.map +1 -0
  58. package/cli/config-schema.js +21 -0
  59. package/cli/config-schema.ts +27 -0
  60. package/cli/config.d.ts +4 -0
  61. package/cli/config.d.ts.map +1 -0
  62. package/cli/config.js +37 -0
  63. package/cli/config.ts +35 -0
  64. package/cli/dcv.d.ts +3 -0
  65. package/cli/dcv.d.ts.map +1 -0
  66. package/cli/dcv.js +86 -0
  67. package/cli/dcv.ts +107 -0
  68. package/cli/engine-helpers.d.ts +8 -0
  69. package/cli/engine-helpers.d.ts.map +1 -0
  70. package/cli/engine-helpers.js +70 -0
  71. package/cli/engine-helpers.ts +61 -0
  72. package/cli/graph-poset.d.ts +9 -0
  73. package/cli/graph-poset.d.ts.map +1 -0
  74. package/cli/graph-poset.js +58 -0
  75. package/cli/graph-poset.ts +74 -0
  76. package/cli/index.d.ts +3 -0
  77. package/cli/index.d.ts.map +1 -0
  78. package/cli/index.js +2 -0
  79. package/cli/index.ts +2 -0
  80. package/cli/result.d.ts +17 -0
  81. package/cli/result.d.ts.map +1 -0
  82. package/cli/result.js +29 -0
  83. package/cli/result.ts +27 -0
  84. package/cli/run.d.ts +3 -0
  85. package/cli/run.d.ts.map +1 -0
  86. package/cli/run.js +47 -0
  87. package/cli/run.ts +54 -0
  88. package/cli/smoke-test.d.ts +2 -0
  89. package/cli/smoke-test.d.ts.map +1 -0
  90. package/cli/smoke-test.js +33 -0
  91. package/cli/smoke-test.ts +40 -0
  92. package/cli/types.d.ts +86 -0
  93. package/cli/types.d.ts.map +1 -0
  94. package/cli/types.js +1 -0
  95. package/cli/types.ts +78 -0
  96. package/core/breakpoints.d.ts +12 -0
  97. package/core/breakpoints.d.ts.map +1 -0
  98. package/core/breakpoints.js +48 -0
  99. package/core/breakpoints.ts +50 -0
  100. package/core/cli-format.d.ts +8 -0
  101. package/core/cli-format.d.ts.map +1 -0
  102. package/core/cli-format.js +29 -0
  103. package/core/cli-format.ts +31 -0
  104. package/core/color.d.ts +14 -0
  105. package/core/color.d.ts.map +1 -0
  106. package/core/color.js +136 -0
  107. package/core/color.ts +148 -0
  108. package/core/constraints/cross-axis.d.ts +33 -0
  109. package/core/constraints/cross-axis.d.ts.map +1 -0
  110. package/core/constraints/cross-axis.js +93 -0
  111. package/core/constraints/cross-axis.ts +114 -0
  112. package/core/constraints/monotonic-lightness.d.ts +5 -0
  113. package/core/constraints/monotonic-lightness.d.ts.map +1 -0
  114. package/core/constraints/monotonic-lightness.js +37 -0
  115. package/core/constraints/monotonic-lightness.ts +38 -0
  116. package/core/constraints/monotonic.d.ts +7 -0
  117. package/core/constraints/monotonic.d.ts.map +1 -0
  118. package/core/constraints/monotonic.js +65 -0
  119. package/core/constraints/monotonic.ts +74 -0
  120. package/core/constraints/threshold.d.ts +10 -0
  121. package/core/constraints/threshold.d.ts.map +1 -0
  122. package/core/constraints/threshold.js +36 -0
  123. package/core/constraints/threshold.ts +43 -0
  124. package/core/constraints/wcag.d.ts +11 -0
  125. package/core/constraints/wcag.d.ts.map +1 -0
  126. package/core/constraints/wcag.js +53 -0
  127. package/core/constraints/wcag.ts +70 -0
  128. package/core/cross-axis-config.d.ts +5 -0
  129. package/core/cross-axis-config.d.ts.map +1 -0
  130. package/core/cross-axis-config.js +144 -0
  131. package/core/cross-axis-config.ts +152 -0
  132. package/core/engine.d.ts +32 -0
  133. package/core/engine.d.ts.map +1 -0
  134. package/core/engine.js +46 -0
  135. package/core/engine.ts +65 -0
  136. package/core/flatten.d.ts +20 -0
  137. package/core/flatten.d.ts.map +1 -0
  138. package/core/flatten.js +80 -0
  139. package/core/flatten.ts +116 -0
  140. package/core/image-export.d.ts +10 -0
  141. package/core/image-export.d.ts.map +1 -0
  142. package/core/image-export.js +43 -0
  143. package/core/image-export.ts +48 -0
  144. package/core/index.d.ts +31 -0
  145. package/core/index.d.ts.map +1 -0
  146. package/core/index.js +54 -0
  147. package/core/index.ts +72 -0
  148. package/core/patch.d.ts +28 -0
  149. package/core/patch.d.ts.map +1 -0
  150. package/core/patch.js +110 -0
  151. package/core/patch.ts +134 -0
  152. package/core/poset.d.ts +41 -0
  153. package/core/poset.d.ts.map +1 -0
  154. package/core/poset.js +275 -0
  155. package/core/poset.ts +311 -0
  156. package/core/why.d.ts +17 -0
  157. package/core/why.d.ts.map +1 -0
  158. package/core/why.js +45 -0
  159. package/core/why.ts +63 -0
  160. package/dist/test-overrides-removal.json +4 -0
  161. package/dist/tmp.patch.json +35 -0
  162. package/package.json +90 -0
  163. package/themes/color.lg.order.json +15 -0
  164. package/themes/color.md.order.json +15 -0
  165. package/themes/color.order.json +15 -0
  166. package/themes/color.sm.order.json +15 -0
  167. package/themes/cross-axis.rules.json +36 -0
  168. package/themes/cross-axis.sm.rules.json +12 -0
  169. package/themes/layout.lg.order.json +18 -0
  170. package/themes/layout.md.order.json +18 -0
  171. package/themes/layout.order.json +18 -0
  172. package/themes/layout.sm.order.json +18 -0
  173. package/themes/spacing.order.json +14 -0
  174. package/themes/typography.lg.order.json +15 -0
  175. package/themes/typography.md.order.json +15 -0
  176. package/themes/typography.order.json +15 -0
  177. package/themes/typography.sm.order.json +15 -0
  178. package/tokens/overrides/base.json +22 -0
  179. package/tokens/overrides/lg.json +20 -0
  180. package/tokens/overrides/md.json +16 -0
  181. package/tokens/overrides/sm.json +16 -0
  182. package/tokens/overrides/viol.color.json +6 -0
  183. package/tokens/overrides/viol.typography.json +6 -0
  184. package/tokens/tokens.demo-violations.json +116 -0
  185. package/tokens/tokens.example.json +128 -0
  186. package/tokens/tokens.json +67 -0
  187. package/tokens/tokens.multi-violations.json +21 -0
  188. package/tokens/tokens.schema.d.ts +2298 -0
  189. package/tokens/tokens.schema.d.ts.map +1 -0
  190. package/tokens/tokens.schema.js +148 -0
  191. package/tokens/tokens.schema.ts +196 -0
  192. package/tokens/tokens.test.json +38 -0
  193. package/tokens/tokens.touch-violation.json +8 -0
  194. package/tokens/typography.classes.css +11 -0
  195. package/tokens/typography.css +20 -0
@@ -0,0 +1,3 @@
1
+ import type { ValidateOptions } from '../types.js';
2
+ export declare function validateCommand(_options: ValidateOptions): Promise<void>;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA0G9E"}
@@ -0,0 +1,131 @@
1
+ import { flattenTokens } from '../../core/flatten.js';
2
+ import { createValidationEngine } from '../engine-helpers.js';
3
+ import { loadConfig } from '../config.js';
4
+ import { parseBreakpoints, loadTokensWithBreakpoint } from '../../core/breakpoints.js';
5
+ import { loadCrossAxisPlugin } from '../../core/cross-axis-config.js';
6
+ export async function validateCommand(_options) {
7
+ try {
8
+ const bps = parseBreakpoints(process.argv);
9
+ const crossAxisDebug = process.argv.includes('--cross-axis-debug');
10
+ const plan = bps.length ? bps : [undefined];
11
+ let anyErrors = false;
12
+ let totalErrors = 0;
13
+ let totalWarnings = 0;
14
+ const argv = process.argv.slice(2);
15
+ const failOnIdx = argv.indexOf('--fail-on');
16
+ const failOn = failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error';
17
+ const sumIdx = argv.indexOf('--summary');
18
+ const summaryFmt = sumIdx >= 0 ? argv[sumIdx + 1] : 'none';
19
+ const rows = [];
20
+ function pushRow(bpLabel, stats) { rows.push({ bp: bpLabel, ...stats }); }
21
+ function printSummaryTable(rs) {
22
+ if (!rs.length)
23
+ return;
24
+ const showTotalLine = !rs.some(r => r.bp === 'TOTAL');
25
+ const cols = ['scope', 'rules', 'warnings', 'errors'];
26
+ const data = rs.map(r => ({ scope: r.bp, rules: String(r.rules), warnings: String(r.warnings), errors: String(r.errors) }));
27
+ const widths = cols.map(c => Math.max(c.length, ...data.map(d => d[c].length)));
28
+ const line = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(' ');
29
+ console.log(line(cols));
30
+ console.log(line(widths.map(w => '-'.repeat(w))));
31
+ for (const d of data)
32
+ console.log(line(cols.map(c => d[c])));
33
+ if (showTotalLine && rs.length > 1) {
34
+ 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 });
35
+ console.log(line(['TOTAL', String(tot.rules), String(tot.warnings), String(tot.errors)]));
36
+ }
37
+ }
38
+ const cfgRes = loadConfig(_options.config);
39
+ if (!cfgRes.ok) {
40
+ console.error(cfgRes.error);
41
+ process.exit(2);
42
+ }
43
+ const config = cfgRes.value;
44
+ const perBpTimings = [];
45
+ const tStartTotal = globalThis.performance.now();
46
+ for (const bp of plan) {
47
+ const tStart = globalThis.performance.now();
48
+ const tokens = loadTokensWithBreakpoint(bp);
49
+ const engine = createValidationEngine(tokens, bp, config);
50
+ const initIds = Object.keys(flattenTokens(tokens).flat).reduce((a, k) => { a[k] = true; return a; }, {});
51
+ const knownIds = new Set(Object.keys(initIds));
52
+ // Load global + bp-specific cross-axis rules (bp file optional)
53
+ engine.use(loadCrossAxisPlugin('themes/cross-axis.rules.json', bp, { debug: crossAxisDebug, knownIds }));
54
+ if (bp) {
55
+ const bpRulesPath = `themes/cross-axis.${bp}.rules.json`;
56
+ engine.use(loadCrossAxisPlugin(bpRulesPath, bp, { debug: crossAxisDebug, knownIds }));
57
+ }
58
+ const { ThresholdPlugin } = await import('../../core/constraints/threshold.js');
59
+ engine.use(ThresholdPlugin([{ id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' }]));
60
+ const allIds = new Set(Object.keys(initIds));
61
+ const issues = engine.evaluate(allIds);
62
+ const errs = issues.filter((i) => i.level === 'error');
63
+ const warns = issues.filter((i) => i.level !== 'error');
64
+ if (errs.length)
65
+ anyErrors = true;
66
+ totalErrors += errs.length;
67
+ totalWarnings += warns.length;
68
+ const rulesEvaluated = errs.length + warns.length;
69
+ pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
70
+ const dur = globalThis.performance.now() - tStart;
71
+ perBpTimings.push({ bp: bp ?? 'global', ms: dur });
72
+ console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
73
+ for (const it of issues) {
74
+ const tag = it.level === 'error' ? 'ERROR' : 'WARN ';
75
+ console.log(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`);
76
+ }
77
+ }
78
+ const totalMs = globalThis.performance.now() - tStartTotal;
79
+ // Append aggregate total row if multiple scopes and not already added
80
+ if (rows.length > 1) {
81
+ 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
+ rows.push({ bp: 'TOTAL', ...agg });
83
+ }
84
+ if (_options.perf) {
85
+ console.log('[perf] per-breakpoint timings:');
86
+ for (const t of perBpTimings)
87
+ console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
88
+ console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
89
+ }
90
+ if (summaryFmt === 'json') {
91
+ // Provide machine-readable aggregate separate from rows if TOTAL present
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));
95
+ }
96
+ else if (summaryFmt === 'table') {
97
+ printSummaryTable(rows);
98
+ }
99
+ let code = anyErrors ? 1 : 0;
100
+ // Budget checks (do not override fail-on semantics unless budgets add failures)
101
+ const budgetTotal = _options['budget-total-ms'] ?? _options.budgetTotalMs;
102
+ const budgetPerBp = _options['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
103
+ let budgetFailed = false;
104
+ if (budgetTotal != null && totalMs > budgetTotal) {
105
+ console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
106
+ budgetFailed = true;
107
+ }
108
+ if (budgetPerBp != null) {
109
+ for (const t of perBpTimings) {
110
+ if (t.ms > budgetPerBp) {
111
+ console.error(`[perf] ${t.bp} time ${t.ms.toFixed(2)}ms exceeded per-breakpoint budget ${budgetPerBp}ms`);
112
+ budgetFailed = true;
113
+ }
114
+ }
115
+ }
116
+ if (failOn === 'off')
117
+ code = 0;
118
+ else if (failOn === 'warn')
119
+ code = (totalErrors + totalWarnings) > 0 ? 1 : 0;
120
+ else
121
+ code = totalErrors > 0 ? 1 : 0;
122
+ if (budgetFailed)
123
+ code = Math.max(code, 1);
124
+ process.exit(code);
125
+ }
126
+ catch (e) {
127
+ const msg = e instanceof Error ? e.message : String(e);
128
+ console.error('validate: failed:', msg);
129
+ process.exit(2);
130
+ }
131
+ }
@@ -0,0 +1,115 @@
1
+ import { flattenTokens } from '../../core/flatten.js';
2
+ import { createValidationEngine } from '../engine-helpers.js';
3
+ import { loadConfig } from '../config.js';
4
+ import { parseBreakpoints, loadTokensWithBreakpoint, type Breakpoint } from '../../core/breakpoints.js';
5
+ import { loadCrossAxisPlugin } from '../../core/cross-axis-config.js';
6
+ import type { ConstraintIssue } from '../../core/engine.js';
7
+ import type { ValidateOptions } from '../types.js';
8
+
9
+ export async function validateCommand(_options: ValidateOptions): Promise<void> {
10
+ try {
11
+ const bps = parseBreakpoints(process.argv);
12
+ const crossAxisDebug = process.argv.includes('--cross-axis-debug');
13
+ const plan: (Breakpoint | undefined)[] = bps.length ? bps : [undefined];
14
+ let anyErrors = false; let totalErrors = 0; let totalWarnings = 0;
15
+ const argv = process.argv.slice(2);
16
+ const failOnIdx = argv.indexOf('--fail-on');
17
+ type FailOn = 'off' | 'warn' | 'error';
18
+ const failOn: FailOn = failOnIdx >= 0 ? (argv[failOnIdx + 1] as FailOn) : 'error';
19
+ const sumIdx = argv.indexOf('--summary');
20
+ type SummaryFmt = 'table' | 'json' | 'none';
21
+ const summaryFmt: SummaryFmt = sumIdx >= 0 ? (argv[sumIdx + 1] as SummaryFmt) : 'none';
22
+ type VRow = { bp: string; rules: number; warnings: number; errors: number };
23
+ const rows: VRow[] = [];
24
+ function pushRow(bpLabel: string, stats: { rules: number; warnings: number; errors: number }) { rows.push({ bp: bpLabel, ...stats }); }
25
+ function printSummaryTable(rs: VRow[]) {
26
+ if (!rs.length) return;
27
+ const showTotalLine = !rs.some(r => r.bp === 'TOTAL');
28
+ const cols = ['scope','rules','warnings','errors'] as const;
29
+ const data = rs.map(r => ({ scope: r.bp, rules: String(r.rules), warnings: String(r.warnings), errors: String(r.errors) }));
30
+ const widths = cols.map(c => Math.max(c.length, ...data.map(d => d[c].length)));
31
+ const line = (vals: string[]) => vals.map((v,i)=>v.padEnd(widths[i])).join(' ');
32
+ console.log(line(cols as unknown as string[]));
33
+ console.log(line(widths.map(w => '-'.repeat(w))));
34
+ for (const d of data) console.log(line(cols.map(c => d[c])));
35
+ if (showTotalLine && rs.length > 1) {
36
+ 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 });
37
+ console.log(line(['TOTAL', String(tot.rules), String(tot.warnings), String(tot.errors)]));
38
+ }
39
+ }
40
+ const cfgRes = loadConfig(_options.config);
41
+ if (!cfgRes.ok) { console.error(cfgRes.error); process.exit(2); }
42
+ const config = cfgRes.value;
43
+ const perBpTimings: Array<{ bp: string; ms: number }> = [];
44
+ const tStartTotal = globalThis.performance.now();
45
+ for (const bp of plan) {
46
+ const tStart = globalThis.performance.now();
47
+ const tokens = loadTokensWithBreakpoint(bp);
48
+ const engine = createValidationEngine(tokens, bp, config);
49
+ const initIds = Object.keys(flattenTokens(tokens).flat).reduce<Record<string, true>>((a,k)=>{a[k]=true;return a;},{});
50
+ const knownIds = new Set(Object.keys(initIds));
51
+ // Load global + bp-specific cross-axis rules (bp file optional)
52
+ engine.use(loadCrossAxisPlugin('themes/cross-axis.rules.json', bp, { debug: crossAxisDebug, knownIds }));
53
+ if (bp) {
54
+ const bpRulesPath = `themes/cross-axis.${bp}.rules.json`;
55
+ engine.use(loadCrossAxisPlugin(bpRulesPath, bp, { debug: crossAxisDebug, knownIds }));
56
+ }
57
+ const { ThresholdPlugin } = await import('../../core/constraints/threshold.js');
58
+ engine.use(ThresholdPlugin([{ id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' }]));
59
+ const allIds = new Set(Object.keys(initIds));
60
+ const issues = engine.evaluate(allIds);
61
+ const errs = issues.filter((i: ConstraintIssue) => i.level === 'error');
62
+ const warns = issues.filter((i: ConstraintIssue) => i.level !== 'error');
63
+ if (errs.length) anyErrors = true;
64
+ totalErrors += errs.length; totalWarnings += warns.length;
65
+ const rulesEvaluated = errs.length + warns.length; pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
66
+ const dur = globalThis.performance.now() - tStart;
67
+ perBpTimings.push({ bp: bp ?? 'global', ms: dur });
68
+ console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
69
+ 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}`); }
70
+ }
71
+ const totalMs = globalThis.performance.now() - tStartTotal;
72
+ // Append aggregate total row if multiple scopes and not already added
73
+ if (rows.length > 1) {
74
+ 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 });
75
+ rows.push({ bp: 'TOTAL', ...agg });
76
+ }
77
+ if (_options.perf) {
78
+ console.log('[perf] per-breakpoint timings:');
79
+ for (const t of perBpTimings) console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
80
+ console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
81
+ }
82
+ if (summaryFmt === 'json') {
83
+ // Provide machine-readable aggregate separate from rows if TOTAL present
84
+ const totalRow = rows.find(r => r.bp === 'TOTAL');
85
+ const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
86
+ console.log(JSON.stringify(json, null, 2));
87
+ } else if (summaryFmt === 'table') {
88
+ printSummaryTable(rows);
89
+ }
90
+ let code = anyErrors ? 1 : 0;
91
+ // Budget checks (do not override fail-on semantics unless budgets add failures)
92
+ const budgetTotal = (_options as any)['budget-total-ms'] ?? _options.budgetTotalMs;
93
+ const budgetPerBp = (_options as any)['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
94
+ let budgetFailed = false;
95
+ if (budgetTotal != null && totalMs > budgetTotal) {
96
+ console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
97
+ budgetFailed = true;
98
+ }
99
+ if (budgetPerBp != null) {
100
+ for (const t of perBpTimings) {
101
+ if (t.ms > budgetPerBp) {
102
+ console.error(`[perf] ${t.bp} time ${t.ms.toFixed(2)}ms exceeded per-breakpoint budget ${budgetPerBp}ms`);
103
+ budgetFailed = true;
104
+ }
105
+ }
106
+ }
107
+ if (failOn === 'off') code = 0; else if (failOn === 'warn') code = (totalErrors + totalWarnings) > 0 ? 1 : 0; else code = totalErrors > 0 ? 1 : 0;
108
+ if (budgetFailed) code = Math.max(code, 1);
109
+ process.exit(code);
110
+ } catch (e) {
111
+ const msg = e instanceof Error ? e.message : String(e);
112
+ console.error('validate: failed:', msg);
113
+ process.exit(2);
114
+ }
115
+ }
@@ -0,0 +1,3 @@
1
+ import type { WhyOptions } from '../types.js';
2
+ export declare function whyCommand(options: WhyOptions): Promise<void>;
3
+ //# sourceMappingURL=why.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnE"}
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { flattenTokens } from '../../core/flatten.js';
3
+ import { explain } from '../../core/why.js';
4
+ import { loadTokens } from './utils.js';
5
+ export async function whyCommand(options) {
6
+ const tokensPath = options.tokens || 'tokens/tokens.json';
7
+ const tokens = loadTokens(tokensPath);
8
+ const { flat, edges } = flattenTokens(tokens);
9
+ const target = options.tokenId;
10
+ if (!flat[target]) {
11
+ console.error(`❌ Token not found: ${target}`);
12
+ const { suggestIds } = await import('../../core/cli-format.js');
13
+ const suggestions = suggestIds(target, Object.keys(flat));
14
+ if (suggestions.length > 0) {
15
+ console.log(`\nDid you mean:`);
16
+ suggestions.slice(0, 5).forEach(s => console.log(` ${s.id}`));
17
+ }
18
+ else {
19
+ console.log(`\nAvailable tokens:`);
20
+ Object.keys(flat).sort().slice(0, 10).forEach(id => console.log(` ${id}`));
21
+ if (Object.keys(flat).length > 10)
22
+ console.log(` ... and ${Object.keys(flat).length - 10} more`);
23
+ }
24
+ process.exit(1);
25
+ }
26
+ function safeLoad(p) { try {
27
+ return JSON.parse(readFileSync(p, 'utf8'));
28
+ }
29
+ catch {
30
+ return {};
31
+ } }
32
+ const overrides = safeLoad('tokens/overrides/local.json');
33
+ const theme = safeLoad('themes/theme.json');
34
+ const report = explain(target, flat, edges, { overrides: overrides?.overrides ?? overrides, theme });
35
+ const format = options.format || 'json';
36
+ if (format === 'table') {
37
+ const { pad, trunc } = await import('../../core/cli-format.js');
38
+ console.log(`\n=== Token Analysis: ${target} ===`);
39
+ console.log(`Value: ${report.value}`);
40
+ console.log(`Raw: ${report.raw || 'N/A'}`);
41
+ console.log(`Provenance: ${report.provenance}`);
42
+ if (report.dependsOn && report.dependsOn.length > 0) {
43
+ console.log(`\nDependencies (${report.dependsOn.length}):`);
44
+ console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
45
+ console.log('-'.repeat(70));
46
+ report.dependsOn.forEach((depId) => { const dep = flat[depId]; if (dep)
47
+ console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown')); });
48
+ }
49
+ if (report.dependents && report.dependents.length > 0) {
50
+ console.log(`\nDependents (${report.dependents.length}):`);
51
+ console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
52
+ console.log('-'.repeat(70));
53
+ report.dependents.forEach((depId) => { const dep = flat[depId]; if (dep)
54
+ console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown')); });
55
+ }
56
+ if (report.refs && report.refs.length > 0)
57
+ console.log(`\nReferences: ${report.refs.join(', ')}`);
58
+ if (report.chain && report.chain.length > 0)
59
+ console.log(`\nReference Chain: ${report.chain.join(' → ')}`);
60
+ }
61
+ else {
62
+ console.log(JSON.stringify(report, null, 2));
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { flattenTokens } from '../../core/flatten.js';
3
+ import { explain } from '../../core/why.js';
4
+ import type { WhyOptions } from '../types.js';
5
+ import { loadTokens } from './utils.js';
6
+
7
+ export async function whyCommand(options: WhyOptions): Promise<void> {
8
+ const tokensPath = options.tokens || 'tokens/tokens.json';
9
+ const tokens = loadTokens(tokensPath);
10
+ const { flat, edges } = flattenTokens(tokens);
11
+ const target = options.tokenId;
12
+ if (!flat[target]) {
13
+ console.error(`❌ Token not found: ${target}`);
14
+ const { suggestIds } = await import('../../core/cli-format.js');
15
+ const suggestions = suggestIds(target, Object.keys(flat));
16
+ if (suggestions.length > 0) {
17
+ console.log(`\nDid you mean:`); suggestions.slice(0, 5).forEach(s => console.log(` ${s.id}`));
18
+ } else {
19
+ console.log(`\nAvailable tokens:`); Object.keys(flat).sort().slice(0, 10).forEach(id => console.log(` ${id}`));
20
+ if (Object.keys(flat).length > 10) console.log(` ... and ${Object.keys(flat).length - 10} more`);
21
+ }
22
+ process.exit(1);
23
+ }
24
+ function safeLoad(p: string) { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return {}; } }
25
+ const overrides = safeLoad('tokens/overrides/local.json');
26
+ const theme = safeLoad('themes/theme.json');
27
+ const report = explain(target, flat, edges, { overrides: overrides?.overrides ?? overrides, theme });
28
+ const format = options.format || 'json';
29
+ if (format === 'table') {
30
+ const { pad, trunc } = await import('../../core/cli-format.js');
31
+ console.log(`\n=== Token Analysis: ${target} ===`);
32
+ console.log(`Value: ${report.value}`); console.log(`Raw: ${report.raw || 'N/A'}`); console.log(`Provenance: ${report.provenance}`);
33
+ if (report.dependsOn && report.dependsOn.length > 0) {
34
+ console.log(`\nDependencies (${report.dependsOn.length}):`); console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE'); console.log('-'.repeat(70));
35
+ report.dependsOn.forEach((depId: string) => { const dep = flat[depId]; if (dep) console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown')); });
36
+ }
37
+ if (report.dependents && report.dependents.length > 0) {
38
+ console.log(`\nDependents (${report.dependents.length}):`); console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE'); console.log('-'.repeat(70));
39
+ report.dependents.forEach((depId: string) => { const dep = flat[depId]; if (dep) console.log(pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown')); });
40
+ }
41
+ if (report.refs && report.refs.length > 0) console.log(`\nReferences: ${report.refs.join(', ')}`);
42
+ if (report.chain && report.chain.length > 0) console.log(`\nReference Chain: ${report.chain.join(' → ')}`);
43
+ } else {
44
+ console.log(JSON.stringify(report, null, 2));
45
+ }
46
+ }
@@ -0,0 +1,238 @@
1
+ import { z } from 'zod';
2
+ export declare const WcagRuleSchema: z.ZodObject<{
3
+ foreground: z.ZodString;
4
+ background: z.ZodString;
5
+ ratio: z.ZodOptional<z.ZodNumber>;
6
+ description: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ foreground: string;
9
+ background: string;
10
+ description?: string | undefined;
11
+ ratio?: number | undefined;
12
+ }, {
13
+ foreground: string;
14
+ background: string;
15
+ description?: string | undefined;
16
+ ratio?: number | undefined;
17
+ }>;
18
+ export declare const ConstraintsSchema: z.ZodObject<{
19
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ foreground: z.ZodString;
21
+ background: z.ZodString;
22
+ ratio: z.ZodOptional<z.ZodNumber>;
23
+ description: z.ZodOptional<z.ZodString>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ foreground: string;
26
+ background: string;
27
+ description?: string | undefined;
28
+ ratio?: number | undefined;
29
+ }, {
30
+ foreground: string;
31
+ background: string;
32
+ description?: string | undefined;
33
+ ratio?: number | undefined;
34
+ }>, "many">>;
35
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
36
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
37
+ foreground: z.ZodString;
38
+ background: z.ZodString;
39
+ ratio: z.ZodOptional<z.ZodNumber>;
40
+ description: z.ZodOptional<z.ZodString>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ foreground: string;
43
+ background: string;
44
+ description?: string | undefined;
45
+ ratio?: number | undefined;
46
+ }, {
47
+ foreground: string;
48
+ background: string;
49
+ description?: string | undefined;
50
+ ratio?: number | undefined;
51
+ }>, "many">>;
52
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
53
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ foreground: z.ZodString;
55
+ background: z.ZodString;
56
+ ratio: z.ZodOptional<z.ZodNumber>;
57
+ description: z.ZodOptional<z.ZodString>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ foreground: string;
60
+ background: string;
61
+ description?: string | undefined;
62
+ ratio?: number | undefined;
63
+ }, {
64
+ foreground: string;
65
+ background: string;
66
+ description?: string | undefined;
67
+ ratio?: number | undefined;
68
+ }>, "many">>;
69
+ }, z.ZodTypeAny, "passthrough">>;
70
+ export declare const DcvConfigSchema: z.ZodObject<{
71
+ version: z.ZodOptional<z.ZodString>;
72
+ constraints: z.ZodOptional<z.ZodObject<{
73
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
74
+ foreground: z.ZodString;
75
+ background: z.ZodString;
76
+ ratio: z.ZodOptional<z.ZodNumber>;
77
+ description: z.ZodOptional<z.ZodString>;
78
+ }, "strip", z.ZodTypeAny, {
79
+ foreground: string;
80
+ background: string;
81
+ description?: string | undefined;
82
+ ratio?: number | undefined;
83
+ }, {
84
+ foreground: string;
85
+ background: string;
86
+ description?: string | undefined;
87
+ ratio?: number | undefined;
88
+ }>, "many">>;
89
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
90
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
91
+ foreground: z.ZodString;
92
+ background: z.ZodString;
93
+ ratio: z.ZodOptional<z.ZodNumber>;
94
+ description: z.ZodOptional<z.ZodString>;
95
+ }, "strip", z.ZodTypeAny, {
96
+ foreground: string;
97
+ background: string;
98
+ description?: string | undefined;
99
+ ratio?: number | undefined;
100
+ }, {
101
+ foreground: string;
102
+ background: string;
103
+ description?: string | undefined;
104
+ ratio?: number | undefined;
105
+ }>, "many">>;
106
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
107
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
108
+ foreground: z.ZodString;
109
+ background: z.ZodString;
110
+ ratio: z.ZodOptional<z.ZodNumber>;
111
+ description: z.ZodOptional<z.ZodString>;
112
+ }, "strip", z.ZodTypeAny, {
113
+ foreground: string;
114
+ background: string;
115
+ description?: string | undefined;
116
+ ratio?: number | undefined;
117
+ }, {
118
+ foreground: string;
119
+ background: string;
120
+ description?: string | undefined;
121
+ ratio?: number | undefined;
122
+ }>, "many">>;
123
+ }, z.ZodTypeAny, "passthrough">>>;
124
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
125
+ version: z.ZodOptional<z.ZodString>;
126
+ constraints: z.ZodOptional<z.ZodObject<{
127
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
128
+ foreground: z.ZodString;
129
+ background: z.ZodString;
130
+ ratio: z.ZodOptional<z.ZodNumber>;
131
+ description: z.ZodOptional<z.ZodString>;
132
+ }, "strip", z.ZodTypeAny, {
133
+ foreground: string;
134
+ background: string;
135
+ description?: string | undefined;
136
+ ratio?: number | undefined;
137
+ }, {
138
+ foreground: string;
139
+ background: string;
140
+ description?: string | undefined;
141
+ ratio?: number | undefined;
142
+ }>, "many">>;
143
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
144
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
145
+ foreground: z.ZodString;
146
+ background: z.ZodString;
147
+ ratio: z.ZodOptional<z.ZodNumber>;
148
+ description: z.ZodOptional<z.ZodString>;
149
+ }, "strip", z.ZodTypeAny, {
150
+ foreground: string;
151
+ background: string;
152
+ description?: string | undefined;
153
+ ratio?: number | undefined;
154
+ }, {
155
+ foreground: string;
156
+ background: string;
157
+ description?: string | undefined;
158
+ ratio?: number | undefined;
159
+ }>, "many">>;
160
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
161
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
162
+ foreground: z.ZodString;
163
+ background: z.ZodString;
164
+ ratio: z.ZodOptional<z.ZodNumber>;
165
+ description: z.ZodOptional<z.ZodString>;
166
+ }, "strip", z.ZodTypeAny, {
167
+ foreground: string;
168
+ background: string;
169
+ description?: string | undefined;
170
+ ratio?: number | undefined;
171
+ }, {
172
+ foreground: string;
173
+ background: string;
174
+ description?: string | undefined;
175
+ ratio?: number | undefined;
176
+ }>, "many">>;
177
+ }, z.ZodTypeAny, "passthrough">>>;
178
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
179
+ version: z.ZodOptional<z.ZodString>;
180
+ constraints: z.ZodOptional<z.ZodObject<{
181
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
182
+ foreground: z.ZodString;
183
+ background: z.ZodString;
184
+ ratio: z.ZodOptional<z.ZodNumber>;
185
+ description: z.ZodOptional<z.ZodString>;
186
+ }, "strip", z.ZodTypeAny, {
187
+ foreground: string;
188
+ background: string;
189
+ description?: string | undefined;
190
+ ratio?: number | undefined;
191
+ }, {
192
+ foreground: string;
193
+ background: string;
194
+ description?: string | undefined;
195
+ ratio?: number | undefined;
196
+ }>, "many">>;
197
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
198
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
199
+ foreground: z.ZodString;
200
+ background: z.ZodString;
201
+ ratio: z.ZodOptional<z.ZodNumber>;
202
+ description: z.ZodOptional<z.ZodString>;
203
+ }, "strip", z.ZodTypeAny, {
204
+ foreground: string;
205
+ background: string;
206
+ description?: string | undefined;
207
+ ratio?: number | undefined;
208
+ }, {
209
+ foreground: string;
210
+ background: string;
211
+ description?: string | undefined;
212
+ ratio?: number | undefined;
213
+ }>, "many">>;
214
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
215
+ wcag: z.ZodOptional<z.ZodArray<z.ZodObject<{
216
+ foreground: z.ZodString;
217
+ background: z.ZodString;
218
+ ratio: z.ZodOptional<z.ZodNumber>;
219
+ description: z.ZodOptional<z.ZodString>;
220
+ }, "strip", z.ZodTypeAny, {
221
+ foreground: string;
222
+ background: string;
223
+ description?: string | undefined;
224
+ ratio?: number | undefined;
225
+ }, {
226
+ foreground: string;
227
+ background: string;
228
+ description?: string | undefined;
229
+ ratio?: number | undefined;
230
+ }>, "many">>;
231
+ }, z.ZodTypeAny, "passthrough">>>;
232
+ }, z.ZodTypeAny, "passthrough">>;
233
+ export type DcvConfigParsed = z.infer<typeof DcvConfigSchema>;
234
+ export declare function validateConfig(raw: unknown): {
235
+ value?: DcvConfigParsed;
236
+ errors?: string[];
237
+ };
238
+ //# sourceMappingURL=config-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAKzB,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAEd,CAAC;AAEjB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAGZ,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,KAAK,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAM3F"}
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export const WcagRuleSchema = z.object({
3
+ foreground: z.string(),
4
+ background: z.string(),
5
+ ratio: z.number().positive().optional(),
6
+ description: z.string().optional()
7
+ });
8
+ export const ConstraintsSchema = z.object({
9
+ wcag: z.array(WcagRuleSchema).optional()
10
+ }).passthrough();
11
+ export const DcvConfigSchema = z.object({
12
+ version: z.string().optional(),
13
+ constraints: ConstraintsSchema.optional()
14
+ }).passthrough();
15
+ export function validateConfig(raw) {
16
+ const res = DcvConfigSchema.safeParse(raw);
17
+ if (!res.success) {
18
+ return { errors: res.error.errors.map(e => `${e.path.join('.') || '<root>'}: ${e.message}`) };
19
+ }
20
+ return { value: res.data };
21
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+
3
+ export const WcagRuleSchema = z.object({
4
+ foreground: z.string(),
5
+ background: z.string(),
6
+ ratio: z.number().positive().optional(),
7
+ description: z.string().optional()
8
+ });
9
+
10
+ export const ConstraintsSchema = z.object({
11
+ wcag: z.array(WcagRuleSchema).optional()
12
+ }).passthrough();
13
+
14
+ export const DcvConfigSchema = z.object({
15
+ version: z.string().optional(),
16
+ constraints: ConstraintsSchema.optional()
17
+ }).passthrough();
18
+
19
+ export type DcvConfigParsed = z.infer<typeof DcvConfigSchema>;
20
+
21
+ export function validateConfig(raw: unknown): { value?: DcvConfigParsed; errors?: string[] } {
22
+ const res = DcvConfigSchema.safeParse(raw);
23
+ if (!res.success) {
24
+ return { errors: res.error.errors.map(e => `${e.path.join('.')||'<root>'}: ${e.message}`) };
25
+ }
26
+ return { value: res.data };
27
+ }