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
@@ -1,74 +1,74 @@
1
- // core/constraints/monotonic.ts
2
- import type { ConstraintPlugin } from "../engine.js";
3
- import type { Order } from "../poset.js";
4
-
5
- export function MonotonicPlugin(
6
- orders: Order[],
7
- parse: (v: unknown) => number | null,
8
- ruleId = "monotonic"
9
- ): ConstraintPlugin {
10
- return {
11
- id: ruleId,
12
- evaluate(engine, candidates) {
13
- const issues = [];
14
- for (const [a, op, b] of orders) {
15
- const va = parse(engine.get(a));
16
- const vb = parse(engine.get(b));
17
- if (va == null || vb == null) continue; // skip unparseable
18
- const ok = op === ">=" ? va >= vb : va <= vb;
19
- if (!ok && (candidates.has(a) || candidates.has(b))) {
20
- issues.push({
21
- id: `${a}|${b}`,
22
- rule: "monotonic",
23
- level: "error" as const,
24
- message: `${a} ${op} ${b} violated: ${va} vs ${vb}`
25
- });
26
- }
27
- }
28
- return issues;
29
- }
30
- };
31
- }
32
-
33
- // a minimal parser for "rem"/"px" numbers
34
- export const parseSize = (v: unknown): number | null => {
35
- if (typeof v !== "string") return null;
36
- const m = v.trim().match(/^([0-9.]+)(rem|px)?$/i);
37
- if (!m) return null;
38
- const num = parseFloat(m[1]);
39
- const unit = (m[2] || "rem").toLowerCase();
40
- return unit === "px" ? num : num * 16; // assume 1rem=16px for comparisons
41
- };
42
-
43
- // Parser for unitless numbers (like scale factors)
44
- export const parseNumber = (v: unknown): number | null => {
45
- if (typeof v === "number") return v;
46
- if (typeof v === "string") {
47
- const num = parseFloat(v.trim());
48
- return isNaN(num) ? null : num;
49
- }
50
- return null;
51
- };
52
-
53
- // Parser for color lightness (OKLCH L channel)
54
- export const parseLightness = (v: unknown): number | null => {
55
- if (typeof v !== "string") return null;
56
-
57
- // Match oklch(L C H / A) format
58
- const oklchMatch = v.trim().match(/oklch\s*\(\s*([0-9.]+)\s+/i);
59
- if (oklchMatch) {
60
- return parseFloat(oklchMatch[1]);
61
- }
62
-
63
- // For hex colors, rough approximation (you'd want a proper converter)
64
- const hexMatch = v.trim().match(/^#([0-9a-f]{6})$/i);
65
- if (hexMatch) {
66
- const r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
67
- const g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
68
- const b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
69
- // Simple luminance approximation
70
- return 0.299 * r + 0.587 * g + 0.114 * b;
71
- }
72
-
73
- return null;
74
- };
1
+ // core/constraints/monotonic.ts
2
+ import type { ConstraintPlugin } from "../engine.js";
3
+ import type { Order } from "../poset.js";
4
+
5
+ export function MonotonicPlugin(
6
+ orders: Order[],
7
+ parse: (v: unknown) => number | null,
8
+ ruleId = "monotonic"
9
+ ): ConstraintPlugin {
10
+ return {
11
+ id: ruleId,
12
+ evaluate(engine, candidates) {
13
+ const issues = [];
14
+ for (const [a, op, b] of orders) {
15
+ const va = parse(engine.get(a));
16
+ const vb = parse(engine.get(b));
17
+ if (va == null || vb == null) continue; // skip unparseable
18
+ const ok = op === ">=" ? va >= vb : va <= vb;
19
+ if (!ok && (candidates.has(a) || candidates.has(b))) {
20
+ issues.push({
21
+ id: `${a}|${b}`,
22
+ rule: "monotonic",
23
+ level: "error" as const,
24
+ message: `${a} ${op} ${b} violated: ${va} vs ${vb}`
25
+ });
26
+ }
27
+ }
28
+ return issues;
29
+ }
30
+ };
31
+ }
32
+
33
+ // a minimal parser for "rem"/"px" numbers
34
+ export const parseSize = (v: unknown): number | null => {
35
+ if (typeof v !== "string") return null;
36
+ const m = v.trim().match(/^([0-9.]+)(rem|px)?$/i);
37
+ if (!m) return null;
38
+ const num = parseFloat(m[1]);
39
+ const unit = (m[2] || "rem").toLowerCase();
40
+ return unit === "px" ? num : num * 16; // assume 1rem=16px for comparisons
41
+ };
42
+
43
+ // Parser for unitless numbers (like scale factors)
44
+ export const parseNumber = (v: unknown): number | null => {
45
+ if (typeof v === "number") return v;
46
+ if (typeof v === "string") {
47
+ const num = parseFloat(v.trim());
48
+ return isNaN(num) ? null : num;
49
+ }
50
+ return null;
51
+ };
52
+
53
+ // Parser for color lightness (OKLCH L channel)
54
+ export const parseLightness = (v: unknown): number | null => {
55
+ if (typeof v !== "string") return null;
56
+
57
+ // Match oklch(L C H / A) format
58
+ const oklchMatch = v.trim().match(/oklch\s*\(\s*([0-9.]+)\s+/i);
59
+ if (oklchMatch) {
60
+ return parseFloat(oklchMatch[1]);
61
+ }
62
+
63
+ // For hex colors, rough approximation (you'd want a proper converter)
64
+ const hexMatch = v.trim().match(/^#([0-9a-f]{6})$/i);
65
+ if (hexMatch) {
66
+ const r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
67
+ const g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
68
+ const b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
69
+ // Simple luminance approximation
70
+ return 0.299 * r + 0.587 * g + 0.114 * b;
71
+ }
72
+
73
+ return null;
74
+ };
@@ -1,43 +1,43 @@
1
- import type { ConstraintPlugin } from "../engine.js";
2
-
3
- export type ThresholdRule = {
4
- id: string; // token id to check
5
- op: ">=" | "<="; // comparison
6
- valuePx: number; // threshold in px
7
- where?: string; // optional context label
8
- level?: "error" | "warn"; // default error
9
- };
10
-
11
- const parseSizePx = (v: unknown): number | null => {
12
- if (typeof v !== "string") return null;
13
- const m = v.trim().match(/^([0-9.]+)\s*(px|rem)?$/i);
14
- if (!m) return null;
15
- const n = parseFloat(m[1]);
16
- const unit = (m[2] || "px").toLowerCase();
17
- return unit === "rem" ? n * 16 : n;
18
- };
19
-
20
- export function ThresholdPlugin(rules: ThresholdRule[], ruleId = "threshold"): ConstraintPlugin {
21
- return {
22
- id: ruleId,
23
- evaluate(engine, candidates) {
24
- const issues = [];
25
- for (const r of rules) {
26
- if (!candidates.has(r.id)) continue; // incremental
27
- const px = parseSizePx(engine.get(r.id));
28
- if (px == null) continue;
29
- const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
30
- if (!ok) {
31
- issues.push({
32
- id: r.id,
33
- rule: ruleId,
34
- level: r.level ?? "error",
35
- where: r.where,
36
- message: `${r.id} ${r.op} ${r.valuePx}px violated: ${px}px`
37
- });
38
- }
39
- }
40
- return issues;
41
- }
42
- };
43
- }
1
+ import type { ConstraintPlugin } from "../engine.js";
2
+
3
+ export type ThresholdRule = {
4
+ id: string; // token id to check
5
+ op: ">=" | "<="; // comparison
6
+ valuePx: number; // threshold in px
7
+ where?: string; // optional context label
8
+ level?: "error" | "warn"; // default error
9
+ };
10
+
11
+ const parseSizePx = (v: unknown): number | null => {
12
+ if (typeof v !== "string") return null;
13
+ const m = v.trim().match(/^([0-9.]+)\s*(px|rem)?$/i);
14
+ if (!m) return null;
15
+ const n = parseFloat(m[1]);
16
+ const unit = (m[2] || "px").toLowerCase();
17
+ return unit === "rem" ? n * 16 : n;
18
+ };
19
+
20
+ export function ThresholdPlugin(rules: ThresholdRule[], ruleId = "threshold"): ConstraintPlugin {
21
+ return {
22
+ id: ruleId,
23
+ evaluate(engine, candidates) {
24
+ const issues = [];
25
+ for (const r of rules) {
26
+ if (!candidates.has(r.id)) continue; // incremental
27
+ const px = parseSizePx(engine.get(r.id));
28
+ if (px == null) continue;
29
+ const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
30
+ if (!ok) {
31
+ issues.push({
32
+ id: r.id,
33
+ rule: ruleId,
34
+ level: r.level ?? "error",
35
+ where: r.where,
36
+ message: `${r.id} ${r.op} ${r.valuePx}px violated: ${px}px`
37
+ });
38
+ }
39
+ }
40
+ return issues;
41
+ }
42
+ };
43
+ }
@@ -1,70 +1,70 @@
1
- import type { ConstraintPlugin } from "../engine.js";
2
- import type { TokenId } from "../flatten.js";
3
- import { parseCssColor, compositeOver, relativeLuminance, contrastRatio } from "../color.js";
4
-
5
- export type ContrastPair = {
6
- fg: TokenId;
7
- bg: TokenId;
8
- min: number; // e.g., 4.5
9
- where?: string;
10
- // Optional ultimate backdrop if bg is transparent; token id or literal color.
11
- backdrop?: TokenId | string; // default: "#ffffff"
12
- };
13
-
14
- function resolveColor(engineGet: (id: TokenId)=>unknown, x: TokenId | string): string | undefined {
15
- if (typeof x !== "string") return undefined;
16
- // If it's a CSS color literal, return as is; else treat as token id.
17
- const isLiteral = /^#|^rgb|^hsl|^oklch|^oklab|^transparent/i.test(x);
18
- return isLiteral ? x : String(engineGet(x) ?? "");
19
- }
20
-
21
- export function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin {
22
- return {
23
- id: "wcag-contrast",
24
- evaluate(engine, candidates) {
25
- const issues = [];
26
- for (const p of pairs) {
27
- if (!candidates.has(p.fg) && !candidates.has(p.bg)) continue;
28
-
29
- const fgStr = String(engine.get(p.fg) ?? "");
30
- const bgStr = String(engine.get(p.bg) ?? "");
31
- const fg = parseCssColor(fgStr);
32
- const bgRaw = parseCssColor(bgStr);
33
-
34
- const backdropStr = resolveColor(engine.get.bind(engine), p.backdrop ?? "#ffffff");
35
- const backdrop = parseCssColor(backdropStr || "#ffffff");
36
-
37
- if (!fg || !bgRaw || !backdrop) {
38
- issues.push({
39
- id: `${p.fg}|${p.bg}`,
40
- rule: "wcag-contrast",
41
- level: "warn" as const,
42
- where: p.where,
43
- message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
44
- });
45
- continue;
46
- }
47
-
48
- // Effective background (handle bg alpha)
49
- const effBg = bgRaw.a < 1 ? compositeOver(bgRaw, backdrop) : bgRaw;
50
- // Effective foreground over effective background
51
- const effFg = fg.a < 1 ? compositeOver(fg, effBg) : fg;
52
-
53
- const Lfg = relativeLuminance(effFg);
54
- const Lbg = relativeLuminance(effBg);
55
- const ratio = contrastRatio(Lfg, Lbg);
56
-
57
- if (ratio < p.min) {
58
- issues.push({
59
- id: `${p.fg}|${p.bg}`,
60
- rule: "wcag-contrast",
61
- level: "error" as const,
62
- where: p.where,
63
- message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
64
- });
65
- }
66
- }
67
- return issues;
68
- }
69
- };
70
- }
1
+ import type { ConstraintPlugin } from "../engine.js";
2
+ import type { TokenId } from "../flatten.js";
3
+ import { parseCssColor, compositeOver, relativeLuminance, contrastRatio } from "../color.js";
4
+
5
+ export type ContrastPair = {
6
+ fg: TokenId;
7
+ bg: TokenId;
8
+ min: number; // e.g., 4.5
9
+ where?: string;
10
+ // Optional ultimate backdrop if bg is transparent; token id or literal color.
11
+ backdrop?: TokenId | string; // default: "#ffffff"
12
+ };
13
+
14
+ function resolveColor(engineGet: (id: TokenId)=>unknown, x: TokenId | string): string | undefined {
15
+ if (typeof x !== "string") return undefined;
16
+ // If it's a CSS color literal, return as is; else treat as token id.
17
+ const isLiteral = /^#|^rgb|^hsl|^oklch|^oklab|^transparent/i.test(x);
18
+ return isLiteral ? x : String(engineGet(x) ?? "");
19
+ }
20
+
21
+ export function WcagContrastPlugin(pairs: ContrastPair[]): ConstraintPlugin {
22
+ return {
23
+ id: "wcag-contrast",
24
+ evaluate(engine, candidates) {
25
+ const issues = [];
26
+ for (const p of pairs) {
27
+ if (!candidates.has(p.fg) && !candidates.has(p.bg)) continue;
28
+
29
+ const fgStr = String(engine.get(p.fg) ?? "");
30
+ const bgStr = String(engine.get(p.bg) ?? "");
31
+ const fg = parseCssColor(fgStr);
32
+ const bgRaw = parseCssColor(bgStr);
33
+
34
+ const backdropStr = resolveColor(engine.get.bind(engine), p.backdrop ?? "#ffffff");
35
+ const backdrop = parseCssColor(backdropStr || "#ffffff");
36
+
37
+ if (!fg || !bgRaw || !backdrop) {
38
+ issues.push({
39
+ id: `${p.fg}|${p.bg}`,
40
+ rule: "wcag-contrast",
41
+ level: "warn" as const,
42
+ where: p.where,
43
+ message: `Unparseable color(s): fg="${fgStr}" bg="${bgStr}" backdrop="${backdropStr}"`
44
+ });
45
+ continue;
46
+ }
47
+
48
+ // Effective background (handle bg alpha)
49
+ const effBg = bgRaw.a < 1 ? compositeOver(bgRaw, backdrop) : bgRaw;
50
+ // Effective foreground over effective background
51
+ const effFg = fg.a < 1 ? compositeOver(fg, effBg) : fg;
52
+
53
+ const Lfg = relativeLuminance(effFg);
54
+ const Lbg = relativeLuminance(effBg);
55
+ const ratio = contrastRatio(Lfg, Lbg);
56
+
57
+ if (ratio < p.min) {
58
+ issues.push({
59
+ id: `${p.fg}|${p.bg}`,
60
+ rule: "wcag-contrast",
61
+ level: "error" as const,
62
+ where: p.where,
63
+ message: `Contrast ${ratio.toFixed(2)}:1 < ${p.min}:1`
64
+ });
65
+ }
66
+ }
67
+ return issues;
68
+ }
69
+ };
70
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"cross-axis-config.d.ts","sourceRoot":"","sources":["cross-axis-config.ts"],"names":[],"mappings":"AAQA,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,0CA2HnD"}
1
+ {"version":3,"file":"cross-axis-config.d.ts","sourceRoot":"","sources":["cross-axis-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAUH;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,0CA2HnD"}
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();