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.
- package/LICENSE +21 -21
- package/README.md +229 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/decisionthemes.d.ts +44 -0
- package/adapters/decisionthemes.d.ts.map +1 -0
- package/adapters/decisionthemes.js +35 -0
- package/adapters/decisionthemes.ts +59 -0
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +89 -33
- package/cli/commands/validate.ts +180 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +69 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +109 -0
- package/cli/json-output.ts +184 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/cli/version-banner.d.ts +20 -0
- package/cli/version-banner.d.ts.map +1 -0
- package/cli/version-banner.js +49 -0
- package/cli/version-banner.ts +61 -0
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/cli/engine-helpers.d.ts +0 -8
- package/cli/engine-helpers.js +0 -70
- package/cli/engine-helpers.ts +0 -61
- package/core/cross-axis-config.d.ts +0 -5
- package/core/cross-axis-config.js +0 -144
- package/core/cross-axis-config.ts +0 -152
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/core/engine.ts
CHANGED
|
@@ -1,65 +1,167 @@
|
|
|
1
|
-
import type { TokenId, TokenValue } from "./flatten.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
}
|
package/core/image-export.ts
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
package/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|