design-constraint-validator 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- 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 +33 -16
- package/cli/commands/graph.ts +28 -15
- 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 +5 -17
- package/cli/commands/validate.ts +10 -18
- 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 +144 -178
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +25 -5
- package/cli/config-schema.ts +27 -5
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +53 -15
- package/cli/constraint-registry.ts +53 -18
- 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 +23 -1
- package/cli/dcv.ts +23 -1
- package/cli/types.d.ts +19 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +23 -10
- package/cli/validate-api.d.ts.map +1 -1
- package/cli/validate-api.js +6 -1
- package/cli/validate-api.ts +6 -1
- 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.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.js +1 -1
- package/core/constraints/wcag.ts +1 -1
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +8 -0
- package/core/flatten.ts +9 -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/mcp/contracts.d.ts +1456 -13
- package/mcp/contracts.d.ts.map +1 -1
- package/mcp/contracts.js +45 -1
- package/mcp/contracts.ts +55 -1
- package/mcp/index.d.ts +6 -4
- package/mcp/index.d.ts.map +1 -1
- package/mcp/index.js +6 -3
- package/mcp/index.ts +28 -1
- 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 +14 -3
- package/mcp/tools.d.ts.map +1 -1
- package/mcp/tools.js +133 -6
- package/mcp/tools.ts +188 -11
- package/package.json +2 -7
- package/server.json +3 -3
package/mcp/tools.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { suggestIds } from '../core/cli-format.js';
|
|
3
3
|
import { flattenTokens } from '../core/flatten.js';
|
|
4
|
-
import { explain } from '../core/why.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';
|
|
5
9
|
import { validate } from '../cli/validate-api.js';
|
|
6
|
-
import {
|
|
10
|
+
import { describeConstraints, explain as explainInsight, suggestFix as suggestFixInsight, InsightError, } from './insights.js';
|
|
11
|
+
import { graphInputShape, validateInputShape, whyInputShape, listConstraintsInputShape, explainInputShape, suggestFixInputShape, } from './contracts.js';
|
|
7
12
|
export class ToolExecutionError extends Error {
|
|
8
13
|
code;
|
|
9
14
|
details;
|
|
@@ -28,6 +33,17 @@ function toFailure(tool, error) {
|
|
|
28
33
|
},
|
|
29
34
|
};
|
|
30
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
|
+
}
|
|
31
47
|
return {
|
|
32
48
|
ok: false,
|
|
33
49
|
tool,
|
|
@@ -73,9 +89,18 @@ function asJsonObject(value, label) {
|
|
|
73
89
|
}
|
|
74
90
|
throw new ToolExecutionError('invalid_input', `${label} must be a JSON object.`);
|
|
75
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
|
+
}
|
|
76
98
|
function resolveTokens(input) {
|
|
77
99
|
if (input.tokens !== undefined) {
|
|
78
|
-
|
|
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');
|
|
79
104
|
}
|
|
80
105
|
if (input.tokensPath !== undefined) {
|
|
81
106
|
return asJsonObject(parseJsonFile(input.tokensPath), 'tokensPath contents');
|
|
@@ -83,7 +108,17 @@ function resolveTokens(input) {
|
|
|
83
108
|
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
84
109
|
}
|
|
85
110
|
function constraints(input) {
|
|
86
|
-
|
|
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;
|
|
87
122
|
}
|
|
88
123
|
export async function validateTool(input) {
|
|
89
124
|
return executeTool('validate', () => {
|
|
@@ -91,7 +126,7 @@ export async function validateTool(input) {
|
|
|
91
126
|
throw new ToolExecutionError('invalid_input', 'Provide either tokens or tokensPath.');
|
|
92
127
|
}
|
|
93
128
|
return validate({
|
|
94
|
-
...(input.tokens !== undefined ? { tokens: input.tokens } : {}),
|
|
129
|
+
...(input.tokens !== undefined ? { tokens: asJsonObject(input.tokens, 'tokens') } : {}),
|
|
95
130
|
...(input.tokens === undefined && input.tokensPath !== undefined ? { tokensPath: input.tokensPath } : {}),
|
|
96
131
|
...(input.constraints !== undefined ? { constraints: constraints(input) } : {}),
|
|
97
132
|
...(input.configPath !== undefined ? { configPath: input.configPath } : {}),
|
|
@@ -111,7 +146,7 @@ export async function whyTool(input) {
|
|
|
111
146
|
}
|
|
112
147
|
return {
|
|
113
148
|
ok: true,
|
|
114
|
-
...
|
|
149
|
+
...explainWhy(input.tokenId, flat, edges),
|
|
115
150
|
};
|
|
116
151
|
});
|
|
117
152
|
}
|
|
@@ -130,6 +165,80 @@ export async function graphTool(input) {
|
|
|
130
165
|
};
|
|
131
166
|
});
|
|
132
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
|
+
}
|
|
133
242
|
export const dcvMcpTools = [
|
|
134
243
|
{
|
|
135
244
|
name: 'validate',
|
|
@@ -149,6 +258,24 @@ export const dcvMcpTools = [
|
|
|
149
258
|
inputSchema: graphInputShape,
|
|
150
259
|
handler: graphTool,
|
|
151
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
|
+
},
|
|
152
279
|
];
|
|
153
280
|
export function registerDcvMcpTools(server) {
|
|
154
281
|
for (const tool of dcvMcpTools) {
|
package/mcp/tools.ts
CHANGED
|
@@ -4,15 +4,44 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
4
4
|
import type { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { suggestIds } from '../core/cli-format.js';
|
|
7
|
-
import { flattenTokens, type TokenNode } from '../core/flatten.js';
|
|
8
|
-
import { explain, type WhyReport } from '../core/why.js';
|
|
7
|
+
import { flattenTokens, type TokenNode, type FlatToken } from '../core/flatten.js';
|
|
8
|
+
import { explain as explainWhy, type WhyReport } from '../core/why.js';
|
|
9
|
+
import { Engine } from '../core/engine.js';
|
|
10
|
+
import { ConstraintsSchema } from '../cli/config-schema.js';
|
|
11
|
+
import { loadConfig } from '../cli/config.js';
|
|
12
|
+
import { discoverConstraints } from '../cli/constraint-registry.js';
|
|
9
13
|
import { validate, type ValidateResult } from '../cli/validate-api.js';
|
|
10
14
|
import type { DcvConfig } from '../cli/types.js';
|
|
11
15
|
import type { Breakpoint } from '../core/breakpoints.js';
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
import {
|
|
17
|
+
describeConstraints,
|
|
18
|
+
explain as explainInsight,
|
|
19
|
+
suggestFix as suggestFixInsight,
|
|
20
|
+
InsightError,
|
|
21
|
+
type ConstraintDescriptor,
|
|
22
|
+
type ExplainResult,
|
|
23
|
+
type SuggestResult,
|
|
24
|
+
type ValueResolver,
|
|
25
|
+
} from './insights.js';
|
|
26
|
+
import type {
|
|
27
|
+
JsonObject,
|
|
28
|
+
ValidateToolInput,
|
|
29
|
+
WhyToolInput,
|
|
30
|
+
GraphToolInput,
|
|
31
|
+
ListConstraintsToolInput,
|
|
32
|
+
ExplainToolInput,
|
|
33
|
+
SuggestFixToolInput,
|
|
34
|
+
} from './contracts.js';
|
|
35
|
+
import {
|
|
36
|
+
graphInputShape,
|
|
37
|
+
validateInputShape,
|
|
38
|
+
whyInputShape,
|
|
39
|
+
listConstraintsInputShape,
|
|
40
|
+
explainInputShape,
|
|
41
|
+
suggestFixInputShape,
|
|
42
|
+
} from './contracts.js';
|
|
43
|
+
|
|
44
|
+
export type DcvMcpToolName = 'validate' | 'why' | 'graph' | 'list-constraints' | 'explain' | 'suggest-fix';
|
|
16
45
|
|
|
17
46
|
export interface ToolFailure {
|
|
18
47
|
ok: false;
|
|
@@ -52,7 +81,7 @@ export type WhyToolResult = { ok: true } & WhyReport;
|
|
|
52
81
|
interface TokenInput {
|
|
53
82
|
tokens?: JsonObject;
|
|
54
83
|
tokensPath?: string;
|
|
55
|
-
constraints?:
|
|
84
|
+
constraints?: unknown;
|
|
56
85
|
configPath?: string;
|
|
57
86
|
constraintsDir?: string;
|
|
58
87
|
breakpoint?: Breakpoint;
|
|
@@ -82,6 +111,18 @@ function toFailure(tool: DcvMcpToolName, error: unknown): ToolFailure {
|
|
|
82
111
|
};
|
|
83
112
|
}
|
|
84
113
|
|
|
114
|
+
// Pure derivation errors (bad input, unsupported rule) carry their own code.
|
|
115
|
+
if (error instanceof InsightError) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
tool,
|
|
119
|
+
error: {
|
|
120
|
+
code: error.code,
|
|
121
|
+
message: error.message,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
85
126
|
return {
|
|
86
127
|
ok: false,
|
|
87
128
|
tool,
|
|
@@ -136,9 +177,19 @@ function asJsonObject(value: unknown, label: string): JsonObject {
|
|
|
136
177
|
throw new ToolExecutionError('invalid_input', `${label} must be a JSON object.`);
|
|
137
178
|
}
|
|
138
179
|
|
|
180
|
+
function zodIssueMessages(error: z.ZodError): string[] {
|
|
181
|
+
return error.issues.map((issue) => {
|
|
182
|
+
const path = issue.path.length > 0 ? `.${issue.path.join('.')}` : '';
|
|
183
|
+
return `constraints${path}: ${issue.message}`;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
139
187
|
function resolveTokens(input: TokenInput): TokenNode {
|
|
140
188
|
if (input.tokens !== undefined) {
|
|
141
|
-
|
|
189
|
+
// TASK-017: validate inline tokens at the handler boundary. A direct caller
|
|
190
|
+
// (not going through the MCP SDK's schema check) could otherwise pass an
|
|
191
|
+
// array/null/scalar that silently flattened to an empty, passing set.
|
|
192
|
+
return asJsonObject(input.tokens, 'tokens') as unknown as TokenNode;
|
|
142
193
|
}
|
|
143
194
|
|
|
144
195
|
if (input.tokensPath !== undefined) {
|
|
@@ -149,7 +200,16 @@ function resolveTokens(input: TokenInput): TokenNode {
|
|
|
149
200
|
}
|
|
150
201
|
|
|
151
202
|
function constraints(input: TokenInput): DcvConfig['constraints'] | undefined {
|
|
152
|
-
|
|
203
|
+
if (input.constraints === undefined) return undefined;
|
|
204
|
+
// TASK-017: reject malformed inline constraints at the boundary.
|
|
205
|
+
const object = asJsonObject(input.constraints, 'constraints');
|
|
206
|
+
const parsed = ConstraintsSchema.safeParse(object);
|
|
207
|
+
if (!parsed.success) {
|
|
208
|
+
throw new ToolExecutionError('invalid_input', 'constraints must match DCV constraint config schema.', {
|
|
209
|
+
issues: zodIssueMessages(parsed.error),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return parsed.data;
|
|
153
213
|
}
|
|
154
214
|
|
|
155
215
|
export async function validateTool(input: ValidateToolInput): Promise<ToolResponse<ValidateResult>> {
|
|
@@ -159,7 +219,7 @@ export async function validateTool(input: ValidateToolInput): Promise<ToolRespon
|
|
|
159
219
|
}
|
|
160
220
|
|
|
161
221
|
return validate({
|
|
162
|
-
...(input.tokens !== undefined ? { tokens: input.tokens as unknown as TokenNode } : {}),
|
|
222
|
+
...(input.tokens !== undefined ? { tokens: asJsonObject(input.tokens, 'tokens') as unknown as TokenNode } : {}),
|
|
163
223
|
...(input.tokens === undefined && input.tokensPath !== undefined ? { tokensPath: input.tokensPath } : {}),
|
|
164
224
|
...(input.constraints !== undefined ? { constraints: constraints(input) } : {}),
|
|
165
225
|
...(input.configPath !== undefined ? { configPath: input.configPath } : {}),
|
|
@@ -181,7 +241,7 @@ export async function whyTool(input: WhyToolInput): Promise<ToolResponse<WhyTool
|
|
|
181
241
|
|
|
182
242
|
return {
|
|
183
243
|
ok: true as const,
|
|
184
|
-
...
|
|
244
|
+
...explainWhy(input.tokenId, flat, edges),
|
|
185
245
|
};
|
|
186
246
|
});
|
|
187
247
|
}
|
|
@@ -203,10 +263,109 @@ export async function graphTool(input: GraphToolInput): Promise<ToolResponse<Gra
|
|
|
203
263
|
});
|
|
204
264
|
}
|
|
205
265
|
|
|
266
|
+
export interface ListConstraintsResult {
|
|
267
|
+
ok: true;
|
|
268
|
+
constraints: ConstraintDescriptor[];
|
|
269
|
+
meta: { count: number };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Resolve the constraint config the same way validate() does: inline → configPath
|
|
273
|
+
* → discovered cwd config. */
|
|
274
|
+
function resolveConstraintsConfig(input: TokenInput): DcvConfig {
|
|
275
|
+
if (input.constraints !== undefined) {
|
|
276
|
+
return { constraints: constraints(input) };
|
|
277
|
+
}
|
|
278
|
+
const res = loadConfig(input.configPath);
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
throw new ToolExecutionError('invalid_config', res.error);
|
|
281
|
+
}
|
|
282
|
+
return res.value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface DerivedContext {
|
|
286
|
+
descriptors: ConstraintDescriptor[];
|
|
287
|
+
getValue: ValueResolver;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Flatten tokens, build an engine for value resolution, and discover the active
|
|
291
|
+
* constraint sources — the shared substrate for the read-only insight tools. */
|
|
292
|
+
function deriveContext(input: TokenInput): DerivedContext {
|
|
293
|
+
const tokens = resolveTokens(input);
|
|
294
|
+
const config = resolveConstraintsConfig(input);
|
|
295
|
+
|
|
296
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
297
|
+
const init: Record<string, string | number> = {};
|
|
298
|
+
for (const t of Object.values(flat)) {
|
|
299
|
+
init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
300
|
+
}
|
|
301
|
+
const engine = new Engine(init, edges);
|
|
302
|
+
const knownIds = new Set(Object.keys(init));
|
|
303
|
+
|
|
304
|
+
const sources = discoverConstraints({
|
|
305
|
+
config,
|
|
306
|
+
bp: input.breakpoint,
|
|
307
|
+
constraintsDir: input.constraintsDir ?? 'themes',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
descriptors: describeConstraints(sources),
|
|
312
|
+
// Token ids resolve to their value; anything else (a literal backdrop color)
|
|
313
|
+
// passes through, mirroring the WCAG plugin's resolveColor.
|
|
314
|
+
getValue: (idOrLiteral) => (knownIds.has(idOrLiteral) ? String(engine.get(idOrLiteral)) : idOrLiteral),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** explain / suggest-fix accept a full violation OR loose ruleId + nodes. */
|
|
319
|
+
function resolveInsightTarget(
|
|
320
|
+
input: ExplainToolInput | SuggestFixToolInput,
|
|
321
|
+
): { ruleId: string; nodes: string[]; context?: Record<string, unknown> } {
|
|
322
|
+
if (input.violation) {
|
|
323
|
+
return {
|
|
324
|
+
ruleId: input.violation.ruleId,
|
|
325
|
+
nodes: input.violation.nodes ?? [],
|
|
326
|
+
context: input.violation.context as Record<string, unknown> | undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (input.ruleId) {
|
|
330
|
+
return {
|
|
331
|
+
ruleId: input.ruleId,
|
|
332
|
+
nodes: input.nodes ?? [],
|
|
333
|
+
context: input.context as Record<string, unknown> | undefined,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new ToolExecutionError('invalid_input', 'Provide either a violation object or ruleId (with nodes).');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function listConstraintsTool(input: ListConstraintsToolInput): Promise<ToolResponse<ListConstraintsResult>> {
|
|
340
|
+
return executeTool('list-constraints', () => {
|
|
341
|
+
const { descriptors } = deriveContext(input);
|
|
342
|
+
return { ok: true as const, constraints: descriptors, meta: { count: descriptors.length } };
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function explainTool(input: ExplainToolInput): Promise<ToolResponse<ExplainResult>> {
|
|
347
|
+
return executeTool('explain', () => {
|
|
348
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
349
|
+
const target = resolveInsightTarget(input);
|
|
350
|
+
return explainInsight({ ...target, getValue, descriptors });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function suggestFixTool(input: SuggestFixToolInput): Promise<ToolResponse<SuggestResult>> {
|
|
355
|
+
return executeTool('suggest-fix', () => {
|
|
356
|
+
const { descriptors, getValue } = deriveContext(input);
|
|
357
|
+
const target = resolveInsightTarget(input);
|
|
358
|
+
return suggestFixInsight({ ...target, getValue, descriptors, target: input.target });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
206
362
|
export const dcvMcpTools: Array<
|
|
207
363
|
| ToolDefinition<ValidateToolInput, ValidateResult>
|
|
208
364
|
| ToolDefinition<WhyToolInput, WhyToolResult>
|
|
209
365
|
| ToolDefinition<GraphToolInput, GraphToolResult>
|
|
366
|
+
| ToolDefinition<ListConstraintsToolInput, ListConstraintsResult>
|
|
367
|
+
| ToolDefinition<ExplainToolInput, ExplainResult>
|
|
368
|
+
| ToolDefinition<SuggestFixToolInput, SuggestResult>
|
|
210
369
|
> = [
|
|
211
370
|
{
|
|
212
371
|
name: 'validate',
|
|
@@ -226,6 +385,24 @@ export const dcvMcpTools: Array<
|
|
|
226
385
|
inputSchema: graphInputShape,
|
|
227
386
|
handler: graphTool,
|
|
228
387
|
},
|
|
388
|
+
{
|
|
389
|
+
name: 'list-constraints',
|
|
390
|
+
description: 'List the active constraints (WCAG pairs, thresholds, order/lightness scales, cross-axis) for a token set/config. Read-only.',
|
|
391
|
+
inputSchema: listConstraintsInputShape,
|
|
392
|
+
handler: listConstraintsTool,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'explain',
|
|
396
|
+
description: 'Explain a validation violation (WCAG, threshold, monotonic) in plain English plus machine-readable facts. Read-only.',
|
|
397
|
+
inputSchema: explainInputShape,
|
|
398
|
+
handler: explainTool,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: 'suggest-fix',
|
|
402
|
+
description: 'Compute a verified satisfying value for a violation without writing it (WCAG color, threshold/monotonic boundary). Read-only.',
|
|
403
|
+
inputSchema: suggestFixInputShape,
|
|
404
|
+
handler: suggestFixTool,
|
|
405
|
+
},
|
|
229
406
|
];
|
|
230
407
|
|
|
231
408
|
export function registerDcvMcpTools(server: McpServer): void {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-constraint-validator",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Mathematical constraint validator for design systems — ensuring consistency, accessibility, and logical coherence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -26,16 +26,11 @@
|
|
|
26
26
|
"design-constraint-validator": "./cli/index.js",
|
|
27
27
|
"dcv-mcp": "./mcp/index.js"
|
|
28
28
|
},
|
|
29
|
-
"mcpName": "io.github.
|
|
29
|
+
"mcpName": "io.github.CseperkePapp/design-constraint-validator",
|
|
30
30
|
"scripts": {
|
|
31
31
|
"test": "vitest run --exclude \"**/*.test.js\"",
|
|
32
32
|
"test:watch": "vitest --exclude \"**/*.test.js\"",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
|
-
"workflow:typecheck": "tsc -p tsconfig.workflow-automation.json --noEmit",
|
|
35
|
-
"validate-headers": "tsx scripts/validate-headers.ts",
|
|
36
|
-
"workflow:test": "vitest run src/__tests__/task-workflow-integrity.test.ts src/__tests__/sync-task-index.test.ts",
|
|
37
|
-
"task:sync": "tsx scripts/sync-task-index.ts",
|
|
38
|
-
"rename-done-tasks": "tsx scripts/rename-done-tasks.ts",
|
|
39
34
|
"build": "tsc",
|
|
40
35
|
"lint": "eslint . --ext .js,.ts,.mjs,.cjs",
|
|
41
36
|
"format": "prettier -w .",
|
package/server.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.
|
|
3
|
+
"name": "io.github.CseperkePapp/design-constraint-validator",
|
|
4
4
|
"title": "Design Constraint Validator",
|
|
5
5
|
"description": "Validate design tokens for accessibility, scales, and design-system constraint consistency.",
|
|
6
|
-
"version": "2.1
|
|
6
|
+
"version": "2.2.1",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/CseperkePapp/design-constraint-validator",
|
|
9
9
|
"source": "github"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "design-constraint-validator",
|
|
15
|
-
"version": "2.1
|
|
15
|
+
"version": "2.2.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|