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/core/poset.ts
CHANGED
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
// core/poset.ts
|
|
2
|
-
// Simple poset model + transitive reduction (Hasse) + mermaid export.
|
|
3
|
-
|
|
4
|
-
export type Id = string;
|
|
5
|
-
export type Comp = "<=" | ">=";
|
|
6
|
-
export type Order = [Id, Comp, Id];
|
|
7
|
-
|
|
8
|
-
export type Digraph = Map<Id, Set<Id>>; // edges u -> v
|
|
9
|
-
|
|
10
|
-
// Edge labels for violation messages
|
|
11
|
-
export type EdgeLabels = Map<string, string>; // key = "a|b" (raw ids)
|
|
12
|
-
|
|
13
|
-
// Small escapes so Mermaid/DOT don't choke on special characters
|
|
14
|
-
const escMermaid = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
15
|
-
const escDot = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
16
|
-
|
|
17
|
-
/** Safe ID for Mermaid/DOT node identifiers */
|
|
18
|
-
export function sanitizeId(id: string): string {
|
|
19
|
-
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type Highlight = {
|
|
23
|
-
nodes?: Set<string>; // raw token IDs to style
|
|
24
|
-
edges?: Set<string>; // "a|b" pairs (raw IDs) to style
|
|
25
|
-
color?: string; // hex or named (Mermaid & DOT)
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export function buildPoset(orders: Order[]): Digraph {
|
|
29
|
-
const g: Digraph = new Map();
|
|
30
|
-
const add = (u: Id, v: Id) => {
|
|
31
|
-
if (!g.has(u)) g.set(u, new Set());
|
|
32
|
-
if (!g.has(v)) g.set(v, new Set());
|
|
33
|
-
g.get(u)!.add(v);
|
|
34
|
-
};
|
|
35
|
-
for (const [a, op, b] of orders) {
|
|
36
|
-
if (op === ">=") add(a, b); // a ≥ b → edge a→b
|
|
37
|
-
else if (op === "<=") add(b, a); // a ≤ b → edge b→a
|
|
38
|
-
}
|
|
39
|
-
return g;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function transitiveReduction(g: Digraph): Digraph {
|
|
43
|
-
// naive but fine for our sizes: remove any edge u->v if a path u->...->v exists without that edge
|
|
44
|
-
const out: Digraph = new Map([...g.entries()].map(([u, set]) => [u, new Set(set)]));
|
|
45
|
-
const nodes = [...g.keys()];
|
|
46
|
-
const hasPath = (src: Id, dst: Id, skipU?: Id, skipV?: Id): boolean => {
|
|
47
|
-
const seen = new Set<Id>(); const stack = [src];
|
|
48
|
-
while (stack.length) {
|
|
49
|
-
const n = stack.pop()!;
|
|
50
|
-
if (n === dst) return true;
|
|
51
|
-
if (seen.has(n)) continue;
|
|
52
|
-
seen.add(n);
|
|
53
|
-
for (const w of g.get(n) ?? []) {
|
|
54
|
-
if (skipU === n && skipV === w) continue;
|
|
55
|
-
stack.push(w);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
};
|
|
60
|
-
for (const u of nodes) {
|
|
61
|
-
for (const v of g.get(u) ?? []) {
|
|
62
|
-
if (hasPath(u, v, u, v)) out.get(u)!.delete(v);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function toMermaidHasse(gHasse: Digraph, {title = "Poset"} = {}): string {
|
|
69
|
-
const lines = ["flowchart TD", `%% ${title}`];
|
|
70
|
-
for (const [u, vs] of gHasse) {
|
|
71
|
-
for (const v of vs) {
|
|
72
|
-
lines.push(` "${u}" --> "${v}"`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return lines.join("\n");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// New: styled Mermaid (ids + labels + classing)
|
|
79
|
-
export function toMermaidHasseStyled(
|
|
80
|
-
g: Digraph,
|
|
81
|
-
opts: { title?: string; highlight?: Highlight; labels?: EdgeLabels } = {}
|
|
82
|
-
): string {
|
|
83
|
-
const { title = "Poset", highlight, labels } = opts;
|
|
84
|
-
const lines = ["flowchart TD", `%% ${title}`];
|
|
85
|
-
const classes: string[] = [];
|
|
86
|
-
|
|
87
|
-
for (const [u, vs] of g) {
|
|
88
|
-
const uId = sanitizeId(u);
|
|
89
|
-
lines.push(` ${uId}["${u}"]`);
|
|
90
|
-
for (const v of vs) {
|
|
91
|
-
const vId = sanitizeId(v);
|
|
92
|
-
lines.push(` ${vId}["${v}"]`);
|
|
93
|
-
|
|
94
|
-
// Add edge with optional label
|
|
95
|
-
const lbl = labels?.get(`${u}|${v}`);
|
|
96
|
-
if (lbl) {
|
|
97
|
-
lines.push(` ${uId} -- "${escMermaid(lbl)}" --> ${vId}`);
|
|
98
|
-
} else {
|
|
99
|
-
lines.push(` ${uId} --> ${vId}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (highlight?.edges?.has(`${u}|${v}`)) {
|
|
103
|
-
// edge styling via linkStyle… we assign after all edges (Mermaid counts links in order)
|
|
104
|
-
// simpler: also mark both nodes; edges get colored via linkStyle below
|
|
105
|
-
classes.push(uId, vId);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const violNodes = new Set<string>(highlight?.nodes ? [...highlight.nodes].map(sanitizeId) : []);
|
|
111
|
-
classes.forEach(id => violNodes.add(id));
|
|
112
|
-
if (violNodes.size) {
|
|
113
|
-
lines.push(` classDef viol fill:#ffe6e6,stroke:${highlight?.color ?? "#ff0000"},stroke-width:2px;`);
|
|
114
|
-
lines.push(` class ${[...violNodes].join(",")} viol;`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Edge coloring: Mermaid lets us style by link index; simpler workaround:
|
|
118
|
-
// Add a class to nodes and rely on thick red node borders (good enough for audit views).
|
|
119
|
-
|
|
120
|
-
return lines.join("\n");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function toDotHasse(
|
|
124
|
-
gHasse: Digraph,
|
|
125
|
-
opts: { title?: string; labels?: EdgeLabels } = {}
|
|
126
|
-
): string {
|
|
127
|
-
const { title = "Poset", labels } = opts;
|
|
128
|
-
const lines = [
|
|
129
|
-
"digraph poset {",
|
|
130
|
-
` label="${title}";`,
|
|
131
|
-
" rankdir=TD;",
|
|
132
|
-
" node [shape=box, style=rounded];",
|
|
133
|
-
""
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
for (const [u, vs] of gHasse) {
|
|
137
|
-
for (const v of vs) {
|
|
138
|
-
const lbl = labels?.get(`${u}|${v}`);
|
|
139
|
-
const labelAttr = lbl ? ` [label="${escDot(lbl)}"]` : "";
|
|
140
|
-
lines.push(` "${u}" -> "${v}"${labelAttr};`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
lines.push("}");
|
|
145
|
-
return lines.join("\n");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// New: styled DOT
|
|
149
|
-
export function toDotHasseStyled(
|
|
150
|
-
g: Digraph,
|
|
151
|
-
opts: { title?: string; highlight?: Highlight; labels?: EdgeLabels } = {}
|
|
152
|
-
): string {
|
|
153
|
-
const { title = "Poset", highlight, labels } = opts;
|
|
154
|
-
const lines = [
|
|
155
|
-
`digraph G {`,
|
|
156
|
-
` label="${title}"; labelloc="t"; rankdir=TB; node [shape=box];`
|
|
157
|
-
];
|
|
158
|
-
const hiColor = highlight?.color ?? "red";
|
|
159
|
-
|
|
160
|
-
const hiNodes = highlight?.nodes ?? new Set<string>();
|
|
161
|
-
const hiEdges = highlight?.edges ?? new Set<string>();
|
|
162
|
-
|
|
163
|
-
// declare nodes so we can style them
|
|
164
|
-
const declared = new Set<string>();
|
|
165
|
-
for (const [u, vs] of g) {
|
|
166
|
-
if (!declared.has(u)) {
|
|
167
|
-
const style = hiNodes.has(u) ? ` [color="${hiColor}", penwidth=2]` : "";
|
|
168
|
-
lines.push(` "${u}"${style};`);
|
|
169
|
-
declared.add(u);
|
|
170
|
-
}
|
|
171
|
-
for (const v of vs) {
|
|
172
|
-
if (!declared.has(v)) {
|
|
173
|
-
const style = hiNodes.has(v) ? ` [color="${hiColor}", penwidth=2]` : "";
|
|
174
|
-
lines.push(` "${v}"${style};`);
|
|
175
|
-
declared.add(v);
|
|
176
|
-
}
|
|
177
|
-
const eKey = `${u}|${v}`;
|
|
178
|
-
const attrs: string[] = [];
|
|
179
|
-
if (hiEdges.has(eKey)) {
|
|
180
|
-
attrs.push(`color="${hiColor}"`, "penwidth=2");
|
|
181
|
-
}
|
|
182
|
-
const lbl = labels?.get(eKey);
|
|
183
|
-
if (lbl) {
|
|
184
|
-
attrs.push(`label="${escDot(lbl)}"`);
|
|
185
|
-
}
|
|
186
|
-
const attrStr = attrs.length ? ` [${attrs.join(", ")}]` : "";
|
|
187
|
-
lines.push(` "${u}" -> "${v}"${attrStr};`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
lines.push("}");
|
|
192
|
-
return lines.join("\n");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Utility to validate poset (check for cycles)
|
|
196
|
-
export function validatePoset(g: Digraph): { valid: boolean; cycles?: Id[][] } {
|
|
197
|
-
const cycles: Id[][] = [];
|
|
198
|
-
const state = new Map<Id, 'white' | 'gray' | 'black'>();
|
|
199
|
-
|
|
200
|
-
// Initialize all nodes as white (unvisited)
|
|
201
|
-
for (const node of g.keys()) {
|
|
202
|
-
state.set(node, 'white');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const dfs = (node: Id, path: Id[]): boolean => {
|
|
206
|
-
if (state.get(node) === 'gray') {
|
|
207
|
-
// Found a cycle
|
|
208
|
-
const cycleStart = path.indexOf(node);
|
|
209
|
-
cycles.push([...path.slice(cycleStart), node]);
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (state.get(node) === 'black') {
|
|
214
|
-
return false; // Already processed
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
state.set(node, 'gray');
|
|
218
|
-
path.push(node);
|
|
219
|
-
|
|
220
|
-
let foundCycle = false;
|
|
221
|
-
for (const neighbor of g.get(node) ?? []) {
|
|
222
|
-
if (dfs(neighbor, path)) {
|
|
223
|
-
foundCycle = true;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
path.pop();
|
|
228
|
-
state.set(node, 'black');
|
|
229
|
-
return foundCycle;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
for (const node of g.keys()) {
|
|
233
|
-
if (state.get(node) === 'white') {
|
|
234
|
-
dfs(node, []);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return { valid: cycles.length === 0, cycles: cycles.length > 0 ? cycles : undefined };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function filterDigraph(
|
|
242
|
-
g: Digraph,
|
|
243
|
-
predicate: (id: string) => boolean
|
|
244
|
-
): Digraph {
|
|
245
|
-
const out: Digraph = new Map();
|
|
246
|
-
for (const [u, vs] of g) {
|
|
247
|
-
if (!predicate(u)) continue;
|
|
248
|
-
for (const v of vs) {
|
|
249
|
-
if (!predicate(v)) continue;
|
|
250
|
-
if (!out.has(u)) out.set(u, new Set());
|
|
251
|
-
if (!out.has(v)) out.set(v, new Set());
|
|
252
|
-
out.get(u)!.add(v);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return out;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function filterByPrefix(g: Digraph, prefixes: string[]): Digraph {
|
|
259
|
-
const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
|
|
260
|
-
const keep = (id: string) =>
|
|
261
|
-
norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
|
|
262
|
-
return filterDigraph(g, keep);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function filterExcludePrefix(g: Digraph, prefixes: string[]): Digraph {
|
|
266
|
-
const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
|
|
267
|
-
const match = (id: string) =>
|
|
268
|
-
norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
|
|
269
|
-
return filterDigraph(g, id => !match(id));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function khopSubgraph(g: Digraph, seeds: Set<string>, k = 1): Digraph {
|
|
273
|
-
// treat edges as undirected for neighborhood; then filter directed edges
|
|
274
|
-
const undirected = new Map<string, Set<string>>();
|
|
275
|
-
const addU = (a: string, b: string) => {
|
|
276
|
-
if (!undirected.has(a)) undirected.set(a, new Set());
|
|
277
|
-
if (!undirected.has(b)) undirected.set(b, new Set());
|
|
278
|
-
undirected.get(a)!.add(b); undirected.get(b)!.add(a);
|
|
279
|
-
};
|
|
280
|
-
for (const [u, vs] of g) for (const v of vs) addU(u, v);
|
|
281
|
-
|
|
282
|
-
const keep = new Set<string>(seeds);
|
|
283
|
-
let frontier = new Set<string>(seeds);
|
|
284
|
-
for (let step = 0; step < k; step++) {
|
|
285
|
-
const next = new Set<string>();
|
|
286
|
-
for (const n of frontier) {
|
|
287
|
-
for (const m of undirected.get(n) ?? []) if (!keep.has(m)) { keep.add(m); next.add(m); }
|
|
288
|
-
}
|
|
289
|
-
if (!next.size) break;
|
|
290
|
-
frontier = next;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const out: Digraph = new Map();
|
|
294
|
-
for (const [u, vs] of g) if (keep.has(u)) {
|
|
295
|
-
for (const v of vs) if (keep.has(v)) {
|
|
296
|
-
if (!out.has(u)) out.set(u, new Set());
|
|
297
|
-
if (!out.has(v)) out.set(v, new Set());
|
|
298
|
-
out.get(u)!.add(v);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return out;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function pickSeedsByPattern(nodes: Iterable<string>, pattern: string): Set<string> {
|
|
305
|
-
// exact id or prefix with trailing *
|
|
306
|
-
if (pattern.endsWith("*")) {
|
|
307
|
-
const pref = pattern.slice(0, -1);
|
|
308
|
-
return new Set([...nodes].filter(id => id === pref || id.startsWith(pref)));
|
|
309
|
-
}
|
|
310
|
-
return new Set([...nodes].filter(id => id === pattern));
|
|
311
|
-
}
|
|
1
|
+
// core/poset.ts
|
|
2
|
+
// Simple poset model + transitive reduction (Hasse) + mermaid export.
|
|
3
|
+
|
|
4
|
+
export type Id = string;
|
|
5
|
+
export type Comp = "<=" | ">=";
|
|
6
|
+
export type Order = [Id, Comp, Id];
|
|
7
|
+
|
|
8
|
+
export type Digraph = Map<Id, Set<Id>>; // edges u -> v
|
|
9
|
+
|
|
10
|
+
// Edge labels for violation messages
|
|
11
|
+
export type EdgeLabels = Map<string, string>; // key = "a|b" (raw ids)
|
|
12
|
+
|
|
13
|
+
// Small escapes so Mermaid/DOT don't choke on special characters
|
|
14
|
+
const escMermaid = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
15
|
+
const escDot = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
16
|
+
|
|
17
|
+
/** Safe ID for Mermaid/DOT node identifiers */
|
|
18
|
+
export function sanitizeId(id: string): string {
|
|
19
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Highlight = {
|
|
23
|
+
nodes?: Set<string>; // raw token IDs to style
|
|
24
|
+
edges?: Set<string>; // "a|b" pairs (raw IDs) to style
|
|
25
|
+
color?: string; // hex or named (Mermaid & DOT)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function buildPoset(orders: Order[]): Digraph {
|
|
29
|
+
const g: Digraph = new Map();
|
|
30
|
+
const add = (u: Id, v: Id) => {
|
|
31
|
+
if (!g.has(u)) g.set(u, new Set());
|
|
32
|
+
if (!g.has(v)) g.set(v, new Set());
|
|
33
|
+
g.get(u)!.add(v);
|
|
34
|
+
};
|
|
35
|
+
for (const [a, op, b] of orders) {
|
|
36
|
+
if (op === ">=") add(a, b); // a ≥ b → edge a→b
|
|
37
|
+
else if (op === "<=") add(b, a); // a ≤ b → edge b→a
|
|
38
|
+
}
|
|
39
|
+
return g;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function transitiveReduction(g: Digraph): Digraph {
|
|
43
|
+
// naive but fine for our sizes: remove any edge u->v if a path u->...->v exists without that edge
|
|
44
|
+
const out: Digraph = new Map([...g.entries()].map(([u, set]) => [u, new Set(set)]));
|
|
45
|
+
const nodes = [...g.keys()];
|
|
46
|
+
const hasPath = (src: Id, dst: Id, skipU?: Id, skipV?: Id): boolean => {
|
|
47
|
+
const seen = new Set<Id>(); const stack = [src];
|
|
48
|
+
while (stack.length) {
|
|
49
|
+
const n = stack.pop()!;
|
|
50
|
+
if (n === dst) return true;
|
|
51
|
+
if (seen.has(n)) continue;
|
|
52
|
+
seen.add(n);
|
|
53
|
+
for (const w of g.get(n) ?? []) {
|
|
54
|
+
if (skipU === n && skipV === w) continue;
|
|
55
|
+
stack.push(w);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
for (const u of nodes) {
|
|
61
|
+
for (const v of g.get(u) ?? []) {
|
|
62
|
+
if (hasPath(u, v, u, v)) out.get(u)!.delete(v);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function toMermaidHasse(gHasse: Digraph, {title = "Poset"} = {}): string {
|
|
69
|
+
const lines = ["flowchart TD", `%% ${title}`];
|
|
70
|
+
for (const [u, vs] of gHasse) {
|
|
71
|
+
for (const v of vs) {
|
|
72
|
+
lines.push(` "${u}" --> "${v}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// New: styled Mermaid (ids + labels + classing)
|
|
79
|
+
export function toMermaidHasseStyled(
|
|
80
|
+
g: Digraph,
|
|
81
|
+
opts: { title?: string; highlight?: Highlight; labels?: EdgeLabels } = {}
|
|
82
|
+
): string {
|
|
83
|
+
const { title = "Poset", highlight, labels } = opts;
|
|
84
|
+
const lines = ["flowchart TD", `%% ${title}`];
|
|
85
|
+
const classes: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (const [u, vs] of g) {
|
|
88
|
+
const uId = sanitizeId(u);
|
|
89
|
+
lines.push(` ${uId}["${u}"]`);
|
|
90
|
+
for (const v of vs) {
|
|
91
|
+
const vId = sanitizeId(v);
|
|
92
|
+
lines.push(` ${vId}["${v}"]`);
|
|
93
|
+
|
|
94
|
+
// Add edge with optional label
|
|
95
|
+
const lbl = labels?.get(`${u}|${v}`);
|
|
96
|
+
if (lbl) {
|
|
97
|
+
lines.push(` ${uId} -- "${escMermaid(lbl)}" --> ${vId}`);
|
|
98
|
+
} else {
|
|
99
|
+
lines.push(` ${uId} --> ${vId}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (highlight?.edges?.has(`${u}|${v}`)) {
|
|
103
|
+
// edge styling via linkStyle… we assign after all edges (Mermaid counts links in order)
|
|
104
|
+
// simpler: also mark both nodes; edges get colored via linkStyle below
|
|
105
|
+
classes.push(uId, vId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const violNodes = new Set<string>(highlight?.nodes ? [...highlight.nodes].map(sanitizeId) : []);
|
|
111
|
+
classes.forEach(id => violNodes.add(id));
|
|
112
|
+
if (violNodes.size) {
|
|
113
|
+
lines.push(` classDef viol fill:#ffe6e6,stroke:${highlight?.color ?? "#ff0000"},stroke-width:2px;`);
|
|
114
|
+
lines.push(` class ${[...violNodes].join(",")} viol;`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Edge coloring: Mermaid lets us style by link index; simpler workaround:
|
|
118
|
+
// Add a class to nodes and rely on thick red node borders (good enough for audit views).
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function toDotHasse(
|
|
124
|
+
gHasse: Digraph,
|
|
125
|
+
opts: { title?: string; labels?: EdgeLabels } = {}
|
|
126
|
+
): string {
|
|
127
|
+
const { title = "Poset", labels } = opts;
|
|
128
|
+
const lines = [
|
|
129
|
+
"digraph poset {",
|
|
130
|
+
` label="${title}";`,
|
|
131
|
+
" rankdir=TD;",
|
|
132
|
+
" node [shape=box, style=rounded];",
|
|
133
|
+
""
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
for (const [u, vs] of gHasse) {
|
|
137
|
+
for (const v of vs) {
|
|
138
|
+
const lbl = labels?.get(`${u}|${v}`);
|
|
139
|
+
const labelAttr = lbl ? ` [label="${escDot(lbl)}"]` : "";
|
|
140
|
+
lines.push(` "${u}" -> "${v}"${labelAttr};`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push("}");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// New: styled DOT
|
|
149
|
+
export function toDotHasseStyled(
|
|
150
|
+
g: Digraph,
|
|
151
|
+
opts: { title?: string; highlight?: Highlight; labels?: EdgeLabels } = {}
|
|
152
|
+
): string {
|
|
153
|
+
const { title = "Poset", highlight, labels } = opts;
|
|
154
|
+
const lines = [
|
|
155
|
+
`digraph G {`,
|
|
156
|
+
` label="${title}"; labelloc="t"; rankdir=TB; node [shape=box];`
|
|
157
|
+
];
|
|
158
|
+
const hiColor = highlight?.color ?? "red";
|
|
159
|
+
|
|
160
|
+
const hiNodes = highlight?.nodes ?? new Set<string>();
|
|
161
|
+
const hiEdges = highlight?.edges ?? new Set<string>();
|
|
162
|
+
|
|
163
|
+
// declare nodes so we can style them
|
|
164
|
+
const declared = new Set<string>();
|
|
165
|
+
for (const [u, vs] of g) {
|
|
166
|
+
if (!declared.has(u)) {
|
|
167
|
+
const style = hiNodes.has(u) ? ` [color="${hiColor}", penwidth=2]` : "";
|
|
168
|
+
lines.push(` "${u}"${style};`);
|
|
169
|
+
declared.add(u);
|
|
170
|
+
}
|
|
171
|
+
for (const v of vs) {
|
|
172
|
+
if (!declared.has(v)) {
|
|
173
|
+
const style = hiNodes.has(v) ? ` [color="${hiColor}", penwidth=2]` : "";
|
|
174
|
+
lines.push(` "${v}"${style};`);
|
|
175
|
+
declared.add(v);
|
|
176
|
+
}
|
|
177
|
+
const eKey = `${u}|${v}`;
|
|
178
|
+
const attrs: string[] = [];
|
|
179
|
+
if (hiEdges.has(eKey)) {
|
|
180
|
+
attrs.push(`color="${hiColor}"`, "penwidth=2");
|
|
181
|
+
}
|
|
182
|
+
const lbl = labels?.get(eKey);
|
|
183
|
+
if (lbl) {
|
|
184
|
+
attrs.push(`label="${escDot(lbl)}"`);
|
|
185
|
+
}
|
|
186
|
+
const attrStr = attrs.length ? ` [${attrs.join(", ")}]` : "";
|
|
187
|
+
lines.push(` "${u}" -> "${v}"${attrStr};`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
lines.push("}");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Utility to validate poset (check for cycles)
|
|
196
|
+
export function validatePoset(g: Digraph): { valid: boolean; cycles?: Id[][] } {
|
|
197
|
+
const cycles: Id[][] = [];
|
|
198
|
+
const state = new Map<Id, 'white' | 'gray' | 'black'>();
|
|
199
|
+
|
|
200
|
+
// Initialize all nodes as white (unvisited)
|
|
201
|
+
for (const node of g.keys()) {
|
|
202
|
+
state.set(node, 'white');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const dfs = (node: Id, path: Id[]): boolean => {
|
|
206
|
+
if (state.get(node) === 'gray') {
|
|
207
|
+
// Found a cycle
|
|
208
|
+
const cycleStart = path.indexOf(node);
|
|
209
|
+
cycles.push([...path.slice(cycleStart), node]);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (state.get(node) === 'black') {
|
|
214
|
+
return false; // Already processed
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
state.set(node, 'gray');
|
|
218
|
+
path.push(node);
|
|
219
|
+
|
|
220
|
+
let foundCycle = false;
|
|
221
|
+
for (const neighbor of g.get(node) ?? []) {
|
|
222
|
+
if (dfs(neighbor, path)) {
|
|
223
|
+
foundCycle = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
path.pop();
|
|
228
|
+
state.set(node, 'black');
|
|
229
|
+
return foundCycle;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (const node of g.keys()) {
|
|
233
|
+
if (state.get(node) === 'white') {
|
|
234
|
+
dfs(node, []);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { valid: cycles.length === 0, cycles: cycles.length > 0 ? cycles : undefined };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function filterDigraph(
|
|
242
|
+
g: Digraph,
|
|
243
|
+
predicate: (id: string) => boolean
|
|
244
|
+
): Digraph {
|
|
245
|
+
const out: Digraph = new Map();
|
|
246
|
+
for (const [u, vs] of g) {
|
|
247
|
+
if (!predicate(u)) continue;
|
|
248
|
+
for (const v of vs) {
|
|
249
|
+
if (!predicate(v)) continue;
|
|
250
|
+
if (!out.has(u)) out.set(u, new Set());
|
|
251
|
+
if (!out.has(v)) out.set(v, new Set());
|
|
252
|
+
out.get(u)!.add(v);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function filterByPrefix(g: Digraph, prefixes: string[]): Digraph {
|
|
259
|
+
const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
|
|
260
|
+
const keep = (id: string) =>
|
|
261
|
+
norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
|
|
262
|
+
return filterDigraph(g, keep);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function filterExcludePrefix(g: Digraph, prefixes: string[]): Digraph {
|
|
266
|
+
const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
|
|
267
|
+
const match = (id: string) =>
|
|
268
|
+
norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
|
|
269
|
+
return filterDigraph(g, id => !match(id));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function khopSubgraph(g: Digraph, seeds: Set<string>, k = 1): Digraph {
|
|
273
|
+
// treat edges as undirected for neighborhood; then filter directed edges
|
|
274
|
+
const undirected = new Map<string, Set<string>>();
|
|
275
|
+
const addU = (a: string, b: string) => {
|
|
276
|
+
if (!undirected.has(a)) undirected.set(a, new Set());
|
|
277
|
+
if (!undirected.has(b)) undirected.set(b, new Set());
|
|
278
|
+
undirected.get(a)!.add(b); undirected.get(b)!.add(a);
|
|
279
|
+
};
|
|
280
|
+
for (const [u, vs] of g) for (const v of vs) addU(u, v);
|
|
281
|
+
|
|
282
|
+
const keep = new Set<string>(seeds);
|
|
283
|
+
let frontier = new Set<string>(seeds);
|
|
284
|
+
for (let step = 0; step < k; step++) {
|
|
285
|
+
const next = new Set<string>();
|
|
286
|
+
for (const n of frontier) {
|
|
287
|
+
for (const m of undirected.get(n) ?? []) if (!keep.has(m)) { keep.add(m); next.add(m); }
|
|
288
|
+
}
|
|
289
|
+
if (!next.size) break;
|
|
290
|
+
frontier = next;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const out: Digraph = new Map();
|
|
294
|
+
for (const [u, vs] of g) if (keep.has(u)) {
|
|
295
|
+
for (const v of vs) if (keep.has(v)) {
|
|
296
|
+
if (!out.has(u)) out.set(u, new Set());
|
|
297
|
+
if (!out.has(v)) out.set(v, new Set());
|
|
298
|
+
out.get(u)!.add(v);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function pickSeedsByPattern(nodes: Iterable<string>, pattern: string): Set<string> {
|
|
305
|
+
// exact id or prefix with trailing *
|
|
306
|
+
if (pattern.endsWith("*")) {
|
|
307
|
+
const pref = pattern.slice(0, -1);
|
|
308
|
+
return new Set([...nodes].filter(id => id === pref || id.startsWith(pref)));
|
|
309
|
+
}
|
|
310
|
+
return new Set([...nodes].filter(id => id === pattern));
|
|
311
|
+
}
|