design-constraint-validator 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -659
  3. package/adapters/README.md +46 -46
  4. package/adapters/css.ts +116 -116
  5. package/adapters/js.ts +14 -14
  6. package/adapters/json.ts +45 -45
  7. package/cli/build-css.ts +32 -32
  8. package/cli/commands/build.ts +65 -65
  9. package/cli/commands/graph.d.ts.map +1 -1
  10. package/cli/commands/graph.js +26 -10
  11. package/cli/commands/graph.ts +180 -137
  12. package/cli/commands/index.ts +7 -7
  13. package/cli/commands/patch-apply.ts +80 -80
  14. package/cli/commands/patch.ts +22 -22
  15. package/cli/commands/set.d.ts.map +1 -1
  16. package/cli/commands/set.js +12 -4
  17. package/cli/commands/set.ts +239 -225
  18. package/cli/commands/utils.ts +50 -50
  19. package/cli/commands/validate.d.ts.map +1 -1
  20. package/cli/commands/validate.js +86 -33
  21. package/cli/commands/validate.ts +176 -115
  22. package/cli/commands/why.d.ts.map +1 -1
  23. package/cli/commands/why.js +86 -20
  24. package/cli/commands/why.ts +158 -46
  25. package/cli/config-schema.ts +27 -27
  26. package/cli/config.ts +35 -35
  27. package/cli/constraint-registry.d.ts +101 -0
  28. package/cli/constraint-registry.d.ts.map +1 -0
  29. package/cli/constraint-registry.js +225 -0
  30. package/cli/constraint-registry.ts +304 -0
  31. package/cli/constraints-loader.d.ts +30 -0
  32. package/cli/constraints-loader.d.ts.map +1 -0
  33. package/cli/constraints-loader.js +58 -0
  34. package/cli/constraints-loader.ts +83 -0
  35. package/cli/cross-axis-loader.d.ts +91 -0
  36. package/cli/cross-axis-loader.d.ts.map +1 -0
  37. package/cli/cross-axis-loader.js +222 -0
  38. package/cli/cross-axis-loader.ts +289 -0
  39. package/cli/dcv.js +4 -0
  40. package/cli/dcv.ts +111 -107
  41. package/cli/engine-helpers.d.ts +33 -0
  42. package/cli/engine-helpers.d.ts.map +1 -1
  43. package/cli/engine-helpers.js +87 -22
  44. package/cli/engine-helpers.ts +133 -61
  45. package/cli/graph-poset.ts +74 -74
  46. package/cli/json-output.d.ts +64 -0
  47. package/cli/json-output.d.ts.map +1 -0
  48. package/cli/json-output.js +107 -0
  49. package/cli/json-output.ts +177 -0
  50. package/cli/result.ts +27 -27
  51. package/cli/run.ts +54 -54
  52. package/cli/smoke-test.ts +40 -40
  53. package/cli/types.d.ts +6 -0
  54. package/cli/types.d.ts.map +1 -1
  55. package/cli/types.ts +84 -78
  56. package/core/breakpoints.ts +50 -50
  57. package/core/cli-format.ts +31 -31
  58. package/core/color.ts +148 -148
  59. package/core/constraints/cross-axis.ts +114 -114
  60. package/core/constraints/monotonic-lightness.ts +38 -38
  61. package/core/constraints/monotonic.ts +74 -74
  62. package/core/constraints/threshold.ts +43 -43
  63. package/core/constraints/wcag.ts +70 -70
  64. package/core/cross-axis-config.d.ts +29 -0
  65. package/core/cross-axis-config.d.ts.map +1 -1
  66. package/core/cross-axis-config.js +29 -0
  67. package/core/cross-axis-config.ts +181 -151
  68. package/core/engine.d.ts +95 -0
  69. package/core/engine.d.ts.map +1 -1
  70. package/core/engine.js +22 -0
  71. package/core/engine.ts +167 -65
  72. package/core/flatten.ts +116 -116
  73. package/core/image-export.ts +48 -48
  74. package/core/index.d.ts +9 -30
  75. package/core/index.d.ts.map +1 -1
  76. package/core/index.js +7 -54
  77. package/core/index.ts +10 -72
  78. package/core/patch.ts +134 -134
  79. package/core/poset.ts +311 -311
  80. package/core/why.ts +63 -63
  81. package/package.json +96 -90
  82. package/themes/color.lg.order.json +15 -15
  83. package/themes/color.md.order.json +15 -15
  84. package/themes/color.order.json +15 -15
  85. package/themes/color.sm.order.json +15 -15
  86. package/themes/cross-axis.rules.json +35 -35
  87. package/themes/cross-axis.sm.rules.json +12 -12
  88. package/themes/layout.lg.order.json +18 -18
  89. package/themes/layout.md.order.json +18 -18
  90. package/themes/layout.order.json +18 -18
  91. package/themes/layout.sm.order.json +18 -18
  92. package/themes/spacing.order.json +14 -14
  93. package/themes/typography.lg.order.json +15 -15
  94. package/themes/typography.md.order.json +15 -15
  95. package/themes/typography.order.json +15 -15
  96. package/themes/typography.sm.order.json +15 -15
  97. package/dist/test-overrides-removal.json +0 -4
  98. package/dist/tmp.patch.json +0 -35
  99. package/tokens/overrides/base.json +0 -22
  100. package/tokens/overrides/lg.json +0 -20
  101. package/tokens/overrides/md.json +0 -16
  102. package/tokens/overrides/sm.json +0 -16
  103. package/tokens/overrides/viol.color.json +0 -6
  104. package/tokens/overrides/viol.typography.json +0 -6
  105. package/tokens/tokens.demo-violations.json +0 -116
  106. package/tokens/tokens.example.json +0 -128
  107. package/tokens/tokens.json +0 -67
  108. package/tokens/tokens.multi-violations.json +0 -21
  109. package/tokens/tokens.schema.d.ts +0 -2298
  110. package/tokens/tokens.schema.d.ts.map +0 -1
  111. package/tokens/tokens.schema.js +0 -148
  112. package/tokens/tokens.schema.ts +0 -196
  113. package/tokens/tokens.test.json +0 -38
  114. package/tokens/tokens.touch-violation.json +0 -8
  115. package/tokens/typography.classes.css +0 -11
  116. package/tokens/typography.css +0 -20
@@ -1,152 +1,182 @@
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);
1
+ /**
2
+ * @deprecated This module is deprecated. Use cli/cross-axis-loader.ts instead.
3
+ *
4
+ * Phase 3B (Filesystem Separation): This file contains filesystem access logic
5
+ * that has been moved to the CLI layer (cli/cross-axis-loader.ts).
6
+ *
7
+ * Core modules should not import from node:fs. Instead:
8
+ * - CLI code uses cli/cross-axis-loader.ts to read and parse rules
9
+ * - Core plugin (core/constraints/cross-axis.ts) accepts pre-parsed rules
10
+ *
11
+ * Migration:
12
+ * ```ts
13
+ * // OLD (core reads filesystem):
14
+ * import { loadCrossAxisPlugin } from './core/cross-axis-config.js';
15
+ * engine.use(loadCrossAxisPlugin(path, bp, { knownIds }));
16
+ *
17
+ * // NEW (CLI reads, core receives data):
18
+ * import { loadCrossAxisRules } from './cli/cross-axis-loader.js';
19
+ * import { CrossAxisPlugin } from './core/constraints/cross-axis.js';
20
+ * const rules = loadCrossAxisRules(path, { bp, knownIds });
21
+ * engine.use(CrossAxisPlugin(rules, bp));
22
+ * ```
23
+ *
24
+ * This file will be removed in a future major version.
25
+ */
26
+
27
+ import fs from "node:fs";
28
+ import { CrossAxisPlugin, type CrossAxisRule, type Ctx } from "./constraints/cross-axis.js";
29
+
30
+ type When = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; value: number };
31
+ type Require = { id: string; op: "<="|">="|"<"|">"|"=="|"!="; ref?: string; fallback?: string|number };
32
+ type Compare = { a: string; op: "<="|">="|"<"|">"|"=="|"!="; b: string; delta?: string|number };
33
+ type RawRule = { id: string; level?: "error"|"warn"; where?: string; bp?: string; when?: When; require?: Require; compare?: Compare; };
34
+
35
+ /**
36
+ * @deprecated Use cli/cross-axis-loader.ts loadCrossAxisRules() + CrossAxisPlugin() instead.
37
+ * This function will be removed in a future major version.
38
+ */
39
+ export function loadCrossAxisPlugin(
40
+ path: string,
41
+ bp?: string,
42
+ opts?: { debug?: boolean; knownIds?: Set<string> }
43
+ ) {
44
+ const debug = !!opts?.debug;
45
+ const known = opts?.knownIds ?? new Set<string>();
46
+ const log = (...args: any[]) => { if (debug) console.log("[cross-axis]", ...args); };
47
+
48
+ if (!fs.existsSync(path)) {
49
+ log(`no rules file at ${path} (bp=${bp ?? "global"})`);
50
+ return CrossAxisPlugin([], bp);
51
+ }
52
+
53
+ const raw = JSON.parse(fs.readFileSync(path, "utf8")) as { rules: RawRule[] };
54
+ const rules: CrossAxisRule[] = [];
55
+ const unknownIds = new Set<string>();
56
+ const skipped: Array<{ id?: string; reason: string }> = [];
57
+
58
+ // Fuzzy suggestion helpers (lightweight Levenshtein)
59
+ function levenshtein(a: string, b: string) {
60
+ const dp = Array(b.length + 1).fill(0).map((_, j) => j);
61
+ for (let i = 1; i <= a.length; i++) {
62
+ let prev = i - 1, cur = i;
63
+ for (let j = 1; j <= b.length; j++) {
64
+ const tmp = cur;
65
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
66
+ cur = Math.min(dp[j] + 1, cur + 1, prev + cost);
67
+ dp[j] = tmp;
68
+ prev = tmp;
69
+ }
70
+ dp[b.length] = cur;
71
+ }
72
+ return dp[b.length];
73
+ }
74
+ function suggest(id: string, k = 3) {
75
+ return [...known].map(c => ({ id: c, d: levenshtein(id, c) }))
76
+ .sort((a, b) => a.d - b.d)
77
+ .slice(0, k);
78
+ }
79
+
80
+ const needId = (id?: string) => {
81
+ if (!id) return false;
82
+ if (!known.has(id)) { unknownIds.add(id); }
83
+ return true;
84
+ };
85
+
86
+ for (const r of raw.rules || []) {
87
+ if (r.bp && bp && r.bp !== bp) { continue; }
88
+ if (r.bp && !bp) { // rule targets specific breakpoint; skip in global run
89
+ continue;
90
+ }
91
+ try {
92
+ if (r.when && r.require) {
93
+ // Validate IDs
94
+ needId(r.when.id);
95
+ needId(r.require.id);
96
+ if (r.require.ref) needId(r.require.ref);
97
+
98
+ rules.push({
99
+ id: r.id, level: r.level, where: r.where,
100
+ when: { id: r.when.id, test: makeOp(r.when.op, r.when.value) },
101
+ require: {
102
+ id: r.require.id,
103
+ test: (v: number, ctx: Ctx) => {
104
+ const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
105
+ return cmp(v, rhs, r.require!.op);
106
+ },
107
+ msg: (v: number, ctx: Ctx) => {
108
+ const rhs = valueOrRef(ctx, r.require!.ref, r.require!.fallback);
109
+ return `${r.require!.id} ${prettyFail(r.require!.op)} ${fmt(rhs)} (was ${fmt(v)})`;
110
+ }
111
+ }
112
+ });
113
+ } else if (r.compare) {
114
+ needId(r.compare.a);
115
+ needId(r.compare.b);
116
+
117
+ rules.push({
118
+ id: r.id, level: r.level, where: r.where,
119
+ when: { id: r.compare.a, test: () => true },
120
+ require: {
121
+ id: r.compare.a,
122
+ test: (_: number, ctx: Ctx) => {
123
+ const a = ctx.getPx(r.compare!.a) ?? NaN;
124
+ const b = ctx.getPx(r.compare!.b) ?? NaN;
125
+ const delta = px(r.compare!.delta ?? 0);
126
+ if (Number.isNaN(a) || Number.isNaN(b)) return true; // skip check if missing
127
+ return cmp(a, b + delta, r.compare!.op);
128
+ },
129
+ msg: (_: number, ctx: Ctx) => {
130
+ const a = ctx.getPx(r.compare!.a);
131
+ const b = ctx.getPx(r.compare!.b);
132
+ const delta = px(r.compare!.delta ?? 0);
133
+ return `${r.compare!.a} ${prettyFail(r.compare!.op)} ${fmt((b ?? 0) + delta)} (was ${fmt(a ?? NaN)})`;
134
+ }
135
+ }
136
+ });
137
+ } else {
138
+ skipped.push({ id: r.id, reason: "neither when+require nor compare present" });
139
+ }
140
+ } catch (e: any) {
141
+ skipped.push({ id: r.id, reason: `exception: ${e?.message ?? e}` });
142
+ }
143
+ }
144
+
145
+ log(`loaded ${rules.length} rule(s) from ${path}${bp ? ` [bp=${bp}]` : ""}`);
146
+ if (unknownIds.size) {
147
+ log(`unknown ids referenced:`, [...unknownIds].join(", "));
148
+ for (const u of unknownIds) {
149
+ const s = suggest(u, 3);
150
+ if (s.length) log(` did you mean: ${s.map(x => `${x.id} (d=${x.d})`).join(', ')}`);
151
+ }
152
+ }
153
+ if (skipped.length) {
154
+ for (const s of skipped) log(`skipped rule ${s.id ?? "(no id)"} — ${s.reason}`);
155
+ }
156
+
157
+ // Extra hint for common anchor pitfall
158
+ for (const r of raw.rules || []) {
159
+ if (r.require?.ref && !known.has(r.require.ref)) {
160
+ log(`anchor missing: ${r.require.ref} → will use fallback=${JSON.stringify(r.require.fallback)} when evaluating`);
161
+ }
162
+ }
163
+
164
+ return CrossAxisPlugin(rules, bp);
165
+ }
166
+
167
+ // helpers
168
+ const px = (v: string|number) => typeof v === "number" ? v : parseFloat(String(v)) * (String(v).trim().endsWith("rem") ? 16 : 1);
169
+ const cmp = (a:number,b:number,op:When["op"]) =>
170
+ op === ">="? a>=b : op === ">"? a>b : op === "<="? a<=b : op === "<"? a<b : op === "=="? a===b : a!==b;
171
+ const prettyFail = (op: string) => ({">=":"<",">":"≤","<=":">","<": "≥","==":"≠","!=":"="} as any)[op] || "≠";
172
+ const fmt = (v:number|string) => Number.isFinite(Number(v)) ? `${Number(v)}px` : String(v);
173
+ function valueOrRef(ctx: Ctx, ref?: string, fallback?: string|number) {
174
+ if (ref) {
175
+ const v = ctx.getPx(ref);
176
+ if (v != null) return v;
177
+ }
178
+ return typeof fallback === "number" ? fallback : px(fallback ?? 0);
179
+ }
180
+ function makeOp(op: When["op"], rhs: number) {
181
+ return (v: number) => cmp(v, rhs, op);
152
182
  }
package/core/engine.d.ts CHANGED
@@ -1,13 +1,90 @@
1
1
  import type { TokenId, TokenValue } from "./flatten.js";
2
+ /**
3
+ * Represents a constraint violation.
4
+ *
5
+ * Phase 3C: Enhanced with metadata for tooling and visualization.
6
+ */
2
7
  export type ConstraintIssue = {
3
8
  id: TokenId | string;
4
9
  rule: string;
5
10
  level: "error" | "warn";
6
11
  message: string;
7
12
  where?: string;
13
+ /**
14
+ * Token IDs involved in this violation.
15
+ * Useful for filtering, highlighting, and incremental validation.
16
+ *
17
+ * Example: For a WCAG contrast violation between fg and bg tokens,
18
+ * this would be [fgTokenId, bgTokenId].
19
+ */
20
+ involvedTokens?: TokenId[];
21
+ /**
22
+ * Graph edges (reference relationships) involved in this violation.
23
+ * Useful for visualization and "why" explanations.
24
+ *
25
+ * Example: If a token references another that violates a constraint,
26
+ * this captures that reference edge.
27
+ */
28
+ involvedEdges?: Array<[TokenId, TokenId]>;
8
29
  };
30
+ /**
31
+ * Constraint plugin interface.
32
+ *
33
+ * Phase 3C: Documented contract for candidate-based evaluation.
34
+ *
35
+ * ## Candidate Contract
36
+ *
37
+ * Plugins MUST honor the `candidates` set for incremental validation:
38
+ * - Only evaluate constraints that involve at least one candidate token
39
+ * - Return violations where at least one involved token is in candidates
40
+ * - This enables efficient re-validation when tokens change
41
+ *
42
+ * ## Metadata Contract
43
+ *
44
+ * Plugins SHOULD populate `involvedTokens` in returned issues:
45
+ * - List all token IDs that participate in the constraint
46
+ * - This enables filtering, highlighting, and graph visualization
47
+ * - Optional but recommended for better tooling support
48
+ *
49
+ * ## Example Implementation
50
+ *
51
+ * ```ts
52
+ * export function MyPlugin(rules: Rule[]): ConstraintPlugin {
53
+ * return {
54
+ * id: "my-plugin",
55
+ * evaluate(engine, candidates) {
56
+ * const issues: ConstraintIssue[] = [];
57
+ * for (const rule of rules) {
58
+ * // Honor candidates: skip if no involved tokens are candidates
59
+ * if (!candidates.has(rule.tokenA) && !candidates.has(rule.tokenB)) {
60
+ * continue;
61
+ * }
62
+ * // Check constraint...
63
+ * if (violated) {
64
+ * issues.push({
65
+ * id: `${rule.tokenA}|${rule.tokenB}`,
66
+ * rule: "my-plugin",
67
+ * level: "error",
68
+ * message: "...",
69
+ * involvedTokens: [rule.tokenA, rule.tokenB], // Metadata
70
+ * });
71
+ * }
72
+ * }
73
+ * return issues;
74
+ * }
75
+ * };
76
+ * }
77
+ * ```
78
+ */
9
79
  export type ConstraintPlugin = {
10
80
  id: string;
81
+ /**
82
+ * Evaluate constraints for a set of candidate tokens.
83
+ *
84
+ * @param engine Engine instance providing token values and graph
85
+ * @param candidates Set of token IDs to evaluate (changed + affected)
86
+ * @returns Array of constraint violations
87
+ */
11
88
  evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
12
89
  };
13
90
  export type Graph = Map<TokenId, Set<TokenId>>;
@@ -19,6 +96,24 @@ export declare class Engine {
19
96
  use(plugin: ConstraintPlugin): this;
20
97
  get(id: TokenId): TokenValue | undefined;
21
98
  set(id: TokenId, value: TokenValue): void;
99
+ /**
100
+ * Get all token IDs in the engine.
101
+ *
102
+ * Phase 3C: Exposed for plugins and adapters.
103
+ * Useful for iterating all tokens or creating a full candidate set.
104
+ *
105
+ * @returns Array of all token IDs
106
+ */
107
+ getAllIds(): TokenId[];
108
+ /**
109
+ * Get flat token map (ID → value).
110
+ *
111
+ * Phase 3C: Exposed to avoid duplicate flattening in CLI/adapters.
112
+ * Returns a plain object suitable for serialization or adapter use.
113
+ *
114
+ * @returns Record mapping token IDs to their current values
115
+ */
116
+ getFlatTokens(): Record<TokenId, TokenValue>;
22
117
  /** All nodes that depend (directly/indirectly) on start. */
23
118
  affected(start: TokenId): Set<TokenId>;
24
119
  evaluate(candidates: Set<TokenId>): ConstraintIssue[];
@@ -1 +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"}
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;;;;GAIG;AACH,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;IAEf;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;IAE3B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,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;;;;;;;OAOG;IACH,SAAS,IAAI,OAAO,EAAE;IAItB;;;;;;;OAOG;IACH,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC;IAI5C,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 CHANGED
@@ -16,6 +16,28 @@ export class Engine {
16
16
  use(plugin) { this.plugins.push(plugin); return this; }
17
17
  get(id) { return this.values.get(id); }
18
18
  set(id, value) { this.values.set(id, value); }
19
+ /**
20
+ * Get all token IDs in the engine.
21
+ *
22
+ * Phase 3C: Exposed for plugins and adapters.
23
+ * Useful for iterating all tokens or creating a full candidate set.
24
+ *
25
+ * @returns Array of all token IDs
26
+ */
27
+ getAllIds() {
28
+ return Array.from(this.values.keys());
29
+ }
30
+ /**
31
+ * Get flat token map (ID → value).
32
+ *
33
+ * Phase 3C: Exposed to avoid duplicate flattening in CLI/adapters.
34
+ * Returns a plain object suitable for serialization or adapter use.
35
+ *
36
+ * @returns Record mapping token IDs to their current values
37
+ */
38
+ getFlatTokens() {
39
+ return Object.fromEntries(this.values);
40
+ }
19
41
  /** All nodes that depend (directly/indirectly) on start. */
20
42
  affected(start) {
21
43
  const seen = new Set();