design-constraint-validator 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +215 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +86 -33
- package/cli/commands/validate.ts +176 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts +30 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/constraints-loader.js +58 -0
- package/cli/constraints-loader.ts +83 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts +33 -0
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/engine-helpers.js +87 -22
- package/cli/engine-helpers.ts +133 -61
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +64 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +107 -0
- package/cli/json-output.ts +177 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts +29 -0
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/cross-axis-config.js +29 -0
- package/core/cross-axis-config.ts +181 -151
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/cli/commands/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 { attachRuntimeConstraints } from '../constraints-loader.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
|
+
attachRuntimeConstraints(engine, { config, knownIds, bp: breakpoint });
|
|
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 { attachRuntimeConstraints } from '../constraints-loader.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
|
+
attachRuntimeConstraints(engine, { config, knownIds, bp: breakpoint });
|
|
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';
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { loadTokens, outputResult } from './utils.js';
|
|
2
|
-
import type { PatchApplyOptions } from '../types.js';
|
|
3
|
-
import type { TokenNode } from '../../core/flatten.js';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import { flattenTokens } from '../../core/flatten.js';
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
|
|
8
|
-
interface PatchDocumentV1 {
|
|
9
|
-
version: 1;
|
|
10
|
-
changes: Array<{ id: string; from: any; to: any; type: 'modify'|'add'|'remove' }>;
|
|
11
|
-
patch: Record<string, any>;
|
|
12
|
-
baseTokensHash?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function applyChange(root: any, id: string, to: any, type: 'modify'|'add'|'remove') {
|
|
16
|
-
const parts = id.split('.');
|
|
17
|
-
let cur: any = root;
|
|
18
|
-
for (let i = 0; i < parts.length; i++) {
|
|
19
|
-
const p = parts[i];
|
|
20
|
-
if (i === parts.length - 1) {
|
|
21
|
-
if (type === 'remove') {
|
|
22
|
-
if (cur[p] && typeof cur[p] === 'object') {
|
|
23
|
-
delete cur[p].$value; // delete leaf value
|
|
24
|
-
}
|
|
25
|
-
} else {
|
|
26
|
-
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
|
27
|
-
cur[p].$value = to;
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
|
31
|
-
cur = cur[p];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function patchApplyCommand(opts: PatchApplyOptions): Promise<void> {
|
|
37
|
-
const tokens: TokenNode = loadTokens(opts.tokens || 'tokens/tokens.example.json');
|
|
38
|
-
// Compute current base tokens hash for drift detection (same logic as buildPatch)
|
|
39
|
-
function computeBaseHash(toks: TokenNode): string {
|
|
40
|
-
const flat = flattenTokens(JSON.parse(JSON.stringify(toks))).flat as Record<string, any>;
|
|
41
|
-
const values: Record<string, any> = {};
|
|
42
|
-
Object.keys(flat).sort().forEach(id => { values[id] = flat[id]?.value; });
|
|
43
|
-
// Keep deterministic ordering
|
|
44
|
-
const ordered = Object.keys(values).sort().reduce((acc, k) => { acc[k] = values[k]; return acc; }, {} as Record<string, any>);
|
|
45
|
-
return createHash('sha256').update(JSON.stringify(ordered)).digest('hex');
|
|
46
|
-
}
|
|
47
|
-
// Parse patch
|
|
48
|
-
let patchDoc: PatchDocumentV1;
|
|
49
|
-
if (fs.existsSync(opts.patch)) {
|
|
50
|
-
patchDoc = JSON.parse(fs.readFileSync(opts.patch, 'utf8'));
|
|
51
|
-
} else if (opts.patch.trim().startsWith('{')) {
|
|
52
|
-
patchDoc = JSON.parse(opts.patch);
|
|
53
|
-
} else {
|
|
54
|
-
throw new Error(`Patch not found: ${opts.patch}`);
|
|
55
|
-
}
|
|
56
|
-
if (patchDoc.version !== 1) throw new Error('Unsupported patch version');
|
|
57
|
-
if (patchDoc.baseTokensHash) {
|
|
58
|
-
const currentHash = computeBaseHash(tokens);
|
|
59
|
-
if (currentHash !== patchDoc.baseTokensHash) {
|
|
60
|
-
console.warn(`⚠ Base tokens hash mismatch. Patch built against ${patchDoc.baseTokensHash} but current base is ${currentHash}. Proceeding (use --dry-run to inspect first).`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Apply changes
|
|
65
|
-
for (const c of patchDoc.changes) {
|
|
66
|
-
applyChange(tokens, c.id, c.to, c.type);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (opts.dryRun) {
|
|
70
|
-
outputResult(tokens, 'json');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (opts.output) {
|
|
75
|
-
fs.writeFileSync(opts.output, JSON.stringify(tokens, null, 2));
|
|
76
|
-
if (!opts.quiet) console.log(`✔ Patch applied to ${opts.output}`);
|
|
77
|
-
} else {
|
|
78
|
-
outputResult(tokens, 'json');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
1
|
+
import { loadTokens, outputResult } from './utils.js';
|
|
2
|
+
import type { PatchApplyOptions } from '../types.js';
|
|
3
|
+
import type { TokenNode } from '../../core/flatten.js';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { flattenTokens } from '../../core/flatten.js';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
interface PatchDocumentV1 {
|
|
9
|
+
version: 1;
|
|
10
|
+
changes: Array<{ id: string; from: any; to: any; type: 'modify'|'add'|'remove' }>;
|
|
11
|
+
patch: Record<string, any>;
|
|
12
|
+
baseTokensHash?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applyChange(root: any, id: string, to: any, type: 'modify'|'add'|'remove') {
|
|
16
|
+
const parts = id.split('.');
|
|
17
|
+
let cur: any = root;
|
|
18
|
+
for (let i = 0; i < parts.length; i++) {
|
|
19
|
+
const p = parts[i];
|
|
20
|
+
if (i === parts.length - 1) {
|
|
21
|
+
if (type === 'remove') {
|
|
22
|
+
if (cur[p] && typeof cur[p] === 'object') {
|
|
23
|
+
delete cur[p].$value; // delete leaf value
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
|
27
|
+
cur[p].$value = to;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
|
31
|
+
cur = cur[p];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function patchApplyCommand(opts: PatchApplyOptions): Promise<void> {
|
|
37
|
+
const tokens: TokenNode = loadTokens(opts.tokens || 'tokens/tokens.example.json');
|
|
38
|
+
// Compute current base tokens hash for drift detection (same logic as buildPatch)
|
|
39
|
+
function computeBaseHash(toks: TokenNode): string {
|
|
40
|
+
const flat = flattenTokens(JSON.parse(JSON.stringify(toks))).flat as Record<string, any>;
|
|
41
|
+
const values: Record<string, any> = {};
|
|
42
|
+
Object.keys(flat).sort().forEach(id => { values[id] = flat[id]?.value; });
|
|
43
|
+
// Keep deterministic ordering
|
|
44
|
+
const ordered = Object.keys(values).sort().reduce((acc, k) => { acc[k] = values[k]; return acc; }, {} as Record<string, any>);
|
|
45
|
+
return createHash('sha256').update(JSON.stringify(ordered)).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
// Parse patch
|
|
48
|
+
let patchDoc: PatchDocumentV1;
|
|
49
|
+
if (fs.existsSync(opts.patch)) {
|
|
50
|
+
patchDoc = JSON.parse(fs.readFileSync(opts.patch, 'utf8'));
|
|
51
|
+
} else if (opts.patch.trim().startsWith('{')) {
|
|
52
|
+
patchDoc = JSON.parse(opts.patch);
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`Patch not found: ${opts.patch}`);
|
|
55
|
+
}
|
|
56
|
+
if (patchDoc.version !== 1) throw new Error('Unsupported patch version');
|
|
57
|
+
if (patchDoc.baseTokensHash) {
|
|
58
|
+
const currentHash = computeBaseHash(tokens);
|
|
59
|
+
if (currentHash !== patchDoc.baseTokensHash) {
|
|
60
|
+
console.warn(`⚠ Base tokens hash mismatch. Patch built against ${patchDoc.baseTokensHash} but current base is ${currentHash}. Proceeding (use --dry-run to inspect first).`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Apply changes
|
|
65
|
+
for (const c of patchDoc.changes) {
|
|
66
|
+
applyChange(tokens, c.id, c.to, c.type);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (opts.dryRun) {
|
|
70
|
+
outputResult(tokens, 'json');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (opts.output) {
|
|
75
|
+
fs.writeFileSync(opts.output, JSON.stringify(tokens, null, 2));
|
|
76
|
+
if (!opts.quiet) console.log(`✔ Patch applied to ${opts.output}`);
|
|
77
|
+
} else {
|
|
78
|
+
outputResult(tokens, 'json');
|
|
79
|
+
}
|
|
80
|
+
}
|