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.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -659
  3. package/adapters/README.md +46 -46
  4. package/adapters/css.ts +116 -116
  5. package/adapters/js.ts +14 -14
  6. package/adapters/json.ts +45 -45
  7. package/cli/build-css.ts +32 -32
  8. package/cli/commands/build.ts +65 -65
  9. package/cli/commands/graph.d.ts.map +1 -1
  10. package/cli/commands/graph.js +26 -10
  11. package/cli/commands/graph.ts +180 -137
  12. package/cli/commands/index.ts +7 -7
  13. package/cli/commands/patch-apply.ts +80 -80
  14. package/cli/commands/patch.ts +22 -22
  15. package/cli/commands/set.d.ts.map +1 -1
  16. package/cli/commands/set.js +12 -4
  17. package/cli/commands/set.ts +239 -225
  18. package/cli/commands/utils.ts +50 -50
  19. package/cli/commands/validate.d.ts.map +1 -1
  20. package/cli/commands/validate.js +86 -33
  21. package/cli/commands/validate.ts +176 -115
  22. package/cli/commands/why.d.ts.map +1 -1
  23. package/cli/commands/why.js +86 -20
  24. package/cli/commands/why.ts +158 -46
  25. package/cli/config-schema.ts +27 -27
  26. package/cli/config.ts +35 -35
  27. package/cli/constraint-registry.d.ts +101 -0
  28. package/cli/constraint-registry.d.ts.map +1 -0
  29. package/cli/constraint-registry.js +225 -0
  30. package/cli/constraint-registry.ts +304 -0
  31. package/cli/constraints-loader.d.ts +30 -0
  32. package/cli/constraints-loader.d.ts.map +1 -0
  33. package/cli/constraints-loader.js +58 -0
  34. package/cli/constraints-loader.ts +83 -0
  35. package/cli/cross-axis-loader.d.ts +91 -0
  36. package/cli/cross-axis-loader.d.ts.map +1 -0
  37. package/cli/cross-axis-loader.js +222 -0
  38. package/cli/cross-axis-loader.ts +289 -0
  39. package/cli/dcv.js +4 -0
  40. package/cli/dcv.ts +111 -107
  41. package/cli/engine-helpers.d.ts +33 -0
  42. package/cli/engine-helpers.d.ts.map +1 -1
  43. package/cli/engine-helpers.js +87 -22
  44. package/cli/engine-helpers.ts +133 -61
  45. package/cli/graph-poset.ts +74 -74
  46. package/cli/json-output.d.ts +64 -0
  47. package/cli/json-output.d.ts.map +1 -0
  48. package/cli/json-output.js +107 -0
  49. package/cli/json-output.ts +177 -0
  50. package/cli/result.ts +27 -27
  51. package/cli/run.ts +54 -54
  52. package/cli/smoke-test.ts +40 -40
  53. package/cli/types.d.ts +6 -0
  54. package/cli/types.d.ts.map +1 -1
  55. package/cli/types.ts +84 -78
  56. package/core/breakpoints.ts +50 -50
  57. package/core/cli-format.ts +31 -31
  58. package/core/color.ts +148 -148
  59. package/core/constraints/cross-axis.ts +114 -114
  60. package/core/constraints/monotonic-lightness.ts +38 -38
  61. package/core/constraints/monotonic.ts +74 -74
  62. package/core/constraints/threshold.ts +43 -43
  63. package/core/constraints/wcag.ts +70 -70
  64. package/core/cross-axis-config.d.ts +29 -0
  65. package/core/cross-axis-config.d.ts.map +1 -1
  66. package/core/cross-axis-config.js +29 -0
  67. package/core/cross-axis-config.ts +181 -151
  68. package/core/engine.d.ts +95 -0
  69. package/core/engine.d.ts.map +1 -1
  70. package/core/engine.js +22 -0
  71. package/core/engine.ts +167 -65
  72. package/core/flatten.ts +116 -116
  73. package/core/image-export.ts +48 -48
  74. package/core/index.d.ts +9 -30
  75. package/core/index.d.ts.map +1 -1
  76. package/core/index.js +7 -54
  77. package/core/index.ts +10 -72
  78. package/core/patch.ts +134 -134
  79. package/core/poset.ts +311 -311
  80. package/core/why.ts +63 -63
  81. package/package.json +96 -90
  82. package/themes/color.lg.order.json +15 -15
  83. package/themes/color.md.order.json +15 -15
  84. package/themes/color.order.json +15 -15
  85. package/themes/color.sm.order.json +15 -15
  86. package/themes/cross-axis.rules.json +35 -35
  87. package/themes/cross-axis.sm.rules.json +12 -12
  88. package/themes/layout.lg.order.json +18 -18
  89. package/themes/layout.md.order.json +18 -18
  90. package/themes/layout.order.json +18 -18
  91. package/themes/layout.sm.order.json +18 -18
  92. package/themes/spacing.order.json +14 -14
  93. package/themes/typography.lg.order.json +15 -15
  94. package/themes/typography.md.order.json +15 -15
  95. package/themes/typography.order.json +15 -15
  96. package/themes/typography.sm.order.json +15 -15
  97. package/dist/test-overrides-removal.json +0 -4
  98. package/dist/tmp.patch.json +0 -35
  99. package/tokens/overrides/base.json +0 -22
  100. package/tokens/overrides/lg.json +0 -20
  101. package/tokens/overrides/md.json +0 -16
  102. package/tokens/overrides/sm.json +0 -16
  103. package/tokens/overrides/viol.color.json +0 -6
  104. package/tokens/overrides/viol.typography.json +0 -6
  105. package/tokens/tokens.demo-violations.json +0 -116
  106. package/tokens/tokens.example.json +0 -128
  107. package/tokens/tokens.json +0 -67
  108. package/tokens/tokens.multi-violations.json +0 -21
  109. package/tokens/tokens.schema.d.ts +0 -2298
  110. package/tokens/tokens.schema.d.ts.map +0 -1
  111. package/tokens/tokens.schema.js +0 -148
  112. package/tokens/tokens.schema.ts +0 -196
  113. package/tokens/tokens.test.json +0 -38
  114. package/tokens/tokens.touch-violation.json +0 -8
  115. package/tokens/typography.classes.css +0 -11
  116. package/tokens/typography.css +0 -20
@@ -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 { ThresholdPlugin } = await import('../../core/constraints/threshold.js');
107
- const { flat, edges } = flattenTokens(tokens);
108
+ const { flat, edges: depEdges } = flattenTokens(tokens);
108
109
  const init = {};
109
- Object.values(flat).forEach(t => { const ft = t; init[ft.id] = ft.value; });
110
- const engine = new Engine(init, edges);
111
- const allIdsInHasse = new Set([...h.keys(), ...Array.from(h.values()).flatMap(s => [...s])]);
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
- const thresholdIssues = ThresholdPlugin([{ id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' }]).evaluate(engine, allIdsInHasse);
122
- issues.push(...thresholdIssues);
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 => { const level = it.level || 'error'; return severityRank[level] >= severityRank[minSeverity]; });
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) {
@@ -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
- // Local helper for non-poset dependency graphs
7
- function generateDependencyGraph(edges: Array<[string, string]>, format: string): string {
8
- switch (format) {
9
- case 'dot': {
10
- const dotNodes = new Set<string>(); edges.forEach(([f,t]) => { dotNodes.add(f); dotNodes.add(t); });
11
- let dot = 'digraph tokens {\n rankdir=LR;\n node [shape=box, style=rounded];\n\n';
12
- dotNodes.forEach(n => { dot += ` "${n}";\n`; });
13
- dot += '\n'; edges.forEach(([f,t]) => { dot += ` "${f}" -> "${t}";\n`; });
14
- return dot + '}\n'; }
15
- case 'mermaid': {
16
- let mermaid = 'graph LR\n'; const mermaidNodes = new Set<string>();
17
- edges.forEach(([from,to]) => { const fromId = from.replace(/[^a-zA-Z0-9]/g,'_'); const toId = to.replace(/[^a-zA-Z0-9]/g,'_');
18
- if (!mermaidNodes.has(fromId)) { mermaid += ` ${fromId}["${from}"]\n`; mermaidNodes.add(fromId); }
19
- if (!mermaidNodes.has(toId)) { mermaid += ` ${toId}["${to}"]\n`; mermaidNodes.add(toId); }
20
- mermaid += ` ${fromId} --> ${toId}\n`; });
21
- return mermaid; }
22
- case 'json':
23
- default: return JSON.stringify({ nodes: Array.from(new Set(edges.flat())), edges }, null, 2);
24
- }
25
- }
26
-
27
- export async function graphCommand(options: GraphOptions): Promise<void> {
28
- const { parseBreakpoints } = await import('../../core/breakpoints.js');
29
- const bps = parseBreakpoints(process.argv);
30
- const plan = bps.length ? bps : [undefined];
31
- if (options.hasse) {
32
- const name = options.hasse;
33
- const bundle = (options as any).bundle;
34
- const fmt = (options.format === 'json' ? 'mermaid' : options.format) as 'mermaid' | 'dot' | 'svg' | 'png';
35
- const imageFrom = options.imageFrom || 'mermaid';
36
- const filterPrefixes = options.filterPrefix ? options.filterPrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
37
- const excludePrefixes = options.excludePrefix ? options.excludePrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
38
- const onlyViolations = options.onlyViolations || false;
39
- const highlightViolations = options.highlightViolations || false;
40
- const violationColor = options.violationColor || '#ff2d55';
41
- const labelViolations = options.labelViolations || false;
42
- const labelTruncate = Math.max(0, options.labelTruncate || 0);
43
- const minSeverity = options.minSeverity || 'warn';
44
- const focus = options.focus; const radius = Math.max(0, options.radius || 1);
45
- for (const breakpoint of plan) {
46
- const suffixParts: string[] = [];
47
- if (breakpoint) suffixParts.push(breakpoint);
48
- if (filterPrefixes.length) suffixParts.push(filterPrefixes.join('_'));
49
- if (excludePrefixes.length) suffixParts.push('not-' + excludePrefixes.join('_'));
50
- if (focus) suffixParts.push(`focus-${focus.replace(/[^\w.*-]/g,'_')}-r${radius}`);
51
- if (onlyViolations) suffixParts.push('violations'); else if (highlightViolations) suffixParts.push('highlight-violations');
52
- if (labelViolations) suffixParts.push('labeled');
53
- const suffix = suffixParts.length ? '-' + suffixParts.map(s=>s.replace(/[^\w.-]/g,'_')).join('__') : '';
54
- const baseFmt = fmt === 'svg' || fmt === 'png' ? (imageFrom === 'dot' ? 'dot' : 'mermaid') : fmt;
55
- const ext = baseFmt === 'mermaid' ? 'mmd' : 'dot';
56
- const outDir = 'dist/graphs'; const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
57
- try {
58
- const src = `themes/${name}.order.json`;
59
- if (!existsSync(src)) { console.error(`❌ Order constraint file not found: ${src}`); process.exit(1); }
60
- const { order } = JSON.parse(readFileSync(src, 'utf8'));
61
- const { buildPoset, transitiveReduction, toMermaidHasseStyled, toDotHasseStyled, filterByPrefix, filterExcludePrefix, khopSubgraph, pickSeedsByPattern } = await import('../../core/poset.js');
62
- let g = buildPoset(order);
63
- if (filterPrefixes.length) g = filterByPrefix(g, filterPrefixes);
64
- if (excludePrefixes.length) g = filterExcludePrefix(g, excludePrefixes);
65
- let h = transitiveReduction(g);
66
- 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); }
67
- let highlight: { nodes: Set<string>; edges: Set<string>; color?: string } | undefined; let edgeLabels: Map<string,string> | undefined;
68
- if (onlyViolations || highlightViolations || labelViolations) {
69
- const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
70
- const tokens = loadTokensWithBreakpoint(breakpoint); const { flattenTokens } = await import('../../core/flatten.js');
71
- const { Engine } = await import('../../core/engine.js');
72
- const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
73
- const { MonotonicLightness } = await import('../../core/constraints/monotonic-lightness.js');
74
- const { ThresholdPlugin } = await import('../../core/constraints/threshold.js');
75
- const { flat, edges } = flattenTokens(tokens); const init: Record<string,string|number> = {}; Object.values(flat).forEach(t => { const ft = t as FlatToken; init[ft.id] = ft.value; });
76
- const engine = new Engine(init, edges);
77
- const allIdsInHasse = new Set<string>([...h.keys(), ...Array.from(h.values()).flatMap(s=>[...s])]);
78
- let issues: any[] = [];
79
- if (name === 'color') {
80
- const colorOrders = order as [string, '<='|'>=', string][]; issues = MonotonicLightness(colorOrders).evaluate(engine, allIdsInHasse);
81
- } else {
82
- const numericOrders = order as [string, '<='|'>=', string][]; issues = MonotonicPlugin(numericOrders, parseSize, 'monotonic').evaluate(engine, allIdsInHasse);
83
- }
84
- const thresholdIssues = ThresholdPlugin([{ id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' }]).evaluate(engine, allIdsInHasse); issues.push(...thresholdIssues);
85
- const severityRank = { error: 2, warn: 1 } as const;
86
- const filteredIssues = issues.filter(it => { const level = (it.level as 'warn'|'error') || 'error'; return severityRank[level] >= severityRank[minSeverity]; });
87
- const edgeViol = new Set<string>(); const nodeViol = new Set<string>(); if (labelViolations) edgeLabels = new Map<string,string>();
88
- for (const it of filteredIssues) {
89
- if (typeof it.id === 'string' && it.id.includes('|')) { const [a,b] = it.id.split('|'); edgeViol.add(`${a}|${b}`); nodeViol.add(a); nodeViol.add(b); if (labelViolations && edgeLabels) { let label = it.message ?? 'violation'; if (labelTruncate > 0 && label.length > labelTruncate) label = label.slice(0, labelTruncate - 1) + '…'; edgeLabels.set(`${a}|${b}`, label); } }
90
- else if (typeof it.id === 'string') nodeViol.add(it.id);
91
- }
92
- highlight = { nodes: nodeViol, edges: edgeViol, color: violationColor };
93
- if (onlyViolations) {
94
- const pruned: Map<string, Set<string>> = new Map();
95
- for (const [u, vs] of h) {
96
- for (const v of vs) {
97
- if (edgeViol.has(`${u}|${v}`) || nodeViol.has(u) || nodeViol.has(v)) { if (!pruned.has(u)) pruned.set(u, new Set()); pruned.get(u)!.add(v); if (!pruned.has(v)) pruned.set(v, new Set()); }
98
- }
99
- }
100
- h = pruned;
101
- }
102
- }
103
- const bpLabel = breakpoint ? ` @${breakpoint}` : '';
104
- const title = `${name}${suffix ? ' ' + suffix : ''}${bpLabel} (Hasse)`;
105
- const mermaidContent = toMermaidHasseStyled(h, { title, highlight, labels: edgeLabels });
106
- const dotContent = toDotHasseStyled(h, { title, highlight, labels: edgeLabels });
107
- mkdirSync(outDir, { recursive: true });
108
- if (bundle) {
109
- const mFile = baseFile.replace(/\.dot$/,'-bundle.mmd').replace(/\.mmd$/,'-bundle.mmd');
110
- const dFile = baseFile.replace(/\.mmd$/,'-bundle.dot').replace(/\.dot$/,'-bundle.dot');
111
- writeFileSync(mFile, mermaidContent);
112
- writeFileSync(dFile, dotContent);
113
- }
114
- const baseContent = baseFmt === 'mermaid' ? mermaidContent : dotContent;
115
- writeFileSync(baseFile, baseContent);
116
- if (fmt === 'svg' || fmt === 'png') {
117
- const imgFile = `${outDir}/${name}${suffix}-hasse.${fmt}`;
118
- const { ok, hint } = exportGraphImage(baseFile, imgFile, fmt, imageFrom);
119
- if (ok) console.log(`✓ Wrote ${baseFile} and ${imgFile}`); else console.log(`✓ Wrote ${baseFile} (image export skipped). ${hint}`);
120
- } else {
121
- const hasViolations = highlight && (highlight.edges.size > 0 || highlight.nodes.size > 0);
122
- const message = (onlyViolations || highlightViolations) && !hasViolations ? `✓ Wrote ${baseFile} (no violations in slice)` : `✓ Wrote ${baseFile}`;
123
- console.log(message);
124
- }
125
- } catch (error) { console.error(`❌ Error generating Hasse diagram for ${breakpoint}:`, error); process.exit(1); }
126
- }
127
- return; }
128
- for (const breakpoint of plan) {
129
- const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
130
- const tokens = loadTokensWithBreakpoint(breakpoint); const { edges } = flattenTokens(tokens);
131
- let filteredEdges = edges;
132
- if (options.filter) { const filterRegex = new RegExp(options.filter); filteredEdges = edges.filter(([from,to]) => filterRegex.test(from) || filterRegex.test(to)); }
133
- const format = options.format || 'json'; const graph = generateDependencyGraph(filteredEdges, format);
134
- 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}`); }
135
- else { if (breakpoint) console.log(`\n=== ${breakpoint.toUpperCase()} ===`); console.log(graph); }
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
+ }
@@ -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
+ }