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
|
@@ -0,0 +1,225 @@
|
|
|
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 { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { MonotonicPlugin, parseSize as parseSizePx } from '../core/constraints/monotonic.js';
|
|
16
|
+
import { MonotonicLightness } from '../core/constraints/monotonic-lightness.js';
|
|
17
|
+
import { WcagContrastPlugin } from '../core/constraints/wcag.js';
|
|
18
|
+
import { ThresholdPlugin } from '../core/constraints/threshold.js';
|
|
19
|
+
import { CrossAxisPlugin } from '../core/constraints/cross-axis.js';
|
|
20
|
+
import { loadCrossAxisRules } from './cross-axis-loader.js';
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Discovery
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Discover all constraint sources for a given configuration and breakpoint.
|
|
26
|
+
*
|
|
27
|
+
* This function scans the filesystem and config to determine which constraints
|
|
28
|
+
* should be active, but does not load or attach them yet.
|
|
29
|
+
*
|
|
30
|
+
* @param opts Discovery options (config, basePath, breakpoint)
|
|
31
|
+
* @returns Array of constraint sources
|
|
32
|
+
*/
|
|
33
|
+
export function discoverConstraints(opts) {
|
|
34
|
+
const { config, basePath = '.', bp, constraintsDir = 'themes' } = opts;
|
|
35
|
+
const sources = [];
|
|
36
|
+
const constraintsCfg = config.constraints ?? {};
|
|
37
|
+
// 1. Built-in WCAG defaults
|
|
38
|
+
const enableBuiltInWcag = constraintsCfg.enableBuiltInWcagDefaults === undefined ? true : !!constraintsCfg.enableBuiltInWcagDefaults;
|
|
39
|
+
if (enableBuiltInWcag) {
|
|
40
|
+
sources.push({ type: 'builtin-wcag', enabled: true });
|
|
41
|
+
}
|
|
42
|
+
// 2. Built-in threshold (44px touch target)
|
|
43
|
+
const enableBuiltInThreshold = constraintsCfg.enableBuiltInThreshold === undefined ? true : !!constraintsCfg.enableBuiltInThreshold;
|
|
44
|
+
if (enableBuiltInThreshold) {
|
|
45
|
+
sources.push({ type: 'builtin-threshold', enabled: true });
|
|
46
|
+
}
|
|
47
|
+
// 3. Config-defined WCAG rules
|
|
48
|
+
if (constraintsCfg.wcag && Array.isArray(constraintsCfg.wcag) && constraintsCfg.wcag.length > 0) {
|
|
49
|
+
const rules = constraintsCfg.wcag.map((r) => ({
|
|
50
|
+
fg: r.foreground,
|
|
51
|
+
bg: r.background,
|
|
52
|
+
min: r.ratio || 4.5,
|
|
53
|
+
where: r.description || 'Unknown',
|
|
54
|
+
backdrop: r.backdrop,
|
|
55
|
+
}));
|
|
56
|
+
sources.push({ type: 'config-wcag', rules });
|
|
57
|
+
}
|
|
58
|
+
// 4. Config-defined threshold rules
|
|
59
|
+
if (constraintsCfg.thresholds && Array.isArray(constraintsCfg.thresholds) && constraintsCfg.thresholds.length > 0) {
|
|
60
|
+
const rules = constraintsCfg.thresholds.map((r) => ({
|
|
61
|
+
id: r.id,
|
|
62
|
+
op: r.op,
|
|
63
|
+
valuePx: r.valuePx,
|
|
64
|
+
where: r.where,
|
|
65
|
+
}));
|
|
66
|
+
sources.push({ type: 'custom-threshold', rules });
|
|
67
|
+
}
|
|
68
|
+
// 5. Order files (monotonic constraints)
|
|
69
|
+
const axes = ['typography', 'spacing', 'layout'];
|
|
70
|
+
for (const axis of axes) {
|
|
71
|
+
const suffix = bp ? `.${bp}` : '';
|
|
72
|
+
const orderPath = join(basePath, constraintsDir, `${axis}${suffix}.order.json`);
|
|
73
|
+
if (existsSync(orderPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(readFileSync(orderPath, 'utf8'));
|
|
76
|
+
const orders = data.order || [];
|
|
77
|
+
if (orders.length > 0) {
|
|
78
|
+
sources.push({ type: 'order-file', axis, orders, path: orderPath });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Silently skip malformed order files (consistent with old behavior)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 6. Color lightness order files
|
|
87
|
+
const suffix = bp ? `.${bp}` : '';
|
|
88
|
+
const colorOrderPath = join(basePath, constraintsDir, `color${suffix}.order.json`);
|
|
89
|
+
if (existsSync(colorOrderPath)) {
|
|
90
|
+
try {
|
|
91
|
+
const data = JSON.parse(readFileSync(colorOrderPath, 'utf8'));
|
|
92
|
+
const orders = data.order || [];
|
|
93
|
+
if (orders.length > 0) {
|
|
94
|
+
sources.push({ type: 'lightness-file', orders, path: colorOrderPath });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Silently skip malformed color order files
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 7. Cross-axis rules (global)
|
|
102
|
+
const crossAxisPath = join(basePath, constraintsDir, 'cross-axis.rules.json');
|
|
103
|
+
if (existsSync(crossAxisPath)) {
|
|
104
|
+
sources.push({ type: 'cross-axis-file', path: crossAxisPath });
|
|
105
|
+
}
|
|
106
|
+
// 8. Cross-axis rules (breakpoint-specific)
|
|
107
|
+
if (bp) {
|
|
108
|
+
const crossAxisBpPath = join(basePath, constraintsDir, `cross-axis.${bp}.rules.json`);
|
|
109
|
+
if (existsSync(crossAxisBpPath)) {
|
|
110
|
+
sources.push({ type: 'cross-axis-file', path: crossAxisBpPath, bp });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return sources;
|
|
114
|
+
}
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Attachment
|
|
117
|
+
// ============================================================================
|
|
118
|
+
/**
|
|
119
|
+
* Attach constraint plugins to an engine based on discovered sources.
|
|
120
|
+
*
|
|
121
|
+
* This function takes the output of `discoverConstraints()` and registers
|
|
122
|
+
* the appropriate plugins on the engine.
|
|
123
|
+
*
|
|
124
|
+
* @param engine Engine to attach plugins to
|
|
125
|
+
* @param sources Constraint sources (from discoverConstraints)
|
|
126
|
+
* @param opts Attachment options (knownIds, debug flags)
|
|
127
|
+
*/
|
|
128
|
+
export function attachConstraints(engine, sources, opts) {
|
|
129
|
+
const { knownIds, crossAxisDebug = false } = opts;
|
|
130
|
+
for (const source of sources) {
|
|
131
|
+
try {
|
|
132
|
+
switch (source.type) {
|
|
133
|
+
case 'builtin-wcag': {
|
|
134
|
+
if (source.enabled) {
|
|
135
|
+
const defaultWcagPairs = [
|
|
136
|
+
{
|
|
137
|
+
fg: 'color.role.text.default',
|
|
138
|
+
bg: 'color.role.bg.surface',
|
|
139
|
+
min: 4.5,
|
|
140
|
+
where: 'Body text on surface',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
fg: 'color.role.accent.default',
|
|
144
|
+
bg: 'color.role.bg.surface',
|
|
145
|
+
min: 3.0,
|
|
146
|
+
where: 'Accent on surface',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
fg: 'color.role.focus.ring',
|
|
150
|
+
bg: 'color.role.bg.surface',
|
|
151
|
+
min: 3.0,
|
|
152
|
+
where: 'Focus ring on surface',
|
|
153
|
+
backdrop: '#ffffff',
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
engine.use(WcagContrastPlugin(defaultWcagPairs));
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'builtin-threshold': {
|
|
161
|
+
if (source.enabled) {
|
|
162
|
+
const defaultThresholds = [
|
|
163
|
+
{
|
|
164
|
+
id: 'control.size.min',
|
|
165
|
+
op: '>=',
|
|
166
|
+
valuePx: 44,
|
|
167
|
+
where: 'Touch target (WCAG / Apple HIG)',
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
engine.use(ThresholdPlugin(defaultThresholds, 'threshold'));
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'config-wcag': {
|
|
175
|
+
engine.use(WcagContrastPlugin(source.rules));
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'custom-threshold': {
|
|
179
|
+
engine.use(ThresholdPlugin(source.rules, 'custom-threshold'));
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'order-file': {
|
|
183
|
+
const pluginId = `monotonic-${source.axis}`;
|
|
184
|
+
engine.use(MonotonicPlugin(source.orders, parseSizePx, pluginId));
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'lightness-file': {
|
|
188
|
+
engine.use(MonotonicLightness(source.orders));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'cross-axis-file': {
|
|
192
|
+
// Phase 3B: Load rules from filesystem in CLI layer, pass to core plugin
|
|
193
|
+
const rules = loadCrossAxisRules(source.path, {
|
|
194
|
+
bp: source.bp,
|
|
195
|
+
knownIds,
|
|
196
|
+
debug: crossAxisDebug,
|
|
197
|
+
});
|
|
198
|
+
engine.use(CrossAxisPlugin(rules, source.bp));
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Silently skip failed constraint attachments (consistent with old behavior)
|
|
205
|
+
// In the future, we may want to surface these as warnings
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Convenience
|
|
211
|
+
// ============================================================================
|
|
212
|
+
/**
|
|
213
|
+
* Discover and attach constraints in one call.
|
|
214
|
+
*
|
|
215
|
+
* This is the main entry point for most use cases.
|
|
216
|
+
*
|
|
217
|
+
* @param engine Engine to attach plugins to
|
|
218
|
+
* @param discoveryOpts Discovery options
|
|
219
|
+
* @param attachOpts Attachment options
|
|
220
|
+
*/
|
|
221
|
+
export function setupConstraints(engine, discoveryOpts, attachOpts) {
|
|
222
|
+
const sources = discoverConstraints(discoveryOpts);
|
|
223
|
+
attachConstraints(engine, sources, attachOpts);
|
|
224
|
+
return sources;
|
|
225
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
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
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import type { Engine } from '../core/engine.js';
|
|
17
|
+
import type { Breakpoint } from '../core/breakpoints.js';
|
|
18
|
+
import type { DcvConfig } from './types.js';
|
|
19
|
+
import { MonotonicPlugin, parseSize as parseSizePx } from '../core/constraints/monotonic.js';
|
|
20
|
+
import { MonotonicLightness } from '../core/constraints/monotonic-lightness.js';
|
|
21
|
+
import { WcagContrastPlugin } from '../core/constraints/wcag.js';
|
|
22
|
+
import { ThresholdPlugin } from '../core/constraints/threshold.js';
|
|
23
|
+
import { CrossAxisPlugin } from '../core/constraints/cross-axis.js';
|
|
24
|
+
import { loadCrossAxisRules } from './cross-axis-loader.js';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Types
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export type OrderRule = [string, '<=' | '>=', string];
|
|
31
|
+
|
|
32
|
+
export type WcagRule = {
|
|
33
|
+
fg: string;
|
|
34
|
+
bg: string;
|
|
35
|
+
min: number;
|
|
36
|
+
where: string;
|
|
37
|
+
backdrop?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ThresholdRule = {
|
|
41
|
+
id: string;
|
|
42
|
+
op: '<=' | '>=';
|
|
43
|
+
valuePx: number;
|
|
44
|
+
where?: string;
|
|
45
|
+
level?: 'error' | 'warn';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Represents a constraint source discovered from config or filesystem.
|
|
50
|
+
*/
|
|
51
|
+
export type ConstraintSource =
|
|
52
|
+
| { type: 'builtin-wcag'; enabled: boolean }
|
|
53
|
+
| { type: 'builtin-threshold'; enabled: boolean }
|
|
54
|
+
| { type: 'config-wcag'; rules: WcagRule[] }
|
|
55
|
+
| { type: 'order-file'; axis: string; orders: OrderRule[]; path: string }
|
|
56
|
+
| { type: 'lightness-file'; orders: OrderRule[]; path: string }
|
|
57
|
+
| { type: 'cross-axis-file'; path: string; bp?: Breakpoint }
|
|
58
|
+
| { type: 'custom-threshold'; rules: ThresholdRule[] };
|
|
59
|
+
|
|
60
|
+
export type DiscoveryOptions = {
|
|
61
|
+
config: DcvConfig;
|
|
62
|
+
basePath?: string;
|
|
63
|
+
bp?: Breakpoint;
|
|
64
|
+
constraintsDir?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type AttachOptions = {
|
|
68
|
+
knownIds: Set<string>;
|
|
69
|
+
crossAxisDebug?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Discovery
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Discover all constraint sources for a given configuration and breakpoint.
|
|
78
|
+
*
|
|
79
|
+
* This function scans the filesystem and config to determine which constraints
|
|
80
|
+
* should be active, but does not load or attach them yet.
|
|
81
|
+
*
|
|
82
|
+
* @param opts Discovery options (config, basePath, breakpoint)
|
|
83
|
+
* @returns Array of constraint sources
|
|
84
|
+
*/
|
|
85
|
+
export function discoverConstraints(opts: DiscoveryOptions): ConstraintSource[] {
|
|
86
|
+
const { config, basePath = '.', bp, constraintsDir = 'themes' } = opts;
|
|
87
|
+
const sources: ConstraintSource[] = [];
|
|
88
|
+
const constraintsCfg = config.constraints ?? {};
|
|
89
|
+
|
|
90
|
+
// 1. Built-in WCAG defaults
|
|
91
|
+
const enableBuiltInWcag =
|
|
92
|
+
constraintsCfg.enableBuiltInWcagDefaults === undefined ? true : !!constraintsCfg.enableBuiltInWcagDefaults;
|
|
93
|
+
|
|
94
|
+
if (enableBuiltInWcag) {
|
|
95
|
+
sources.push({ type: 'builtin-wcag', enabled: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Built-in threshold (44px touch target)
|
|
99
|
+
const enableBuiltInThreshold =
|
|
100
|
+
constraintsCfg.enableBuiltInThreshold === undefined ? true : !!constraintsCfg.enableBuiltInThreshold;
|
|
101
|
+
|
|
102
|
+
if (enableBuiltInThreshold) {
|
|
103
|
+
sources.push({ type: 'builtin-threshold', enabled: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Config-defined WCAG rules
|
|
107
|
+
if (constraintsCfg.wcag && Array.isArray(constraintsCfg.wcag) && constraintsCfg.wcag.length > 0) {
|
|
108
|
+
const rules: WcagRule[] = constraintsCfg.wcag.map((r: any) => ({
|
|
109
|
+
fg: r.foreground,
|
|
110
|
+
bg: r.background,
|
|
111
|
+
min: r.ratio || 4.5,
|
|
112
|
+
where: r.description || 'Unknown',
|
|
113
|
+
backdrop: r.backdrop,
|
|
114
|
+
}));
|
|
115
|
+
sources.push({ type: 'config-wcag', rules });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Config-defined threshold rules
|
|
119
|
+
if (constraintsCfg.thresholds && Array.isArray(constraintsCfg.thresholds) && constraintsCfg.thresholds.length > 0) {
|
|
120
|
+
const rules: ThresholdRule[] = constraintsCfg.thresholds.map((r: any) => ({
|
|
121
|
+
id: r.id,
|
|
122
|
+
op: r.op,
|
|
123
|
+
valuePx: r.valuePx,
|
|
124
|
+
where: r.where,
|
|
125
|
+
}));
|
|
126
|
+
sources.push({ type: 'custom-threshold', rules });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 5. Order files (monotonic constraints)
|
|
130
|
+
const axes = ['typography', 'spacing', 'layout'] as const;
|
|
131
|
+
for (const axis of axes) {
|
|
132
|
+
const suffix = bp ? `.${bp}` : '';
|
|
133
|
+
const orderPath = join(basePath, constraintsDir, `${axis}${suffix}.order.json`);
|
|
134
|
+
|
|
135
|
+
if (existsSync(orderPath)) {
|
|
136
|
+
try {
|
|
137
|
+
const data = JSON.parse(readFileSync(orderPath, 'utf8'));
|
|
138
|
+
const orders: OrderRule[] = data.order || [];
|
|
139
|
+
if (orders.length > 0) {
|
|
140
|
+
sources.push({ type: 'order-file', axis, orders, path: orderPath });
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Silently skip malformed order files (consistent with old behavior)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 6. Color lightness order files
|
|
149
|
+
const suffix = bp ? `.${bp}` : '';
|
|
150
|
+
const colorOrderPath = join(basePath, constraintsDir, `color${suffix}.order.json`);
|
|
151
|
+
|
|
152
|
+
if (existsSync(colorOrderPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const data = JSON.parse(readFileSync(colorOrderPath, 'utf8'));
|
|
155
|
+
const orders: OrderRule[] = data.order || [];
|
|
156
|
+
if (orders.length > 0) {
|
|
157
|
+
sources.push({ type: 'lightness-file', orders, path: colorOrderPath });
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Silently skip malformed color order files
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 7. Cross-axis rules (global)
|
|
165
|
+
const crossAxisPath = join(basePath, constraintsDir, 'cross-axis.rules.json');
|
|
166
|
+
if (existsSync(crossAxisPath)) {
|
|
167
|
+
sources.push({ type: 'cross-axis-file', path: crossAxisPath });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 8. Cross-axis rules (breakpoint-specific)
|
|
171
|
+
if (bp) {
|
|
172
|
+
const crossAxisBpPath = join(basePath, constraintsDir, `cross-axis.${bp}.rules.json`);
|
|
173
|
+
if (existsSync(crossAxisBpPath)) {
|
|
174
|
+
sources.push({ type: 'cross-axis-file', path: crossAxisBpPath, bp });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return sources;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Attachment
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Attach constraint plugins to an engine based on discovered sources.
|
|
187
|
+
*
|
|
188
|
+
* This function takes the output of `discoverConstraints()` and registers
|
|
189
|
+
* the appropriate plugins on the engine.
|
|
190
|
+
*
|
|
191
|
+
* @param engine Engine to attach plugins to
|
|
192
|
+
* @param sources Constraint sources (from discoverConstraints)
|
|
193
|
+
* @param opts Attachment options (knownIds, debug flags)
|
|
194
|
+
*/
|
|
195
|
+
export function attachConstraints(engine: Engine, sources: ConstraintSource[], opts: AttachOptions): void {
|
|
196
|
+
const { knownIds, crossAxisDebug = false } = opts;
|
|
197
|
+
|
|
198
|
+
for (const source of sources) {
|
|
199
|
+
try {
|
|
200
|
+
switch (source.type) {
|
|
201
|
+
case 'builtin-wcag': {
|
|
202
|
+
if (source.enabled) {
|
|
203
|
+
const defaultWcagPairs: WcagRule[] = [
|
|
204
|
+
{
|
|
205
|
+
fg: 'color.role.text.default',
|
|
206
|
+
bg: 'color.role.bg.surface',
|
|
207
|
+
min: 4.5,
|
|
208
|
+
where: 'Body text on surface',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
fg: 'color.role.accent.default',
|
|
212
|
+
bg: 'color.role.bg.surface',
|
|
213
|
+
min: 3.0,
|
|
214
|
+
where: 'Accent on surface',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
fg: 'color.role.focus.ring',
|
|
218
|
+
bg: 'color.role.bg.surface',
|
|
219
|
+
min: 3.0,
|
|
220
|
+
where: 'Focus ring on surface',
|
|
221
|
+
backdrop: '#ffffff',
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
engine.use(WcagContrastPlugin(defaultWcagPairs));
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'builtin-threshold': {
|
|
230
|
+
if (source.enabled) {
|
|
231
|
+
const defaultThresholds: ThresholdRule[] = [
|
|
232
|
+
{
|
|
233
|
+
id: 'control.size.min',
|
|
234
|
+
op: '>=',
|
|
235
|
+
valuePx: 44,
|
|
236
|
+
where: 'Touch target (WCAG / Apple HIG)',
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
engine.use(ThresholdPlugin(defaultThresholds, 'threshold'));
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'config-wcag': {
|
|
245
|
+
engine.use(WcagContrastPlugin(source.rules));
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'custom-threshold': {
|
|
250
|
+
engine.use(ThresholdPlugin(source.rules, 'custom-threshold'));
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case 'order-file': {
|
|
255
|
+
const pluginId = `monotonic-${source.axis}`;
|
|
256
|
+
engine.use(MonotonicPlugin(source.orders, parseSizePx, pluginId));
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'lightness-file': {
|
|
261
|
+
engine.use(MonotonicLightness(source.orders));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'cross-axis-file': {
|
|
266
|
+
// Phase 3B: Load rules from filesystem in CLI layer, pass to core plugin
|
|
267
|
+
const rules = loadCrossAxisRules(source.path, {
|
|
268
|
+
bp: source.bp,
|
|
269
|
+
knownIds,
|
|
270
|
+
debug: crossAxisDebug,
|
|
271
|
+
});
|
|
272
|
+
engine.use(CrossAxisPlugin(rules, source.bp));
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Silently skip failed constraint attachments (consistent with old behavior)
|
|
278
|
+
// In the future, we may want to surface these as warnings
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Convenience
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Discover and attach constraints in one call.
|
|
289
|
+
*
|
|
290
|
+
* This is the main entry point for most use cases.
|
|
291
|
+
*
|
|
292
|
+
* @param engine Engine to attach plugins to
|
|
293
|
+
* @param discoveryOpts Discovery options
|
|
294
|
+
* @param attachOpts Attachment options
|
|
295
|
+
*/
|
|
296
|
+
export function setupConstraints(
|
|
297
|
+
engine: Engine,
|
|
298
|
+
discoveryOpts: DiscoveryOptions,
|
|
299
|
+
attachOpts: AttachOptions,
|
|
300
|
+
): ConstraintSource[] {
|
|
301
|
+
const sources = discoverConstraints(discoveryOpts);
|
|
302
|
+
attachConstraints(engine, sources, attachOpts);
|
|
303
|
+
return sources;
|
|
304
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated This module is deprecated. Use constraint-registry.ts instead.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3A (Architectural Cleanup): This file contains legacy runtime constraint
|
|
5
|
+
* loading logic that has been replaced by the centralized constraint-registry.ts module.
|
|
6
|
+
*
|
|
7
|
+
* Migration: Replace attachRuntimeConstraints() with setupConstraints() from constraint-registry.ts
|
|
8
|
+
*
|
|
9
|
+
* This file will be removed in a future major version.
|
|
10
|
+
*/
|
|
11
|
+
import type { Engine } from '../core/engine.js';
|
|
12
|
+
import type { Breakpoint } from '../core/breakpoints.js';
|
|
13
|
+
import type { DcvConfig } from './types.js';
|
|
14
|
+
type AttachRuntimeOpts = {
|
|
15
|
+
config: DcvConfig;
|
|
16
|
+
knownIds: Set<string>;
|
|
17
|
+
bp?: Breakpoint;
|
|
18
|
+
crossAxisDebug?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Attach runtime constraints that depend on project files or built-in policies:
|
|
22
|
+
* - Cross-axis rules from themes/cross-axis*.rules.json
|
|
23
|
+
* - Built-in threshold rules (e.g., control.size.min >= 44px)
|
|
24
|
+
*
|
|
25
|
+
* @deprecated Use setupConstraints() from constraint-registry.ts instead.
|
|
26
|
+
* This function will be removed in a future major version.
|
|
27
|
+
*/
|
|
28
|
+
export declare function attachRuntimeConstraints(engine: Engine, opts: AttachRuntimeOpts): void;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=constraints-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints-loader.d.ts","sourceRoot":"","sources":["constraints-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAkDtF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated This module is deprecated. Use constraint-registry.ts instead.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3A (Architectural Cleanup): This file contains legacy runtime constraint
|
|
5
|
+
* loading logic that has been replaced by the centralized constraint-registry.ts module.
|
|
6
|
+
*
|
|
7
|
+
* Migration: Replace attachRuntimeConstraints() with setupConstraints() from constraint-registry.ts
|
|
8
|
+
*
|
|
9
|
+
* This file will be removed in a future major version.
|
|
10
|
+
*/
|
|
11
|
+
import { loadCrossAxisPlugin } from '../core/cross-axis-config.js';
|
|
12
|
+
import { ThresholdPlugin } from '../core/constraints/threshold.js';
|
|
13
|
+
/**
|
|
14
|
+
* Attach runtime constraints that depend on project files or built-in policies:
|
|
15
|
+
* - Cross-axis rules from themes/cross-axis*.rules.json
|
|
16
|
+
* - Built-in threshold rules (e.g., control.size.min >= 44px)
|
|
17
|
+
*
|
|
18
|
+
* @deprecated Use setupConstraints() from constraint-registry.ts instead.
|
|
19
|
+
* This function will be removed in a future major version.
|
|
20
|
+
*/
|
|
21
|
+
export function attachRuntimeConstraints(engine, opts) {
|
|
22
|
+
const { knownIds, bp, crossAxisDebug, config } = opts;
|
|
23
|
+
// Cross-axis rules: global + optional breakpoint-specific
|
|
24
|
+
try {
|
|
25
|
+
engine.use(loadCrossAxisPlugin('themes/cross-axis.rules.json', bp, {
|
|
26
|
+
debug: !!crossAxisDebug,
|
|
27
|
+
knownIds,
|
|
28
|
+
}));
|
|
29
|
+
if (bp) {
|
|
30
|
+
const bpRulesPath = `themes/cross-axis.${bp}.rules.json`;
|
|
31
|
+
engine.use(loadCrossAxisPlugin(bpRulesPath, bp, {
|
|
32
|
+
debug: !!crossAxisDebug,
|
|
33
|
+
knownIds,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// If cross-axis configuration fails, continue with other constraints.
|
|
39
|
+
}
|
|
40
|
+
const constraintsCfg = config.constraints ?? {};
|
|
41
|
+
// Built-in threshold rule for touch targets (configurable)
|
|
42
|
+
const enableBuiltInThreshold = constraintsCfg.enableBuiltInThreshold === undefined ? true : !!constraintsCfg.enableBuiltInThreshold;
|
|
43
|
+
if (enableBuiltInThreshold) {
|
|
44
|
+
try {
|
|
45
|
+
engine.use(ThresholdPlugin([
|
|
46
|
+
{
|
|
47
|
+
id: 'control.size.min',
|
|
48
|
+
op: '>=',
|
|
49
|
+
valuePx: 44,
|
|
50
|
+
where: 'Touch target (WCAG / Apple HIG)',
|
|
51
|
+
},
|
|
52
|
+
], 'threshold'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Threshold attachment is best-effort; failures should not abort validation.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|