@decantr/mcp-server 2.5.0 → 3.0.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 +28 -15
- package/dist/bin.js +1 -1
- package/dist/{chunk-J2UBHHEI.js → chunk-ZGTZEPMH.js} +1902 -81
- package/dist/index.js +1 -1
- package/package.json +11 -8
|
@@ -5,9 +5,22 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
5
5
|
|
|
6
6
|
// src/tools.ts
|
|
7
7
|
import { execFileSync } from "child_process";
|
|
8
|
+
import { createHash } from "crypto";
|
|
8
9
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
10
|
import { readFile as readFile2 } from "fs/promises";
|
|
10
|
-
import { basename as basename2, dirname as dirname2, join as join2, relative as relative2 } from "path";
|
|
11
|
+
import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2 } from "path";
|
|
12
|
+
import {
|
|
13
|
+
buildGraphImpactContext,
|
|
14
|
+
buildGraphRouteContext,
|
|
15
|
+
createMemoryGraphStore,
|
|
16
|
+
diffGraphSnapshots,
|
|
17
|
+
GRAPH_NODE_TYPES,
|
|
18
|
+
GRAPH_RELATIONS,
|
|
19
|
+
graphPayloadString,
|
|
20
|
+
sortGraphEdges,
|
|
21
|
+
sortGraphNodes,
|
|
22
|
+
summarizeGraphDiff
|
|
23
|
+
} from "@decantr/core";
|
|
11
24
|
import { evaluateGuard, isV4 as isV42, validateEssence } from "@decantr/essence-spec";
|
|
12
25
|
import {
|
|
13
26
|
isContentIntelligenceSource,
|
|
@@ -15,6 +28,12 @@ import {
|
|
|
15
28
|
rankPatternCandidates,
|
|
16
29
|
resolvePatternPreset
|
|
17
30
|
} from "@decantr/registry";
|
|
31
|
+
import {
|
|
32
|
+
anchorFindingsToGraph,
|
|
33
|
+
buildProjectHealthRepairPlan,
|
|
34
|
+
deriveVerificationDiagnostic,
|
|
35
|
+
KNOWN_VERIFICATION_DIAGNOSTICS
|
|
36
|
+
} from "@decantr/verifier";
|
|
18
37
|
|
|
19
38
|
// src/helpers.ts
|
|
20
39
|
import { realpathSync } from "fs";
|
|
@@ -138,6 +157,516 @@ function readJsonIfExists(path) {
|
|
|
138
157
|
return null;
|
|
139
158
|
}
|
|
140
159
|
}
|
|
160
|
+
function graphProjectRoot(args) {
|
|
161
|
+
const projectPath = args.project_path;
|
|
162
|
+
return typeof projectPath === "string" && projectPath.trim() ? resolveWorkspacePath(projectPath) : process.cwd();
|
|
163
|
+
}
|
|
164
|
+
function graphArtifactPath(projectRoot, file) {
|
|
165
|
+
return join2(projectRoot, ".decantr", "graph", file);
|
|
166
|
+
}
|
|
167
|
+
function graphSnapshotHistoryFileName(snapshotId) {
|
|
168
|
+
return `${snapshotId.replace(/[^a-zA-Z0-9_.-]+/g, "-")}.json`;
|
|
169
|
+
}
|
|
170
|
+
function graphSnapshotHistoryPath(projectRoot, snapshotId) {
|
|
171
|
+
return graphArtifactPath(
|
|
172
|
+
projectRoot,
|
|
173
|
+
join2("snapshots", graphSnapshotHistoryFileName(snapshotId))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
function graphSnapshotHistoryCount(projectRoot) {
|
|
177
|
+
const snapshotsDir = graphArtifactPath(projectRoot, "snapshots");
|
|
178
|
+
if (!existsSync(snapshotsDir)) return 0;
|
|
179
|
+
try {
|
|
180
|
+
return readdirSync(snapshotsDir).filter((entry) => entry.endsWith(".json")).length;
|
|
181
|
+
} catch {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function readGraphSnapshotHistory(projectRoot, limit = 20) {
|
|
186
|
+
const snapshotsDir = graphArtifactPath(projectRoot, "snapshots");
|
|
187
|
+
if (!existsSync(snapshotsDir)) return [];
|
|
188
|
+
try {
|
|
189
|
+
return readdirSync(snapshotsDir).filter((entry) => entry.endsWith(".json")).map((entry) => {
|
|
190
|
+
const absolutePath = join2(snapshotsDir, entry);
|
|
191
|
+
const snapshot = readJsonIfExists(absolutePath);
|
|
192
|
+
if (!snapshot) return null;
|
|
193
|
+
return {
|
|
194
|
+
id: snapshot.id,
|
|
195
|
+
path: displayWorkspacePath(absolutePath),
|
|
196
|
+
created_at: snapshot.created_at,
|
|
197
|
+
source_hash: snapshot.source_hash,
|
|
198
|
+
parent_id: snapshot.parent_id ?? null,
|
|
199
|
+
summary: snapshot.summary
|
|
200
|
+
};
|
|
201
|
+
}).filter((entry) => Boolean(entry)).sort((a, b) => b.created_at.localeCompare(a.created_at) || a.id.localeCompare(b.id)).slice(0, Math.max(1, Math.min(100, limit)));
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function readGraphSnapshotById(projectRoot, snapshotId) {
|
|
207
|
+
const currentPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
|
|
208
|
+
const currentSnapshot = readJsonIfExists(currentPath);
|
|
209
|
+
if (!snapshotId || snapshotId === "current" || currentSnapshot?.id === snapshotId) {
|
|
210
|
+
return { snapshot: currentSnapshot, path: currentPath };
|
|
211
|
+
}
|
|
212
|
+
const path = graphSnapshotHistoryPath(projectRoot, snapshotId);
|
|
213
|
+
return { snapshot: readJsonIfExists(path), path };
|
|
214
|
+
}
|
|
215
|
+
function readMcpGraphSnapshot(projectRoot) {
|
|
216
|
+
return readJsonIfExists(graphArtifactPath(projectRoot, "graph.snapshot.json"));
|
|
217
|
+
}
|
|
218
|
+
function mcpAnchorHealthFindings(projectRoot, findings) {
|
|
219
|
+
return anchorFindingsToGraph(readMcpGraphSnapshot(projectRoot), findings);
|
|
220
|
+
}
|
|
221
|
+
function displayWorkspacePath(path) {
|
|
222
|
+
const rel = relative2(process.cwd(), path).replace(/\\/g, "/");
|
|
223
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
224
|
+
}
|
|
225
|
+
function displayProjectFile(projectRoot, path) {
|
|
226
|
+
if (!path) return null;
|
|
227
|
+
if (/^[a-z]+:\/\//i.test(path)) return path;
|
|
228
|
+
if (isAbsolute2(path)) return displayWorkspacePath(path);
|
|
229
|
+
return displayWorkspacePath(join2(projectRoot, path));
|
|
230
|
+
}
|
|
231
|
+
function graphAvailableRoutes(snapshot) {
|
|
232
|
+
return snapshot.nodes.filter((node) => node.type === "Route").map((node) => graphPayloadString(node.payload, "path") ?? node.id.replace(/^rt:/, "")).sort();
|
|
233
|
+
}
|
|
234
|
+
function graphProjectRelativePath(projectRoot, value) {
|
|
235
|
+
if (!value) return null;
|
|
236
|
+
const absolutePath = isAbsolute2(value) ? value : join2(projectRoot, value);
|
|
237
|
+
const relativePath = relative2(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
238
|
+
if (!relativePath || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return relativePath;
|
|
242
|
+
}
|
|
243
|
+
function graphSourceNodeIdForFile(projectRoot, snapshot, filePath) {
|
|
244
|
+
if (!filePath) return null;
|
|
245
|
+
const trimmed = filePath.trim();
|
|
246
|
+
if (!trimmed) return null;
|
|
247
|
+
if (trimmed.startsWith("src:") && snapshot.nodes.some((node) => node.id === trimmed)) {
|
|
248
|
+
return trimmed;
|
|
249
|
+
}
|
|
250
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
251
|
+
const projectRelative = graphProjectRelativePath(projectRoot, trimmed);
|
|
252
|
+
if (projectRelative) candidates.add(projectRelative);
|
|
253
|
+
try {
|
|
254
|
+
const workspaceRelative = graphProjectRelativePath(projectRoot, resolveWorkspacePath(trimmed));
|
|
255
|
+
if (workspaceRelative) candidates.add(workspaceRelative);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const nodeId = `src:${candidate}`;
|
|
260
|
+
if (snapshot.nodes.some((node) => node.id === nodeId)) return nodeId;
|
|
261
|
+
}
|
|
262
|
+
return snapshot.nodes.find((node) => {
|
|
263
|
+
if (node.type !== "SourceArtifact") return false;
|
|
264
|
+
const path = graphPayloadString(node.payload, "path");
|
|
265
|
+
return Boolean(path && (path === trimmed || candidates.has(path)));
|
|
266
|
+
})?.id ?? null;
|
|
267
|
+
}
|
|
268
|
+
function graphAvailableSourceArtifacts(snapshot) {
|
|
269
|
+
return snapshot.nodes.filter((node) => node.type === "SourceArtifact").map((node) => ({
|
|
270
|
+
id: node.id,
|
|
271
|
+
path: graphPayloadString(node.payload, "path") ?? node.id.replace(/^src:/, ""),
|
|
272
|
+
kind: graphPayloadString(node.payload, "kind") ?? null
|
|
273
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
274
|
+
}
|
|
275
|
+
function readProjectEssence(projectRoot) {
|
|
276
|
+
return readJsonIfExists(join2(projectRoot, "decantr.essence.json"));
|
|
277
|
+
}
|
|
278
|
+
function readProjectPackManifest(projectRoot) {
|
|
279
|
+
return readJsonIfExists(
|
|
280
|
+
join2(projectRoot, ".decantr", "context", "pack-manifest.json")
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function mcpHashFile(path) {
|
|
284
|
+
if (!existsSync(path)) return null;
|
|
285
|
+
return `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
|
|
286
|
+
}
|
|
287
|
+
function mcpStableJson(value) {
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
return `[${value.map((item) => mcpStableJson(item)).join(",")}]`;
|
|
290
|
+
}
|
|
291
|
+
if (value && typeof value === "object") {
|
|
292
|
+
const record = value;
|
|
293
|
+
return `{${Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${mcpStableJson(record[key])}`).join(",")}}`;
|
|
294
|
+
}
|
|
295
|
+
return JSON.stringify(value);
|
|
296
|
+
}
|
|
297
|
+
function mcpHashJson(value) {
|
|
298
|
+
return `sha256:${createHash("sha256").update(mcpStableJson(value)).digest("hex")}`;
|
|
299
|
+
}
|
|
300
|
+
function mcpVisualManifestSourceHash(path) {
|
|
301
|
+
const manifest = readJsonIfExists(path);
|
|
302
|
+
if (!manifest) return null;
|
|
303
|
+
return mcpHashJson({
|
|
304
|
+
version: manifest.version,
|
|
305
|
+
localOnly: manifest.localOnly,
|
|
306
|
+
baseUrl: manifest.baseUrl ?? null,
|
|
307
|
+
routes: (manifest.routes ?? []).map((route) => ({
|
|
308
|
+
route: route.route,
|
|
309
|
+
url: route.url,
|
|
310
|
+
screenshot: route.screenshot,
|
|
311
|
+
screenshotHash: route.screenshotHash ?? null,
|
|
312
|
+
status: route.status,
|
|
313
|
+
error: route.error
|
|
314
|
+
}))
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function mcpStableFindingGraphAnchor(finding) {
|
|
318
|
+
if (!finding.graph) return void 0;
|
|
319
|
+
return {
|
|
320
|
+
node_id: finding.graph.node_id,
|
|
321
|
+
node_type: finding.graph.node_type,
|
|
322
|
+
route: finding.graph.route,
|
|
323
|
+
confidence: finding.graph.confidence,
|
|
324
|
+
reason: finding.graph.reason
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function mcpEvidenceBundleSourceHash(path) {
|
|
328
|
+
const bundle = readJsonIfExists(path);
|
|
329
|
+
if (!bundle) return null;
|
|
330
|
+
return mcpHashJson({
|
|
331
|
+
health: bundle.health ? {
|
|
332
|
+
status: bundle.health.status,
|
|
333
|
+
score: bundle.health.score,
|
|
334
|
+
errorCount: bundle.health.errorCount,
|
|
335
|
+
warnCount: bundle.health.warnCount,
|
|
336
|
+
infoCount: bundle.health.infoCount,
|
|
337
|
+
findingCount: bundle.health.findingCount
|
|
338
|
+
} : null,
|
|
339
|
+
provenance: Object.entries(bundle.provenance ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
|
|
340
|
+
key,
|
|
341
|
+
path: entry.path,
|
|
342
|
+
present: entry.present,
|
|
343
|
+
hash: entry.hash ?? null
|
|
344
|
+
})),
|
|
345
|
+
findings: (bundle.findings ?? []).map((finding) => ({
|
|
346
|
+
id: finding.id,
|
|
347
|
+
code: finding.code,
|
|
348
|
+
source: finding.source,
|
|
349
|
+
category: finding.category,
|
|
350
|
+
severity: finding.severity,
|
|
351
|
+
message: finding.message,
|
|
352
|
+
target: finding.target,
|
|
353
|
+
rule: finding.rule,
|
|
354
|
+
suggestedFix: finding.suggestedFix,
|
|
355
|
+
graph: mcpStableFindingGraphAnchor(finding),
|
|
356
|
+
repair: finding.repair?.id,
|
|
357
|
+
repairPlan: finding.repairPlan ? {
|
|
358
|
+
id: finding.repairPlan.id,
|
|
359
|
+
actions: finding.repairPlan.actions,
|
|
360
|
+
readTargets: finding.repairPlan.readTargets,
|
|
361
|
+
commands: finding.repairPlan.commands
|
|
362
|
+
} : void 0,
|
|
363
|
+
evidence: finding.evidence,
|
|
364
|
+
commands: finding.commands
|
|
365
|
+
}))
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function mcpAnalysisSourceHash(path) {
|
|
369
|
+
const analysis = readJsonIfExists(path);
|
|
370
|
+
if (!analysis) return null;
|
|
371
|
+
return mcpHashJson({
|
|
372
|
+
project: {
|
|
373
|
+
framework: analysis.project?.framework,
|
|
374
|
+
frameworkVersion: analysis.project?.frameworkVersion,
|
|
375
|
+
packageManager: analysis.project?.packageManager,
|
|
376
|
+
hasTypeScript: analysis.project?.hasTypeScript,
|
|
377
|
+
hasTailwind: analysis.project?.hasTailwind,
|
|
378
|
+
projectScope: analysis.project?.projectScope
|
|
379
|
+
},
|
|
380
|
+
routes: {
|
|
381
|
+
strategy: analysis.routes?.strategy,
|
|
382
|
+
routes: (analysis.routes?.routes ?? []).map((route) => ({
|
|
383
|
+
path: route.path,
|
|
384
|
+
file: route.file,
|
|
385
|
+
hasLayout: route.hasLayout
|
|
386
|
+
}))
|
|
387
|
+
},
|
|
388
|
+
styling: {
|
|
389
|
+
approach: analysis.styling?.approach,
|
|
390
|
+
configFile: analysis.styling?.configFile,
|
|
391
|
+
darkMode: analysis.styling?.darkMode,
|
|
392
|
+
cssVariables: analysis.styling?.cssVariables
|
|
393
|
+
},
|
|
394
|
+
layout: {
|
|
395
|
+
shellPattern: analysis.layout?.shellPattern
|
|
396
|
+
},
|
|
397
|
+
features: {
|
|
398
|
+
detected: analysis.features?.detected
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function mcpHealthBaselineDiffSourceHash(path) {
|
|
403
|
+
const diff = readJsonIfExists(path);
|
|
404
|
+
if (!diff) return null;
|
|
405
|
+
return mcpHashJson({
|
|
406
|
+
savedAt: diff.savedAt ?? null,
|
|
407
|
+
statusChanged: diff.statusChanged ?? false,
|
|
408
|
+
scoreDelta: diff.scoreDelta ?? null,
|
|
409
|
+
addedFindings: diff.addedFindings ?? [],
|
|
410
|
+
resolvedFindings: diff.resolvedFindings ?? [],
|
|
411
|
+
changedFiles: diff.changedFiles ?? [],
|
|
412
|
+
changedRoutes: diff.changedRoutes ?? [],
|
|
413
|
+
changedScreenshots: diff.changedScreenshots ?? [],
|
|
414
|
+
contractDrift: diff.contractDrift ?? []
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function mcpHashGraphSource(projectRoot, source) {
|
|
418
|
+
if (!source.path) return null;
|
|
419
|
+
const path = join2(projectRoot, String(source.path));
|
|
420
|
+
if (source.kind === "brownfield-analysis") return mcpAnalysisSourceHash(path);
|
|
421
|
+
if (source.kind === "health-baseline-diff") return mcpHealthBaselineDiffSourceHash(path);
|
|
422
|
+
if (source.kind === "visual-manifest") return mcpVisualManifestSourceHash(path);
|
|
423
|
+
if (source.kind === "evidence-bundle") return mcpEvidenceBundleSourceHash(path);
|
|
424
|
+
return mcpHashFile(path);
|
|
425
|
+
}
|
|
426
|
+
function inspectMcpGraphFreshness(projectRoot) {
|
|
427
|
+
const manifest = readJsonIfExists(
|
|
428
|
+
graphArtifactPath(projectRoot, "graph.manifest.json")
|
|
429
|
+
);
|
|
430
|
+
if (!manifest) {
|
|
431
|
+
return { manifest: null, current: null, staleSources: [] };
|
|
432
|
+
}
|
|
433
|
+
const staleSources = (manifest.sources ?? []).filter((source) => source.path && source.hash).map((source) => {
|
|
434
|
+
const actualHash = mcpHashGraphSource(projectRoot, source);
|
|
435
|
+
return {
|
|
436
|
+
path: String(source.path),
|
|
437
|
+
expected_hash: source.hash,
|
|
438
|
+
actual_hash: actualHash
|
|
439
|
+
};
|
|
440
|
+
}).filter((source) => source.actual_hash !== source.expected_hash);
|
|
441
|
+
return {
|
|
442
|
+
manifest,
|
|
443
|
+
current: staleSources.length === 0,
|
|
444
|
+
staleSources
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function mcpInspectProjectHealthGraph(projectRoot) {
|
|
448
|
+
const graphDir = join2(projectRoot, ".decantr", "graph");
|
|
449
|
+
const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
|
|
450
|
+
const manifestPath = graphArtifactPath(projectRoot, "graph.manifest.json");
|
|
451
|
+
const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
|
|
452
|
+
const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
|
|
453
|
+
const graphDirPresent = existsSync(graphDir);
|
|
454
|
+
const projectMetadataPresent = existsSync(join2(projectRoot, ".decantr", "project.json"));
|
|
455
|
+
const snapshot = readJsonIfExists(snapshotPath);
|
|
456
|
+
const capsule = readJsonIfExists(capsulePath);
|
|
457
|
+
const freshness = inspectMcpGraphFreshness(projectRoot);
|
|
458
|
+
const requiredArtifactPaths = [snapshotPath, manifestPath, diffPath, capsulePath];
|
|
459
|
+
const missingArtifacts = requiredArtifactPaths.filter((path) => !existsSync(path)).map((path) => relative2(projectRoot, path).replace(/\\/g, "/"));
|
|
460
|
+
const current = graphDirPresent || projectMetadataPresent ? missingArtifacts.length === 0 && freshness.current === true : null;
|
|
461
|
+
return {
|
|
462
|
+
present: graphDirPresent,
|
|
463
|
+
ready: current === true && Boolean(snapshot) && Boolean(capsule),
|
|
464
|
+
current,
|
|
465
|
+
snapshotPresent: existsSync(snapshotPath),
|
|
466
|
+
manifestPresent: existsSync(manifestPath),
|
|
467
|
+
diffPresent: existsSync(diffPath),
|
|
468
|
+
capsulePresent: existsSync(capsulePath),
|
|
469
|
+
snapshotId: snapshot?.id ?? null,
|
|
470
|
+
sourceHash: snapshot?.source_hash ?? null,
|
|
471
|
+
contractHash: capsule?.contract_hash ?? null,
|
|
472
|
+
contractCacheKey: capsule?.contract_cache_key ?? null,
|
|
473
|
+
sourceArtifactCount: snapshot?.nodes.filter((node) => node.type === "SourceArtifact").length ?? capsule?.summary?.source_artifacts ?? 0,
|
|
474
|
+
capsuleSourceArtifactLimit: capsule?.source_artifact_limit ?? null,
|
|
475
|
+
capsuleSourceArtifactsTruncated: capsule?.source_artifacts_truncated ?? null,
|
|
476
|
+
staleArtifacts: current === false ? [
|
|
477
|
+
...missingArtifacts,
|
|
478
|
+
...freshness.staleSources.map(
|
|
479
|
+
(source) => `${source.path} changed since graph manifest generation`
|
|
480
|
+
)
|
|
481
|
+
] : [],
|
|
482
|
+
error: null
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
var MCP_GRAPH_NODE_TYPES = new Set(GRAPH_NODE_TYPES);
|
|
486
|
+
var MCP_GRAPH_RELATIONS = new Set(GRAPH_RELATIONS);
|
|
487
|
+
var MCP_GRAPH_DEFAULT_LIMIT = 200;
|
|
488
|
+
var MCP_GRAPH_MAX_LIMIT = 500;
|
|
489
|
+
function mcpGraphEdgeKey(edge) {
|
|
490
|
+
return [edge.src, edge.relation, edge.dst, String(edge.idx ?? "")].join("\0");
|
|
491
|
+
}
|
|
492
|
+
function graphToolLimit(args) {
|
|
493
|
+
if (typeof args.limit !== "number" || !Number.isFinite(args.limit)) {
|
|
494
|
+
return MCP_GRAPH_DEFAULT_LIMIT;
|
|
495
|
+
}
|
|
496
|
+
return Math.max(1, Math.min(MCP_GRAPH_MAX_LIMIT, Math.floor(args.limit)));
|
|
497
|
+
}
|
|
498
|
+
function stringListArg(args, key) {
|
|
499
|
+
const value = args[key];
|
|
500
|
+
if (value === void 0) return {};
|
|
501
|
+
if (typeof value === "string") {
|
|
502
|
+
const trimmed = value.trim();
|
|
503
|
+
return trimmed ? { values: [trimmed] } : { values: [] };
|
|
504
|
+
}
|
|
505
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
506
|
+
return { error: `Optional parameter "${key}" must be a string or an array of strings.` };
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
values: value.map((item) => item.trim()).filter(Boolean)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function graphNodeTypeArg(args, key) {
|
|
513
|
+
const value = args[key];
|
|
514
|
+
if (value === void 0) return {};
|
|
515
|
+
if (typeof value !== "string" || !MCP_GRAPH_NODE_TYPES.has(value)) {
|
|
516
|
+
return {
|
|
517
|
+
error: `Optional parameter "${key}" must be one of: ${GRAPH_NODE_TYPES.join(", ")}.`
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return { value };
|
|
521
|
+
}
|
|
522
|
+
function graphNodeTypesArg(args, key) {
|
|
523
|
+
const parsed = stringListArg(args, key);
|
|
524
|
+
if (parsed.error) return { error: parsed.error };
|
|
525
|
+
if (!parsed.values) return {};
|
|
526
|
+
const invalid = parsed.values.find((value) => !MCP_GRAPH_NODE_TYPES.has(value));
|
|
527
|
+
if (invalid) {
|
|
528
|
+
return {
|
|
529
|
+
error: `Optional parameter "${key}" contains invalid node type "${invalid}". Expected one of: ${GRAPH_NODE_TYPES.join(", ")}.`
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return { values: parsed.values };
|
|
533
|
+
}
|
|
534
|
+
function graphRelationArg(args, key) {
|
|
535
|
+
const value = args[key];
|
|
536
|
+
if (value === void 0) return {};
|
|
537
|
+
if (typeof value !== "string" || !MCP_GRAPH_RELATIONS.has(value)) {
|
|
538
|
+
return {
|
|
539
|
+
error: `Optional parameter "${key}" must be one of: ${GRAPH_RELATIONS.join(", ")}.`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return { value };
|
|
543
|
+
}
|
|
544
|
+
function graphRelationsArg(args, key) {
|
|
545
|
+
const parsed = stringListArg(args, key);
|
|
546
|
+
if (parsed.error) return { error: parsed.error };
|
|
547
|
+
if (!parsed.values) return {};
|
|
548
|
+
const invalid = parsed.values.find((value) => !MCP_GRAPH_RELATIONS.has(value));
|
|
549
|
+
if (invalid) {
|
|
550
|
+
return {
|
|
551
|
+
error: `Optional parameter "${key}" contains invalid relation "${invalid}". Expected one of: ${GRAPH_RELATIONS.join(", ")}.`
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return { values: parsed.values };
|
|
555
|
+
}
|
|
556
|
+
function graphTraverseDirectionArg(args) {
|
|
557
|
+
const value = args.direction;
|
|
558
|
+
if (value === void 0) return {};
|
|
559
|
+
if (value !== "out" && value !== "in" && value !== "both") {
|
|
560
|
+
return { error: 'Optional parameter "direction" must be one of: out, in, both.' };
|
|
561
|
+
}
|
|
562
|
+
return { value };
|
|
563
|
+
}
|
|
564
|
+
function graphTraverseDepthArg(args) {
|
|
565
|
+
const value = args.depth;
|
|
566
|
+
if (value === void 0) return { value: 1 };
|
|
567
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
568
|
+
return { value: 1, error: 'Optional parameter "depth" must be a number.' };
|
|
569
|
+
}
|
|
570
|
+
return { value: Math.max(0, Math.min(4, Math.floor(value))) };
|
|
571
|
+
}
|
|
572
|
+
function dedupeGraphNodes(nodes) {
|
|
573
|
+
return sortGraphNodes([...new Map(nodes.map((node) => [node.id, node])).values()]);
|
|
574
|
+
}
|
|
575
|
+
function dedupeGraphEdges(edges) {
|
|
576
|
+
return sortGraphEdges([...new Map(edges.map((edge) => [mcpGraphEdgeKey(edge), edge])).values()]);
|
|
577
|
+
}
|
|
578
|
+
function graphPayloadFilterArgs(args) {
|
|
579
|
+
const key = args.payload_key;
|
|
580
|
+
const value = args.payload_value;
|
|
581
|
+
const contains = args.payload_contains;
|
|
582
|
+
if (key !== void 0 && typeof key !== "string") {
|
|
583
|
+
return { error: 'Optional parameter "payload_key" must be a string.' };
|
|
584
|
+
}
|
|
585
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
586
|
+
return { error: 'Optional parameter "payload_value" must be a string.' };
|
|
587
|
+
}
|
|
588
|
+
if (contains !== void 0 && typeof contains !== "string") {
|
|
589
|
+
return { error: 'Optional parameter "payload_contains" must be a string.' };
|
|
590
|
+
}
|
|
591
|
+
if (value !== void 0 && (typeof key !== "string" || !key.trim())) {
|
|
592
|
+
return { error: 'Optional parameter "payload_value" requires "payload_key".' };
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
key: typeof key === "string" && key.trim() ? key.trim() : void 0,
|
|
596
|
+
value: typeof value === "string" ? value : void 0,
|
|
597
|
+
contains: typeof contains === "string" && contains.trim() ? contains.trim() : void 0
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function limitGraphSubgraph(nodes, edges, limit) {
|
|
601
|
+
const limitedNodes = nodes.slice(0, limit);
|
|
602
|
+
const allowedNodeIds = new Set(limitedNodes.map((node) => node.id));
|
|
603
|
+
const limitedEdges = edges.filter((edge) => allowedNodeIds.has(edge.src) && allowedNodeIds.has(edge.dst)).slice(0, limit);
|
|
604
|
+
return {
|
|
605
|
+
nodes: limitedNodes,
|
|
606
|
+
edges: limitedEdges,
|
|
607
|
+
truncated: nodes.length > limitedNodes.length || edges.length > limitedEdges.length
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function buildTaskTypedGraphContext(projectRoot, route, task = "", changedFiles = []) {
|
|
611
|
+
const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
|
|
612
|
+
const snapshot = readJsonIfExists(snapshotPath);
|
|
613
|
+
if (!snapshot) return null;
|
|
614
|
+
const capsule = readJsonIfExists(graphArtifactPath(projectRoot, "contract-capsule.json"));
|
|
615
|
+
const freshness = inspectMcpGraphFreshness(projectRoot);
|
|
616
|
+
const routeContext = route ? buildGraphRouteContext(snapshot, route, { task }) : null;
|
|
617
|
+
const limitedRouteContext = routeContext ? limitGraphSubgraph(routeContext.nodes, routeContext.edges, 120) : null;
|
|
618
|
+
const changedFileNodeIds = [
|
|
619
|
+
...new Set(
|
|
620
|
+
changedFiles.map((file) => graphSourceNodeIdForFile(projectRoot, snapshot, file)).filter((nodeId) => Boolean(nodeId))
|
|
621
|
+
)
|
|
622
|
+
];
|
|
623
|
+
const changedFileImpact = changedFileNodeIds.length > 0 ? buildGraphImpactContext(snapshot, changedFileNodeIds, { task, limit: 120 }) : null;
|
|
624
|
+
const limitedChangedFileImpact = changedFileImpact ? limitGraphSubgraph(changedFileImpact.nodes, changedFileImpact.edges, 120) : null;
|
|
625
|
+
return {
|
|
626
|
+
source: "local_graph",
|
|
627
|
+
artifact_path: displayWorkspacePath(snapshotPath),
|
|
628
|
+
snapshot_id: snapshot.id,
|
|
629
|
+
schema_version: snapshot.schema_version,
|
|
630
|
+
project_id: snapshot.project_id,
|
|
631
|
+
source_hash: snapshot.source_hash,
|
|
632
|
+
current: freshness.current,
|
|
633
|
+
stale_sources: freshness.staleSources,
|
|
634
|
+
contract: capsule ? {
|
|
635
|
+
cache_key: capsule.cache_key ?? null,
|
|
636
|
+
contract_hash: capsule.contract_hash ?? null,
|
|
637
|
+
contract_cache_key: capsule.contract_cache_key ?? null,
|
|
638
|
+
summary: capsule.summary ?? null
|
|
639
|
+
} : null,
|
|
640
|
+
route_context: routeContext ? {
|
|
641
|
+
route,
|
|
642
|
+
ranking: routeContext.ranking,
|
|
643
|
+
summary: routeContext.summary,
|
|
644
|
+
ids: routeContext.ids,
|
|
645
|
+
ranked: routeContext.ranked.slice(0, 24),
|
|
646
|
+
nodes: limitedRouteContext?.nodes ?? [],
|
|
647
|
+
edges: limitedRouteContext?.edges ?? [],
|
|
648
|
+
truncated: limitedRouteContext?.truncated ?? false
|
|
649
|
+
} : route ? {
|
|
650
|
+
route,
|
|
651
|
+
error: "Route not found in graph snapshot.",
|
|
652
|
+
available_routes: graphAvailableRoutes(snapshot)
|
|
653
|
+
} : null,
|
|
654
|
+
changed_file_context: changedFiles.length > 0 ? {
|
|
655
|
+
changed_files: changedFiles.slice(0, 40),
|
|
656
|
+
resolved_node_ids: changedFileNodeIds,
|
|
657
|
+
missing_files: changedFiles.filter((file) => !changedFileNodeIds.includes(`src:${file}`)).slice(0, 40),
|
|
658
|
+
impact: changedFileImpact ? {
|
|
659
|
+
ranking: changedFileImpact.ranking,
|
|
660
|
+
summary: changedFileImpact.summary,
|
|
661
|
+
ids: changedFileImpact.ids,
|
|
662
|
+
ranked: changedFileImpact.ranked.slice(0, 24),
|
|
663
|
+
nodes: limitedChangedFileImpact?.nodes ?? [],
|
|
664
|
+
edges: limitedChangedFileImpact?.edges ?? [],
|
|
665
|
+
truncated: limitedChangedFileImpact?.truncated ?? false
|
|
666
|
+
} : null
|
|
667
|
+
} : null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
141
670
|
function changedFilesForTask(projectRoot) {
|
|
142
671
|
const changed = /* @__PURE__ */ new Set();
|
|
143
672
|
try {
|
|
@@ -172,17 +701,51 @@ function impactedRoutesForFiles(projectRoot, files) {
|
|
|
172
701
|
}
|
|
173
702
|
return [...impacted].sort();
|
|
174
703
|
}
|
|
704
|
+
function stringArray(value) {
|
|
705
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
706
|
+
}
|
|
707
|
+
function behaviorObligationSummary(pattern) {
|
|
708
|
+
const contract = pattern.behavior_obligations;
|
|
709
|
+
if (!contract || !Array.isArray(contract.obligations)) return null;
|
|
710
|
+
const obligations = contract.obligations.map((entry, index) => {
|
|
711
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
712
|
+
const record = entry;
|
|
713
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : `obligation-${index + 1}`;
|
|
714
|
+
const label = typeof record.label === "string" && record.label.trim() ? record.label.trim() : id;
|
|
715
|
+
return {
|
|
716
|
+
id,
|
|
717
|
+
label,
|
|
718
|
+
severity: typeof record.severity === "string" ? record.severity : null,
|
|
719
|
+
evidence: typeof record.evidence === "string" ? record.evidence : null
|
|
720
|
+
};
|
|
721
|
+
}).filter((entry) => Boolean(entry));
|
|
722
|
+
if (obligations.length === 0) return null;
|
|
723
|
+
return {
|
|
724
|
+
pattern_id: pattern.id ?? "unknown",
|
|
725
|
+
pattern_role: contract.pattern_role ?? pattern.role ?? null,
|
|
726
|
+
intent: contract.intent ?? null,
|
|
727
|
+
modalities: stringArray(contract.modalities),
|
|
728
|
+
states: stringArray(contract.states),
|
|
729
|
+
risk_profile: stringArray(contract.risk_profile),
|
|
730
|
+
obligations,
|
|
731
|
+
test_hints: stringArray(contract.test_hints),
|
|
732
|
+
component_paths: pattern.componentPaths ?? []
|
|
733
|
+
};
|
|
734
|
+
}
|
|
175
735
|
function localLawSummary(projectRoot) {
|
|
176
736
|
const patterns = readJsonIfExists(join2(projectRoot, ".decantr", "local-patterns.json"));
|
|
177
737
|
const rules = readJsonIfExists(join2(projectRoot, ".decantr", "rules.json"));
|
|
738
|
+
const behaviorObligations = patterns?.patterns?.map((pattern) => behaviorObligationSummary(pattern)).filter((entry) => Boolean(entry)) ?? [];
|
|
178
739
|
return {
|
|
179
740
|
patterns_path: patterns ? ".decantr/local-patterns.json" : null,
|
|
180
741
|
rules_path: rules ? ".decantr/rules.json" : null,
|
|
181
742
|
patterns: patterns?.patterns?.map((pattern) => ({
|
|
182
743
|
id: pattern.id ?? "unknown",
|
|
183
744
|
role: pattern.role ?? null,
|
|
184
|
-
component_paths: pattern.componentPaths ?? []
|
|
745
|
+
component_paths: pattern.componentPaths ?? [],
|
|
746
|
+
behavior_obligations: behaviorObligationSummary(pattern)
|
|
185
747
|
})) ?? [],
|
|
748
|
+
behavior_obligations: behaviorObligations,
|
|
186
749
|
rules: rules?.rules?.map((rule) => ({
|
|
187
750
|
id: rule.id ?? "unknown",
|
|
188
751
|
enabled: rule.enabled ?? false,
|
|
@@ -681,6 +1244,10 @@ function mcpCommandsForFinding(source) {
|
|
|
681
1244
|
return ["decantr check", "decantr health"];
|
|
682
1245
|
case "design-token":
|
|
683
1246
|
return ["decantr export --to figma-tokens", "decantr health --evidence"];
|
|
1247
|
+
case "style-bridge":
|
|
1248
|
+
return ["decantr codify --style-bridge", "decantr verify --evidence"];
|
|
1249
|
+
case "graph":
|
|
1250
|
+
return ["decantr graph", "decantr health --evidence"];
|
|
684
1251
|
case "interaction":
|
|
685
1252
|
return ["decantr check --strict", "decantr health"];
|
|
686
1253
|
case "pack":
|
|
@@ -708,6 +1275,9 @@ function mcpSourceFromFinding(finding) {
|
|
|
708
1275
|
if (category.includes("interaction") || id.includes("interaction") || rule.includes("interaction")) {
|
|
709
1276
|
return "interaction";
|
|
710
1277
|
}
|
|
1278
|
+
if (category.includes("style bridge") || id.includes("style-bridge") || rule.includes("style-bridge")) {
|
|
1279
|
+
return "style-bridge";
|
|
1280
|
+
}
|
|
711
1281
|
return "audit";
|
|
712
1282
|
}
|
|
713
1283
|
function mcpBuildRepairPrompt(input) {
|
|
@@ -720,7 +1290,9 @@ function mcpBuildRepairPrompt(input) {
|
|
|
720
1290
|
`Source: ${input.source}`,
|
|
721
1291
|
`Severity: ${input.severity}`,
|
|
722
1292
|
`Category: ${input.category}`,
|
|
1293
|
+
input.code ? `Code: ${input.code}` : null,
|
|
723
1294
|
`Message: ${input.message}`,
|
|
1295
|
+
input.repair ? `Repair: ${input.repair.id}` : null,
|
|
724
1296
|
input.evidence.length > 0 ? `Evidence:
|
|
725
1297
|
${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
|
|
726
1298
|
input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
|
|
@@ -735,8 +1307,22 @@ ${input.commands.map((command) => `- ${command}`).join("\n")}`
|
|
|
735
1307
|
function mcpHealthFinding(input) {
|
|
736
1308
|
const id = `${input.source}-${mcpSlug(input.baseId || input.rule || `${input.category}-${input.message}`)}`;
|
|
737
1309
|
const commands = mcpCommandsForFinding(input.source);
|
|
1310
|
+
const diagnostic = deriveVerificationDiagnostic({
|
|
1311
|
+
id,
|
|
1312
|
+
source: input.source,
|
|
1313
|
+
category: input.category,
|
|
1314
|
+
message: input.message,
|
|
1315
|
+
rule: input.rule,
|
|
1316
|
+
target: input.target,
|
|
1317
|
+
file: input.file,
|
|
1318
|
+
suggestedFix: input.suggestedFix,
|
|
1319
|
+
evidence: input.evidence
|
|
1320
|
+
});
|
|
1321
|
+
const code = input.code ?? diagnostic.code;
|
|
1322
|
+
const repair = input.repair ?? diagnostic.repair;
|
|
738
1323
|
return {
|
|
739
1324
|
id,
|
|
1325
|
+
code,
|
|
740
1326
|
source: input.source,
|
|
741
1327
|
category: input.category,
|
|
742
1328
|
severity: input.severity,
|
|
@@ -746,6 +1332,7 @@ function mcpHealthFinding(input) {
|
|
|
746
1332
|
file: input.file,
|
|
747
1333
|
rule: input.rule,
|
|
748
1334
|
suggestedFix: input.suggestedFix,
|
|
1335
|
+
repair,
|
|
749
1336
|
remediation: {
|
|
750
1337
|
summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
|
|
751
1338
|
commands,
|
|
@@ -755,13 +1342,69 @@ function mcpHealthFinding(input) {
|
|
|
755
1342
|
category: input.category,
|
|
756
1343
|
severity: input.severity,
|
|
757
1344
|
message: input.message,
|
|
1345
|
+
code,
|
|
758
1346
|
evidence: input.evidence ?? [],
|
|
759
1347
|
suggestedFix: input.suggestedFix,
|
|
1348
|
+
repair,
|
|
760
1349
|
commands
|
|
761
1350
|
})
|
|
762
1351
|
}
|
|
763
1352
|
};
|
|
764
1353
|
}
|
|
1354
|
+
function mcpCollectGraphArtifactFindings(projectRoot) {
|
|
1355
|
+
const graphDirPresent = existsSync(join2(projectRoot, ".decantr", "graph"));
|
|
1356
|
+
const projectMetadataPresent = existsSync(join2(projectRoot, ".decantr", "project.json"));
|
|
1357
|
+
if (!graphDirPresent && !projectMetadataPresent) {
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
const essence = readProjectEssence(projectRoot);
|
|
1361
|
+
if (essence && !isV42(essence)) {
|
|
1362
|
+
return [
|
|
1363
|
+
mcpHealthFinding({
|
|
1364
|
+
source: "graph",
|
|
1365
|
+
category: "Typed Contract Graph",
|
|
1366
|
+
severity: "warn",
|
|
1367
|
+
message: "Typed Contract graph could not be derived: active graph workflows require Essence v4.0.0.",
|
|
1368
|
+
evidence: [
|
|
1369
|
+
"Graph derivation reads decantr.essence.json, local rules, style bridge, visual manifest, and saved evidence bundle artifacts."
|
|
1370
|
+
],
|
|
1371
|
+
target: ".decantr/graph",
|
|
1372
|
+
rule: "typed-graph-current",
|
|
1373
|
+
suggestedFix: "Run `decantr migrate --to v4`, then run `decantr graph`.",
|
|
1374
|
+
baseId: "typed-graph-current"
|
|
1375
|
+
})
|
|
1376
|
+
];
|
|
1377
|
+
}
|
|
1378
|
+
const graphDir = join2(projectRoot, ".decantr", "graph");
|
|
1379
|
+
const requiredArtifacts = [
|
|
1380
|
+
"graph.snapshot.json",
|
|
1381
|
+
"graph.manifest.json",
|
|
1382
|
+
"graph.diff.json",
|
|
1383
|
+
"contract-capsule.json"
|
|
1384
|
+
];
|
|
1385
|
+
const missingArtifacts = requiredArtifacts.map((file) => join2(graphDir, file)).filter((path) => !existsSync(path)).map((path) => relative2(projectRoot, path).replace(/\\/g, "/"));
|
|
1386
|
+
const graphFreshness = inspectMcpGraphFreshness(projectRoot);
|
|
1387
|
+
const staleSources = graphFreshness.staleSources.map((source) => source.path);
|
|
1388
|
+
if (!missingArtifacts.length && !staleSources.length) {
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
return [
|
|
1392
|
+
mcpHealthFinding({
|
|
1393
|
+
source: "graph",
|
|
1394
|
+
category: "Typed Contract Graph",
|
|
1395
|
+
severity: "warn",
|
|
1396
|
+
message: "Typed Contract graph artifacts are missing or stale.",
|
|
1397
|
+
evidence: [
|
|
1398
|
+
...missingArtifacts,
|
|
1399
|
+
...staleSources.map((path) => `${path} changed since graph manifest generation`)
|
|
1400
|
+
].slice(0, 8),
|
|
1401
|
+
target: ".decantr/graph",
|
|
1402
|
+
rule: "typed-graph-current",
|
|
1403
|
+
suggestedFix: "Run `decantr graph` to regenerate graph snapshot, history, diff, manifest, and capsule.",
|
|
1404
|
+
baseId: "typed-graph-current"
|
|
1405
|
+
})
|
|
1406
|
+
];
|
|
1407
|
+
}
|
|
765
1408
|
function mcpCollectDeclaredRoutes(essence) {
|
|
766
1409
|
if (!essence || !isV42(essence)) return [];
|
|
767
1410
|
return Object.keys(essence.blueprint.routes ?? {}).sort();
|
|
@@ -787,6 +1430,8 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
|
|
|
787
1430
|
file: finding.file,
|
|
788
1431
|
rule: finding.rule,
|
|
789
1432
|
suggestedFix: finding.suggestedFix,
|
|
1433
|
+
code: finding.code,
|
|
1434
|
+
repair: finding.repair,
|
|
790
1435
|
baseId: finding.id
|
|
791
1436
|
})
|
|
792
1437
|
);
|
|
@@ -820,10 +1465,17 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
|
|
|
820
1465
|
})
|
|
821
1466
|
);
|
|
822
1467
|
}
|
|
1468
|
+
for (const finding of mcpCollectGraphArtifactFindings(projectRoot)) {
|
|
1469
|
+
pushUnique(finding);
|
|
1470
|
+
}
|
|
1471
|
+
const anchoredFindings = mcpAnchorHealthFindings(projectRoot, findings).map((finding) => ({
|
|
1472
|
+
...finding,
|
|
1473
|
+
repairPlan: buildProjectHealthRepairPlan(projectRoot, finding)
|
|
1474
|
+
}));
|
|
823
1475
|
const counts = {
|
|
824
|
-
errorCount:
|
|
825
|
-
warnCount:
|
|
826
|
-
infoCount:
|
|
1476
|
+
errorCount: anchoredFindings.filter((finding) => finding.severity === "error").length,
|
|
1477
|
+
warnCount: anchoredFindings.filter((finding) => finding.severity === "warn").length,
|
|
1478
|
+
infoCount: anchoredFindings.filter((finding) => finding.severity === "info").length
|
|
827
1479
|
};
|
|
828
1480
|
const manifest = audit.packManifest;
|
|
829
1481
|
return {
|
|
@@ -834,7 +1486,7 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
|
|
|
834
1486
|
score: mcpScoreFromCounts(counts),
|
|
835
1487
|
summary: {
|
|
836
1488
|
...counts,
|
|
837
|
-
findingCount:
|
|
1489
|
+
findingCount: anchoredFindings.length,
|
|
838
1490
|
workflowMode: null,
|
|
839
1491
|
adoptionMode: null,
|
|
840
1492
|
essenceVersion: audit.summary.essenceVersion,
|
|
@@ -849,7 +1501,7 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
|
|
|
849
1501
|
runtimeChecked: audit.runtimeAudit.routeHintsChecked,
|
|
850
1502
|
runtimeMatched: audit.runtimeAudit.routeHintsMatched,
|
|
851
1503
|
runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
|
|
852
|
-
issues:
|
|
1504
|
+
issues: anchoredFindings.filter(
|
|
853
1505
|
(finding) => finding.category.toLowerCase().includes("route") || finding.rule?.toLowerCase().includes("route") || finding.id.toLowerCase().includes("route")
|
|
854
1506
|
).map((finding) => finding.message)
|
|
855
1507
|
},
|
|
@@ -862,11 +1514,12 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
|
|
|
862
1514
|
mutationPackCount: manifest?.mutations?.length ?? 0,
|
|
863
1515
|
generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
|
|
864
1516
|
},
|
|
1517
|
+
graph: mcpInspectProjectHealthGraph(projectRoot),
|
|
865
1518
|
ci: {
|
|
866
1519
|
recommendedCommand: "decantr health --ci --fail-on error",
|
|
867
1520
|
failOn: "error"
|
|
868
1521
|
},
|
|
869
|
-
findings
|
|
1522
|
+
findings: anchoredFindings
|
|
870
1523
|
};
|
|
871
1524
|
}
|
|
872
1525
|
function resolveMcpProjectRoot(value) {
|
|
@@ -890,6 +1543,170 @@ async function getMcpHealthState(projectRoot) {
|
|
|
890
1543
|
});
|
|
891
1544
|
return { audit, assertions, report, evidence };
|
|
892
1545
|
}
|
|
1546
|
+
function compactMcpFinding(finding, includePrompt) {
|
|
1547
|
+
return {
|
|
1548
|
+
id: finding.id,
|
|
1549
|
+
code: finding.code,
|
|
1550
|
+
source: finding.source,
|
|
1551
|
+
category: finding.category,
|
|
1552
|
+
severity: finding.severity,
|
|
1553
|
+
message: finding.message,
|
|
1554
|
+
evidence: finding.evidence,
|
|
1555
|
+
target: finding.target,
|
|
1556
|
+
file: finding.file,
|
|
1557
|
+
rule: finding.rule,
|
|
1558
|
+
suggestedFix: finding.suggestedFix,
|
|
1559
|
+
graph: finding.graph,
|
|
1560
|
+
repair: finding.repair,
|
|
1561
|
+
repairPlan: finding.repairPlan,
|
|
1562
|
+
remediation: {
|
|
1563
|
+
summary: finding.remediation.summary,
|
|
1564
|
+
commands: finding.remediation.commands,
|
|
1565
|
+
prompt: includePrompt ? finding.remediation.prompt : void 0
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function selectMcpRepairFinding(report, options = {}) {
|
|
1570
|
+
return (options.findingId ? report.findings.find((entry) => entry.id === options.findingId) : void 0) ?? (options.code ? report.findings.find((entry) => entry.code === options.code) : void 0) ?? report.findings.find((entry) => entry.severity === "error") ?? report.findings.find((entry) => entry.severity === "warn") ?? report.findings[0] ?? null;
|
|
1571
|
+
}
|
|
1572
|
+
function mcpRepairPlanAction(finding) {
|
|
1573
|
+
const repairId = finding.repair?.id ?? "manual-repair";
|
|
1574
|
+
if (repairId === "regenerate-typed-graph" || finding.source === "graph") {
|
|
1575
|
+
return {
|
|
1576
|
+
id: repairId,
|
|
1577
|
+
kind: "regenerate_artifact",
|
|
1578
|
+
target: ".decantr/graph",
|
|
1579
|
+
description: "Regenerate the typed Contract graph artifacts from the current project sources.",
|
|
1580
|
+
payload: finding.repair?.payload ?? {}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
if (repairId === "import-existing-component") {
|
|
1584
|
+
return {
|
|
1585
|
+
id: repairId,
|
|
1586
|
+
kind: "replace_duplicate_with_import",
|
|
1587
|
+
target: finding.file ?? finding.target ?? null,
|
|
1588
|
+
description: "Remove the locally redeclared UI primitive and import the existing project-owned component.",
|
|
1589
|
+
payload: finding.repair?.payload ?? {}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
if (repairId === "replace-raw-control-with-local-component") {
|
|
1593
|
+
return {
|
|
1594
|
+
id: repairId,
|
|
1595
|
+
kind: "replace_raw_control_with_component",
|
|
1596
|
+
target: finding.file ?? finding.target ?? null,
|
|
1597
|
+
description: "Replace the raw JSX control with the existing project-owned primitive component.",
|
|
1598
|
+
payload: finding.repair?.payload ?? {}
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
if (repairId === "replace-arbitrary-style-with-bridge-token") {
|
|
1602
|
+
return {
|
|
1603
|
+
id: repairId,
|
|
1604
|
+
kind: "replace_arbitrary_style_with_bridge_token",
|
|
1605
|
+
target: finding.file ?? finding.target ?? null,
|
|
1606
|
+
description: "Replace the arbitrary Tailwind value with an accepted project token/class from the style bridge, or update the bridge if the value is approved.",
|
|
1607
|
+
payload: finding.repair?.payload ?? {}
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
id: repairId,
|
|
1612
|
+
kind: "manual_repair",
|
|
1613
|
+
target: finding.file ?? finding.target ?? null,
|
|
1614
|
+
description: finding.suggestedFix ?? finding.remediation.summary,
|
|
1615
|
+
payload: finding.repair?.payload ?? {}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function mcpRepairReadTargets(finding) {
|
|
1619
|
+
const targets = /* @__PURE__ */ new Set(["DECANTR.md", "decantr.essence.json"]);
|
|
1620
|
+
if (finding.source === "graph") {
|
|
1621
|
+
targets.add(".decantr/graph/graph.manifest.json");
|
|
1622
|
+
targets.add(".decantr/graph/graph.snapshot.json");
|
|
1623
|
+
targets.add(".decantr/graph/graph.diff.json");
|
|
1624
|
+
targets.add(".decantr/graph/snapshots/");
|
|
1625
|
+
}
|
|
1626
|
+
if (finding.source === "style-bridge") {
|
|
1627
|
+
targets.add(".decantr/style-bridge.json");
|
|
1628
|
+
}
|
|
1629
|
+
if (finding.source === "pack" || finding.source === "assertion") {
|
|
1630
|
+
targets.add(".decantr/context/pack-manifest.json");
|
|
1631
|
+
}
|
|
1632
|
+
if (finding.graph?.node_id) {
|
|
1633
|
+
targets.add(".decantr/graph/contract-capsule.json");
|
|
1634
|
+
}
|
|
1635
|
+
if (finding.file) {
|
|
1636
|
+
targets.add(finding.file);
|
|
1637
|
+
}
|
|
1638
|
+
if (finding.target && !finding.target.startsWith("http")) {
|
|
1639
|
+
targets.add(finding.target);
|
|
1640
|
+
}
|
|
1641
|
+
return [...targets];
|
|
1642
|
+
}
|
|
1643
|
+
function mcpRepairImpactContext(projectRoot, finding) {
|
|
1644
|
+
const nodeId = finding.graph?.node_id;
|
|
1645
|
+
if (!nodeId) return null;
|
|
1646
|
+
const impact = buildGraphImpactContext(readMcpGraphSnapshot(projectRoot), nodeId, {
|
|
1647
|
+
task: finding.message,
|
|
1648
|
+
limit: 120
|
|
1649
|
+
});
|
|
1650
|
+
if (!impact) return null;
|
|
1651
|
+
return {
|
|
1652
|
+
snapshot_id: impact.snapshotId,
|
|
1653
|
+
source_hash: impact.sourceHash,
|
|
1654
|
+
seed_nodes: impact.seedNodes,
|
|
1655
|
+
summary: impact.summary,
|
|
1656
|
+
ids: impact.ids,
|
|
1657
|
+
ranked: impact.ranked,
|
|
1658
|
+
nodes: impact.nodes,
|
|
1659
|
+
edges: impact.edges
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function buildMcpRepairPlan(input) {
|
|
1663
|
+
if (!input.finding) {
|
|
1664
|
+
return {
|
|
1665
|
+
project: input.evidence.project,
|
|
1666
|
+
health: input.evidence.health,
|
|
1667
|
+
finding: null,
|
|
1668
|
+
plan: null,
|
|
1669
|
+
message: "No Project Health findings require repair.",
|
|
1670
|
+
commands: ["decantr health --evidence"]
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
const finding = input.finding;
|
|
1674
|
+
const action = mcpRepairPlanAction(finding);
|
|
1675
|
+
return {
|
|
1676
|
+
project: input.evidence.project,
|
|
1677
|
+
health: input.evidence.health,
|
|
1678
|
+
finding: compactMcpFinding(finding, false),
|
|
1679
|
+
plan: {
|
|
1680
|
+
id: `repair-plan:${finding.id}`,
|
|
1681
|
+
finding_id: finding.id,
|
|
1682
|
+
diagnostic_code: finding.code ?? null,
|
|
1683
|
+
repair_id: finding.repair?.id ?? null,
|
|
1684
|
+
severity: finding.severity,
|
|
1685
|
+
source: finding.source,
|
|
1686
|
+
category: finding.category,
|
|
1687
|
+
graph_anchor: finding.graph ?? null,
|
|
1688
|
+
impact_context: mcpRepairImpactContext(input.projectRoot, finding),
|
|
1689
|
+
actions: [action],
|
|
1690
|
+
evidence: finding.evidence.map((entry, index) => ({
|
|
1691
|
+
id: `evidence:${finding.id}:${index + 1}`,
|
|
1692
|
+
text: entry
|
|
1693
|
+
})),
|
|
1694
|
+
read_targets: mcpRepairReadTargets(finding),
|
|
1695
|
+
preserve: [
|
|
1696
|
+
"existing framework, routing, and styling system",
|
|
1697
|
+
"existing production behavior unrelated to this finding",
|
|
1698
|
+
"accepted local law, style bridge mappings, and graph anchors"
|
|
1699
|
+
],
|
|
1700
|
+
avoid: [
|
|
1701
|
+
"rewriting unrelated routes",
|
|
1702
|
+
"replacing the app styling system",
|
|
1703
|
+
"regenerating Decantr artifacts unless the finding is about generated context or graph freshness"
|
|
1704
|
+
],
|
|
1705
|
+
commands: finding.remediation.commands,
|
|
1706
|
+
prompt: input.includePrompt === true ? finding.remediation.prompt : void 0
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
893
1710
|
function discoverMcpWorkspaceProjects(root, maxProjects = 500) {
|
|
894
1711
|
const projects = [];
|
|
895
1712
|
function walk(dir, depth) {
|
|
@@ -1011,8 +1828,8 @@ var TOOLS = [
|
|
|
1011
1828
|
// 3. decantr_search_registry — network
|
|
1012
1829
|
{
|
|
1013
1830
|
name: "decantr_search_registry",
|
|
1014
|
-
title: "Search
|
|
1015
|
-
description: "Search
|
|
1831
|
+
title: "Search Vocabulary",
|
|
1832
|
+
description: "Search Decantr official/community vocabulary for patterns, archetypes, themes, and shells.",
|
|
1016
1833
|
inputSchema: {
|
|
1017
1834
|
type: "object",
|
|
1018
1835
|
properties: {
|
|
@@ -1256,69 +2073,297 @@ var TOOLS = [
|
|
|
1256
2073
|
type: "string",
|
|
1257
2074
|
description: 'Section ID (archetype ID, e.g., "ai-chatbot", "auth-full", "settings-full")'
|
|
1258
2075
|
},
|
|
1259
|
-
path: {
|
|
2076
|
+
path: {
|
|
2077
|
+
type: "string",
|
|
2078
|
+
description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
|
|
2079
|
+
},
|
|
2080
|
+
namespace: {
|
|
2081
|
+
type: "string",
|
|
2082
|
+
description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
required: ["section_id"]
|
|
2086
|
+
},
|
|
2087
|
+
annotations: READ_ONLY
|
|
2088
|
+
},
|
|
2089
|
+
// 15. decantr_get_page_context — local read
|
|
2090
|
+
{
|
|
2091
|
+
name: "decantr_get_page_context",
|
|
2092
|
+
title: "Get Page Context",
|
|
2093
|
+
description: "Get the route-local context for a specific page. Returns the compiled page execution pack plus its parent section pack and section context when available. Falls back to hosted execution-pack compilation when local pack artifacts are missing.",
|
|
2094
|
+
inputSchema: {
|
|
2095
|
+
type: "object",
|
|
2096
|
+
properties: {
|
|
2097
|
+
page_id: {
|
|
2098
|
+
type: "string",
|
|
2099
|
+
description: 'Page ID (for example "overview", "settings", or "home").'
|
|
2100
|
+
},
|
|
2101
|
+
path: {
|
|
2102
|
+
type: "string",
|
|
2103
|
+
description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
|
|
2104
|
+
},
|
|
2105
|
+
namespace: {
|
|
2106
|
+
type: "string",
|
|
2107
|
+
description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
required: ["page_id"]
|
|
2111
|
+
},
|
|
2112
|
+
annotations: READ_ONLY
|
|
2113
|
+
},
|
|
2114
|
+
// 16. decantr_get_project_state — local read
|
|
2115
|
+
{
|
|
2116
|
+
name: "decantr_get_project_state",
|
|
2117
|
+
title: "Get Project State",
|
|
2118
|
+
description: "Read a compact typed summary of the active Decantr project: Essence version, routes, generated packs, typed graph artifacts, local law, style bridge, diagnostic catalog, and recommended next MCP tools.",
|
|
2119
|
+
inputSchema: {
|
|
2120
|
+
type: "object",
|
|
2121
|
+
properties: {
|
|
2122
|
+
project_path: {
|
|
2123
|
+
type: "string",
|
|
2124
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
annotations: READ_ONLY
|
|
2129
|
+
},
|
|
2130
|
+
// 17. decantr_prepare_task_context — local read
|
|
2131
|
+
{
|
|
2132
|
+
name: "decantr_prepare_task_context",
|
|
2133
|
+
title: "Prepare Task Context",
|
|
2134
|
+
description: "Resolve compact Brownfield/Essence task-time context for a route or page before editing. Returns route, section, page pack, directives, local law, behavior obligations, style bridge mappings, typed graph context, health evidence, and local screenshot references when available.",
|
|
2135
|
+
inputSchema: {
|
|
2136
|
+
type: "object",
|
|
2137
|
+
properties: {
|
|
2138
|
+
project_path: {
|
|
2139
|
+
type: "string",
|
|
2140
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2141
|
+
},
|
|
2142
|
+
route: {
|
|
2143
|
+
type: "string",
|
|
2144
|
+
description: 'Route being edited, for example "/feed". Preferred when known.'
|
|
2145
|
+
},
|
|
2146
|
+
page_id: {
|
|
2147
|
+
type: "string",
|
|
2148
|
+
description: "Page ID when route is unknown."
|
|
2149
|
+
},
|
|
2150
|
+
task: {
|
|
2151
|
+
type: "string",
|
|
2152
|
+
description: "Short task description used to rank relevant patterns and context."
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
annotations: READ_ONLY
|
|
2157
|
+
},
|
|
2158
|
+
// 17. decantr_get_contract_capsule — local typed graph read
|
|
2159
|
+
{
|
|
2160
|
+
name: "decantr_get_contract_capsule",
|
|
2161
|
+
title: "Get Contract Capsule",
|
|
2162
|
+
description: "Read the Decantr typed Contract capsule generated by `decantr graph`. This is the compact, cache-friendly Contract summary agents should load near session start.",
|
|
2163
|
+
inputSchema: {
|
|
2164
|
+
type: "object",
|
|
2165
|
+
properties: {
|
|
2166
|
+
project_path: {
|
|
2167
|
+
type: "string",
|
|
2168
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
annotations: READ_ONLY
|
|
2173
|
+
},
|
|
2174
|
+
// 18. decantr_get_graph_snapshot — local typed graph read
|
|
2175
|
+
{
|
|
2176
|
+
name: "decantr_get_graph_snapshot",
|
|
2177
|
+
title: "Get Graph Snapshot",
|
|
2178
|
+
description: "Read the Decantr typed graph snapshot generated by `decantr graph`. By default returns snapshot metadata and available routes; pass route for a scoped route subgraph, include_history for a compact local timeline, or include_full for the full snapshot.",
|
|
2179
|
+
inputSchema: {
|
|
2180
|
+
type: "object",
|
|
2181
|
+
properties: {
|
|
2182
|
+
project_path: {
|
|
2183
|
+
type: "string",
|
|
2184
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2185
|
+
},
|
|
2186
|
+
route: {
|
|
2187
|
+
type: "string",
|
|
2188
|
+
description: 'Optional route path, for example "/settings", to return a scoped subgraph.'
|
|
2189
|
+
},
|
|
2190
|
+
node_id: {
|
|
2191
|
+
type: "string",
|
|
2192
|
+
description: 'Optional graph node ID, for example "cmp:button" or "tkn:surface", to return dependency impact context.'
|
|
2193
|
+
},
|
|
2194
|
+
file_path: {
|
|
2195
|
+
type: "string",
|
|
2196
|
+
description: 'Optional project-relative source file path, for example "src/app/page.tsx", to return dependency impact context for its SourceArtifact node.'
|
|
2197
|
+
},
|
|
2198
|
+
snapshot_id: {
|
|
2199
|
+
type: "string",
|
|
2200
|
+
description: 'Optional graph snapshot id to read from .decantr/graph/snapshots. Use "current" or omit for graph.snapshot.json.'
|
|
2201
|
+
},
|
|
2202
|
+
compare_to: {
|
|
2203
|
+
type: "string",
|
|
2204
|
+
description: 'Optional snapshot id to diff against the selected snapshot. Use "current" for graph.snapshot.json.'
|
|
2205
|
+
},
|
|
2206
|
+
include_diff_ops: {
|
|
2207
|
+
type: "boolean",
|
|
2208
|
+
description: "Include diff operation details when compare_to is provided. Defaults to false."
|
|
2209
|
+
},
|
|
2210
|
+
limit: {
|
|
2211
|
+
type: "number",
|
|
2212
|
+
description: "Maximum diff operations or impact nodes to return. Defaults to 200, maximum 500."
|
|
2213
|
+
},
|
|
2214
|
+
include_full: {
|
|
2215
|
+
type: "boolean",
|
|
2216
|
+
description: "Return the full graph snapshot instead of metadata. Defaults to false."
|
|
2217
|
+
},
|
|
2218
|
+
include_history: {
|
|
2219
|
+
type: "boolean",
|
|
2220
|
+
description: "Include a compact index of local snapshot history entries from .decantr/graph/snapshots. Defaults to false."
|
|
2221
|
+
},
|
|
2222
|
+
task: {
|
|
2223
|
+
type: "string",
|
|
2224
|
+
description: "Optional task description used to boost matching nodes in route-scoped or impact ranked context."
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
},
|
|
2228
|
+
annotations: READ_ONLY
|
|
2229
|
+
},
|
|
2230
|
+
// 19. decantr_query_graph — local typed graph read
|
|
2231
|
+
{
|
|
2232
|
+
name: "decantr_query_graph",
|
|
2233
|
+
title: "Query Graph",
|
|
2234
|
+
description: "Run a narrow typed query against the local Decantr graph snapshot generated by `decantr graph`. Use for direct node/type/relation lookups instead of reading the full graph.",
|
|
2235
|
+
inputSchema: {
|
|
2236
|
+
type: "object",
|
|
2237
|
+
properties: {
|
|
2238
|
+
project_path: {
|
|
2239
|
+
type: "string",
|
|
2240
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2241
|
+
},
|
|
2242
|
+
snapshot_id: {
|
|
2243
|
+
type: "string",
|
|
2244
|
+
description: 'Optional snapshot id from local graph history, or "current". Defaults to current.'
|
|
2245
|
+
},
|
|
2246
|
+
node_ids: {
|
|
2247
|
+
type: "array",
|
|
2248
|
+
items: { type: "string" },
|
|
2249
|
+
description: 'Optional exact graph node IDs to return, for example ["rt:/feed"].'
|
|
2250
|
+
},
|
|
2251
|
+
file_path: {
|
|
2252
|
+
type: "string",
|
|
2253
|
+
description: 'Optional project-relative source file path, for example "src/app/page.tsx", resolved to a SourceArtifact node selector.'
|
|
2254
|
+
},
|
|
2255
|
+
node_type: {
|
|
2256
|
+
type: "string",
|
|
2257
|
+
enum: [...GRAPH_NODE_TYPES],
|
|
2258
|
+
description: 'Optional single node type selector, for example "Route" or "Component".'
|
|
2259
|
+
},
|
|
2260
|
+
node_types: {
|
|
2261
|
+
type: "array",
|
|
2262
|
+
items: { type: "string", enum: [...GRAPH_NODE_TYPES] },
|
|
2263
|
+
description: "Optional node type selectors."
|
|
2264
|
+
},
|
|
2265
|
+
payload_key: {
|
|
2266
|
+
type: "string",
|
|
2267
|
+
description: 'Optional node payload key or dotted path filter, for example "code" for Finding nodes.'
|
|
2268
|
+
},
|
|
2269
|
+
payload_value: {
|
|
2270
|
+
type: "string",
|
|
2271
|
+
description: 'Optional exact stringified payload value used with payload_key, for example "COMP010".'
|
|
2272
|
+
},
|
|
2273
|
+
payload_contains: {
|
|
2274
|
+
type: "string",
|
|
2275
|
+
description: "Optional case-insensitive substring filter over the node payload JSON."
|
|
2276
|
+
},
|
|
2277
|
+
edge_src: {
|
|
2278
|
+
type: "string",
|
|
2279
|
+
description: "Optional edge source node ID selector."
|
|
2280
|
+
},
|
|
2281
|
+
edge_dst: {
|
|
2282
|
+
type: "string",
|
|
2283
|
+
description: "Optional edge destination node ID selector."
|
|
2284
|
+
},
|
|
2285
|
+
relation: {
|
|
2286
|
+
type: "string",
|
|
2287
|
+
enum: [...GRAPH_RELATIONS],
|
|
2288
|
+
description: 'Optional single edge relation selector, for example "PAGE_COMPOSES_PATTERN".'
|
|
2289
|
+
},
|
|
2290
|
+
relations: {
|
|
2291
|
+
type: "array",
|
|
2292
|
+
items: { type: "string", enum: [...GRAPH_RELATIONS] },
|
|
2293
|
+
description: "Optional edge relation selectors."
|
|
2294
|
+
},
|
|
2295
|
+
include_edges: {
|
|
2296
|
+
type: "boolean",
|
|
2297
|
+
description: "When querying nodes, include incident edges and their opposite endpoint nodes. Defaults to false unless edge selectors are present."
|
|
2298
|
+
},
|
|
2299
|
+
include_impact: {
|
|
2300
|
+
type: "boolean",
|
|
2301
|
+
description: "When querying nodes, also return the dependency impact context for the matched node IDs."
|
|
2302
|
+
},
|
|
2303
|
+
task: {
|
|
1260
2304
|
type: "string",
|
|
1261
|
-
description: "Optional
|
|
2305
|
+
description: "Optional task description used to boost matching nodes in the impact ranking when include_impact is true."
|
|
1262
2306
|
},
|
|
1263
|
-
|
|
1264
|
-
type: "
|
|
1265
|
-
description:
|
|
2307
|
+
limit: {
|
|
2308
|
+
type: "number",
|
|
2309
|
+
description: "Maximum nodes and edges to return. Defaults to 200, maximum 500."
|
|
1266
2310
|
}
|
|
1267
|
-
}
|
|
1268
|
-
required: ["section_id"]
|
|
2311
|
+
}
|
|
1269
2312
|
},
|
|
1270
2313
|
annotations: READ_ONLY
|
|
1271
2314
|
},
|
|
1272
|
-
//
|
|
2315
|
+
// 20. decantr_traverse_graph — local typed graph read
|
|
1273
2316
|
{
|
|
1274
|
-
name: "
|
|
1275
|
-
title: "
|
|
1276
|
-
description: "
|
|
2317
|
+
name: "decantr_traverse_graph",
|
|
2318
|
+
title: "Traverse Graph",
|
|
2319
|
+
description: "Traverse the local Decantr graph from one or more node IDs or a source file by relation, direction, and depth. Use for questions like which pages use this token or which graph nodes point at this source file.",
|
|
1277
2320
|
inputSchema: {
|
|
1278
2321
|
type: "object",
|
|
1279
2322
|
properties: {
|
|
1280
|
-
|
|
2323
|
+
project_path: {
|
|
1281
2324
|
type: "string",
|
|
1282
|
-
description:
|
|
2325
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
1283
2326
|
},
|
|
1284
|
-
|
|
2327
|
+
snapshot_id: {
|
|
1285
2328
|
type: "string",
|
|
1286
|
-
description:
|
|
2329
|
+
description: 'Optional snapshot id from local graph history, or "current". Defaults to current.'
|
|
1287
2330
|
},
|
|
1288
|
-
|
|
1289
|
-
type: "string",
|
|
1290
|
-
description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
|
|
1291
|
-
}
|
|
1292
|
-
},
|
|
1293
|
-
required: ["page_id"]
|
|
1294
|
-
},
|
|
1295
|
-
annotations: READ_ONLY
|
|
1296
|
-
},
|
|
1297
|
-
// 16. decantr_prepare_task_context — local read
|
|
1298
|
-
{
|
|
1299
|
-
name: "decantr_prepare_task_context",
|
|
1300
|
-
title: "Prepare Task Context",
|
|
1301
|
-
description: "Resolve compact Brownfield/Essence task-time context for a route or page before editing. Returns route, section, page pack, directives, patterns, shared components, visual target, health evidence, and local screenshot references when available.",
|
|
1302
|
-
inputSchema: {
|
|
1303
|
-
type: "object",
|
|
1304
|
-
properties: {
|
|
1305
|
-
route: {
|
|
2331
|
+
from: {
|
|
1306
2332
|
type: "string",
|
|
1307
|
-
description: '
|
|
2333
|
+
description: 'Start node ID, for example "rt:/feed" or "tkn:color.primary".'
|
|
1308
2334
|
},
|
|
1309
|
-
|
|
2335
|
+
from_ids: {
|
|
2336
|
+
type: "array",
|
|
2337
|
+
items: { type: "string" },
|
|
2338
|
+
description: "Optional start node IDs. Used when traversing from multiple anchors."
|
|
2339
|
+
},
|
|
2340
|
+
file_path: {
|
|
1310
2341
|
type: "string",
|
|
1311
|
-
description: "
|
|
2342
|
+
description: 'Optional project-relative source file path, for example "src/app/page.tsx", resolved to a SourceArtifact start node.'
|
|
1312
2343
|
},
|
|
1313
|
-
|
|
2344
|
+
relations: {
|
|
2345
|
+
type: "array",
|
|
2346
|
+
items: { type: "string", enum: [...GRAPH_RELATIONS] },
|
|
2347
|
+
description: "Optional relation allow-list. When omitted, all relations are traversed."
|
|
2348
|
+
},
|
|
2349
|
+
direction: {
|
|
1314
2350
|
type: "string",
|
|
1315
|
-
|
|
2351
|
+
enum: ["out", "in", "both"],
|
|
2352
|
+
description: "Traversal direction. Defaults to out."
|
|
2353
|
+
},
|
|
2354
|
+
depth: {
|
|
2355
|
+
type: "number",
|
|
2356
|
+
description: "Traversal depth. Defaults to 1, maximum 4."
|
|
2357
|
+
},
|
|
2358
|
+
limit: {
|
|
2359
|
+
type: "number",
|
|
2360
|
+
description: "Maximum nodes and edges to return. Defaults to 200, maximum 500."
|
|
1316
2361
|
}
|
|
1317
2362
|
}
|
|
1318
2363
|
},
|
|
1319
2364
|
annotations: READ_ONLY
|
|
1320
2365
|
},
|
|
1321
|
-
//
|
|
2366
|
+
// 21. decantr_get_execution_pack — local read
|
|
1322
2367
|
{
|
|
1323
2368
|
name: "decantr_get_execution_pack",
|
|
1324
2369
|
title: "Get Execution Pack",
|
|
@@ -1474,7 +2519,85 @@ var TOOLS = [
|
|
|
1474
2519
|
},
|
|
1475
2520
|
annotations: READ_ONLY_NETWORK
|
|
1476
2521
|
},
|
|
1477
|
-
//
|
|
2522
|
+
// 24. decantr_get_findings — local typed findings read
|
|
2523
|
+
{
|
|
2524
|
+
name: "decantr_get_findings",
|
|
2525
|
+
title: "Get Findings",
|
|
2526
|
+
description: "Return typed Project Health findings for the current Decantr project, with stable codes, repair IDs, graph anchors when available, and optional repair prompts.",
|
|
2527
|
+
inputSchema: {
|
|
2528
|
+
type: "object",
|
|
2529
|
+
properties: {
|
|
2530
|
+
project_path: {
|
|
2531
|
+
type: "string",
|
|
2532
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2533
|
+
},
|
|
2534
|
+
severity: {
|
|
2535
|
+
type: "string",
|
|
2536
|
+
enum: ["error", "warn", "info"],
|
|
2537
|
+
description: "Optional severity filter."
|
|
2538
|
+
},
|
|
2539
|
+
source: {
|
|
2540
|
+
type: "string",
|
|
2541
|
+
enum: [
|
|
2542
|
+
"audit",
|
|
2543
|
+
"assertion",
|
|
2544
|
+
"browser",
|
|
2545
|
+
"check",
|
|
2546
|
+
"brownfield",
|
|
2547
|
+
"design-token",
|
|
2548
|
+
"style-bridge",
|
|
2549
|
+
"graph",
|
|
2550
|
+
"runtime",
|
|
2551
|
+
"pack",
|
|
2552
|
+
"interaction"
|
|
2553
|
+
],
|
|
2554
|
+
description: "Optional finding source filter."
|
|
2555
|
+
},
|
|
2556
|
+
code: {
|
|
2557
|
+
type: "string",
|
|
2558
|
+
description: 'Optional stable diagnostic code filter, for example "TOKEN010".'
|
|
2559
|
+
},
|
|
2560
|
+
include_prompts: {
|
|
2561
|
+
type: "boolean",
|
|
2562
|
+
description: "Include full repair prompts on each finding. Defaults to false to keep context compact."
|
|
2563
|
+
},
|
|
2564
|
+
limit: {
|
|
2565
|
+
type: "number",
|
|
2566
|
+
description: "Maximum findings to return. Defaults to 50, maximum 200."
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
annotations: READ_ONLY
|
|
2571
|
+
},
|
|
2572
|
+
// 25. decantr_get_repair_plan — local structured repair loop
|
|
2573
|
+
{
|
|
2574
|
+
name: "decantr_get_repair_plan",
|
|
2575
|
+
title: "Get Repair Plan",
|
|
2576
|
+
description: "Return a typed repair plan for one Project Health finding: diagnostic code, repair ID, graph anchor, action payload, evidence, read targets, preserve/avoid constraints, rerun commands, and optional prompt text.",
|
|
2577
|
+
inputSchema: {
|
|
2578
|
+
type: "object",
|
|
2579
|
+
properties: {
|
|
2580
|
+
project_path: {
|
|
2581
|
+
type: "string",
|
|
2582
|
+
description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
|
|
2583
|
+
},
|
|
2584
|
+
finding_id: {
|
|
2585
|
+
type: "string",
|
|
2586
|
+
description: "Optional finding id. Defaults to the first error or warning, then the first finding."
|
|
2587
|
+
},
|
|
2588
|
+
code: {
|
|
2589
|
+
type: "string",
|
|
2590
|
+
description: 'Optional stable diagnostic code selector, for example "GRAPH001".'
|
|
2591
|
+
},
|
|
2592
|
+
include_prompt: {
|
|
2593
|
+
type: "boolean",
|
|
2594
|
+
description: "Include the human-readable repair prompt alongside the typed plan."
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
},
|
|
2598
|
+
annotations: READ_ONLY
|
|
2599
|
+
},
|
|
2600
|
+
// 26. decantr_get_evidence_bundle — local reliability artifact
|
|
1478
2601
|
{
|
|
1479
2602
|
name: "decantr_get_evidence_bundle",
|
|
1480
2603
|
title: "Get Evidence Bundle",
|
|
@@ -2128,6 +3251,578 @@ async function handleTool(name, args) {
|
|
|
2128
3251
|
return { error: `Failed to update essence: ${e.message}` };
|
|
2129
3252
|
}
|
|
2130
3253
|
}
|
|
3254
|
+
case "decantr_get_project_state": {
|
|
3255
|
+
try {
|
|
3256
|
+
const projectRoot = graphProjectRoot(args);
|
|
3257
|
+
const essence = readProjectEssence(projectRoot);
|
|
3258
|
+
const packManifest = readProjectPackManifest(projectRoot);
|
|
3259
|
+
const graphDir = join2(projectRoot, ".decantr", "graph");
|
|
3260
|
+
const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
|
|
3261
|
+
const manifestPath = graphArtifactPath(projectRoot, "graph.manifest.json");
|
|
3262
|
+
const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
|
|
3263
|
+
const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
|
|
3264
|
+
const snapshot = readJsonIfExists(snapshotPath);
|
|
3265
|
+
const graphDiff = readJsonIfExists(diffPath);
|
|
3266
|
+
const snapshotHistoryPath = snapshot ? graphSnapshotHistoryPath(projectRoot, snapshot.id) : null;
|
|
3267
|
+
const snapshotHistoryPresent = snapshotHistoryPath ? existsSync(snapshotHistoryPath) : false;
|
|
3268
|
+
const snapshotHistoryCount = graphSnapshotHistoryCount(projectRoot);
|
|
3269
|
+
const capsule = readJsonIfExists(capsulePath);
|
|
3270
|
+
const graphFreshness = inspectMcpGraphFreshness(projectRoot);
|
|
3271
|
+
const projectConfig = readJsonIfExists(join2(projectRoot, ".decantr", "project.json"));
|
|
3272
|
+
const localPatternsPresent = existsSync(
|
|
3273
|
+
join2(projectRoot, ".decantr", "local-patterns.json")
|
|
3274
|
+
);
|
|
3275
|
+
const localRulesPresent = existsSync(join2(projectRoot, ".decantr", "rules.json"));
|
|
3276
|
+
const styleBridgePresent = existsSync(join2(projectRoot, ".decantr", "style-bridge.json"));
|
|
3277
|
+
const hasGraphArtifacts = existsSync(snapshotPath) && snapshotHistoryPresent && existsSync(manifestPath) && existsSync(diffPath) && existsSync(capsulePath);
|
|
3278
|
+
const graphReady = Boolean(snapshot) && hasGraphArtifacts && graphFreshness.current === true;
|
|
3279
|
+
return {
|
|
3280
|
+
source: "local_workspace",
|
|
3281
|
+
project_root: displayWorkspacePath(projectRoot),
|
|
3282
|
+
essence: essence ? {
|
|
3283
|
+
present: true,
|
|
3284
|
+
version: typeof essence.version === "string" ? essence.version : null,
|
|
3285
|
+
active_v4: isV42(essence),
|
|
3286
|
+
routes: isV42(essence) ? Object.keys(essence.blueprint.routes ?? {}).sort() : [],
|
|
3287
|
+
sections: isV42(essence) ? essence.blueprint.sections.map((section) => ({
|
|
3288
|
+
id: section.id,
|
|
3289
|
+
role: section.role,
|
|
3290
|
+
pages: section.pages.length
|
|
3291
|
+
})) : [],
|
|
3292
|
+
features: isV42(essence) ? essence.blueprint.features : [],
|
|
3293
|
+
guard: isV42(essence) ? essence.meta.guard : null
|
|
3294
|
+
} : {
|
|
3295
|
+
present: false,
|
|
3296
|
+
version: null,
|
|
3297
|
+
active_v4: false,
|
|
3298
|
+
routes: [],
|
|
3299
|
+
sections: [],
|
|
3300
|
+
features: [],
|
|
3301
|
+
guard: null
|
|
3302
|
+
},
|
|
3303
|
+
project_config: {
|
|
3304
|
+
present: Boolean(projectConfig),
|
|
3305
|
+
workflow_mode: projectConfig?.workflowMode ?? null,
|
|
3306
|
+
adoption_mode: projectConfig?.adoptionMode ?? null,
|
|
3307
|
+
telemetry_enabled: projectConfig?.telemetry === true
|
|
3308
|
+
},
|
|
3309
|
+
context: {
|
|
3310
|
+
manifest_present: Boolean(packManifest),
|
|
3311
|
+
scaffold_pack_present: Boolean(packManifest?.scaffold),
|
|
3312
|
+
review_pack_present: Boolean(packManifest?.review),
|
|
3313
|
+
section_pack_count: packManifest?.sections.length ?? 0,
|
|
3314
|
+
page_pack_count: packManifest?.pages.length ?? 0,
|
|
3315
|
+
mutation_pack_count: packManifest?.mutations?.length ?? 0,
|
|
3316
|
+
generated_at: packManifest && typeof packManifest.generatedAt === "string" ? packManifest.generatedAt : null
|
|
3317
|
+
},
|
|
3318
|
+
graph: {
|
|
3319
|
+
graph_dir_present: existsSync(graphDir),
|
|
3320
|
+
manifest_present: Boolean(graphFreshness.manifest),
|
|
3321
|
+
snapshot_present: Boolean(snapshot),
|
|
3322
|
+
snapshot_history_present: snapshotHistoryPresent,
|
|
3323
|
+
snapshot_history_path: snapshotHistoryPath ? displayWorkspacePath(snapshotHistoryPath) : null,
|
|
3324
|
+
snapshot_history_count: snapshotHistoryCount,
|
|
3325
|
+
capsule_present: existsSync(capsulePath),
|
|
3326
|
+
diff_present: existsSync(diffPath),
|
|
3327
|
+
ready: graphReady,
|
|
3328
|
+
current: graphFreshness.current,
|
|
3329
|
+
stale_sources: graphFreshness.staleSources,
|
|
3330
|
+
snapshot_id: snapshot?.id ?? null,
|
|
3331
|
+
schema_version: snapshot?.schema_version ?? null,
|
|
3332
|
+
source_hash: snapshot?.source_hash ?? null,
|
|
3333
|
+
cache_key: capsule?.cache_key ?? null,
|
|
3334
|
+
contract_hash: capsule?.contract_hash ?? null,
|
|
3335
|
+
contract_cache_key: capsule?.contract_cache_key ?? null,
|
|
3336
|
+
capsule_source_artifact_count: capsule?.summary?.source_artifacts ?? null,
|
|
3337
|
+
capsule_source_artifact_limit: capsule?.source_artifact_limit ?? null,
|
|
3338
|
+
capsule_source_artifacts_truncated: capsule?.source_artifacts_truncated ?? null,
|
|
3339
|
+
summary: snapshot?.summary ?? null,
|
|
3340
|
+
diff_summary: graphDiff ? summarizeGraphDiff(graphDiff) : null,
|
|
3341
|
+
available_routes: snapshot ? graphAvailableRoutes(snapshot) : [],
|
|
3342
|
+
source_artifact_count: snapshot ? snapshot.nodes.filter((node) => node.type === "SourceArtifact").length : 0,
|
|
3343
|
+
available_source_artifacts: snapshot ? graphAvailableSourceArtifacts(snapshot).slice(0, 40) : []
|
|
3344
|
+
},
|
|
3345
|
+
local_authority: {
|
|
3346
|
+
local_patterns_present: localPatternsPresent,
|
|
3347
|
+
local_rules_present: localRulesPresent,
|
|
3348
|
+
style_bridge_present: styleBridgePresent
|
|
3349
|
+
},
|
|
3350
|
+
diagnostics: {
|
|
3351
|
+
known_count: KNOWN_VERIFICATION_DIAGNOSTICS.length,
|
|
3352
|
+
families: [
|
|
3353
|
+
...new Set(KNOWN_VERIFICATION_DIAGNOSTICS.map((entry) => entry.family))
|
|
3354
|
+
].sort(),
|
|
3355
|
+
codes: KNOWN_VERIFICATION_DIAGNOSTICS.map((entry) => ({
|
|
3356
|
+
code: entry.code,
|
|
3357
|
+
rule: entry.rule,
|
|
3358
|
+
repair_id: entry.repairId,
|
|
3359
|
+
family: entry.family
|
|
3360
|
+
})).sort((a, b) => a.code.localeCompare(b.code) || a.rule.localeCompare(b.rule))
|
|
3361
|
+
},
|
|
3362
|
+
recommended_next_tools: [
|
|
3363
|
+
graphReady ? "decantr_get_contract_capsule" : "decantr_get_findings",
|
|
3364
|
+
graphReady ? null : "decantr_get_repair_plan",
|
|
3365
|
+
snapshot ? "decantr_get_graph_snapshot" : null,
|
|
3366
|
+
"decantr_prepare_task_context",
|
|
3367
|
+
"decantr_get_findings",
|
|
3368
|
+
"decantr_get_evidence_bundle"
|
|
3369
|
+
].filter((tool) => Boolean(tool))
|
|
3370
|
+
};
|
|
3371
|
+
} catch (e) {
|
|
3372
|
+
return { error: `Could not read project state: ${e.message}` };
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
case "decantr_get_contract_capsule": {
|
|
3376
|
+
try {
|
|
3377
|
+
const projectRoot = graphProjectRoot(args);
|
|
3378
|
+
const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
|
|
3379
|
+
const capsule = readJsonIfExists(capsulePath);
|
|
3380
|
+
if (!capsule) {
|
|
3381
|
+
return {
|
|
3382
|
+
error: "Contract capsule not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
|
|
3383
|
+
expected_path: displayWorkspacePath(capsulePath)
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
return {
|
|
3387
|
+
source: "local_graph",
|
|
3388
|
+
artifact_path: displayWorkspacePath(capsulePath),
|
|
3389
|
+
capsule
|
|
3390
|
+
};
|
|
3391
|
+
} catch (e) {
|
|
3392
|
+
return { error: `Could not read contract capsule: ${e.message}` };
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
case "decantr_get_graph_snapshot": {
|
|
3396
|
+
try {
|
|
3397
|
+
const projectRoot = graphProjectRoot(args);
|
|
3398
|
+
const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
|
|
3399
|
+
const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
|
|
3400
|
+
const currentSnapshot = readJsonIfExists(snapshotPath);
|
|
3401
|
+
const graphDiff = readJsonIfExists(diffPath);
|
|
3402
|
+
if (!currentSnapshot) {
|
|
3403
|
+
return {
|
|
3404
|
+
error: "Graph snapshot not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
|
|
3405
|
+
expected_path: displayWorkspacePath(snapshotPath)
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
|
|
3409
|
+
if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
|
|
3410
|
+
return { error: 'Optional parameter "snapshot_id" must be a string.' };
|
|
3411
|
+
}
|
|
3412
|
+
const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
|
|
3413
|
+
if (!selected.snapshot) {
|
|
3414
|
+
return {
|
|
3415
|
+
error: `Graph snapshot not found in local history: ${snapshotId}`,
|
|
3416
|
+
expected_path: displayWorkspacePath(selected.path),
|
|
3417
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3418
|
+
history: readGraphSnapshotHistory(projectRoot)
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
const snapshot = selected.snapshot;
|
|
3422
|
+
const snapshotHistoryPath = graphSnapshotHistoryPath(projectRoot, snapshot.id);
|
|
3423
|
+
let comparison = null;
|
|
3424
|
+
if (args.compare_to !== void 0 && typeof args.compare_to !== "string") {
|
|
3425
|
+
return { error: 'Optional parameter "compare_to" must be a string.' };
|
|
3426
|
+
}
|
|
3427
|
+
const compareTo = typeof args.compare_to === "string" ? args.compare_to.trim() : "";
|
|
3428
|
+
if (compareTo) {
|
|
3429
|
+
const baseline = readGraphSnapshotById(projectRoot, compareTo);
|
|
3430
|
+
if (!baseline.snapshot) {
|
|
3431
|
+
return {
|
|
3432
|
+
error: `Comparison graph snapshot not found in local history: ${compareTo}`,
|
|
3433
|
+
expected_path: displayWorkspacePath(baseline.path),
|
|
3434
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3435
|
+
selected_snapshot_id: snapshot.id,
|
|
3436
|
+
history: readGraphSnapshotHistory(projectRoot)
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
const diff = diffGraphSnapshots(baseline.snapshot, snapshot);
|
|
3440
|
+
const limit = graphToolLimit(args);
|
|
3441
|
+
comparison = {
|
|
3442
|
+
from: baseline.snapshot.id,
|
|
3443
|
+
to: snapshot.id,
|
|
3444
|
+
summary: summarizeGraphDiff(diff),
|
|
3445
|
+
...args.include_diff_ops === true ? {
|
|
3446
|
+
ops: diff.ops.slice(0, limit),
|
|
3447
|
+
ops_truncated: diff.ops.length > limit,
|
|
3448
|
+
limit
|
|
3449
|
+
} : {}
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
const route = typeof args.route === "string" ? args.route : void 0;
|
|
3453
|
+
const task = typeof args.task === "string" ? args.task : "";
|
|
3454
|
+
if (args.node_id !== void 0 && typeof args.node_id !== "string") {
|
|
3455
|
+
return { error: 'Optional parameter "node_id" must be a string.' };
|
|
3456
|
+
}
|
|
3457
|
+
if (args.file_path !== void 0 && typeof args.file_path !== "string") {
|
|
3458
|
+
return { error: 'Optional parameter "file_path" must be a string.' };
|
|
3459
|
+
}
|
|
3460
|
+
const nodeId = typeof args.node_id === "string" ? args.node_id.trim() : "";
|
|
3461
|
+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
|
|
3462
|
+
const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
|
|
3463
|
+
if (filePath && !fileNodeId) {
|
|
3464
|
+
return {
|
|
3465
|
+
error: `Source file not found in graph snapshot: ${filePath}`,
|
|
3466
|
+
snapshot_id: snapshot.id,
|
|
3467
|
+
available_routes: graphAvailableRoutes(snapshot),
|
|
3468
|
+
available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
if (route) {
|
|
3472
|
+
const subgraph = buildGraphRouteContext(snapshot, route, { task });
|
|
3473
|
+
if (!subgraph) {
|
|
3474
|
+
return {
|
|
3475
|
+
error: `Route not found in graph snapshot: ${route}`,
|
|
3476
|
+
snapshot_id: snapshot.id,
|
|
3477
|
+
available_routes: graphAvailableRoutes(snapshot)
|
|
3478
|
+
};
|
|
3479
|
+
}
|
|
3480
|
+
return {
|
|
3481
|
+
source: "local_graph",
|
|
3482
|
+
artifact_path: displayWorkspacePath(selected.path),
|
|
3483
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3484
|
+
snapshot_id: snapshot.id,
|
|
3485
|
+
schema_version: snapshot.schema_version,
|
|
3486
|
+
project_id: snapshot.project_id,
|
|
3487
|
+
source_hash: snapshot.source_hash,
|
|
3488
|
+
route,
|
|
3489
|
+
comparison,
|
|
3490
|
+
ranking: subgraph.ranking,
|
|
3491
|
+
summary: subgraph.summary,
|
|
3492
|
+
route_node: subgraph.routeNode,
|
|
3493
|
+
ids: subgraph.ids,
|
|
3494
|
+
ranked: subgraph.ranked,
|
|
3495
|
+
nodes: subgraph.nodes,
|
|
3496
|
+
edges: subgraph.edges
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
const impactSeedIds = [
|
|
3500
|
+
...new Set([nodeId, fileNodeId].filter((value) => Boolean(value)))
|
|
3501
|
+
];
|
|
3502
|
+
if (impactSeedIds.length > 0) {
|
|
3503
|
+
const limit = graphToolLimit(args);
|
|
3504
|
+
const impact = buildGraphImpactContext(snapshot, impactSeedIds, { task, limit });
|
|
3505
|
+
if (!impact) {
|
|
3506
|
+
return {
|
|
3507
|
+
error: `Impact seed not found in graph snapshot: ${impactSeedIds.join(", ")}`,
|
|
3508
|
+
snapshot_id: snapshot.id,
|
|
3509
|
+
available_routes: graphAvailableRoutes(snapshot),
|
|
3510
|
+
available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
return {
|
|
3514
|
+
source: "local_graph",
|
|
3515
|
+
artifact_path: displayWorkspacePath(selected.path),
|
|
3516
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3517
|
+
snapshot_id: snapshot.id,
|
|
3518
|
+
schema_version: snapshot.schema_version,
|
|
3519
|
+
project_id: snapshot.project_id,
|
|
3520
|
+
source_hash: snapshot.source_hash,
|
|
3521
|
+
node_id: nodeId || void 0,
|
|
3522
|
+
file_path: filePath || void 0,
|
|
3523
|
+
resolved_node_ids: impactSeedIds,
|
|
3524
|
+
comparison,
|
|
3525
|
+
ranking: impact.ranking,
|
|
3526
|
+
summary: impact.summary,
|
|
3527
|
+
seed_nodes: impact.seedNodes,
|
|
3528
|
+
missing_node_ids: impact.missingNodeIds,
|
|
3529
|
+
ids: impact.ids,
|
|
3530
|
+
ranked: impact.ranked,
|
|
3531
|
+
nodes: impact.nodes,
|
|
3532
|
+
edges: impact.edges
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
if (args.include_full === true) {
|
|
3536
|
+
return {
|
|
3537
|
+
source: "local_graph",
|
|
3538
|
+
artifact_path: displayWorkspacePath(selected.path),
|
|
3539
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3540
|
+
comparison,
|
|
3541
|
+
snapshot
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
return {
|
|
3545
|
+
source: "local_graph",
|
|
3546
|
+
artifact_path: displayWorkspacePath(selected.path),
|
|
3547
|
+
snapshot_history_path: displayWorkspacePath(snapshotHistoryPath),
|
|
3548
|
+
snapshot_history_present: existsSync(snapshotHistoryPath),
|
|
3549
|
+
snapshot_history_count: graphSnapshotHistoryCount(projectRoot),
|
|
3550
|
+
current_snapshot_id: currentSnapshot.id,
|
|
3551
|
+
snapshot_id: snapshot.id,
|
|
3552
|
+
schema_version: snapshot.schema_version,
|
|
3553
|
+
project_id: snapshot.project_id,
|
|
3554
|
+
created_at: snapshot.created_at,
|
|
3555
|
+
source_hash: snapshot.source_hash,
|
|
3556
|
+
summary: snapshot.summary,
|
|
3557
|
+
history: args.include_history === true ? readGraphSnapshotHistory(projectRoot) : void 0,
|
|
3558
|
+
diff_summary: !snapshotId || snapshot.id === currentSnapshot.id ? graphDiff ? summarizeGraphDiff(graphDiff) : null : null,
|
|
3559
|
+
comparison,
|
|
3560
|
+
available_routes: graphAvailableRoutes(snapshot)
|
|
3561
|
+
};
|
|
3562
|
+
} catch (e) {
|
|
3563
|
+
return { error: `Could not read graph snapshot: ${e.message}` };
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
case "decantr_query_graph": {
|
|
3567
|
+
try {
|
|
3568
|
+
const projectRoot = graphProjectRoot(args);
|
|
3569
|
+
const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
|
|
3570
|
+
if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
|
|
3571
|
+
return { error: 'Optional parameter "snapshot_id" must be a string.' };
|
|
3572
|
+
}
|
|
3573
|
+
const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
|
|
3574
|
+
const snapshotPath = selected.path;
|
|
3575
|
+
const snapshot = selected.snapshot;
|
|
3576
|
+
if (!snapshot) {
|
|
3577
|
+
return {
|
|
3578
|
+
error: snapshotId && snapshotId !== "current" ? `Graph snapshot not found: ${snapshotId}. Run \`decantr graph\` to generate snapshot history.` : "Graph snapshot not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
|
|
3579
|
+
expected_path: displayWorkspacePath(snapshotPath)
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
const currentSnapshot = readMcpGraphSnapshot(projectRoot);
|
|
3583
|
+
const nodeIds = stringListArg(args, "node_ids");
|
|
3584
|
+
if (nodeIds.error) return { error: nodeIds.error };
|
|
3585
|
+
if (args.file_path !== void 0 && typeof args.file_path !== "string") {
|
|
3586
|
+
return { error: 'Optional parameter "file_path" must be a string.' };
|
|
3587
|
+
}
|
|
3588
|
+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
|
|
3589
|
+
const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
|
|
3590
|
+
if (filePath && !fileNodeId) {
|
|
3591
|
+
return {
|
|
3592
|
+
error: `Source file not found in graph snapshot: ${filePath}`,
|
|
3593
|
+
snapshot_id: snapshot.id,
|
|
3594
|
+
available_routes: graphAvailableRoutes(snapshot),
|
|
3595
|
+
available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
const resolvedNodeIds = [
|
|
3599
|
+
.../* @__PURE__ */ new Set([...nodeIds.values ?? [], ...fileNodeId ? [fileNodeId] : []])
|
|
3600
|
+
];
|
|
3601
|
+
const nodeType = graphNodeTypeArg(args, "node_type");
|
|
3602
|
+
if (nodeType.error) return { error: nodeType.error };
|
|
3603
|
+
const nodeTypes = graphNodeTypesArg(args, "node_types");
|
|
3604
|
+
if (nodeTypes.error) return { error: nodeTypes.error };
|
|
3605
|
+
const relation = graphRelationArg(args, "relation");
|
|
3606
|
+
if (relation.error) return { error: relation.error };
|
|
3607
|
+
const relations = graphRelationsArg(args, "relations");
|
|
3608
|
+
if (relations.error) return { error: relations.error };
|
|
3609
|
+
const payloadFilter = graphPayloadFilterArgs(args);
|
|
3610
|
+
if (payloadFilter.error) return { error: payloadFilter.error };
|
|
3611
|
+
if (args.task !== void 0 && typeof args.task !== "string") {
|
|
3612
|
+
return { error: 'Optional parameter "task" must be a string.' };
|
|
3613
|
+
}
|
|
3614
|
+
const edgeSrc = typeof args.edge_src === "string" ? args.edge_src.trim() : void 0;
|
|
3615
|
+
const edgeDst = typeof args.edge_dst === "string" ? args.edge_dst.trim() : void 0;
|
|
3616
|
+
if (args.edge_src !== void 0 && typeof args.edge_src !== "string") {
|
|
3617
|
+
return { error: 'Optional parameter "edge_src" must be a string.' };
|
|
3618
|
+
}
|
|
3619
|
+
if (args.edge_dst !== void 0 && typeof args.edge_dst !== "string") {
|
|
3620
|
+
return { error: 'Optional parameter "edge_dst" must be a string.' };
|
|
3621
|
+
}
|
|
3622
|
+
const hasNodeSelector = resolvedNodeIds.length > 0 || !!nodeType.value || !!nodeTypes.values?.length || !!payloadFilter.key || !!payloadFilter.contains;
|
|
3623
|
+
const hasEdgeSelector = !!edgeSrc || !!edgeDst || !!relation.value || !!relations.values?.length;
|
|
3624
|
+
if (!hasNodeSelector && !hasEdgeSelector) {
|
|
3625
|
+
return {
|
|
3626
|
+
error: "Provide at least one graph selector: node_ids, file_path, node_type, node_types, payload_key, payload_contains, edge_src, edge_dst, relation, or relations."
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
const store = createMemoryGraphStore({
|
|
3630
|
+
nodes: snapshot.nodes,
|
|
3631
|
+
edges: snapshot.edges,
|
|
3632
|
+
snapshots: [snapshot]
|
|
3633
|
+
});
|
|
3634
|
+
const limit = graphToolLimit(args);
|
|
3635
|
+
let nodes = [];
|
|
3636
|
+
let edges = [];
|
|
3637
|
+
if (hasNodeSelector) {
|
|
3638
|
+
nodes = await store.queryNodes({
|
|
3639
|
+
ids: resolvedNodeIds.length > 0 ? resolvedNodeIds : void 0,
|
|
3640
|
+
type: nodeType.value,
|
|
3641
|
+
types: nodeTypes.values,
|
|
3642
|
+
payloadKey: payloadFilter.key,
|
|
3643
|
+
payloadValue: payloadFilter.value,
|
|
3644
|
+
payloadContains: payloadFilter.contains
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
if (hasEdgeSelector) {
|
|
3648
|
+
edges = await store.queryEdges({
|
|
3649
|
+
src: edgeSrc,
|
|
3650
|
+
dst: edgeDst,
|
|
3651
|
+
relation: relation.value,
|
|
3652
|
+
relations: relations.values
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
const shouldIncludeEdges = args.include_edges === true || hasEdgeSelector;
|
|
3656
|
+
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
|
3657
|
+
if (shouldIncludeEdges && hasNodeSelector) {
|
|
3658
|
+
const selectedIds = new Set(nodeMap.keys());
|
|
3659
|
+
edges = dedupeGraphEdges([
|
|
3660
|
+
...edges,
|
|
3661
|
+
...snapshot.edges.filter(
|
|
3662
|
+
(edge) => selectedIds.has(edge.src) || selectedIds.has(edge.dst)
|
|
3663
|
+
)
|
|
3664
|
+
]);
|
|
3665
|
+
}
|
|
3666
|
+
for (const edge of edges) {
|
|
3667
|
+
const srcNode = snapshot.nodes.find((node) => node.id === edge.src);
|
|
3668
|
+
const dstNode = snapshot.nodes.find((node) => node.id === edge.dst);
|
|
3669
|
+
if (srcNode) nodeMap.set(srcNode.id, srcNode);
|
|
3670
|
+
if (dstNode) nodeMap.set(dstNode.id, dstNode);
|
|
3671
|
+
}
|
|
3672
|
+
nodes = dedupeGraphNodes([...nodeMap.values()]);
|
|
3673
|
+
edges = dedupeGraphEdges(edges);
|
|
3674
|
+
const limited = limitGraphSubgraph(nodes, edges, limit);
|
|
3675
|
+
const impact = args.include_impact === true && limited.nodes.length > 0 ? buildGraphImpactContext(
|
|
3676
|
+
snapshot,
|
|
3677
|
+
limited.nodes.map((node) => node.id),
|
|
3678
|
+
{
|
|
3679
|
+
task: typeof args.task === "string" ? args.task : void 0,
|
|
3680
|
+
limit
|
|
3681
|
+
}
|
|
3682
|
+
) : null;
|
|
3683
|
+
return {
|
|
3684
|
+
source: "local_graph",
|
|
3685
|
+
artifact_path: displayWorkspacePath(snapshotPath),
|
|
3686
|
+
current_snapshot_id: currentSnapshot?.id ?? null,
|
|
3687
|
+
snapshot_id: snapshot.id,
|
|
3688
|
+
schema_version: snapshot.schema_version,
|
|
3689
|
+
project_id: snapshot.project_id,
|
|
3690
|
+
source_hash: snapshot.source_hash,
|
|
3691
|
+
query: {
|
|
3692
|
+
node_ids: resolvedNodeIds.length > 0 ? resolvedNodeIds : nodeIds.values,
|
|
3693
|
+
file_path: filePath || void 0,
|
|
3694
|
+
node_type: nodeType.value,
|
|
3695
|
+
node_types: nodeTypes.values,
|
|
3696
|
+
payload_key: payloadFilter.key,
|
|
3697
|
+
payload_value: payloadFilter.value,
|
|
3698
|
+
payload_contains: payloadFilter.contains,
|
|
3699
|
+
edge_src: edgeSrc,
|
|
3700
|
+
edge_dst: edgeDst,
|
|
3701
|
+
relation: relation.value,
|
|
3702
|
+
relations: relations.values,
|
|
3703
|
+
include_edges: shouldIncludeEdges,
|
|
3704
|
+
include_impact: args.include_impact === true,
|
|
3705
|
+
task: typeof args.task === "string" ? args.task : void 0,
|
|
3706
|
+
limit
|
|
3707
|
+
},
|
|
3708
|
+
summary: {
|
|
3709
|
+
nodes: limited.nodes.length,
|
|
3710
|
+
edges: limited.edges.length,
|
|
3711
|
+
total_nodes: nodes.length,
|
|
3712
|
+
total_edges: edges.length,
|
|
3713
|
+
truncated: limited.truncated
|
|
3714
|
+
},
|
|
3715
|
+
nodes: limited.nodes,
|
|
3716
|
+
edges: limited.edges,
|
|
3717
|
+
impact
|
|
3718
|
+
};
|
|
3719
|
+
} catch (e) {
|
|
3720
|
+
return { error: `Could not query graph snapshot: ${e.message}` };
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
case "decantr_traverse_graph": {
|
|
3724
|
+
try {
|
|
3725
|
+
const projectRoot = graphProjectRoot(args);
|
|
3726
|
+
const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
|
|
3727
|
+
if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
|
|
3728
|
+
return { error: 'Optional parameter "snapshot_id" must be a string.' };
|
|
3729
|
+
}
|
|
3730
|
+
const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
|
|
3731
|
+
const snapshotPath = selected.path;
|
|
3732
|
+
const snapshot = selected.snapshot;
|
|
3733
|
+
if (!snapshot) {
|
|
3734
|
+
return {
|
|
3735
|
+
error: snapshotId && snapshotId !== "current" ? `Graph snapshot not found: ${snapshotId}. Run \`decantr graph\` to generate snapshot history.` : "Graph snapshot not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
|
|
3736
|
+
expected_path: displayWorkspacePath(snapshotPath)
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
const currentSnapshot = readMcpGraphSnapshot(projectRoot);
|
|
3740
|
+
const fromIds = stringListArg(args, "from_ids");
|
|
3741
|
+
if (fromIds.error) return { error: fromIds.error };
|
|
3742
|
+
const from = typeof args.from === "string" && args.from.trim() ? [args.from.trim()] : [];
|
|
3743
|
+
if (args.from !== void 0 && typeof args.from !== "string") {
|
|
3744
|
+
return { error: 'Optional parameter "from" must be a string.' };
|
|
3745
|
+
}
|
|
3746
|
+
if (args.file_path !== void 0 && typeof args.file_path !== "string") {
|
|
3747
|
+
return { error: 'Optional parameter "file_path" must be a string.' };
|
|
3748
|
+
}
|
|
3749
|
+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
|
|
3750
|
+
const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
|
|
3751
|
+
if (filePath && !fileNodeId) {
|
|
3752
|
+
return {
|
|
3753
|
+
error: `Source file not found in graph snapshot: ${filePath}`,
|
|
3754
|
+
snapshot_id: snapshot.id,
|
|
3755
|
+
available_routes: graphAvailableRoutes(snapshot),
|
|
3756
|
+
available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
const startIds = [
|
|
3760
|
+
.../* @__PURE__ */ new Set([...from, ...fromIds.values ?? [], ...fileNodeId ? [fileNodeId] : []])
|
|
3761
|
+
];
|
|
3762
|
+
if (!startIds.length) {
|
|
3763
|
+
return { error: 'Provide a graph start node with "from", "from_ids", or "file_path".' };
|
|
3764
|
+
}
|
|
3765
|
+
const missingStartIds = startIds.filter(
|
|
3766
|
+
(id) => !snapshot.nodes.some((node) => node.id === id)
|
|
3767
|
+
);
|
|
3768
|
+
if (missingStartIds.length) {
|
|
3769
|
+
return {
|
|
3770
|
+
error: `Start node not found in graph snapshot: ${missingStartIds.join(", ")}`,
|
|
3771
|
+
snapshot_id: snapshot.id,
|
|
3772
|
+
available_routes: graphAvailableRoutes(snapshot),
|
|
3773
|
+
available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
const relations = graphRelationsArg(args, "relations");
|
|
3777
|
+
if (relations.error) return { error: relations.error };
|
|
3778
|
+
const direction = graphTraverseDirectionArg(args);
|
|
3779
|
+
if (direction.error) return { error: direction.error };
|
|
3780
|
+
const depth = graphTraverseDepthArg(args);
|
|
3781
|
+
if (depth.error) return { error: depth.error };
|
|
3782
|
+
const store = createMemoryGraphStore({
|
|
3783
|
+
nodes: snapshot.nodes,
|
|
3784
|
+
edges: snapshot.edges,
|
|
3785
|
+
snapshots: [snapshot]
|
|
3786
|
+
});
|
|
3787
|
+
const result = await store.traverse({
|
|
3788
|
+
from: startIds,
|
|
3789
|
+
relations: relations.values,
|
|
3790
|
+
direction: direction.value,
|
|
3791
|
+
depth: depth.value
|
|
3792
|
+
});
|
|
3793
|
+
const limit = graphToolLimit(args);
|
|
3794
|
+
const limited = limitGraphSubgraph(result.nodes, result.edges, limit);
|
|
3795
|
+
return {
|
|
3796
|
+
source: "local_graph",
|
|
3797
|
+
artifact_path: displayWorkspacePath(snapshotPath),
|
|
3798
|
+
current_snapshot_id: currentSnapshot?.id ?? null,
|
|
3799
|
+
snapshot_id: snapshot.id,
|
|
3800
|
+
schema_version: snapshot.schema_version,
|
|
3801
|
+
project_id: snapshot.project_id,
|
|
3802
|
+
source_hash: snapshot.source_hash,
|
|
3803
|
+
traversal: {
|
|
3804
|
+
from: startIds,
|
|
3805
|
+
file_path: filePath || void 0,
|
|
3806
|
+
resolved_node_ids: startIds,
|
|
3807
|
+
relations: relations.values,
|
|
3808
|
+
direction: direction.value ?? "out",
|
|
3809
|
+
depth: depth.value,
|
|
3810
|
+
limit
|
|
3811
|
+
},
|
|
3812
|
+
summary: {
|
|
3813
|
+
nodes: limited.nodes.length,
|
|
3814
|
+
edges: limited.edges.length,
|
|
3815
|
+
total_nodes: result.nodes.length,
|
|
3816
|
+
total_edges: result.edges.length,
|
|
3817
|
+
truncated: limited.truncated
|
|
3818
|
+
},
|
|
3819
|
+
nodes: limited.nodes,
|
|
3820
|
+
edges: limited.edges
|
|
3821
|
+
};
|
|
3822
|
+
} catch (e) {
|
|
3823
|
+
return { error: `Could not traverse graph snapshot: ${e.message}` };
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
2131
3826
|
case "decantr_get_scaffold_context": {
|
|
2132
3827
|
const contextDir = join2(process.cwd(), ".decantr", "context");
|
|
2133
3828
|
const manifestPath = join2(contextDir, "pack-manifest.json");
|
|
@@ -2446,6 +4141,8 @@ async function handleTool(name, args) {
|
|
|
2446
4141
|
};
|
|
2447
4142
|
}
|
|
2448
4143
|
case "decantr_prepare_task_context": {
|
|
4144
|
+
const projectRoot = graphProjectRoot(args);
|
|
4145
|
+
const projectArg = typeof args.project_path === "string" && args.project_path.trim() ? args.project_path.trim() : null;
|
|
2449
4146
|
const routeArg = typeof args.route === "string" ? args.route : void 0;
|
|
2450
4147
|
const pageArg = typeof args.page_id === "string" ? args.page_id : void 0;
|
|
2451
4148
|
const task = typeof args.task === "string" ? args.task : "";
|
|
@@ -2454,7 +4151,7 @@ async function handleTool(name, args) {
|
|
|
2454
4151
|
}
|
|
2455
4152
|
let essence;
|
|
2456
4153
|
try {
|
|
2457
|
-
const result = await readEssenceFile();
|
|
4154
|
+
const result = await readEssenceFile(join2(projectRoot, "decantr.essence.json"));
|
|
2458
4155
|
essence = result.essence;
|
|
2459
4156
|
} catch {
|
|
2460
4157
|
return { error: "No valid essence file found. Run decantr init first." };
|
|
@@ -2480,7 +4177,10 @@ async function handleTool(name, args) {
|
|
|
2480
4177
|
)
|
|
2481
4178
|
};
|
|
2482
4179
|
}
|
|
2483
|
-
const
|
|
4180
|
+
const resolvedRoute = routeArg ?? (typeof page.route === "string" ? page.route : Object.entries(essence.blueprint.routes ?? {}).find(
|
|
4181
|
+
([, entry]) => entry.section === section.id && entry.page === pageId
|
|
4182
|
+
)?.[0]) ?? null;
|
|
4183
|
+
const contextDir = join2(projectRoot, ".decantr", "context");
|
|
2484
4184
|
const manifest = readJsonIfExists(join2(contextDir, "pack-manifest.json"));
|
|
2485
4185
|
const pageManifest = manifest?.pages.find((entry) => entry.id === pageId) ?? null;
|
|
2486
4186
|
const sectionManifest = manifest?.sections.find((entry) => entry.id === section.id) ?? null;
|
|
@@ -2491,19 +4191,28 @@ async function handleTool(name, args) {
|
|
|
2491
4191
|
const sectionContext = existsSync(sectionContextPath) ? readFileSync(sectionContextPath, "utf-8") : null;
|
|
2492
4192
|
const pagePackSummary = summarizePackJson(pagePackJson);
|
|
2493
4193
|
const sectionPackSummary = summarizePackJson(sectionPackJson);
|
|
2494
|
-
const visualManifest = readJsonIfExists(join2(
|
|
2495
|
-
const visualRoute = visualManifest?.routes?.find((entry) => entry.route ===
|
|
2496
|
-
(entry) => entry.screenshot?.includes(routeSlug(
|
|
4194
|
+
const visualManifest = readJsonIfExists(join2(projectRoot, ".decantr", "evidence", "visual-manifest.json"));
|
|
4195
|
+
const visualRoute = visualManifest?.routes?.find((entry) => entry.route === resolvedRoute) ?? visualManifest?.routes?.find(
|
|
4196
|
+
(entry) => entry.screenshot?.includes(routeSlug(resolvedRoute ?? pageId))
|
|
2497
4197
|
) ?? null;
|
|
2498
|
-
const health = readJsonIfExists(join2(
|
|
4198
|
+
const health = readJsonIfExists(join2(projectRoot, ".decantr", "health-baseline-diff.json"));
|
|
2499
4199
|
const themeInventory = readJsonIfExists(
|
|
2500
|
-
join2(
|
|
4200
|
+
join2(projectRoot, ".decantr", "theme-inventory.json")
|
|
2501
4201
|
);
|
|
2502
|
-
const localLaw = localLawSummary(
|
|
2503
|
-
const styleBridge = styleBridgeSummary(
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
4202
|
+
const localLaw = localLawSummary(projectRoot);
|
|
4203
|
+
const styleBridge = styleBridgeSummary(projectRoot);
|
|
4204
|
+
const displayedLocalLaw = {
|
|
4205
|
+
...localLaw,
|
|
4206
|
+
patterns_path: displayProjectFile(projectRoot, localLaw.patterns_path),
|
|
4207
|
+
rules_path: displayProjectFile(projectRoot, localLaw.rules_path)
|
|
4208
|
+
};
|
|
4209
|
+
const displayedStyleBridge = {
|
|
4210
|
+
...styleBridge,
|
|
4211
|
+
path: displayProjectFile(projectRoot, styleBridge.path)
|
|
4212
|
+
};
|
|
4213
|
+
const projectJson = readJsonIfExists(join2(projectRoot, ".decantr", "project.json"));
|
|
4214
|
+
const changedFiles = changedFilesForTask(projectRoot);
|
|
4215
|
+
const changedRoutes = impactedRoutesForFiles(projectRoot, changedFiles);
|
|
2507
4216
|
const patternIds = extractPagePatternIds(page);
|
|
2508
4217
|
const ranked = rankPatternCandidates(
|
|
2509
4218
|
{
|
|
@@ -2513,7 +4222,7 @@ async function handleTool(name, args) {
|
|
|
2513
4222
|
patternIds.map((id) => patternToDiscoveryCandidate({ id, name: id, description: id }))
|
|
2514
4223
|
);
|
|
2515
4224
|
return {
|
|
2516
|
-
route:
|
|
4225
|
+
route: resolvedRoute,
|
|
2517
4226
|
page_id: pageId,
|
|
2518
4227
|
section_id: section.id,
|
|
2519
4228
|
section_role: section.role,
|
|
@@ -2531,18 +4240,18 @@ async function handleTool(name, args) {
|
|
|
2531
4240
|
section_context: sectionContext,
|
|
2532
4241
|
page_pack_excerpt: pagePackMarkdown ? pagePackMarkdown.slice(0, 12e3) : null,
|
|
2533
4242
|
health_evidence: health ? {
|
|
2534
|
-
baseline_path: health.baselinePath,
|
|
4243
|
+
baseline_path: displayProjectFile(projectRoot, health.baselinePath),
|
|
2535
4244
|
saved_at: health.savedAt,
|
|
2536
4245
|
status_changed: health.statusChanged,
|
|
2537
4246
|
score_delta: health.scoreDelta,
|
|
2538
4247
|
added_findings: health.addedFindings?.slice(0, 8) ?? [],
|
|
2539
4248
|
resolved_findings: health.resolvedFindings?.slice(0, 8) ?? [],
|
|
2540
4249
|
changed_routes: health.changedRoutes ?? [],
|
|
2541
|
-
changed_screenshots: health.changedScreenshots ?? [],
|
|
4250
|
+
changed_screenshots: (health.changedScreenshots ?? []).map((path) => displayProjectFile(projectRoot, path)).filter((path) => Boolean(path)),
|
|
2542
4251
|
contract_drift: health.contractDrift ?? []
|
|
2543
4252
|
} : null,
|
|
2544
4253
|
visual_evidence: visualRoute ? {
|
|
2545
|
-
screenshot: visualRoute.screenshot
|
|
4254
|
+
screenshot: displayProjectFile(projectRoot, visualRoute.screenshot),
|
|
2546
4255
|
screenshot_hash: visualRoute.screenshotHash ?? null,
|
|
2547
4256
|
status: visualRoute.status ?? null,
|
|
2548
4257
|
error: visualRoute.error ?? null
|
|
@@ -2550,10 +4259,10 @@ async function handleTool(name, args) {
|
|
|
2550
4259
|
theme_inventory: themeInventory ? {
|
|
2551
4260
|
modes: themeInventory.modes,
|
|
2552
4261
|
variants: themeInventory.variants,
|
|
2553
|
-
path: ".decantr/theme-inventory.json"
|
|
4262
|
+
path: displayProjectFile(projectRoot, ".decantr/theme-inventory.json")
|
|
2554
4263
|
} : null,
|
|
2555
|
-
local_law:
|
|
2556
|
-
style_bridge:
|
|
4264
|
+
local_law: displayedLocalLaw,
|
|
4265
|
+
style_bridge: displayedStyleBridge,
|
|
2557
4266
|
authority: taskAuthoritySummary({
|
|
2558
4267
|
workflowMode: projectJson?.initialized?.workflowMode ?? null,
|
|
2559
4268
|
adoptionMode: projectJson?.initialized?.adoptionMode ?? null,
|
|
@@ -2567,17 +4276,25 @@ async function handleTool(name, args) {
|
|
|
2567
4276
|
changed_file_count: changedFiles.length,
|
|
2568
4277
|
impacted_routes: changedRoutes
|
|
2569
4278
|
},
|
|
2570
|
-
|
|
4279
|
+
typed_graph: buildTaskTypedGraphContext(projectRoot, resolvedRoute, task, changedFiles),
|
|
4280
|
+
verify_command: projectArg ? `decantr verify --project ${projectArg} --brownfield --local-patterns` : "decantr verify --brownfield --local-patterns",
|
|
2571
4281
|
local_files: {
|
|
2572
|
-
page_pack:
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
4282
|
+
page_pack: displayProjectFile(
|
|
4283
|
+
projectRoot,
|
|
4284
|
+
pageManifest ? join2(".decantr", "context", pageManifest.markdown) : null
|
|
4285
|
+
),
|
|
4286
|
+
section_pack: displayProjectFile(
|
|
4287
|
+
projectRoot,
|
|
4288
|
+
sectionManifest ? join2(".decantr", "context", sectionManifest.markdown) : null
|
|
4289
|
+
),
|
|
4290
|
+
graph_snapshot: existsSync(join2(projectRoot, ".decantr", "graph", "graph.snapshot.json")) ? displayProjectFile(projectRoot, ".decantr/graph/graph.snapshot.json") : null,
|
|
4291
|
+
section_context: existsSync(sectionContextPath) ? displayProjectFile(projectRoot, `.decantr/context/section-${section.id}.md`) : null,
|
|
4292
|
+
local_patterns: displayedLocalLaw.patterns_path,
|
|
4293
|
+
local_rules: displayedLocalLaw.rules_path,
|
|
4294
|
+
style_bridge: displayedStyleBridge.path,
|
|
2578
4295
|
visual_manifest: existsSync(
|
|
2579
|
-
join2(
|
|
2580
|
-
) ? ".decantr/evidence/visual-manifest.json" : null
|
|
4296
|
+
join2(projectRoot, ".decantr", "evidence", "visual-manifest.json")
|
|
4297
|
+
) ? displayProjectFile(projectRoot, ".decantr/evidence/visual-manifest.json") : null
|
|
2581
4298
|
}
|
|
2582
4299
|
};
|
|
2583
4300
|
}
|
|
@@ -2828,6 +4545,101 @@ async function handleTool(name, args) {
|
|
|
2828
4545
|
}
|
|
2829
4546
|
return auditProject(projectRoot);
|
|
2830
4547
|
}
|
|
4548
|
+
case "decantr_get_findings": {
|
|
4549
|
+
if (args.severity != null && args.severity !== "error" && args.severity !== "warn" && args.severity !== "info") {
|
|
4550
|
+
return { error: "Invalid severity. Must be one of: error, warn, info." };
|
|
4551
|
+
}
|
|
4552
|
+
const findingSources = [
|
|
4553
|
+
"audit",
|
|
4554
|
+
"assertion",
|
|
4555
|
+
"browser",
|
|
4556
|
+
"check",
|
|
4557
|
+
"brownfield",
|
|
4558
|
+
"design-token",
|
|
4559
|
+
"style-bridge",
|
|
4560
|
+
"graph",
|
|
4561
|
+
"runtime",
|
|
4562
|
+
"pack",
|
|
4563
|
+
"interaction"
|
|
4564
|
+
];
|
|
4565
|
+
if (args.source != null && (typeof args.source !== "string" || !findingSources.includes(args.source))) {
|
|
4566
|
+
return { error: `Invalid source. Must be one of: ${findingSources.join(", ")}.` };
|
|
4567
|
+
}
|
|
4568
|
+
if (args.code != null && typeof args.code !== "string") {
|
|
4569
|
+
return { error: "Invalid code. Must be a string when provided." };
|
|
4570
|
+
}
|
|
4571
|
+
if (args.include_prompts != null && typeof args.include_prompts !== "boolean") {
|
|
4572
|
+
return { error: "Invalid include_prompts. Must be a boolean when provided." };
|
|
4573
|
+
}
|
|
4574
|
+
if (args.limit != null && (typeof args.limit !== "number" || !Number.isFinite(args.limit))) {
|
|
4575
|
+
return { error: "Invalid limit. Must be a finite number when provided." };
|
|
4576
|
+
}
|
|
4577
|
+
try {
|
|
4578
|
+
const projectRoot = resolveMcpProjectRoot(args.project_path);
|
|
4579
|
+
const state = await getMcpHealthState(projectRoot);
|
|
4580
|
+
const severity = args.severity;
|
|
4581
|
+
const source = args.source;
|
|
4582
|
+
const code = typeof args.code === "string" ? args.code : void 0;
|
|
4583
|
+
const includePrompts = args.include_prompts === true;
|
|
4584
|
+
const limit = typeof args.limit === "number" ? Math.max(1, Math.min(200, Math.floor(args.limit))) : 50;
|
|
4585
|
+
const filtered = state.report.findings.filter((finding) => {
|
|
4586
|
+
if (severity && finding.severity !== severity) return false;
|
|
4587
|
+
if (source && finding.source !== source) return false;
|
|
4588
|
+
if (code && finding.code !== code) return false;
|
|
4589
|
+
return true;
|
|
4590
|
+
});
|
|
4591
|
+
const findings = filtered.slice(0, limit).map((finding) => compactMcpFinding(finding, includePrompts));
|
|
4592
|
+
return {
|
|
4593
|
+
project: state.evidence.project,
|
|
4594
|
+
health: state.evidence.health,
|
|
4595
|
+
filters: {
|
|
4596
|
+
severity,
|
|
4597
|
+
source,
|
|
4598
|
+
code,
|
|
4599
|
+
include_prompts: includePrompts,
|
|
4600
|
+
limit
|
|
4601
|
+
},
|
|
4602
|
+
summary: {
|
|
4603
|
+
status: state.report.status,
|
|
4604
|
+
score: state.report.score,
|
|
4605
|
+
total_findings: state.report.findings.length,
|
|
4606
|
+
matched_findings: filtered.length,
|
|
4607
|
+
returned_findings: findings.length,
|
|
4608
|
+
truncated: filtered.length > findings.length
|
|
4609
|
+
},
|
|
4610
|
+
findings
|
|
4611
|
+
};
|
|
4612
|
+
} catch (error) {
|
|
4613
|
+
return { error: error.message };
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
case "decantr_get_repair_plan": {
|
|
4617
|
+
if (args.finding_id != null && typeof args.finding_id !== "string") {
|
|
4618
|
+
return { error: "Invalid finding_id. Must be a string when provided." };
|
|
4619
|
+
}
|
|
4620
|
+
if (args.code != null && typeof args.code !== "string") {
|
|
4621
|
+
return { error: "Invalid code. Must be a string when provided." };
|
|
4622
|
+
}
|
|
4623
|
+
if (args.include_prompt != null && typeof args.include_prompt !== "boolean") {
|
|
4624
|
+
return { error: "Invalid include_prompt. Must be a boolean when provided." };
|
|
4625
|
+
}
|
|
4626
|
+
try {
|
|
4627
|
+
const projectRoot = resolveMcpProjectRoot(args.project_path);
|
|
4628
|
+
const state = await getMcpHealthState(projectRoot);
|
|
4629
|
+
const finding = selectMcpRepairFinding(state.report, {
|
|
4630
|
+
findingId: typeof args.finding_id === "string" ? args.finding_id : void 0,
|
|
4631
|
+
code: typeof args.code === "string" ? args.code : void 0
|
|
4632
|
+
});
|
|
4633
|
+
return buildMcpRepairPlan({
|
|
4634
|
+
evidence: state.evidence,
|
|
4635
|
+
finding,
|
|
4636
|
+
projectRoot,
|
|
4637
|
+
includePrompt: args.include_prompt === true
|
|
4638
|
+
});
|
|
4639
|
+
} catch (error) {
|
|
4640
|
+
return { error: error.message };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
2831
4643
|
case "decantr_get_evidence_bundle": {
|
|
2832
4644
|
try {
|
|
2833
4645
|
const projectRoot = resolveMcpProjectRoot(args.project_path);
|
|
@@ -2857,7 +4669,9 @@ async function handleTool(name, args) {
|
|
|
2857
4669
|
try {
|
|
2858
4670
|
const projectRoot = resolveMcpProjectRoot(args.project_path);
|
|
2859
4671
|
const state = await getMcpHealthState(projectRoot);
|
|
2860
|
-
const finding = (
|
|
4672
|
+
const finding = selectMcpRepairFinding(state.report, {
|
|
4673
|
+
findingId: typeof args.finding_id === "string" ? args.finding_id : void 0
|
|
4674
|
+
});
|
|
2861
4675
|
if (!finding) {
|
|
2862
4676
|
return {
|
|
2863
4677
|
project: state.evidence.project,
|
|
@@ -2891,12 +4705,19 @@ async function handleTool(name, args) {
|
|
|
2891
4705
|
try {
|
|
2892
4706
|
const projectRoot = resolveMcpProjectRoot(args.project_path);
|
|
2893
4707
|
const state = await getMcpHealthState(projectRoot);
|
|
2894
|
-
const finding = (
|
|
4708
|
+
const finding = selectMcpRepairFinding(state.report, {
|
|
4709
|
+
findingId: typeof args.finding_id === "string" ? args.finding_id : void 0
|
|
4710
|
+
});
|
|
2895
4711
|
return {
|
|
2896
4712
|
project: state.evidence.project,
|
|
2897
4713
|
health: state.evidence.health,
|
|
2898
4714
|
report: state.report,
|
|
2899
4715
|
evidence: state.evidence,
|
|
4716
|
+
repair_plan: buildMcpRepairPlan({
|
|
4717
|
+
evidence: state.evidence,
|
|
4718
|
+
finding,
|
|
4719
|
+
projectRoot
|
|
4720
|
+
}),
|
|
2900
4721
|
repair: finding === null ? {
|
|
2901
4722
|
finding: null,
|
|
2902
4723
|
prompt: null,
|
|
@@ -3077,7 +4898,7 @@ function describeUpdate(operation, payload) {
|
|
|
3077
4898
|
}
|
|
3078
4899
|
|
|
3079
4900
|
// src/index.ts
|
|
3080
|
-
var VERSION = "
|
|
4901
|
+
var VERSION = "3.0.0";
|
|
3081
4902
|
var server = new Server({ name: "decantr", version: VERSION }, { capabilities: { tools: {} } });
|
|
3082
4903
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3083
4904
|
return { tools: TOOLS };
|