design-constraint-validator 1.0.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +659 -0
  3. package/adapters/README.md +46 -0
  4. package/adapters/css.d.ts +44 -0
  5. package/adapters/css.d.ts.map +1 -0
  6. package/adapters/css.js +97 -0
  7. package/adapters/css.ts +116 -0
  8. package/adapters/js.d.ts +3 -0
  9. package/adapters/js.d.ts.map +1 -0
  10. package/adapters/js.js +15 -0
  11. package/adapters/js.ts +14 -0
  12. package/adapters/json.d.ts +18 -0
  13. package/adapters/json.d.ts.map +1 -0
  14. package/adapters/json.js +35 -0
  15. package/adapters/json.ts +45 -0
  16. package/cli/build-css.d.ts +2 -0
  17. package/cli/build-css.d.ts.map +1 -0
  18. package/cli/build-css.js +23 -0
  19. package/cli/build-css.ts +32 -0
  20. package/cli/commands/build.d.ts +5 -0
  21. package/cli/commands/build.d.ts.map +1 -0
  22. package/cli/commands/build.js +89 -0
  23. package/cli/commands/build.ts +65 -0
  24. package/cli/commands/graph.d.ts +3 -0
  25. package/cli/commands/graph.d.ts.map +1 -0
  26. package/cli/commands/graph.js +219 -0
  27. package/cli/commands/graph.ts +137 -0
  28. package/cli/commands/index.d.ts +8 -0
  29. package/cli/commands/index.d.ts.map +1 -0
  30. package/cli/commands/index.js +7 -0
  31. package/cli/commands/index.ts +7 -0
  32. package/cli/commands/patch-apply.d.ts +3 -0
  33. package/cli/commands/patch-apply.d.ts.map +1 -0
  34. package/cli/commands/patch-apply.js +75 -0
  35. package/cli/commands/patch-apply.ts +80 -0
  36. package/cli/commands/patch.d.ts +3 -0
  37. package/cli/commands/patch.d.ts.map +1 -0
  38. package/cli/commands/patch.js +21 -0
  39. package/cli/commands/patch.ts +22 -0
  40. package/cli/commands/set.d.ts +3 -0
  41. package/cli/commands/set.d.ts.map +1 -0
  42. package/cli/commands/set.js +286 -0
  43. package/cli/commands/set.ts +225 -0
  44. package/cli/commands/utils.d.ts +4 -0
  45. package/cli/commands/utils.d.ts.map +1 -0
  46. package/cli/commands/utils.js +51 -0
  47. package/cli/commands/utils.ts +50 -0
  48. package/cli/commands/validate.d.ts +3 -0
  49. package/cli/commands/validate.d.ts.map +1 -0
  50. package/cli/commands/validate.js +131 -0
  51. package/cli/commands/validate.ts +115 -0
  52. package/cli/commands/why.d.ts +3 -0
  53. package/cli/commands/why.d.ts.map +1 -0
  54. package/cli/commands/why.js +64 -0
  55. package/cli/commands/why.ts +46 -0
  56. package/cli/config-schema.d.ts +238 -0
  57. package/cli/config-schema.d.ts.map +1 -0
  58. package/cli/config-schema.js +21 -0
  59. package/cli/config-schema.ts +27 -0
  60. package/cli/config.d.ts +4 -0
  61. package/cli/config.d.ts.map +1 -0
  62. package/cli/config.js +37 -0
  63. package/cli/config.ts +35 -0
  64. package/cli/dcv.d.ts +3 -0
  65. package/cli/dcv.d.ts.map +1 -0
  66. package/cli/dcv.js +86 -0
  67. package/cli/dcv.ts +107 -0
  68. package/cli/engine-helpers.d.ts +8 -0
  69. package/cli/engine-helpers.d.ts.map +1 -0
  70. package/cli/engine-helpers.js +70 -0
  71. package/cli/engine-helpers.ts +61 -0
  72. package/cli/graph-poset.d.ts +9 -0
  73. package/cli/graph-poset.d.ts.map +1 -0
  74. package/cli/graph-poset.js +58 -0
  75. package/cli/graph-poset.ts +74 -0
  76. package/cli/index.d.ts +3 -0
  77. package/cli/index.d.ts.map +1 -0
  78. package/cli/index.js +2 -0
  79. package/cli/index.ts +2 -0
  80. package/cli/result.d.ts +17 -0
  81. package/cli/result.d.ts.map +1 -0
  82. package/cli/result.js +29 -0
  83. package/cli/result.ts +27 -0
  84. package/cli/run.d.ts +3 -0
  85. package/cli/run.d.ts.map +1 -0
  86. package/cli/run.js +47 -0
  87. package/cli/run.ts +54 -0
  88. package/cli/smoke-test.d.ts +2 -0
  89. package/cli/smoke-test.d.ts.map +1 -0
  90. package/cli/smoke-test.js +33 -0
  91. package/cli/smoke-test.ts +40 -0
  92. package/cli/types.d.ts +86 -0
  93. package/cli/types.d.ts.map +1 -0
  94. package/cli/types.js +1 -0
  95. package/cli/types.ts +78 -0
  96. package/core/breakpoints.d.ts +12 -0
  97. package/core/breakpoints.d.ts.map +1 -0
  98. package/core/breakpoints.js +48 -0
  99. package/core/breakpoints.ts +50 -0
  100. package/core/cli-format.d.ts +8 -0
  101. package/core/cli-format.d.ts.map +1 -0
  102. package/core/cli-format.js +29 -0
  103. package/core/cli-format.ts +31 -0
  104. package/core/color.d.ts +14 -0
  105. package/core/color.d.ts.map +1 -0
  106. package/core/color.js +136 -0
  107. package/core/color.ts +148 -0
  108. package/core/constraints/cross-axis.d.ts +33 -0
  109. package/core/constraints/cross-axis.d.ts.map +1 -0
  110. package/core/constraints/cross-axis.js +93 -0
  111. package/core/constraints/cross-axis.ts +114 -0
  112. package/core/constraints/monotonic-lightness.d.ts +5 -0
  113. package/core/constraints/monotonic-lightness.d.ts.map +1 -0
  114. package/core/constraints/monotonic-lightness.js +37 -0
  115. package/core/constraints/monotonic-lightness.ts +38 -0
  116. package/core/constraints/monotonic.d.ts +7 -0
  117. package/core/constraints/monotonic.d.ts.map +1 -0
  118. package/core/constraints/monotonic.js +65 -0
  119. package/core/constraints/monotonic.ts +74 -0
  120. package/core/constraints/threshold.d.ts +10 -0
  121. package/core/constraints/threshold.d.ts.map +1 -0
  122. package/core/constraints/threshold.js +36 -0
  123. package/core/constraints/threshold.ts +43 -0
  124. package/core/constraints/wcag.d.ts +11 -0
  125. package/core/constraints/wcag.d.ts.map +1 -0
  126. package/core/constraints/wcag.js +53 -0
  127. package/core/constraints/wcag.ts +70 -0
  128. package/core/cross-axis-config.d.ts +5 -0
  129. package/core/cross-axis-config.d.ts.map +1 -0
  130. package/core/cross-axis-config.js +144 -0
  131. package/core/cross-axis-config.ts +152 -0
  132. package/core/engine.d.ts +32 -0
  133. package/core/engine.d.ts.map +1 -0
  134. package/core/engine.js +46 -0
  135. package/core/engine.ts +65 -0
  136. package/core/flatten.d.ts +20 -0
  137. package/core/flatten.d.ts.map +1 -0
  138. package/core/flatten.js +80 -0
  139. package/core/flatten.ts +116 -0
  140. package/core/image-export.d.ts +10 -0
  141. package/core/image-export.d.ts.map +1 -0
  142. package/core/image-export.js +43 -0
  143. package/core/image-export.ts +48 -0
  144. package/core/index.d.ts +31 -0
  145. package/core/index.d.ts.map +1 -0
  146. package/core/index.js +54 -0
  147. package/core/index.ts +72 -0
  148. package/core/patch.d.ts +28 -0
  149. package/core/patch.d.ts.map +1 -0
  150. package/core/patch.js +110 -0
  151. package/core/patch.ts +134 -0
  152. package/core/poset.d.ts +41 -0
  153. package/core/poset.d.ts.map +1 -0
  154. package/core/poset.js +275 -0
  155. package/core/poset.ts +311 -0
  156. package/core/why.d.ts +17 -0
  157. package/core/why.d.ts.map +1 -0
  158. package/core/why.js +45 -0
  159. package/core/why.ts +63 -0
  160. package/dist/test-overrides-removal.json +4 -0
  161. package/dist/tmp.patch.json +35 -0
  162. package/package.json +90 -0
  163. package/themes/color.lg.order.json +15 -0
  164. package/themes/color.md.order.json +15 -0
  165. package/themes/color.order.json +15 -0
  166. package/themes/color.sm.order.json +15 -0
  167. package/themes/cross-axis.rules.json +36 -0
  168. package/themes/cross-axis.sm.rules.json +12 -0
  169. package/themes/layout.lg.order.json +18 -0
  170. package/themes/layout.md.order.json +18 -0
  171. package/themes/layout.order.json +18 -0
  172. package/themes/layout.sm.order.json +18 -0
  173. package/themes/spacing.order.json +14 -0
  174. package/themes/typography.lg.order.json +15 -0
  175. package/themes/typography.md.order.json +15 -0
  176. package/themes/typography.order.json +15 -0
  177. package/themes/typography.sm.order.json +15 -0
  178. package/tokens/overrides/base.json +22 -0
  179. package/tokens/overrides/lg.json +20 -0
  180. package/tokens/overrides/md.json +16 -0
  181. package/tokens/overrides/sm.json +16 -0
  182. package/tokens/overrides/viol.color.json +6 -0
  183. package/tokens/overrides/viol.typography.json +6 -0
  184. package/tokens/tokens.demo-violations.json +116 -0
  185. package/tokens/tokens.example.json +128 -0
  186. package/tokens/tokens.json +67 -0
  187. package/tokens/tokens.multi-violations.json +21 -0
  188. package/tokens/tokens.schema.d.ts +2298 -0
  189. package/tokens/tokens.schema.d.ts.map +1 -0
  190. package/tokens/tokens.schema.js +148 -0
  191. package/tokens/tokens.schema.ts +196 -0
  192. package/tokens/tokens.test.json +38 -0
  193. package/tokens/tokens.touch-violation.json +8 -0
  194. package/tokens/typography.classes.css +11 -0
  195. package/tokens/typography.css +20 -0
package/core/poset.js ADDED
@@ -0,0 +1,275 @@
1
+ // core/poset.ts
2
+ // Simple poset model + transitive reduction (Hasse) + mermaid export.
3
+ // Small escapes so Mermaid/DOT don't choke on special characters
4
+ const escMermaid = (s) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
5
+ const escDot = (s) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
6
+ /** Safe ID for Mermaid/DOT node identifiers */
7
+ export function sanitizeId(id) {
8
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
9
+ }
10
+ export function buildPoset(orders) {
11
+ const g = new Map();
12
+ const add = (u, v) => {
13
+ if (!g.has(u))
14
+ g.set(u, new Set());
15
+ if (!g.has(v))
16
+ g.set(v, new Set());
17
+ g.get(u).add(v);
18
+ };
19
+ for (const [a, op, b] of orders) {
20
+ if (op === ">=")
21
+ add(a, b); // a ≥ b → edge a→b
22
+ else if (op === "<=")
23
+ add(b, a); // a ≤ b → edge b→a
24
+ }
25
+ return g;
26
+ }
27
+ export function transitiveReduction(g) {
28
+ // naive but fine for our sizes: remove any edge u->v if a path u->...->v exists without that edge
29
+ const out = new Map([...g.entries()].map(([u, set]) => [u, new Set(set)]));
30
+ const nodes = [...g.keys()];
31
+ const hasPath = (src, dst, skipU, skipV) => {
32
+ const seen = new Set();
33
+ const stack = [src];
34
+ while (stack.length) {
35
+ const n = stack.pop();
36
+ if (n === dst)
37
+ return true;
38
+ if (seen.has(n))
39
+ continue;
40
+ seen.add(n);
41
+ for (const w of g.get(n) ?? []) {
42
+ if (skipU === n && skipV === w)
43
+ continue;
44
+ stack.push(w);
45
+ }
46
+ }
47
+ return false;
48
+ };
49
+ for (const u of nodes) {
50
+ for (const v of g.get(u) ?? []) {
51
+ if (hasPath(u, v, u, v))
52
+ out.get(u).delete(v);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ export function toMermaidHasse(gHasse, { title = "Poset" } = {}) {
58
+ const lines = ["flowchart TD", `%% ${title}`];
59
+ for (const [u, vs] of gHasse) {
60
+ for (const v of vs) {
61
+ lines.push(` "${u}" --> "${v}"`);
62
+ }
63
+ }
64
+ return lines.join("\n");
65
+ }
66
+ // New: styled Mermaid (ids + labels + classing)
67
+ export function toMermaidHasseStyled(g, opts = {}) {
68
+ const { title = "Poset", highlight, labels } = opts;
69
+ const lines = ["flowchart TD", `%% ${title}`];
70
+ const classes = [];
71
+ for (const [u, vs] of g) {
72
+ const uId = sanitizeId(u);
73
+ lines.push(` ${uId}["${u}"]`);
74
+ for (const v of vs) {
75
+ const vId = sanitizeId(v);
76
+ lines.push(` ${vId}["${v}"]`);
77
+ // Add edge with optional label
78
+ const lbl = labels?.get(`${u}|${v}`);
79
+ if (lbl) {
80
+ lines.push(` ${uId} -- "${escMermaid(lbl)}" --> ${vId}`);
81
+ }
82
+ else {
83
+ lines.push(` ${uId} --> ${vId}`);
84
+ }
85
+ if (highlight?.edges?.has(`${u}|${v}`)) {
86
+ // edge styling via linkStyle… we assign after all edges (Mermaid counts links in order)
87
+ // simpler: also mark both nodes; edges get colored via linkStyle below
88
+ classes.push(uId, vId);
89
+ }
90
+ }
91
+ }
92
+ const violNodes = new Set(highlight?.nodes ? [...highlight.nodes].map(sanitizeId) : []);
93
+ classes.forEach(id => violNodes.add(id));
94
+ if (violNodes.size) {
95
+ lines.push(` classDef viol fill:#ffe6e6,stroke:${highlight?.color ?? "#ff0000"},stroke-width:2px;`);
96
+ lines.push(` class ${[...violNodes].join(",")} viol;`);
97
+ }
98
+ // Edge coloring: Mermaid lets us style by link index; simpler workaround:
99
+ // Add a class to nodes and rely on thick red node borders (good enough for audit views).
100
+ return lines.join("\n");
101
+ }
102
+ export function toDotHasse(gHasse, opts = {}) {
103
+ const { title = "Poset", labels } = opts;
104
+ const lines = [
105
+ "digraph poset {",
106
+ ` label="${title}";`,
107
+ " rankdir=TD;",
108
+ " node [shape=box, style=rounded];",
109
+ ""
110
+ ];
111
+ for (const [u, vs] of gHasse) {
112
+ for (const v of vs) {
113
+ const lbl = labels?.get(`${u}|${v}`);
114
+ const labelAttr = lbl ? ` [label="${escDot(lbl)}"]` : "";
115
+ lines.push(` "${u}" -> "${v}"${labelAttr};`);
116
+ }
117
+ }
118
+ lines.push("}");
119
+ return lines.join("\n");
120
+ }
121
+ // New: styled DOT
122
+ export function toDotHasseStyled(g, opts = {}) {
123
+ const { title = "Poset", highlight, labels } = opts;
124
+ const lines = [
125
+ `digraph G {`,
126
+ ` label="${title}"; labelloc="t"; rankdir=TB; node [shape=box];`
127
+ ];
128
+ const hiColor = highlight?.color ?? "red";
129
+ const hiNodes = highlight?.nodes ?? new Set();
130
+ const hiEdges = highlight?.edges ?? new Set();
131
+ // declare nodes so we can style them
132
+ const declared = new Set();
133
+ for (const [u, vs] of g) {
134
+ if (!declared.has(u)) {
135
+ const style = hiNodes.has(u) ? ` [color="${hiColor}", penwidth=2]` : "";
136
+ lines.push(` "${u}"${style};`);
137
+ declared.add(u);
138
+ }
139
+ for (const v of vs) {
140
+ if (!declared.has(v)) {
141
+ const style = hiNodes.has(v) ? ` [color="${hiColor}", penwidth=2]` : "";
142
+ lines.push(` "${v}"${style};`);
143
+ declared.add(v);
144
+ }
145
+ const eKey = `${u}|${v}`;
146
+ const attrs = [];
147
+ if (hiEdges.has(eKey)) {
148
+ attrs.push(`color="${hiColor}"`, "penwidth=2");
149
+ }
150
+ const lbl = labels?.get(eKey);
151
+ if (lbl) {
152
+ attrs.push(`label="${escDot(lbl)}"`);
153
+ }
154
+ const attrStr = attrs.length ? ` [${attrs.join(", ")}]` : "";
155
+ lines.push(` "${u}" -> "${v}"${attrStr};`);
156
+ }
157
+ }
158
+ lines.push("}");
159
+ return lines.join("\n");
160
+ }
161
+ // Utility to validate poset (check for cycles)
162
+ export function validatePoset(g) {
163
+ const cycles = [];
164
+ const state = new Map();
165
+ // Initialize all nodes as white (unvisited)
166
+ for (const node of g.keys()) {
167
+ state.set(node, 'white');
168
+ }
169
+ const dfs = (node, path) => {
170
+ if (state.get(node) === 'gray') {
171
+ // Found a cycle
172
+ const cycleStart = path.indexOf(node);
173
+ cycles.push([...path.slice(cycleStart), node]);
174
+ return true;
175
+ }
176
+ if (state.get(node) === 'black') {
177
+ return false; // Already processed
178
+ }
179
+ state.set(node, 'gray');
180
+ path.push(node);
181
+ let foundCycle = false;
182
+ for (const neighbor of g.get(node) ?? []) {
183
+ if (dfs(neighbor, path)) {
184
+ foundCycle = true;
185
+ }
186
+ }
187
+ path.pop();
188
+ state.set(node, 'black');
189
+ return foundCycle;
190
+ };
191
+ for (const node of g.keys()) {
192
+ if (state.get(node) === 'white') {
193
+ dfs(node, []);
194
+ }
195
+ }
196
+ return { valid: cycles.length === 0, cycles: cycles.length > 0 ? cycles : undefined };
197
+ }
198
+ export function filterDigraph(g, predicate) {
199
+ const out = new Map();
200
+ for (const [u, vs] of g) {
201
+ if (!predicate(u))
202
+ continue;
203
+ for (const v of vs) {
204
+ if (!predicate(v))
205
+ continue;
206
+ if (!out.has(u))
207
+ out.set(u, new Set());
208
+ if (!out.has(v))
209
+ out.set(v, new Set());
210
+ out.get(u).add(v);
211
+ }
212
+ }
213
+ return out;
214
+ }
215
+ export function filterByPrefix(g, prefixes) {
216
+ const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
217
+ const keep = (id) => norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
218
+ return filterDigraph(g, keep);
219
+ }
220
+ export function filterExcludePrefix(g, prefixes) {
221
+ const norm = prefixes.map(p => (p.endsWith(".") ? p : p + "."));
222
+ const match = (id) => norm.some(p => id === p.slice(0, -1) || id.startsWith(p));
223
+ return filterDigraph(g, id => !match(id));
224
+ }
225
+ export function khopSubgraph(g, seeds, k = 1) {
226
+ // treat edges as undirected for neighborhood; then filter directed edges
227
+ const undirected = new Map();
228
+ const addU = (a, b) => {
229
+ if (!undirected.has(a))
230
+ undirected.set(a, new Set());
231
+ if (!undirected.has(b))
232
+ undirected.set(b, new Set());
233
+ undirected.get(a).add(b);
234
+ undirected.get(b).add(a);
235
+ };
236
+ for (const [u, vs] of g)
237
+ for (const v of vs)
238
+ addU(u, v);
239
+ const keep = new Set(seeds);
240
+ let frontier = new Set(seeds);
241
+ for (let step = 0; step < k; step++) {
242
+ const next = new Set();
243
+ for (const n of frontier) {
244
+ for (const m of undirected.get(n) ?? [])
245
+ if (!keep.has(m)) {
246
+ keep.add(m);
247
+ next.add(m);
248
+ }
249
+ }
250
+ if (!next.size)
251
+ break;
252
+ frontier = next;
253
+ }
254
+ const out = new Map();
255
+ for (const [u, vs] of g)
256
+ if (keep.has(u)) {
257
+ for (const v of vs)
258
+ if (keep.has(v)) {
259
+ if (!out.has(u))
260
+ out.set(u, new Set());
261
+ if (!out.has(v))
262
+ out.set(v, new Set());
263
+ out.get(u).add(v);
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+ export function pickSeedsByPattern(nodes, pattern) {
269
+ // exact id or prefix with trailing *
270
+ if (pattern.endsWith("*")) {
271
+ const pref = pattern.slice(0, -1);
272
+ return new Set([...nodes].filter(id => id === pref || id.startsWith(pref)));
273
+ }
274
+ return new Set([...nodes].filter(id => id === pattern));
275
+ }
package/core/poset.ts ADDED
@@ -0,0 +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
+ }
package/core/why.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { FlatToken } from "./flatten.js";
2
+ export type WhyReport = {
3
+ id: string;
4
+ value: string | number | undefined;
5
+ raw?: string | number;
6
+ refs?: string[];
7
+ provenance: "base" | "theme" | "override" | "unknown";
8
+ dependsOn: string[];
9
+ dependents: string[];
10
+ chain?: string[];
11
+ };
12
+ export declare function makeReverseEdges(edges: Array<[string, string]>): Map<string, Set<string>>;
13
+ export declare function explain(id: string, flat: Record<string, FlatToken>, edges: Array<[string, string]>, layers?: {
14
+ overrides?: Record<string, unknown>;
15
+ theme?: Record<string, unknown>;
16
+ }): WhyReport;
17
+ //# sourceMappingURL=why.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAQxF;AAED,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC/B,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAC,MAAM,CAAC,CAAC,EAC7B,MAAM,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GACpF,SAAS,CAkCX"}
package/core/why.js ADDED
@@ -0,0 +1,45 @@
1
+ export function makeReverseEdges(edges) {
2
+ const rev = new Map();
3
+ for (const [from, to] of edges) {
4
+ if (!rev.has(to))
5
+ rev.set(to, new Set());
6
+ rev.get(to).add(from);
7
+ if (!rev.has(from))
8
+ rev.set(from, new Set());
9
+ }
10
+ return rev;
11
+ }
12
+ export function explain(id, flat, edges, layers = {}) {
13
+ const node = flat[id];
14
+ const rev = makeReverseEdges(edges);
15
+ const dependsOn = Array.from(rev.get(id) ?? []);
16
+ const dependents = edges.filter(([from, _to]) => from === id).map(([_, to]) => to);
17
+ const prov = layers.overrides && id in layers.overrides ? "override" :
18
+ layers.theme && id in layers.theme ? "theme" :
19
+ flat[id] ? "base" : "unknown";
20
+ // Try to produce a simple chain if it's a 1→1 alias chain
21
+ const chain = [id];
22
+ let cursor = id;
23
+ let guard = 0;
24
+ while (guard++ < 32) {
25
+ const f = flat[cursor];
26
+ if (!f || !Array.isArray(f.refs) || f.refs.length !== 1)
27
+ break;
28
+ const next = f.refs[0];
29
+ chain.push(next);
30
+ cursor = next;
31
+ const nxt = flat[next];
32
+ if (!nxt || !nxt.refs?.length)
33
+ break;
34
+ }
35
+ return {
36
+ id,
37
+ value: node?.value,
38
+ raw: node?.raw,
39
+ refs: node?.refs,
40
+ provenance: prov,
41
+ dependsOn,
42
+ dependents,
43
+ chain: chain.length > 1 ? chain : undefined
44
+ };
45
+ }