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/tools.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { suggestIds } from '../core/cli-format.js';
|
|
3
|
+
import { flattenTokens } from '../core/flatten.js';
|
|
4
|
+
import { explain as explainWhy } from '../core/why.js';
|
|
5
|
+
import { Engine } from '../core/engine.js';
|
|
6
|
+
import { ConstraintsSchema } from '../cli/config-schema.js';
|
|
7
|
+
import { loadConfig } from '../cli/config.js';
|
|
8
|
+
import { discoverConstraints } from '../cli/constraint-registry.js';
|
|
9
|
+
import { validate } from '../cli/validate-api.js';
|
|
10
|
+
import { describeConstraints, explain as explainInsight, suggestFix as suggestFixInsight, InsightError, } from './insights.js';
|
|
11
|
+
import { graphInputShape, validateInputShape, whyInputShape, listConstraintsInputShape, explainInputShape, suggestFixInputShape, } from './contracts.js';
|
|
12
|
+
export class ToolExecutionError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
details;
|
|
15
|
+
constructor(code, message, details) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.details = details;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function errorMessage(error) {
|
|
22
|
+
return error instanceof Error ? error.message : 'Unknown tool failure';
|
|
23
|
+
}
|
|
24
|
+
function toFailure(tool, error) {
|
|
25
|
+
if (error instanceof ToolExecutionError) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
tool,
|
|
29
|
+
error: {
|
|
30
|
+
code: error.code,
|
|
31
|
+
message: error.message,
|
|
32
|
+
...(error.details ? { details: error.details } : {}),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Pure derivation errors (bad input, unsupported rule) carry their own code.
|
|
37
|
+
if (error instanceof InsightError) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
tool,
|
|
41
|
+
error: {
|
|
42
|
+
code: error.code,
|
|
43
|
+
message: error.message,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
tool,
|
|
50
|
+
error: {
|
|
51
|
+
code: 'tool_execution_failed',
|
|
52
|
+
message: errorMessage(error),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function executeTool(tool, fn) {
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
tool,
|
|
60
|
+
...(await fn()),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return toFailure(tool, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isToolFailure(result) {
|
|
68
|
+
return result.ok === false && 'error' in result;
|
|
69
|
+
}
|
|
70
|
+
function responseToContent(result) {
|
|
71
|
+
return JSON.stringify(result, null, 2);
|
|
72
|
+
}
|
|
73
|
+
function parseJsonFile(filePath) {
|
|
74
|
+
if (!fs.existsSync(filePath)) {
|
|
75
|
+
throw new ToolExecutionError('tokens_not_found', `Tokens file not found: ${filePath}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new ToolExecutionError('invalid_tokens', `Tokens file is not valid JSON: ${filePath}`, {
|
|
82
|
+
cause: errorMessage(error),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function asJsonObject(value, label) {
|
|
87
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
throw new ToolExecutionError('invalid_input', `${label} must be a JSON object.`);
|
|
91
|
+
}
|
|
92
|
+
function zodIssueMessages(error) {
|
|
93
|
+
return error.issues.map((issue) => {
|
|
94
|
+
const path = issue.path.length > 0 ? `.${issue.path.join('.')}` : '';
|
|
95
|
+
return `constraints${path}: ${issue.message}`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function resolveTokens(input) {
|
|
99
|
+
if (input.tokens !== undefined) {
|
|
100
|
+
// TASK-017: validate inline tokens at the handler boundary. A direct caller
|
|
101
|
+
// (not going through the MCP SDK's schema check) could otherwise pass an
|
|
102
|
+
// array/null/scalar that silently flattened to an empty, passing set.
|
|
103
|
+
return asJsonObject(input.tokens, 'tokens');
|
|
104
|
+
}
|
|
105
|
+
if (input.tokensPath !== undefined) {
|
|
106
|
+
return asJsonObject(parseJsonFile(input.tokensPath), 'tokensPath contents');
|
|
107
|
+
}
|
|
108
|
+
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
109
|
+
}
|
|
110
|
+
function constraints(input) {
|
|
111
|
+
if (input.constraints === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
// TASK-017: reject malformed inline constraints at the boundary.
|
|
114
|
+
const object = asJsonObject(input.constraints, 'constraints');
|
|
115
|
+
const parsed = ConstraintsSchema.safeParse(object);
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
throw new ToolExecutionError('invalid_input', 'constraints must match DCV constraint config schema.', {
|
|
118
|
+
issues: zodIssueMessages(parsed.error),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return parsed.data;
|
|
122
|
+
}
|
|
123
|
+
export async function validateTool(input) {
|
|
124
|
+
return executeTool('validate', () => {
|
|
125
|
+
if (input.tokens === undefined && input.tokensPath === undefined) {
|
|
126
|
+
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
127
|
+
}
|
|
128
|
+
return validate({
|
|
129
|
+
...(input.tokens !== undefined ? { tokens: asJsonObject(input.tokens, 'tokens') } : {}),
|
|
130
|
+
...(input.tokens === undefined && input.tokensPath !== undefined ? { tokensPath: input.tokensPath } : {}),
|
|
131
|
+
...(input.constraints !== undefined ? { constraints: constraints(input) } : {}),
|
|
132
|
+
...(input.configPath !== undefined ? { configPath: input.configPath } : {}),
|
|
133
|
+
...(input.constraintsDir !== undefined ? { constraintsDir: input.constraintsDir } : {}),
|
|
134
|
+
...(input.breakpoint !== undefined ? { breakpoint: input.breakpoint } : {}),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export async function whyTool(input) {
|
|
139
|
+
return executeTool('why', () => {
|
|
140
|
+
const { flat, edges } = flattenTokens(resolveTokens(input));
|
|
141
|
+
if (!Object.prototype.hasOwnProperty.call(flat, input.tokenId)) {
|
|
142
|
+
throw new ToolExecutionError('unknown_token', `Unknown token id: ${input.tokenId}`, {
|
|
143
|
+
tokenId: input.tokenId,
|
|
144
|
+
suggestions: suggestIds(input.tokenId, Object.keys(flat)).map((suggestion) => suggestion.id),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
...explainWhy(input.tokenId, flat, edges),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function graphTool(input) {
|
|
154
|
+
return executeTool('graph', () => {
|
|
155
|
+
const { flat, edges } = flattenTokens(resolveTokens(input));
|
|
156
|
+
const nodes = Object.keys(flat).sort();
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
nodes,
|
|
160
|
+
edges,
|
|
161
|
+
meta: {
|
|
162
|
+
nodeCount: nodes.length,
|
|
163
|
+
edgeCount: edges.length,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Resolve the constraint config the same way validate() does: inline → configPath
|
|
169
|
+
* → discovered cwd config. */
|
|
170
|
+
function resolveConstraintsConfig(input) {
|
|
171
|
+
if (input.constraints !== undefined) {
|
|
172
|
+
return { constraints: constraints(input) };
|
|
173
|
+
}
|
|
174
|
+
const res = loadConfig(input.configPath);
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
throw new ToolExecutionError('invalid_config', res.error);
|
|
177
|
+
}
|
|
178
|
+
return res.value;
|
|
179
|
+
}
|
|
180
|
+
/** Flatten tokens, build an engine for value resolution, and discover the active
|
|
181
|
+
* constraint sources — the shared substrate for the read-only insight tools. */
|
|
182
|
+
function deriveContext(input) {
|
|
183
|
+
const tokens = resolveTokens(input);
|
|
184
|
+
const config = resolveConstraintsConfig(input);
|
|
185
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
186
|
+
const init = {};
|
|
187
|
+
for (const t of Object.values(flat)) {
|
|
188
|
+
init[t.id] = t.value;
|
|
189
|
+
}
|
|
190
|
+
const engine = new Engine(init, edges);
|
|
191
|
+
const knownIds = new Set(Object.keys(init));
|
|
192
|
+
const sources = discoverConstraints({
|
|
193
|
+
config,
|
|
194
|
+
bp: input.breakpoint,
|
|
195
|
+
constraintsDir: input.constraintsDir ?? 'themes',
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
descriptors: describeConstraints(sources),
|
|
199
|
+
// Token ids resolve to their value; anything else (a literal backdrop color)
|
|
200
|
+
// passes through, mirroring the WCAG plugin's resolveColor.
|
|
201
|
+
getValue: (idOrLiteral) => (knownIds.has(idOrLiteral) ? String(engine.get(idOrLiteral)) : idOrLiteral),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** explain / suggest-fix accept a full violation OR loose ruleId + nodes. */
|
|
205
|
+
function resolveInsightTarget(input) {
|
|
206
|
+
if (input.violation) {
|
|
207
|
+
return {
|
|
208
|
+
ruleId: input.violation.ruleId,
|
|
209
|
+
nodes: input.violation.nodes ?? [],
|
|
210
|
+
context: input.violation.context,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (input.ruleId) {
|
|
214
|
+
return {
|
|
215
|
+
ruleId: input.ruleId,
|
|
216
|
+
nodes: input.nodes ?? [],
|
|
217
|
+
context: input.context,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
throw new ToolExecutionError('invalid_input', 'Provide either a violation object or ruleId (with nodes).');
|
|
221
|
+
}
|
|
222
|
+
export async function listConstraintsTool(input) {
|
|
223
|
+
return executeTool('list-constraints', () => {
|
|
224
|
+
const { descriptors } = deriveContext(input);
|
|
225
|
+
return { ok: true, constraints: descriptors, meta: { count: descriptors.length } };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
export async function explainTool(input) {
|
|
229
|
+
return executeTool('explain', () => {
|
|
230
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
231
|
+
const target = resolveInsightTarget(input);
|
|
232
|
+
return explainInsight({ ...target, getValue, descriptors });
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
export async function suggestFixTool(input) {
|
|
236
|
+
return executeTool('suggest-fix', () => {
|
|
237
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
238
|
+
const target = resolveInsightTarget(input);
|
|
239
|
+
return suggestFixInsight({ ...target, getValue, descriptors, target: input.target });
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
export const dcvMcpTools = [
|
|
243
|
+
{
|
|
244
|
+
name: 'validate',
|
|
245
|
+
description: 'Validate DTCG-style design tokens against DCV constraints and return structured violations.',
|
|
246
|
+
inputSchema: validateInputShape,
|
|
247
|
+
handler: validateTool,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'why',
|
|
251
|
+
description: 'Explain token provenance, aliases, immediate dependencies, dependents, and alias chain.',
|
|
252
|
+
inputSchema: whyInputShape,
|
|
253
|
+
handler: whyTool,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'graph',
|
|
257
|
+
description: 'Return the token dependency graph as nodes and directed edges.',
|
|
258
|
+
inputSchema: graphInputShape,
|
|
259
|
+
handler: graphTool,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'list-constraints',
|
|
263
|
+
description: 'List the active constraints (WCAG pairs, thresholds, order/lightness scales, cross-axis) for a token set/config. Read-only.',
|
|
264
|
+
inputSchema: listConstraintsInputShape,
|
|
265
|
+
handler: listConstraintsTool,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'explain',
|
|
269
|
+
description: 'Explain a validation violation (WCAG, threshold, monotonic) in plain English plus machine-readable facts. Read-only.',
|
|
270
|
+
inputSchema: explainInputShape,
|
|
271
|
+
handler: explainTool,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'suggest-fix',
|
|
275
|
+
description: 'Compute a verified satisfying value for a violation without writing it (WCAG color, threshold/monotonic boundary). Read-only.',
|
|
276
|
+
inputSchema: suggestFixInputShape,
|
|
277
|
+
handler: suggestFixTool,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
export function registerDcvMcpTools(server) {
|
|
281
|
+
for (const tool of dcvMcpTools) {
|
|
282
|
+
server.registerTool(tool.name, {
|
|
283
|
+
description: tool.description,
|
|
284
|
+
inputSchema: tool.inputSchema,
|
|
285
|
+
}, async (input) => {
|
|
286
|
+
const result = await tool.handler(input);
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: 'text',
|
|
291
|
+
text: responseToContent(result),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
structuredContent: result,
|
|
295
|
+
...(isToolFailure(result) ? { isError: true } : {}),
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|