design-constraint-validator 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +215 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- 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 +86 -33
- package/cli/commands/validate.ts +176 -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 +30 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/constraints-loader.js +58 -0
- package/cli/constraints-loader.ts +83 -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 +33 -0
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/engine-helpers.js +87 -22
- package/cli/engine-helpers.ts +133 -61
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +64 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +107 -0
- package/cli/json-output.ts +177 -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/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 +29 -0
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/cross-axis-config.js +29 -0
- package/core/cross-axis-config.ts +181 -151
- 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/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/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));
|
package/cli/commands/why.ts
CHANGED
|
@@ -1,46 +1,158 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { flattenTokens, type FlatToken } 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
|
+
import { Engine } from '../../core/engine.js';
|
|
7
|
+
import { loadConfig } from '../config.js';
|
|
8
|
+
import type { ConstraintIssue } from '../../core/engine.js';
|
|
9
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
10
|
+
|
|
11
|
+
export async function whyCommand(options: WhyOptions): Promise<void> {
|
|
12
|
+
const tokensPath = options.tokens || 'tokens/tokens.json';
|
|
13
|
+
const tokens = loadTokens(tokensPath);
|
|
14
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
15
|
+
const target = options.tokenId;
|
|
16
|
+
|
|
17
|
+
if (!flat[target]) {
|
|
18
|
+
console.error(`Token not found: ${target}`);
|
|
19
|
+
const { suggestIds } = await import('../../core/cli-format.js');
|
|
20
|
+
const suggestions = suggestIds(target, Object.keys(flat));
|
|
21
|
+
if (suggestions.length > 0) {
|
|
22
|
+
console.log('\nDid you mean:');
|
|
23
|
+
suggestions.slice(0, 5).forEach((s) => console.log(` ${s.id}`));
|
|
24
|
+
} else {
|
|
25
|
+
console.log('\nAvailable tokens:');
|
|
26
|
+
Object.keys(flat)
|
|
27
|
+
.sort()
|
|
28
|
+
.slice(0, 10)
|
|
29
|
+
.forEach((id) => console.log(` ${id}`));
|
|
30
|
+
if (Object.keys(flat).length > 10) {
|
|
31
|
+
console.log(` ... and ${Object.keys(flat).length - 10} more`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeLoad(p: string) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const overrides = safeLoad('tokens/overrides/local.json');
|
|
46
|
+
const theme = safeLoad('themes/theme.json');
|
|
47
|
+
|
|
48
|
+
const baseReport = explain(target, flat, edges, {
|
|
49
|
+
overrides: (overrides as any)?.overrides ?? overrides,
|
|
50
|
+
theme,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Best-effort constraint summary: which rules currently implicate this token
|
|
54
|
+
let constraintsSummary:
|
|
55
|
+
| {
|
|
56
|
+
ruleId: string;
|
|
57
|
+
level: 'error' | 'warn';
|
|
58
|
+
message: string;
|
|
59
|
+
where?: string;
|
|
60
|
+
}[]
|
|
61
|
+
| undefined;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const cfgRes = loadConfig(options.config);
|
|
65
|
+
if (cfgRes.ok) {
|
|
66
|
+
const config = cfgRes.value;
|
|
67
|
+
|
|
68
|
+
// Create engine with flattened tokens
|
|
69
|
+
const init: Record<string, string | number> = {};
|
|
70
|
+
for (const t of Object.values(flat)) {
|
|
71
|
+
init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
72
|
+
}
|
|
73
|
+
const engine = new Engine(init, edges);
|
|
74
|
+
const knownIds = new Set(Object.keys(init));
|
|
75
|
+
|
|
76
|
+
// Discover and attach all constraints via centralized registry
|
|
77
|
+
setupConstraints(
|
|
78
|
+
engine,
|
|
79
|
+
{ config, constraintsDir: 'themes' },
|
|
80
|
+
{ knownIds },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const candidates = new Set<string>([target]);
|
|
84
|
+
const allIssues: ConstraintIssue[] = engine.evaluate(candidates);
|
|
85
|
+
if (allIssues.length) {
|
|
86
|
+
const related = allIssues.filter((issue) => {
|
|
87
|
+
const parts = String(issue.id).split('|');
|
|
88
|
+
return parts.includes(target);
|
|
89
|
+
});
|
|
90
|
+
if (related.length) {
|
|
91
|
+
constraintsSummary = related.map((issue) => ({
|
|
92
|
+
ruleId: issue.rule,
|
|
93
|
+
level: issue.level,
|
|
94
|
+
message: issue.message,
|
|
95
|
+
where: issue.where,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// If constraint analysis fails, fall back to provenance-only report.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const report: any = constraintsSummary ? { ...baseReport, constraints: constraintsSummary } : baseReport;
|
|
105
|
+
|
|
106
|
+
const format = options.format || 'json';
|
|
107
|
+
if (format === 'table') {
|
|
108
|
+
const { pad, trunc } = await import('../../core/cli-format.js');
|
|
109
|
+
console.log(`\n=== Token Analysis: ${target} ===`);
|
|
110
|
+
console.log(`Value: ${report.value}`);
|
|
111
|
+
console.log(`Raw: ${report.raw ?? 'N/A'}`);
|
|
112
|
+
console.log(`Provenance: ${report.provenance}`);
|
|
113
|
+
|
|
114
|
+
if (report.dependsOn && report.dependsOn.length > 0) {
|
|
115
|
+
console.log(`\nDependencies (${report.dependsOn.length}):`);
|
|
116
|
+
console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
|
|
117
|
+
console.log('-'.repeat(70));
|
|
118
|
+
report.dependsOn.forEach((depId: string) => {
|
|
119
|
+
const dep = flat[depId] as any;
|
|
120
|
+
if (dep) {
|
|
121
|
+
console.log(
|
|
122
|
+
pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown'),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (report.dependents && report.dependents.length > 0) {
|
|
129
|
+
console.log(`\nDependents (${report.dependents.length}):`);
|
|
130
|
+
console.log(pad('TOKEN', 30) + pad('VALUE', 20) + 'TYPE');
|
|
131
|
+
console.log('-'.repeat(70));
|
|
132
|
+
report.dependents.forEach((depId: string) => {
|
|
133
|
+
const dep = flat[depId] as any;
|
|
134
|
+
if (dep) {
|
|
135
|
+
console.log(
|
|
136
|
+
pad(trunc(depId, 28), 30) + pad(trunc(String(dep.value), 18), 20) + (dep.type || 'unknown'),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (report.refs && report.refs.length > 0) {
|
|
143
|
+
console.log(`\nReferences: ${report.refs.join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
if (report.chain && report.chain.length > 0) {
|
|
146
|
+
console.log(`\nReference Chain: ${report.chain.join(' -> ')}`);
|
|
147
|
+
}
|
|
148
|
+
if (constraintsSummary && constraintsSummary.length > 0) {
|
|
149
|
+
console.log('\nConstraints (violations involving this token):');
|
|
150
|
+
constraintsSummary.forEach((c) => {
|
|
151
|
+
const where = c.where ? ` @ ${c.where}` : '';
|
|
152
|
+
console.log(`- [${c.level}] ${c.ruleId}${where}: ${c.message}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
console.log(JSON.stringify(report, null, 2));
|
|
157
|
+
}
|
|
158
|
+
}
|
package/cli/config-schema.ts
CHANGED
|
@@ -1,27 +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
|
-
}
|
|
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
|
+
}
|
package/cli/config.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { validateConfig } from './config-schema.js';
|
|
3
|
-
import { ok, err, type Result } from './result.js';
|
|
4
|
-
import type { DcvConfig } from './types.js';
|
|
5
|
-
|
|
6
|
-
export function loadConfig(configPath?: string): Result<DcvConfig, string> {
|
|
7
|
-
const candidates = configPath ? [configPath] : [
|
|
8
|
-
'dcv.config.json',
|
|
9
|
-
'dcv.config.js',
|
|
10
|
-
'.dcvrc.json',
|
|
11
|
-
'package.json'
|
|
12
|
-
];
|
|
13
|
-
for (const p of candidates) {
|
|
14
|
-
if (!existsSync(p)) continue;
|
|
15
|
-
try {
|
|
16
|
-
const rawTxt = readFileSync(p, 'utf8');
|
|
17
|
-
let raw: unknown = JSON.parse(rawTxt);
|
|
18
|
-
if (p === 'package.json' && raw && typeof raw === 'object') {
|
|
19
|
-
const pkg = raw as Record<string, unknown>;
|
|
20
|
-
if ('dcv' in pkg) {
|
|
21
|
-
raw = pkg.dcv;
|
|
22
|
-
} else {
|
|
23
|
-
continue; // No dcv config in package.json
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const { value, errors } = validateConfig(raw);
|
|
27
|
-
if (errors) return err(`Config validation failed in ${p}:\n - ${errors.join('\n - ')}`);
|
|
28
|
-
return ok(value!);
|
|
29
|
-
} catch (e) {
|
|
30
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
31
|
-
return err(`Failed reading config ${p}: ${msg}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return ok({});
|
|
35
|
-
}
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { validateConfig } from './config-schema.js';
|
|
3
|
+
import { ok, err, type Result } from './result.js';
|
|
4
|
+
import type { DcvConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function loadConfig(configPath?: string): Result<DcvConfig, string> {
|
|
7
|
+
const candidates = configPath ? [configPath] : [
|
|
8
|
+
'dcv.config.json',
|
|
9
|
+
'dcv.config.js',
|
|
10
|
+
'.dcvrc.json',
|
|
11
|
+
'package.json'
|
|
12
|
+
];
|
|
13
|
+
for (const p of candidates) {
|
|
14
|
+
if (!existsSync(p)) continue;
|
|
15
|
+
try {
|
|
16
|
+
const rawTxt = readFileSync(p, 'utf8');
|
|
17
|
+
let raw: unknown = JSON.parse(rawTxt);
|
|
18
|
+
if (p === 'package.json' && raw && typeof raw === 'object') {
|
|
19
|
+
const pkg = raw as Record<string, unknown>;
|
|
20
|
+
if ('dcv' in pkg) {
|
|
21
|
+
raw = pkg.dcv;
|
|
22
|
+
} else {
|
|
23
|
+
continue; // No dcv config in package.json
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const { value, errors } = validateConfig(raw);
|
|
27
|
+
if (errors) return err(`Config validation failed in ${p}:\n - ${errors.join('\n - ')}`);
|
|
28
|
+
return ok(value!);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
31
|
+
return err(`Failed reading config ${p}: ${msg}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return ok({});
|
|
35
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized constraint discovery and loading.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a single source of truth for determining which constraints
|
|
5
|
+
* are active for a given validation run. It replaces the scattered constraint-loading
|
|
6
|
+
* logic previously split across engine-helpers.ts and constraints-loader.ts.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Constraints are discovered from config and filesystem in ONE place
|
|
10
|
+
* - Core modules receive in-memory data (no filesystem access)
|
|
11
|
+
* - All entry points (validate, set, graph) use this registry for consistency
|
|
12
|
+
*/
|
|
13
|
+
import type { Engine } from '../core/engine.js';
|
|
14
|
+
import type { Breakpoint } from '../core/breakpoints.js';
|
|
15
|
+
import type { DcvConfig } from './types.js';
|
|
16
|
+
export type OrderRule = [string, '<=' | '>=', string];
|
|
17
|
+
export type WcagRule = {
|
|
18
|
+
fg: string;
|
|
19
|
+
bg: string;
|
|
20
|
+
min: number;
|
|
21
|
+
where: string;
|
|
22
|
+
backdrop?: string;
|
|
23
|
+
};
|
|
24
|
+
export type ThresholdRule = {
|
|
25
|
+
id: string;
|
|
26
|
+
op: '<=' | '>=';
|
|
27
|
+
valuePx: number;
|
|
28
|
+
where?: string;
|
|
29
|
+
level?: 'error' | 'warn';
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Represents a constraint source discovered from config or filesystem.
|
|
33
|
+
*/
|
|
34
|
+
export type ConstraintSource = {
|
|
35
|
+
type: 'builtin-wcag';
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'builtin-threshold';
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'config-wcag';
|
|
42
|
+
rules: WcagRule[];
|
|
43
|
+
} | {
|
|
44
|
+
type: 'order-file';
|
|
45
|
+
axis: string;
|
|
46
|
+
orders: OrderRule[];
|
|
47
|
+
path: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'lightness-file';
|
|
50
|
+
orders: OrderRule[];
|
|
51
|
+
path: string;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'cross-axis-file';
|
|
54
|
+
path: string;
|
|
55
|
+
bp?: Breakpoint;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'custom-threshold';
|
|
58
|
+
rules: ThresholdRule[];
|
|
59
|
+
};
|
|
60
|
+
export type DiscoveryOptions = {
|
|
61
|
+
config: DcvConfig;
|
|
62
|
+
basePath?: string;
|
|
63
|
+
bp?: Breakpoint;
|
|
64
|
+
constraintsDir?: string;
|
|
65
|
+
};
|
|
66
|
+
export type AttachOptions = {
|
|
67
|
+
knownIds: Set<string>;
|
|
68
|
+
crossAxisDebug?: boolean;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Discover all constraint sources for a given configuration and breakpoint.
|
|
72
|
+
*
|
|
73
|
+
* This function scans the filesystem and config to determine which constraints
|
|
74
|
+
* should be active, but does not load or attach them yet.
|
|
75
|
+
*
|
|
76
|
+
* @param opts Discovery options (config, basePath, breakpoint)
|
|
77
|
+
* @returns Array of constraint sources
|
|
78
|
+
*/
|
|
79
|
+
export declare function discoverConstraints(opts: DiscoveryOptions): ConstraintSource[];
|
|
80
|
+
/**
|
|
81
|
+
* Attach constraint plugins to an engine based on discovered sources.
|
|
82
|
+
*
|
|
83
|
+
* This function takes the output of `discoverConstraints()` and registers
|
|
84
|
+
* the appropriate plugins on the engine.
|
|
85
|
+
*
|
|
86
|
+
* @param engine Engine to attach plugins to
|
|
87
|
+
* @param sources Constraint sources (from discoverConstraints)
|
|
88
|
+
* @param opts Attachment options (knownIds, debug flags)
|
|
89
|
+
*/
|
|
90
|
+
export declare function attachConstraints(engine: Engine, sources: ConstraintSource[], opts: AttachOptions): void;
|
|
91
|
+
/**
|
|
92
|
+
* Discover and attach constraints in one call.
|
|
93
|
+
*
|
|
94
|
+
* This is the main entry point for most use cases.
|
|
95
|
+
*
|
|
96
|
+
* @param engine Engine to attach plugins to
|
|
97
|
+
* @param discoveryOpts Discovery options
|
|
98
|
+
* @param attachOpts Attachment options
|
|
99
|
+
*/
|
|
100
|
+
export declare function setupConstraints(engine: Engine, discoveryOpts: DiscoveryOptions, attachOpts: AttachOptions): ConstraintSource[];
|
|
101
|
+
//# sourceMappingURL=constraint-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraint-registry.d.ts","sourceRoot":"","sources":["constraint-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAY5C,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,UAAU,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAMF;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CA8F9E;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAsFxG;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,gBAAgB,EAC/B,UAAU,EAAE,aAAa,GACxB,gBAAgB,EAAE,CAIpB"}
|