design-constraint-validator 2.0.1 → 2.2.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/README.md +89 -23
- package/cli/commands/build.d.ts.map +1 -1
- package/cli/commands/build.js +32 -24
- package/cli/commands/build.ts +26 -17
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +35 -18
- package/cli/commands/graph.ts +30 -17
- package/cli/commands/patch-apply.d.ts.map +1 -1
- package/cli/commands/patch-apply.js +4 -1
- package/cli/commands/patch-apply.ts +4 -1
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +18 -19
- package/cli/commands/set.ts +19 -19
- package/cli/commands/utils.d.ts +1 -0
- package/cli/commands/utils.d.ts.map +1 -1
- package/cli/commands/utils.js +20 -1
- package/cli/commands/utils.ts +23 -1
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +45 -23
- package/cli/commands/validate.ts +47 -26
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +22 -10
- package/cli/commands/why.ts +20 -9
- package/cli/config-schema.d.ts +171 -166
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +29 -7
- package/cli/config-schema.ts +31 -7
- package/cli/config.d.ts.map +1 -1
- package/cli/config.js +8 -2
- package/cli/config.ts +8 -2
- package/cli/constraint-registry.d.ts +16 -0
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +115 -44
- package/cli/constraint-registry.ts +118 -47
- package/cli/cross-axis-loader.d.ts +62 -0
- package/cli/cross-axis-loader.d.ts.map +1 -1
- package/cli/cross-axis-loader.js +186 -31
- package/cli/cross-axis-loader.ts +199 -24
- package/cli/dcv.js +31 -25
- package/cli/dcv.ts +31 -21
- package/cli/json-output.d.ts +3 -1
- package/cli/json-output.d.ts.map +1 -1
- package/cli/json-output.js +11 -4
- package/cli/json-output.ts +13 -4
- package/cli/types.d.ts +21 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +25 -10
- package/cli/validate-api.d.ts +40 -0
- package/cli/validate-api.d.ts.map +1 -0
- package/cli/validate-api.js +90 -0
- package/cli/validate-api.ts +131 -0
- package/core/breakpoints.d.ts +8 -2
- package/core/breakpoints.d.ts.map +1 -1
- package/core/breakpoints.js +24 -3
- package/core/breakpoints.ts +22 -3
- package/core/color.js +4 -4
- package/core/color.ts +4 -4
- package/core/constraints/cross-axis.d.ts.map +1 -1
- package/core/constraints/cross-axis.js +37 -9
- package/core/constraints/cross-axis.ts +37 -9
- package/core/constraints/monotonic-lightness.d.ts.map +1 -1
- package/core/constraints/monotonic-lightness.js +9 -5
- package/core/constraints/monotonic-lightness.ts +9 -4
- package/core/constraints/monotonic.d.ts.map +1 -1
- package/core/constraints/monotonic.js +32 -8
- package/core/constraints/monotonic.ts +29 -8
- package/core/constraints/threshold.d.ts.map +1 -1
- package/core/constraints/threshold.js +24 -4
- package/core/constraints/threshold.ts +23 -4
- package/core/constraints/wcag.d.ts.map +1 -1
- package/core/constraints/wcag.js +7 -1
- package/core/constraints/wcag.ts +7 -1
- package/core/dtcg.d.ts +38 -0
- package/core/dtcg.d.ts.map +1 -0
- package/core/dtcg.js +88 -0
- package/core/dtcg.ts +102 -0
- package/core/engine.d.ts +6 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.ts +7 -0
- package/core/flatten.d.ts +5 -3
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +32 -10
- package/core/flatten.ts +48 -16
- package/core/image-export.d.ts.map +1 -1
- package/core/image-export.js +10 -7
- package/core/image-export.ts +9 -6
- package/core/index.d.ts +2 -0
- package/core/index.d.ts.map +1 -1
- package/core/index.js +4 -0
- package/core/index.ts +6 -0
- package/core/poset.d.ts +6 -1
- package/core/poset.d.ts.map +1 -1
- package/core/poset.js +7 -2
- package/core/poset.ts +7 -2
- package/core/why.d.ts +1 -1
- package/core/why.d.ts.map +1 -1
- package/core/why.ts +1 -1
- package/mcp/contracts.d.ts +1561 -0
- package/mcp/contracts.d.ts.map +1 -0
- package/mcp/contracts.js +74 -0
- package/mcp/contracts.ts +105 -0
- package/mcp/index.d.ts +11 -0
- package/mcp/index.d.ts.map +1 -0
- package/mcp/index.js +35 -0
- package/mcp/index.ts +97 -0
- package/mcp/insights.d.ts +94 -0
- package/mcp/insights.d.ts.map +1 -0
- package/mcp/insights.js +445 -0
- package/mcp/insights.ts +541 -0
- package/mcp/tools.d.ts +63 -0
- package/mcp/tools.d.ts.map +1 -0
- package/mcp/tools.js +299 -0
- package/mcp/tools.ts +431 -0
- package/package.json +36 -26
- package/server.json +21 -0
- package/cli/constraints-loader.d.ts.map +0 -1
- package/cli/engine-helpers.d.ts.map +0 -1
- package/core/cross-axis-config.d.ts.map +0 -1
package/mcp/insights.ts
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation logic for the read-only insight tools
|
|
3
|
+
* (`list-constraints`, `explain`, `suggest-fix`).
|
|
4
|
+
*
|
|
5
|
+
* Everything here is side-effect free: no filesystem, no engine construction, no
|
|
6
|
+
* writes. Callers pass in a value resolver and the already-discovered constraint
|
|
7
|
+
* sources; these functions turn raw violations into explanations and verified
|
|
8
|
+
* suggestions. The WCAG math reuses core/color so suggestions are checked against
|
|
9
|
+
* the same contrast pipeline the validator uses.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_THRESHOLDS,
|
|
13
|
+
DEFAULT_WCAG_PAIRS,
|
|
14
|
+
type ConstraintSource,
|
|
15
|
+
type OrderRule,
|
|
16
|
+
} from '../cli/constraint-registry.js';
|
|
17
|
+
import { parseSize } from '../core/constraints/monotonic.js';
|
|
18
|
+
import { parseLightness } from '../core/constraints/monotonic-lightness.js';
|
|
19
|
+
import {
|
|
20
|
+
parseCssColor,
|
|
21
|
+
compositeOver,
|
|
22
|
+
relativeLuminance,
|
|
23
|
+
contrastRatio,
|
|
24
|
+
type RGBA,
|
|
25
|
+
} from '../core/color.js';
|
|
26
|
+
|
|
27
|
+
/** Thrown for caller-facing problems (bad input, unsupported rule). The MCP
|
|
28
|
+
* handler maps `.code` onto a structured tool error. */
|
|
29
|
+
export class InsightError extends Error {
|
|
30
|
+
readonly code: string;
|
|
31
|
+
constructor(code: string, message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Resolves a token id to its value string, or returns the argument unchanged
|
|
38
|
+
* when it is a literal (e.g. a backdrop color), mirroring the WCAG plugin. */
|
|
39
|
+
export type ValueResolver = (idOrLiteral: string) => string;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// list-constraints
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export type ConstraintDescriptor =
|
|
46
|
+
| { kind: 'wcag'; source: 'builtin' | 'config'; foreground: string; background: string; minRatio: number; backdrop?: string; where?: string }
|
|
47
|
+
| { kind: 'threshold'; source: 'builtin' | 'config'; tokenId: string; op: '>=' | '<='; valuePx: number; where?: string }
|
|
48
|
+
| { kind: 'order'; source: 'file'; axis: string; path: string; orders: OrderRule[] }
|
|
49
|
+
| { kind: 'lightness'; source: 'file'; path: string; orders: OrderRule[] }
|
|
50
|
+
| { kind: 'cross-axis'; source: 'file'; path: string; breakpoint?: string };
|
|
51
|
+
|
|
52
|
+
/** Flatten discovered constraint sources into a stable, agent-friendly list. */
|
|
53
|
+
export function describeConstraints(sources: ConstraintSource[]): ConstraintDescriptor[] {
|
|
54
|
+
const out: ConstraintDescriptor[] = [];
|
|
55
|
+
for (const s of sources) {
|
|
56
|
+
switch (s.type) {
|
|
57
|
+
case 'builtin-wcag':
|
|
58
|
+
if (s.enabled) {
|
|
59
|
+
for (const p of DEFAULT_WCAG_PAIRS) {
|
|
60
|
+
out.push({ kind: 'wcag', source: 'builtin', foreground: p.fg, background: p.bg, minRatio: p.min, ...(p.backdrop ? { backdrop: p.backdrop } : {}), ...(p.where ? { where: p.where } : {}) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case 'builtin-threshold':
|
|
65
|
+
if (s.enabled) {
|
|
66
|
+
for (const t of DEFAULT_THRESHOLDS) {
|
|
67
|
+
out.push({ kind: 'threshold', source: 'builtin', tokenId: t.id, op: t.op, valuePx: t.valuePx, ...(t.where ? { where: t.where } : {}) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case 'config-wcag':
|
|
72
|
+
for (const r of s.rules) {
|
|
73
|
+
out.push({ kind: 'wcag', source: 'config', foreground: r.fg, background: r.bg, minRatio: r.min, ...(r.backdrop ? { backdrop: r.backdrop } : {}), ...(r.where ? { where: r.where } : {}) });
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'custom-threshold':
|
|
77
|
+
for (const r of s.rules) {
|
|
78
|
+
out.push({ kind: 'threshold', source: 'config', tokenId: r.id, op: r.op, valuePx: r.valuePx, ...(r.where ? { where: r.where } : {}) });
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case 'order-file':
|
|
82
|
+
out.push({ kind: 'order', source: 'file', axis: s.axis, path: s.path, orders: s.orders });
|
|
83
|
+
break;
|
|
84
|
+
case 'lightness-file':
|
|
85
|
+
out.push({ kind: 'lightness', source: 'file', path: s.path, orders: s.orders });
|
|
86
|
+
break;
|
|
87
|
+
case 'cross-axis-file':
|
|
88
|
+
out.push({ kind: 'cross-axis', source: 'file', path: s.path, ...(s.bp ? { breakpoint: s.bp } : {}) });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// shared helpers
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
type RuleKind = 'wcag' | 'threshold' | 'monotonic' | 'monotonic-lightness';
|
|
100
|
+
|
|
101
|
+
function classifyRule(ruleId: string): RuleKind {
|
|
102
|
+
switch (ruleId) {
|
|
103
|
+
case 'wcag-contrast':
|
|
104
|
+
return 'wcag';
|
|
105
|
+
case 'threshold':
|
|
106
|
+
case 'custom-threshold':
|
|
107
|
+
return 'threshold';
|
|
108
|
+
case 'monotonic':
|
|
109
|
+
return 'monotonic';
|
|
110
|
+
case 'monotonic-lightness':
|
|
111
|
+
return 'monotonic-lightness';
|
|
112
|
+
default:
|
|
113
|
+
throw new InsightError('unsupported_rule', `explain/suggest-fix does not support rule "${ruleId}" (supported: wcag-contrast, threshold, custom-threshold, monotonic, monotonic-lightness).`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A monotonic issue id is `a|b`; a caller may also pass [a, b] directly. */
|
|
118
|
+
function orderPair(nodes: string[]): [string, string] {
|
|
119
|
+
if (nodes.length === 1 && nodes[0].includes('|')) {
|
|
120
|
+
const parts = nodes[0].split('|');
|
|
121
|
+
if (parts.length !== 2) {
|
|
122
|
+
throw new InsightError('invalid_input', `A monotonic node id must be "a|b" (got ${parts.length} segments in "${nodes[0]}").`);
|
|
123
|
+
}
|
|
124
|
+
return [parts[0], parts[1]];
|
|
125
|
+
}
|
|
126
|
+
if (nodes.length === 2) return [nodes[0], nodes[1]];
|
|
127
|
+
throw new InsightError('invalid_input', 'A monotonic violation needs exactly two token ids (nodes: [a, b] or ["a|b"]).');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findOrderOp(descriptors: ConstraintDescriptor[], kind: 'order' | 'lightness', a: string, b: string): '<=' | '>=' | undefined {
|
|
131
|
+
for (const d of descriptors) {
|
|
132
|
+
if (d.kind !== kind) continue;
|
|
133
|
+
for (const [x, op, y] of d.orders) {
|
|
134
|
+
if (x === a && y === b) return op;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Mirrors the px-default parser in core/constraints/threshold.ts (incl. `em` and
|
|
141
|
+
// the real-number guard) so `explain` agrees with the validator (TASK-034).
|
|
142
|
+
function parseThresholdPx(v: string): number | null {
|
|
143
|
+
const m = v.trim().match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
144
|
+
if (!m) return null;
|
|
145
|
+
const n = parseFloat(m[1]);
|
|
146
|
+
if (!Number.isFinite(n)) return null;
|
|
147
|
+
const unit = (m[2] || 'px').toLowerCase();
|
|
148
|
+
return unit === 'rem' || unit === 'em' ? n * 16 : n;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function round2(n: number): number {
|
|
152
|
+
return Number(n.toFixed(2));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toHex(c: RGBA): string {
|
|
156
|
+
const h = (n: number) => Math.round(n).toString(16).padStart(2, '0');
|
|
157
|
+
return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Effective (alpha-composited) contrast for a fg/bg/backdrop triplet, or null
|
|
161
|
+
* when any color is unparseable. Same pipeline as WcagContrastPlugin. */
|
|
162
|
+
function effectiveContrast(fgVal: string, bgVal: string, backdropVal: string): { ratio: number; effFg: RGBA; effBg: RGBA } | null {
|
|
163
|
+
const fg = parseCssColor(fgVal);
|
|
164
|
+
const bg = parseCssColor(bgVal);
|
|
165
|
+
const backdrop = parseCssColor(backdropVal || '#ffffff');
|
|
166
|
+
if (!fg || !bg || !backdrop) return null;
|
|
167
|
+
const effBg = bg.a < 1 ? compositeOver(bg, backdrop) : bg;
|
|
168
|
+
const effFg = fg.a < 1 ? compositeOver(fg, effBg) : fg;
|
|
169
|
+
return { ratio: contrastRatio(relativeLuminance(effFg), relativeLuminance(effBg)), effFg, effBg };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// explain
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
export interface InsightRequest {
|
|
177
|
+
ruleId: string;
|
|
178
|
+
nodes: string[];
|
|
179
|
+
context?: Record<string, unknown>;
|
|
180
|
+
getValue: ValueResolver;
|
|
181
|
+
descriptors: ConstraintDescriptor[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ExplainResult {
|
|
185
|
+
ok: true;
|
|
186
|
+
ruleId: string;
|
|
187
|
+
kind: RuleKind;
|
|
188
|
+
nodes: string[];
|
|
189
|
+
facts: Record<string, unknown>;
|
|
190
|
+
explanation: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function explain(req: InsightRequest): ExplainResult {
|
|
194
|
+
const kind = classifyRule(req.ruleId);
|
|
195
|
+
const { getValue, descriptors, context } = req;
|
|
196
|
+
const whereSuffix = (where?: string) => (where ? ` (${where})` : '');
|
|
197
|
+
|
|
198
|
+
if (kind === 'wcag') {
|
|
199
|
+
if (req.nodes.length < 2) {
|
|
200
|
+
throw new InsightError('invalid_input', 'A WCAG violation needs nodes: [foreground, background].');
|
|
201
|
+
}
|
|
202
|
+
const [fg, bg] = req.nodes;
|
|
203
|
+
const rule = descriptors.find((d) => d.kind === 'wcag' && d.foreground === fg && d.background === bg) as Extract<ConstraintDescriptor, { kind: 'wcag' }> | undefined;
|
|
204
|
+
const required = rule?.minRatio ?? (typeof context?.required === 'number' ? (context.required as number) : undefined);
|
|
205
|
+
if (required === undefined) {
|
|
206
|
+
throw new InsightError('invalid_input', `No active WCAG rule found for ${fg} on ${bg}, and no required ratio in context.`);
|
|
207
|
+
}
|
|
208
|
+
const fgVal = getValue(fg);
|
|
209
|
+
const bgVal = getValue(bg);
|
|
210
|
+
const backdropVal = getValue(rule?.backdrop ?? '#ffffff');
|
|
211
|
+
const computed = effectiveContrast(fgVal, bgVal, backdropVal);
|
|
212
|
+
const where = rule?.where;
|
|
213
|
+
const actual = computed ? round2(computed.ratio) : null;
|
|
214
|
+
const facts = {
|
|
215
|
+
foreground: fg,
|
|
216
|
+
background: bg,
|
|
217
|
+
foregroundValue: fgVal,
|
|
218
|
+
backgroundValue: bgVal,
|
|
219
|
+
backdrop: rule?.backdrop ?? '#ffffff',
|
|
220
|
+
requiredRatio: required,
|
|
221
|
+
actualRatio: actual,
|
|
222
|
+
parseable: computed !== null,
|
|
223
|
+
...(where ? { where } : {}),
|
|
224
|
+
};
|
|
225
|
+
const explanation = computed
|
|
226
|
+
? `Contrast of ${fg} (${fgVal}) on ${bg} (${bgVal}) is ${actual}:1, ${computed.ratio < required ? 'below' : 'at or above'} the required ${required}:1${whereSuffix(where)}.`
|
|
227
|
+
: `Could not compute contrast for ${fg} (${fgVal}) on ${bg} (${bgVal}) — one or more colors are unparseable.`;
|
|
228
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [fg, bg], facts, explanation };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (kind === 'threshold') {
|
|
232
|
+
const id = req.nodes[0];
|
|
233
|
+
if (!id) throw new InsightError('invalid_input', 'A threshold violation needs nodes: [tokenId].');
|
|
234
|
+
const rule = descriptors.find((d) => d.kind === 'threshold' && d.tokenId === id) as Extract<ConstraintDescriptor, { kind: 'threshold' }> | undefined;
|
|
235
|
+
const op = rule?.op ?? (context?.op as '<=' | '>=' | undefined);
|
|
236
|
+
const valuePx = rule?.valuePx ?? (typeof context?.threshold === 'number' ? (context.threshold as number) : undefined);
|
|
237
|
+
if (op === undefined || valuePx === undefined) {
|
|
238
|
+
throw new InsightError('invalid_input', `No active threshold rule found for ${id}, and no op/threshold in context.`);
|
|
239
|
+
}
|
|
240
|
+
const val = getValue(id);
|
|
241
|
+
const actualPx = parseThresholdPx(val);
|
|
242
|
+
const satisfied = actualPx === null ? null : op === '>=' ? actualPx >= valuePx : actualPx <= valuePx;
|
|
243
|
+
const where = rule?.where;
|
|
244
|
+
const facts = { tokenId: id, value: val, actualPx, op, requiredPx: valuePx, satisfied, ...(where ? { where } : {}) };
|
|
245
|
+
const explanation =
|
|
246
|
+
actualPx === null
|
|
247
|
+
? `${id} is "${val}", which is not a parseable px/rem size; it must be ${op} ${valuePx}px${whereSuffix(where)}.`
|
|
248
|
+
: `${id} is ${actualPx}px, but must be ${op} ${valuePx}px${whereSuffix(where)}.`;
|
|
249
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [id], facts, explanation };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// monotonic (size) or monotonic-lightness (relative luminance)
|
|
253
|
+
const [a, b] = orderPair(req.nodes);
|
|
254
|
+
const isLightness = kind === 'monotonic-lightness';
|
|
255
|
+
const op = findOrderOp(descriptors, isLightness ? 'lightness' : 'order', a, b);
|
|
256
|
+
if (op === undefined) {
|
|
257
|
+
throw new InsightError('invalid_input', `No active ${isLightness ? 'lightness' : 'order'} constraint found for ${a} ${'<=/>='} ${b}.`);
|
|
258
|
+
}
|
|
259
|
+
const aVal = getValue(a);
|
|
260
|
+
const bVal = getValue(b);
|
|
261
|
+
const parse = isLightness ? parseLightness : parseSize;
|
|
262
|
+
const na = parse(aVal);
|
|
263
|
+
const nb = parse(bVal);
|
|
264
|
+
const unit = isLightness ? 'luminance' : 'px';
|
|
265
|
+
// Actually compare — explain accepts a loose {ruleId, nodes} pair, so the order
|
|
266
|
+
// may hold; don't unconditionally claim a violation (TASK-032).
|
|
267
|
+
const comparable = na !== null && nb !== null;
|
|
268
|
+
const satisfied = comparable ? (op === '>=' ? na >= nb : na <= nb) : null;
|
|
269
|
+
const facts = {
|
|
270
|
+
left: a,
|
|
271
|
+
right: b,
|
|
272
|
+
leftValue: aVal,
|
|
273
|
+
rightValue: bVal,
|
|
274
|
+
op,
|
|
275
|
+
unit,
|
|
276
|
+
leftMeasure: na,
|
|
277
|
+
rightMeasure: nb,
|
|
278
|
+
satisfied,
|
|
279
|
+
};
|
|
280
|
+
const explanation = !comparable
|
|
281
|
+
? `${a} (${aVal}) ${op} ${b} (${bVal}) can't be evaluated — one or both values aren't parseable as ${unit}.`
|
|
282
|
+
: satisfied
|
|
283
|
+
? `${a} (${aVal}) ${op} ${b} (${bVal}) by ${unit} — the order holds.`
|
|
284
|
+
: `${a} (${aVal}) must be ${op} ${b} (${bVal}) by ${unit}, but the order is violated — the scale is out of order.`;
|
|
285
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [a, b], facts, explanation };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// suggest-fix
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
export interface Suggestion {
|
|
293
|
+
tokenId: string;
|
|
294
|
+
role: string;
|
|
295
|
+
currentValue: string;
|
|
296
|
+
suggestedValue: string;
|
|
297
|
+
resultingValue: number;
|
|
298
|
+
satisfies: string;
|
|
299
|
+
why: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface SuggestResult {
|
|
303
|
+
ok: true;
|
|
304
|
+
ruleId: string;
|
|
305
|
+
kind: RuleKind;
|
|
306
|
+
nodes: string[];
|
|
307
|
+
suggestions: Suggestion[];
|
|
308
|
+
note?: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface SuggestRequest extends InsightRequest {
|
|
312
|
+
target?: 'foreground' | 'background';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Find the smallest change to `color` (toward white or black) whose contrast
|
|
317
|
+
* against a fixed luminance reaches `min`. Returns an opaque, integer-channel
|
|
318
|
+
* RGBA already verified to clear `min`, or null when neither endpoint reaches it.
|
|
319
|
+
*
|
|
320
|
+
* We scan in fine steps rather than binary-search because contrast is NOT
|
|
321
|
+
* monotonic along the blend when `otherLum` falls between the start and the
|
|
322
|
+
* target (it dips to 1:1 as the colors cross). Scanning from the original
|
|
323
|
+
* outward returns the first — i.e. minimal — integer color that genuinely
|
|
324
|
+
* clears the ratio; we keep the closer of the two directions.
|
|
325
|
+
*/
|
|
326
|
+
function pushToContrast(color: RGBA, otherLum: number, min: number): RGBA | null {
|
|
327
|
+
const white: RGBA = { r: 255, g: 255, b: 255, a: 1 };
|
|
328
|
+
const black: RGBA = { r: 0, g: 0, b: 0, a: 1 };
|
|
329
|
+
const STEPS = 256;
|
|
330
|
+
const blendSnap = (target: RGBA, t: number): RGBA => ({
|
|
331
|
+
r: Math.round(color.r + (target.r - color.r) * t),
|
|
332
|
+
g: Math.round(color.g + (target.g - color.g) * t),
|
|
333
|
+
b: Math.round(color.b + (target.b - color.b) * t),
|
|
334
|
+
a: 1,
|
|
335
|
+
});
|
|
336
|
+
const ratioAt = (cand: RGBA) => contrastRatio(relativeLuminance(cand), otherLum);
|
|
337
|
+
const dist = (c: RGBA) => (c.r - color.r) ** 2 + (c.g - color.g) ** 2 + (c.b - color.b) ** 2;
|
|
338
|
+
|
|
339
|
+
let best: RGBA | null = null;
|
|
340
|
+
let bestDist = Infinity;
|
|
341
|
+
for (const target of [white, black]) {
|
|
342
|
+
for (let i = 1; i <= STEPS; i++) {
|
|
343
|
+
const cand = blendSnap(target, i / STEPS);
|
|
344
|
+
if (ratioAt(cand) >= min) {
|
|
345
|
+
const d = dist(cand);
|
|
346
|
+
if (d < bestDist) {
|
|
347
|
+
bestDist = d;
|
|
348
|
+
best = cand;
|
|
349
|
+
}
|
|
350
|
+
break; // first satisfying step in a direction is that direction's minimal change
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return best;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Find the smallest change to an (opaque) background that clears `min` against
|
|
359
|
+
* `fg`. Unlike a foreground tweak, changing the background also changes the
|
|
360
|
+
* EFFECTIVE foreground when `fg` is semi-transparent (it composites over the new
|
|
361
|
+
* background), so the ratio is recomputed through the full pipeline for every
|
|
362
|
+
* candidate — never against a stale precomputed foreground. Returns a verified
|
|
363
|
+
* opaque RGBA, or null when no candidate reaches `min`.
|
|
364
|
+
*/
|
|
365
|
+
function pushBackgroundToContrast(fg: RGBA, bgStart: RGBA, min: number): RGBA | null {
|
|
366
|
+
const white: RGBA = { r: 255, g: 255, b: 255, a: 1 };
|
|
367
|
+
const black: RGBA = { r: 0, g: 0, b: 0, a: 1 };
|
|
368
|
+
const STEPS = 256;
|
|
369
|
+
const blendSnap = (target: RGBA, t: number): RGBA => ({
|
|
370
|
+
r: Math.round(bgStart.r + (target.r - bgStart.r) * t),
|
|
371
|
+
g: Math.round(bgStart.g + (target.g - bgStart.g) * t),
|
|
372
|
+
b: Math.round(bgStart.b + (target.b - bgStart.b) * t),
|
|
373
|
+
a: 1,
|
|
374
|
+
});
|
|
375
|
+
const trueRatio = (bgCand: RGBA): number => {
|
|
376
|
+
const effFg = fg.a < 1 ? compositeOver(fg, bgCand) : fg;
|
|
377
|
+
return contrastRatio(relativeLuminance(effFg), relativeLuminance(bgCand));
|
|
378
|
+
};
|
|
379
|
+
const dist = (c: RGBA) => (c.r - bgStart.r) ** 2 + (c.g - bgStart.g) ** 2 + (c.b - bgStart.b) ** 2;
|
|
380
|
+
|
|
381
|
+
let best: RGBA | null = null;
|
|
382
|
+
let bestDist = Infinity;
|
|
383
|
+
for (const target of [white, black]) {
|
|
384
|
+
for (let i = 1; i <= STEPS; i++) {
|
|
385
|
+
const cand = blendSnap(target, i / STEPS);
|
|
386
|
+
if (trueRatio(cand) >= min) {
|
|
387
|
+
const d = dist(cand);
|
|
388
|
+
if (d < bestDist) {
|
|
389
|
+
bestDist = d;
|
|
390
|
+
best = cand;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return best;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function suggestFix(req: SuggestRequest): SuggestResult {
|
|
400
|
+
const kind = classifyRule(req.ruleId);
|
|
401
|
+
const { getValue, descriptors, context } = req;
|
|
402
|
+
|
|
403
|
+
if (kind === 'wcag') {
|
|
404
|
+
if (req.nodes.length < 2) {
|
|
405
|
+
throw new InsightError('invalid_input', 'A WCAG violation needs nodes: [foreground, background].');
|
|
406
|
+
}
|
|
407
|
+
const [fg, bg] = req.nodes;
|
|
408
|
+
const rule = descriptors.find((d) => d.kind === 'wcag' && d.foreground === fg && d.background === bg) as Extract<ConstraintDescriptor, { kind: 'wcag' }> | undefined;
|
|
409
|
+
const min = rule?.minRatio ?? (typeof context?.required === 'number' ? (context.required as number) : undefined);
|
|
410
|
+
if (min === undefined) {
|
|
411
|
+
throw new InsightError('invalid_input', `No active WCAG rule found for ${fg} on ${bg}, and no required ratio in context.`);
|
|
412
|
+
}
|
|
413
|
+
const fgVal = getValue(fg);
|
|
414
|
+
const bgVal = getValue(bg);
|
|
415
|
+
const backdropVal = getValue(rule?.backdrop ?? '#ffffff');
|
|
416
|
+
const fgColor = parseCssColor(fgVal);
|
|
417
|
+
const bgColor = parseCssColor(bgVal);
|
|
418
|
+
const backdrop = parseCssColor(backdropVal || '#ffffff');
|
|
419
|
+
if (!fgColor || !bgColor || !backdrop) {
|
|
420
|
+
throw new InsightError('invalid_input', `Cannot suggest a fix: unparseable color(s) — foreground "${fgVal}", background "${bgVal}".`);
|
|
421
|
+
}
|
|
422
|
+
const effBg = bgColor.a < 1 ? compositeOver(bgColor, backdrop) : bgColor;
|
|
423
|
+
const effFg = fgColor.a < 1 ? compositeOver(fgColor, effBg) : fgColor;
|
|
424
|
+
const sides: Array<'foreground' | 'background'> = req.target ? [req.target] : ['foreground', 'background'];
|
|
425
|
+
const suggestions: Suggestion[] = [];
|
|
426
|
+
|
|
427
|
+
for (const side of sides) {
|
|
428
|
+
if (side === 'foreground') {
|
|
429
|
+
const adj = pushToContrast(effFg, relativeLuminance(effBg), min);
|
|
430
|
+
if (adj) {
|
|
431
|
+
const ratio = contrastRatio(relativeLuminance(adj), relativeLuminance(effBg));
|
|
432
|
+
if (ratio >= min) {
|
|
433
|
+
suggestions.push({
|
|
434
|
+
tokenId: fg,
|
|
435
|
+
role: 'foreground',
|
|
436
|
+
currentValue: fgVal,
|
|
437
|
+
suggestedValue: toHex(adj),
|
|
438
|
+
resultingValue: round2(ratio),
|
|
439
|
+
satisfies: `wcag-contrast >= ${min}:1`,
|
|
440
|
+
why: 'Foreground lightness adjusted (opaque) until contrast clears the ratio; verified with WCAG contrast math.',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// Recompute the effective foreground over the candidate background: when
|
|
446
|
+
// the foreground has alpha, changing the background changes the composited
|
|
447
|
+
// foreground too, so a fixed precomputed effFg would over-report contrast.
|
|
448
|
+
const adj = pushBackgroundToContrast(fgColor, effBg, min);
|
|
449
|
+
if (adj) {
|
|
450
|
+
const effFgOverAdj = fgColor.a < 1 ? compositeOver(fgColor, adj) : fgColor;
|
|
451
|
+
const ratio = contrastRatio(relativeLuminance(effFgOverAdj), relativeLuminance(adj));
|
|
452
|
+
if (ratio >= min) {
|
|
453
|
+
suggestions.push({
|
|
454
|
+
tokenId: bg,
|
|
455
|
+
role: 'background',
|
|
456
|
+
currentValue: bgVal,
|
|
457
|
+
suggestedValue: toHex(adj),
|
|
458
|
+
resultingValue: round2(ratio),
|
|
459
|
+
satisfies: `wcag-contrast >= ${min}:1`,
|
|
460
|
+
why: 'Background lightness adjusted (opaque) until contrast clears the ratio; verified against the recomposited foreground.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const note = suggestions.length === 0
|
|
468
|
+
? `No sRGB ${req.target ?? 'foreground or background'} color reaches ${min}:1 against the other color; widen the lightness separation or change the backdrop.`
|
|
469
|
+
: undefined;
|
|
470
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [fg, bg], suggestions, ...(note ? { note } : {}) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (kind === 'threshold') {
|
|
474
|
+
const id = req.nodes[0];
|
|
475
|
+
if (!id) throw new InsightError('invalid_input', 'A threshold violation needs nodes: [tokenId].');
|
|
476
|
+
const rule = descriptors.find((d) => d.kind === 'threshold' && d.tokenId === id) as Extract<ConstraintDescriptor, { kind: 'threshold' }> | undefined;
|
|
477
|
+
const op = rule?.op ?? (context?.op as '<=' | '>=' | undefined);
|
|
478
|
+
const valuePx = rule?.valuePx ?? (typeof context?.threshold === 'number' ? (context.threshold as number) : undefined);
|
|
479
|
+
if (op === undefined || valuePx === undefined) {
|
|
480
|
+
throw new InsightError('invalid_input', `No active threshold rule found for ${id}, and no op/threshold in context.`);
|
|
481
|
+
}
|
|
482
|
+
const current = getValue(id);
|
|
483
|
+
const suggestion: Suggestion = {
|
|
484
|
+
tokenId: id,
|
|
485
|
+
role: op === '>=' ? 'raise-to-min' : 'lower-to-max',
|
|
486
|
+
currentValue: current,
|
|
487
|
+
suggestedValue: `${valuePx}px`,
|
|
488
|
+
resultingValue: valuePx,
|
|
489
|
+
satisfies: `${id} ${op} ${valuePx}px`,
|
|
490
|
+
why: op === '>=' ? `Set to the ${valuePx}px minimum (the boundary that satisfies ${op}).` : `Set to the ${valuePx}px maximum (the boundary that satisfies ${op}).`,
|
|
491
|
+
};
|
|
492
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [id], suggestions: [suggestion] };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (kind === 'monotonic-lightness') {
|
|
496
|
+
throw new InsightError('unsupported_rule', 'suggest-fix does not synthesize colors for lightness ordering; adjust the out-of-order token\'s lightness manually (use explain to see the luminance gap).');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// monotonic (size): a op b violated → raise the low side or lower the high side
|
|
500
|
+
// to the boundary. Both candidates satisfy the order at equality.
|
|
501
|
+
const [a, b] = orderPair(req.nodes);
|
|
502
|
+
const op = findOrderOp(descriptors, 'order', a, b);
|
|
503
|
+
if (op === undefined) {
|
|
504
|
+
throw new InsightError('invalid_input', `No active order constraint found for ${a} .. ${b}.`);
|
|
505
|
+
}
|
|
506
|
+
const aVal = getValue(a);
|
|
507
|
+
const bVal = getValue(b);
|
|
508
|
+
const na = parseSize(aVal);
|
|
509
|
+
const nb = parseSize(bVal);
|
|
510
|
+
if (na === null || nb === null) {
|
|
511
|
+
throw new InsightError('invalid_input', `Cannot compute a numeric boundary: ${a}="${aVal}" or ${b}="${bVal}" is not a parseable size.`);
|
|
512
|
+
}
|
|
513
|
+
// The order holds at equality regardless of `op`, so the boundary is the same
|
|
514
|
+
// either way: move `a` to b's value, or move `b` to a's value. (The earlier
|
|
515
|
+
// op-conditional made the `<=` case suggest each token's own value — a no-op.)
|
|
516
|
+
// Role reflects the real direction of the move for the reported value.
|
|
517
|
+
const aTarget = round2(nb);
|
|
518
|
+
const bTarget = round2(na);
|
|
519
|
+
const direction = (from: number, to: number): string => (to > from ? 'raise' : to < from ? 'lower' : 'keep');
|
|
520
|
+
const suggestions: Suggestion[] = [
|
|
521
|
+
{
|
|
522
|
+
tokenId: a,
|
|
523
|
+
role: direction(na, aTarget),
|
|
524
|
+
currentValue: aVal,
|
|
525
|
+
suggestedValue: `${aTarget}px`,
|
|
526
|
+
resultingValue: aTarget,
|
|
527
|
+
satisfies: `${a} ${op} ${b}`,
|
|
528
|
+
why: `Set ${a} to ${b}'s value (${aTarget}px) so the order holds at equality.`,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
tokenId: b,
|
|
532
|
+
role: direction(nb, bTarget),
|
|
533
|
+
currentValue: bVal,
|
|
534
|
+
suggestedValue: `${bTarget}px`,
|
|
535
|
+
resultingValue: bTarget,
|
|
536
|
+
satisfies: `${a} ${op} ${b}`,
|
|
537
|
+
why: `Set ${b} to ${a}'s value (${bTarget}px) so the order holds at equality.`,
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
return { ok: true, ruleId: req.ruleId, kind, nodes: [a, b], suggestions };
|
|
541
|
+
}
|
package/mcp/tools.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { type WhyReport } from '../core/why.js';
|
|
4
|
+
import { type ValidateResult } from '../cli/validate-api.js';
|
|
5
|
+
import { type ConstraintDescriptor, type ExplainResult, type SuggestResult } from './insights.js';
|
|
6
|
+
import type { ValidateToolInput, WhyToolInput, GraphToolInput, ListConstraintsToolInput, ExplainToolInput, SuggestFixToolInput } from './contracts.js';
|
|
7
|
+
export type DcvMcpToolName = 'validate' | 'why' | 'graph' | 'list-constraints' | 'explain' | 'suggest-fix';
|
|
8
|
+
export interface ToolFailure {
|
|
9
|
+
ok: false;
|
|
10
|
+
tool: DcvMcpToolName;
|
|
11
|
+
error: {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
details?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export type ToolResponse<T extends {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
} & object> = ({
|
|
20
|
+
tool: DcvMcpToolName;
|
|
21
|
+
} & T) | ToolFailure;
|
|
22
|
+
export declare class ToolExecutionError extends Error {
|
|
23
|
+
readonly code: string;
|
|
24
|
+
readonly details?: Record<string, unknown>;
|
|
25
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
26
|
+
}
|
|
27
|
+
export interface GraphToolResult {
|
|
28
|
+
ok: true;
|
|
29
|
+
nodes: string[];
|
|
30
|
+
edges: Array<[string, string]>;
|
|
31
|
+
meta: {
|
|
32
|
+
nodeCount: number;
|
|
33
|
+
edgeCount: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type WhyToolResult = {
|
|
37
|
+
ok: true;
|
|
38
|
+
} & WhyReport;
|
|
39
|
+
interface ToolDefinition<TInput, TResult extends {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
} & object> {
|
|
42
|
+
name: DcvMcpToolName;
|
|
43
|
+
description: string;
|
|
44
|
+
inputSchema: Record<string, z.ZodTypeAny>;
|
|
45
|
+
handler: (input: TInput) => Promise<ToolResponse<TResult>> | ToolResponse<TResult>;
|
|
46
|
+
}
|
|
47
|
+
export declare function validateTool(input: ValidateToolInput): Promise<ToolResponse<ValidateResult>>;
|
|
48
|
+
export declare function whyTool(input: WhyToolInput): Promise<ToolResponse<WhyToolResult>>;
|
|
49
|
+
export declare function graphTool(input: GraphToolInput): Promise<ToolResponse<GraphToolResult>>;
|
|
50
|
+
export interface ListConstraintsResult {
|
|
51
|
+
ok: true;
|
|
52
|
+
constraints: ConstraintDescriptor[];
|
|
53
|
+
meta: {
|
|
54
|
+
count: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export declare function listConstraintsTool(input: ListConstraintsToolInput): Promise<ToolResponse<ListConstraintsResult>>;
|
|
58
|
+
export declare function explainTool(input: ExplainToolInput): Promise<ToolResponse<ExplainResult>>;
|
|
59
|
+
export declare function suggestFixTool(input: SuggestFixToolInput): Promise<ToolResponse<SuggestResult>>;
|
|
60
|
+
export declare const dcvMcpTools: Array<ToolDefinition<ValidateToolInput, ValidateResult> | ToolDefinition<WhyToolInput, WhyToolResult> | ToolDefinition<GraphToolInput, GraphToolResult> | ToolDefinition<ListConstraintsToolInput, ListConstraintsResult> | ToolDefinition<ExplainToolInput, ExplainResult> | ToolDefinition<SuggestFixToolInput, SuggestResult>>;
|
|
61
|
+
export declare function registerDcvMcpTools(server: McpServer): void;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["tools.ts"],"names":[],"mappings":"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"}
|