@decantr/mcp-server 2.4.0 → 3.0.0-next.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.
@@ -4,10 +4,23 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
5
 
6
6
  // src/tools.ts
7
+ import { createHash } from "crypto";
7
8
  import { execFileSync } from "child_process";
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
+ GRAPH_NODE_TYPES,
14
+ GRAPH_RELATIONS,
15
+ buildGraphImpactContext,
16
+ buildGraphRouteContext,
17
+ createMemoryGraphStore,
18
+ diffGraphSnapshots,
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,518 @@ 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(
202
+ (a, b) => b.created_at.localeCompare(a.created_at) || a.id.localeCompare(b.id)
203
+ ).slice(0, Math.max(1, Math.min(100, limit)));
204
+ } catch {
205
+ return [];
206
+ }
207
+ }
208
+ function readGraphSnapshotById(projectRoot, snapshotId) {
209
+ const currentPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
210
+ const currentSnapshot = readJsonIfExists(currentPath);
211
+ if (!snapshotId || snapshotId === "current" || currentSnapshot?.id === snapshotId) {
212
+ return { snapshot: currentSnapshot, path: currentPath };
213
+ }
214
+ const path = graphSnapshotHistoryPath(projectRoot, snapshotId);
215
+ return { snapshot: readJsonIfExists(path), path };
216
+ }
217
+ function readMcpGraphSnapshot(projectRoot) {
218
+ return readJsonIfExists(graphArtifactPath(projectRoot, "graph.snapshot.json"));
219
+ }
220
+ function mcpAnchorHealthFindings(projectRoot, findings) {
221
+ return anchorFindingsToGraph(readMcpGraphSnapshot(projectRoot), findings);
222
+ }
223
+ function displayWorkspacePath(path) {
224
+ const rel = relative2(process.cwd(), path).replace(/\\/g, "/");
225
+ return rel && !rel.startsWith("..") ? rel : path;
226
+ }
227
+ function displayProjectFile(projectRoot, path) {
228
+ if (!path) return null;
229
+ if (/^[a-z]+:\/\//i.test(path)) return path;
230
+ if (isAbsolute2(path)) return displayWorkspacePath(path);
231
+ return displayWorkspacePath(join2(projectRoot, path));
232
+ }
233
+ function graphAvailableRoutes(snapshot) {
234
+ return snapshot.nodes.filter((node) => node.type === "Route").map((node) => graphPayloadString(node.payload, "path") ?? node.id.replace(/^rt:/, "")).sort();
235
+ }
236
+ function graphProjectRelativePath(projectRoot, value) {
237
+ if (!value) return null;
238
+ const absolutePath = isAbsolute2(value) ? value : join2(projectRoot, value);
239
+ const relativePath = relative2(projectRoot, absolutePath).replace(/\\/g, "/");
240
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
241
+ return null;
242
+ }
243
+ return relativePath;
244
+ }
245
+ function graphSourceNodeIdForFile(projectRoot, snapshot, filePath) {
246
+ if (!filePath) return null;
247
+ const trimmed = filePath.trim();
248
+ if (!trimmed) return null;
249
+ if (trimmed.startsWith("src:") && snapshot.nodes.some((node) => node.id === trimmed)) {
250
+ return trimmed;
251
+ }
252
+ const candidates = /* @__PURE__ */ new Set();
253
+ const projectRelative = graphProjectRelativePath(projectRoot, trimmed);
254
+ if (projectRelative) candidates.add(projectRelative);
255
+ try {
256
+ const workspaceRelative = graphProjectRelativePath(projectRoot, resolveWorkspacePath(trimmed));
257
+ if (workspaceRelative) candidates.add(workspaceRelative);
258
+ } catch {
259
+ }
260
+ for (const candidate of candidates) {
261
+ const nodeId = `src:${candidate}`;
262
+ if (snapshot.nodes.some((node) => node.id === nodeId)) return nodeId;
263
+ }
264
+ return snapshot.nodes.find((node) => {
265
+ if (node.type !== "SourceArtifact") return false;
266
+ const path = graphPayloadString(node.payload, "path");
267
+ return Boolean(path && (path === trimmed || candidates.has(path)));
268
+ })?.id ?? null;
269
+ }
270
+ function graphAvailableSourceArtifacts(snapshot) {
271
+ return snapshot.nodes.filter((node) => node.type === "SourceArtifact").map((node) => ({
272
+ id: node.id,
273
+ path: graphPayloadString(node.payload, "path") ?? node.id.replace(/^src:/, ""),
274
+ kind: graphPayloadString(node.payload, "kind") ?? null
275
+ })).sort((a, b) => a.path.localeCompare(b.path));
276
+ }
277
+ function readProjectEssence(projectRoot) {
278
+ return readJsonIfExists(join2(projectRoot, "decantr.essence.json"));
279
+ }
280
+ function readProjectPackManifest(projectRoot) {
281
+ return readJsonIfExists(
282
+ join2(projectRoot, ".decantr", "context", "pack-manifest.json")
283
+ );
284
+ }
285
+ function mcpHashFile(path) {
286
+ if (!existsSync(path)) return null;
287
+ return `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
288
+ }
289
+ function mcpStableJson(value) {
290
+ if (Array.isArray(value)) {
291
+ return `[${value.map((item) => mcpStableJson(item)).join(",")}]`;
292
+ }
293
+ if (value && typeof value === "object") {
294
+ const record = value;
295
+ return `{${Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${mcpStableJson(record[key])}`).join(",")}}`;
296
+ }
297
+ return JSON.stringify(value);
298
+ }
299
+ function mcpHashJson(value) {
300
+ return `sha256:${createHash("sha256").update(mcpStableJson(value)).digest("hex")}`;
301
+ }
302
+ function mcpVisualManifestSourceHash(path) {
303
+ const manifest = readJsonIfExists(path);
304
+ if (!manifest) return null;
305
+ return mcpHashJson({
306
+ version: manifest.version,
307
+ localOnly: manifest.localOnly,
308
+ baseUrl: manifest.baseUrl ?? null,
309
+ routes: (manifest.routes ?? []).map((route) => ({
310
+ route: route.route,
311
+ url: route.url,
312
+ screenshot: route.screenshot,
313
+ screenshotHash: route.screenshotHash ?? null,
314
+ status: route.status,
315
+ error: route.error
316
+ }))
317
+ });
318
+ }
319
+ function mcpStableFindingGraphAnchor(finding) {
320
+ if (!finding.graph) return void 0;
321
+ return {
322
+ node_id: finding.graph.node_id,
323
+ node_type: finding.graph.node_type,
324
+ route: finding.graph.route,
325
+ confidence: finding.graph.confidence,
326
+ reason: finding.graph.reason
327
+ };
328
+ }
329
+ function mcpEvidenceBundleSourceHash(path) {
330
+ const bundle = readJsonIfExists(path);
331
+ if (!bundle) return null;
332
+ return mcpHashJson({
333
+ health: bundle.health ? {
334
+ status: bundle.health.status,
335
+ score: bundle.health.score,
336
+ errorCount: bundle.health.errorCount,
337
+ warnCount: bundle.health.warnCount,
338
+ infoCount: bundle.health.infoCount,
339
+ findingCount: bundle.health.findingCount
340
+ } : null,
341
+ provenance: Object.entries(bundle.provenance ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
342
+ key,
343
+ path: entry.path,
344
+ present: entry.present,
345
+ hash: entry.hash ?? null
346
+ })),
347
+ findings: (bundle.findings ?? []).map((finding) => ({
348
+ id: finding.id,
349
+ code: finding.code,
350
+ source: finding.source,
351
+ category: finding.category,
352
+ severity: finding.severity,
353
+ message: finding.message,
354
+ target: finding.target,
355
+ rule: finding.rule,
356
+ suggestedFix: finding.suggestedFix,
357
+ graph: mcpStableFindingGraphAnchor(finding),
358
+ repair: finding.repair?.id,
359
+ repairPlan: finding.repairPlan ? {
360
+ id: finding.repairPlan.id,
361
+ actions: finding.repairPlan.actions,
362
+ readTargets: finding.repairPlan.readTargets,
363
+ commands: finding.repairPlan.commands
364
+ } : void 0,
365
+ evidence: finding.evidence,
366
+ commands: finding.commands
367
+ }))
368
+ });
369
+ }
370
+ function mcpAnalysisSourceHash(path) {
371
+ const analysis = readJsonIfExists(path);
372
+ if (!analysis) return null;
373
+ return mcpHashJson({
374
+ project: {
375
+ framework: analysis.project?.framework,
376
+ frameworkVersion: analysis.project?.frameworkVersion,
377
+ packageManager: analysis.project?.packageManager,
378
+ hasTypeScript: analysis.project?.hasTypeScript,
379
+ hasTailwind: analysis.project?.hasTailwind,
380
+ projectScope: analysis.project?.projectScope
381
+ },
382
+ routes: {
383
+ strategy: analysis.routes?.strategy,
384
+ routes: (analysis.routes?.routes ?? []).map((route) => ({
385
+ path: route.path,
386
+ file: route.file,
387
+ hasLayout: route.hasLayout
388
+ }))
389
+ },
390
+ styling: {
391
+ approach: analysis.styling?.approach,
392
+ configFile: analysis.styling?.configFile,
393
+ darkMode: analysis.styling?.darkMode,
394
+ cssVariables: analysis.styling?.cssVariables
395
+ },
396
+ layout: {
397
+ shellPattern: analysis.layout?.shellPattern
398
+ },
399
+ features: {
400
+ detected: analysis.features?.detected
401
+ }
402
+ });
403
+ }
404
+ function mcpHealthBaselineDiffSourceHash(path) {
405
+ const diff = readJsonIfExists(path);
406
+ if (!diff) return null;
407
+ return mcpHashJson({
408
+ savedAt: diff.savedAt ?? null,
409
+ statusChanged: diff.statusChanged ?? false,
410
+ scoreDelta: diff.scoreDelta ?? null,
411
+ addedFindings: diff.addedFindings ?? [],
412
+ resolvedFindings: diff.resolvedFindings ?? [],
413
+ changedFiles: diff.changedFiles ?? [],
414
+ changedRoutes: diff.changedRoutes ?? [],
415
+ changedScreenshots: diff.changedScreenshots ?? [],
416
+ contractDrift: diff.contractDrift ?? []
417
+ });
418
+ }
419
+ function mcpHashGraphSource(projectRoot, source) {
420
+ if (!source.path) return null;
421
+ const path = join2(projectRoot, String(source.path));
422
+ if (source.kind === "brownfield-analysis") return mcpAnalysisSourceHash(path);
423
+ if (source.kind === "health-baseline-diff") return mcpHealthBaselineDiffSourceHash(path);
424
+ if (source.kind === "visual-manifest") return mcpVisualManifestSourceHash(path);
425
+ if (source.kind === "evidence-bundle") return mcpEvidenceBundleSourceHash(path);
426
+ return mcpHashFile(path);
427
+ }
428
+ function inspectMcpGraphFreshness(projectRoot) {
429
+ const manifest = readJsonIfExists(
430
+ graphArtifactPath(projectRoot, "graph.manifest.json")
431
+ );
432
+ if (!manifest) {
433
+ return { manifest: null, current: null, staleSources: [] };
434
+ }
435
+ const staleSources = (manifest.sources ?? []).filter((source) => source.path && source.hash).map((source) => {
436
+ const actualHash = mcpHashGraphSource(projectRoot, source);
437
+ return {
438
+ path: String(source.path),
439
+ expected_hash: source.hash,
440
+ actual_hash: actualHash
441
+ };
442
+ }).filter((source) => source.actual_hash !== source.expected_hash);
443
+ return {
444
+ manifest,
445
+ current: staleSources.length === 0,
446
+ staleSources
447
+ };
448
+ }
449
+ function mcpInspectProjectHealthGraph(projectRoot) {
450
+ const graphDir = join2(projectRoot, ".decantr", "graph");
451
+ const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
452
+ const manifestPath = graphArtifactPath(projectRoot, "graph.manifest.json");
453
+ const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
454
+ const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
455
+ const graphDirPresent = existsSync(graphDir);
456
+ const projectMetadataPresent = existsSync(join2(projectRoot, ".decantr", "project.json"));
457
+ const snapshot = readJsonIfExists(snapshotPath);
458
+ const capsule = readJsonIfExists(capsulePath);
459
+ const freshness = inspectMcpGraphFreshness(projectRoot);
460
+ const requiredArtifactPaths = [snapshotPath, manifestPath, diffPath, capsulePath];
461
+ const missingArtifacts = requiredArtifactPaths.filter((path) => !existsSync(path)).map((path) => relative2(projectRoot, path).replace(/\\/g, "/"));
462
+ const current = graphDirPresent || projectMetadataPresent ? missingArtifacts.length === 0 && freshness.current === true : null;
463
+ return {
464
+ present: graphDirPresent,
465
+ ready: current === true && Boolean(snapshot) && Boolean(capsule),
466
+ current,
467
+ snapshotPresent: existsSync(snapshotPath),
468
+ manifestPresent: existsSync(manifestPath),
469
+ diffPresent: existsSync(diffPath),
470
+ capsulePresent: existsSync(capsulePath),
471
+ snapshotId: snapshot?.id ?? null,
472
+ sourceHash: snapshot?.source_hash ?? null,
473
+ contractHash: capsule?.contract_hash ?? null,
474
+ contractCacheKey: capsule?.contract_cache_key ?? null,
475
+ sourceArtifactCount: snapshot?.nodes.filter((node) => node.type === "SourceArtifact").length ?? capsule?.summary?.source_artifacts ?? 0,
476
+ capsuleSourceArtifactLimit: capsule?.source_artifact_limit ?? null,
477
+ capsuleSourceArtifactsTruncated: capsule?.source_artifacts_truncated ?? null,
478
+ staleArtifacts: current === false ? [
479
+ ...missingArtifacts,
480
+ ...freshness.staleSources.map(
481
+ (source) => `${source.path} changed since graph manifest generation`
482
+ )
483
+ ] : [],
484
+ error: null
485
+ };
486
+ }
487
+ var MCP_GRAPH_NODE_TYPES = new Set(GRAPH_NODE_TYPES);
488
+ var MCP_GRAPH_RELATIONS = new Set(GRAPH_RELATIONS);
489
+ var MCP_GRAPH_DEFAULT_LIMIT = 200;
490
+ var MCP_GRAPH_MAX_LIMIT = 500;
491
+ function mcpGraphEdgeKey(edge) {
492
+ return [edge.src, edge.relation, edge.dst, String(edge.idx ?? "")].join("\0");
493
+ }
494
+ function graphToolLimit(args) {
495
+ if (typeof args.limit !== "number" || !Number.isFinite(args.limit)) {
496
+ return MCP_GRAPH_DEFAULT_LIMIT;
497
+ }
498
+ return Math.max(1, Math.min(MCP_GRAPH_MAX_LIMIT, Math.floor(args.limit)));
499
+ }
500
+ function stringListArg(args, key) {
501
+ const value = args[key];
502
+ if (value === void 0) return {};
503
+ if (typeof value === "string") {
504
+ const trimmed = value.trim();
505
+ return trimmed ? { values: [trimmed] } : { values: [] };
506
+ }
507
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
508
+ return { error: `Optional parameter "${key}" must be a string or an array of strings.` };
509
+ }
510
+ return {
511
+ values: value.map((item) => item.trim()).filter(Boolean)
512
+ };
513
+ }
514
+ function graphNodeTypeArg(args, key) {
515
+ const value = args[key];
516
+ if (value === void 0) return {};
517
+ if (typeof value !== "string" || !MCP_GRAPH_NODE_TYPES.has(value)) {
518
+ return {
519
+ error: `Optional parameter "${key}" must be one of: ${GRAPH_NODE_TYPES.join(", ")}.`
520
+ };
521
+ }
522
+ return { value };
523
+ }
524
+ function graphNodeTypesArg(args, key) {
525
+ const parsed = stringListArg(args, key);
526
+ if (parsed.error) return { error: parsed.error };
527
+ if (!parsed.values) return {};
528
+ const invalid = parsed.values.find((value) => !MCP_GRAPH_NODE_TYPES.has(value));
529
+ if (invalid) {
530
+ return {
531
+ error: `Optional parameter "${key}" contains invalid node type "${invalid}". Expected one of: ${GRAPH_NODE_TYPES.join(", ")}.`
532
+ };
533
+ }
534
+ return { values: parsed.values };
535
+ }
536
+ function graphRelationArg(args, key) {
537
+ const value = args[key];
538
+ if (value === void 0) return {};
539
+ if (typeof value !== "string" || !MCP_GRAPH_RELATIONS.has(value)) {
540
+ return {
541
+ error: `Optional parameter "${key}" must be one of: ${GRAPH_RELATIONS.join(", ")}.`
542
+ };
543
+ }
544
+ return { value };
545
+ }
546
+ function graphRelationsArg(args, key) {
547
+ const parsed = stringListArg(args, key);
548
+ if (parsed.error) return { error: parsed.error };
549
+ if (!parsed.values) return {};
550
+ const invalid = parsed.values.find((value) => !MCP_GRAPH_RELATIONS.has(value));
551
+ if (invalid) {
552
+ return {
553
+ error: `Optional parameter "${key}" contains invalid relation "${invalid}". Expected one of: ${GRAPH_RELATIONS.join(", ")}.`
554
+ };
555
+ }
556
+ return { values: parsed.values };
557
+ }
558
+ function graphTraverseDirectionArg(args) {
559
+ const value = args.direction;
560
+ if (value === void 0) return {};
561
+ if (value !== "out" && value !== "in" && value !== "both") {
562
+ return { error: 'Optional parameter "direction" must be one of: out, in, both.' };
563
+ }
564
+ return { value };
565
+ }
566
+ function graphTraverseDepthArg(args) {
567
+ const value = args.depth;
568
+ if (value === void 0) return { value: 1 };
569
+ if (typeof value !== "number" || !Number.isFinite(value)) {
570
+ return { value: 1, error: 'Optional parameter "depth" must be a number.' };
571
+ }
572
+ return { value: Math.max(0, Math.min(4, Math.floor(value))) };
573
+ }
574
+ function dedupeGraphNodes(nodes) {
575
+ return sortGraphNodes([...new Map(nodes.map((node) => [node.id, node])).values()]);
576
+ }
577
+ function dedupeGraphEdges(edges) {
578
+ return sortGraphEdges([...new Map(edges.map((edge) => [mcpGraphEdgeKey(edge), edge])).values()]);
579
+ }
580
+ function graphPayloadFilterArgs(args) {
581
+ const key = args.payload_key;
582
+ const value = args.payload_value;
583
+ const contains = args.payload_contains;
584
+ if (key !== void 0 && typeof key !== "string") {
585
+ return { error: 'Optional parameter "payload_key" must be a string.' };
586
+ }
587
+ if (value !== void 0 && typeof value !== "string") {
588
+ return { error: 'Optional parameter "payload_value" must be a string.' };
589
+ }
590
+ if (contains !== void 0 && typeof contains !== "string") {
591
+ return { error: 'Optional parameter "payload_contains" must be a string.' };
592
+ }
593
+ if (value !== void 0 && (typeof key !== "string" || !key.trim())) {
594
+ return { error: 'Optional parameter "payload_value" requires "payload_key".' };
595
+ }
596
+ return {
597
+ key: typeof key === "string" && key.trim() ? key.trim() : void 0,
598
+ value: typeof value === "string" ? value : void 0,
599
+ contains: typeof contains === "string" && contains.trim() ? contains.trim() : void 0
600
+ };
601
+ }
602
+ function limitGraphSubgraph(nodes, edges, limit) {
603
+ const limitedNodes = nodes.slice(0, limit);
604
+ const allowedNodeIds = new Set(limitedNodes.map((node) => node.id));
605
+ const limitedEdges = edges.filter((edge) => allowedNodeIds.has(edge.src) && allowedNodeIds.has(edge.dst)).slice(0, limit);
606
+ return {
607
+ nodes: limitedNodes,
608
+ edges: limitedEdges,
609
+ truncated: nodes.length > limitedNodes.length || edges.length > limitedEdges.length
610
+ };
611
+ }
612
+ function buildTaskTypedGraphContext(projectRoot, route, task = "", changedFiles = []) {
613
+ const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
614
+ const snapshot = readJsonIfExists(snapshotPath);
615
+ if (!snapshot) return null;
616
+ const capsule = readJsonIfExists(graphArtifactPath(projectRoot, "contract-capsule.json"));
617
+ const freshness = inspectMcpGraphFreshness(projectRoot);
618
+ const routeContext = route ? buildGraphRouteContext(snapshot, route, { task }) : null;
619
+ const limitedRouteContext = routeContext ? limitGraphSubgraph(routeContext.nodes, routeContext.edges, 120) : null;
620
+ const changedFileNodeIds = [
621
+ ...new Set(
622
+ changedFiles.map((file) => graphSourceNodeIdForFile(projectRoot, snapshot, file)).filter((nodeId) => Boolean(nodeId))
623
+ )
624
+ ];
625
+ const changedFileImpact = changedFileNodeIds.length > 0 ? buildGraphImpactContext(snapshot, changedFileNodeIds, { task, limit: 120 }) : null;
626
+ const limitedChangedFileImpact = changedFileImpact ? limitGraphSubgraph(changedFileImpact.nodes, changedFileImpact.edges, 120) : null;
627
+ return {
628
+ source: "local_graph",
629
+ artifact_path: displayWorkspacePath(snapshotPath),
630
+ snapshot_id: snapshot.id,
631
+ schema_version: snapshot.schema_version,
632
+ project_id: snapshot.project_id,
633
+ source_hash: snapshot.source_hash,
634
+ current: freshness.current,
635
+ stale_sources: freshness.staleSources,
636
+ contract: capsule ? {
637
+ cache_key: capsule.cache_key ?? null,
638
+ contract_hash: capsule.contract_hash ?? null,
639
+ contract_cache_key: capsule.contract_cache_key ?? null,
640
+ summary: capsule.summary ?? null
641
+ } : null,
642
+ route_context: routeContext ? {
643
+ route,
644
+ ranking: routeContext.ranking,
645
+ summary: routeContext.summary,
646
+ ids: routeContext.ids,
647
+ ranked: routeContext.ranked.slice(0, 24),
648
+ nodes: limitedRouteContext?.nodes ?? [],
649
+ edges: limitedRouteContext?.edges ?? [],
650
+ truncated: limitedRouteContext?.truncated ?? false
651
+ } : route ? {
652
+ route,
653
+ error: "Route not found in graph snapshot.",
654
+ available_routes: graphAvailableRoutes(snapshot)
655
+ } : null,
656
+ changed_file_context: changedFiles.length > 0 ? {
657
+ changed_files: changedFiles.slice(0, 40),
658
+ resolved_node_ids: changedFileNodeIds,
659
+ missing_files: changedFiles.filter((file) => !changedFileNodeIds.includes(`src:${file}`)).slice(0, 40),
660
+ impact: changedFileImpact ? {
661
+ ranking: changedFileImpact.ranking,
662
+ summary: changedFileImpact.summary,
663
+ ids: changedFileImpact.ids,
664
+ ranked: changedFileImpact.ranked.slice(0, 24),
665
+ nodes: limitedChangedFileImpact?.nodes ?? [],
666
+ edges: limitedChangedFileImpact?.edges ?? [],
667
+ truncated: limitedChangedFileImpact?.truncated ?? false
668
+ } : null
669
+ } : null
670
+ };
671
+ }
141
672
  function changedFilesForTask(projectRoot) {
142
673
  const changed = /* @__PURE__ */ new Set();
143
674
  try {
@@ -191,12 +722,29 @@ function localLawSummary(projectRoot) {
191
722
  })) ?? []
192
723
  };
193
724
  }
725
+ function styleBridgeSummary(projectRoot) {
726
+ const bridge = readJsonIfExists(join2(projectRoot, ".decantr", "style-bridge.json"));
727
+ return {
728
+ path: bridge ? ".decantr/style-bridge.json" : null,
729
+ status: bridge?.status ?? null,
730
+ styling_approach: bridge?.styling?.approach ?? null,
731
+ theme_modes: bridge?.styling?.themeModes ?? [],
732
+ mappings: bridge?.mappings?.map((mapping) => ({
733
+ id: mapping.id ?? "unknown",
734
+ label: mapping.label ?? null,
735
+ token_hints: mapping.tokenHints?.slice(0, 6) ?? [],
736
+ class_hints: mapping.classHints?.slice(0, 4) ?? [],
737
+ guardrails: mapping.guardrails?.slice(0, 3) ?? []
738
+ })) ?? []
739
+ };
740
+ }
194
741
  function mentionsWord(text, term) {
195
742
  const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
196
743
  return new RegExp(`\\b${escaped}\\b`, "i").test(text);
197
744
  }
198
745
  function taskAuthoritySummary(input) {
199
746
  const hasLocalLaw = input.localLaw.patterns.length > 0 || input.localLaw.rules.length > 0;
747
+ const hasStyleBridge = Boolean(input.styleBridge.path) || input.adoptionMode === "style-bridge";
200
748
  let lane = "Brownfield contract-only";
201
749
  let sourceAuthority = "Existing app is authoritative; Decantr supplies contract context.";
202
750
  let styleAuthority = "Use the existing styling system.";
@@ -209,11 +757,11 @@ function taskAuthoritySummary(input) {
209
757
  sourceAuthority = "Existing app remains authoritative except where Decantr CSS is explicitly adopted.";
210
758
  styleAuthority = "Decantr CSS runtime is active where adopted.";
211
759
  activeAuthorities.push("Decantr CSS runtime");
212
- } else if (input.workflowMode === "brownfield-attach" && input.adoptionMode === "style-bridge") {
760
+ } else if (input.workflowMode === "brownfield-attach" && hasStyleBridge) {
213
761
  lane = "Hybrid style bridge";
214
762
  sourceAuthority = "Existing app remains authoritative; Decantr intent maps through the style bridge.";
215
763
  styleAuthority = "Use bridge tokens/classes as a mapping layer onto the app styling system.";
216
- activeAuthorities.push("style bridge");
764
+ activeAuthorities.push("accepted style bridge");
217
765
  } else if (input.workflowMode === "brownfield-attach" && hasLocalLaw) {
218
766
  lane = "Hybrid local law";
219
767
  sourceAuthority = "Existing app plus accepted project-owned UI law are authoritative.";
@@ -664,6 +1212,10 @@ function mcpCommandsForFinding(source) {
664
1212
  return ["decantr check", "decantr health"];
665
1213
  case "design-token":
666
1214
  return ["decantr export --to figma-tokens", "decantr health --evidence"];
1215
+ case "style-bridge":
1216
+ return ["decantr codify --style-bridge", "decantr verify --evidence"];
1217
+ case "graph":
1218
+ return ["decantr graph", "decantr health --evidence"];
667
1219
  case "interaction":
668
1220
  return ["decantr check --strict", "decantr health"];
669
1221
  case "pack":
@@ -691,6 +1243,9 @@ function mcpSourceFromFinding(finding) {
691
1243
  if (category.includes("interaction") || id.includes("interaction") || rule.includes("interaction")) {
692
1244
  return "interaction";
693
1245
  }
1246
+ if (category.includes("style bridge") || id.includes("style-bridge") || rule.includes("style-bridge")) {
1247
+ return "style-bridge";
1248
+ }
694
1249
  return "audit";
695
1250
  }
696
1251
  function mcpBuildRepairPrompt(input) {
@@ -703,7 +1258,9 @@ function mcpBuildRepairPrompt(input) {
703
1258
  `Source: ${input.source}`,
704
1259
  `Severity: ${input.severity}`,
705
1260
  `Category: ${input.category}`,
1261
+ input.code ? `Code: ${input.code}` : null,
706
1262
  `Message: ${input.message}`,
1263
+ input.repair ? `Repair: ${input.repair.id}` : null,
707
1264
  input.evidence.length > 0 ? `Evidence:
708
1265
  ${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
709
1266
  input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
@@ -718,8 +1275,22 @@ ${input.commands.map((command) => `- ${command}`).join("\n")}`
718
1275
  function mcpHealthFinding(input) {
719
1276
  const id = `${input.source}-${mcpSlug(input.baseId || input.rule || `${input.category}-${input.message}`)}`;
720
1277
  const commands = mcpCommandsForFinding(input.source);
1278
+ const diagnostic = deriveVerificationDiagnostic({
1279
+ id,
1280
+ source: input.source,
1281
+ category: input.category,
1282
+ message: input.message,
1283
+ rule: input.rule,
1284
+ target: input.target,
1285
+ file: input.file,
1286
+ suggestedFix: input.suggestedFix,
1287
+ evidence: input.evidence
1288
+ });
1289
+ const code = input.code ?? diagnostic.code;
1290
+ const repair = input.repair ?? diagnostic.repair;
721
1291
  return {
722
1292
  id,
1293
+ code,
723
1294
  source: input.source,
724
1295
  category: input.category,
725
1296
  severity: input.severity,
@@ -729,6 +1300,7 @@ function mcpHealthFinding(input) {
729
1300
  file: input.file,
730
1301
  rule: input.rule,
731
1302
  suggestedFix: input.suggestedFix,
1303
+ repair,
732
1304
  remediation: {
733
1305
  summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
734
1306
  commands,
@@ -738,13 +1310,69 @@ function mcpHealthFinding(input) {
738
1310
  category: input.category,
739
1311
  severity: input.severity,
740
1312
  message: input.message,
1313
+ code,
741
1314
  evidence: input.evidence ?? [],
742
1315
  suggestedFix: input.suggestedFix,
1316
+ repair,
743
1317
  commands
744
1318
  })
745
1319
  }
746
1320
  };
747
1321
  }
1322
+ function mcpCollectGraphArtifactFindings(projectRoot) {
1323
+ const graphDirPresent = existsSync(join2(projectRoot, ".decantr", "graph"));
1324
+ const projectMetadataPresent = existsSync(join2(projectRoot, ".decantr", "project.json"));
1325
+ if (!graphDirPresent && !projectMetadataPresent) {
1326
+ return [];
1327
+ }
1328
+ const essence = readProjectEssence(projectRoot);
1329
+ if (essence && !isV42(essence)) {
1330
+ return [
1331
+ mcpHealthFinding({
1332
+ source: "graph",
1333
+ category: "Typed Contract Graph",
1334
+ severity: "warn",
1335
+ message: "Typed Contract graph could not be derived: active graph workflows require Essence v4.0.0.",
1336
+ evidence: [
1337
+ "Graph derivation reads decantr.essence.json, local rules, style bridge, visual manifest, and saved evidence bundle artifacts."
1338
+ ],
1339
+ target: ".decantr/graph",
1340
+ rule: "typed-graph-current",
1341
+ suggestedFix: "Run `decantr migrate --to v4`, then run `decantr graph`.",
1342
+ baseId: "typed-graph-current"
1343
+ })
1344
+ ];
1345
+ }
1346
+ const graphDir = join2(projectRoot, ".decantr", "graph");
1347
+ const requiredArtifacts = [
1348
+ "graph.snapshot.json",
1349
+ "graph.manifest.json",
1350
+ "graph.diff.json",
1351
+ "contract-capsule.json"
1352
+ ];
1353
+ const missingArtifacts = requiredArtifacts.map((file) => join2(graphDir, file)).filter((path) => !existsSync(path)).map((path) => relative2(projectRoot, path).replace(/\\/g, "/"));
1354
+ const graphFreshness = inspectMcpGraphFreshness(projectRoot);
1355
+ const staleSources = graphFreshness.staleSources.map((source) => source.path);
1356
+ if (!missingArtifacts.length && !staleSources.length) {
1357
+ return [];
1358
+ }
1359
+ return [
1360
+ mcpHealthFinding({
1361
+ source: "graph",
1362
+ category: "Typed Contract Graph",
1363
+ severity: "warn",
1364
+ message: "Typed Contract graph artifacts are missing or stale.",
1365
+ evidence: [
1366
+ ...missingArtifacts,
1367
+ ...staleSources.map((path) => `${path} changed since graph manifest generation`)
1368
+ ].slice(0, 8),
1369
+ target: ".decantr/graph",
1370
+ rule: "typed-graph-current",
1371
+ suggestedFix: "Run `decantr graph` to regenerate graph snapshot, history, diff, manifest, and capsule.",
1372
+ baseId: "typed-graph-current"
1373
+ })
1374
+ ];
1375
+ }
748
1376
  function mcpCollectDeclaredRoutes(essence) {
749
1377
  if (!essence || !isV42(essence)) return [];
750
1378
  return Object.keys(essence.blueprint.routes ?? {}).sort();
@@ -770,6 +1398,8 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
770
1398
  file: finding.file,
771
1399
  rule: finding.rule,
772
1400
  suggestedFix: finding.suggestedFix,
1401
+ code: finding.code,
1402
+ repair: finding.repair,
773
1403
  baseId: finding.id
774
1404
  })
775
1405
  );
@@ -803,10 +1433,17 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
803
1433
  })
804
1434
  );
805
1435
  }
1436
+ for (const finding of mcpCollectGraphArtifactFindings(projectRoot)) {
1437
+ pushUnique(finding);
1438
+ }
1439
+ const anchoredFindings = mcpAnchorHealthFindings(projectRoot, findings).map((finding) => ({
1440
+ ...finding,
1441
+ repairPlan: buildProjectHealthRepairPlan(projectRoot, finding)
1442
+ }));
806
1443
  const counts = {
807
- errorCount: findings.filter((finding) => finding.severity === "error").length,
808
- warnCount: findings.filter((finding) => finding.severity === "warn").length,
809
- infoCount: findings.filter((finding) => finding.severity === "info").length
1444
+ errorCount: anchoredFindings.filter((finding) => finding.severity === "error").length,
1445
+ warnCount: anchoredFindings.filter((finding) => finding.severity === "warn").length,
1446
+ infoCount: anchoredFindings.filter((finding) => finding.severity === "info").length
810
1447
  };
811
1448
  const manifest = audit.packManifest;
812
1449
  return {
@@ -817,7 +1454,7 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
817
1454
  score: mcpScoreFromCounts(counts),
818
1455
  summary: {
819
1456
  ...counts,
820
- findingCount: findings.length,
1457
+ findingCount: anchoredFindings.length,
821
1458
  workflowMode: null,
822
1459
  adoptionMode: null,
823
1460
  essenceVersion: audit.summary.essenceVersion,
@@ -832,7 +1469,7 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
832
1469
  runtimeChecked: audit.runtimeAudit.routeHintsChecked,
833
1470
  runtimeMatched: audit.runtimeAudit.routeHintsMatched,
834
1471
  runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
835
- issues: findings.filter(
1472
+ issues: anchoredFindings.filter(
836
1473
  (finding) => finding.category.toLowerCase().includes("route") || finding.rule?.toLowerCase().includes("route") || finding.id.toLowerCase().includes("route")
837
1474
  ).map((finding) => finding.message)
838
1475
  },
@@ -845,11 +1482,12 @@ function mcpReportFromAudit(projectRoot, audit, assertions) {
845
1482
  mutationPackCount: manifest?.mutations?.length ?? 0,
846
1483
  generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
847
1484
  },
1485
+ graph: mcpInspectProjectHealthGraph(projectRoot),
848
1486
  ci: {
849
1487
  recommendedCommand: "decantr health --ci --fail-on error",
850
1488
  failOn: "error"
851
1489
  },
852
- findings
1490
+ findings: anchoredFindings
853
1491
  };
854
1492
  }
855
1493
  function resolveMcpProjectRoot(value) {
@@ -873,6 +1511,170 @@ async function getMcpHealthState(projectRoot) {
873
1511
  });
874
1512
  return { audit, assertions, report, evidence };
875
1513
  }
1514
+ function compactMcpFinding(finding, includePrompt) {
1515
+ return {
1516
+ id: finding.id,
1517
+ code: finding.code,
1518
+ source: finding.source,
1519
+ category: finding.category,
1520
+ severity: finding.severity,
1521
+ message: finding.message,
1522
+ evidence: finding.evidence,
1523
+ target: finding.target,
1524
+ file: finding.file,
1525
+ rule: finding.rule,
1526
+ suggestedFix: finding.suggestedFix,
1527
+ graph: finding.graph,
1528
+ repair: finding.repair,
1529
+ repairPlan: finding.repairPlan,
1530
+ remediation: {
1531
+ summary: finding.remediation.summary,
1532
+ commands: finding.remediation.commands,
1533
+ prompt: includePrompt ? finding.remediation.prompt : void 0
1534
+ }
1535
+ };
1536
+ }
1537
+ function selectMcpRepairFinding(report, options = {}) {
1538
+ 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;
1539
+ }
1540
+ function mcpRepairPlanAction(finding) {
1541
+ const repairId = finding.repair?.id ?? "manual-repair";
1542
+ if (repairId === "regenerate-typed-graph" || finding.source === "graph") {
1543
+ return {
1544
+ id: repairId,
1545
+ kind: "regenerate_artifact",
1546
+ target: ".decantr/graph",
1547
+ description: "Regenerate the typed Contract graph artifacts from the current project sources.",
1548
+ payload: finding.repair?.payload ?? {}
1549
+ };
1550
+ }
1551
+ if (repairId === "import-existing-component") {
1552
+ return {
1553
+ id: repairId,
1554
+ kind: "replace_duplicate_with_import",
1555
+ target: finding.file ?? finding.target ?? null,
1556
+ description: "Remove the locally redeclared UI primitive and import the existing project-owned component.",
1557
+ payload: finding.repair?.payload ?? {}
1558
+ };
1559
+ }
1560
+ if (repairId === "replace-raw-control-with-local-component") {
1561
+ return {
1562
+ id: repairId,
1563
+ kind: "replace_raw_control_with_component",
1564
+ target: finding.file ?? finding.target ?? null,
1565
+ description: "Replace the raw JSX control with the existing project-owned primitive component.",
1566
+ payload: finding.repair?.payload ?? {}
1567
+ };
1568
+ }
1569
+ if (repairId === "replace-arbitrary-style-with-bridge-token") {
1570
+ return {
1571
+ id: repairId,
1572
+ kind: "replace_arbitrary_style_with_bridge_token",
1573
+ target: finding.file ?? finding.target ?? null,
1574
+ 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.",
1575
+ payload: finding.repair?.payload ?? {}
1576
+ };
1577
+ }
1578
+ return {
1579
+ id: repairId,
1580
+ kind: "manual_repair",
1581
+ target: finding.file ?? finding.target ?? null,
1582
+ description: finding.suggestedFix ?? finding.remediation.summary,
1583
+ payload: finding.repair?.payload ?? {}
1584
+ };
1585
+ }
1586
+ function mcpRepairReadTargets(finding) {
1587
+ const targets = /* @__PURE__ */ new Set(["DECANTR.md", "decantr.essence.json"]);
1588
+ if (finding.source === "graph") {
1589
+ targets.add(".decantr/graph/graph.manifest.json");
1590
+ targets.add(".decantr/graph/graph.snapshot.json");
1591
+ targets.add(".decantr/graph/graph.diff.json");
1592
+ targets.add(".decantr/graph/snapshots/");
1593
+ }
1594
+ if (finding.source === "style-bridge") {
1595
+ targets.add(".decantr/style-bridge.json");
1596
+ }
1597
+ if (finding.source === "pack" || finding.source === "assertion") {
1598
+ targets.add(".decantr/context/pack-manifest.json");
1599
+ }
1600
+ if (finding.graph?.node_id) {
1601
+ targets.add(".decantr/graph/contract-capsule.json");
1602
+ }
1603
+ if (finding.file) {
1604
+ targets.add(finding.file);
1605
+ }
1606
+ if (finding.target && !finding.target.startsWith("http")) {
1607
+ targets.add(finding.target);
1608
+ }
1609
+ return [...targets];
1610
+ }
1611
+ function mcpRepairImpactContext(projectRoot, finding) {
1612
+ const nodeId = finding.graph?.node_id;
1613
+ if (!nodeId) return null;
1614
+ const impact = buildGraphImpactContext(readMcpGraphSnapshot(projectRoot), nodeId, {
1615
+ task: finding.message,
1616
+ limit: 120
1617
+ });
1618
+ if (!impact) return null;
1619
+ return {
1620
+ snapshot_id: impact.snapshotId,
1621
+ source_hash: impact.sourceHash,
1622
+ seed_nodes: impact.seedNodes,
1623
+ summary: impact.summary,
1624
+ ids: impact.ids,
1625
+ ranked: impact.ranked,
1626
+ nodes: impact.nodes,
1627
+ edges: impact.edges
1628
+ };
1629
+ }
1630
+ function buildMcpRepairPlan(input) {
1631
+ if (!input.finding) {
1632
+ return {
1633
+ project: input.evidence.project,
1634
+ health: input.evidence.health,
1635
+ finding: null,
1636
+ plan: null,
1637
+ message: "No Project Health findings require repair.",
1638
+ commands: ["decantr health --evidence"]
1639
+ };
1640
+ }
1641
+ const finding = input.finding;
1642
+ const action = mcpRepairPlanAction(finding);
1643
+ return {
1644
+ project: input.evidence.project,
1645
+ health: input.evidence.health,
1646
+ finding: compactMcpFinding(finding, false),
1647
+ plan: {
1648
+ id: `repair-plan:${finding.id}`,
1649
+ finding_id: finding.id,
1650
+ diagnostic_code: finding.code ?? null,
1651
+ repair_id: finding.repair?.id ?? null,
1652
+ severity: finding.severity,
1653
+ source: finding.source,
1654
+ category: finding.category,
1655
+ graph_anchor: finding.graph ?? null,
1656
+ impact_context: mcpRepairImpactContext(input.projectRoot, finding),
1657
+ actions: [action],
1658
+ evidence: finding.evidence.map((entry, index) => ({
1659
+ id: `evidence:${finding.id}:${index + 1}`,
1660
+ text: entry
1661
+ })),
1662
+ read_targets: mcpRepairReadTargets(finding),
1663
+ preserve: [
1664
+ "existing framework, routing, and styling system",
1665
+ "existing production behavior unrelated to this finding",
1666
+ "accepted local law, style bridge mappings, and graph anchors"
1667
+ ],
1668
+ avoid: [
1669
+ "rewriting unrelated routes",
1670
+ "replacing the app styling system",
1671
+ "regenerating Decantr artifacts unless the finding is about generated context or graph freshness"
1672
+ ],
1673
+ commands: finding.remediation.commands,
1674
+ prompt: input.includePrompt === true ? finding.remediation.prompt : void 0
1675
+ }
1676
+ };
1677
+ }
876
1678
  function discoverMcpWorkspaceProjects(root, maxProjects = 500) {
877
1679
  const projects = [];
878
1680
  function walk(dir, depth) {
@@ -994,8 +1796,8 @@ var TOOLS = [
994
1796
  // 3. decantr_search_registry — network
995
1797
  {
996
1798
  name: "decantr_search_registry",
997
- title: "Search Registry",
998
- description: "Search the Decantr community content registry for patterns, archetypes, themes, and shells.",
1799
+ title: "Search Vocabulary",
1800
+ description: "Search Decantr official/community vocabulary for patterns, archetypes, themes, and shells.",
999
1801
  inputSchema: {
1000
1802
  type: "object",
1001
1803
  properties: {
@@ -1239,69 +2041,297 @@ var TOOLS = [
1239
2041
  type: "string",
1240
2042
  description: 'Section ID (archetype ID, e.g., "ai-chatbot", "auth-full", "settings-full")'
1241
2043
  },
1242
- path: {
2044
+ path: {
2045
+ type: "string",
2046
+ description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
2047
+ },
2048
+ namespace: {
2049
+ type: "string",
2050
+ description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
2051
+ }
2052
+ },
2053
+ required: ["section_id"]
2054
+ },
2055
+ annotations: READ_ONLY
2056
+ },
2057
+ // 15. decantr_get_page_context — local read
2058
+ {
2059
+ name: "decantr_get_page_context",
2060
+ title: "Get Page Context",
2061
+ 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.",
2062
+ inputSchema: {
2063
+ type: "object",
2064
+ properties: {
2065
+ page_id: {
2066
+ type: "string",
2067
+ description: 'Page ID (for example "overview", "settings", or "home").'
2068
+ },
2069
+ path: {
2070
+ type: "string",
2071
+ description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
2072
+ },
2073
+ namespace: {
2074
+ type: "string",
2075
+ description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
2076
+ }
2077
+ },
2078
+ required: ["page_id"]
2079
+ },
2080
+ annotations: READ_ONLY
2081
+ },
2082
+ // 16. decantr_get_project_state — local read
2083
+ {
2084
+ name: "decantr_get_project_state",
2085
+ title: "Get Project State",
2086
+ 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.",
2087
+ inputSchema: {
2088
+ type: "object",
2089
+ properties: {
2090
+ project_path: {
2091
+ type: "string",
2092
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2093
+ }
2094
+ }
2095
+ },
2096
+ annotations: READ_ONLY
2097
+ },
2098
+ // 17. decantr_prepare_task_context — local read
2099
+ {
2100
+ name: "decantr_prepare_task_context",
2101
+ title: "Prepare Task Context",
2102
+ 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.",
2103
+ inputSchema: {
2104
+ type: "object",
2105
+ properties: {
2106
+ project_path: {
2107
+ type: "string",
2108
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2109
+ },
2110
+ route: {
2111
+ type: "string",
2112
+ description: 'Route being edited, for example "/feed". Preferred when known.'
2113
+ },
2114
+ page_id: {
2115
+ type: "string",
2116
+ description: "Page ID when route is unknown."
2117
+ },
2118
+ task: {
2119
+ type: "string",
2120
+ description: "Short task description used to rank relevant patterns and context."
2121
+ }
2122
+ }
2123
+ },
2124
+ annotations: READ_ONLY
2125
+ },
2126
+ // 17. decantr_get_contract_capsule — local typed graph read
2127
+ {
2128
+ name: "decantr_get_contract_capsule",
2129
+ title: "Get Contract Capsule",
2130
+ 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.",
2131
+ inputSchema: {
2132
+ type: "object",
2133
+ properties: {
2134
+ project_path: {
2135
+ type: "string",
2136
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2137
+ }
2138
+ }
2139
+ },
2140
+ annotations: READ_ONLY
2141
+ },
2142
+ // 18. decantr_get_graph_snapshot — local typed graph read
2143
+ {
2144
+ name: "decantr_get_graph_snapshot",
2145
+ title: "Get Graph Snapshot",
2146
+ 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.",
2147
+ inputSchema: {
2148
+ type: "object",
2149
+ properties: {
2150
+ project_path: {
2151
+ type: "string",
2152
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2153
+ },
2154
+ route: {
2155
+ type: "string",
2156
+ description: 'Optional route path, for example "/settings", to return a scoped subgraph.'
2157
+ },
2158
+ node_id: {
2159
+ type: "string",
2160
+ description: 'Optional graph node ID, for example "cmp:button" or "tkn:surface", to return dependency impact context.'
2161
+ },
2162
+ file_path: {
2163
+ type: "string",
2164
+ description: 'Optional project-relative source file path, for example "src/app/page.tsx", to return dependency impact context for its SourceArtifact node.'
2165
+ },
2166
+ snapshot_id: {
2167
+ type: "string",
2168
+ description: 'Optional graph snapshot id to read from .decantr/graph/snapshots. Use "current" or omit for graph.snapshot.json.'
2169
+ },
2170
+ compare_to: {
2171
+ type: "string",
2172
+ description: 'Optional snapshot id to diff against the selected snapshot. Use "current" for graph.snapshot.json.'
2173
+ },
2174
+ include_diff_ops: {
2175
+ type: "boolean",
2176
+ description: "Include diff operation details when compare_to is provided. Defaults to false."
2177
+ },
2178
+ limit: {
2179
+ type: "number",
2180
+ description: "Maximum diff operations or impact nodes to return. Defaults to 200, maximum 500."
2181
+ },
2182
+ include_full: {
2183
+ type: "boolean",
2184
+ description: "Return the full graph snapshot instead of metadata. Defaults to false."
2185
+ },
2186
+ include_history: {
2187
+ type: "boolean",
2188
+ description: "Include a compact index of local snapshot history entries from .decantr/graph/snapshots. Defaults to false."
2189
+ },
2190
+ task: {
2191
+ type: "string",
2192
+ description: "Optional task description used to boost matching nodes in route-scoped or impact ranked context."
2193
+ }
2194
+ }
2195
+ },
2196
+ annotations: READ_ONLY
2197
+ },
2198
+ // 19. decantr_query_graph — local typed graph read
2199
+ {
2200
+ name: "decantr_query_graph",
2201
+ title: "Query Graph",
2202
+ 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.",
2203
+ inputSchema: {
2204
+ type: "object",
2205
+ properties: {
2206
+ project_path: {
2207
+ type: "string",
2208
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2209
+ },
2210
+ snapshot_id: {
2211
+ type: "string",
2212
+ description: 'Optional snapshot id from local graph history, or "current". Defaults to current.'
2213
+ },
2214
+ node_ids: {
2215
+ type: "array",
2216
+ items: { type: "string" },
2217
+ description: 'Optional exact graph node IDs to return, for example ["rt:/feed"].'
2218
+ },
2219
+ file_path: {
2220
+ type: "string",
2221
+ description: 'Optional project-relative source file path, for example "src/app/page.tsx", resolved to a SourceArtifact node selector.'
2222
+ },
2223
+ node_type: {
2224
+ type: "string",
2225
+ enum: [...GRAPH_NODE_TYPES],
2226
+ description: 'Optional single node type selector, for example "Route" or "Component".'
2227
+ },
2228
+ node_types: {
2229
+ type: "array",
2230
+ items: { type: "string", enum: [...GRAPH_NODE_TYPES] },
2231
+ description: "Optional node type selectors."
2232
+ },
2233
+ payload_key: {
2234
+ type: "string",
2235
+ description: 'Optional node payload key or dotted path filter, for example "code" for Finding nodes.'
2236
+ },
2237
+ payload_value: {
2238
+ type: "string",
2239
+ description: 'Optional exact stringified payload value used with payload_key, for example "COMP010".'
2240
+ },
2241
+ payload_contains: {
2242
+ type: "string",
2243
+ description: "Optional case-insensitive substring filter over the node payload JSON."
2244
+ },
2245
+ edge_src: {
2246
+ type: "string",
2247
+ description: "Optional edge source node ID selector."
2248
+ },
2249
+ edge_dst: {
2250
+ type: "string",
2251
+ description: "Optional edge destination node ID selector."
2252
+ },
2253
+ relation: {
2254
+ type: "string",
2255
+ enum: [...GRAPH_RELATIONS],
2256
+ description: 'Optional single edge relation selector, for example "PAGE_COMPOSES_PATTERN".'
2257
+ },
2258
+ relations: {
2259
+ type: "array",
2260
+ items: { type: "string", enum: [...GRAPH_RELATIONS] },
2261
+ description: "Optional edge relation selectors."
2262
+ },
2263
+ include_edges: {
2264
+ type: "boolean",
2265
+ description: "When querying nodes, include incident edges and their opposite endpoint nodes. Defaults to false unless edge selectors are present."
2266
+ },
2267
+ include_impact: {
2268
+ type: "boolean",
2269
+ description: "When querying nodes, also return the dependency impact context for the matched node IDs."
2270
+ },
2271
+ task: {
1243
2272
  type: "string",
1244
- description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
2273
+ description: "Optional task description used to boost matching nodes in the impact ranking when include_impact is true."
1245
2274
  },
1246
- namespace: {
1247
- type: "string",
1248
- description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
2275
+ limit: {
2276
+ type: "number",
2277
+ description: "Maximum nodes and edges to return. Defaults to 200, maximum 500."
1249
2278
  }
1250
- },
1251
- required: ["section_id"]
2279
+ }
1252
2280
  },
1253
2281
  annotations: READ_ONLY
1254
2282
  },
1255
- // 15. decantr_get_page_context — local read
2283
+ // 20. decantr_traverse_graph — local typed graph read
1256
2284
  {
1257
- name: "decantr_get_page_context",
1258
- title: "Get Page Context",
1259
- 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.",
2285
+ name: "decantr_traverse_graph",
2286
+ title: "Traverse Graph",
2287
+ 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.",
1260
2288
  inputSchema: {
1261
2289
  type: "object",
1262
2290
  properties: {
1263
- page_id: {
2291
+ project_path: {
1264
2292
  type: "string",
1265
- description: 'Page ID (for example "overview", "settings", or "home").'
2293
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
1266
2294
  },
1267
- path: {
2295
+ snapshot_id: {
1268
2296
  type: "string",
1269
- description: "Optional path to an essence file when using hosted fallback compilation. Defaults to ./decantr.essence.json."
2297
+ description: 'Optional snapshot id from local graph history, or "current". Defaults to current.'
1270
2298
  },
1271
- namespace: {
1272
- type: "string",
1273
- description: 'Optional preferred public namespace for hosted fallback compilation. Defaults to "@official".'
1274
- }
1275
- },
1276
- required: ["page_id"]
1277
- },
1278
- annotations: READ_ONLY
1279
- },
1280
- // 16. decantr_prepare_task_context — local read
1281
- {
1282
- name: "decantr_prepare_task_context",
1283
- title: "Prepare Task Context",
1284
- 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.",
1285
- inputSchema: {
1286
- type: "object",
1287
- properties: {
1288
- route: {
2299
+ from: {
1289
2300
  type: "string",
1290
- description: 'Route being edited, for example "/feed". Preferred when known.'
2301
+ description: 'Start node ID, for example "rt:/feed" or "tkn:color.primary".'
1291
2302
  },
1292
- page_id: {
2303
+ from_ids: {
2304
+ type: "array",
2305
+ items: { type: "string" },
2306
+ description: "Optional start node IDs. Used when traversing from multiple anchors."
2307
+ },
2308
+ file_path: {
1293
2309
  type: "string",
1294
- description: "Page ID when route is unknown."
2310
+ description: 'Optional project-relative source file path, for example "src/app/page.tsx", resolved to a SourceArtifact start node.'
1295
2311
  },
1296
- task: {
2312
+ relations: {
2313
+ type: "array",
2314
+ items: { type: "string", enum: [...GRAPH_RELATIONS] },
2315
+ description: "Optional relation allow-list. When omitted, all relations are traversed."
2316
+ },
2317
+ direction: {
1297
2318
  type: "string",
1298
- description: "Short task description used to rank relevant patterns and context."
2319
+ enum: ["out", "in", "both"],
2320
+ description: "Traversal direction. Defaults to out."
2321
+ },
2322
+ depth: {
2323
+ type: "number",
2324
+ description: "Traversal depth. Defaults to 1, maximum 4."
2325
+ },
2326
+ limit: {
2327
+ type: "number",
2328
+ description: "Maximum nodes and edges to return. Defaults to 200, maximum 500."
1299
2329
  }
1300
2330
  }
1301
2331
  },
1302
2332
  annotations: READ_ONLY
1303
2333
  },
1304
- // 17. decantr_get_execution_pack — local read
2334
+ // 21. decantr_get_execution_pack — local read
1305
2335
  {
1306
2336
  name: "decantr_get_execution_pack",
1307
2337
  title: "Get Execution Pack",
@@ -1457,7 +2487,85 @@ var TOOLS = [
1457
2487
  },
1458
2488
  annotations: READ_ONLY_NETWORK
1459
2489
  },
1460
- // 22. decantr_get_evidence_bundle — local reliability artifact
2490
+ // 24. decantr_get_findings — local typed findings read
2491
+ {
2492
+ name: "decantr_get_findings",
2493
+ title: "Get Findings",
2494
+ description: "Return typed Project Health findings for the current Decantr project, with stable codes, repair IDs, graph anchors when available, and optional repair prompts.",
2495
+ inputSchema: {
2496
+ type: "object",
2497
+ properties: {
2498
+ project_path: {
2499
+ type: "string",
2500
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2501
+ },
2502
+ severity: {
2503
+ type: "string",
2504
+ enum: ["error", "warn", "info"],
2505
+ description: "Optional severity filter."
2506
+ },
2507
+ source: {
2508
+ type: "string",
2509
+ enum: [
2510
+ "audit",
2511
+ "assertion",
2512
+ "browser",
2513
+ "check",
2514
+ "brownfield",
2515
+ "design-token",
2516
+ "style-bridge",
2517
+ "graph",
2518
+ "runtime",
2519
+ "pack",
2520
+ "interaction"
2521
+ ],
2522
+ description: "Optional finding source filter."
2523
+ },
2524
+ code: {
2525
+ type: "string",
2526
+ description: 'Optional stable diagnostic code filter, for example "TOKEN010".'
2527
+ },
2528
+ include_prompts: {
2529
+ type: "boolean",
2530
+ description: "Include full repair prompts on each finding. Defaults to false to keep context compact."
2531
+ },
2532
+ limit: {
2533
+ type: "number",
2534
+ description: "Maximum findings to return. Defaults to 50, maximum 200."
2535
+ }
2536
+ }
2537
+ },
2538
+ annotations: READ_ONLY
2539
+ },
2540
+ // 25. decantr_get_repair_plan — local structured repair loop
2541
+ {
2542
+ name: "decantr_get_repair_plan",
2543
+ title: "Get Repair Plan",
2544
+ 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.",
2545
+ inputSchema: {
2546
+ type: "object",
2547
+ properties: {
2548
+ project_path: {
2549
+ type: "string",
2550
+ description: "Optional relative project path inside the active workspace. Defaults to the current working directory."
2551
+ },
2552
+ finding_id: {
2553
+ type: "string",
2554
+ description: "Optional finding id. Defaults to the first error or warning, then the first finding."
2555
+ },
2556
+ code: {
2557
+ type: "string",
2558
+ description: 'Optional stable diagnostic code selector, for example "GRAPH001".'
2559
+ },
2560
+ include_prompt: {
2561
+ type: "boolean",
2562
+ description: "Include the human-readable repair prompt alongside the typed plan."
2563
+ }
2564
+ }
2565
+ },
2566
+ annotations: READ_ONLY
2567
+ },
2568
+ // 26. decantr_get_evidence_bundle — local reliability artifact
1461
2569
  {
1462
2570
  name: "decantr_get_evidence_bundle",
1463
2571
  title: "Get Evidence Bundle",
@@ -2111,6 +3219,576 @@ async function handleTool(name, args) {
2111
3219
  return { error: `Failed to update essence: ${e.message}` };
2112
3220
  }
2113
3221
  }
3222
+ case "decantr_get_project_state": {
3223
+ try {
3224
+ const projectRoot = graphProjectRoot(args);
3225
+ const essence = readProjectEssence(projectRoot);
3226
+ const packManifest = readProjectPackManifest(projectRoot);
3227
+ const graphDir = join2(projectRoot, ".decantr", "graph");
3228
+ const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
3229
+ const manifestPath = graphArtifactPath(projectRoot, "graph.manifest.json");
3230
+ const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
3231
+ const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
3232
+ const snapshot = readJsonIfExists(snapshotPath);
3233
+ const graphDiff = readJsonIfExists(diffPath);
3234
+ const snapshotHistoryPath = snapshot ? graphSnapshotHistoryPath(projectRoot, snapshot.id) : null;
3235
+ const snapshotHistoryPresent = snapshotHistoryPath ? existsSync(snapshotHistoryPath) : false;
3236
+ const snapshotHistoryCount = graphSnapshotHistoryCount(projectRoot);
3237
+ const capsule = readJsonIfExists(capsulePath);
3238
+ const graphFreshness = inspectMcpGraphFreshness(projectRoot);
3239
+ const projectConfig = readJsonIfExists(join2(projectRoot, ".decantr", "project.json"));
3240
+ const localPatternsPresent = existsSync(join2(projectRoot, ".decantr", "local-patterns.json"));
3241
+ const localRulesPresent = existsSync(join2(projectRoot, ".decantr", "rules.json"));
3242
+ const styleBridgePresent = existsSync(join2(projectRoot, ".decantr", "style-bridge.json"));
3243
+ const hasGraphArtifacts = existsSync(snapshotPath) && snapshotHistoryPresent && existsSync(manifestPath) && existsSync(diffPath) && existsSync(capsulePath);
3244
+ const graphReady = Boolean(snapshot) && hasGraphArtifacts && graphFreshness.current === true;
3245
+ return {
3246
+ source: "local_workspace",
3247
+ project_root: displayWorkspacePath(projectRoot),
3248
+ essence: essence ? {
3249
+ present: true,
3250
+ version: typeof essence.version === "string" ? essence.version : null,
3251
+ active_v4: isV42(essence),
3252
+ routes: isV42(essence) ? Object.keys(essence.blueprint.routes ?? {}).sort() : [],
3253
+ sections: isV42(essence) ? essence.blueprint.sections.map((section) => ({
3254
+ id: section.id,
3255
+ role: section.role,
3256
+ pages: section.pages.length
3257
+ })) : [],
3258
+ features: isV42(essence) ? essence.blueprint.features : [],
3259
+ guard: isV42(essence) ? essence.meta.guard : null
3260
+ } : {
3261
+ present: false,
3262
+ version: null,
3263
+ active_v4: false,
3264
+ routes: [],
3265
+ sections: [],
3266
+ features: [],
3267
+ guard: null
3268
+ },
3269
+ project_config: {
3270
+ present: Boolean(projectConfig),
3271
+ workflow_mode: projectConfig?.workflowMode ?? null,
3272
+ adoption_mode: projectConfig?.adoptionMode ?? null,
3273
+ telemetry_enabled: projectConfig?.telemetry === true
3274
+ },
3275
+ context: {
3276
+ manifest_present: Boolean(packManifest),
3277
+ scaffold_pack_present: Boolean(packManifest?.scaffold),
3278
+ review_pack_present: Boolean(packManifest?.review),
3279
+ section_pack_count: packManifest?.sections.length ?? 0,
3280
+ page_pack_count: packManifest?.pages.length ?? 0,
3281
+ mutation_pack_count: packManifest?.mutations?.length ?? 0,
3282
+ generated_at: packManifest && typeof packManifest.generatedAt === "string" ? packManifest.generatedAt : null
3283
+ },
3284
+ graph: {
3285
+ graph_dir_present: existsSync(graphDir),
3286
+ manifest_present: Boolean(graphFreshness.manifest),
3287
+ snapshot_present: Boolean(snapshot),
3288
+ snapshot_history_present: snapshotHistoryPresent,
3289
+ snapshot_history_path: snapshotHistoryPath ? displayWorkspacePath(snapshotHistoryPath) : null,
3290
+ snapshot_history_count: snapshotHistoryCount,
3291
+ capsule_present: existsSync(capsulePath),
3292
+ diff_present: existsSync(diffPath),
3293
+ ready: graphReady,
3294
+ current: graphFreshness.current,
3295
+ stale_sources: graphFreshness.staleSources,
3296
+ snapshot_id: snapshot?.id ?? null,
3297
+ schema_version: snapshot?.schema_version ?? null,
3298
+ source_hash: snapshot?.source_hash ?? null,
3299
+ cache_key: capsule?.cache_key ?? null,
3300
+ contract_hash: capsule?.contract_hash ?? null,
3301
+ contract_cache_key: capsule?.contract_cache_key ?? null,
3302
+ capsule_source_artifact_count: capsule?.summary?.source_artifacts ?? null,
3303
+ capsule_source_artifact_limit: capsule?.source_artifact_limit ?? null,
3304
+ capsule_source_artifacts_truncated: capsule?.source_artifacts_truncated ?? null,
3305
+ summary: snapshot?.summary ?? null,
3306
+ diff_summary: graphDiff ? summarizeGraphDiff(graphDiff) : null,
3307
+ available_routes: snapshot ? graphAvailableRoutes(snapshot) : [],
3308
+ source_artifact_count: snapshot ? snapshot.nodes.filter((node) => node.type === "SourceArtifact").length : 0,
3309
+ available_source_artifacts: snapshot ? graphAvailableSourceArtifacts(snapshot).slice(0, 40) : []
3310
+ },
3311
+ local_authority: {
3312
+ local_patterns_present: localPatternsPresent,
3313
+ local_rules_present: localRulesPresent,
3314
+ style_bridge_present: styleBridgePresent
3315
+ },
3316
+ diagnostics: {
3317
+ known_count: KNOWN_VERIFICATION_DIAGNOSTICS.length,
3318
+ families: [
3319
+ ...new Set(KNOWN_VERIFICATION_DIAGNOSTICS.map((entry) => entry.family))
3320
+ ].sort(),
3321
+ codes: KNOWN_VERIFICATION_DIAGNOSTICS.map((entry) => ({
3322
+ code: entry.code,
3323
+ rule: entry.rule,
3324
+ repair_id: entry.repairId,
3325
+ family: entry.family
3326
+ })).sort((a, b) => a.code.localeCompare(b.code) || a.rule.localeCompare(b.rule))
3327
+ },
3328
+ recommended_next_tools: [
3329
+ graphReady ? "decantr_get_contract_capsule" : "decantr_get_findings",
3330
+ graphReady ? null : "decantr_get_repair_plan",
3331
+ snapshot ? "decantr_get_graph_snapshot" : null,
3332
+ "decantr_prepare_task_context",
3333
+ "decantr_get_findings",
3334
+ "decantr_get_evidence_bundle"
3335
+ ].filter((tool) => Boolean(tool))
3336
+ };
3337
+ } catch (e) {
3338
+ return { error: `Could not read project state: ${e.message}` };
3339
+ }
3340
+ }
3341
+ case "decantr_get_contract_capsule": {
3342
+ try {
3343
+ const projectRoot = graphProjectRoot(args);
3344
+ const capsulePath = graphArtifactPath(projectRoot, "contract-capsule.json");
3345
+ const capsule = readJsonIfExists(capsulePath);
3346
+ if (!capsule) {
3347
+ return {
3348
+ error: "Contract capsule not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
3349
+ expected_path: displayWorkspacePath(capsulePath)
3350
+ };
3351
+ }
3352
+ return {
3353
+ source: "local_graph",
3354
+ artifact_path: displayWorkspacePath(capsulePath),
3355
+ capsule
3356
+ };
3357
+ } catch (e) {
3358
+ return { error: `Could not read contract capsule: ${e.message}` };
3359
+ }
3360
+ }
3361
+ case "decantr_get_graph_snapshot": {
3362
+ try {
3363
+ const projectRoot = graphProjectRoot(args);
3364
+ const snapshotPath = graphArtifactPath(projectRoot, "graph.snapshot.json");
3365
+ const diffPath = graphArtifactPath(projectRoot, "graph.diff.json");
3366
+ const currentSnapshot = readJsonIfExists(snapshotPath);
3367
+ const graphDiff = readJsonIfExists(diffPath);
3368
+ if (!currentSnapshot) {
3369
+ return {
3370
+ error: "Graph snapshot not found. Run `decantr graph` from the project root, or `decantr graph --project <path>` from a workspace root.",
3371
+ expected_path: displayWorkspacePath(snapshotPath)
3372
+ };
3373
+ }
3374
+ const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
3375
+ if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
3376
+ return { error: 'Optional parameter "snapshot_id" must be a string.' };
3377
+ }
3378
+ const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
3379
+ if (!selected.snapshot) {
3380
+ return {
3381
+ error: `Graph snapshot not found in local history: ${snapshotId}`,
3382
+ expected_path: displayWorkspacePath(selected.path),
3383
+ current_snapshot_id: currentSnapshot.id,
3384
+ history: readGraphSnapshotHistory(projectRoot)
3385
+ };
3386
+ }
3387
+ const snapshot = selected.snapshot;
3388
+ const snapshotHistoryPath = graphSnapshotHistoryPath(projectRoot, snapshot.id);
3389
+ let comparison = null;
3390
+ if (args.compare_to !== void 0 && typeof args.compare_to !== "string") {
3391
+ return { error: 'Optional parameter "compare_to" must be a string.' };
3392
+ }
3393
+ const compareTo = typeof args.compare_to === "string" ? args.compare_to.trim() : "";
3394
+ if (compareTo) {
3395
+ const baseline = readGraphSnapshotById(projectRoot, compareTo);
3396
+ if (!baseline.snapshot) {
3397
+ return {
3398
+ error: `Comparison graph snapshot not found in local history: ${compareTo}`,
3399
+ expected_path: displayWorkspacePath(baseline.path),
3400
+ current_snapshot_id: currentSnapshot.id,
3401
+ selected_snapshot_id: snapshot.id,
3402
+ history: readGraphSnapshotHistory(projectRoot)
3403
+ };
3404
+ }
3405
+ const diff = diffGraphSnapshots(baseline.snapshot, snapshot);
3406
+ const limit = graphToolLimit(args);
3407
+ comparison = {
3408
+ from: baseline.snapshot.id,
3409
+ to: snapshot.id,
3410
+ summary: summarizeGraphDiff(diff),
3411
+ ...args.include_diff_ops === true ? {
3412
+ ops: diff.ops.slice(0, limit),
3413
+ ops_truncated: diff.ops.length > limit,
3414
+ limit
3415
+ } : {}
3416
+ };
3417
+ }
3418
+ const route = typeof args.route === "string" ? args.route : void 0;
3419
+ const task = typeof args.task === "string" ? args.task : "";
3420
+ if (args.node_id !== void 0 && typeof args.node_id !== "string") {
3421
+ return { error: 'Optional parameter "node_id" must be a string.' };
3422
+ }
3423
+ if (args.file_path !== void 0 && typeof args.file_path !== "string") {
3424
+ return { error: 'Optional parameter "file_path" must be a string.' };
3425
+ }
3426
+ const nodeId = typeof args.node_id === "string" ? args.node_id.trim() : "";
3427
+ const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
3428
+ const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
3429
+ if (filePath && !fileNodeId) {
3430
+ return {
3431
+ error: `Source file not found in graph snapshot: ${filePath}`,
3432
+ snapshot_id: snapshot.id,
3433
+ available_routes: graphAvailableRoutes(snapshot),
3434
+ available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
3435
+ };
3436
+ }
3437
+ if (route) {
3438
+ const subgraph = buildGraphRouteContext(snapshot, route, { task });
3439
+ if (!subgraph) {
3440
+ return {
3441
+ error: `Route not found in graph snapshot: ${route}`,
3442
+ snapshot_id: snapshot.id,
3443
+ available_routes: graphAvailableRoutes(snapshot)
3444
+ };
3445
+ }
3446
+ return {
3447
+ source: "local_graph",
3448
+ artifact_path: displayWorkspacePath(selected.path),
3449
+ current_snapshot_id: currentSnapshot.id,
3450
+ snapshot_id: snapshot.id,
3451
+ schema_version: snapshot.schema_version,
3452
+ project_id: snapshot.project_id,
3453
+ source_hash: snapshot.source_hash,
3454
+ route,
3455
+ comparison,
3456
+ ranking: subgraph.ranking,
3457
+ summary: subgraph.summary,
3458
+ route_node: subgraph.routeNode,
3459
+ ids: subgraph.ids,
3460
+ ranked: subgraph.ranked,
3461
+ nodes: subgraph.nodes,
3462
+ edges: subgraph.edges
3463
+ };
3464
+ }
3465
+ const impactSeedIds = [
3466
+ ...new Set([nodeId, fileNodeId].filter((value) => Boolean(value)))
3467
+ ];
3468
+ if (impactSeedIds.length > 0) {
3469
+ const limit = graphToolLimit(args);
3470
+ const impact = buildGraphImpactContext(snapshot, impactSeedIds, { task, limit });
3471
+ if (!impact) {
3472
+ return {
3473
+ error: `Impact seed not found in graph snapshot: ${impactSeedIds.join(", ")}`,
3474
+ snapshot_id: snapshot.id,
3475
+ available_routes: graphAvailableRoutes(snapshot),
3476
+ available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
3477
+ };
3478
+ }
3479
+ return {
3480
+ source: "local_graph",
3481
+ artifact_path: displayWorkspacePath(selected.path),
3482
+ current_snapshot_id: currentSnapshot.id,
3483
+ snapshot_id: snapshot.id,
3484
+ schema_version: snapshot.schema_version,
3485
+ project_id: snapshot.project_id,
3486
+ source_hash: snapshot.source_hash,
3487
+ node_id: nodeId || void 0,
3488
+ file_path: filePath || void 0,
3489
+ resolved_node_ids: impactSeedIds,
3490
+ comparison,
3491
+ ranking: impact.ranking,
3492
+ summary: impact.summary,
3493
+ seed_nodes: impact.seedNodes,
3494
+ missing_node_ids: impact.missingNodeIds,
3495
+ ids: impact.ids,
3496
+ ranked: impact.ranked,
3497
+ nodes: impact.nodes,
3498
+ edges: impact.edges
3499
+ };
3500
+ }
3501
+ if (args.include_full === true) {
3502
+ return {
3503
+ source: "local_graph",
3504
+ artifact_path: displayWorkspacePath(selected.path),
3505
+ current_snapshot_id: currentSnapshot.id,
3506
+ comparison,
3507
+ snapshot
3508
+ };
3509
+ }
3510
+ return {
3511
+ source: "local_graph",
3512
+ artifact_path: displayWorkspacePath(selected.path),
3513
+ snapshot_history_path: displayWorkspacePath(snapshotHistoryPath),
3514
+ snapshot_history_present: existsSync(snapshotHistoryPath),
3515
+ snapshot_history_count: graphSnapshotHistoryCount(projectRoot),
3516
+ current_snapshot_id: currentSnapshot.id,
3517
+ snapshot_id: snapshot.id,
3518
+ schema_version: snapshot.schema_version,
3519
+ project_id: snapshot.project_id,
3520
+ created_at: snapshot.created_at,
3521
+ source_hash: snapshot.source_hash,
3522
+ summary: snapshot.summary,
3523
+ history: args.include_history === true ? readGraphSnapshotHistory(projectRoot) : void 0,
3524
+ diff_summary: !snapshotId || snapshot.id === currentSnapshot.id ? graphDiff ? summarizeGraphDiff(graphDiff) : null : null,
3525
+ comparison,
3526
+ available_routes: graphAvailableRoutes(snapshot)
3527
+ };
3528
+ } catch (e) {
3529
+ return { error: `Could not read graph snapshot: ${e.message}` };
3530
+ }
3531
+ }
3532
+ case "decantr_query_graph": {
3533
+ try {
3534
+ const projectRoot = graphProjectRoot(args);
3535
+ const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
3536
+ if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
3537
+ return { error: 'Optional parameter "snapshot_id" must be a string.' };
3538
+ }
3539
+ const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
3540
+ const snapshotPath = selected.path;
3541
+ const snapshot = selected.snapshot;
3542
+ if (!snapshot) {
3543
+ return {
3544
+ 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.",
3545
+ expected_path: displayWorkspacePath(snapshotPath)
3546
+ };
3547
+ }
3548
+ const currentSnapshot = readMcpGraphSnapshot(projectRoot);
3549
+ const nodeIds = stringListArg(args, "node_ids");
3550
+ if (nodeIds.error) return { error: nodeIds.error };
3551
+ if (args.file_path !== void 0 && typeof args.file_path !== "string") {
3552
+ return { error: 'Optional parameter "file_path" must be a string.' };
3553
+ }
3554
+ const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
3555
+ const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
3556
+ if (filePath && !fileNodeId) {
3557
+ return {
3558
+ error: `Source file not found in graph snapshot: ${filePath}`,
3559
+ snapshot_id: snapshot.id,
3560
+ available_routes: graphAvailableRoutes(snapshot),
3561
+ available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
3562
+ };
3563
+ }
3564
+ const resolvedNodeIds = [
3565
+ .../* @__PURE__ */ new Set([...nodeIds.values ?? [], ...fileNodeId ? [fileNodeId] : []])
3566
+ ];
3567
+ const nodeType = graphNodeTypeArg(args, "node_type");
3568
+ if (nodeType.error) return { error: nodeType.error };
3569
+ const nodeTypes = graphNodeTypesArg(args, "node_types");
3570
+ if (nodeTypes.error) return { error: nodeTypes.error };
3571
+ const relation = graphRelationArg(args, "relation");
3572
+ if (relation.error) return { error: relation.error };
3573
+ const relations = graphRelationsArg(args, "relations");
3574
+ if (relations.error) return { error: relations.error };
3575
+ const payloadFilter = graphPayloadFilterArgs(args);
3576
+ if (payloadFilter.error) return { error: payloadFilter.error };
3577
+ if (args.task !== void 0 && typeof args.task !== "string") {
3578
+ return { error: 'Optional parameter "task" must be a string.' };
3579
+ }
3580
+ const edgeSrc = typeof args.edge_src === "string" ? args.edge_src.trim() : void 0;
3581
+ const edgeDst = typeof args.edge_dst === "string" ? args.edge_dst.trim() : void 0;
3582
+ if (args.edge_src !== void 0 && typeof args.edge_src !== "string") {
3583
+ return { error: 'Optional parameter "edge_src" must be a string.' };
3584
+ }
3585
+ if (args.edge_dst !== void 0 && typeof args.edge_dst !== "string") {
3586
+ return { error: 'Optional parameter "edge_dst" must be a string.' };
3587
+ }
3588
+ const hasNodeSelector = resolvedNodeIds.length > 0 || !!nodeType.value || !!nodeTypes.values?.length || !!payloadFilter.key || !!payloadFilter.contains;
3589
+ const hasEdgeSelector = !!edgeSrc || !!edgeDst || !!relation.value || !!relations.values?.length;
3590
+ if (!hasNodeSelector && !hasEdgeSelector) {
3591
+ return {
3592
+ 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."
3593
+ };
3594
+ }
3595
+ const store = createMemoryGraphStore({
3596
+ nodes: snapshot.nodes,
3597
+ edges: snapshot.edges,
3598
+ snapshots: [snapshot]
3599
+ });
3600
+ const limit = graphToolLimit(args);
3601
+ let nodes = [];
3602
+ let edges = [];
3603
+ if (hasNodeSelector) {
3604
+ nodes = await store.queryNodes({
3605
+ ids: resolvedNodeIds.length > 0 ? resolvedNodeIds : void 0,
3606
+ type: nodeType.value,
3607
+ types: nodeTypes.values,
3608
+ payloadKey: payloadFilter.key,
3609
+ payloadValue: payloadFilter.value,
3610
+ payloadContains: payloadFilter.contains
3611
+ });
3612
+ }
3613
+ if (hasEdgeSelector) {
3614
+ edges = await store.queryEdges({
3615
+ src: edgeSrc,
3616
+ dst: edgeDst,
3617
+ relation: relation.value,
3618
+ relations: relations.values
3619
+ });
3620
+ }
3621
+ const shouldIncludeEdges = args.include_edges === true || hasEdgeSelector;
3622
+ const nodeMap = new Map(nodes.map((node) => [node.id, node]));
3623
+ if (shouldIncludeEdges && hasNodeSelector) {
3624
+ const selectedIds = new Set(nodeMap.keys());
3625
+ edges = dedupeGraphEdges([
3626
+ ...edges,
3627
+ ...snapshot.edges.filter(
3628
+ (edge) => selectedIds.has(edge.src) || selectedIds.has(edge.dst)
3629
+ )
3630
+ ]);
3631
+ }
3632
+ for (const edge of edges) {
3633
+ const srcNode = snapshot.nodes.find((node) => node.id === edge.src);
3634
+ const dstNode = snapshot.nodes.find((node) => node.id === edge.dst);
3635
+ if (srcNode) nodeMap.set(srcNode.id, srcNode);
3636
+ if (dstNode) nodeMap.set(dstNode.id, dstNode);
3637
+ }
3638
+ nodes = dedupeGraphNodes([...nodeMap.values()]);
3639
+ edges = dedupeGraphEdges(edges);
3640
+ const limited = limitGraphSubgraph(nodes, edges, limit);
3641
+ const impact = args.include_impact === true && limited.nodes.length > 0 ? buildGraphImpactContext(
3642
+ snapshot,
3643
+ limited.nodes.map((node) => node.id),
3644
+ {
3645
+ task: typeof args.task === "string" ? args.task : void 0,
3646
+ limit
3647
+ }
3648
+ ) : null;
3649
+ return {
3650
+ source: "local_graph",
3651
+ artifact_path: displayWorkspacePath(snapshotPath),
3652
+ current_snapshot_id: currentSnapshot?.id ?? null,
3653
+ snapshot_id: snapshot.id,
3654
+ schema_version: snapshot.schema_version,
3655
+ project_id: snapshot.project_id,
3656
+ source_hash: snapshot.source_hash,
3657
+ query: {
3658
+ node_ids: resolvedNodeIds.length > 0 ? resolvedNodeIds : nodeIds.values,
3659
+ file_path: filePath || void 0,
3660
+ node_type: nodeType.value,
3661
+ node_types: nodeTypes.values,
3662
+ payload_key: payloadFilter.key,
3663
+ payload_value: payloadFilter.value,
3664
+ payload_contains: payloadFilter.contains,
3665
+ edge_src: edgeSrc,
3666
+ edge_dst: edgeDst,
3667
+ relation: relation.value,
3668
+ relations: relations.values,
3669
+ include_edges: shouldIncludeEdges,
3670
+ include_impact: args.include_impact === true,
3671
+ task: typeof args.task === "string" ? args.task : void 0,
3672
+ limit
3673
+ },
3674
+ summary: {
3675
+ nodes: limited.nodes.length,
3676
+ edges: limited.edges.length,
3677
+ total_nodes: nodes.length,
3678
+ total_edges: edges.length,
3679
+ truncated: limited.truncated
3680
+ },
3681
+ nodes: limited.nodes,
3682
+ edges: limited.edges,
3683
+ impact
3684
+ };
3685
+ } catch (e) {
3686
+ return { error: `Could not query graph snapshot: ${e.message}` };
3687
+ }
3688
+ }
3689
+ case "decantr_traverse_graph": {
3690
+ try {
3691
+ const projectRoot = graphProjectRoot(args);
3692
+ const snapshotId = typeof args.snapshot_id === "string" ? args.snapshot_id.trim() : "";
3693
+ if (args.snapshot_id !== void 0 && typeof args.snapshot_id !== "string") {
3694
+ return { error: 'Optional parameter "snapshot_id" must be a string.' };
3695
+ }
3696
+ const selected = readGraphSnapshotById(projectRoot, snapshotId || void 0);
3697
+ const snapshotPath = selected.path;
3698
+ const snapshot = selected.snapshot;
3699
+ if (!snapshot) {
3700
+ return {
3701
+ 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.",
3702
+ expected_path: displayWorkspacePath(snapshotPath)
3703
+ };
3704
+ }
3705
+ const currentSnapshot = readMcpGraphSnapshot(projectRoot);
3706
+ const fromIds = stringListArg(args, "from_ids");
3707
+ if (fromIds.error) return { error: fromIds.error };
3708
+ const from = typeof args.from === "string" && args.from.trim() ? [args.from.trim()] : [];
3709
+ if (args.from !== void 0 && typeof args.from !== "string") {
3710
+ return { error: 'Optional parameter "from" must be a string.' };
3711
+ }
3712
+ if (args.file_path !== void 0 && typeof args.file_path !== "string") {
3713
+ return { error: 'Optional parameter "file_path" must be a string.' };
3714
+ }
3715
+ const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
3716
+ const fileNodeId = graphSourceNodeIdForFile(projectRoot, snapshot, filePath || void 0);
3717
+ if (filePath && !fileNodeId) {
3718
+ return {
3719
+ error: `Source file not found in graph snapshot: ${filePath}`,
3720
+ snapshot_id: snapshot.id,
3721
+ available_routes: graphAvailableRoutes(snapshot),
3722
+ available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
3723
+ };
3724
+ }
3725
+ const startIds = [
3726
+ .../* @__PURE__ */ new Set([...from, ...fromIds.values ?? [], ...fileNodeId ? [fileNodeId] : []])
3727
+ ];
3728
+ if (!startIds.length) {
3729
+ return { error: 'Provide a graph start node with "from", "from_ids", or "file_path".' };
3730
+ }
3731
+ const missingStartIds = startIds.filter(
3732
+ (id) => !snapshot.nodes.some((node) => node.id === id)
3733
+ );
3734
+ if (missingStartIds.length) {
3735
+ return {
3736
+ error: `Start node not found in graph snapshot: ${missingStartIds.join(", ")}`,
3737
+ snapshot_id: snapshot.id,
3738
+ available_routes: graphAvailableRoutes(snapshot),
3739
+ available_source_artifacts: graphAvailableSourceArtifacts(snapshot)
3740
+ };
3741
+ }
3742
+ const relations = graphRelationsArg(args, "relations");
3743
+ if (relations.error) return { error: relations.error };
3744
+ const direction = graphTraverseDirectionArg(args);
3745
+ if (direction.error) return { error: direction.error };
3746
+ const depth = graphTraverseDepthArg(args);
3747
+ if (depth.error) return { error: depth.error };
3748
+ const store = createMemoryGraphStore({
3749
+ nodes: snapshot.nodes,
3750
+ edges: snapshot.edges,
3751
+ snapshots: [snapshot]
3752
+ });
3753
+ const result = await store.traverse({
3754
+ from: startIds,
3755
+ relations: relations.values,
3756
+ direction: direction.value,
3757
+ depth: depth.value
3758
+ });
3759
+ const limit = graphToolLimit(args);
3760
+ const limited = limitGraphSubgraph(result.nodes, result.edges, limit);
3761
+ return {
3762
+ source: "local_graph",
3763
+ artifact_path: displayWorkspacePath(snapshotPath),
3764
+ current_snapshot_id: currentSnapshot?.id ?? null,
3765
+ snapshot_id: snapshot.id,
3766
+ schema_version: snapshot.schema_version,
3767
+ project_id: snapshot.project_id,
3768
+ source_hash: snapshot.source_hash,
3769
+ traversal: {
3770
+ from: startIds,
3771
+ file_path: filePath || void 0,
3772
+ resolved_node_ids: startIds,
3773
+ relations: relations.values,
3774
+ direction: direction.value ?? "out",
3775
+ depth: depth.value,
3776
+ limit
3777
+ },
3778
+ summary: {
3779
+ nodes: limited.nodes.length,
3780
+ edges: limited.edges.length,
3781
+ total_nodes: result.nodes.length,
3782
+ total_edges: result.edges.length,
3783
+ truncated: limited.truncated
3784
+ },
3785
+ nodes: limited.nodes,
3786
+ edges: limited.edges
3787
+ };
3788
+ } catch (e) {
3789
+ return { error: `Could not traverse graph snapshot: ${e.message}` };
3790
+ }
3791
+ }
2114
3792
  case "decantr_get_scaffold_context": {
2115
3793
  const contextDir = join2(process.cwd(), ".decantr", "context");
2116
3794
  const manifestPath = join2(contextDir, "pack-manifest.json");
@@ -2429,6 +4107,8 @@ async function handleTool(name, args) {
2429
4107
  };
2430
4108
  }
2431
4109
  case "decantr_prepare_task_context": {
4110
+ const projectRoot = graphProjectRoot(args);
4111
+ const projectArg = typeof args.project_path === "string" && args.project_path.trim() ? args.project_path.trim() : null;
2432
4112
  const routeArg = typeof args.route === "string" ? args.route : void 0;
2433
4113
  const pageArg = typeof args.page_id === "string" ? args.page_id : void 0;
2434
4114
  const task = typeof args.task === "string" ? args.task : "";
@@ -2437,7 +4117,7 @@ async function handleTool(name, args) {
2437
4117
  }
2438
4118
  let essence;
2439
4119
  try {
2440
- const result = await readEssenceFile();
4120
+ const result = await readEssenceFile(join2(projectRoot, "decantr.essence.json"));
2441
4121
  essence = result.essence;
2442
4122
  } catch {
2443
4123
  return { error: "No valid essence file found. Run decantr init first." };
@@ -2463,7 +4143,10 @@ async function handleTool(name, args) {
2463
4143
  )
2464
4144
  };
2465
4145
  }
2466
- const contextDir = join2(process.cwd(), ".decantr", "context");
4146
+ const resolvedRoute = routeArg ?? (typeof page.route === "string" ? page.route : Object.entries(essence.blueprint.routes ?? {}).find(
4147
+ ([, entry]) => entry.section === section.id && entry.page === pageId
4148
+ )?.[0]) ?? null;
4149
+ const contextDir = join2(projectRoot, ".decantr", "context");
2467
4150
  const manifest = readJsonIfExists(join2(contextDir, "pack-manifest.json"));
2468
4151
  const pageManifest = manifest?.pages.find((entry) => entry.id === pageId) ?? null;
2469
4152
  const sectionManifest = manifest?.sections.find((entry) => entry.id === section.id) ?? null;
@@ -2474,18 +4157,28 @@ async function handleTool(name, args) {
2474
4157
  const sectionContext = existsSync(sectionContextPath) ? readFileSync(sectionContextPath, "utf-8") : null;
2475
4158
  const pagePackSummary = summarizePackJson(pagePackJson);
2476
4159
  const sectionPackSummary = summarizePackJson(sectionPackJson);
2477
- const visualManifest = readJsonIfExists(join2(process.cwd(), ".decantr", "evidence", "visual-manifest.json"));
2478
- const visualRoute = visualManifest?.routes?.find((entry) => entry.route === routeArg) ?? visualManifest?.routes?.find(
2479
- (entry) => entry.screenshot?.includes(routeSlug(routeArg ?? pageId))
4160
+ const visualManifest = readJsonIfExists(join2(projectRoot, ".decantr", "evidence", "visual-manifest.json"));
4161
+ const visualRoute = visualManifest?.routes?.find((entry) => entry.route === resolvedRoute) ?? visualManifest?.routes?.find(
4162
+ (entry) => entry.screenshot?.includes(routeSlug(resolvedRoute ?? pageId))
2480
4163
  ) ?? null;
2481
- const health = readJsonIfExists(join2(process.cwd(), ".decantr", "health-baseline-diff.json"));
4164
+ const health = readJsonIfExists(join2(projectRoot, ".decantr", "health-baseline-diff.json"));
2482
4165
  const themeInventory = readJsonIfExists(
2483
- join2(process.cwd(), ".decantr", "theme-inventory.json")
4166
+ join2(projectRoot, ".decantr", "theme-inventory.json")
2484
4167
  );
2485
- const localLaw = localLawSummary(process.cwd());
2486
- const projectJson = readJsonIfExists(join2(process.cwd(), ".decantr", "project.json"));
2487
- const changedFiles = changedFilesForTask(process.cwd());
2488
- const changedRoutes = impactedRoutesForFiles(process.cwd(), changedFiles);
4168
+ const localLaw = localLawSummary(projectRoot);
4169
+ const styleBridge = styleBridgeSummary(projectRoot);
4170
+ const displayedLocalLaw = {
4171
+ ...localLaw,
4172
+ patterns_path: displayProjectFile(projectRoot, localLaw.patterns_path),
4173
+ rules_path: displayProjectFile(projectRoot, localLaw.rules_path)
4174
+ };
4175
+ const displayedStyleBridge = {
4176
+ ...styleBridge,
4177
+ path: displayProjectFile(projectRoot, styleBridge.path)
4178
+ };
4179
+ const projectJson = readJsonIfExists(join2(projectRoot, ".decantr", "project.json"));
4180
+ const changedFiles = changedFilesForTask(projectRoot);
4181
+ const changedRoutes = impactedRoutesForFiles(projectRoot, changedFiles);
2489
4182
  const patternIds = extractPagePatternIds(page);
2490
4183
  const ranked = rankPatternCandidates(
2491
4184
  {
@@ -2495,7 +4188,7 @@ async function handleTool(name, args) {
2495
4188
  patternIds.map((id) => patternToDiscoveryCandidate({ id, name: id, description: id }))
2496
4189
  );
2497
4190
  return {
2498
- route: routeArg ?? null,
4191
+ route: resolvedRoute,
2499
4192
  page_id: pageId,
2500
4193
  section_id: section.id,
2501
4194
  section_role: section.role,
@@ -2513,18 +4206,18 @@ async function handleTool(name, args) {
2513
4206
  section_context: sectionContext,
2514
4207
  page_pack_excerpt: pagePackMarkdown ? pagePackMarkdown.slice(0, 12e3) : null,
2515
4208
  health_evidence: health ? {
2516
- baseline_path: health.baselinePath,
4209
+ baseline_path: displayProjectFile(projectRoot, health.baselinePath),
2517
4210
  saved_at: health.savedAt,
2518
4211
  status_changed: health.statusChanged,
2519
4212
  score_delta: health.scoreDelta,
2520
4213
  added_findings: health.addedFindings?.slice(0, 8) ?? [],
2521
4214
  resolved_findings: health.resolvedFindings?.slice(0, 8) ?? [],
2522
4215
  changed_routes: health.changedRoutes ?? [],
2523
- changed_screenshots: health.changedScreenshots ?? [],
4216
+ changed_screenshots: (health.changedScreenshots ?? []).map((path) => displayProjectFile(projectRoot, path)).filter((path) => Boolean(path)),
2524
4217
  contract_drift: health.contractDrift ?? []
2525
4218
  } : null,
2526
4219
  visual_evidence: visualRoute ? {
2527
- screenshot: visualRoute.screenshot ?? null,
4220
+ screenshot: displayProjectFile(projectRoot, visualRoute.screenshot),
2528
4221
  screenshot_hash: visualRoute.screenshotHash ?? null,
2529
4222
  status: visualRoute.status ?? null,
2530
4223
  error: visualRoute.error ?? null
@@ -2532,13 +4225,15 @@ async function handleTool(name, args) {
2532
4225
  theme_inventory: themeInventory ? {
2533
4226
  modes: themeInventory.modes,
2534
4227
  variants: themeInventory.variants,
2535
- path: ".decantr/theme-inventory.json"
4228
+ path: displayProjectFile(projectRoot, ".decantr/theme-inventory.json")
2536
4229
  } : null,
2537
- local_law: localLaw,
4230
+ local_law: displayedLocalLaw,
4231
+ style_bridge: displayedStyleBridge,
2538
4232
  authority: taskAuthoritySummary({
2539
4233
  workflowMode: projectJson?.initialized?.workflowMode ?? null,
2540
4234
  adoptionMode: projectJson?.initialized?.adoptionMode ?? null,
2541
4235
  localLaw,
4236
+ styleBridge,
2542
4237
  hasPackManifest: Boolean(manifest),
2543
4238
  task
2544
4239
  }),
@@ -2547,16 +4242,25 @@ async function handleTool(name, args) {
2547
4242
  changed_file_count: changedFiles.length,
2548
4243
  impacted_routes: changedRoutes
2549
4244
  },
2550
- verify_command: "decantr verify --brownfield --local-patterns",
4245
+ typed_graph: buildTaskTypedGraphContext(projectRoot, resolvedRoute, task, changedFiles),
4246
+ verify_command: projectArg ? `decantr verify --project ${projectArg} --brownfield --local-patterns` : "decantr verify --brownfield --local-patterns",
2551
4247
  local_files: {
2552
- page_pack: pageManifest?.markdown ?? null,
2553
- section_pack: sectionManifest?.markdown ?? null,
2554
- section_context: existsSync(sectionContextPath) ? `.decantr/context/section-${section.id}.md` : null,
2555
- local_patterns: localLaw.patterns_path,
2556
- local_rules: localLaw.rules_path,
4248
+ page_pack: displayProjectFile(
4249
+ projectRoot,
4250
+ pageManifest ? join2(".decantr", "context", pageManifest.markdown) : null
4251
+ ),
4252
+ section_pack: displayProjectFile(
4253
+ projectRoot,
4254
+ sectionManifest ? join2(".decantr", "context", sectionManifest.markdown) : null
4255
+ ),
4256
+ graph_snapshot: existsSync(join2(projectRoot, ".decantr", "graph", "graph.snapshot.json")) ? displayProjectFile(projectRoot, ".decantr/graph/graph.snapshot.json") : null,
4257
+ section_context: existsSync(sectionContextPath) ? displayProjectFile(projectRoot, `.decantr/context/section-${section.id}.md`) : null,
4258
+ local_patterns: displayedLocalLaw.patterns_path,
4259
+ local_rules: displayedLocalLaw.rules_path,
4260
+ style_bridge: displayedStyleBridge.path,
2557
4261
  visual_manifest: existsSync(
2558
- join2(process.cwd(), ".decantr", "evidence", "visual-manifest.json")
2559
- ) ? ".decantr/evidence/visual-manifest.json" : null
4262
+ join2(projectRoot, ".decantr", "evidence", "visual-manifest.json")
4263
+ ) ? displayProjectFile(projectRoot, ".decantr/evidence/visual-manifest.json") : null
2560
4264
  }
2561
4265
  };
2562
4266
  }
@@ -2807,6 +4511,101 @@ async function handleTool(name, args) {
2807
4511
  }
2808
4512
  return auditProject(projectRoot);
2809
4513
  }
4514
+ case "decantr_get_findings": {
4515
+ if (args.severity != null && args.severity !== "error" && args.severity !== "warn" && args.severity !== "info") {
4516
+ return { error: "Invalid severity. Must be one of: error, warn, info." };
4517
+ }
4518
+ const findingSources = [
4519
+ "audit",
4520
+ "assertion",
4521
+ "browser",
4522
+ "check",
4523
+ "brownfield",
4524
+ "design-token",
4525
+ "style-bridge",
4526
+ "graph",
4527
+ "runtime",
4528
+ "pack",
4529
+ "interaction"
4530
+ ];
4531
+ if (args.source != null && (typeof args.source !== "string" || !findingSources.includes(args.source))) {
4532
+ return { error: `Invalid source. Must be one of: ${findingSources.join(", ")}.` };
4533
+ }
4534
+ if (args.code != null && typeof args.code !== "string") {
4535
+ return { error: "Invalid code. Must be a string when provided." };
4536
+ }
4537
+ if (args.include_prompts != null && typeof args.include_prompts !== "boolean") {
4538
+ return { error: "Invalid include_prompts. Must be a boolean when provided." };
4539
+ }
4540
+ if (args.limit != null && (typeof args.limit !== "number" || !Number.isFinite(args.limit))) {
4541
+ return { error: "Invalid limit. Must be a finite number when provided." };
4542
+ }
4543
+ try {
4544
+ const projectRoot = resolveMcpProjectRoot(args.project_path);
4545
+ const state = await getMcpHealthState(projectRoot);
4546
+ const severity = args.severity;
4547
+ const source = args.source;
4548
+ const code = typeof args.code === "string" ? args.code : void 0;
4549
+ const includePrompts = args.include_prompts === true;
4550
+ const limit = typeof args.limit === "number" ? Math.max(1, Math.min(200, Math.floor(args.limit))) : 50;
4551
+ const filtered = state.report.findings.filter((finding) => {
4552
+ if (severity && finding.severity !== severity) return false;
4553
+ if (source && finding.source !== source) return false;
4554
+ if (code && finding.code !== code) return false;
4555
+ return true;
4556
+ });
4557
+ const findings = filtered.slice(0, limit).map((finding) => compactMcpFinding(finding, includePrompts));
4558
+ return {
4559
+ project: state.evidence.project,
4560
+ health: state.evidence.health,
4561
+ filters: {
4562
+ severity,
4563
+ source,
4564
+ code,
4565
+ include_prompts: includePrompts,
4566
+ limit
4567
+ },
4568
+ summary: {
4569
+ status: state.report.status,
4570
+ score: state.report.score,
4571
+ total_findings: state.report.findings.length,
4572
+ matched_findings: filtered.length,
4573
+ returned_findings: findings.length,
4574
+ truncated: filtered.length > findings.length
4575
+ },
4576
+ findings
4577
+ };
4578
+ } catch (error) {
4579
+ return { error: error.message };
4580
+ }
4581
+ }
4582
+ case "decantr_get_repair_plan": {
4583
+ if (args.finding_id != null && typeof args.finding_id !== "string") {
4584
+ return { error: "Invalid finding_id. Must be a string when provided." };
4585
+ }
4586
+ if (args.code != null && typeof args.code !== "string") {
4587
+ return { error: "Invalid code. Must be a string when provided." };
4588
+ }
4589
+ if (args.include_prompt != null && typeof args.include_prompt !== "boolean") {
4590
+ return { error: "Invalid include_prompt. Must be a boolean when provided." };
4591
+ }
4592
+ try {
4593
+ const projectRoot = resolveMcpProjectRoot(args.project_path);
4594
+ const state = await getMcpHealthState(projectRoot);
4595
+ const finding = selectMcpRepairFinding(state.report, {
4596
+ findingId: typeof args.finding_id === "string" ? args.finding_id : void 0,
4597
+ code: typeof args.code === "string" ? args.code : void 0
4598
+ });
4599
+ return buildMcpRepairPlan({
4600
+ evidence: state.evidence,
4601
+ finding,
4602
+ projectRoot,
4603
+ includePrompt: args.include_prompt === true
4604
+ });
4605
+ } catch (error) {
4606
+ return { error: error.message };
4607
+ }
4608
+ }
2810
4609
  case "decantr_get_evidence_bundle": {
2811
4610
  try {
2812
4611
  const projectRoot = resolveMcpProjectRoot(args.project_path);
@@ -2836,7 +4635,9 @@ async function handleTool(name, args) {
2836
4635
  try {
2837
4636
  const projectRoot = resolveMcpProjectRoot(args.project_path);
2838
4637
  const state = await getMcpHealthState(projectRoot);
2839
- const finding = (typeof args.finding_id === "string" ? state.report.findings.find((entry) => entry.id === args.finding_id) : void 0) ?? state.report.findings.find((entry) => entry.severity === "error") ?? state.report.findings.find((entry) => entry.severity === "warn") ?? state.report.findings[0] ?? null;
4638
+ const finding = selectMcpRepairFinding(state.report, {
4639
+ findingId: typeof args.finding_id === "string" ? args.finding_id : void 0
4640
+ });
2840
4641
  if (!finding) {
2841
4642
  return {
2842
4643
  project: state.evidence.project,
@@ -2870,12 +4671,19 @@ async function handleTool(name, args) {
2870
4671
  try {
2871
4672
  const projectRoot = resolveMcpProjectRoot(args.project_path);
2872
4673
  const state = await getMcpHealthState(projectRoot);
2873
- const finding = (typeof args.finding_id === "string" ? state.report.findings.find((entry) => entry.id === args.finding_id) : void 0) ?? state.report.findings.find((entry) => entry.severity === "error") ?? state.report.findings.find((entry) => entry.severity === "warn") ?? state.report.findings[0] ?? null;
4674
+ const finding = selectMcpRepairFinding(state.report, {
4675
+ findingId: typeof args.finding_id === "string" ? args.finding_id : void 0
4676
+ });
2874
4677
  return {
2875
4678
  project: state.evidence.project,
2876
4679
  health: state.evidence.health,
2877
4680
  report: state.report,
2878
4681
  evidence: state.evidence,
4682
+ repair_plan: buildMcpRepairPlan({
4683
+ evidence: state.evidence,
4684
+ finding,
4685
+ projectRoot
4686
+ }),
2879
4687
  repair: finding === null ? {
2880
4688
  finding: null,
2881
4689
  prompt: null,
@@ -3056,7 +4864,7 @@ function describeUpdate(operation, payload) {
3056
4864
  }
3057
4865
 
3058
4866
  // src/index.ts
3059
- var VERSION = "2.2.0";
4867
+ var VERSION = "3.0.0-next.0";
3060
4868
  var server = new Server({ name: "decantr", version: VERSION }, { capabilities: { tools: {} } });
3061
4869
  server.setRequestHandler(ListToolsRequestSchema, async () => {
3062
4870
  return { tools: TOOLS };