design-constraint-validator 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +229 -659
  3. package/adapters/README.md +46 -46
  4. package/adapters/css.ts +116 -116
  5. package/adapters/decisionthemes.d.ts +44 -0
  6. package/adapters/decisionthemes.d.ts.map +1 -0
  7. package/adapters/decisionthemes.js +35 -0
  8. package/adapters/decisionthemes.ts +59 -0
  9. package/adapters/js.ts +14 -14
  10. package/adapters/json.ts +45 -45
  11. package/cli/build-css.ts +32 -32
  12. package/cli/commands/build.ts +65 -65
  13. package/cli/commands/graph.d.ts.map +1 -1
  14. package/cli/commands/graph.js +26 -10
  15. package/cli/commands/graph.ts +180 -137
  16. package/cli/commands/index.ts +7 -7
  17. package/cli/commands/patch-apply.ts +80 -80
  18. package/cli/commands/patch.ts +22 -22
  19. package/cli/commands/set.d.ts.map +1 -1
  20. package/cli/commands/set.js +12 -4
  21. package/cli/commands/set.ts +239 -225
  22. package/cli/commands/utils.ts +50 -50
  23. package/cli/commands/validate.d.ts.map +1 -1
  24. package/cli/commands/validate.js +89 -33
  25. package/cli/commands/validate.ts +180 -115
  26. package/cli/commands/why.d.ts.map +1 -1
  27. package/cli/commands/why.js +86 -20
  28. package/cli/commands/why.ts +158 -46
  29. package/cli/config-schema.ts +27 -27
  30. package/cli/config.ts +35 -35
  31. package/cli/constraint-registry.d.ts +101 -0
  32. package/cli/constraint-registry.d.ts.map +1 -0
  33. package/cli/constraint-registry.js +225 -0
  34. package/cli/constraint-registry.ts +304 -0
  35. package/cli/constraints-loader.d.ts.map +1 -0
  36. package/cli/cross-axis-loader.d.ts +91 -0
  37. package/cli/cross-axis-loader.d.ts.map +1 -0
  38. package/cli/cross-axis-loader.js +222 -0
  39. package/cli/cross-axis-loader.ts +289 -0
  40. package/cli/dcv.js +4 -0
  41. package/cli/dcv.ts +111 -107
  42. package/cli/engine-helpers.d.ts.map +1 -1
  43. package/cli/graph-poset.ts +74 -74
  44. package/cli/json-output.d.ts +69 -0
  45. package/cli/json-output.d.ts.map +1 -0
  46. package/cli/json-output.js +109 -0
  47. package/cli/json-output.ts +184 -0
  48. package/cli/result.ts +27 -27
  49. package/cli/run.ts +54 -54
  50. package/cli/smoke-test.ts +40 -40
  51. package/cli/types.d.ts +6 -0
  52. package/cli/types.d.ts.map +1 -1
  53. package/cli/types.ts +84 -78
  54. package/cli/version-banner.d.ts +20 -0
  55. package/cli/version-banner.d.ts.map +1 -0
  56. package/cli/version-banner.js +49 -0
  57. package/cli/version-banner.ts +61 -0
  58. package/core/breakpoints.ts +50 -50
  59. package/core/cli-format.ts +31 -31
  60. package/core/color.ts +148 -148
  61. package/core/constraints/cross-axis.ts +114 -114
  62. package/core/constraints/monotonic-lightness.ts +38 -38
  63. package/core/constraints/monotonic.ts +74 -74
  64. package/core/constraints/threshold.ts +43 -43
  65. package/core/constraints/wcag.ts +70 -70
  66. package/core/cross-axis-config.d.ts.map +1 -1
  67. package/core/engine.d.ts +95 -0
  68. package/core/engine.d.ts.map +1 -1
  69. package/core/engine.js +22 -0
  70. package/core/engine.ts +167 -65
  71. package/core/flatten.ts +116 -116
  72. package/core/image-export.ts +48 -48
  73. package/core/index.d.ts +9 -30
  74. package/core/index.d.ts.map +1 -1
  75. package/core/index.js +7 -54
  76. package/core/index.ts +10 -72
  77. package/core/patch.ts +134 -134
  78. package/core/poset.ts +311 -311
  79. package/core/why.ts +63 -63
  80. package/package.json +96 -90
  81. package/themes/color.lg.order.json +15 -15
  82. package/themes/color.md.order.json +15 -15
  83. package/themes/color.order.json +15 -15
  84. package/themes/color.sm.order.json +15 -15
  85. package/themes/cross-axis.rules.json +35 -35
  86. package/themes/cross-axis.sm.rules.json +12 -12
  87. package/themes/layout.lg.order.json +18 -18
  88. package/themes/layout.md.order.json +18 -18
  89. package/themes/layout.order.json +18 -18
  90. package/themes/layout.sm.order.json +18 -18
  91. package/themes/spacing.order.json +14 -14
  92. package/themes/typography.lg.order.json +15 -15
  93. package/themes/typography.md.order.json +15 -15
  94. package/themes/typography.order.json +15 -15
  95. package/themes/typography.sm.order.json +15 -15
  96. package/cli/engine-helpers.d.ts +0 -8
  97. package/cli/engine-helpers.js +0 -70
  98. package/cli/engine-helpers.ts +0 -61
  99. package/core/cross-axis-config.d.ts +0 -5
  100. package/core/cross-axis-config.js +0 -144
  101. package/core/cross-axis-config.ts +0 -152
  102. package/dist/test-overrides-removal.json +0 -4
  103. package/dist/tmp.patch.json +0 -35
  104. package/tokens/overrides/base.json +0 -22
  105. package/tokens/overrides/lg.json +0 -20
  106. package/tokens/overrides/md.json +0 -16
  107. package/tokens/overrides/sm.json +0 -16
  108. package/tokens/overrides/viol.color.json +0 -6
  109. package/tokens/overrides/viol.typography.json +0 -6
  110. package/tokens/tokens.demo-violations.json +0 -116
  111. package/tokens/tokens.example.json +0 -128
  112. package/tokens/tokens.json +0 -67
  113. package/tokens/tokens.multi-violations.json +0 -21
  114. package/tokens/tokens.schema.d.ts +0 -2298
  115. package/tokens/tokens.schema.d.ts.map +0 -1
  116. package/tokens/tokens.schema.js +0 -148
  117. package/tokens/tokens.schema.ts +0 -196
  118. package/tokens/tokens.test.json +0 -38
  119. package/tokens/tokens.touch-violation.json +0 -8
  120. package/tokens/typography.classes.css +0 -11
  121. package/tokens/typography.css +0 -20
package/core/engine.ts CHANGED
@@ -1,65 +1,167 @@
1
- import type { TokenId, TokenValue } from "./flatten.js";
2
-
3
- export type ConstraintIssue = {
4
- id: TokenId | string;
5
- rule: string; // e.g., "wcag-contrast"
6
- level: "error" | "warn";
7
- message: string;
8
- where?: string; // optional hint (e.g., "body text on surface")
9
- };
10
-
11
- export type ConstraintPlugin = {
12
- id: string;
13
- // Called with the set of candidate IDs (changed + affected).
14
- evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
15
- };
16
-
17
- export type Graph = Map<TokenId, Set<TokenId>>; // id -> dependents
18
-
19
- export class Engine {
20
- private values = new Map<TokenId, TokenValue>();
21
- private graph: Graph = new Map();
22
- private plugins: ConstraintPlugin[] = [];
23
-
24
- constructor(initValues: Record<TokenId, TokenValue>, edges: Array<[TokenId, TokenId]>) {
25
- for (const [k, v] of Object.entries(initValues)) this.values.set(k, v);
26
- for (const [from, to] of edges) {
27
- if (!this.graph.has(from)) this.graph.set(from, new Set());
28
- this.graph.get(from)!.add(to);
29
- if (!this.graph.has(to)) this.graph.set(to, new Set());
30
- }
31
- }
32
-
33
- use(plugin: ConstraintPlugin) { this.plugins.push(plugin); return this; }
34
-
35
- get(id: TokenId): TokenValue | undefined { return this.values.get(id); }
36
- set(id: TokenId, value: TokenValue) { this.values.set(id, value); }
37
-
38
- /** All nodes that depend (directly/indirectly) on start. */
39
- affected(start: TokenId): Set<TokenId> {
40
- const seen = new Set<TokenId>();
41
- const stack = [start];
42
- while (stack.length) {
43
- const n = stack.pop()!;
44
- if (seen.has(n)) continue;
45
- seen.add(n);
46
- for (const d of this.graph.get(n) ?? []) stack.push(d);
47
- }
48
- seen.delete(start);
49
- return seen;
50
- }
51
-
52
- evaluate(candidates: Set<TokenId>) {
53
- return this.plugins.flatMap(p => p.evaluate(this, candidates));
54
- }
55
-
56
- /** Single change -> returns affected set, issues, and a patch you can feed to adapters. */
57
- commit(id: TokenId, value: TokenValue) {
58
- this.set(id, value);
59
- const A = this.affected(id);
60
- const candidates = new Set<TokenId>([id, ...A]);
61
- const issues = this.evaluate(candidates);
62
- const patch: Record<TokenId, TokenValue> = { [id]: value };
63
- return { affected: [...A], issues, patch };
64
- }
65
- }
1
+ import type { TokenId, TokenValue } from "./flatten.js";
2
+
3
+ /**
4
+ * Represents a constraint violation.
5
+ *
6
+ * Phase 3C: Enhanced with metadata for tooling and visualization.
7
+ */
8
+ export type ConstraintIssue = {
9
+ id: TokenId | string;
10
+ rule: string; // e.g., "wcag-contrast"
11
+ level: "error" | "warn";
12
+ message: string;
13
+ where?: string; // optional hint (e.g., "body text on surface")
14
+
15
+ /**
16
+ * Token IDs involved in this violation.
17
+ * Useful for filtering, highlighting, and incremental validation.
18
+ *
19
+ * Example: For a WCAG contrast violation between fg and bg tokens,
20
+ * this would be [fgTokenId, bgTokenId].
21
+ */
22
+ involvedTokens?: TokenId[];
23
+
24
+ /**
25
+ * Graph edges (reference relationships) involved in this violation.
26
+ * Useful for visualization and "why" explanations.
27
+ *
28
+ * Example: If a token references another that violates a constraint,
29
+ * this captures that reference edge.
30
+ */
31
+ involvedEdges?: Array<[TokenId, TokenId]>;
32
+ };
33
+
34
+ /**
35
+ * Constraint plugin interface.
36
+ *
37
+ * Phase 3C: Documented contract for candidate-based evaluation.
38
+ *
39
+ * ## Candidate Contract
40
+ *
41
+ * Plugins MUST honor the `candidates` set for incremental validation:
42
+ * - Only evaluate constraints that involve at least one candidate token
43
+ * - Return violations where at least one involved token is in candidates
44
+ * - This enables efficient re-validation when tokens change
45
+ *
46
+ * ## Metadata Contract
47
+ *
48
+ * Plugins SHOULD populate `involvedTokens` in returned issues:
49
+ * - List all token IDs that participate in the constraint
50
+ * - This enables filtering, highlighting, and graph visualization
51
+ * - Optional but recommended for better tooling support
52
+ *
53
+ * ## Example Implementation
54
+ *
55
+ * ```ts
56
+ * export function MyPlugin(rules: Rule[]): ConstraintPlugin {
57
+ * return {
58
+ * id: "my-plugin",
59
+ * evaluate(engine, candidates) {
60
+ * const issues: ConstraintIssue[] = [];
61
+ * for (const rule of rules) {
62
+ * // Honor candidates: skip if no involved tokens are candidates
63
+ * if (!candidates.has(rule.tokenA) && !candidates.has(rule.tokenB)) {
64
+ * continue;
65
+ * }
66
+ * // Check constraint...
67
+ * if (violated) {
68
+ * issues.push({
69
+ * id: `${rule.tokenA}|${rule.tokenB}`,
70
+ * rule: "my-plugin",
71
+ * level: "error",
72
+ * message: "...",
73
+ * involvedTokens: [rule.tokenA, rule.tokenB], // Metadata
74
+ * });
75
+ * }
76
+ * }
77
+ * return issues;
78
+ * }
79
+ * };
80
+ * }
81
+ * ```
82
+ */
83
+ export type ConstraintPlugin = {
84
+ id: string;
85
+ /**
86
+ * Evaluate constraints for a set of candidate tokens.
87
+ *
88
+ * @param engine Engine instance providing token values and graph
89
+ * @param candidates Set of token IDs to evaluate (changed + affected)
90
+ * @returns Array of constraint violations
91
+ */
92
+ evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
93
+ };
94
+
95
+ export type Graph = Map<TokenId, Set<TokenId>>; // id -> dependents
96
+
97
+ export class Engine {
98
+ private values = new Map<TokenId, TokenValue>();
99
+ private graph: Graph = new Map();
100
+ private plugins: ConstraintPlugin[] = [];
101
+
102
+ constructor(initValues: Record<TokenId, TokenValue>, edges: Array<[TokenId, TokenId]>) {
103
+ for (const [k, v] of Object.entries(initValues)) this.values.set(k, v);
104
+ for (const [from, to] of edges) {
105
+ if (!this.graph.has(from)) this.graph.set(from, new Set());
106
+ this.graph.get(from)!.add(to);
107
+ if (!this.graph.has(to)) this.graph.set(to, new Set());
108
+ }
109
+ }
110
+
111
+ use(plugin: ConstraintPlugin) { this.plugins.push(plugin); return this; }
112
+
113
+ get(id: TokenId): TokenValue | undefined { return this.values.get(id); }
114
+ set(id: TokenId, value: TokenValue) { this.values.set(id, value); }
115
+
116
+ /**
117
+ * Get all token IDs in the engine.
118
+ *
119
+ * Phase 3C: Exposed for plugins and adapters.
120
+ * Useful for iterating all tokens or creating a full candidate set.
121
+ *
122
+ * @returns Array of all token IDs
123
+ */
124
+ getAllIds(): TokenId[] {
125
+ return Array.from(this.values.keys());
126
+ }
127
+
128
+ /**
129
+ * Get flat token map (ID → value).
130
+ *
131
+ * Phase 3C: Exposed to avoid duplicate flattening in CLI/adapters.
132
+ * Returns a plain object suitable for serialization or adapter use.
133
+ *
134
+ * @returns Record mapping token IDs to their current values
135
+ */
136
+ getFlatTokens(): Record<TokenId, TokenValue> {
137
+ return Object.fromEntries(this.values);
138
+ }
139
+
140
+ /** All nodes that depend (directly/indirectly) on start. */
141
+ affected(start: TokenId): Set<TokenId> {
142
+ const seen = new Set<TokenId>();
143
+ const stack = [start];
144
+ while (stack.length) {
145
+ const n = stack.pop()!;
146
+ if (seen.has(n)) continue;
147
+ seen.add(n);
148
+ for (const d of this.graph.get(n) ?? []) stack.push(d);
149
+ }
150
+ seen.delete(start);
151
+ return seen;
152
+ }
153
+
154
+ evaluate(candidates: Set<TokenId>) {
155
+ return this.plugins.flatMap(p => p.evaluate(this, candidates));
156
+ }
157
+
158
+ /** Single change -> returns affected set, issues, and a patch you can feed to adapters. */
159
+ commit(id: TokenId, value: TokenValue) {
160
+ this.set(id, value);
161
+ const A = this.affected(id);
162
+ const candidates = new Set<TokenId>([id, ...A]);
163
+ const issues = this.evaluate(candidates);
164
+ const patch: Record<TokenId, TokenValue> = { [id]: value };
165
+ return { affected: [...A], issues, patch };
166
+ }
167
+ }
package/core/flatten.ts CHANGED
@@ -1,116 +1,116 @@
1
- export type TokenId = string; // e.g. "color.palette.brand.600"
2
- export type TokenValue = string | number;
3
- export type TokenNode = {
4
- $type?: string;
5
- $value?: TokenValue;
6
- [k: string]: TokenNode | string | number | undefined;
7
- };
8
-
9
- export type FlatToken = {
10
- id: TokenId;
11
- type: string;
12
- value: TokenValue; // resolved (if ref)
13
- raw: TokenValue; // original $value
14
- refs: TokenId[]; // referenced token IDs found in raw
15
- };
16
-
17
- export type FlattenResult = {
18
- flat: Record<TokenId, FlatToken>;
19
- edges: Array<[from: TokenId, to: TokenId]>; // from ref -> to dependent
20
- };
21
-
22
- const REF_RE = /\{([a-z0-9.-]+)\}/gi;
23
-
24
- export function flattenTokens(root: TokenNode): FlattenResult {
25
- const flat: Record<TokenId, FlatToken> = {};
26
- const edges: Array<[TokenId, TokenId]> = [];
27
-
28
- // First pass: collect all tokens
29
- function walk(node: TokenNode, path: string[] = []) {
30
- if (!node || typeof node !== 'object') return;
31
-
32
- if (Object.prototype.hasOwnProperty.call(node, '$value')) {
33
- const id = path.join('.');
34
- const raw = node.$value;
35
- if (raw === undefined) return; // Skip tokens without values
36
-
37
- const refs: TokenId[] = [];
38
-
39
- // Find all references in the value
40
- if (typeof raw === 'string') {
41
- const matches = raw.matchAll(REF_RE);
42
- for (const match of matches) {
43
- refs.push(match[1]);
44
- }
45
- }
46
-
47
- flat[id] = {
48
- id,
49
- type: String(node.$type ?? 'unknown'),
50
- value: raw,
51
- raw,
52
- refs
53
- };
54
-
55
- // Add edges for dependencies
56
- refs.forEach(refId => edges.push([refId, id]));
57
- return;
58
- }
59
-
60
- // Recursively walk children
61
- for (const key of Object.keys(node)) {
62
- if (key.startsWith('$')) continue;
63
- const child = node[key];
64
- if (typeof child === 'object' && child !== null) {
65
- walk(child as TokenNode, path.concat(key));
66
- }
67
- }
68
- }
69
-
70
- walk(root);
71
-
72
- // Second pass: resolve references iteratively
73
- let changed = true;
74
- let iterations = 0;
75
- const maxIterations = Object.keys(flat).length * 2; // Safety limit
76
-
77
- while (changed && iterations < maxIterations) {
78
- changed = false;
79
- iterations++;
80
-
81
- for (const token of Object.values(flat)) {
82
- if (typeof token.value === 'string' && token.value.includes('{')) {
83
- let newValue = token.value;
84
- let fullyResolved = true;
85
-
86
- for (const refId of token.refs) {
87
- const refToken = flat[refId];
88
- if (!refToken) {
89
- throw new Error(`Could not resolve token ${refId}`);
90
- }
91
-
92
- // If the referenced token still has unresolved refs, skip this iteration
93
- if (typeof refToken.value === 'string' && refToken.value.includes('{')) {
94
- fullyResolved = false;
95
- break;
96
- }
97
-
98
- // Replace the reference with the resolved value
99
- const refPattern = new RegExp(`\\{${refId}\\}`, 'g');
100
- newValue = newValue.replace(refPattern, String(refToken.value));
101
- }
102
-
103
- if (fullyResolved && newValue !== token.value) {
104
- token.value = newValue;
105
- changed = true;
106
- }
107
- }
108
- }
109
- }
110
-
111
- if (iterations >= maxIterations) {
112
- throw new Error('Token resolution exceeded maximum iterations - possible circular reference');
113
- }
114
-
115
- return { flat, edges };
116
- }
1
+ export type TokenId = string; // e.g. "color.palette.brand.600"
2
+ export type TokenValue = string | number;
3
+ export type TokenNode = {
4
+ $type?: string;
5
+ $value?: TokenValue;
6
+ [k: string]: TokenNode | string | number | undefined;
7
+ };
8
+
9
+ export type FlatToken = {
10
+ id: TokenId;
11
+ type: string;
12
+ value: TokenValue; // resolved (if ref)
13
+ raw: TokenValue; // original $value
14
+ refs: TokenId[]; // referenced token IDs found in raw
15
+ };
16
+
17
+ export type FlattenResult = {
18
+ flat: Record<TokenId, FlatToken>;
19
+ edges: Array<[from: TokenId, to: TokenId]>; // from ref -> to dependent
20
+ };
21
+
22
+ const REF_RE = /\{([a-z0-9.-]+)\}/gi;
23
+
24
+ export function flattenTokens(root: TokenNode): FlattenResult {
25
+ const flat: Record<TokenId, FlatToken> = {};
26
+ const edges: Array<[TokenId, TokenId]> = [];
27
+
28
+ // First pass: collect all tokens
29
+ function walk(node: TokenNode, path: string[] = []) {
30
+ if (!node || typeof node !== 'object') return;
31
+
32
+ if (Object.prototype.hasOwnProperty.call(node, '$value')) {
33
+ const id = path.join('.');
34
+ const raw = node.$value;
35
+ if (raw === undefined) return; // Skip tokens without values
36
+
37
+ const refs: TokenId[] = [];
38
+
39
+ // Find all references in the value
40
+ if (typeof raw === 'string') {
41
+ const matches = raw.matchAll(REF_RE);
42
+ for (const match of matches) {
43
+ refs.push(match[1]);
44
+ }
45
+ }
46
+
47
+ flat[id] = {
48
+ id,
49
+ type: String(node.$type ?? 'unknown'),
50
+ value: raw,
51
+ raw,
52
+ refs
53
+ };
54
+
55
+ // Add edges for dependencies
56
+ refs.forEach(refId => edges.push([refId, id]));
57
+ return;
58
+ }
59
+
60
+ // Recursively walk children
61
+ for (const key of Object.keys(node)) {
62
+ if (key.startsWith('$')) continue;
63
+ const child = node[key];
64
+ if (typeof child === 'object' && child !== null) {
65
+ walk(child as TokenNode, path.concat(key));
66
+ }
67
+ }
68
+ }
69
+
70
+ walk(root);
71
+
72
+ // Second pass: resolve references iteratively
73
+ let changed = true;
74
+ let iterations = 0;
75
+ const maxIterations = Object.keys(flat).length * 2; // Safety limit
76
+
77
+ while (changed && iterations < maxIterations) {
78
+ changed = false;
79
+ iterations++;
80
+
81
+ for (const token of Object.values(flat)) {
82
+ if (typeof token.value === 'string' && token.value.includes('{')) {
83
+ let newValue = token.value;
84
+ let fullyResolved = true;
85
+
86
+ for (const refId of token.refs) {
87
+ const refToken = flat[refId];
88
+ if (!refToken) {
89
+ throw new Error(`Could not resolve token ${refId}`);
90
+ }
91
+
92
+ // If the referenced token still has unresolved refs, skip this iteration
93
+ if (typeof refToken.value === 'string' && refToken.value.includes('{')) {
94
+ fullyResolved = false;
95
+ break;
96
+ }
97
+
98
+ // Replace the reference with the resolved value
99
+ const refPattern = new RegExp(`\\{${refId}\\}`, 'g');
100
+ newValue = newValue.replace(refPattern, String(refToken.value));
101
+ }
102
+
103
+ if (fullyResolved && newValue !== token.value) {
104
+ token.value = newValue;
105
+ changed = true;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ if (iterations >= maxIterations) {
112
+ throw new Error('Token resolution exceeded maximum iterations - possible circular reference');
113
+ }
114
+
115
+ return { flat, edges };
116
+ }
@@ -1,48 +1,48 @@
1
- // core/image-export.ts
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
-
6
- function which(cmd: string): string | null {
7
- const paths = (process.env.PATH || "").split(path.delimiter);
8
- for (const p of paths) {
9
- const full = path.join(p, cmd + (process.platform === "win32" ? ".cmd" : ""));
10
- try {
11
- fs.accessSync(full, fs.constants.X_OK);
12
- return full;
13
- } catch {
14
- // File not accessible, continue to next path
15
- }
16
- }
17
- return null;
18
- }
19
-
20
- export type ImageFmt = "svg" | "png";
21
- export type Renderer = "mermaid" | "dot";
22
-
23
- export function exportGraphImage(
24
- inputPath: string, // .mmd or .dot we just wrote
25
- outPath: string, // .svg or .png to write
26
- fmt: ImageFmt, // "svg" | "png"
27
- renderer: Renderer // "mermaid" | "dot"
28
- ): { ok: boolean; hint?: string } {
29
- if (renderer === "mermaid") {
30
- const mmdc = which("mmdc");
31
- if (!mmdc) {
32
- return { ok: false, hint: "Install @mermaid-js/mermaid-cli (mmdc) to render images. Fallback written: .mmd" };
33
- }
34
- const args = ["-i", inputPath, "-o", outPath, "-t", "default", "-b", "transparent"];
35
- if (fmt === "png") args.push("-p", "puppeteer-config.json"); // optional, if you keep one
36
- const res = spawnSync(mmdc, args, { stdio: "inherit" });
37
- return { ok: res.status === 0, hint: res.status !== 0 ? "mmdc failed; see logs." : undefined };
38
- }
39
-
40
- // renderer === "dot"
41
- const dot = which("dot");
42
- if (!dot) {
43
- return { ok: false, hint: "Install Graphviz (dot) to render images. Fallback written: .dot" };
44
- }
45
- const argFmt = fmt === "svg" ? "-Tsvg" : "-Tpng";
46
- const res = spawnSync(dot, [argFmt, inputPath, "-o", outPath], { stdio: "inherit" });
47
- return { ok: res.status === 0, hint: res.status !== 0 ? "dot failed; see logs." : undefined };
48
- }
1
+ // core/image-export.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ function which(cmd: string): string | null {
7
+ const paths = (process.env.PATH || "").split(path.delimiter);
8
+ for (const p of paths) {
9
+ const full = path.join(p, cmd + (process.platform === "win32" ? ".cmd" : ""));
10
+ try {
11
+ fs.accessSync(full, fs.constants.X_OK);
12
+ return full;
13
+ } catch {
14
+ // File not accessible, continue to next path
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export type ImageFmt = "svg" | "png";
21
+ export type Renderer = "mermaid" | "dot";
22
+
23
+ export function exportGraphImage(
24
+ inputPath: string, // .mmd or .dot we just wrote
25
+ outPath: string, // .svg or .png to write
26
+ fmt: ImageFmt, // "svg" | "png"
27
+ renderer: Renderer // "mermaid" | "dot"
28
+ ): { ok: boolean; hint?: string } {
29
+ if (renderer === "mermaid") {
30
+ const mmdc = which("mmdc");
31
+ if (!mmdc) {
32
+ return { ok: false, hint: "Install @mermaid-js/mermaid-cli (mmdc) to render images. Fallback written: .mmd" };
33
+ }
34
+ const args = ["-i", inputPath, "-o", outPath, "-t", "default", "-b", "transparent"];
35
+ if (fmt === "png") args.push("-p", "puppeteer-config.json"); // optional, if you keep one
36
+ const res = spawnSync(mmdc, args, { stdio: "inherit" });
37
+ return { ok: res.status === 0, hint: res.status !== 0 ? "mmdc failed; see logs." : undefined };
38
+ }
39
+
40
+ // renderer === "dot"
41
+ const dot = which("dot");
42
+ if (!dot) {
43
+ return { ok: false, hint: "Install Graphviz (dot) to render images. Fallback written: .dot" };
44
+ }
45
+ const argFmt = fmt === "svg" ? "-Tsvg" : "-Tpng";
46
+ const res = spawnSync(dot, [argFmt, inputPath, "-o", outPath], { stdio: "inherit" });
47
+ return { ok: res.status === 0, hint: res.status !== 0 ? "dot failed; see logs." : undefined };
48
+ }
package/core/index.d.ts CHANGED
@@ -1,31 +1,10 @@
1
- export type TokenId = string;
2
- export type TokenValue = string | number;
3
- export type Graph = Map<TokenId, Set<TokenId>>;
4
- export type ConstraintIssue = {
5
- id: TokenId;
6
- rule: string;
7
- level: "error" | "warn";
8
- message: string;
9
- };
10
- export type ConstraintPlugin = {
11
- id: string;
12
- evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
13
- };
14
- export declare class Engine {
15
- private values;
16
- private graph;
17
- private plugins;
18
- constructor(initValues: Record<TokenId, TokenValue>, edges: Array<[TokenId, TokenId]>);
19
- set(id: TokenId, value: TokenValue): void;
20
- get(id: TokenId): TokenValue | undefined;
21
- use(plugin: ConstraintPlugin): this;
22
- affected(start: TokenId): Set<TokenId>;
23
- evaluate(ids: Iterable<TokenId>): ConstraintIssue[];
24
- /** Apply a single change and return a batch: affected set + issues + patch */
25
- commit(id: TokenId, value: TokenValue): {
26
- affected: string[];
27
- issues: ConstraintIssue[];
28
- patch: Record<string, TokenValue>;
29
- };
30
- }
1
+ /**
2
+ * Public API exports for Design Constraint Validator core.
3
+ *
4
+ * This file re-exports the main types and classes from the core engine.
5
+ * See engine.ts for full implementation with Phase 3C enhancements.
6
+ */
7
+ export type { TokenId, TokenValue } from "./flatten.js";
8
+ export type { ConstraintIssue, ConstraintPlugin, Graph } from "./engine.js";
9
+ export { Engine } from "./engine.js";
31
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AACzC,MAAM,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,eAAe,EAAE,CAAC;CACvE,CAAC;AAEF,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,OAAO,CAA0B;gBAE7B,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IASrF,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;IAClC,GAAG,CAAC,EAAE,EAAE,OAAO;IAEf,GAAG,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAKnC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAYtC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,eAAe,EAAE;IAYnD,8EAA8E;IAC9E,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;;;;;CAQtC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}