design-constraint-validator 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +229 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/decisionthemes.d.ts +44 -0
- package/adapters/decisionthemes.d.ts.map +1 -0
- package/adapters/decisionthemes.js +35 -0
- package/adapters/decisionthemes.ts +59 -0
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +89 -33
- package/cli/commands/validate.ts +180 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +69 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +109 -0
- package/cli/json-output.ts +184 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/cli/version-banner.d.ts +20 -0
- package/cli/version-banner.d.ts.map +1 -0
- package/cli/version-banner.js +49 -0
- package/cli/version-banner.ts +61 -0
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/cli/engine-helpers.d.ts +0 -8
- package/cli/engine-helpers.js +0 -70
- package/cli/engine-helpers.ts +0 -61
- package/core/cross-axis-config.d.ts +0 -5
- package/core/cross-axis-config.js +0 -144
- package/core/cross-axis-config.ts +0 -152
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/cli/commands/build.ts
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { join, dirname, resolve } from 'node:path';
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
-
import { valuesToCss, type ManifestRow } from '../../adapters/css.js';
|
|
5
|
-
import { emitJSON } from '../../adapters/json.js';
|
|
6
|
-
import { emitJS } from '../../adapters/js.js';
|
|
7
|
-
import type { BuildOptions } from '../types.js';
|
|
8
|
-
|
|
9
|
-
export async function buildCommand(options: BuildOptions & { [k: string]: any }): Promise<void> {
|
|
10
|
-
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
11
|
-
const tokens = loadTokensWithBreakpoint();
|
|
12
|
-
const { flat } = flattenTokens(tokens);
|
|
13
|
-
let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, (t as FlatToken).value]));
|
|
14
|
-
if (options.theme) {
|
|
15
|
-
const themePath = join('tokens/themes', `${options.theme}.json`);
|
|
16
|
-
if (existsSync(themePath)) {
|
|
17
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
18
|
-
Object.assign(allValues, themeTokens);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
const format = options.format || 'css';
|
|
22
|
-
const defaultOutput = `dist/tokens.${format}`;
|
|
23
|
-
let manifest: ManifestRow[] | undefined;
|
|
24
|
-
if (options.mapper) {
|
|
25
|
-
try {
|
|
26
|
-
const mp = resolve(options.mapper);
|
|
27
|
-
if (!existsSync(mp)) throw new Error(`mapper file not found: ${mp}`);
|
|
28
|
-
manifest = JSON.parse(readFileSync(mp, 'utf8')) as ManifestRow[];
|
|
29
|
-
if (!Array.isArray(manifest)) throw new Error('mapper manifest must be an array');
|
|
30
|
-
} catch (e) {
|
|
31
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
32
|
-
console.error(`Failed to load mapper manifest: ${msg}`);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const allFormats = options.allFormats ?? options['all-formats'];
|
|
37
|
-
if (allFormats) {
|
|
38
|
-
const dir = 'dist'; if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
-
const css = valuesToCss(allValues, { manifest });
|
|
40
|
-
writeFileSync('dist/tokens.css', css, 'utf8');
|
|
41
|
-
if (options.dryRun) console.log(css);
|
|
42
|
-
writeFileSync('dist/tokens.json', emitJSON(allValues, manifest), 'utf8');
|
|
43
|
-
writeFileSync('dist/tokens.js', emitJS(allValues, manifest), 'utf8');
|
|
44
|
-
console.log(`Tokens written (all formats) to dist/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const dryRun = options.dryRun ?? options['dry-run'];
|
|
48
|
-
if (format === 'css') {
|
|
49
|
-
const css = valuesToCss(allValues, { manifest });
|
|
50
|
-
if (dryRun) { console.log(css); return; }
|
|
51
|
-
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
52
|
-
writeFileSync(outPath, css, 'utf8');
|
|
53
|
-
console.log(`CSS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
54
|
-
} else if (format === 'json') {
|
|
55
|
-
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
-
if (dryRun) { console.log(emitJSON(allValues, manifest)); return; }
|
|
57
|
-
writeFileSync(outPath, emitJSON(allValues, manifest), 'utf8');
|
|
58
|
-
console.log(`JSON tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
59
|
-
} else if (format === 'js') {
|
|
60
|
-
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
61
|
-
if (dryRun) { console.log(emitJS(allValues, manifest)); return; }
|
|
62
|
-
writeFileSync(outPath, emitJS(allValues, manifest), 'utf8');
|
|
63
|
-
console.log(`JS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
import { join, dirname, resolve } from 'node:path';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
+
import { valuesToCss, type ManifestRow } from '../../adapters/css.js';
|
|
5
|
+
import { emitJSON } from '../../adapters/json.js';
|
|
6
|
+
import { emitJS } from '../../adapters/js.js';
|
|
7
|
+
import type { BuildOptions } from '../types.js';
|
|
8
|
+
|
|
9
|
+
export async function buildCommand(options: BuildOptions & { [k: string]: any }): Promise<void> {
|
|
10
|
+
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
11
|
+
const tokens = loadTokensWithBreakpoint();
|
|
12
|
+
const { flat } = flattenTokens(tokens);
|
|
13
|
+
let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, (t as FlatToken).value]));
|
|
14
|
+
if (options.theme) {
|
|
15
|
+
const themePath = join('tokens/themes', `${options.theme}.json`);
|
|
16
|
+
if (existsSync(themePath)) {
|
|
17
|
+
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
18
|
+
Object.assign(allValues, themeTokens);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const format = options.format || 'css';
|
|
22
|
+
const defaultOutput = `dist/tokens.${format}`;
|
|
23
|
+
let manifest: ManifestRow[] | undefined;
|
|
24
|
+
if (options.mapper) {
|
|
25
|
+
try {
|
|
26
|
+
const mp = resolve(options.mapper);
|
|
27
|
+
if (!existsSync(mp)) throw new Error(`mapper file not found: ${mp}`);
|
|
28
|
+
manifest = JSON.parse(readFileSync(mp, 'utf8')) as ManifestRow[];
|
|
29
|
+
if (!Array.isArray(manifest)) throw new Error('mapper manifest must be an array');
|
|
30
|
+
} catch (e) {
|
|
31
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
32
|
+
console.error(`Failed to load mapper manifest: ${msg}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const allFormats = options.allFormats ?? options['all-formats'];
|
|
37
|
+
if (allFormats) {
|
|
38
|
+
const dir = 'dist'; if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
+
const css = valuesToCss(allValues, { manifest });
|
|
40
|
+
writeFileSync('dist/tokens.css', css, 'utf8');
|
|
41
|
+
if (options.dryRun) console.log(css);
|
|
42
|
+
writeFileSync('dist/tokens.json', emitJSON(allValues, manifest), 'utf8');
|
|
43
|
+
writeFileSync('dist/tokens.js', emitJS(allValues, manifest), 'utf8');
|
|
44
|
+
console.log(`Tokens written (all formats) to dist/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const dryRun = options.dryRun ?? options['dry-run'];
|
|
48
|
+
if (format === 'css') {
|
|
49
|
+
const css = valuesToCss(allValues, { manifest });
|
|
50
|
+
if (dryRun) { console.log(css); return; }
|
|
51
|
+
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
52
|
+
writeFileSync(outPath, css, 'utf8');
|
|
53
|
+
console.log(`CSS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
54
|
+
} else if (format === 'json') {
|
|
55
|
+
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
+
if (dryRun) { console.log(emitJSON(allValues, manifest)); return; }
|
|
57
|
+
writeFileSync(outPath, emitJSON(allValues, manifest), 'utf8');
|
|
58
|
+
console.log(`JSON tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
59
|
+
} else if (format === 'js') {
|
|
60
|
+
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
61
|
+
if (dryRun) { console.log(emitJS(allValues, manifest)); return; }
|
|
62
|
+
writeFileSync(outPath, emitJS(allValues, manifest), 'utf8');
|
|
63
|
+
console.log(`JS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2BhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuJvE"}
|
package/cli/commands/graph.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { flattenTokens } from '../../core/flatten.js';
|
|
3
3
|
import { exportGraphImage } from '../../core/image-export.js';
|
|
4
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
5
|
+
import { loadConfig } from '../config.js';
|
|
4
6
|
// Local helper for non-poset dependency graphs
|
|
5
7
|
function generateDependencyGraph(edges, format) {
|
|
6
8
|
switch (format) {
|
|
@@ -103,12 +105,14 @@ export async function graphCommand(options) {
|
|
|
103
105
|
const { Engine } = await import('../../core/engine.js');
|
|
104
106
|
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
|
105
107
|
const { MonotonicLightness } = await import('../../core/constraints/monotonic-lightness.js');
|
|
106
|
-
const {
|
|
107
|
-
const { flat, edges } = flattenTokens(tokens);
|
|
108
|
+
const { flat, edges: depEdges } = flattenTokens(tokens);
|
|
108
109
|
const init = {};
|
|
109
|
-
Object.values(flat).forEach(t => {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
Object.values(flat).forEach((t) => {
|
|
111
|
+
const ft = t;
|
|
112
|
+
init[ft.id] = ft.value;
|
|
113
|
+
});
|
|
114
|
+
const engine = new Engine(init, depEdges);
|
|
115
|
+
const allIdsInHasse = new Set([...h.keys(), ...Array.from(h.values()).flatMap((s) => [...s])]);
|
|
112
116
|
let issues = [];
|
|
113
117
|
if (name === 'color') {
|
|
114
118
|
const colorOrders = order;
|
|
@@ -118,10 +122,20 @@ export async function graphCommand(options) {
|
|
|
118
122
|
const numericOrders = order;
|
|
119
123
|
issues = MonotonicPlugin(numericOrders, parseSize, 'monotonic').evaluate(engine, allIdsInHasse);
|
|
120
124
|
}
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
// Attach threshold (and any other runtime constraints) respecting config flags
|
|
126
|
+
const cfgRes = loadConfig(undefined);
|
|
127
|
+
if (cfgRes.ok) {
|
|
128
|
+
const config = cfgRes.value;
|
|
129
|
+
const knownIds = new Set(Object.keys(flat));
|
|
130
|
+
setupConstraints(engine, { config, bp: breakpoint }, { knownIds });
|
|
131
|
+
const runtimeIssues = engine.evaluate(allIdsInHasse);
|
|
132
|
+
issues.push(...runtimeIssues);
|
|
133
|
+
}
|
|
123
134
|
const severityRank = { error: 2, warn: 1 };
|
|
124
|
-
const filteredIssues = issues.filter(it => {
|
|
135
|
+
const filteredIssues = issues.filter((it) => {
|
|
136
|
+
const level = it.level || 'error';
|
|
137
|
+
return severityRank[level] >= severityRank[minSeverity];
|
|
138
|
+
});
|
|
125
139
|
const edgeViol = new Set();
|
|
126
140
|
const nodeViol = new Set();
|
|
127
141
|
if (labelViolations)
|
|
@@ -134,13 +148,15 @@ export async function graphCommand(options) {
|
|
|
134
148
|
nodeViol.add(b);
|
|
135
149
|
if (labelViolations && edgeLabels) {
|
|
136
150
|
let label = it.message ?? 'violation';
|
|
137
|
-
if (labelTruncate > 0 && label.length > labelTruncate)
|
|
151
|
+
if (labelTruncate > 0 && label.length > labelTruncate) {
|
|
138
152
|
label = label.slice(0, labelTruncate - 1) + '…';
|
|
153
|
+
}
|
|
139
154
|
edgeLabels.set(`${a}|${b}`, label);
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
|
-
else if (typeof it.id === 'string')
|
|
157
|
+
else if (typeof it.id === 'string') {
|
|
143
158
|
nodeViol.add(it.id);
|
|
159
|
+
}
|
|
144
160
|
}
|
|
145
161
|
highlight = { nodes: nodeViol, edges: edgeViol, color: violationColor };
|
|
146
162
|
if (onlyViolations) {
|
package/cli/commands/graph.ts
CHANGED
|
@@ -1,137 +1,180 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import type { GraphOptions } from '../types.js';
|
|
3
|
-
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
-
import { exportGraphImage } from '../../core/image-export.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
dotNodes.forEach(
|
|
13
|
-
dot
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
mermaid += ` ${fromId}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
let
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import type { GraphOptions } from '../types.js';
|
|
3
|
+
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
+
import { exportGraphImage } from '../../core/image-export.js';
|
|
5
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
|
|
8
|
+
// Local helper for non-poset dependency graphs
|
|
9
|
+
function generateDependencyGraph(edges: Array<[string, string]>, format: string): string {
|
|
10
|
+
switch (format) {
|
|
11
|
+
case 'dot': {
|
|
12
|
+
const dotNodes = new Set<string>(); edges.forEach(([f,t]) => { dotNodes.add(f); dotNodes.add(t); });
|
|
13
|
+
let dot = 'digraph tokens {\n rankdir=LR;\n node [shape=box, style=rounded];\n\n';
|
|
14
|
+
dotNodes.forEach(n => { dot += ` "${n}";\n`; });
|
|
15
|
+
dot += '\n'; edges.forEach(([f,t]) => { dot += ` "${f}" -> "${t}";\n`; });
|
|
16
|
+
return dot + '}\n'; }
|
|
17
|
+
case 'mermaid': {
|
|
18
|
+
let mermaid = 'graph LR\n'; const mermaidNodes = new Set<string>();
|
|
19
|
+
edges.forEach(([from,to]) => { const fromId = from.replace(/[^a-zA-Z0-9]/g,'_'); const toId = to.replace(/[^a-zA-Z0-9]/g,'_');
|
|
20
|
+
if (!mermaidNodes.has(fromId)) { mermaid += ` ${fromId}["${from}"]\n`; mermaidNodes.add(fromId); }
|
|
21
|
+
if (!mermaidNodes.has(toId)) { mermaid += ` ${toId}["${to}"]\n`; mermaidNodes.add(toId); }
|
|
22
|
+
mermaid += ` ${fromId} --> ${toId}\n`; });
|
|
23
|
+
return mermaid; }
|
|
24
|
+
case 'json':
|
|
25
|
+
default: return JSON.stringify({ nodes: Array.from(new Set(edges.flat())), edges }, null, 2);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
30
|
+
const { parseBreakpoints } = await import('../../core/breakpoints.js');
|
|
31
|
+
const bps = parseBreakpoints(process.argv);
|
|
32
|
+
const plan = bps.length ? bps : [undefined];
|
|
33
|
+
if (options.hasse) {
|
|
34
|
+
const name = options.hasse;
|
|
35
|
+
const bundle = (options as any).bundle;
|
|
36
|
+
const fmt = (options.format === 'json' ? 'mermaid' : options.format) as 'mermaid' | 'dot' | 'svg' | 'png';
|
|
37
|
+
const imageFrom = options.imageFrom || 'mermaid';
|
|
38
|
+
const filterPrefixes = options.filterPrefix ? options.filterPrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
39
|
+
const excludePrefixes = options.excludePrefix ? options.excludePrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
40
|
+
const onlyViolations = options.onlyViolations || false;
|
|
41
|
+
const highlightViolations = options.highlightViolations || false;
|
|
42
|
+
const violationColor = options.violationColor || '#ff2d55';
|
|
43
|
+
const labelViolations = options.labelViolations || false;
|
|
44
|
+
const labelTruncate = Math.max(0, options.labelTruncate || 0);
|
|
45
|
+
const minSeverity = options.minSeverity || 'warn';
|
|
46
|
+
const focus = options.focus; const radius = Math.max(0, options.radius || 1);
|
|
47
|
+
for (const breakpoint of plan) {
|
|
48
|
+
const suffixParts: string[] = [];
|
|
49
|
+
if (breakpoint) suffixParts.push(breakpoint);
|
|
50
|
+
if (filterPrefixes.length) suffixParts.push(filterPrefixes.join('_'));
|
|
51
|
+
if (excludePrefixes.length) suffixParts.push('not-' + excludePrefixes.join('_'));
|
|
52
|
+
if (focus) suffixParts.push(`focus-${focus.replace(/[^\w.*-]/g,'_')}-r${radius}`);
|
|
53
|
+
if (onlyViolations) suffixParts.push('violations'); else if (highlightViolations) suffixParts.push('highlight-violations');
|
|
54
|
+
if (labelViolations) suffixParts.push('labeled');
|
|
55
|
+
const suffix = suffixParts.length ? '-' + suffixParts.map(s=>s.replace(/[^\w.-]/g,'_')).join('__') : '';
|
|
56
|
+
const baseFmt = fmt === 'svg' || fmt === 'png' ? (imageFrom === 'dot' ? 'dot' : 'mermaid') : fmt;
|
|
57
|
+
const ext = baseFmt === 'mermaid' ? 'mmd' : 'dot';
|
|
58
|
+
const outDir = 'dist/graphs'; const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
|
|
59
|
+
try {
|
|
60
|
+
const src = `themes/${name}.order.json`;
|
|
61
|
+
if (!existsSync(src)) { console.error(`❌ Order constraint file not found: ${src}`); process.exit(1); }
|
|
62
|
+
const { order } = JSON.parse(readFileSync(src, 'utf8'));
|
|
63
|
+
const { buildPoset, transitiveReduction, toMermaidHasseStyled, toDotHasseStyled, filterByPrefix, filterExcludePrefix, khopSubgraph, pickSeedsByPattern } = await import('../../core/poset.js');
|
|
64
|
+
let g = buildPoset(order);
|
|
65
|
+
if (filterPrefixes.length) g = filterByPrefix(g, filterPrefixes);
|
|
66
|
+
if (excludePrefixes.length) g = filterExcludePrefix(g, excludePrefixes);
|
|
67
|
+
let h = transitiveReduction(g);
|
|
68
|
+
if (focus) { const nodes = new Set<string>([...h.keys(), ...Array.from(h.values()).flatMap(s=>[...s])]); const seeds = pickSeedsByPattern(nodes, focus); h = khopSubgraph(h, seeds, radius); }
|
|
69
|
+
let highlight: { nodes: Set<string>; edges: Set<string>; color?: string } | undefined; let edgeLabels: Map<string,string> | undefined;
|
|
70
|
+
if (onlyViolations || highlightViolations || labelViolations) {
|
|
71
|
+
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
72
|
+
const tokens = loadTokensWithBreakpoint(breakpoint);
|
|
73
|
+
const { flattenTokens } = await import('../../core/flatten.js');
|
|
74
|
+
const { Engine } = await import('../../core/engine.js');
|
|
75
|
+
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
|
76
|
+
const { MonotonicLightness } = await import('../../core/constraints/monotonic-lightness.js');
|
|
77
|
+
|
|
78
|
+
const { flat, edges: depEdges } = flattenTokens(tokens);
|
|
79
|
+
const init: Record<string, string | number> = {};
|
|
80
|
+
Object.values(flat).forEach((t) => {
|
|
81
|
+
const ft = t as FlatToken;
|
|
82
|
+
init[ft.id] = ft.value;
|
|
83
|
+
});
|
|
84
|
+
const engine = new Engine(init, depEdges);
|
|
85
|
+
|
|
86
|
+
const allIdsInHasse = new Set<string>([...h.keys(), ...Array.from(h.values()).flatMap((s) => [...s])]);
|
|
87
|
+
let issues: any[] = [];
|
|
88
|
+
if (name === 'color') {
|
|
89
|
+
const colorOrders = order as [string, '<=' | '>=', string][];
|
|
90
|
+
issues = MonotonicLightness(colorOrders).evaluate(engine, allIdsInHasse);
|
|
91
|
+
} else {
|
|
92
|
+
const numericOrders = order as [string, '<=' | '>=', string][];
|
|
93
|
+
issues = MonotonicPlugin(numericOrders, parseSize, 'monotonic').evaluate(engine, allIdsInHasse);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Attach threshold (and any other runtime constraints) respecting config flags
|
|
97
|
+
const cfgRes = loadConfig(undefined);
|
|
98
|
+
if (cfgRes.ok) {
|
|
99
|
+
const config = cfgRes.value;
|
|
100
|
+
const knownIds = new Set(Object.keys(flat as Record<string, FlatToken>));
|
|
101
|
+
setupConstraints(engine, { config, bp: breakpoint }, { knownIds });
|
|
102
|
+
const runtimeIssues = engine.evaluate(allIdsInHasse);
|
|
103
|
+
issues.push(...runtimeIssues);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const severityRank = { error: 2, warn: 1 } as const;
|
|
107
|
+
const filteredIssues = issues.filter((it) => {
|
|
108
|
+
const level = (it.level as 'warn' | 'error') || 'error';
|
|
109
|
+
return severityRank[level] >= severityRank[minSeverity];
|
|
110
|
+
});
|
|
111
|
+
const edgeViol = new Set<string>();
|
|
112
|
+
const nodeViol = new Set<string>();
|
|
113
|
+
if (labelViolations) edgeLabels = new Map<string, string>();
|
|
114
|
+
for (const it of filteredIssues) {
|
|
115
|
+
if (typeof it.id === 'string' && it.id.includes('|')) {
|
|
116
|
+
const [a, b] = it.id.split('|');
|
|
117
|
+
edgeViol.add(`${a}|${b}`);
|
|
118
|
+
nodeViol.add(a);
|
|
119
|
+
nodeViol.add(b);
|
|
120
|
+
if (labelViolations && edgeLabels) {
|
|
121
|
+
let label = it.message ?? 'violation';
|
|
122
|
+
if (labelTruncate > 0 && label.length > labelTruncate) {
|
|
123
|
+
label = label.slice(0, labelTruncate - 1) + '…';
|
|
124
|
+
}
|
|
125
|
+
edgeLabels.set(`${a}|${b}`, label);
|
|
126
|
+
}
|
|
127
|
+
} else if (typeof it.id === 'string') {
|
|
128
|
+
nodeViol.add(it.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
highlight = { nodes: nodeViol, edges: edgeViol, color: violationColor };
|
|
132
|
+
if (onlyViolations) {
|
|
133
|
+
const pruned: Map<string, Set<string>> = new Map();
|
|
134
|
+
for (const [u, vs] of h) {
|
|
135
|
+
for (const v of vs) {
|
|
136
|
+
if (edgeViol.has(`${u}|${v}`) || nodeViol.has(u) || nodeViol.has(v)) {
|
|
137
|
+
if (!pruned.has(u)) pruned.set(u, new Set());
|
|
138
|
+
pruned.get(u)!.add(v);
|
|
139
|
+
if (!pruned.has(v)) pruned.set(v, new Set());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
h = pruned;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const bpLabel = breakpoint ? ` @${breakpoint}` : '';
|
|
147
|
+
const title = `${name}${suffix ? ' ' + suffix : ''}${bpLabel} (Hasse)`;
|
|
148
|
+
const mermaidContent = toMermaidHasseStyled(h, { title, highlight, labels: edgeLabels });
|
|
149
|
+
const dotContent = toDotHasseStyled(h, { title, highlight, labels: edgeLabels });
|
|
150
|
+
mkdirSync(outDir, { recursive: true });
|
|
151
|
+
if (bundle) {
|
|
152
|
+
const mFile = baseFile.replace(/\.dot$/,'-bundle.mmd').replace(/\.mmd$/,'-bundle.mmd');
|
|
153
|
+
const dFile = baseFile.replace(/\.mmd$/,'-bundle.dot').replace(/\.dot$/,'-bundle.dot');
|
|
154
|
+
writeFileSync(mFile, mermaidContent);
|
|
155
|
+
writeFileSync(dFile, dotContent);
|
|
156
|
+
}
|
|
157
|
+
const baseContent = baseFmt === 'mermaid' ? mermaidContent : dotContent;
|
|
158
|
+
writeFileSync(baseFile, baseContent);
|
|
159
|
+
if (fmt === 'svg' || fmt === 'png') {
|
|
160
|
+
const imgFile = `${outDir}/${name}${suffix}-hasse.${fmt}`;
|
|
161
|
+
const { ok, hint } = exportGraphImage(baseFile, imgFile, fmt, imageFrom);
|
|
162
|
+
if (ok) console.log(`✓ Wrote ${baseFile} and ${imgFile}`); else console.log(`✓ Wrote ${baseFile} (image export skipped). ${hint}`);
|
|
163
|
+
} else {
|
|
164
|
+
const hasViolations = highlight && (highlight.edges.size > 0 || highlight.nodes.size > 0);
|
|
165
|
+
const message = (onlyViolations || highlightViolations) && !hasViolations ? `✓ Wrote ${baseFile} (no violations in slice)` : `✓ Wrote ${baseFile}`;
|
|
166
|
+
console.log(message);
|
|
167
|
+
}
|
|
168
|
+
} catch (error) { console.error(`❌ Error generating Hasse diagram for ${breakpoint}:`, error); process.exit(1); }
|
|
169
|
+
}
|
|
170
|
+
return; }
|
|
171
|
+
for (const breakpoint of plan) {
|
|
172
|
+
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
173
|
+
const tokens = loadTokensWithBreakpoint(breakpoint); const { edges } = flattenTokens(tokens);
|
|
174
|
+
let filteredEdges = edges;
|
|
175
|
+
if (options.filter) { const filterRegex = new RegExp(options.filter); filteredEdges = edges.filter(([from,to]) => filterRegex.test(from) || filterRegex.test(to)); }
|
|
176
|
+
const format = options.format || 'json'; const graph = generateDependencyGraph(filteredEdges, format);
|
|
177
|
+
if (options.output) { const bpSuffix = breakpoint ? `.${breakpoint}` : ''; const outputPath = options.output.replace(/(\.[^.]+)$/ , `${bpSuffix}$1`); writeFileSync(outputPath, graph, 'utf8'); console.log(`Dependency graph written to: ${outputPath}`); }
|
|
178
|
+
else { if (breakpoint) console.log(`\n=== ${breakpoint.toUpperCase()} ===`); console.log(graph); }
|
|
179
|
+
}
|
|
180
|
+
}
|
package/cli/commands/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { setCommand } from './set.js';
|
|
2
|
-
export { buildCommand } from './build.js';
|
|
3
|
-
export { validateCommand } from './validate.js';
|
|
4
|
-
export { graphCommand } from './graph.js';
|
|
5
|
-
export { whyCommand } from './why.js';
|
|
6
|
-
export { patchCommand } from './patch.js';
|
|
7
|
-
export { patchApplyCommand } from './patch-apply.js';
|
|
1
|
+
export { setCommand } from './set.js';
|
|
2
|
+
export { buildCommand } from './build.js';
|
|
3
|
+
export { validateCommand } from './validate.js';
|
|
4
|
+
export { graphCommand } from './graph.js';
|
|
5
|
+
export { whyCommand } from './why.js';
|
|
6
|
+
export { patchCommand } from './patch.js';
|
|
7
|
+
export { patchApplyCommand } from './patch-apply.js';
|