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.
- package/LICENSE +21 -0
- package/README.md +659 -0
- package/adapters/README.md +46 -0
- package/adapters/css.d.ts +44 -0
- package/adapters/css.d.ts.map +1 -0
- package/adapters/css.js +97 -0
- package/adapters/css.ts +116 -0
- package/adapters/js.d.ts +3 -0
- package/adapters/js.d.ts.map +1 -0
- package/adapters/js.js +15 -0
- package/adapters/js.ts +14 -0
- package/adapters/json.d.ts +18 -0
- package/adapters/json.d.ts.map +1 -0
- package/adapters/json.js +35 -0
- package/adapters/json.ts +45 -0
- package/cli/build-css.d.ts +2 -0
- package/cli/build-css.d.ts.map +1 -0
- package/cli/build-css.js +23 -0
- package/cli/build-css.ts +32 -0
- package/cli/commands/build.d.ts +5 -0
- package/cli/commands/build.d.ts.map +1 -0
- package/cli/commands/build.js +89 -0
- package/cli/commands/build.ts +65 -0
- package/cli/commands/graph.d.ts +3 -0
- package/cli/commands/graph.d.ts.map +1 -0
- package/cli/commands/graph.js +219 -0
- package/cli/commands/graph.ts +137 -0
- package/cli/commands/index.d.ts +8 -0
- package/cli/commands/index.d.ts.map +1 -0
- package/cli/commands/index.js +7 -0
- package/cli/commands/index.ts +7 -0
- package/cli/commands/patch-apply.d.ts +3 -0
- package/cli/commands/patch-apply.d.ts.map +1 -0
- package/cli/commands/patch-apply.js +75 -0
- package/cli/commands/patch-apply.ts +80 -0
- package/cli/commands/patch.d.ts +3 -0
- package/cli/commands/patch.d.ts.map +1 -0
- package/cli/commands/patch.js +21 -0
- package/cli/commands/patch.ts +22 -0
- package/cli/commands/set.d.ts +3 -0
- package/cli/commands/set.d.ts.map +1 -0
- package/cli/commands/set.js +286 -0
- package/cli/commands/set.ts +225 -0
- package/cli/commands/utils.d.ts +4 -0
- package/cli/commands/utils.d.ts.map +1 -0
- package/cli/commands/utils.js +51 -0
- package/cli/commands/utils.ts +50 -0
- package/cli/commands/validate.d.ts +3 -0
- package/cli/commands/validate.d.ts.map +1 -0
- package/cli/commands/validate.js +131 -0
- package/cli/commands/validate.ts +115 -0
- package/cli/commands/why.d.ts +3 -0
- package/cli/commands/why.d.ts.map +1 -0
- package/cli/commands/why.js +64 -0
- package/cli/commands/why.ts +46 -0
- package/cli/config-schema.d.ts +238 -0
- package/cli/config-schema.d.ts.map +1 -0
- package/cli/config-schema.js +21 -0
- package/cli/config-schema.ts +27 -0
- package/cli/config.d.ts +4 -0
- package/cli/config.d.ts.map +1 -0
- package/cli/config.js +37 -0
- package/cli/config.ts +35 -0
- package/cli/dcv.d.ts +3 -0
- package/cli/dcv.d.ts.map +1 -0
- package/cli/dcv.js +86 -0
- package/cli/dcv.ts +107 -0
- package/cli/engine-helpers.d.ts +8 -0
- package/cli/engine-helpers.d.ts.map +1 -0
- package/cli/engine-helpers.js +70 -0
- package/cli/engine-helpers.ts +61 -0
- package/cli/graph-poset.d.ts +9 -0
- package/cli/graph-poset.d.ts.map +1 -0
- package/cli/graph-poset.js +58 -0
- package/cli/graph-poset.ts +74 -0
- package/cli/index.d.ts +3 -0
- package/cli/index.d.ts.map +1 -0
- package/cli/index.js +2 -0
- package/cli/index.ts +2 -0
- package/cli/result.d.ts +17 -0
- package/cli/result.d.ts.map +1 -0
- package/cli/result.js +29 -0
- package/cli/result.ts +27 -0
- package/cli/run.d.ts +3 -0
- package/cli/run.d.ts.map +1 -0
- package/cli/run.js +47 -0
- package/cli/run.ts +54 -0
- package/cli/smoke-test.d.ts +2 -0
- package/cli/smoke-test.d.ts.map +1 -0
- package/cli/smoke-test.js +33 -0
- package/cli/smoke-test.ts +40 -0
- package/cli/types.d.ts +86 -0
- package/cli/types.d.ts.map +1 -0
- package/cli/types.js +1 -0
- package/cli/types.ts +78 -0
- package/core/breakpoints.d.ts +12 -0
- package/core/breakpoints.d.ts.map +1 -0
- package/core/breakpoints.js +48 -0
- package/core/breakpoints.ts +50 -0
- package/core/cli-format.d.ts +8 -0
- package/core/cli-format.d.ts.map +1 -0
- package/core/cli-format.js +29 -0
- package/core/cli-format.ts +31 -0
- package/core/color.d.ts +14 -0
- package/core/color.d.ts.map +1 -0
- package/core/color.js +136 -0
- package/core/color.ts +148 -0
- package/core/constraints/cross-axis.d.ts +33 -0
- package/core/constraints/cross-axis.d.ts.map +1 -0
- package/core/constraints/cross-axis.js +93 -0
- package/core/constraints/cross-axis.ts +114 -0
- package/core/constraints/monotonic-lightness.d.ts +5 -0
- package/core/constraints/monotonic-lightness.d.ts.map +1 -0
- package/core/constraints/monotonic-lightness.js +37 -0
- package/core/constraints/monotonic-lightness.ts +38 -0
- package/core/constraints/monotonic.d.ts +7 -0
- package/core/constraints/monotonic.d.ts.map +1 -0
- package/core/constraints/monotonic.js +65 -0
- package/core/constraints/monotonic.ts +74 -0
- package/core/constraints/threshold.d.ts +10 -0
- package/core/constraints/threshold.d.ts.map +1 -0
- package/core/constraints/threshold.js +36 -0
- package/core/constraints/threshold.ts +43 -0
- package/core/constraints/wcag.d.ts +11 -0
- package/core/constraints/wcag.d.ts.map +1 -0
- package/core/constraints/wcag.js +53 -0
- package/core/constraints/wcag.ts +70 -0
- package/core/cross-axis-config.d.ts +5 -0
- package/core/cross-axis-config.d.ts.map +1 -0
- package/core/cross-axis-config.js +144 -0
- package/core/cross-axis-config.ts +152 -0
- package/core/engine.d.ts +32 -0
- package/core/engine.d.ts.map +1 -0
- package/core/engine.js +46 -0
- package/core/engine.ts +65 -0
- package/core/flatten.d.ts +20 -0
- package/core/flatten.d.ts.map +1 -0
- package/core/flatten.js +80 -0
- package/core/flatten.ts +116 -0
- package/core/image-export.d.ts +10 -0
- package/core/image-export.d.ts.map +1 -0
- package/core/image-export.js +43 -0
- package/core/image-export.ts +48 -0
- package/core/index.d.ts +31 -0
- package/core/index.d.ts.map +1 -0
- package/core/index.js +54 -0
- package/core/index.ts +72 -0
- package/core/patch.d.ts +28 -0
- package/core/patch.d.ts.map +1 -0
- package/core/patch.js +110 -0
- package/core/patch.ts +134 -0
- package/core/poset.d.ts +41 -0
- package/core/poset.d.ts.map +1 -0
- package/core/poset.js +275 -0
- package/core/poset.ts +311 -0
- package/core/why.d.ts +17 -0
- package/core/why.d.ts.map +1 -0
- package/core/why.js +45 -0
- package/core/why.ts +63 -0
- package/dist/test-overrides-removal.json +4 -0
- package/dist/tmp.patch.json +35 -0
- package/package.json +90 -0
- package/themes/color.lg.order.json +15 -0
- package/themes/color.md.order.json +15 -0
- package/themes/color.order.json +15 -0
- package/themes/color.sm.order.json +15 -0
- package/themes/cross-axis.rules.json +36 -0
- package/themes/cross-axis.sm.rules.json +12 -0
- package/themes/layout.lg.order.json +18 -0
- package/themes/layout.md.order.json +18 -0
- package/themes/layout.order.json +18 -0
- package/themes/layout.sm.order.json +18 -0
- package/themes/spacing.order.json +14 -0
- package/themes/typography.lg.order.json +15 -0
- package/themes/typography.md.order.json +15 -0
- package/themes/typography.order.json +15 -0
- package/themes/typography.sm.order.json +15 -0
- package/tokens/overrides/base.json +22 -0
- package/tokens/overrides/lg.json +20 -0
- package/tokens/overrides/md.json +16 -0
- package/tokens/overrides/sm.json +16 -0
- package/tokens/overrides/viol.color.json +6 -0
- package/tokens/overrides/viol.typography.json +6 -0
- package/tokens/tokens.demo-violations.json +116 -0
- package/tokens/tokens.example.json +128 -0
- package/tokens/tokens.json +67 -0
- package/tokens/tokens.multi-violations.json +21 -0
- package/tokens/tokens.schema.d.ts +2298 -0
- package/tokens/tokens.schema.d.ts.map +1 -0
- package/tokens/tokens.schema.js +148 -0
- package/tokens/tokens.schema.ts +196 -0
- package/tokens/tokens.test.json +38 -0
- package/tokens/tokens.touch-violation.json +8 -0
- package/tokens/typography.classes.css +11 -0
- package/tokens/typography.css +20 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { CrossAxisPlugin, type CrossAxisRule, type Ctx } from "./constraints/cross-axis.js";
|
|
3
|
+
|
|
4
|
+
type When = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; value: number };
|
|
5
|
+
type Require = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; ref?: string; fallback?: string|number };
|
|
6
|
+
type Compare = { a: string; op: "<="|">="|"<"|">"|"=="|"!="; b: string; delta?: string|number };
|
|
7
|
+
type RawRule = { id: string; level?: "error"|"warn"; where?: string; bp?: string; when?: When; require?: Require; compare?: Compare; };
|
|
8
|
+
|
|
9
|
+
export function loadCrossAxisPlugin(
|
|
10
|
+
path: string,
|
|
11
|
+
bp?: string,
|
|
12
|
+
opts?: { debug?: boolean; knownIds?: Set<string> }
|
|
13
|
+
) {
|
|
14
|
+
const debug = !!opts?.debug;
|
|
15
|
+
const known = opts?.knownIds ?? new Set<string>();
|
|
16
|
+
const log = (...args: any[]) => { if (debug) console.log("[cross-axis]", ...args); };
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(path)) {
|
|
19
|
+
log(`no rules file at ${path} (bp=${bp ?? "global"})`);
|
|
20
|
+
return CrossAxisPlugin([], bp);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(path, "utf8")) as { rules: RawRule[] };
|
|
24
|
+
const rules: CrossAxisRule[] = [];
|
|
25
|
+
const unknownIds = new Set<string>();
|
|
26
|
+
const skipped: Array<{ id?: string; reason: string }> = [];
|
|
27
|
+
|
|
28
|
+
// Fuzzy suggestion helpers (lightweight Levenshtein)
|
|
29
|
+
function levenshtein(a: string, b: string) {
|
|
30
|
+
const dp = Array(b.length + 1).fill(0).map((_, j) => j);
|
|
31
|
+
for (let i = 1; i <= a.length; i++) {
|
|
32
|
+
let prev = i - 1, cur = i;
|
|
33
|
+
for (let j = 1; j <= b.length; j++) {
|
|
34
|
+
const tmp = cur;
|
|
35
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
36
|
+
cur = Math.min(dp[j] + 1, cur + 1, prev + cost);
|
|
37
|
+
dp[j] = tmp;
|
|
38
|
+
prev = tmp;
|
|
39
|
+
}
|
|
40
|
+
dp[b.length] = cur;
|
|
41
|
+
}
|
|
42
|
+
return dp[b.length];
|
|
43
|
+
}
|
|
44
|
+
function suggest(id: string, k = 3) {
|
|
45
|
+
return [...known].map(c => ({ id: c, d: levenshtein(id, c) }))
|
|
46
|
+
.sort((a, b) => a.d - b.d)
|
|
47
|
+
.slice(0, k);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const needId = (id?: string) => {
|
|
51
|
+
if (!id) return false;
|
|
52
|
+
if (!known.has(id)) { unknownIds.add(id); }
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (const r of raw.rules || []) {
|
|
57
|
+
if (r.bp && bp && r.bp !== bp) { continue; }
|
|
58
|
+
if (r.bp && !bp) { // rule targets specific breakpoint; skip in global run
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (r.when && r.require) {
|
|
63
|
+
// Validate IDs
|
|
64
|
+
needId(r.when.id);
|
|
65
|
+
needId(r.require.id);
|
|
66
|
+
if (r.require.ref) needId(r.require.ref);
|
|
67
|
+
|
|
68
|
+
rules.push({
|
|
69
|
+
id: r.id, level: r.level, where: r.where,
|
|
70
|
+
when: { id: r.when.id, test: makeOp(r.when.op, r.when.value) },
|
|
71
|
+
require: {
|
|
72
|
+
id: r.require.id,
|
|
73
|
+
test: (v: number, ctx: Ctx) => {
|
|
74
|
+
const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
|
|
75
|
+
return cmp(v, rhs, r.require!.op);
|
|
76
|
+
},
|
|
77
|
+
msg: (v: number, ctx: Ctx) => {
|
|
78
|
+
const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
|
|
79
|
+
return `${r.require!.id} ${prettyFail(r.require!.op)} ${fmt(rhs)} (was ${fmt(v)})`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
} else if (r.compare) {
|
|
84
|
+
needId(r.compare.a);
|
|
85
|
+
needId(r.compare.b);
|
|
86
|
+
|
|
87
|
+
rules.push({
|
|
88
|
+
id: r.id, level: r.level, where: r.where,
|
|
89
|
+
when: { id: r.compare.a, test: () => true },
|
|
90
|
+
require: {
|
|
91
|
+
id: r.compare.a,
|
|
92
|
+
test: (_: number, ctx: Ctx) => {
|
|
93
|
+
const a = ctx.getPx(r.compare!.a) ?? NaN;
|
|
94
|
+
const b = ctx.getPx(r.compare!.b) ?? NaN;
|
|
95
|
+
const delta = px(r.compare!.delta ?? 0);
|
|
96
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return true; // skip check if missing
|
|
97
|
+
return cmp(a, b + delta, r.compare!.op);
|
|
98
|
+
},
|
|
99
|
+
msg: (_: number, ctx: Ctx) => {
|
|
100
|
+
const a = ctx.getPx(r.compare!.a);
|
|
101
|
+
const b = ctx.getPx(r.compare!.b);
|
|
102
|
+
const delta = px(r.compare!.delta ?? 0);
|
|
103
|
+
return `${r.compare!.a} ${prettyFail(r.compare!.op)} ${fmt((b ?? 0) + delta)} (was ${fmt(a ?? NaN)})`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
skipped.push({ id: r.id, reason: "neither when+require nor compare present" });
|
|
109
|
+
}
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
skipped.push({ id: r.id, reason: `exception: ${e?.message ?? e}` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log(`loaded ${rules.length} rule(s) from ${path}${bp ? ` [bp=${bp}]` : ""}`);
|
|
116
|
+
if (unknownIds.size) {
|
|
117
|
+
log(`unknown ids referenced:`, [...unknownIds].join(", "));
|
|
118
|
+
for (const u of unknownIds) {
|
|
119
|
+
const s = suggest(u, 3);
|
|
120
|
+
if (s.length) log(` did you mean: ${s.map(x => `${x.id} (d=${x.d})`).join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (skipped.length) {
|
|
124
|
+
for (const s of skipped) log(`skipped rule ${s.id ?? "(no id)"} — ${s.reason}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extra hint for common anchor pitfall
|
|
128
|
+
for (const r of raw.rules || []) {
|
|
129
|
+
if (r.require?.ref && !known.has(r.require.ref)) {
|
|
130
|
+
log(`anchor missing: ${r.require.ref} → will use fallback=${JSON.stringify(r.require.fallback)} when evaluating`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return CrossAxisPlugin(rules, bp);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// helpers
|
|
138
|
+
const px = (v: string|number) => typeof v === "number" ? v : parseFloat(String(v)) * (String(v).trim().endsWith("rem") ? 16 : 1);
|
|
139
|
+
const cmp = (a:number,b:number,op:When["op"]) =>
|
|
140
|
+
op === ">="? a>=b : op === ">"? a>b : op === "<="? a<=b : op === "<"? a<b : op === "=="? a===b : a!==b;
|
|
141
|
+
const prettyFail = (op: string) => ({">=":"<",">":"≤","<=":">","<": "≥","==":"≠","!=":"="} as any)[op] || "≠";
|
|
142
|
+
const fmt = (v:number|string) => Number.isFinite(Number(v)) ? `${Number(v)}px` : String(v);
|
|
143
|
+
function valueOrRef(ctx: Ctx, ref?: string, fallback?: string|number) {
|
|
144
|
+
if (ref) {
|
|
145
|
+
const v = ctx.getPx(ref);
|
|
146
|
+
if (v != null) return v;
|
|
147
|
+
}
|
|
148
|
+
return typeof fallback === "number" ? fallback : px(fallback ?? 0);
|
|
149
|
+
}
|
|
150
|
+
function makeOp(op: When["op"], rhs: number) {
|
|
151
|
+
return (v: number) => cmp(v, rhs, op);
|
|
152
|
+
}
|
package/core/engine.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TokenId, TokenValue } from "./flatten.js";
|
|
2
|
+
export type ConstraintIssue = {
|
|
3
|
+
id: TokenId | string;
|
|
4
|
+
rule: string;
|
|
5
|
+
level: "error" | "warn";
|
|
6
|
+
message: string;
|
|
7
|
+
where?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConstraintPlugin = {
|
|
10
|
+
id: string;
|
|
11
|
+
evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
|
|
12
|
+
};
|
|
13
|
+
export type Graph = Map<TokenId, Set<TokenId>>;
|
|
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
|
+
use(plugin: ConstraintPlugin): this;
|
|
20
|
+
get(id: TokenId): TokenValue | undefined;
|
|
21
|
+
set(id: TokenId, value: TokenValue): void;
|
|
22
|
+
/** All nodes that depend (directly/indirectly) on start. */
|
|
23
|
+
affected(start: TokenId): Set<TokenId>;
|
|
24
|
+
evaluate(candidates: Set<TokenId>): ConstraintIssue[];
|
|
25
|
+
/** Single change -> returns affected set, issues, and a patch you can feed to adapters. */
|
|
26
|
+
commit(id: TokenId, value: TokenValue): {
|
|
27
|
+
affected: string[];
|
|
28
|
+
issues: ConstraintIssue[];
|
|
29
|
+
patch: Record<string, TokenValue>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IAEX,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,eAAe,EAAE,CAAC;CACvE,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,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,MAAM,EAAE,gBAAgB;IAE5B,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IACxC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;IAElC,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAatC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC;IAIjC,2FAA2F;IAC3F,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU;;;;;CAQtC"}
|
package/core/engine.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class Engine {
|
|
2
|
+
values = new Map();
|
|
3
|
+
graph = new Map();
|
|
4
|
+
plugins = [];
|
|
5
|
+
constructor(initValues, edges) {
|
|
6
|
+
for (const [k, v] of Object.entries(initValues))
|
|
7
|
+
this.values.set(k, v);
|
|
8
|
+
for (const [from, to] of edges) {
|
|
9
|
+
if (!this.graph.has(from))
|
|
10
|
+
this.graph.set(from, new Set());
|
|
11
|
+
this.graph.get(from).add(to);
|
|
12
|
+
if (!this.graph.has(to))
|
|
13
|
+
this.graph.set(to, new Set());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
use(plugin) { this.plugins.push(plugin); return this; }
|
|
17
|
+
get(id) { return this.values.get(id); }
|
|
18
|
+
set(id, value) { this.values.set(id, value); }
|
|
19
|
+
/** All nodes that depend (directly/indirectly) on start. */
|
|
20
|
+
affected(start) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const stack = [start];
|
|
23
|
+
while (stack.length) {
|
|
24
|
+
const n = stack.pop();
|
|
25
|
+
if (seen.has(n))
|
|
26
|
+
continue;
|
|
27
|
+
seen.add(n);
|
|
28
|
+
for (const d of this.graph.get(n) ?? [])
|
|
29
|
+
stack.push(d);
|
|
30
|
+
}
|
|
31
|
+
seen.delete(start);
|
|
32
|
+
return seen;
|
|
33
|
+
}
|
|
34
|
+
evaluate(candidates) {
|
|
35
|
+
return this.plugins.flatMap(p => p.evaluate(this, candidates));
|
|
36
|
+
}
|
|
37
|
+
/** Single change -> returns affected set, issues, and a patch you can feed to adapters. */
|
|
38
|
+
commit(id, value) {
|
|
39
|
+
this.set(id, value);
|
|
40
|
+
const A = this.affected(id);
|
|
41
|
+
const candidates = new Set([id, ...A]);
|
|
42
|
+
const issues = this.evaluate(candidates);
|
|
43
|
+
const patch = { [id]: value };
|
|
44
|
+
return { affected: [...A], issues, patch };
|
|
45
|
+
}
|
|
46
|
+
}
|
package/core/engine.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type TokenId = string;
|
|
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
|
+
export type FlatToken = {
|
|
9
|
+
id: TokenId;
|
|
10
|
+
type: string;
|
|
11
|
+
value: TokenValue;
|
|
12
|
+
raw: TokenValue;
|
|
13
|
+
refs: TokenId[];
|
|
14
|
+
};
|
|
15
|
+
export type FlattenResult = {
|
|
16
|
+
flat: Record<TokenId, FlatToken>;
|
|
17
|
+
edges: Array<[from: TokenId, to: TokenId]>;
|
|
18
|
+
};
|
|
19
|
+
export declare function flattenTokens(root: TokenNode): FlattenResult;
|
|
20
|
+
//# sourceMappingURL=flatten.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["flatten.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AACzC,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC;IAChB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAIF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CA4F5D"}
|
package/core/flatten.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const REF_RE = /\{([a-z0-9.-]+)\}/gi;
|
|
2
|
+
export function flattenTokens(root) {
|
|
3
|
+
const flat = {};
|
|
4
|
+
const edges = [];
|
|
5
|
+
// First pass: collect all tokens
|
|
6
|
+
function walk(node, path = []) {
|
|
7
|
+
if (!node || typeof node !== 'object')
|
|
8
|
+
return;
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(node, '$value')) {
|
|
10
|
+
const id = path.join('.');
|
|
11
|
+
const raw = node.$value;
|
|
12
|
+
if (raw === undefined)
|
|
13
|
+
return; // Skip tokens without values
|
|
14
|
+
const refs = [];
|
|
15
|
+
// Find all references in the value
|
|
16
|
+
if (typeof raw === 'string') {
|
|
17
|
+
const matches = raw.matchAll(REF_RE);
|
|
18
|
+
for (const match of matches) {
|
|
19
|
+
refs.push(match[1]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
flat[id] = {
|
|
23
|
+
id,
|
|
24
|
+
type: String(node.$type ?? 'unknown'),
|
|
25
|
+
value: raw,
|
|
26
|
+
raw,
|
|
27
|
+
refs
|
|
28
|
+
};
|
|
29
|
+
// Add edges for dependencies
|
|
30
|
+
refs.forEach(refId => edges.push([refId, id]));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Recursively walk children
|
|
34
|
+
for (const key of Object.keys(node)) {
|
|
35
|
+
if (key.startsWith('$'))
|
|
36
|
+
continue;
|
|
37
|
+
const child = node[key];
|
|
38
|
+
if (typeof child === 'object' && child !== null) {
|
|
39
|
+
walk(child, path.concat(key));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(root);
|
|
44
|
+
// Second pass: resolve references iteratively
|
|
45
|
+
let changed = true;
|
|
46
|
+
let iterations = 0;
|
|
47
|
+
const maxIterations = Object.keys(flat).length * 2; // Safety limit
|
|
48
|
+
while (changed && iterations < maxIterations) {
|
|
49
|
+
changed = false;
|
|
50
|
+
iterations++;
|
|
51
|
+
for (const token of Object.values(flat)) {
|
|
52
|
+
if (typeof token.value === 'string' && token.value.includes('{')) {
|
|
53
|
+
let newValue = token.value;
|
|
54
|
+
let fullyResolved = true;
|
|
55
|
+
for (const refId of token.refs) {
|
|
56
|
+
const refToken = flat[refId];
|
|
57
|
+
if (!refToken) {
|
|
58
|
+
throw new Error(`Could not resolve token ${refId}`);
|
|
59
|
+
}
|
|
60
|
+
// If the referenced token still has unresolved refs, skip this iteration
|
|
61
|
+
if (typeof refToken.value === 'string' && refToken.value.includes('{')) {
|
|
62
|
+
fullyResolved = false;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
// Replace the reference with the resolved value
|
|
66
|
+
const refPattern = new RegExp(`\\{${refId}\\}`, 'g');
|
|
67
|
+
newValue = newValue.replace(refPattern, String(refToken.value));
|
|
68
|
+
}
|
|
69
|
+
if (fullyResolved && newValue !== token.value) {
|
|
70
|
+
token.value = newValue;
|
|
71
|
+
changed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (iterations >= maxIterations) {
|
|
77
|
+
throw new Error('Token resolution exceeded maximum iterations - possible circular reference');
|
|
78
|
+
}
|
|
79
|
+
return { flat, edges };
|
|
80
|
+
}
|
package/core/flatten.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ImageFmt = "svg" | "png";
|
|
2
|
+
export type Renderer = "mermaid" | "dot";
|
|
3
|
+
export declare function exportGraphImage(inputPath: string, // .mmd or .dot we just wrote
|
|
4
|
+
outPath: string, // .svg or .png to write
|
|
5
|
+
fmt: ImageFmt, // "svg" | "png"
|
|
6
|
+
renderer: Renderer): {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
hint?: string;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=image-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-export.d.ts","sourceRoot":"","sources":["image-export.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AACrC,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;AAEzC,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EAAe,6BAA6B;AAC7D,OAAO,EAAE,MAAM,EAAiB,wBAAwB;AACxD,GAAG,EAAE,QAAQ,EAAmB,gBAAgB;AAChD,QAAQ,EAAE,QAAQ,GACjB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBhC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
function which(cmd) {
|
|
6
|
+
const paths = (process.env.PATH || "").split(path.delimiter);
|
|
7
|
+
for (const p of paths) {
|
|
8
|
+
const full = path.join(p, cmd + (process.platform === "win32" ? ".cmd" : ""));
|
|
9
|
+
try {
|
|
10
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
11
|
+
return full;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// File not accessible, continue to next path
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function exportGraphImage(inputPath, // .mmd or .dot we just wrote
|
|
20
|
+
outPath, // .svg or .png to write
|
|
21
|
+
fmt, // "svg" | "png"
|
|
22
|
+
renderer // "mermaid" | "dot"
|
|
23
|
+
) {
|
|
24
|
+
if (renderer === "mermaid") {
|
|
25
|
+
const mmdc = which("mmdc");
|
|
26
|
+
if (!mmdc) {
|
|
27
|
+
return { ok: false, hint: "Install @mermaid-js/mermaid-cli (mmdc) to render images. Fallback written: .mmd" };
|
|
28
|
+
}
|
|
29
|
+
const args = ["-i", inputPath, "-o", outPath, "-t", "default", "-b", "transparent"];
|
|
30
|
+
if (fmt === "png")
|
|
31
|
+
args.push("-p", "puppeteer-config.json"); // optional, if you keep one
|
|
32
|
+
const res = spawnSync(mmdc, args, { stdio: "inherit" });
|
|
33
|
+
return { ok: res.status === 0, hint: res.status !== 0 ? "mmdc failed; see logs." : undefined };
|
|
34
|
+
}
|
|
35
|
+
// renderer === "dot"
|
|
36
|
+
const dot = which("dot");
|
|
37
|
+
if (!dot) {
|
|
38
|
+
return { ok: false, hint: "Install Graphviz (dot) to render images. Fallback written: .dot" };
|
|
39
|
+
}
|
|
40
|
+
const argFmt = fmt === "svg" ? "-Tsvg" : "-Tpng";
|
|
41
|
+
const res = spawnSync(dot, [argFmt, inputPath, "-o", outPath], { stdio: "inherit" });
|
|
42
|
+
return { ok: res.status === 0, hint: res.status !== 0 ? "dot failed; see logs." : undefined };
|
|
43
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/core/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|