design-constraint-validator 2.0.1 → 2.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 (82) hide show
  1. package/README.md +76 -21
  2. package/cli/commands/build.js +1 -1
  3. package/cli/commands/build.ts +1 -1
  4. package/cli/commands/graph.js +2 -2
  5. package/cli/commands/graph.ts +2 -2
  6. package/cli/commands/validate.d.ts.map +1 -1
  7. package/cli/commands/validate.js +40 -6
  8. package/cli/commands/validate.ts +37 -8
  9. package/cli/config-schema.d.ts +39 -0
  10. package/cli/config-schema.d.ts.map +1 -1
  11. package/cli/config-schema.js +4 -2
  12. package/cli/config-schema.ts +4 -2
  13. package/cli/config.d.ts.map +1 -1
  14. package/cli/config.js +8 -2
  15. package/cli/config.ts +8 -2
  16. package/cli/constraint-registry.d.ts +16 -0
  17. package/cli/constraint-registry.d.ts.map +1 -1
  18. package/cli/constraint-registry.js +64 -31
  19. package/cli/constraint-registry.ts +67 -31
  20. package/cli/dcv.js +8 -24
  21. package/cli/dcv.ts +8 -20
  22. package/cli/json-output.d.ts +3 -1
  23. package/cli/json-output.d.ts.map +1 -1
  24. package/cli/json-output.js +11 -4
  25. package/cli/json-output.ts +13 -4
  26. package/cli/types.d.ts +2 -0
  27. package/cli/types.d.ts.map +1 -1
  28. package/cli/types.ts +2 -0
  29. package/cli/validate-api.d.ts +40 -0
  30. package/cli/validate-api.d.ts.map +1 -0
  31. package/cli/validate-api.js +85 -0
  32. package/cli/validate-api.ts +126 -0
  33. package/core/breakpoints.d.ts +8 -2
  34. package/core/breakpoints.d.ts.map +1 -1
  35. package/core/breakpoints.js +24 -3
  36. package/core/breakpoints.ts +22 -3
  37. package/core/color.js +4 -4
  38. package/core/color.ts +4 -4
  39. package/core/constraints/monotonic-lightness.d.ts.map +1 -1
  40. package/core/constraints/monotonic-lightness.js +9 -5
  41. package/core/constraints/monotonic-lightness.ts +9 -4
  42. package/core/constraints/wcag.d.ts.map +1 -1
  43. package/core/constraints/wcag.js +6 -0
  44. package/core/constraints/wcag.ts +6 -0
  45. package/core/dtcg.d.ts +38 -0
  46. package/core/dtcg.d.ts.map +1 -0
  47. package/core/dtcg.js +88 -0
  48. package/core/dtcg.ts +102 -0
  49. package/core/engine.d.ts +6 -0
  50. package/core/engine.d.ts.map +1 -1
  51. package/core/engine.ts +7 -0
  52. package/core/flatten.d.ts +5 -3
  53. package/core/flatten.d.ts.map +1 -1
  54. package/core/flatten.js +24 -10
  55. package/core/flatten.ts +39 -16
  56. package/core/image-export.d.ts.map +1 -1
  57. package/core/image-export.js +10 -7
  58. package/core/image-export.ts +9 -6
  59. package/core/index.d.ts +2 -0
  60. package/core/index.d.ts.map +1 -1
  61. package/core/index.js +4 -0
  62. package/core/index.ts +6 -0
  63. package/core/why.d.ts +1 -1
  64. package/core/why.d.ts.map +1 -1
  65. package/core/why.ts +1 -1
  66. package/mcp/contracts.d.ts +118 -0
  67. package/mcp/contracts.d.ts.map +1 -0
  68. package/mcp/contracts.js +30 -0
  69. package/mcp/contracts.ts +51 -0
  70. package/mcp/index.d.ts +9 -0
  71. package/mcp/index.d.ts.map +1 -0
  72. package/mcp/index.js +32 -0
  73. package/mcp/index.ts +70 -0
  74. package/mcp/tools.d.ts +52 -0
  75. package/mcp/tools.d.ts.map +1 -0
  76. package/mcp/tools.js +172 -0
  77. package/mcp/tools.ts +254 -0
  78. package/package.json +41 -26
  79. package/server.json +21 -0
  80. package/cli/constraints-loader.d.ts.map +0 -1
  81. package/cli/engine-helpers.d.ts.map +0 -1
  82. package/core/cross-axis-config.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAKzB,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAEd,CAAC;AAEjB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAGZ,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,KAAK,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAM3F"}
1
+ {"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;EAMzB,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAEd,CAAC;AAEjB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAGZ,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,KAAK,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAO3F"}
@@ -3,7 +3,8 @@ export const WcagRuleSchema = z.object({
3
3
  foreground: z.string(),
4
4
  background: z.string(),
5
5
  ratio: z.number().positive().optional(),
6
- description: z.string().optional()
6
+ description: z.string().optional(),
7
+ backdrop: z.string().optional()
7
8
  });
8
9
  export const ConstraintsSchema = z.object({
9
10
  wcag: z.array(WcagRuleSchema).optional()
@@ -15,7 +16,8 @@ export const DcvConfigSchema = z.object({
15
16
  export function validateConfig(raw) {
16
17
  const res = DcvConfigSchema.safeParse(raw);
17
18
  if (!res.success) {
18
- return { errors: res.error.errors.map(e => `${e.path.join('.') || '<root>'}: ${e.message}`) };
19
+ // zod v4 renamed ZodError.errors -> .issues (the .errors getter was removed).
20
+ return { errors: res.error.issues.map((e) => `${e.path.join('.') || '<root>'}: ${e.message}`) };
19
21
  }
20
22
  return { value: res.data };
21
23
  }
@@ -4,7 +4,8 @@ export const WcagRuleSchema = z.object({
4
4
  foreground: z.string(),
5
5
  background: z.string(),
6
6
  ratio: z.number().positive().optional(),
7
- description: z.string().optional()
7
+ description: z.string().optional(),
8
+ backdrop: z.string().optional()
8
9
  });
9
10
 
10
11
  export const ConstraintsSchema = z.object({
@@ -21,7 +22,8 @@ export type DcvConfigParsed = z.infer<typeof DcvConfigSchema>;
21
22
  export function validateConfig(raw: unknown): { value?: DcvConfigParsed; errors?: string[] } {
22
23
  const res = DcvConfigSchema.safeParse(raw);
23
24
  if (!res.success) {
24
- return { errors: res.error.errors.map(e => `${e.path.join('.')||'<root>'}: ${e.message}`) };
25
+ // zod v4 renamed ZodError.errors -> .issues (the .errors getter was removed).
26
+ return { errors: res.error.issues.map((e) => `${e.path.join('.') || '<root>'}: ${e.message}`) };
25
27
  }
26
28
  return { value: res.data };
27
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CA6BzE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAkCzE"}
package/cli/config.js CHANGED
@@ -1,20 +1,26 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
+ import { basename, extname } from 'node:path';
2
3
  import { validateConfig } from './config-schema.js';
3
4
  import { ok, err } from './result.js';
4
5
  export function loadConfig(configPath) {
6
+ if (configPath && !existsSync(configPath)) {
7
+ return err(`Config file not found: ${configPath}`);
8
+ }
5
9
  const candidates = configPath ? [configPath] : [
6
10
  'dcv.config.json',
7
- 'dcv.config.js',
8
11
  '.dcvrc.json',
9
12
  'package.json'
10
13
  ];
11
14
  for (const p of candidates) {
12
15
  if (!existsSync(p))
13
16
  continue;
17
+ if (extname(p) === '.js') {
18
+ return err(`Unsupported config file ${p}: use JSON config (dcv.config.json, .dcvrc.json, or package.json "dcv").`);
19
+ }
14
20
  try {
15
21
  const rawTxt = readFileSync(p, 'utf8');
16
22
  let raw = JSON.parse(rawTxt);
17
- if (p === 'package.json' && raw && typeof raw === 'object') {
23
+ if (basename(p) === 'package.json' && raw && typeof raw === 'object') {
18
24
  const pkg = raw;
19
25
  if ('dcv' in pkg) {
20
26
  raw = pkg.dcv;
package/cli/config.ts CHANGED
@@ -1,21 +1,27 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
+ import { basename, extname } from 'node:path';
2
3
  import { validateConfig } from './config-schema.js';
3
4
  import { ok, err, type Result } from './result.js';
4
5
  import type { DcvConfig } from './types.js';
5
6
 
6
7
  export function loadConfig(configPath?: string): Result<DcvConfig, string> {
8
+ if (configPath && !existsSync(configPath)) {
9
+ return err(`Config file not found: ${configPath}`);
10
+ }
7
11
  const candidates = configPath ? [configPath] : [
8
12
  'dcv.config.json',
9
- 'dcv.config.js',
10
13
  '.dcvrc.json',
11
14
  'package.json'
12
15
  ];
13
16
  for (const p of candidates) {
14
17
  if (!existsSync(p)) continue;
18
+ if (extname(p) === '.js') {
19
+ return err(`Unsupported config file ${p}: use JSON config (dcv.config.json, .dcvrc.json, or package.json "dcv").`);
20
+ }
15
21
  try {
16
22
  const rawTxt = readFileSync(p, 'utf8');
17
23
  let raw: unknown = JSON.parse(rawTxt);
18
- if (p === 'package.json' && raw && typeof raw === 'object') {
24
+ if (basename(p) === 'package.json' && raw && typeof raw === 'object') {
19
25
  const pkg = raw as Record<string, unknown>;
20
26
  if ('dcv' in pkg) {
21
27
  raw = pkg.dcv;
@@ -67,6 +67,8 @@ export type AttachOptions = {
67
67
  knownIds: Set<string>;
68
68
  crossAxisDebug?: boolean;
69
69
  };
70
+ export declare const DEFAULT_WCAG_PAIRS: WcagRule[];
71
+ export declare const DEFAULT_THRESHOLDS: ThresholdRule[];
70
72
  /**
71
73
  * Discover all constraint sources for a given configuration and breakpoint.
72
74
  *
@@ -98,4 +100,18 @@ export declare function attachConstraints(engine: Engine, sources: ConstraintSou
98
100
  * @param attachOpts Attachment options
99
101
  */
100
102
  export declare function setupConstraints(engine: Engine, discoveryOpts: DiscoveryOptions, attachOpts: AttachOptions): ConstraintSource[];
103
+ /**
104
+ * Collect the token ids referenced by the active constraint sources, plus
105
+ * whether that coverage is fully enumerable.
106
+ *
107
+ * Used to detect the silent-pass case: a token file that validates with zero
108
+ * errors only because no active constraint references any of its tokens. Cross-
109
+ * axis rule ids are not enumerated here, so when any cross-axis source is present
110
+ * `coverageKnown` is false and callers must stay conservative (never claim
111
+ * "nothing was checked" when they cannot be sure).
112
+ */
113
+ export declare function collectReferencedIds(sources: ConstraintSource[]): {
114
+ ids: Set<string>;
115
+ coverageKnown: boolean;
116
+ };
101
117
  //# sourceMappingURL=constraint-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constraint-registry.d.ts","sourceRoot":"","sources":["constraint-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAY5C,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,UAAU,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAMF;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CA8F9E;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAsFxG;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,gBAAgB,EAC/B,UAAU,EAAE,aAAa,GACxB,gBAAgB,EAAE,CAIpB"}
1
+ {"version":3,"file":"constraint-registry.d.ts","sourceRoot":"","sources":["constraint-registry.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,16 @@ import { WcagContrastPlugin } from '../core/constraints/wcag.js';
18
18
  import { ThresholdPlugin } from '../core/constraints/threshold.js';
19
19
  import { CrossAxisPlugin } from '../core/constraints/cross-axis.js';
20
20
  import { loadCrossAxisRules } from './cross-axis-loader.js';
21
+ // Built-in default constraint rules. Shared by attachConstraints (to register
22
+ // plugins) and collectReferencedIds (to compute coverage) so the two never drift.
23
+ export const DEFAULT_WCAG_PAIRS = [
24
+ { fg: 'color.role.text.default', bg: 'color.role.bg.surface', min: 4.5, where: 'Body text on surface' },
25
+ { fg: 'color.role.accent.default', bg: 'color.role.bg.surface', min: 3.0, where: 'Accent on surface' },
26
+ { fg: 'color.role.focus.ring', bg: 'color.role.bg.surface', min: 3.0, where: 'Focus ring on surface', backdrop: '#ffffff' },
27
+ ];
28
+ export const DEFAULT_THRESHOLDS = [
29
+ { id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' },
30
+ ];
21
31
  // ============================================================================
22
32
  // Discovery
23
33
  // ============================================================================
@@ -132,42 +142,13 @@ export function attachConstraints(engine, sources, opts) {
132
142
  switch (source.type) {
133
143
  case 'builtin-wcag': {
134
144
  if (source.enabled) {
135
- const defaultWcagPairs = [
136
- {
137
- fg: 'color.role.text.default',
138
- bg: 'color.role.bg.surface',
139
- min: 4.5,
140
- where: 'Body text on surface',
141
- },
142
- {
143
- fg: 'color.role.accent.default',
144
- bg: 'color.role.bg.surface',
145
- min: 3.0,
146
- where: 'Accent on surface',
147
- },
148
- {
149
- fg: 'color.role.focus.ring',
150
- bg: 'color.role.bg.surface',
151
- min: 3.0,
152
- where: 'Focus ring on surface',
153
- backdrop: '#ffffff',
154
- },
155
- ];
156
- engine.use(WcagContrastPlugin(defaultWcagPairs));
145
+ engine.use(WcagContrastPlugin(DEFAULT_WCAG_PAIRS));
157
146
  }
158
147
  break;
159
148
  }
160
149
  case 'builtin-threshold': {
161
150
  if (source.enabled) {
162
- const defaultThresholds = [
163
- {
164
- id: 'control.size.min',
165
- op: '>=',
166
- valuePx: 44,
167
- where: 'Touch target (WCAG / Apple HIG)',
168
- },
169
- ];
170
- engine.use(ThresholdPlugin(defaultThresholds, 'threshold'));
151
+ engine.use(ThresholdPlugin(DEFAULT_THRESHOLDS, 'threshold'));
171
152
  }
172
153
  break;
173
154
  }
@@ -223,3 +204,55 @@ export function setupConstraints(engine, discoveryOpts, attachOpts) {
223
204
  attachConstraints(engine, sources, attachOpts);
224
205
  return sources;
225
206
  }
207
+ /**
208
+ * Collect the token ids referenced by the active constraint sources, plus
209
+ * whether that coverage is fully enumerable.
210
+ *
211
+ * Used to detect the silent-pass case: a token file that validates with zero
212
+ * errors only because no active constraint references any of its tokens. Cross-
213
+ * axis rule ids are not enumerated here, so when any cross-axis source is present
214
+ * `coverageKnown` is false and callers must stay conservative (never claim
215
+ * "nothing was checked" when they cannot be sure).
216
+ */
217
+ export function collectReferencedIds(sources) {
218
+ const ids = new Set();
219
+ let coverageKnown = true;
220
+ const addOrders = (orders) => {
221
+ for (const [a, , b] of orders) {
222
+ ids.add(a);
223
+ ids.add(b);
224
+ }
225
+ };
226
+ for (const source of sources) {
227
+ switch (source.type) {
228
+ case 'builtin-wcag':
229
+ for (const p of DEFAULT_WCAG_PAIRS) {
230
+ ids.add(p.fg);
231
+ ids.add(p.bg);
232
+ }
233
+ break;
234
+ case 'builtin-threshold':
235
+ for (const t of DEFAULT_THRESHOLDS)
236
+ ids.add(t.id);
237
+ break;
238
+ case 'config-wcag':
239
+ for (const r of source.rules) {
240
+ ids.add(r.fg);
241
+ ids.add(r.bg);
242
+ }
243
+ break;
244
+ case 'custom-threshold':
245
+ for (const r of source.rules)
246
+ ids.add(r.id);
247
+ break;
248
+ case 'order-file':
249
+ case 'lightness-file':
250
+ addOrders(source.orders);
251
+ break;
252
+ case 'cross-axis-file':
253
+ coverageKnown = false;
254
+ break;
255
+ }
256
+ }
257
+ return { ids, coverageKnown };
258
+ }
@@ -69,6 +69,18 @@ export type AttachOptions = {
69
69
  crossAxisDebug?: boolean;
70
70
  };
71
71
 
72
+ // Built-in default constraint rules. Shared by attachConstraints (to register
73
+ // plugins) and collectReferencedIds (to compute coverage) so the two never drift.
74
+ export const DEFAULT_WCAG_PAIRS: WcagRule[] = [
75
+ { fg: 'color.role.text.default', bg: 'color.role.bg.surface', min: 4.5, where: 'Body text on surface' },
76
+ { fg: 'color.role.accent.default', bg: 'color.role.bg.surface', min: 3.0, where: 'Accent on surface' },
77
+ { fg: 'color.role.focus.ring', bg: 'color.role.bg.surface', min: 3.0, where: 'Focus ring on surface', backdrop: '#ffffff' },
78
+ ];
79
+
80
+ export const DEFAULT_THRESHOLDS: ThresholdRule[] = [
81
+ { id: 'control.size.min', op: '>=', valuePx: 44, where: 'Touch target (WCAG / Apple HIG)' },
82
+ ];
83
+
72
84
  // ============================================================================
73
85
  // Discovery
74
86
  // ============================================================================
@@ -200,43 +212,14 @@ export function attachConstraints(engine: Engine, sources: ConstraintSource[], o
200
212
  switch (source.type) {
201
213
  case 'builtin-wcag': {
202
214
  if (source.enabled) {
203
- const defaultWcagPairs: WcagRule[] = [
204
- {
205
- fg: 'color.role.text.default',
206
- bg: 'color.role.bg.surface',
207
- min: 4.5,
208
- where: 'Body text on surface',
209
- },
210
- {
211
- fg: 'color.role.accent.default',
212
- bg: 'color.role.bg.surface',
213
- min: 3.0,
214
- where: 'Accent on surface',
215
- },
216
- {
217
- fg: 'color.role.focus.ring',
218
- bg: 'color.role.bg.surface',
219
- min: 3.0,
220
- where: 'Focus ring on surface',
221
- backdrop: '#ffffff',
222
- },
223
- ];
224
- engine.use(WcagContrastPlugin(defaultWcagPairs));
215
+ engine.use(WcagContrastPlugin(DEFAULT_WCAG_PAIRS));
225
216
  }
226
217
  break;
227
218
  }
228
219
 
229
220
  case 'builtin-threshold': {
230
221
  if (source.enabled) {
231
- const defaultThresholds: ThresholdRule[] = [
232
- {
233
- id: 'control.size.min',
234
- op: '>=',
235
- valuePx: 44,
236
- where: 'Touch target (WCAG / Apple HIG)',
237
- },
238
- ];
239
- engine.use(ThresholdPlugin(defaultThresholds, 'threshold'));
222
+ engine.use(ThresholdPlugin(DEFAULT_THRESHOLDS, 'threshold'));
240
223
  }
241
224
  break;
242
225
  }
@@ -302,3 +285,56 @@ export function setupConstraints(
302
285
  attachConstraints(engine, sources, attachOpts);
303
286
  return sources;
304
287
  }
288
+
289
+ /**
290
+ * Collect the token ids referenced by the active constraint sources, plus
291
+ * whether that coverage is fully enumerable.
292
+ *
293
+ * Used to detect the silent-pass case: a token file that validates with zero
294
+ * errors only because no active constraint references any of its tokens. Cross-
295
+ * axis rule ids are not enumerated here, so when any cross-axis source is present
296
+ * `coverageKnown` is false and callers must stay conservative (never claim
297
+ * "nothing was checked" when they cannot be sure).
298
+ */
299
+ export function collectReferencedIds(sources: ConstraintSource[]): { ids: Set<string>; coverageKnown: boolean } {
300
+ const ids = new Set<string>();
301
+ let coverageKnown = true;
302
+ const addOrders = (orders: OrderRule[]) => {
303
+ for (const [a, , b] of orders) {
304
+ ids.add(a);
305
+ ids.add(b);
306
+ }
307
+ };
308
+
309
+ for (const source of sources) {
310
+ switch (source.type) {
311
+ case 'builtin-wcag':
312
+ for (const p of DEFAULT_WCAG_PAIRS) {
313
+ ids.add(p.fg);
314
+ ids.add(p.bg);
315
+ }
316
+ break;
317
+ case 'builtin-threshold':
318
+ for (const t of DEFAULT_THRESHOLDS) ids.add(t.id);
319
+ break;
320
+ case 'config-wcag':
321
+ for (const r of source.rules) {
322
+ ids.add(r.fg);
323
+ ids.add(r.bg);
324
+ }
325
+ break;
326
+ case 'custom-threshold':
327
+ for (const r of source.rules) ids.add(r.id);
328
+ break;
329
+ case 'order-file':
330
+ case 'lightness-file':
331
+ addOrders(source.orders);
332
+ break;
333
+ case 'cross-axis-file':
334
+ coverageKnown = false;
335
+ break;
336
+ }
337
+ }
338
+
339
+ return { ids, coverageKnown };
340
+ }
package/cli/dcv.js CHANGED
@@ -2,30 +2,12 @@
2
2
  // Clean minimal DCV CLI entrypoint
3
3
  import yargs from 'yargs/yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
- import { spawnSync } from 'node:child_process';
6
- import { createRequire } from 'module';
7
- import path from 'node:path';
8
5
  import { setCommand, buildCommand, validateCommand, graphCommand, whyCommand, patchCommand, patchApplyCommand } from './commands/index.js';
9
- // Early intercept: experimental graph diff helper
10
- (() => {
11
- const args = process.argv.slice(2);
12
- if (args[0] === 'graph' && args[1] === 'diff') {
13
- const pass = args.slice(2);
14
- const require = createRequire(import.meta.url);
15
- const tsx = (() => { try {
16
- return require.resolve('tsx/dist/cli.mjs');
17
- }
18
- catch {
19
- return path.resolve('node_modules/tsx/dist/cli.mjs');
20
- } })();
21
- const r = spawnSync(process.execPath, [tsx, path.resolve('scripts/graph-diff.ts'), ...pass], { stdio: 'inherit', env: process.env });
22
- process.exit(r.status ?? 0);
23
- }
24
- })();
25
6
  const cli = yargs(hideBin(process.argv))
26
7
  .scriptName('dcv')
27
8
  .parserConfiguration({ 'camel-case-expansion': false })
28
- .option('quiet', { type: 'boolean' });
9
+ .option('quiet', { type: 'boolean' })
10
+ .option('config', { type: 'string', describe: 'Path to JSON config file' });
29
11
  cli.command('set <expressions..>', 'Set token values', y => y
30
12
  .positional('expressions', { type: 'string', array: true })
31
13
  .option('dry-run', { type: 'boolean', default: false })
@@ -43,14 +25,16 @@ cli.command('build', 'Build token outputs', y => y
43
25
  .option('mapper', { type: 'string' })
44
26
  .option('theme', { type: 'string' })
45
27
  .option('dry-run', { type: 'boolean', default: false })
46
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }), a => buildCommand(a));
47
- cli.command('validate', 'Validate constraints', y => y
28
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }), a => buildCommand(a));
29
+ cli.command('validate [tokens-path]', 'Validate constraints', y => y
30
+ .positional('tokens-path', { type: 'string', describe: 'Path to a tokens file (positional alias for --tokens)' })
31
+ .option('constraints-dir', { type: 'string', describe: 'Directory holding order / cross-axis constraint files (default: themes)' })
48
32
  .option('fail-on', { type: 'string', choices: ['off', 'warn', 'error'], default: 'error' })
49
33
  .option('summary', { type: 'string', choices: ['none', 'table', 'json'], default: 'none' })
50
34
  .option('format', { type: 'string', choices: ['text', 'json'], default: 'text', describe: 'Output format' })
51
35
  .option('output', { type: 'string', describe: 'Write JSON output to file' })
52
36
  .option('receipt', { type: 'string', describe: 'Generate validation receipt with audit trail' })
53
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' })
37
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' })
54
38
  .option('theme', { type: 'string', describe: 'Apply named theme tokens before validation' })
55
39
  .option('breakpoint', { type: 'string' })
56
40
  .option('all-breakpoints', { type: 'boolean' })
@@ -70,7 +54,7 @@ cli.command('graph', 'Generate dependency / constraint graph', y => y
70
54
  .option('min-severity', { type: 'string', choices: ['warn', 'error'], default: 'warn' })
71
55
  .option('focus', { type: 'string' })
72
56
  .option('radius', { type: 'number', default: 1 })
73
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }), a => graphCommand(a));
57
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }), a => graphCommand(a));
74
58
  cli.command('why <tokenId>', 'Explain token provenance', y => y
75
59
  .positional('tokenId', { type: 'string', demandOption: true })
76
60
  .option('format', { type: 'string', choices: ['json', 'table'], default: 'json' })
package/cli/dcv.ts CHANGED
@@ -2,28 +2,14 @@
2
2
  // Clean minimal DCV CLI entrypoint
3
3
  import yargs from 'yargs/yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
- import { spawnSync } from 'node:child_process';
6
- import { createRequire } from 'module';
7
- import path from 'node:path';
8
5
  import { type SetOptions, type BuildOptions, type ValidateOptions, type GraphOptions, type WhyOptions, type PatchOptions, type PatchApplyOptions } from './types.js';
9
6
  import { setCommand, buildCommand, validateCommand, graphCommand, whyCommand, patchCommand, patchApplyCommand } from './commands/index.js';
10
7
 
11
- // Early intercept: experimental graph diff helper
12
- (() => {
13
- const args = process.argv.slice(2);
14
- if (args[0] === 'graph' && args[1] === 'diff') {
15
- const pass = args.slice(2);
16
- const require = createRequire(import.meta.url);
17
- const tsx = (() => { try { return require.resolve('tsx/dist/cli.mjs'); } catch { return path.resolve('node_modules/tsx/dist/cli.mjs'); }})();
18
- const r = spawnSync(process.execPath, [tsx, path.resolve('scripts/graph-diff.ts'), ...pass], { stdio: 'inherit', env: process.env });
19
- process.exit(r.status ?? 0);
20
- }
21
- })();
22
-
23
8
  const cli = yargs(hideBin(process.argv))
24
9
  .scriptName('dcv')
25
10
  .parserConfiguration({ 'camel-case-expansion': false })
26
- .option('quiet', { type: 'boolean' });
11
+ .option('quiet', { type: 'boolean' })
12
+ .option('config', { type: 'string', describe: 'Path to JSON config file' });
27
13
 
28
14
  cli.command<SetOptions>('set <expressions..>', 'Set token values', y => y
29
15
  .positional('expressions', { type: 'string', array: true })
@@ -45,17 +31,19 @@ cli.command<BuildOptions>('build', 'Build token outputs', y => y
45
31
  .option('mapper', { type: 'string' })
46
32
  .option('theme', { type: 'string' })
47
33
  .option('dry-run', { type: 'boolean', default: false })
48
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
34
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }),
49
35
  a => buildCommand(a)
50
36
  );
51
37
 
52
- cli.command<ValidateOptions>('validate', 'Validate constraints', y => y
38
+ cli.command<ValidateOptions>('validate [tokens-path]', 'Validate constraints', y => y
39
+ .positional('tokens-path', { type: 'string', describe: 'Path to a tokens file (positional alias for --tokens)' })
40
+ .option('constraints-dir', { type: 'string', describe: 'Directory holding order / cross-axis constraint files (default: themes)' })
53
41
  .option('fail-on', { type: 'string', choices: ['off','warn','error'], default: 'error' })
54
42
  .option('summary', { type: 'string', choices: ['none','table','json'], default: 'none' })
55
43
  .option('format', { type: 'string', choices: ['text','json'], default: 'text', describe: 'Output format' })
56
44
  .option('output', { type: 'string', describe: 'Write JSON output to file' })
57
45
  .option('receipt', { type: 'string', describe: 'Generate validation receipt with audit trail' })
58
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' })
46
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' })
59
47
  .option('theme', { type: 'string', describe: 'Apply named theme tokens before validation' })
60
48
  .option('breakpoint', { type: 'string' })
61
49
  .option('all-breakpoints', { type: 'boolean' })
@@ -78,7 +66,7 @@ cli.command<GraphOptions>('graph', 'Generate dependency / constraint graph', y =
78
66
  .option('min-severity', { type: 'string', choices: ['warn','error'], default: 'warn' })
79
67
  .option('focus', { type: 'string' })
80
68
  .option('radius', { type: 'number', default: 1 })
81
- .option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
69
+ .option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }),
82
70
  a => graphCommand(a)
83
71
  );
84
72
 
@@ -21,6 +21,8 @@ export interface ValidationResult {
21
21
  };
22
22
  violations: ConstraintViolation[];
23
23
  warnings?: ConstraintViolation[];
24
+ /** Set when tokens were validated but no active constraint referenced any of them. */
25
+ note?: string;
24
26
  stats: {
25
27
  durationMs: number;
26
28
  engineVersion: string;
@@ -57,7 +59,7 @@ export declare function formatViolation(issue: ConstraintIssue): ConstraintViola
57
59
  /**
58
60
  * Generate a ValidationResult from collected issues
59
61
  */
60
- export declare function createValidationResult(errors: ConstraintIssue[], warnings: ConstraintIssue[], durationMs: number, engineVersion: string): ValidationResult;
62
+ export declare function createValidationResult(errors: ConstraintIssue[], warnings: ConstraintIssue[], durationMs: number, engineVersion: string, note?: string): ValidationResult;
61
63
  /**
62
64
  * Generate a ValidationReceipt with full audit trail
63
65
  */
@@ -1 +1 @@
1
- {"version":3,"file":"json-output.d.ts","sourceRoot":"","sources":["json-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMzD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,WAAW,EAAE;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;QACjC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAiB3E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,EAAE,EACzB,QAAQ,EAAE,eAAe,EAAE,EAC3B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,gBAAgB,CAoBlB;AAcD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAC/B,iBAAiB,CAoCnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CASxE"}
1
+ {"version":3,"file":"json-output.d.ts","sourceRoot":"","sources":["json-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMzD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,WAAW,EAAE;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;QACjC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAsB3E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,EAAE,EACzB,QAAQ,EAAE,eAAe,EAAE,EAC3B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,MAAM,GACZ,gBAAgB,CAqBlB;AAcD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAC/B,iBAAiB,CAoCnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CASxE"}
@@ -11,19 +11,25 @@ export function formatViolation(issue) {
11
11
  level: issue.level === 'error' ? 'error' : 'warn',
12
12
  message: issue.message,
13
13
  };
14
- if (issue.id) {
14
+ if (issue.involvedTokens?.length) {
15
+ violation.nodes = issue.involvedTokens;
16
+ }
17
+ else if (issue.id) {
15
18
  violation.nodes = [issue.id];
16
19
  }
17
20
  // Add context from where field or other metadata
18
- if (issue.where) {
19
- violation.context = { where: issue.where };
21
+ if (issue.where || issue.metadata) {
22
+ violation.context = {
23
+ ...(issue.where ? { where: issue.where } : {}),
24
+ ...(issue.metadata ?? {}),
25
+ };
20
26
  }
21
27
  return violation;
22
28
  }
23
29
  /**
24
30
  * Generate a ValidationResult from collected issues
25
31
  */
26
- export function createValidationResult(errors, warnings, durationMs, engineVersion) {
32
+ export function createValidationResult(errors, warnings, durationMs, engineVersion, note) {
27
33
  const violations = errors.map(formatViolation);
28
34
  const warningViolations = warnings.map(formatViolation);
29
35
  return {
@@ -35,6 +41,7 @@ export function createValidationResult(errors, warnings, durationMs, engineVersi
35
41
  },
36
42
  violations,
37
43
  warnings: warningViolations.length > 0 ? warningViolations : undefined,
44
+ note,
38
45
  stats: {
39
46
  durationMs: Math.round(durationMs),
40
47
  engineVersion,
@@ -27,6 +27,8 @@ export interface ValidationResult {
27
27
  };
28
28
  violations: ConstraintViolation[];
29
29
  warnings?: ConstraintViolation[];
30
+ /** Set when tokens were validated but no active constraint referenced any of them. */
31
+ note?: string;
30
32
  stats: {
31
33
  durationMs: number;
32
34
  engineVersion: string;
@@ -68,13 +70,18 @@ export function formatViolation(issue: ConstraintIssue): ConstraintViolation {
68
70
  message: issue.message,
69
71
  };
70
72
 
71
- if (issue.id) {
73
+ if (issue.involvedTokens?.length) {
74
+ violation.nodes = issue.involvedTokens;
75
+ } else if (issue.id) {
72
76
  violation.nodes = [issue.id];
73
77
  }
74
78
 
75
79
  // Add context from where field or other metadata
76
- if (issue.where) {
77
- violation.context = { where: issue.where };
80
+ if (issue.where || issue.metadata) {
81
+ violation.context = {
82
+ ...(issue.where ? { where: issue.where } : {}),
83
+ ...(issue.metadata ?? {}),
84
+ };
78
85
  }
79
86
 
80
87
  return violation;
@@ -87,7 +94,8 @@ export function createValidationResult(
87
94
  errors: ConstraintIssue[],
88
95
  warnings: ConstraintIssue[],
89
96
  durationMs: number,
90
- engineVersion: string
97
+ engineVersion: string,
98
+ note?: string
91
99
  ): ValidationResult {
92
100
  const violations = errors.map(formatViolation);
93
101
  const warningViolations = warnings.map(formatViolation);
@@ -101,6 +109,7 @@ export function createValidationResult(
101
109
  },
102
110
  violations,
103
111
  warnings: warningViolations.length > 0 ? warningViolations : undefined,
112
+ note,
104
113
  stats: {
105
114
  durationMs: Math.round(durationMs),
106
115
  engineVersion,
package/cli/types.d.ts CHANGED
@@ -45,6 +45,8 @@ export interface BuildOptions extends GlobalOptions {
45
45
  export interface ValidateOptions extends GlobalOptions {
46
46
  strict?: boolean;
47
47
  constraints?: string[];
48
+ 'tokens-path'?: string;
49
+ 'constraints-dir'?: string;
48
50
  perf?: boolean;
49
51
  budgetTotalMs?: number;
50
52
  budgetPerBpMs?: number;