dslinter 0.1.13 → 0.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/CHANGELOG.md +43 -0
- package/README.md +50 -29
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +96 -8
- package/bin/lib/scaffold-config.test.mjs +12 -2
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +209 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +51 -3
- package/vite/collectScanModules.ts +85 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { buildAgentContext } from "./agent-context";
|
|
6
|
+
import {
|
|
7
|
+
catalogSummary,
|
|
8
|
+
componentSpec,
|
|
9
|
+
findingsQuery,
|
|
10
|
+
governanceSummary,
|
|
11
|
+
policyFromReport,
|
|
12
|
+
tokenSummary,
|
|
13
|
+
usageExamples,
|
|
14
|
+
} from "./agent-query";
|
|
15
|
+
import type { McpConfig } from "./config";
|
|
16
|
+
import { ReportCache, loadBaseline, saveBaseline } from "./report-cache";
|
|
17
|
+
import {
|
|
18
|
+
agentContextInput,
|
|
19
|
+
catalogInput,
|
|
20
|
+
checkPathsInput,
|
|
21
|
+
componentInput,
|
|
22
|
+
diffSinceInput,
|
|
23
|
+
findingsInput,
|
|
24
|
+
scanInput,
|
|
25
|
+
suggestFixInput,
|
|
26
|
+
tokensInput,
|
|
27
|
+
usageExamplesInput,
|
|
28
|
+
} from "./schemas";
|
|
29
|
+
import {
|
|
30
|
+
computeDrift,
|
|
31
|
+
findingsForPaths,
|
|
32
|
+
suggestFix,
|
|
33
|
+
} from "./verify-loop";
|
|
34
|
+
|
|
35
|
+
function jsonResult(data: unknown, isError = false): CallToolResult {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
38
|
+
...(isError ? { isError: true } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDslinterMcpServer(
|
|
43
|
+
config: McpConfig,
|
|
44
|
+
cache: ReportCache,
|
|
45
|
+
): McpServer {
|
|
46
|
+
const server = new McpServer({
|
|
47
|
+
name: "dslinter",
|
|
48
|
+
version: "0.1.0",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.registerTool(
|
|
52
|
+
"dslinter_scan",
|
|
53
|
+
{
|
|
54
|
+
description:
|
|
55
|
+
"Refresh or load the DSLinter workspace report. Returns governance scores and finding counts.",
|
|
56
|
+
inputSchema: scanInput,
|
|
57
|
+
},
|
|
58
|
+
async (args) => {
|
|
59
|
+
const report = await cache.getReport({ fresh: args.fresh ?? false });
|
|
60
|
+
const gov = governanceSummary(report);
|
|
61
|
+
return jsonResult({
|
|
62
|
+
schema_version: report.schema_version,
|
|
63
|
+
generated_at: report.generated_at,
|
|
64
|
+
source: "scan",
|
|
65
|
+
scores: gov.scores,
|
|
66
|
+
finding_counts: gov.finding_counts,
|
|
67
|
+
total_findings: gov.total_findings,
|
|
68
|
+
component_count: catalogSummary(report, { limit: 10_000 }).length,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.registerTool(
|
|
74
|
+
"dslinter_get_catalog",
|
|
75
|
+
{
|
|
76
|
+
description:
|
|
77
|
+
"List design-system components sorted by usage frequency, with import paths and flags.",
|
|
78
|
+
inputSchema: catalogInput,
|
|
79
|
+
},
|
|
80
|
+
async (args) => {
|
|
81
|
+
const report = await cache.getReport();
|
|
82
|
+
return jsonResult(catalogSummary(report, args));
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
server.registerTool(
|
|
87
|
+
"dslinter_get_component",
|
|
88
|
+
{
|
|
89
|
+
description:
|
|
90
|
+
"Full component spec: props, CVA variants, usage, findings, and example JSX from repo patterns.",
|
|
91
|
+
inputSchema: componentInput,
|
|
92
|
+
},
|
|
93
|
+
async (args) => {
|
|
94
|
+
const report = await cache.getReport();
|
|
95
|
+
const spec = componentSpec(report, args.name);
|
|
96
|
+
if (!spec) {
|
|
97
|
+
return jsonResult({ error: `Component not found: ${args.name}` }, true);
|
|
98
|
+
}
|
|
99
|
+
return jsonResult(spec);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
server.registerTool(
|
|
104
|
+
"dslinter_get_findings",
|
|
105
|
+
{
|
|
106
|
+
description: "Filtered governance findings by component, rule prefix, severity, or path.",
|
|
107
|
+
inputSchema: findingsInput,
|
|
108
|
+
},
|
|
109
|
+
async (args) => {
|
|
110
|
+
const report = await cache.getReport();
|
|
111
|
+
return jsonResult(findingsQuery(report, args));
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
server.registerTool(
|
|
116
|
+
"dslinter_get_usage_examples",
|
|
117
|
+
{
|
|
118
|
+
description:
|
|
119
|
+
"Real call sites and prop value frequencies for a component from repo usage data.",
|
|
120
|
+
inputSchema: usageExamplesInput,
|
|
121
|
+
},
|
|
122
|
+
async (args) => {
|
|
123
|
+
const report = await cache.getReport();
|
|
124
|
+
const examples = usageExamples(report, args.component, args.limit);
|
|
125
|
+
if (!examples) {
|
|
126
|
+
return jsonResult(
|
|
127
|
+
{ error: `No usage data for component: ${args.component}` },
|
|
128
|
+
true,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return jsonResult(examples);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
server.registerTool(
|
|
136
|
+
"dslinter_get_tokens",
|
|
137
|
+
{
|
|
138
|
+
description: "CSS design tokens: definitions, usage counts, and unused tokens.",
|
|
139
|
+
inputSchema: tokensInput,
|
|
140
|
+
},
|
|
141
|
+
async (args) => {
|
|
142
|
+
const report = await cache.getReport();
|
|
143
|
+
return jsonResult(tokenSummary(report, args.category));
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.registerTool(
|
|
148
|
+
"dslinter_get_agent_context",
|
|
149
|
+
{
|
|
150
|
+
description:
|
|
151
|
+
"Compact design-system context pack for LLM system prompts (scores, top components, policy, dos/donts).",
|
|
152
|
+
inputSchema: agentContextInput,
|
|
153
|
+
},
|
|
154
|
+
async (args) => {
|
|
155
|
+
const report = await cache.getReport();
|
|
156
|
+
const context = buildAgentContext(report, args);
|
|
157
|
+
if (args.format === "json") {
|
|
158
|
+
return jsonResult(context);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: String(context) }],
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.registerTool(
|
|
167
|
+
"dslinter_get_policy",
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
"Effective governance policy from .dslinter.json snapshot and rule catalog.",
|
|
171
|
+
},
|
|
172
|
+
async () => {
|
|
173
|
+
const report = await cache.getReport();
|
|
174
|
+
return jsonResult(policyFromReport(report));
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
server.registerTool(
|
|
179
|
+
"dslinter_check_paths",
|
|
180
|
+
{
|
|
181
|
+
description:
|
|
182
|
+
"Findings for specific file paths after agent edits (post-write verification).",
|
|
183
|
+
inputSchema: checkPathsInput,
|
|
184
|
+
},
|
|
185
|
+
async (args) => {
|
|
186
|
+
const report = await cache.getReport({ fresh: args.fresh ?? true });
|
|
187
|
+
return jsonResult(findingsForPaths(report, args.paths));
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
server.registerTool(
|
|
192
|
+
"dslinter_diff_since",
|
|
193
|
+
{
|
|
194
|
+
description:
|
|
195
|
+
"Compare current governance scores/findings to saved MCP baseline (.dslinter/mcp-baseline.json).",
|
|
196
|
+
inputSchema: diffSinceInput,
|
|
197
|
+
},
|
|
198
|
+
async (args) => {
|
|
199
|
+
const report = await cache.getReport({ fresh: args.fresh ?? false });
|
|
200
|
+
const baseline = await loadBaseline(config.projectRoot);
|
|
201
|
+
const drift = computeDrift(report, baseline);
|
|
202
|
+
if (args.save_baseline) {
|
|
203
|
+
const hash = cache.reportHash();
|
|
204
|
+
if (hash) await saveBaseline(config.projectRoot, report, hash);
|
|
205
|
+
}
|
|
206
|
+
return jsonResult(drift);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.registerTool(
|
|
211
|
+
"dslinter_suggest_fix",
|
|
212
|
+
{
|
|
213
|
+
description:
|
|
214
|
+
"Heuristic fix suggestion for a rule id (deprecated components, tokens, a11y).",
|
|
215
|
+
inputSchema: suggestFixInput,
|
|
216
|
+
},
|
|
217
|
+
async (args) => {
|
|
218
|
+
const report = await cache.getReport();
|
|
219
|
+
const fix = suggestFix(report, args);
|
|
220
|
+
if (!fix) {
|
|
221
|
+
return jsonResult({ error: "No suggestion available" }, true);
|
|
222
|
+
}
|
|
223
|
+
return jsonResult(fix);
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
server.registerResource(
|
|
228
|
+
"agent-context",
|
|
229
|
+
"dslinter://context",
|
|
230
|
+
{
|
|
231
|
+
description: "Compact design-system agent context (markdown)",
|
|
232
|
+
mimeType: "text/markdown",
|
|
233
|
+
},
|
|
234
|
+
async () => ({
|
|
235
|
+
contents: [
|
|
236
|
+
{
|
|
237
|
+
uri: "dslinter://context",
|
|
238
|
+
mimeType: "text/markdown",
|
|
239
|
+
text: String(
|
|
240
|
+
buildAgentContext(await cache.getReport(), { format: "markdown" }),
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.registerResource(
|
|
248
|
+
"catalog",
|
|
249
|
+
"dslinter://catalog",
|
|
250
|
+
{
|
|
251
|
+
description: "Component catalog sorted by usage",
|
|
252
|
+
mimeType: "application/json",
|
|
253
|
+
},
|
|
254
|
+
async () => ({
|
|
255
|
+
contents: [
|
|
256
|
+
{
|
|
257
|
+
uri: "dslinter://catalog",
|
|
258
|
+
mimeType: "application/json",
|
|
259
|
+
text: JSON.stringify(
|
|
260
|
+
catalogSummary(await cache.getReport(), { limit: 200 }),
|
|
261
|
+
null,
|
|
262
|
+
2,
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
server.registerResource(
|
|
270
|
+
"component",
|
|
271
|
+
{
|
|
272
|
+
uriTemplate: "dslinter://component/{name}",
|
|
273
|
+
name: "component",
|
|
274
|
+
description: "Single component spec by name",
|
|
275
|
+
mimeType: "application/json",
|
|
276
|
+
},
|
|
277
|
+
async (uri, { name }) => {
|
|
278
|
+
const report = await cache.getReport();
|
|
279
|
+
const spec = componentSpec(report, name);
|
|
280
|
+
return {
|
|
281
|
+
contents: [
|
|
282
|
+
{
|
|
283
|
+
uri: uri.href,
|
|
284
|
+
mimeType: "application/json",
|
|
285
|
+
text: JSON.stringify(
|
|
286
|
+
spec ?? { error: `Component not found: ${name}` },
|
|
287
|
+
null,
|
|
288
|
+
2,
|
|
289
|
+
),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
server.registerPrompt(
|
|
297
|
+
"design-system-ui-task",
|
|
298
|
+
{
|
|
299
|
+
description:
|
|
300
|
+
"System prompt template for building UI with the repo design system.",
|
|
301
|
+
argsSchema: {
|
|
302
|
+
task: z.string().describe("What UI to build"),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
async ({ task }) => {
|
|
306
|
+
const context = buildAgentContext(await cache.getReport(), {
|
|
307
|
+
format: "markdown",
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
messages: [
|
|
311
|
+
{
|
|
312
|
+
role: "user",
|
|
313
|
+
content: {
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `You are building UI in this repository. Use only catalog components and match repo usage patterns.\n\n${context}\n\nTask: ${task}`,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return server;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function runMcpServer(config: McpConfig): Promise<void> {
|
|
327
|
+
const cache = new ReportCache(config);
|
|
328
|
+
cache.startDevWatch();
|
|
329
|
+
const server = createDslinterMcpServer(config, cache);
|
|
330
|
+
const transport = new StdioServerTransport();
|
|
331
|
+
await server.connect(transport);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function runMcpSelfTest(config: McpConfig): Promise<void> {
|
|
335
|
+
const cache = new ReportCache(config);
|
|
336
|
+
const report = await cache.getReport({ fresh: false });
|
|
337
|
+
const catalog = catalogSummary(report, { limit: 5 });
|
|
338
|
+
if (catalog.length === 0 && (report.files?.length ?? 0) === 0) {
|
|
339
|
+
throw new Error("Self-test: empty report");
|
|
340
|
+
}
|
|
341
|
+
governanceSummary(report);
|
|
342
|
+
buildAgentContext(report);
|
|
343
|
+
}
|
package/src/mcp/start.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP stdio server entry (run via `node --experimental-strip-types` or vitest).
|
|
4
|
+
*/
|
|
5
|
+
import { buildMcpConfig } from "./config";
|
|
6
|
+
import { runMcpSelfTest, runMcpServer } from "./server";
|
|
7
|
+
|
|
8
|
+
export type McpStartOptions = {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
scanPath: string;
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
reportPath?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function startMcp(opts: McpStartOptions): Promise<void> {
|
|
16
|
+
const config = buildMcpConfig({
|
|
17
|
+
cwd: opts.cwd,
|
|
18
|
+
scanPath: opts.scanPath,
|
|
19
|
+
projectRoot: opts.projectRoot,
|
|
20
|
+
reportPath: opts.reportPath,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (process.argv.includes("--self-test")) {
|
|
24
|
+
await runMcpSelfTest(config);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await runMcpServer(config);
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { computeDrift, suggestFix } from "./verify-loop";
|
|
6
|
+
import { normalizeReportPaths } from "./normalize-paths";
|
|
7
|
+
import type { WorkspaceReport } from "../types/report";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const demoReportPath = join(__dirname, "../../../../demo/public/dslinter-report.json");
|
|
11
|
+
|
|
12
|
+
function loadDemoReport(): WorkspaceReport {
|
|
13
|
+
const raw = readFileSync(demoReportPath, "utf8");
|
|
14
|
+
return normalizeReportPaths(JSON.parse(raw) as WorkspaceReport);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("verify-loop", () => {
|
|
18
|
+
it("computes drift from baseline", () => {
|
|
19
|
+
const report = loadDemoReport();
|
|
20
|
+
const baseline = {
|
|
21
|
+
saved_at: "2020-01-01T00:00:00Z",
|
|
22
|
+
scores: {
|
|
23
|
+
design_system_health: 100,
|
|
24
|
+
ux_consistency: 100,
|
|
25
|
+
accessibility: 100,
|
|
26
|
+
maintainability: 100,
|
|
27
|
+
},
|
|
28
|
+
finding_count: 0,
|
|
29
|
+
};
|
|
30
|
+
const drift = computeDrift(report, baseline);
|
|
31
|
+
expect(drift.finding_delta).toBeGreaterThan(0);
|
|
32
|
+
expect(drift.score_deltas.design_system_health).toBeLessThan(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("suggests fix for deprecated component", () => {
|
|
36
|
+
const report = loadDemoReport();
|
|
37
|
+
const fix = suggestFix(report, {
|
|
38
|
+
rule_id: "deprecated-component",
|
|
39
|
+
component: "LegacyButton",
|
|
40
|
+
});
|
|
41
|
+
expect(fix?.suggestion).toMatch(/LegacyButton|Replace/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("suggests fix for hardcoded color", () => {
|
|
45
|
+
const report = loadDemoReport();
|
|
46
|
+
const fix = suggestFix(report, { rule_id: "token-hardcoded-color" });
|
|
47
|
+
expect(fix?.fix_hint).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { catalogSummary, componentSpec, findingsForPaths } from "./agent-query";
|
|
2
|
+
import { ruleById } from "./rule-catalog";
|
|
3
|
+
import type { LintFinding, WorkspaceReport } from "../types/report";
|
|
4
|
+
|
|
5
|
+
export type FixSuggestion = {
|
|
6
|
+
rule_id: string;
|
|
7
|
+
fix_hint: string;
|
|
8
|
+
suggestion: string;
|
|
9
|
+
replacement_component?: string;
|
|
10
|
+
replacement_import?: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function suggestFix(
|
|
15
|
+
report: WorkspaceReport,
|
|
16
|
+
opts: { rule_id: string; path?: string; component?: string },
|
|
17
|
+
): FixSuggestion | null {
|
|
18
|
+
const entry = ruleById(opts.rule_id);
|
|
19
|
+
const fix_hint = entry?.fix_hint ?? "Review the finding and align with design system conventions.";
|
|
20
|
+
|
|
21
|
+
if (opts.rule_id === "deprecated-component" && opts.component) {
|
|
22
|
+
const snap = report.config_snapshot?.deprecated_components ?? [];
|
|
23
|
+
const catalog = catalogSummary(report, { limit: 20 });
|
|
24
|
+
const replacement = catalog.find(
|
|
25
|
+
(c) =>
|
|
26
|
+
!c.deprecated &&
|
|
27
|
+
!snap.includes(c.name) &&
|
|
28
|
+
c.name.toLowerCase().includes(
|
|
29
|
+
opts.component!.replace(/^Legacy|Deprecated/i, "").toLowerCase(),
|
|
30
|
+
),
|
|
31
|
+
) ?? catalog.find((c) => !c.deprecated && c.reference_count > 0);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
rule_id: opts.rule_id,
|
|
35
|
+
fix_hint,
|
|
36
|
+
suggestion: replacement
|
|
37
|
+
? `Replace \`${opts.component}\` with \`${replacement.name}\` from ${replacement.import_path ?? "the catalog"}.`
|
|
38
|
+
: `Remove usage of deprecated \`${opts.component}\`; pick a catalog component with similar usage.`,
|
|
39
|
+
replacement_component: replacement?.name,
|
|
40
|
+
replacement_import: replacement?.import_path ?? undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (opts.rule_id === "token-hardcoded-color") {
|
|
45
|
+
const colorToken = report.css_tokens?.definitions.find(
|
|
46
|
+
(d) => d.category === "color",
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
rule_id: opts.rule_id,
|
|
50
|
+
fix_hint,
|
|
51
|
+
suggestion: colorToken
|
|
52
|
+
? `Use theme token \`${colorToken.name}\` or Tailwind utility bound to it instead of a hardcoded color.`
|
|
53
|
+
: "Replace hardcoded color with a CSS variable or Tailwind theme utility.",
|
|
54
|
+
token: colorToken?.name,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (opts.rule_id === "a11y-img-alt" && opts.path) {
|
|
59
|
+
const decorative =
|
|
60
|
+
/avatar|icon|logo|decorative|spacer/i.test(opts.path) ||
|
|
61
|
+
/AvatarImage|Icon|Logo/i.test(opts.path);
|
|
62
|
+
return {
|
|
63
|
+
rule_id: opts.rule_id,
|
|
64
|
+
fix_hint,
|
|
65
|
+
suggestion: decorative
|
|
66
|
+
? 'Add alt="" for decorative images.'
|
|
67
|
+
: "Add a descriptive alt attribute summarizing the image content.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (opts.rule_id === "duplicate-component" && opts.component) {
|
|
72
|
+
const spec = componentSpec(report, opts.component);
|
|
73
|
+
return {
|
|
74
|
+
rule_id: opts.rule_id,
|
|
75
|
+
fix_hint,
|
|
76
|
+
suggestion: spec?.duplicates
|
|
77
|
+
? `Consolidate definitions at: ${spec.duplicates.join(", ")}`
|
|
78
|
+
: "Keep one canonical definition and update imports.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
rule_id: opts.rule_id,
|
|
84
|
+
fix_hint,
|
|
85
|
+
suggestion: fix_hint,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type DriftSummary = {
|
|
90
|
+
baseline: { saved_at: string; scores: WorkspaceReport["scores"]; finding_count: number } | null;
|
|
91
|
+
current: { scores: WorkspaceReport["scores"]; finding_count: number };
|
|
92
|
+
score_deltas: Record<keyof WorkspaceReport["scores"], number>;
|
|
93
|
+
finding_delta: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function computeDrift(
|
|
97
|
+
report: WorkspaceReport,
|
|
98
|
+
baseline: {
|
|
99
|
+
saved_at: string;
|
|
100
|
+
scores: WorkspaceReport["scores"];
|
|
101
|
+
finding_count: number;
|
|
102
|
+
} | null,
|
|
103
|
+
): DriftSummary {
|
|
104
|
+
const currentCount = report.findings?.length ?? 0;
|
|
105
|
+
const score_deltas = {
|
|
106
|
+
design_system_health:
|
|
107
|
+
report.scores.design_system_health -
|
|
108
|
+
(baseline?.scores.design_system_health ?? report.scores.design_system_health),
|
|
109
|
+
ux_consistency:
|
|
110
|
+
report.scores.ux_consistency -
|
|
111
|
+
(baseline?.scores.ux_consistency ?? report.scores.ux_consistency),
|
|
112
|
+
accessibility:
|
|
113
|
+
report.scores.accessibility -
|
|
114
|
+
(baseline?.scores.accessibility ?? report.scores.accessibility),
|
|
115
|
+
maintainability:
|
|
116
|
+
report.scores.maintainability -
|
|
117
|
+
(baseline?.scores.maintainability ?? report.scores.maintainability),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
baseline: baseline
|
|
122
|
+
? {
|
|
123
|
+
saved_at: baseline.saved_at,
|
|
124
|
+
scores: baseline.scores,
|
|
125
|
+
finding_count: baseline.finding_count,
|
|
126
|
+
}
|
|
127
|
+
: null,
|
|
128
|
+
current: { scores: report.scores, finding_count: currentCount },
|
|
129
|
+
score_deltas,
|
|
130
|
+
finding_delta: currentCount - (baseline?.finding_count ?? currentCount),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function findingsWithSuggestions(
|
|
135
|
+
findings: LintFinding[],
|
|
136
|
+
report: WorkspaceReport,
|
|
137
|
+
): Array<LintFinding & { suggestion?: FixSuggestion }> {
|
|
138
|
+
return findings.map((f) => {
|
|
139
|
+
const componentMatch = f.message.match(/`([A-Z][A-Za-z0-9_]*)`/);
|
|
140
|
+
const suggestion = suggestFix(report, {
|
|
141
|
+
rule_id: f.rule_id,
|
|
142
|
+
path: f.path,
|
|
143
|
+
component: componentMatch?.[1],
|
|
144
|
+
});
|
|
145
|
+
return suggestion ? { ...f, suggestion } : f;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { findingsForPaths };
|