design-constraint-validator 1.1.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/adapters/decisionthemes.d.ts +44 -0
- package/adapters/decisionthemes.d.ts.map +1 -0
- package/adapters/decisionthemes.js +35 -0
- package/adapters/decisionthemes.ts +59 -0
- package/cli/commands/graph.js +2 -2
- package/cli/commands/graph.ts +2 -2
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +3 -0
- package/cli/commands/validate.ts +4 -0
- package/cli/json-output.d.ts +5 -0
- package/cli/json-output.d.ts.map +1 -1
- package/cli/json-output.js +2 -0
- package/cli/json-output.ts +7 -0
- package/cli/version-banner.d.ts +20 -0
- package/cli/version-banner.d.ts.map +1 -0
- package/cli/version-banner.js +49 -0
- package/cli/version-banner.ts +61 -0
- package/package.json +1 -1
- package/cli/constraints-loader.d.ts +0 -30
- package/cli/constraints-loader.js +0 -58
- package/cli/constraints-loader.ts +0 -83
- package/cli/engine-helpers.d.ts +0 -41
- package/cli/engine-helpers.js +0 -135
- package/cli/engine-helpers.ts +0 -133
- package/core/cross-axis-config.d.ts +0 -34
- package/core/cross-axis-config.js +0 -173
- package/core/cross-axis-config.ts +0 -182
package/cli/engine-helpers.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated This module is deprecated. Use constraint-registry.ts instead.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3A (Architectural Cleanup): This file contains legacy constraint loading logic
|
|
5
|
-
* that has been replaced by the centralized constraint-registry.ts module.
|
|
6
|
-
*
|
|
7
|
-
* Migration guide:
|
|
8
|
-
* - Replace createEngine() or createValidationEngine() with:
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { Engine } from '../core/engine.js';
|
|
11
|
-
* import { flattenTokens, type FlatToken } from '../core/flatten.js';
|
|
12
|
-
* import { setupConstraints } from './constraint-registry.js';
|
|
13
|
-
*
|
|
14
|
-
* const { flat, edges } = flattenTokens(tokens);
|
|
15
|
-
* const init = {};
|
|
16
|
-
* for (const t of Object.values(flat)) {
|
|
17
|
-
* init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
18
|
-
* }
|
|
19
|
-
* const engine = new Engine(init, edges);
|
|
20
|
-
* const knownIds = new Set(Object.keys(init));
|
|
21
|
-
* setupConstraints(engine, { config, bp }, { knownIds });
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This file will be removed in a future major version.
|
|
25
|
-
*/
|
|
26
|
-
import { flattenTokens } from '../core/flatten.js';
|
|
27
|
-
import { Engine } from '../core/engine.js';
|
|
28
|
-
import { MonotonicPlugin, parseSize as parseSizePx } from '../core/constraints/monotonic.js';
|
|
29
|
-
import { MonotonicLightness } from '../core/constraints/monotonic-lightness.js';
|
|
30
|
-
import { WcagContrastPlugin } from '../core/constraints/wcag.js';
|
|
31
|
-
import { loadOrders as loadOrdersBP, loadTokensWithBreakpoint } from '../core/breakpoints.js';
|
|
32
|
-
function applyMonotonicPlugins(engine, bp) {
|
|
33
|
-
function loadOrders(path) {
|
|
34
|
-
try {
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
-
return JSON.parse(require('node:fs').readFileSync(path, 'utf8')).order;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const suffix = bp ? `.${bp}` : '';
|
|
43
|
-
const typOrders = loadOrders(`themes/typography${suffix}.order.json`);
|
|
44
|
-
const spacingOrders = loadOrders(`themes/spacing${suffix}.order.json`);
|
|
45
|
-
const layoutOrders = loadOrders(`themes/layout${suffix}.order.json`);
|
|
46
|
-
const colorOrders = loadOrders(`themes/color${suffix}.order.json`);
|
|
47
|
-
if (typOrders.length)
|
|
48
|
-
engine.use(MonotonicPlugin(typOrders, parseSizePx, 'monotonic-typography'));
|
|
49
|
-
if (spacingOrders.length)
|
|
50
|
-
engine.use(MonotonicPlugin(spacingOrders, parseSizePx, 'monotonic-spacing'));
|
|
51
|
-
if (layoutOrders.length)
|
|
52
|
-
engine.use(MonotonicPlugin(layoutOrders, parseSizePx, 'monotonic-layout'));
|
|
53
|
-
if (colorOrders.length)
|
|
54
|
-
engine.use(MonotonicLightness(colorOrders));
|
|
55
|
-
}
|
|
56
|
-
function applyWcagPlugins(engine, config) {
|
|
57
|
-
const constraintsCfg = config.constraints ?? {};
|
|
58
|
-
if (constraintsCfg.wcag) {
|
|
59
|
-
const wcagRules = constraintsCfg.wcag.map((r) => ({
|
|
60
|
-
fg: r.foreground,
|
|
61
|
-
bg: r.background,
|
|
62
|
-
min: r.ratio || 4.5,
|
|
63
|
-
where: r.description || 'Unknown',
|
|
64
|
-
}));
|
|
65
|
-
engine.use(WcagContrastPlugin(wcagRules));
|
|
66
|
-
}
|
|
67
|
-
const enableDefaults = constraintsCfg.enableBuiltInWcagDefaults === undefined
|
|
68
|
-
? true
|
|
69
|
-
: !!constraintsCfg.enableBuiltInWcagDefaults;
|
|
70
|
-
if (enableDefaults) {
|
|
71
|
-
const defaultWcagPairs = [
|
|
72
|
-
{
|
|
73
|
-
fg: 'color.role.text.default',
|
|
74
|
-
bg: 'color.role.bg.surface',
|
|
75
|
-
min: 4.5,
|
|
76
|
-
where: 'Body text on surface',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
fg: 'color.role.accent.default',
|
|
80
|
-
bg: 'color.role.bg.surface',
|
|
81
|
-
min: 3.0,
|
|
82
|
-
where: 'Accent on surface',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
fg: 'color.role.focus.ring',
|
|
86
|
-
bg: 'color.role.bg.surface',
|
|
87
|
-
min: 3.0,
|
|
88
|
-
where: 'Focus ring on surface',
|
|
89
|
-
backdrop: '#ffffff',
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
engine.use(WcagContrastPlugin(defaultWcagPairs));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* @deprecated Use constraint-registry.ts setupConstraints() instead.
|
|
97
|
-
* This function will be removed in a future major version.
|
|
98
|
-
*/
|
|
99
|
-
export function createEngine(tokensRoot, config = {}) {
|
|
100
|
-
const { flat, edges } = flattenTokens(tokensRoot);
|
|
101
|
-
const init = {};
|
|
102
|
-
for (const [id, token] of Object.entries(flat))
|
|
103
|
-
init[id] = token.value;
|
|
104
|
-
const engine = new Engine(init, edges);
|
|
105
|
-
applyMonotonicPlugins(engine, undefined);
|
|
106
|
-
applyWcagPlugins(engine, config);
|
|
107
|
-
return engine;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* @deprecated Use constraint-registry.ts setupConstraints() instead.
|
|
111
|
-
* This function will be removed in a future major version.
|
|
112
|
-
*/
|
|
113
|
-
export function createValidationEngine(tokensRoot, bp, config) {
|
|
114
|
-
const { flat, edges } = flattenTokens(tokensRoot);
|
|
115
|
-
const init = {};
|
|
116
|
-
for (const t of Object.values(flat))
|
|
117
|
-
init[t.id] = t.value;
|
|
118
|
-
const engine = new Engine(init, edges);
|
|
119
|
-
// Use breakpoint-aware order loading where available
|
|
120
|
-
const typ = loadOrdersBP('typography', bp);
|
|
121
|
-
const spc = loadOrdersBP('spacing', bp);
|
|
122
|
-
const lay = loadOrdersBP('layout', bp);
|
|
123
|
-
const col = loadOrdersBP('color', bp);
|
|
124
|
-
if (typ.length)
|
|
125
|
-
engine.use(MonotonicPlugin(typ, parseSizePx, 'monotonic-typography'));
|
|
126
|
-
if (spc.length)
|
|
127
|
-
engine.use(MonotonicPlugin(spc, parseSizePx, 'monotonic-spacing'));
|
|
128
|
-
if (lay.length)
|
|
129
|
-
engine.use(MonotonicPlugin(lay, parseSizePx, 'monotonic-layout'));
|
|
130
|
-
if (col.length)
|
|
131
|
-
engine.use(MonotonicLightness(col));
|
|
132
|
-
applyWcagPlugins(engine, config);
|
|
133
|
-
return engine;
|
|
134
|
-
}
|
|
135
|
-
export { loadTokensWithBreakpoint };
|
package/cli/engine-helpers.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated This module is deprecated. Use constraint-registry.ts instead.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3A (Architectural Cleanup): This file contains legacy constraint loading logic
|
|
5
|
-
* that has been replaced by the centralized constraint-registry.ts module.
|
|
6
|
-
*
|
|
7
|
-
* Migration guide:
|
|
8
|
-
* - Replace createEngine() or createValidationEngine() with:
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { Engine } from '../core/engine.js';
|
|
11
|
-
* import { flattenTokens, type FlatToken } from '../core/flatten.js';
|
|
12
|
-
* import { setupConstraints } from './constraint-registry.js';
|
|
13
|
-
*
|
|
14
|
-
* const { flat, edges } = flattenTokens(tokens);
|
|
15
|
-
* const init = {};
|
|
16
|
-
* for (const t of Object.values(flat)) {
|
|
17
|
-
* init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
18
|
-
* }
|
|
19
|
-
* const engine = new Engine(init, edges);
|
|
20
|
-
* const knownIds = new Set(Object.keys(init));
|
|
21
|
-
* setupConstraints(engine, { config, bp }, { knownIds });
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This file will be removed in a future major version.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { flattenTokens, type TokenNode, type FlatToken } from '../core/flatten.js';
|
|
28
|
-
import { Engine } from '../core/engine.js';
|
|
29
|
-
import { MonotonicPlugin, parseSize as parseSizePx } from '../core/constraints/monotonic.js';
|
|
30
|
-
import { MonotonicLightness } from '../core/constraints/monotonic-lightness.js';
|
|
31
|
-
import { WcagContrastPlugin } from '../core/constraints/wcag.js';
|
|
32
|
-
import { loadOrders as loadOrdersBP, loadTokensWithBreakpoint, type Breakpoint } from '../core/breakpoints.js';
|
|
33
|
-
import type { DcvConfig } from './types.js';
|
|
34
|
-
|
|
35
|
-
function applyMonotonicPlugins(engine: Engine, bp: Breakpoint | undefined): void {
|
|
36
|
-
function loadOrders(path: string) {
|
|
37
|
-
try {
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
-
return JSON.parse(require('node:fs').readFileSync(path, 'utf8')).order as [string, '<='|'>=', string][];
|
|
40
|
-
} catch { return []; }
|
|
41
|
-
}
|
|
42
|
-
const suffix = bp ? `.${bp}` : '';
|
|
43
|
-
const typOrders = loadOrders(`themes/typography${suffix}.order.json`);
|
|
44
|
-
const spacingOrders = loadOrders(`themes/spacing${suffix}.order.json`);
|
|
45
|
-
const layoutOrders = loadOrders(`themes/layout${suffix}.order.json`);
|
|
46
|
-
const colorOrders = loadOrders(`themes/color${suffix}.order.json`);
|
|
47
|
-
if (typOrders.length) engine.use(MonotonicPlugin(typOrders, parseSizePx, 'monotonic-typography'));
|
|
48
|
-
if (spacingOrders.length) engine.use(MonotonicPlugin(spacingOrders, parseSizePx, 'monotonic-spacing'));
|
|
49
|
-
if (layoutOrders.length) engine.use(MonotonicPlugin(layoutOrders, parseSizePx, 'monotonic-layout'));
|
|
50
|
-
if (colorOrders.length) engine.use(MonotonicLightness(colorOrders));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function applyWcagPlugins(engine: Engine, config: DcvConfig): void {
|
|
54
|
-
const constraintsCfg = config.constraints ?? {};
|
|
55
|
-
|
|
56
|
-
if (constraintsCfg.wcag) {
|
|
57
|
-
const wcagRules = constraintsCfg.wcag.map((r: any) => ({
|
|
58
|
-
fg: r.foreground,
|
|
59
|
-
bg: r.background,
|
|
60
|
-
min: r.ratio || 4.5,
|
|
61
|
-
where: r.description || 'Unknown',
|
|
62
|
-
}));
|
|
63
|
-
engine.use(WcagContrastPlugin(wcagRules));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const enableDefaults =
|
|
67
|
-
constraintsCfg.enableBuiltInWcagDefaults === undefined
|
|
68
|
-
? true
|
|
69
|
-
: !!constraintsCfg.enableBuiltInWcagDefaults;
|
|
70
|
-
|
|
71
|
-
if (enableDefaults) {
|
|
72
|
-
const defaultWcagPairs = [
|
|
73
|
-
{
|
|
74
|
-
fg: 'color.role.text.default',
|
|
75
|
-
bg: 'color.role.bg.surface',
|
|
76
|
-
min: 4.5,
|
|
77
|
-
where: 'Body text on surface',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
fg: 'color.role.accent.default',
|
|
81
|
-
bg: 'color.role.bg.surface',
|
|
82
|
-
min: 3.0,
|
|
83
|
-
where: 'Accent on surface',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
fg: 'color.role.focus.ring',
|
|
87
|
-
bg: 'color.role.bg.surface',
|
|
88
|
-
min: 3.0,
|
|
89
|
-
where: 'Focus ring on surface',
|
|
90
|
-
backdrop: '#ffffff',
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
engine.use(WcagContrastPlugin(defaultWcagPairs));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @deprecated Use constraint-registry.ts setupConstraints() instead.
|
|
99
|
-
* This function will be removed in a future major version.
|
|
100
|
-
*/
|
|
101
|
-
export function createEngine(tokensRoot: TokenNode, config: DcvConfig = {}): Engine {
|
|
102
|
-
const { flat, edges } = flattenTokens(tokensRoot);
|
|
103
|
-
const init: Record<string, string | number> = {};
|
|
104
|
-
for (const [id, token] of Object.entries(flat)) init[id] = (token as FlatToken).value;
|
|
105
|
-
const engine = new Engine(init, edges);
|
|
106
|
-
applyMonotonicPlugins(engine, undefined);
|
|
107
|
-
applyWcagPlugins(engine, config);
|
|
108
|
-
return engine;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @deprecated Use constraint-registry.ts setupConstraints() instead.
|
|
113
|
-
* This function will be removed in a future major version.
|
|
114
|
-
*/
|
|
115
|
-
export function createValidationEngine(tokensRoot: TokenNode, bp: Breakpoint | undefined, config: DcvConfig): Engine {
|
|
116
|
-
const { flat, edges } = flattenTokens(tokensRoot);
|
|
117
|
-
const init: Record<string, string | number> = {};
|
|
118
|
-
for (const t of Object.values(flat)) init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
119
|
-
const engine = new Engine(init, edges);
|
|
120
|
-
// Use breakpoint-aware order loading where available
|
|
121
|
-
const typ = loadOrdersBP('typography', bp);
|
|
122
|
-
const spc = loadOrdersBP('spacing', bp);
|
|
123
|
-
const lay = loadOrdersBP('layout', bp);
|
|
124
|
-
const col = loadOrdersBP('color', bp);
|
|
125
|
-
if (typ.length) engine.use(MonotonicPlugin(typ, parseSizePx, 'monotonic-typography'));
|
|
126
|
-
if (spc.length) engine.use(MonotonicPlugin(spc, parseSizePx, 'monotonic-spacing'));
|
|
127
|
-
if (lay.length) engine.use(MonotonicPlugin(lay, parseSizePx, 'monotonic-layout'));
|
|
128
|
-
if (col.length) engine.use(MonotonicLightness(col));
|
|
129
|
-
applyWcagPlugins(engine, config);
|
|
130
|
-
return engine;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export { loadTokensWithBreakpoint };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated This module is deprecated. Use cli/cross-axis-loader.ts instead.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3B (Filesystem Separation): This file contains filesystem access logic
|
|
5
|
-
* that has been moved to the CLI layer (cli/cross-axis-loader.ts).
|
|
6
|
-
*
|
|
7
|
-
* Core modules should not import from node:fs. Instead:
|
|
8
|
-
* - CLI code uses cli/cross-axis-loader.ts to read and parse rules
|
|
9
|
-
* - Core plugin (core/constraints/cross-axis.ts) accepts pre-parsed rules
|
|
10
|
-
*
|
|
11
|
-
* Migration:
|
|
12
|
-
* ```ts
|
|
13
|
-
* // OLD (core reads filesystem):
|
|
14
|
-
* import { loadCrossAxisPlugin } from './core/cross-axis-config.js';
|
|
15
|
-
* engine.use(loadCrossAxisPlugin(path, bp, { knownIds }));
|
|
16
|
-
*
|
|
17
|
-
* // NEW (CLI reads, core receives data):
|
|
18
|
-
* import { loadCrossAxisRules } from './cli/cross-axis-loader.js';
|
|
19
|
-
* import { CrossAxisPlugin } from './core/constraints/cross-axis.js';
|
|
20
|
-
* const rules = loadCrossAxisRules(path, { bp, knownIds });
|
|
21
|
-
* engine.use(CrossAxisPlugin(rules, bp));
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This file will be removed in a future major version.
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* @deprecated Use cli/cross-axis-loader.ts loadCrossAxisRules() + CrossAxisPlugin() instead.
|
|
28
|
-
* This function will be removed in a future major version.
|
|
29
|
-
*/
|
|
30
|
-
export declare function loadCrossAxisPlugin(path: string, bp?: string, opts?: {
|
|
31
|
-
debug?: boolean;
|
|
32
|
-
knownIds?: Set<string>;
|
|
33
|
-
}): import("./engine.js").ConstraintPlugin;
|
|
34
|
-
//# sourceMappingURL=cross-axis-config.d.ts.map
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated This module is deprecated. Use cli/cross-axis-loader.ts instead.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3B (Filesystem Separation): This file contains filesystem access logic
|
|
5
|
-
* that has been moved to the CLI layer (cli/cross-axis-loader.ts).
|
|
6
|
-
*
|
|
7
|
-
* Core modules should not import from node:fs. Instead:
|
|
8
|
-
* - CLI code uses cli/cross-axis-loader.ts to read and parse rules
|
|
9
|
-
* - Core plugin (core/constraints/cross-axis.ts) accepts pre-parsed rules
|
|
10
|
-
*
|
|
11
|
-
* Migration:
|
|
12
|
-
* ```ts
|
|
13
|
-
* // OLD (core reads filesystem):
|
|
14
|
-
* import { loadCrossAxisPlugin } from './core/cross-axis-config.js';
|
|
15
|
-
* engine.use(loadCrossAxisPlugin(path, bp, { knownIds }));
|
|
16
|
-
*
|
|
17
|
-
* // NEW (CLI reads, core receives data):
|
|
18
|
-
* import { loadCrossAxisRules } from './cli/cross-axis-loader.js';
|
|
19
|
-
* import { CrossAxisPlugin } from './core/constraints/cross-axis.js';
|
|
20
|
-
* const rules = loadCrossAxisRules(path, { bp, knownIds });
|
|
21
|
-
* engine.use(CrossAxisPlugin(rules, bp));
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This file will be removed in a future major version.
|
|
25
|
-
*/
|
|
26
|
-
import fs from "node:fs";
|
|
27
|
-
import { CrossAxisPlugin } from "./constraints/cross-axis.js";
|
|
28
|
-
/**
|
|
29
|
-
* @deprecated Use cli/cross-axis-loader.ts loadCrossAxisRules() + CrossAxisPlugin() instead.
|
|
30
|
-
* This function will be removed in a future major version.
|
|
31
|
-
*/
|
|
32
|
-
export function loadCrossAxisPlugin(path, bp, opts) {
|
|
33
|
-
const debug = !!opts?.debug;
|
|
34
|
-
const known = opts?.knownIds ?? new Set();
|
|
35
|
-
const log = (...args) => { if (debug)
|
|
36
|
-
console.log("[cross-axis]", ...args); };
|
|
37
|
-
if (!fs.existsSync(path)) {
|
|
38
|
-
log(`no rules file at ${path} (bp=${bp ?? "global"})`);
|
|
39
|
-
return CrossAxisPlugin([], bp);
|
|
40
|
-
}
|
|
41
|
-
const raw = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
42
|
-
const rules = [];
|
|
43
|
-
const unknownIds = new Set();
|
|
44
|
-
const skipped = [];
|
|
45
|
-
// Fuzzy suggestion helpers (lightweight Levenshtein)
|
|
46
|
-
function levenshtein(a, b) {
|
|
47
|
-
const dp = Array(b.length + 1).fill(0).map((_, j) => j);
|
|
48
|
-
for (let i = 1; i <= a.length; i++) {
|
|
49
|
-
let prev = i - 1, cur = i;
|
|
50
|
-
for (let j = 1; j <= b.length; j++) {
|
|
51
|
-
const tmp = cur;
|
|
52
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
53
|
-
cur = Math.min(dp[j] + 1, cur + 1, prev + cost);
|
|
54
|
-
dp[j] = tmp;
|
|
55
|
-
prev = tmp;
|
|
56
|
-
}
|
|
57
|
-
dp[b.length] = cur;
|
|
58
|
-
}
|
|
59
|
-
return dp[b.length];
|
|
60
|
-
}
|
|
61
|
-
function suggest(id, k = 3) {
|
|
62
|
-
return [...known].map(c => ({ id: c, d: levenshtein(id, c) }))
|
|
63
|
-
.sort((a, b) => a.d - b.d)
|
|
64
|
-
.slice(0, k);
|
|
65
|
-
}
|
|
66
|
-
const needId = (id) => {
|
|
67
|
-
if (!id)
|
|
68
|
-
return false;
|
|
69
|
-
if (!known.has(id)) {
|
|
70
|
-
unknownIds.add(id);
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
};
|
|
74
|
-
for (const r of raw.rules || []) {
|
|
75
|
-
if (r.bp && bp && r.bp !== bp) {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (r.bp && !bp) { // rule targets specific breakpoint; skip in global run
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
if (r.when && r.require) {
|
|
83
|
-
// Validate IDs
|
|
84
|
-
needId(r.when.id);
|
|
85
|
-
needId(r.require.id);
|
|
86
|
-
if (r.require.ref)
|
|
87
|
-
needId(r.require.ref);
|
|
88
|
-
rules.push({
|
|
89
|
-
id: r.id, level: r.level, where: r.where,
|
|
90
|
-
when: { id: r.when.id, test: makeOp(r.when.op, r.when.value) },
|
|
91
|
-
require: {
|
|
92
|
-
id: r.require.id,
|
|
93
|
-
test: (v, ctx) => {
|
|
94
|
-
const rhs = valueOrRef(ctx, r.require.ref, r.require.fallback);
|
|
95
|
-
return cmp(v, rhs, r.require.op);
|
|
96
|
-
},
|
|
97
|
-
msg: (v, ctx) => {
|
|
98
|
-
const rhs = valueOrRef(ctx, r.require.ref, r.require.fallback);
|
|
99
|
-
return `${r.require.id} ${prettyFail(r.require.op)} ${fmt(rhs)} (was ${fmt(v)})`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
else if (r.compare) {
|
|
105
|
-
needId(r.compare.a);
|
|
106
|
-
needId(r.compare.b);
|
|
107
|
-
rules.push({
|
|
108
|
-
id: r.id, level: r.level, where: r.where,
|
|
109
|
-
when: { id: r.compare.a, test: () => true },
|
|
110
|
-
require: {
|
|
111
|
-
id: r.compare.a,
|
|
112
|
-
test: (_, ctx) => {
|
|
113
|
-
const a = ctx.getPx(r.compare.a) ?? NaN;
|
|
114
|
-
const b = ctx.getPx(r.compare.b) ?? NaN;
|
|
115
|
-
const delta = px(r.compare.delta ?? 0);
|
|
116
|
-
if (Number.isNaN(a) || Number.isNaN(b))
|
|
117
|
-
return true; // skip check if missing
|
|
118
|
-
return cmp(a, b + delta, r.compare.op);
|
|
119
|
-
},
|
|
120
|
-
msg: (_, ctx) => {
|
|
121
|
-
const a = ctx.getPx(r.compare.a);
|
|
122
|
-
const b = ctx.getPx(r.compare.b);
|
|
123
|
-
const delta = px(r.compare.delta ?? 0);
|
|
124
|
-
return `${r.compare.a} ${prettyFail(r.compare.op)} ${fmt((b ?? 0) + delta)} (was ${fmt(a ?? NaN)})`;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
skipped.push({ id: r.id, reason: "neither when+require nor compare present" });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch (e) {
|
|
134
|
-
skipped.push({ id: r.id, reason: `exception: ${e?.message ?? e}` });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
log(`loaded ${rules.length} rule(s) from ${path}${bp ? ` [bp=${bp}]` : ""}`);
|
|
138
|
-
if (unknownIds.size) {
|
|
139
|
-
log(`unknown ids referenced:`, [...unknownIds].join(", "));
|
|
140
|
-
for (const u of unknownIds) {
|
|
141
|
-
const s = suggest(u, 3);
|
|
142
|
-
if (s.length)
|
|
143
|
-
log(` did you mean: ${s.map(x => `${x.id} (d=${x.d})`).join(', ')}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (skipped.length) {
|
|
147
|
-
for (const s of skipped)
|
|
148
|
-
log(`skipped rule ${s.id ?? "(no id)"} — ${s.reason}`);
|
|
149
|
-
}
|
|
150
|
-
// Extra hint for common anchor pitfall
|
|
151
|
-
for (const r of raw.rules || []) {
|
|
152
|
-
if (r.require?.ref && !known.has(r.require.ref)) {
|
|
153
|
-
log(`anchor missing: ${r.require.ref} → will use fallback=${JSON.stringify(r.require.fallback)} when evaluating`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return CrossAxisPlugin(rules, bp);
|
|
157
|
-
}
|
|
158
|
-
// helpers
|
|
159
|
-
const px = (v) => typeof v === "number" ? v : parseFloat(String(v)) * (String(v).trim().endsWith("rem") ? 16 : 1);
|
|
160
|
-
const cmp = (a, b, op) => op === ">=" ? a >= b : op === ">" ? a > b : op === "<=" ? a <= b : op === "<" ? a < b : op === "==" ? a === b : a !== b;
|
|
161
|
-
const prettyFail = (op) => ({ ">=": "<", ">": "≤", "<=": ">", "<": "≥", "==": "≠", "!=": "=" }[op] || "≠");
|
|
162
|
-
const fmt = (v) => Number.isFinite(Number(v)) ? `${Number(v)}px` : String(v);
|
|
163
|
-
function valueOrRef(ctx, ref, fallback) {
|
|
164
|
-
if (ref) {
|
|
165
|
-
const v = ctx.getPx(ref);
|
|
166
|
-
if (v != null)
|
|
167
|
-
return v;
|
|
168
|
-
}
|
|
169
|
-
return typeof fallback === "number" ? fallback : px(fallback ?? 0);
|
|
170
|
-
}
|
|
171
|
-
function makeOp(op, rhs) {
|
|
172
|
-
return (v) => cmp(v, rhs, op);
|
|
173
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deprecated This module is deprecated. Use cli/cross-axis-loader.ts instead.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3B (Filesystem Separation): This file contains filesystem access logic
|
|
5
|
-
* that has been moved to the CLI layer (cli/cross-axis-loader.ts).
|
|
6
|
-
*
|
|
7
|
-
* Core modules should not import from node:fs. Instead:
|
|
8
|
-
* - CLI code uses cli/cross-axis-loader.ts to read and parse rules
|
|
9
|
-
* - Core plugin (core/constraints/cross-axis.ts) accepts pre-parsed rules
|
|
10
|
-
*
|
|
11
|
-
* Migration:
|
|
12
|
-
* ```ts
|
|
13
|
-
* // OLD (core reads filesystem):
|
|
14
|
-
* import { loadCrossAxisPlugin } from './core/cross-axis-config.js';
|
|
15
|
-
* engine.use(loadCrossAxisPlugin(path, bp, { knownIds }));
|
|
16
|
-
*
|
|
17
|
-
* // NEW (CLI reads, core receives data):
|
|
18
|
-
* import { loadCrossAxisRules } from './cli/cross-axis-loader.js';
|
|
19
|
-
* import { CrossAxisPlugin } from './core/constraints/cross-axis.js';
|
|
20
|
-
* const rules = loadCrossAxisRules(path, { bp, knownIds });
|
|
21
|
-
* engine.use(CrossAxisPlugin(rules, bp));
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* This file will be removed in a future major version.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import fs from "node:fs";
|
|
28
|
-
import { CrossAxisPlugin, type CrossAxisRule, type Ctx } from "./constraints/cross-axis.js";
|
|
29
|
-
|
|
30
|
-
type When = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; value: number };
|
|
31
|
-
type Require = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; ref?: string; fallback?: string|number };
|
|
32
|
-
type Compare = { a: string; op: "<="|">="|"<"|">"|"=="|"!="; b: string; delta?: string|number };
|
|
33
|
-
type RawRule = { id: string; level?: "error"|"warn"; where?: string; bp?: string; when?: When; require?: Require; compare?: Compare; };
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @deprecated Use cli/cross-axis-loader.ts loadCrossAxisRules() + CrossAxisPlugin() instead.
|
|
37
|
-
* This function will be removed in a future major version.
|
|
38
|
-
*/
|
|
39
|
-
export function loadCrossAxisPlugin(
|
|
40
|
-
path: string,
|
|
41
|
-
bp?: string,
|
|
42
|
-
opts?: { debug?: boolean; knownIds?: Set<string> }
|
|
43
|
-
) {
|
|
44
|
-
const debug = !!opts?.debug;
|
|
45
|
-
const known = opts?.knownIds ?? new Set<string>();
|
|
46
|
-
const log = (...args: any[]) => { if (debug) console.log("[cross-axis]", ...args); };
|
|
47
|
-
|
|
48
|
-
if (!fs.existsSync(path)) {
|
|
49
|
-
log(`no rules file at ${path} (bp=${bp ?? "global"})`);
|
|
50
|
-
return CrossAxisPlugin([], bp);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const raw = JSON.parse(fs.readFileSync(path, "utf8")) as { rules: RawRule[] };
|
|
54
|
-
const rules: CrossAxisRule[] = [];
|
|
55
|
-
const unknownIds = new Set<string>();
|
|
56
|
-
const skipped: Array<{ id?: string; reason: string }> = [];
|
|
57
|
-
|
|
58
|
-
// Fuzzy suggestion helpers (lightweight Levenshtein)
|
|
59
|
-
function levenshtein(a: string, b: string) {
|
|
60
|
-
const dp = Array(b.length + 1).fill(0).map((_, j) => j);
|
|
61
|
-
for (let i = 1; i <= a.length; i++) {
|
|
62
|
-
let prev = i - 1, cur = i;
|
|
63
|
-
for (let j = 1; j <= b.length; j++) {
|
|
64
|
-
const tmp = cur;
|
|
65
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
66
|
-
cur = Math.min(dp[j] + 1, cur + 1, prev + cost);
|
|
67
|
-
dp[j] = tmp;
|
|
68
|
-
prev = tmp;
|
|
69
|
-
}
|
|
70
|
-
dp[b.length] = cur;
|
|
71
|
-
}
|
|
72
|
-
return dp[b.length];
|
|
73
|
-
}
|
|
74
|
-
function suggest(id: string, k = 3) {
|
|
75
|
-
return [...known].map(c => ({ id: c, d: levenshtein(id, c) }))
|
|
76
|
-
.sort((a, b) => a.d - b.d)
|
|
77
|
-
.slice(0, k);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const needId = (id?: string) => {
|
|
81
|
-
if (!id) return false;
|
|
82
|
-
if (!known.has(id)) { unknownIds.add(id); }
|
|
83
|
-
return true;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
for (const r of raw.rules || []) {
|
|
87
|
-
if (r.bp && bp && r.bp !== bp) { continue; }
|
|
88
|
-
if (r.bp && !bp) { // rule targets specific breakpoint; skip in global run
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
if (r.when && r.require) {
|
|
93
|
-
// Validate IDs
|
|
94
|
-
needId(r.when.id);
|
|
95
|
-
needId(r.require.id);
|
|
96
|
-
if (r.require.ref) needId(r.require.ref);
|
|
97
|
-
|
|
98
|
-
rules.push({
|
|
99
|
-
id: r.id, level: r.level, where: r.where,
|
|
100
|
-
when: { id: r.when.id, test: makeOp(r.when.op, r.when.value) },
|
|
101
|
-
require: {
|
|
102
|
-
id: r.require.id,
|
|
103
|
-
test: (v: number, ctx: Ctx) => {
|
|
104
|
-
const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
|
|
105
|
-
return cmp(v, rhs, r.require!.op);
|
|
106
|
-
},
|
|
107
|
-
msg: (v: number, ctx: Ctx) => {
|
|
108
|
-
const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
|
|
109
|
-
return `${r.require!.id} ${prettyFail(r.require!.op)} ${fmt(rhs)} (was ${fmt(v)})`;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
} else if (r.compare) {
|
|
114
|
-
needId(r.compare.a);
|
|
115
|
-
needId(r.compare.b);
|
|
116
|
-
|
|
117
|
-
rules.push({
|
|
118
|
-
id: r.id, level: r.level, where: r.where,
|
|
119
|
-
when: { id: r.compare.a, test: () => true },
|
|
120
|
-
require: {
|
|
121
|
-
id: r.compare.a,
|
|
122
|
-
test: (_: number, ctx: Ctx) => {
|
|
123
|
-
const a = ctx.getPx(r.compare!.a) ?? NaN;
|
|
124
|
-
const b = ctx.getPx(r.compare!.b) ?? NaN;
|
|
125
|
-
const delta = px(r.compare!.delta ?? 0);
|
|
126
|
-
if (Number.isNaN(a) || Number.isNaN(b)) return true; // skip check if missing
|
|
127
|
-
return cmp(a, b + delta, r.compare!.op);
|
|
128
|
-
},
|
|
129
|
-
msg: (_: number, ctx: Ctx) => {
|
|
130
|
-
const a = ctx.getPx(r.compare!.a);
|
|
131
|
-
const b = ctx.getPx(r.compare!.b);
|
|
132
|
-
const delta = px(r.compare!.delta ?? 0);
|
|
133
|
-
return `${r.compare!.a} ${prettyFail(r.compare!.op)} ${fmt((b ?? 0) + delta)} (was ${fmt(a ?? NaN)})`;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
} else {
|
|
138
|
-
skipped.push({ id: r.id, reason: "neither when+require nor compare present" });
|
|
139
|
-
}
|
|
140
|
-
} catch (e: any) {
|
|
141
|
-
skipped.push({ id: r.id, reason: `exception: ${e?.message ?? e}` });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
log(`loaded ${rules.length} rule(s) from ${path}${bp ? ` [bp=${bp}]` : ""}`);
|
|
146
|
-
if (unknownIds.size) {
|
|
147
|
-
log(`unknown ids referenced:`, [...unknownIds].join(", "));
|
|
148
|
-
for (const u of unknownIds) {
|
|
149
|
-
const s = suggest(u, 3);
|
|
150
|
-
if (s.length) log(` did you mean: ${s.map(x => `${x.id} (d=${x.d})`).join(', ')}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (skipped.length) {
|
|
154
|
-
for (const s of skipped) log(`skipped rule ${s.id ?? "(no id)"} — ${s.reason}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Extra hint for common anchor pitfall
|
|
158
|
-
for (const r of raw.rules || []) {
|
|
159
|
-
if (r.require?.ref && !known.has(r.require.ref)) {
|
|
160
|
-
log(`anchor missing: ${r.require.ref} → will use fallback=${JSON.stringify(r.require.fallback)} when evaluating`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return CrossAxisPlugin(rules, bp);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// helpers
|
|
168
|
-
const px = (v: string|number) => typeof v === "number" ? v : parseFloat(String(v)) * (String(v).trim().endsWith("rem") ? 16 : 1);
|
|
169
|
-
const cmp = (a:number,b:number,op:When["op"]) =>
|
|
170
|
-
op === ">="? a>=b : op === ">"? a>b : op === "<="? a<=b : op === "<"? a<b : op === "=="? a===b : a!==b;
|
|
171
|
-
const prettyFail = (op: string) => ({">=":"<",">":"≤","<=":">","<": "≥","==":"≠","!=":"="} as any)[op] || "≠";
|
|
172
|
-
const fmt = (v:number|string) => Number.isFinite(Number(v)) ? `${Number(v)}px` : String(v);
|
|
173
|
-
function valueOrRef(ctx: Ctx, ref?: string, fallback?: string|number) {
|
|
174
|
-
if (ref) {
|
|
175
|
-
const v = ctx.getPx(ref);
|
|
176
|
-
if (v != null) return v;
|
|
177
|
-
}
|
|
178
|
-
return typeof fallback === "number" ? fallback : px(fallback ?? 0);
|
|
179
|
-
}
|
|
180
|
-
function makeOp(op: When["op"], rhs: number) {
|
|
181
|
-
return (v: number) => cmp(v, rhs, op);
|
|
182
|
-
}
|