ctxo-mcp 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,19 +2,22 @@
2
2
  import {
3
3
  RevertDetector,
4
4
  SimpleGitAdapter,
5
- SqliteStorageAdapter
6
- } from "./chunk-P7JUSY3I.js";
5
+ SqliteStorageAdapter,
6
+ createLogger,
7
+ loadCoChangeMap
8
+ } from "./chunk-XSHNN6PU.js";
7
9
  import {
8
10
  DetailLevelSchema,
9
11
  JsonIndexReader
10
- } from "./chunk-54ETLIQX.js";
12
+ } from "./chunk-N6GPODUY.js";
13
+ import "./chunk-JIDIH7DS.js";
11
14
 
12
15
  // src/index.ts
13
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { z as z6 } from "zod";
16
- import { existsSync, readFileSync } from "fs";
17
- import { join } from "path";
18
+ import { z as z15 } from "zod";
19
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20
+ import { join as join2 } from "path";
18
21
 
19
22
  // src/core/masking/masking-pipeline.ts
20
23
  var DEFAULT_PATTERNS = [
@@ -76,9 +79,9 @@ var SymbolGraph = class {
76
79
  edgeSet = /* @__PURE__ */ new Set();
77
80
  addNode(node) {
78
81
  this.nodes.set(node.symbolId, node);
79
- const parts = node.symbolId.split("::");
80
- if (parts.length >= 2) {
81
- this.nodesByFileAndName.set(`${parts[0]}::${parts[1]}`, node);
82
+ const [p0, p1] = node.symbolId.split("::");
83
+ if (p0 !== void 0 && p1 !== void 0) {
84
+ this.nodesByFileAndName.set(`${p0}::${p1}`, node);
82
85
  }
83
86
  }
84
87
  addEdge(edge) {
@@ -100,18 +103,29 @@ var SymbolGraph = class {
100
103
  }
101
104
  resolveNodeId(id) {
102
105
  if (this.nodes.has(id)) return id;
103
- const parts = id.split("::");
104
- if (parts.length >= 2) {
105
- const fuzzyKey = `${parts[0]}::${parts[1]}`;
106
+ const [p0, p1] = id.split("::");
107
+ if (p0 !== void 0 && p1 !== void 0) {
108
+ const fuzzyKey = `${p0}::${p1}`;
106
109
  const match = this.nodesByFileAndName.get(fuzzyKey);
107
110
  if (match) return match.symbolId;
111
+ const jsToTs = p0.replace(/\.js$/, ".ts");
112
+ if (jsToTs !== p0) {
113
+ const altKey = `${jsToTs}::${p1}`;
114
+ const altMatch = this.nodesByFileAndName.get(altKey);
115
+ if (altMatch) return altMatch.symbolId;
116
+ }
108
117
  }
109
118
  return id;
110
119
  }
111
120
  resolveNodeFuzzy(id) {
112
- const parts = id.split("::");
113
- if (parts.length >= 2) {
114
- return this.nodesByFileAndName.get(`${parts[0]}::${parts[1]}`);
121
+ const [p0, p1] = id.split("::");
122
+ if (p0 !== void 0 && p1 !== void 0) {
123
+ const match = this.nodesByFileAndName.get(`${p0}::${p1}`);
124
+ if (match) return match;
125
+ const jsToTs = p0.replace(/\.js$/, ".ts");
126
+ if (jsToTs !== p0) {
127
+ return this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
128
+ }
115
129
  }
116
130
  return void 0;
117
131
  }
@@ -122,7 +136,7 @@ var SymbolGraph = class {
122
136
  return this.reverseEdges.get(symbolId) ?? [];
123
137
  }
124
138
  hasNode(symbolId) {
125
- return this.nodes.has(symbolId);
139
+ return this.nodes.has(symbolId) || this.resolveNodeFuzzy(symbolId) !== void 0;
126
140
  }
127
141
  get nodeCount() {
128
142
  return this.nodes.size;
@@ -252,6 +266,9 @@ var DetailFormatter = class {
252
266
  return Math.ceil(chars / 4);
253
267
  }
254
268
  symbolCharEstimate(node) {
269
+ if (node.startOffset !== void 0 && node.endOffset !== void 0) {
270
+ return node.endOffset - node.startOffset;
271
+ }
255
272
  return (node.endLine - node.startLine + 1) * 40;
256
273
  }
257
274
  truncateToTokenBudget(slice, budget) {
@@ -272,11 +289,142 @@ var DetailFormatter = class {
272
289
  }
273
290
  };
274
291
 
292
+ // src/core/response-envelope.ts
293
+ var DEFAULT_THRESHOLD = 8192;
294
+ var TRUNCATABLE_FIELDS = {
295
+ impactedSymbols: "Use search_symbols to find specific impacted symbols, or get_blast_radius with a confidence filter.",
296
+ importers: "Use find_importers with edgeKinds filter or maxDepth to narrow results.",
297
+ deadSymbols: "Use search_symbols with kind filter to find specific dead symbols.",
298
+ unusedExports: "Use find_importers to check specific symbols.",
299
+ results: "Use search_symbols or get_ranked_context with a narrower query.",
300
+ rankings: "Use get_symbol_importance with kind or filePattern filter to narrow results.",
301
+ context: "Use get_context_for_task with a smaller tokenBudget.",
302
+ hierarchies: "Use get_class_hierarchy with a specific symbolId.",
303
+ scaffolding: "Review the scaffolding markers directly in the listed files.",
304
+ deadFiles: "Use find_dead_code with includeTests to adjust scope.",
305
+ files: "Use get_changed_symbols with a narrower since ref or smaller maxFiles."
306
+ };
307
+ function getThreshold() {
308
+ const env = process.env["CTXO_RESPONSE_LIMIT"];
309
+ if (env) {
310
+ const n = parseInt(env, 10);
311
+ if (!isNaN(n) && n > 0) return n;
312
+ }
313
+ return DEFAULT_THRESHOLD;
314
+ }
315
+ function findTruncatableArray(data) {
316
+ let best = null;
317
+ for (const [key, value] of Object.entries(data)) {
318
+ if (Array.isArray(value) && key in TRUNCATABLE_FIELDS) {
319
+ if (!best || value.length > best.arr.length) {
320
+ best = { key, arr: value };
321
+ }
322
+ }
323
+ }
324
+ return best;
325
+ }
326
+ function wrapResponse(data) {
327
+ const threshold = getThreshold();
328
+ const fullJson = JSON.stringify(data);
329
+ const totalBytes = Buffer.byteLength(fullJson, "utf-8");
330
+ const truncatable = findTruncatableArray(data);
331
+ const totalItems = truncatable ? truncatable.arr.length : 0;
332
+ if (totalBytes <= threshold) {
333
+ return {
334
+ ...data,
335
+ _meta: {
336
+ totalItems,
337
+ returnedItems: totalItems,
338
+ truncated: false,
339
+ totalBytes
340
+ }
341
+ };
342
+ }
343
+ if (!truncatable || truncatable.arr.length <= 1) {
344
+ return {
345
+ ...data,
346
+ _meta: {
347
+ totalItems,
348
+ returnedItems: totalItems,
349
+ truncated: false,
350
+ totalBytes
351
+ }
352
+ };
353
+ }
354
+ let lo = 1;
355
+ let hi = truncatable.arr.length;
356
+ while (lo < hi) {
357
+ const mid = Math.ceil((lo + hi) / 2);
358
+ const trial = {
359
+ ...data,
360
+ [truncatable.key]: truncatable.arr.slice(0, mid),
361
+ _meta: {
362
+ totalItems,
363
+ returnedItems: mid,
364
+ truncated: true,
365
+ totalBytes,
366
+ hint: TRUNCATABLE_FIELDS[truncatable.key]
367
+ }
368
+ };
369
+ const size = Buffer.byteLength(JSON.stringify(trial), "utf-8");
370
+ if (size <= threshold) {
371
+ lo = mid;
372
+ } else {
373
+ hi = mid - 1;
374
+ }
375
+ }
376
+ const hint = TRUNCATABLE_FIELDS[truncatable.key];
377
+ return {
378
+ ...data,
379
+ [truncatable.key]: truncatable.arr.slice(0, lo),
380
+ _meta: {
381
+ totalItems,
382
+ returnedItems: lo,
383
+ truncated: true,
384
+ totalBytes,
385
+ hint
386
+ }
387
+ };
388
+ }
389
+
390
+ // src/core/intent-filter.ts
391
+ function extractKeywords(intent) {
392
+ return intent.toLowerCase().split(/\s+/).filter((w) => w.length >= 2);
393
+ }
394
+ function matchesAny(keywords, values) {
395
+ const lower = values.map((v) => v.toLowerCase());
396
+ return keywords.some((kw) => lower.some((v) => v.includes(kw)));
397
+ }
398
+ function collectMatchableStrings(entry) {
399
+ const strings = [];
400
+ for (const key of ["symbolId", "file", "name", "kind", "edgeKind", "reason", "confidence"]) {
401
+ const v = entry[key];
402
+ if (typeof v === "string") strings.push(v);
403
+ }
404
+ const edgeKinds = entry["edgeKinds"];
405
+ if (Array.isArray(edgeKinds)) {
406
+ for (const ek of edgeKinds) {
407
+ if (typeof ek === "string") strings.push(ek);
408
+ }
409
+ }
410
+ return strings;
411
+ }
412
+ function filterByIntent(items, intent) {
413
+ if (!intent || intent.trim().length === 0) return items;
414
+ const keywords = extractKeywords(intent);
415
+ if (keywords.length === 0) return items;
416
+ return items.filter((item) => {
417
+ const strings = collectMatchableStrings(item);
418
+ return matchesAny(keywords, strings);
419
+ });
420
+ }
421
+
275
422
  // src/adapters/mcp/get-logic-slice.ts
276
423
  var InputSchema = z.object({
277
424
  symbolId: z.string().min(1).optional(),
278
425
  symbolIds: z.array(z.string().min(1)).optional(),
279
- level: DetailLevelSchema.optional().default(3)
426
+ level: DetailLevelSchema.optional().default(3),
427
+ intent: z.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter", "storage")')
280
428
  }).refine(
281
429
  (data) => data.symbolId || data.symbolIds && data.symbolIds.length > 0,
282
430
  { message: "Either symbolId or symbolIds must be provided" }
@@ -323,20 +471,27 @@ function handleGetLogicSlice(storage, masking, staleness, ctxoRoot = ".ctxo") {
323
471
  content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
324
472
  };
325
473
  }
326
- const { symbolId, symbolIds, level } = parsed.data;
474
+ const { symbolId, symbolIds, level, intent } = parsed.data;
327
475
  const ids = symbolIds ?? (symbolId ? [symbolId] : []);
328
476
  const graph = getGraph();
329
477
  const results = [];
330
478
  for (const id of ids) {
331
479
  const slice = query.getLogicSlice(graph, id);
332
480
  if (slice) {
333
- results.push(formatter.format(slice, level));
481
+ const formatted = formatter.format(slice, level);
482
+ if (intent && Array.isArray(formatted["dependencies"])) {
483
+ formatted["dependencies"] = filterByIntent(
484
+ formatted["dependencies"],
485
+ intent
486
+ );
487
+ }
488
+ results.push(formatted);
334
489
  } else {
335
490
  results.push({ found: false, symbolId: id, hint: 'Symbol not found. Run "ctxo index".' });
336
491
  }
337
492
  }
338
493
  const responseData = ids.length === 1 ? results[0] : { batch: true, results };
339
- const payload = masking.mask(JSON.stringify(responseData));
494
+ const payload = masking.mask(JSON.stringify(wrapResponse(responseData)));
340
495
  const content = [];
341
496
  if (staleness) {
342
497
  const warning = staleness.check(storage.listIndexedFiles());
@@ -358,7 +513,8 @@ function handleGetLogicSlice(storage, masking, staleness, ctxoRoot = ".ctxo") {
358
513
  // src/adapters/mcp/get-why-context.ts
359
514
  import { z as z2 } from "zod";
360
515
  var InputSchema2 = z2.object({
361
- symbolId: z2.string().min(1)
516
+ symbolId: z2.string().min(1),
517
+ maxCommits: z2.number().int().min(1).optional()
362
518
  });
363
519
  function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
364
520
  const revertDetector = new RevertDetector();
@@ -371,7 +527,7 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
371
527
  content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
372
528
  };
373
529
  }
374
- const { symbolId } = parsed.data;
530
+ const { symbolId, maxCommits } = parsed.data;
375
531
  const symbol = storage.getSymbolById(symbolId);
376
532
  if (!symbol) {
377
533
  return {
@@ -396,8 +552,9 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
396
552
  }));
397
553
  antiPatterns = revertDetector.detect(commits);
398
554
  }
555
+ const limitedHistory = maxCommits ? commitHistory.slice(0, maxCommits) : commitHistory;
399
556
  const responsePayload = {
400
- commitHistory,
557
+ commitHistory: limitedHistory,
401
558
  antiPatternWarnings: antiPatterns
402
559
  };
403
560
  if (antiPatterns.length > 0) {
@@ -511,44 +668,97 @@ function handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot
511
668
  import { z as z4 } from "zod";
512
669
 
513
670
  // src/core/blast-radius/blast-radius-calculator.ts
671
+ var CONFIRMED_KINDS = /* @__PURE__ */ new Set(["calls", "extends", "implements"]);
672
+ var LIKELY_KINDS = /* @__PURE__ */ new Set(["uses"]);
673
+ var CONFIDENCE_RANK = {
674
+ confirmed: 2,
675
+ likely: 1,
676
+ potential: 0
677
+ };
678
+ function edgeKindToConfidence(kind) {
679
+ if (CONFIRMED_KINDS.has(kind)) return "confirmed";
680
+ if (LIKELY_KINDS.has(kind)) return "likely";
681
+ return "potential";
682
+ }
514
683
  var BlastRadiusCalculator = class {
515
- calculate(graph, symbolId) {
684
+ calculate(graph, symbolId, coChangeMap) {
516
685
  if (!graph.hasNode(symbolId)) {
517
- return { impactedSymbols: [], directDependentsCount: 0, overallRiskScore: 0 };
686
+ return { impactedSymbols: [], directDependentsCount: 0, confirmedCount: 0, likelyCount: 0, potentialCount: 0, overallRiskScore: 0 };
518
687
  }
688
+ const sourceFile = symbolId.split("::")[0];
519
689
  const visited = /* @__PURE__ */ new Set([symbolId]);
520
- const entries = [];
690
+ const rawEntries = [];
521
691
  const queue = [{ id: symbolId, depth: 0 }];
522
692
  while (queue.length > 0) {
523
693
  const current = queue.shift();
524
694
  const reverseEdges = graph.getReverseEdges(current.id);
695
+ const bestByNode = /* @__PURE__ */ new Map();
525
696
  for (const edge of reverseEdges) {
526
697
  if (visited.has(edge.from)) continue;
527
- visited.add(edge.from);
528
- if (!graph.hasNode(edge.from)) continue;
698
+ const conf = edgeKindToConfidence(edge.kind);
699
+ const existing = bestByNode.get(edge.from);
700
+ if (!existing) {
701
+ bestByNode.set(edge.from, { confidence: conf, kinds: /* @__PURE__ */ new Set([edge.kind]) });
702
+ } else {
703
+ existing.kinds.add(edge.kind);
704
+ if (CONFIDENCE_RANK[conf] > CONFIDENCE_RANK[existing.confidence]) {
705
+ existing.confidence = conf;
706
+ }
707
+ }
708
+ }
709
+ for (const [nodeId, info] of bestByNode) {
710
+ visited.add(nodeId);
711
+ if (!graph.hasNode(nodeId)) continue;
529
712
  const depth = current.depth + 1;
530
713
  const riskScore = 1 / Math.pow(depth, 0.7);
531
- entries.push({
532
- symbolId: edge.from,
714
+ let confidence = info.confidence;
715
+ let coChangeFrequency;
716
+ if (coChangeMap) {
717
+ const nodeFile = nodeId.split("::")[0];
718
+ const coEntries = coChangeMap.get(sourceFile);
719
+ if (coEntries) {
720
+ const match = coEntries.find((e) => e.file1 === nodeFile || e.file2 === nodeFile);
721
+ if (match) {
722
+ coChangeFrequency = match.frequency;
723
+ if (confidence === "potential" && match.frequency > 0.5) {
724
+ confidence = "likely";
725
+ }
726
+ }
727
+ }
728
+ }
729
+ rawEntries.push({
730
+ symbolId: nodeId,
533
731
  depth,
534
- dependentCount: graph.getReverseEdges(edge.from).length,
535
- riskScore: Math.round(riskScore * 1e3) / 1e3
732
+ riskScore: Math.round(riskScore * 1e3) / 1e3,
733
+ confidence,
734
+ edgeKinds: [...info.kinds],
735
+ coChangeFrequency
536
736
  });
537
- queue.push({ id: edge.from, depth });
737
+ queue.push({ id: nodeId, depth });
538
738
  }
539
739
  }
740
+ const blastSet = new Set(rawEntries.map((e) => e.symbolId));
741
+ blastSet.add(symbolId);
742
+ const entries = rawEntries.map((e) => ({
743
+ ...e,
744
+ dependentCount: graph.getReverseEdges(e.symbolId).filter((re) => blastSet.has(re.from)).length
745
+ }));
540
746
  entries.sort((a, b) => a.depth - b.depth);
541
747
  const directDependentsCount = entries.filter((e) => e.depth === 1).length;
542
748
  const totalRisk = entries.reduce((sum, e) => sum + e.riskScore, 0);
543
- const maxPossibleRisk = entries.length > 0 ? entries.length : 1;
544
- const overallRiskScore = Math.round(Math.min(totalRisk / maxPossibleRisk, 1) * 1e3) / 1e3;
545
- return { impactedSymbols: entries, directDependentsCount, overallRiskScore };
749
+ const overallRiskScore = Math.round(Math.min(totalRisk / Math.max(directDependentsCount, 1), 1) * 1e3) / 1e3;
750
+ const confirmedCount = entries.filter((e) => e.confidence === "confirmed").length;
751
+ const likelyCount = entries.filter((e) => e.confidence === "likely").length;
752
+ const potentialCount = entries.filter((e) => e.confidence === "potential").length;
753
+ return { impactedSymbols: entries, directDependentsCount, confirmedCount, likelyCount, potentialCount, overallRiskScore };
546
754
  }
547
755
  };
548
756
 
549
757
  // src/adapters/mcp/get-blast-radius.ts
550
758
  var InputSchema4 = z4.object({
551
- symbolId: z4.string().min(1)
759
+ symbolId: z4.string().min(1),
760
+ confidence: z4.enum(["confirmed", "likely", "potential"]).optional(),
761
+ intent: z4.string().optional().describe('Filter results by intent keywords (e.g., "test", "adapter", "security")')
552
762
  });
553
763
  function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
554
764
  const calculator = new BlastRadiusCalculator();
@@ -573,13 +783,24 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
573
783
  };
574
784
  }
575
785
  const result = calculator.calculate(graph, symbolId);
576
- const payload = masking.mask(JSON.stringify({
786
+ let symbols = result.impactedSymbols;
787
+ if (parsed.data.confidence) {
788
+ symbols = symbols.filter((s) => s.confidence === parsed.data.confidence);
789
+ }
790
+ symbols = filterByIntent(symbols, parsed.data.intent);
791
+ const confirmedCount = symbols.filter((s) => s.confidence === "confirmed").length;
792
+ const likelyCount = symbols.filter((s) => s.confidence === "likely").length;
793
+ const potentialCount = symbols.filter((s) => s.confidence === "potential").length;
794
+ const payload = masking.mask(JSON.stringify(wrapResponse({
577
795
  symbolId,
578
- impactScore: result.impactedSymbols.length,
796
+ impactScore: symbols.length,
579
797
  directDependentsCount: result.directDependentsCount,
798
+ confirmedCount,
799
+ likelyCount,
800
+ potentialCount,
580
801
  overallRiskScore: result.overallRiskScore,
581
- impactedSymbols: result.impactedSymbols
582
- }));
802
+ impactedSymbols: symbols
803
+ })));
583
804
  const content = [];
584
805
  if (staleness) {
585
806
  const warning = staleness.check(storage.listIndexedFiles());
@@ -688,17 +909,1228 @@ function handleGetArchitecturalOverlay(storage, masking, staleness) {
688
909
  };
689
910
  }
690
911
 
912
+ // src/adapters/mcp/get-dead-code.ts
913
+ import { z as z6 } from "zod";
914
+
915
+ // src/core/dead-code/dead-code-detector.ts
916
+ var TEST_PATTERNS = [/__tests__/, /\.test\.ts$/, /\.spec\.ts$/, /\btests\//, /\bfixtures?\//];
917
+ var CONFIG_PATTERNS = [/\.(config|rc)\.(ts|js|json)$/, /tsconfig/, /eslint/, /\.d\.ts$/];
918
+ var FRAMEWORK_PATTERNS = [
919
+ // Vitest
920
+ /^(describe|it|expect|beforeAll|beforeEach|afterAll|afterEach|vi)$/,
921
+ // MCP SDK
922
+ /^(registerTool|registerPrompt|connect|close)$/,
923
+ // Zod schemas (conventionally exported for validation)
924
+ /Schema$/,
925
+ // Node.js lifecycle
926
+ /^main$/
927
+ ];
928
+ var SCAFFOLDING_PATTERNS = [
929
+ { regex: /\bTODO\b/i, pattern: "TODO" },
930
+ { regex: /\bFIXME\b/i, pattern: "FIXME" },
931
+ { regex: /\bHACK\b/i, pattern: "HACK" },
932
+ { regex: /\bPLACEHOLDER\b/i, pattern: "PLACEHOLDER" },
933
+ { regex: /\bXXX\b/, pattern: "XXX" },
934
+ { regex: /Phase\s+\d|Step\s+\d/i, pattern: "PHASE/STEP" },
935
+ { regex: /not\s+(?:yet\s+)?implement/i, pattern: "NOT_IMPLEMENTED" },
936
+ { regex: /temporary|temp\s+fix/i, pattern: "TEMPORARY" }
937
+ ];
938
+ var DeadCodeDetector = class {
939
+ detect(graph, options = {}) {
940
+ const allNodes = graph.allNodes();
941
+ const candidates = options.includeTests ? allNodes : allNodes.filter((n) => {
942
+ const file = n.symbolId.split("::")[0] ?? "";
943
+ return !this.matchesAny(file, TEST_PATTERNS) && !this.matchesAny(file, CONFIG_PATTERNS);
944
+ });
945
+ if (candidates.length === 0) {
946
+ return { totalSymbols: 0, reachableSymbols: 0, deadSymbols: [], unusedExports: [], deadFiles: [], scaffolding: this.detectScaffolding(options.sourceContents), deadCodePercentage: 0 };
947
+ }
948
+ const candidateIds = new Set(candidates.map((n) => n.symbolId));
949
+ const entryPoints = /* @__PURE__ */ new Set();
950
+ for (const node of candidates) {
951
+ if (this.isFrameworkSymbol(node.name)) {
952
+ entryPoints.add(node.symbolId);
953
+ continue;
954
+ }
955
+ const reverseEdges = graph.getReverseEdges(node.symbolId);
956
+ const forwardEdges = graph.getForwardEdges(node.symbolId);
957
+ const incomingFromCandidates = reverseEdges.filter((e) => candidateIds.has(e.from) && e.from !== node.symbolId);
958
+ if (incomingFromCandidates.length === 0) {
959
+ const hasOutgoing = forwardEdges.some((e) => candidateIds.has(e.to));
960
+ if (hasOutgoing || reverseEdges.length > 0) {
961
+ entryPoints.add(node.symbolId);
962
+ }
963
+ }
964
+ }
965
+ const reachable = /* @__PURE__ */ new Set();
966
+ const queue = [...entryPoints];
967
+ while (queue.length > 0) {
968
+ const current = queue.shift();
969
+ if (reachable.has(current)) continue;
970
+ reachable.add(current);
971
+ for (const edge of graph.getForwardEdges(current)) {
972
+ if (!reachable.has(edge.to) && candidateIds.has(edge.to)) {
973
+ queue.push(edge.to);
974
+ }
975
+ }
976
+ }
977
+ const deadIds = /* @__PURE__ */ new Set();
978
+ for (const node of candidates) {
979
+ if (!reachable.has(node.symbolId)) {
980
+ deadIds.add(node.symbolId);
981
+ }
982
+ }
983
+ const cascadeDepthMap = this.computeCascadeDepths(graph, deadIds, candidateIds);
984
+ const deadSymbols = [];
985
+ for (const node of candidates) {
986
+ if (!deadIds.has(node.symbolId)) continue;
987
+ const file = node.symbolId.split("::")[0] ?? "";
988
+ const confidence = this.computeConfidence(graph, node.symbolId, deadIds);
989
+ const cascadeDepth = cascadeDepthMap.get(node.symbolId);
990
+ deadSymbols.push({
991
+ symbolId: node.symbolId,
992
+ file,
993
+ name: node.name,
994
+ kind: node.kind,
995
+ confidence,
996
+ reason: this.describeReason(confidence),
997
+ ...cascadeDepth !== void 0 && cascadeDepth > 0 ? { cascadeDepth } : {}
998
+ });
999
+ }
1000
+ const fileStats = /* @__PURE__ */ new Map();
1001
+ for (const node of candidates) {
1002
+ const file = node.symbolId.split("::")[0] ?? "";
1003
+ const stats = fileStats.get(file) ?? { total: 0, dead: 0 };
1004
+ stats.total++;
1005
+ if (deadIds.has(node.symbolId)) stats.dead++;
1006
+ fileStats.set(file, stats);
1007
+ }
1008
+ const deadFiles = [...fileStats.entries()].filter(([, s]) => s.total > 0 && s.dead === s.total).map(([file]) => file);
1009
+ const unusedExports = [];
1010
+ for (const node of candidates) {
1011
+ const reverseEdges = graph.getReverseEdges(node.symbolId);
1012
+ const externalImporters = reverseEdges.filter(
1013
+ (e) => candidateIds.has(e.from) && e.from !== node.symbolId
1014
+ );
1015
+ if (externalImporters.length === 0 && !deadIds.has(node.symbolId)) {
1016
+ unusedExports.push({
1017
+ symbolId: node.symbolId,
1018
+ file: node.symbolId.split("::")[0] ?? "",
1019
+ name: node.name,
1020
+ kind: node.kind
1021
+ });
1022
+ }
1023
+ }
1024
+ const scaffolding = this.detectScaffolding(options.sourceContents);
1025
+ const deadCodePercentage = Math.round(deadSymbols.length / candidates.length * 100 * 10) / 10;
1026
+ return {
1027
+ totalSymbols: candidates.length,
1028
+ reachableSymbols: reachable.size,
1029
+ deadSymbols,
1030
+ unusedExports,
1031
+ deadFiles,
1032
+ scaffolding,
1033
+ deadCodePercentage
1034
+ };
1035
+ }
1036
+ computeConfidence(graph, symbolId, deadIds) {
1037
+ const reverseEdges = graph.getReverseEdges(symbolId);
1038
+ if (reverseEdges.length === 0) {
1039
+ return 1;
1040
+ }
1041
+ const allImportersExcluded = reverseEdges.every((e) => {
1042
+ const importerFile = e.from.split("::")[0] ?? "";
1043
+ return this.matchesAny(importerFile, TEST_PATTERNS) || this.matchesAny(importerFile, CONFIG_PATTERNS);
1044
+ });
1045
+ if (allImportersExcluded) {
1046
+ return 0.9;
1047
+ }
1048
+ const allImportersDead = reverseEdges.every((e) => deadIds.has(e.from));
1049
+ if (allImportersDead) {
1050
+ return 0.7;
1051
+ }
1052
+ return 0.7;
1053
+ }
1054
+ describeReason(confidence) {
1055
+ switch (confidence) {
1056
+ case 1:
1057
+ return "Zero importers \u2014 no code references this symbol";
1058
+ case 0.9:
1059
+ return "Only referenced from test/config files";
1060
+ case 0.7:
1061
+ return "All importers are themselves dead (cascading)";
1062
+ }
1063
+ }
1064
+ isFrameworkSymbol(name) {
1065
+ return FRAMEWORK_PATTERNS.some((p) => p.test(name));
1066
+ }
1067
+ computeCascadeDepths(graph, deadIds, candidateIds) {
1068
+ const depths = /* @__PURE__ */ new Map();
1069
+ const rootDead = /* @__PURE__ */ new Set();
1070
+ for (const id of deadIds) {
1071
+ const reverseEdges = graph.getReverseEdges(id);
1072
+ const deadImporters = reverseEdges.filter((e) => deadIds.has(e.from) && e.from !== id);
1073
+ if (deadImporters.length === 0) {
1074
+ rootDead.add(id);
1075
+ depths.set(id, 0);
1076
+ }
1077
+ }
1078
+ const queue = [...rootDead].map((id) => ({ id, depth: 0 }));
1079
+ const visited = new Set(rootDead);
1080
+ while (queue.length > 0) {
1081
+ const current = queue.shift();
1082
+ for (const edge of graph.getForwardEdges(current.id)) {
1083
+ if (deadIds.has(edge.to) && !visited.has(edge.to) && candidateIds.has(edge.to)) {
1084
+ const newDepth = current.depth + 1;
1085
+ depths.set(edge.to, newDepth);
1086
+ visited.add(edge.to);
1087
+ queue.push({ id: edge.to, depth: newDepth });
1088
+ }
1089
+ }
1090
+ }
1091
+ return depths;
1092
+ }
1093
+ detectScaffolding(sourceContents) {
1094
+ if (!sourceContents || sourceContents.size === 0) return [];
1095
+ const results = [];
1096
+ for (const [file, content] of sourceContents) {
1097
+ if (this.matchesAny(file, TEST_PATTERNS) || this.matchesAny(file, CONFIG_PATTERNS)) continue;
1098
+ const lines = content.split("\n");
1099
+ for (let i = 0; i < lines.length; i++) {
1100
+ const line = lines[i];
1101
+ for (const { regex, pattern } of SCAFFOLDING_PATTERNS) {
1102
+ if (regex.test(line)) {
1103
+ results.push({
1104
+ file,
1105
+ line: i + 1,
1106
+ pattern,
1107
+ text: line.trim().slice(0, 120)
1108
+ });
1109
+ break;
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ return results;
1115
+ }
1116
+ matchesAny(value, patterns) {
1117
+ return patterns.some((p) => p.test(value));
1118
+ }
1119
+ };
1120
+
1121
+ // src/adapters/mcp/get-dead-code.ts
1122
+ var InputSchema6 = z6.object({
1123
+ includeTests: z6.boolean().optional().default(false),
1124
+ intent: z6.string().optional().describe('Filter dead code results by intent keywords (e.g., "adapter", "core", "function")')
1125
+ });
1126
+ function handleFindDeadCode(storage, masking, staleness, ctxoRoot = ".ctxo") {
1127
+ const detector = new DeadCodeDetector();
1128
+ const getGraph = () => {
1129
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1130
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1131
+ return buildGraphFromStorage(storage);
1132
+ };
1133
+ return (args) => {
1134
+ try {
1135
+ const parsed = InputSchema6.safeParse(args);
1136
+ if (!parsed.success) {
1137
+ return {
1138
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1139
+ };
1140
+ }
1141
+ const graph = getGraph();
1142
+ const result = detector.detect(graph, { includeTests: parsed.data.includeTests });
1143
+ const filtered = {
1144
+ ...result,
1145
+ deadSymbols: filterByIntent(result.deadSymbols, parsed.data.intent)
1146
+ };
1147
+ const payload = masking.mask(JSON.stringify(wrapResponse(filtered)));
1148
+ const content = [];
1149
+ if (staleness) {
1150
+ const warning = staleness.check(storage.listIndexedFiles());
1151
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1152
+ }
1153
+ content.push({ type: "text", text: payload });
1154
+ return { content };
1155
+ } catch (err) {
1156
+ return {
1157
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1158
+ };
1159
+ }
1160
+ };
1161
+ }
1162
+
1163
+ // src/adapters/mcp/get-context-for-task.ts
1164
+ import { z as z7 } from "zod";
1165
+
1166
+ // src/core/context-assembly/task-context-strategy.ts
1167
+ var TASK_WEIGHTS = {
1168
+ fix: {
1169
+ directDependency: 0.3,
1170
+ interfaceTypeDef: 0.1,
1171
+ blastRadiusDependent: 0.1,
1172
+ highComplexity: 0.2,
1173
+ antiPatternHistory: 0.3
1174
+ },
1175
+ extend: {
1176
+ directDependency: 0.4,
1177
+ interfaceTypeDef: 0.5,
1178
+ blastRadiusDependent: 0.1,
1179
+ highComplexity: 0,
1180
+ antiPatternHistory: 0
1181
+ },
1182
+ refactor: {
1183
+ directDependency: 0.2,
1184
+ interfaceTypeDef: 0.1,
1185
+ blastRadiusDependent: 0.5,
1186
+ highComplexity: 0.1,
1187
+ antiPatternHistory: 0.1
1188
+ },
1189
+ understand: {
1190
+ directDependency: 0.5,
1191
+ interfaceTypeDef: 0.3,
1192
+ blastRadiusDependent: 0.1,
1193
+ highComplexity: 0.1,
1194
+ antiPatternHistory: 0
1195
+ }
1196
+ };
1197
+ function getWeightsForTask(taskType) {
1198
+ return TASK_WEIGHTS[taskType];
1199
+ }
1200
+
1201
+ // src/core/context-assembly/context-assembler.ts
1202
+ var DEFAULT_TOKEN_BUDGET = 4e3;
1203
+ var ContextAssembler = class {
1204
+ logicSlice = new LogicSliceQuery();
1205
+ blastRadius = new BlastRadiusCalculator();
1206
+ assembleForTask(graph, symbolId, taskType, indices, tokenBudget = DEFAULT_TOKEN_BUDGET) {
1207
+ const target = graph.getNode(symbolId);
1208
+ if (!target) return void 0;
1209
+ const weights = getWeightsForTask(taskType);
1210
+ const slice = this.logicSlice.getLogicSlice(graph, symbolId);
1211
+ const blastResult = this.blastRadius.calculate(graph, symbolId);
1212
+ const candidateMap = /* @__PURE__ */ new Map();
1213
+ if (slice) {
1214
+ for (const dep of slice.dependencies) {
1215
+ candidateMap.set(dep.symbolId, {
1216
+ node: dep,
1217
+ signals: { isDirectDep: true, isInterfaceOrType: this.isInterfaceOrType(dep.kind), isDependent: false, complexity: 0, hasAntiPattern: false }
1218
+ });
1219
+ }
1220
+ }
1221
+ for (const entry of blastResult.impactedSymbols) {
1222
+ const node = graph.getNode(entry.symbolId);
1223
+ if (!node) continue;
1224
+ const existing = candidateMap.get(entry.symbolId);
1225
+ if (existing) {
1226
+ existing.signals.isDependent = true;
1227
+ } else {
1228
+ candidateMap.set(entry.symbolId, {
1229
+ node,
1230
+ signals: { isDirectDep: false, isInterfaceOrType: this.isInterfaceOrType(node.kind), isDependent: true, complexity: 0, hasAntiPattern: false }
1231
+ });
1232
+ }
1233
+ }
1234
+ const fileIndexMap = new Map(indices.map((i) => [i.file, i]));
1235
+ for (const [sid, candidate] of candidateMap) {
1236
+ const file = sid.split("::")[0] ?? "";
1237
+ const fileIndex = fileIndexMap.get(file);
1238
+ if (fileIndex) {
1239
+ const complexity = fileIndex.complexity?.find((c) => c.symbolId === sid);
1240
+ candidate.signals.complexity = complexity?.cyclomatic ?? 0;
1241
+ candidate.signals.hasAntiPattern = fileIndex.antiPatterns.length > 0;
1242
+ }
1243
+ }
1244
+ const scored = [...candidateMap.entries()].map(([, { node, signals }]) => ({
1245
+ entry: this.buildContextEntry(node, signals, weights),
1246
+ score: this.computeScore(signals, weights)
1247
+ }));
1248
+ scored.sort((a, b) => b.score - a.score);
1249
+ const context = [];
1250
+ let totalTokens = 0;
1251
+ for (const { entry } of scored) {
1252
+ if (totalTokens + entry.tokens > tokenBudget) continue;
1253
+ context.push(entry);
1254
+ totalTokens += entry.tokens;
1255
+ }
1256
+ const warnings = [];
1257
+ const targetFile = symbolId.split("::")[0] ?? "";
1258
+ const targetIndex = fileIndexMap.get(targetFile);
1259
+ if (targetIndex && targetIndex.antiPatterns.length > 0) {
1260
+ warnings.push(`\u26A0 Target symbol has ${targetIndex.antiPatterns.length} anti-pattern(s) in file history`);
1261
+ }
1262
+ return { target, taskType, context, totalTokens, tokenBudget, warnings };
1263
+ }
1264
+ assembleRanked(graph, query, strategy = "combined", tokenBudget = DEFAULT_TOKEN_BUDGET) {
1265
+ const allNodes = graph.allNodes();
1266
+ const queryLower = query.toLowerCase();
1267
+ let maxReverseEdges = 1;
1268
+ for (const node of allNodes) {
1269
+ const count = graph.getReverseEdges(node.symbolId).length;
1270
+ if (count > maxReverseEdges) maxReverseEdges = count;
1271
+ }
1272
+ const scored = allNodes.map((node) => {
1273
+ const relevanceScore = this.computeTextRelevance(node.name, queryLower);
1274
+ const importanceScore = graph.getReverseEdges(node.symbolId).length / maxReverseEdges;
1275
+ let combinedScore;
1276
+ switch (strategy) {
1277
+ case "dependency":
1278
+ combinedScore = relevanceScore;
1279
+ break;
1280
+ case "importance":
1281
+ combinedScore = importanceScore;
1282
+ break;
1283
+ default:
1284
+ combinedScore = relevanceScore * 0.6 + importanceScore * 0.4;
1285
+ }
1286
+ return {
1287
+ symbolId: node.symbolId,
1288
+ name: node.name,
1289
+ kind: node.kind,
1290
+ file: node.symbolId.split("::")[0] ?? "",
1291
+ relevanceScore: Math.round(relevanceScore * 1e3) / 1e3,
1292
+ importanceScore: Math.round(importanceScore * 1e3) / 1e3,
1293
+ combinedScore: Math.round(combinedScore * 1e3) / 1e3,
1294
+ tokens: this.estimateTokens(node)
1295
+ };
1296
+ });
1297
+ const filtered = strategy === "importance" ? scored : scored.filter((s) => s.relevanceScore > 0 || s.importanceScore > 0.1);
1298
+ filtered.sort((a, b) => b.combinedScore - a.combinedScore);
1299
+ const results = [];
1300
+ let totalTokens = 0;
1301
+ for (const item of filtered) {
1302
+ if (totalTokens + item.tokens > tokenBudget) continue;
1303
+ results.push(item);
1304
+ totalTokens += item.tokens;
1305
+ }
1306
+ return { query, strategy, results, totalTokens, tokenBudget };
1307
+ }
1308
+ computeTextRelevance(name, queryLower) {
1309
+ const nameLower = name.toLowerCase();
1310
+ if (nameLower === queryLower) return 1;
1311
+ if (nameLower.includes(queryLower)) return 0.7;
1312
+ if (queryLower.split(/\s+/).some((word) => nameLower.includes(word))) return 0.4;
1313
+ return 0;
1314
+ }
1315
+ computeScore(signals, weights) {
1316
+ let score = 0;
1317
+ if (signals.isDirectDep) score += weights.directDependency;
1318
+ if (signals.isInterfaceOrType) score += weights.interfaceTypeDef;
1319
+ if (signals.isDependent) score += weights.blastRadiusDependent;
1320
+ if (signals.complexity > 5) score += weights.highComplexity;
1321
+ if (signals.hasAntiPattern) score += weights.antiPatternHistory;
1322
+ return Math.round(score * 1e3) / 1e3;
1323
+ }
1324
+ buildContextEntry(node, signals, weights) {
1325
+ const reasons = [];
1326
+ if (signals.isDirectDep) reasons.push("direct dependency");
1327
+ if (signals.isInterfaceOrType) reasons.push("type/interface definition");
1328
+ if (signals.isDependent) reasons.push("blast radius dependent");
1329
+ if (signals.complexity > 5) reasons.push(`high complexity (CC=${signals.complexity})`);
1330
+ if (signals.hasAntiPattern) reasons.push("anti-pattern history");
1331
+ return {
1332
+ symbolId: node.symbolId,
1333
+ name: node.name,
1334
+ kind: node.kind,
1335
+ file: node.symbolId.split("::")[0] ?? "",
1336
+ relevanceScore: this.computeScore(signals, weights),
1337
+ reason: reasons.join(", ") || "related symbol",
1338
+ lines: node.endLine - node.startLine + 1,
1339
+ tokens: this.estimateTokens(node)
1340
+ };
1341
+ }
1342
+ isInterfaceOrType(kind) {
1343
+ return kind === "interface" || kind === "type";
1344
+ }
1345
+ estimateTokens(node) {
1346
+ if (node.startOffset !== void 0 && node.endOffset !== void 0) {
1347
+ return Math.ceil((node.endOffset - node.startOffset) / 4);
1348
+ }
1349
+ return (node.endLine - node.startLine + 1) * 10;
1350
+ }
1351
+ };
1352
+
1353
+ // src/adapters/mcp/get-context-for-task.ts
1354
+ var TaskTypeSchema = z7.enum(["fix", "extend", "refactor", "understand"]);
1355
+ var InputSchema7 = z7.object({
1356
+ symbolId: z7.string().min(1),
1357
+ taskType: TaskTypeSchema,
1358
+ tokenBudget: z7.number().min(100).optional().default(4e3)
1359
+ });
1360
+ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo") {
1361
+ const assembler = new ContextAssembler();
1362
+ const indexReader = new JsonIndexReader(ctxoRoot);
1363
+ const getGraph = () => {
1364
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1365
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1366
+ return buildGraphFromStorage(storage);
1367
+ };
1368
+ return (args) => {
1369
+ try {
1370
+ const parsed = InputSchema7.safeParse(args);
1371
+ if (!parsed.success) {
1372
+ return {
1373
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1374
+ };
1375
+ }
1376
+ const { symbolId, taskType, tokenBudget } = parsed.data;
1377
+ const graph = getGraph();
1378
+ const indices = indexReader.readAll();
1379
+ const result = assembler.assembleForTask(graph, symbolId, taskType, indices, tokenBudget);
1380
+ if (!result) {
1381
+ return {
1382
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index".' }) }]
1383
+ };
1384
+ }
1385
+ const payload = masking.mask(JSON.stringify(wrapResponse(result)));
1386
+ const content = [];
1387
+ if (staleness) {
1388
+ const warning = staleness.check(storage.listIndexedFiles());
1389
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1390
+ }
1391
+ content.push({ type: "text", text: payload });
1392
+ return { content };
1393
+ } catch (err) {
1394
+ return {
1395
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1396
+ };
1397
+ }
1398
+ };
1399
+ }
1400
+
1401
+ // src/adapters/mcp/get-ranked-context.ts
1402
+ import { z as z8 } from "zod";
1403
+ var InputSchema8 = z8.object({
1404
+ query: z8.string().min(1),
1405
+ tokenBudget: z8.number().min(100).optional().default(4e3),
1406
+ strategy: z8.enum(["combined", "dependency", "importance"]).optional().default("combined")
1407
+ });
1408
+ function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo") {
1409
+ const assembler = new ContextAssembler();
1410
+ const getGraph = () => {
1411
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1412
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1413
+ return buildGraphFromStorage(storage);
1414
+ };
1415
+ return (args) => {
1416
+ try {
1417
+ const parsed = InputSchema8.safeParse(args);
1418
+ if (!parsed.success) {
1419
+ return {
1420
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1421
+ };
1422
+ }
1423
+ const { query, tokenBudget, strategy } = parsed.data;
1424
+ const graph = getGraph();
1425
+ const result = assembler.assembleRanked(graph, query, strategy, tokenBudget);
1426
+ const payload = masking.mask(JSON.stringify(wrapResponse(result)));
1427
+ const content = [];
1428
+ if (staleness) {
1429
+ const warning = staleness.check(storage.listIndexedFiles());
1430
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1431
+ }
1432
+ content.push({ type: "text", text: payload });
1433
+ return { content };
1434
+ } catch (err) {
1435
+ return {
1436
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1437
+ };
1438
+ }
1439
+ };
1440
+ }
1441
+
1442
+ // src/adapters/mcp/search-symbols.ts
1443
+ import { z as z9 } from "zod";
1444
+ var InputSchema9 = z9.object({
1445
+ pattern: z9.string().min(1),
1446
+ kind: z9.enum(["function", "class", "interface", "method", "variable", "type"]).optional(),
1447
+ filePattern: z9.string().optional(),
1448
+ limit: z9.number().int().min(1).max(100).optional().default(25)
1449
+ });
1450
+ function handleSearchSymbols(storage, masking, staleness, ctxoRoot = ".ctxo") {
1451
+ const getGraph = () => {
1452
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1453
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1454
+ return buildGraphFromStorage(storage);
1455
+ };
1456
+ return (args) => {
1457
+ try {
1458
+ const parsed = InputSchema9.safeParse(args);
1459
+ if (!parsed.success) {
1460
+ return {
1461
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1462
+ };
1463
+ }
1464
+ const { pattern, kind, filePattern, limit } = parsed.data;
1465
+ const graph = getGraph();
1466
+ const allNodes = graph.allNodes();
1467
+ let matcher;
1468
+ try {
1469
+ const regex = new RegExp(pattern, "i");
1470
+ matcher = (name) => regex.test(name);
1471
+ } catch {
1472
+ const lowerPattern = pattern.toLowerCase();
1473
+ matcher = (name) => name.toLowerCase().includes(lowerPattern);
1474
+ }
1475
+ const matches = allNodes.filter((node) => {
1476
+ if (!matcher(node.name)) return false;
1477
+ if (kind && node.kind !== kind) return false;
1478
+ if (filePattern) {
1479
+ const file = node.symbolId.split("::")[0] ?? "";
1480
+ if (!file.toLowerCase().includes(filePattern.toLowerCase())) return false;
1481
+ }
1482
+ return true;
1483
+ });
1484
+ const results = matches.slice(0, limit).map((node) => ({
1485
+ symbolId: node.symbolId,
1486
+ name: node.name,
1487
+ kind: node.kind,
1488
+ file: node.symbolId.split("::")[0],
1489
+ startLine: node.startLine,
1490
+ endLine: node.endLine
1491
+ }));
1492
+ const payload = masking.mask(JSON.stringify(wrapResponse({
1493
+ totalMatches: matches.length,
1494
+ results
1495
+ })));
1496
+ const content = [];
1497
+ if (staleness) {
1498
+ const warning = staleness.check(storage.listIndexedFiles());
1499
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1500
+ }
1501
+ content.push({ type: "text", text: payload });
1502
+ return { content };
1503
+ } catch (err) {
1504
+ return {
1505
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1506
+ };
1507
+ }
1508
+ };
1509
+ }
1510
+
1511
+ // src/adapters/mcp/get-changed-symbols.ts
1512
+ import { z as z10 } from "zod";
1513
+ var InputSchema10 = z10.object({
1514
+ since: z10.string().optional().default("HEAD~1"),
1515
+ maxFiles: z10.number().int().min(1).optional().default(50)
1516
+ });
1517
+ function handleGetChangedSymbols(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
1518
+ const getGraph = () => {
1519
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1520
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1521
+ return buildGraphFromStorage(storage);
1522
+ };
1523
+ return async (args) => {
1524
+ try {
1525
+ const parsed = InputSchema10.safeParse(args);
1526
+ if (!parsed.success) {
1527
+ return {
1528
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1529
+ };
1530
+ }
1531
+ const { since, maxFiles } = parsed.data;
1532
+ const changedPaths = await git.getChangedFiles(since);
1533
+ const graph = getGraph();
1534
+ const nodesByFile = /* @__PURE__ */ new Map();
1535
+ for (const node of graph.allNodes()) {
1536
+ const file = node.symbolId.split("::")[0] ?? "";
1537
+ const existing = nodesByFile.get(file) ?? [];
1538
+ existing.push({ symbolId: node.symbolId, name: node.name, kind: node.kind, startLine: node.startLine, endLine: node.endLine });
1539
+ nodesByFile.set(file, existing);
1540
+ }
1541
+ const files = [];
1542
+ let totalSymbols = 0;
1543
+ for (const filePath of changedPaths.slice(0, maxFiles)) {
1544
+ const symbols = nodesByFile.get(filePath);
1545
+ if (symbols && symbols.length > 0) {
1546
+ files.push({ file: filePath, symbols });
1547
+ totalSymbols += symbols.length;
1548
+ }
1549
+ }
1550
+ const payload = masking.mask(JSON.stringify(wrapResponse({
1551
+ since,
1552
+ changedFiles: files.length,
1553
+ changedSymbols: totalSymbols,
1554
+ files
1555
+ })));
1556
+ const content = [];
1557
+ if (staleness) {
1558
+ const warning = staleness.check(storage.listIndexedFiles());
1559
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1560
+ }
1561
+ content.push({ type: "text", text: payload });
1562
+ return { content };
1563
+ } catch (err) {
1564
+ return {
1565
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1566
+ };
1567
+ }
1568
+ };
1569
+ }
1570
+
1571
+ // src/adapters/mcp/find-importers.ts
1572
+ import { z as z11 } from "zod";
1573
+ var InputSchema11 = z11.object({
1574
+ symbolId: z11.string().min(1),
1575
+ edgeKinds: z11.array(z11.enum(["imports", "calls", "extends", "implements", "uses"])).optional(),
1576
+ transitive: z11.boolean().optional().default(false),
1577
+ maxDepth: z11.number().int().min(1).max(10).optional().default(5),
1578
+ intent: z11.string().optional().describe('Filter results by intent keywords (e.g., "test", "adapter", "core")')
1579
+ });
1580
+ function handleFindImporters(storage, masking, staleness, ctxoRoot = ".ctxo") {
1581
+ const getGraph = () => {
1582
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1583
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1584
+ return buildGraphFromStorage(storage);
1585
+ };
1586
+ return (args) => {
1587
+ try {
1588
+ const parsed = InputSchema11.safeParse(args);
1589
+ if (!parsed.success) {
1590
+ return {
1591
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1592
+ };
1593
+ }
1594
+ const { symbolId, edgeKinds, transitive, maxDepth } = parsed.data;
1595
+ const graph = getGraph();
1596
+ if (!graph.hasNode(symbolId)) {
1597
+ return {
1598
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
1599
+ };
1600
+ }
1601
+ const importers = [];
1602
+ if (!transitive) {
1603
+ const reverseEdges = graph.getReverseEdges(symbolId);
1604
+ const seen = /* @__PURE__ */ new Map();
1605
+ for (const edge of reverseEdges) {
1606
+ if (edgeKinds && !edgeKinds.includes(edge.kind)) continue;
1607
+ const node = graph.getNode(edge.from);
1608
+ if (node) {
1609
+ const existing = seen.get(node.symbolId);
1610
+ if (existing) {
1611
+ if (!existing.edgeKinds.includes(edge.kind)) {
1612
+ existing.edgeKinds.push(edge.kind);
1613
+ }
1614
+ } else {
1615
+ const entry = {
1616
+ symbolId: node.symbolId,
1617
+ name: node.name,
1618
+ kind: node.kind,
1619
+ file: node.symbolId.split("::")[0] ?? "",
1620
+ edgeKind: edge.kind,
1621
+ depth: 1
1622
+ };
1623
+ seen.set(node.symbolId, { node: entry, edgeKinds: [edge.kind] });
1624
+ importers.push(entry);
1625
+ }
1626
+ }
1627
+ }
1628
+ } else {
1629
+ const visited = /* @__PURE__ */ new Set([symbolId]);
1630
+ const queue = [{ id: symbolId, depth: 0 }];
1631
+ while (queue.length > 0) {
1632
+ const current = queue.shift();
1633
+ if (current.depth >= maxDepth) continue;
1634
+ const reverseEdges = graph.getReverseEdges(current.id);
1635
+ for (const edge of reverseEdges) {
1636
+ if (edgeKinds && !edgeKinds.includes(edge.kind)) continue;
1637
+ if (visited.has(edge.from)) continue;
1638
+ visited.add(edge.from);
1639
+ const node = graph.getNode(edge.from);
1640
+ if (node) {
1641
+ const depth = current.depth + 1;
1642
+ importers.push({
1643
+ symbolId: node.symbolId,
1644
+ name: node.name,
1645
+ kind: node.kind,
1646
+ file: node.symbolId.split("::")[0] ?? "",
1647
+ edgeKind: edge.kind,
1648
+ depth
1649
+ });
1650
+ queue.push({ id: edge.from, depth });
1651
+ }
1652
+ }
1653
+ }
1654
+ }
1655
+ importers.sort((a, b) => a.depth - b.depth);
1656
+ const filtered = filterByIntent(importers, parsed.data.intent);
1657
+ const payload = masking.mask(JSON.stringify(wrapResponse({
1658
+ symbolId,
1659
+ importerCount: filtered.length,
1660
+ importers: filtered
1661
+ })));
1662
+ const content = [];
1663
+ if (staleness) {
1664
+ const warning = staleness.check(storage.listIndexedFiles());
1665
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1666
+ }
1667
+ content.push({ type: "text", text: payload });
1668
+ return { content };
1669
+ } catch (err) {
1670
+ return {
1671
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1672
+ };
1673
+ }
1674
+ };
1675
+ }
1676
+
1677
+ // src/adapters/mcp/get-class-hierarchy.ts
1678
+ import { z as z12 } from "zod";
1679
+ var InputSchema12 = z12.object({
1680
+ symbolId: z12.string().min(1).optional(),
1681
+ direction: z12.enum(["ancestors", "descendants", "both"]).optional().default("both")
1682
+ });
1683
+ function handleGetClassHierarchy(storage, masking, staleness, ctxoRoot = ".ctxo") {
1684
+ const getGraph = () => {
1685
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1686
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1687
+ return buildGraphFromStorage(storage);
1688
+ };
1689
+ return (args) => {
1690
+ try {
1691
+ const parsed = InputSchema12.safeParse(args);
1692
+ if (!parsed.success) {
1693
+ return {
1694
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1695
+ };
1696
+ }
1697
+ const { symbolId, direction } = parsed.data;
1698
+ const graph = getGraph();
1699
+ if (symbolId) {
1700
+ return handleRooted(graph, symbolId, direction, masking, staleness, storage);
1701
+ }
1702
+ return handleFull(graph, masking, staleness, storage);
1703
+ } catch (err) {
1704
+ return {
1705
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1706
+ };
1707
+ }
1708
+ };
1709
+ function handleRooted(graph, symbolId, direction, maskingPort, stalenessCheck, storagePort) {
1710
+ if (!graph.hasNode(symbolId)) {
1711
+ return {
1712
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
1713
+ };
1714
+ }
1715
+ const ancestors = [];
1716
+ const descendants = [];
1717
+ const isHierarchyEdge = (kind) => kind === "extends" || kind === "implements";
1718
+ if (direction === "ancestors" || direction === "both") {
1719
+ const visited = /* @__PURE__ */ new Set([symbolId]);
1720
+ const queue = [{ id: symbolId, depth: 0 }];
1721
+ while (queue.length > 0) {
1722
+ const current = queue.shift();
1723
+ for (const edge of graph.getForwardEdges(current.id)) {
1724
+ if (!isHierarchyEdge(edge.kind)) continue;
1725
+ if (visited.has(edge.to)) continue;
1726
+ visited.add(edge.to);
1727
+ const node = graph.getNode(edge.to);
1728
+ if (node) {
1729
+ const depth = current.depth + 1;
1730
+ ancestors.push({
1731
+ symbolId: node.symbolId,
1732
+ name: node.name,
1733
+ kind: node.kind,
1734
+ file: node.symbolId.split("::")[0] ?? "",
1735
+ edgeKind: edge.kind,
1736
+ depth
1737
+ });
1738
+ queue.push({ id: edge.to, depth });
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ if (direction === "descendants" || direction === "both") {
1744
+ const visited = /* @__PURE__ */ new Set([symbolId]);
1745
+ const queue = [{ id: symbolId, depth: 0 }];
1746
+ while (queue.length > 0) {
1747
+ const current = queue.shift();
1748
+ for (const edge of graph.getReverseEdges(current.id)) {
1749
+ if (!isHierarchyEdge(edge.kind)) continue;
1750
+ if (visited.has(edge.from)) continue;
1751
+ visited.add(edge.from);
1752
+ const node = graph.getNode(edge.from);
1753
+ if (node) {
1754
+ const depth = current.depth + 1;
1755
+ descendants.push({
1756
+ symbolId: node.symbolId,
1757
+ name: node.name,
1758
+ kind: node.kind,
1759
+ file: node.symbolId.split("::")[0] ?? "",
1760
+ edgeKind: edge.kind,
1761
+ depth
1762
+ });
1763
+ queue.push({ id: edge.from, depth });
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ const result = { symbolId };
1769
+ if (direction === "ancestors" || direction === "both") result.ancestors = ancestors;
1770
+ if (direction === "descendants" || direction === "both") result.descendants = descendants;
1771
+ const payload = maskingPort.mask(JSON.stringify(wrapResponse(result)));
1772
+ const content = [];
1773
+ if (stalenessCheck) {
1774
+ const warning = stalenessCheck.check(storagePort.listIndexedFiles());
1775
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1776
+ }
1777
+ content.push({ type: "text", text: payload });
1778
+ return { content };
1779
+ }
1780
+ function handleFull(graph, maskingPort, stalenessCheck, storagePort) {
1781
+ const isHierarchyEdge = (kind) => kind === "extends" || kind === "implements";
1782
+ const hierarchyEdges = graph.allEdges().filter((e) => isHierarchyEdge(e.kind));
1783
+ const involved = /* @__PURE__ */ new Set();
1784
+ for (const edge of hierarchyEdges) {
1785
+ involved.add(edge.from);
1786
+ involved.add(edge.to);
1787
+ }
1788
+ const sources = new Set(hierarchyEdges.map((e) => e.from));
1789
+ const roots = [...involved].filter((id) => !sources.has(id));
1790
+ const rootSet = roots.length > 0 ? roots : [...involved];
1791
+ function buildTree(rootId, visited2) {
1792
+ const node = graph.getNode(rootId);
1793
+ if (!node) return void 0;
1794
+ visited2.add(rootId);
1795
+ const children = [];
1796
+ for (const edge of graph.getReverseEdges(rootId)) {
1797
+ if (!isHierarchyEdge(edge.kind)) continue;
1798
+ if (visited2.has(edge.from)) continue;
1799
+ const child = buildTree(edge.from, visited2);
1800
+ if (child) {
1801
+ child.edgeKind = edge.kind;
1802
+ children.push(child);
1803
+ }
1804
+ }
1805
+ return {
1806
+ symbolId: node.symbolId,
1807
+ name: node.name,
1808
+ kind: node.kind,
1809
+ file: node.symbolId.split("::")[0] ?? "",
1810
+ children
1811
+ };
1812
+ }
1813
+ const visited = /* @__PURE__ */ new Set();
1814
+ const hierarchies = [];
1815
+ for (const rootId of rootSet) {
1816
+ if (visited.has(rootId)) continue;
1817
+ const tree = buildTree(rootId, visited);
1818
+ if (tree) hierarchies.push(tree);
1819
+ }
1820
+ const payload = maskingPort.mask(JSON.stringify(wrapResponse({
1821
+ hierarchies,
1822
+ totalClasses: involved.size,
1823
+ totalEdges: hierarchyEdges.length
1824
+ })));
1825
+ const content = [];
1826
+ if (stalenessCheck) {
1827
+ const warning = stalenessCheck.check(storagePort.listIndexedFiles());
1828
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1829
+ }
1830
+ content.push({ type: "text", text: payload });
1831
+ return { content };
1832
+ }
1833
+ }
1834
+
1835
+ // src/adapters/mcp/get-symbol-importance.ts
1836
+ import { z as z13 } from "zod";
1837
+
1838
+ // src/core/importance/pagerank-calculator.ts
1839
+ var DEFAULT_DAMPING = 0.85;
1840
+ var DEFAULT_MAX_ITERATIONS = 100;
1841
+ var DEFAULT_TOLERANCE = 1e-6;
1842
+ var DEFAULT_LIMIT = 25;
1843
+ var PageRankCalculator = class {
1844
+ calculate(graph, options = {}) {
1845
+ const damping = options.damping ?? DEFAULT_DAMPING;
1846
+ const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
1847
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
1848
+ const limit = options.limit ?? DEFAULT_LIMIT;
1849
+ const nodes = graph.allNodes();
1850
+ const n = nodes.length;
1851
+ if (n === 0) {
1852
+ return { rankings: [], totalSymbols: 0, iterations: 0, converged: true };
1853
+ }
1854
+ const scores = /* @__PURE__ */ new Map();
1855
+ const initialScore = 1 / n;
1856
+ for (const node of nodes) {
1857
+ scores.set(node.symbolId, initialScore);
1858
+ }
1859
+ const outDegree = /* @__PURE__ */ new Map();
1860
+ for (const node of nodes) {
1861
+ outDegree.set(node.symbolId, graph.getForwardEdges(node.symbolId).length);
1862
+ }
1863
+ let iterations = 0;
1864
+ let converged = false;
1865
+ while (iterations < maxIterations) {
1866
+ const newScores = /* @__PURE__ */ new Map();
1867
+ const base = (1 - damping) / n;
1868
+ let danglingSum = 0;
1869
+ for (const node of nodes) {
1870
+ if ((outDegree.get(node.symbolId) ?? 0) === 0) {
1871
+ danglingSum += scores.get(node.symbolId) ?? 0;
1872
+ }
1873
+ }
1874
+ const danglingContrib = damping * danglingSum / n;
1875
+ for (const node of nodes) {
1876
+ let incomingScore = 0;
1877
+ const reverseEdges = graph.getReverseEdges(node.symbolId);
1878
+ const contributors = /* @__PURE__ */ new Set();
1879
+ for (const edge of reverseEdges) {
1880
+ if (contributors.has(edge.from)) continue;
1881
+ contributors.add(edge.from);
1882
+ const fromScore = scores.get(edge.from) ?? 0;
1883
+ const fromOut = outDegree.get(edge.from) ?? 1;
1884
+ incomingScore += fromScore / fromOut;
1885
+ }
1886
+ newScores.set(node.symbolId, base + damping * incomingScore + danglingContrib);
1887
+ }
1888
+ let maxDelta = 0;
1889
+ for (const node of nodes) {
1890
+ const delta = Math.abs((newScores.get(node.symbolId) ?? 0) - (scores.get(node.symbolId) ?? 0));
1891
+ if (delta > maxDelta) maxDelta = delta;
1892
+ }
1893
+ for (const [id, score] of newScores) {
1894
+ scores.set(id, score);
1895
+ }
1896
+ iterations++;
1897
+ if (maxDelta < tolerance) {
1898
+ converged = true;
1899
+ break;
1900
+ }
1901
+ }
1902
+ const entries = nodes.map((node) => ({
1903
+ symbolId: node.symbolId,
1904
+ name: node.name,
1905
+ kind: node.kind,
1906
+ file: node.symbolId.split("::")[0] ?? "",
1907
+ score: Math.round((scores.get(node.symbolId) ?? 0) * 1e6) / 1e6,
1908
+ inDegree: graph.getReverseEdges(node.symbolId).length,
1909
+ outDegree: outDegree.get(node.symbolId) ?? 0
1910
+ }));
1911
+ entries.sort((a, b) => b.score - a.score);
1912
+ return {
1913
+ rankings: entries.slice(0, limit),
1914
+ totalSymbols: n,
1915
+ iterations,
1916
+ converged
1917
+ };
1918
+ }
1919
+ };
1920
+
1921
+ // src/adapters/mcp/get-symbol-importance.ts
1922
+ var InputSchema13 = z13.object({
1923
+ limit: z13.number().int().min(1).max(200).optional().default(25),
1924
+ kind: z13.enum(["function", "class", "interface", "method", "variable", "type"]).optional(),
1925
+ filePattern: z13.string().optional(),
1926
+ damping: z13.number().min(0).max(1).optional().default(0.85)
1927
+ });
1928
+ function handleGetSymbolImportance(storage, masking, staleness, ctxoRoot = ".ctxo") {
1929
+ const calculator = new PageRankCalculator();
1930
+ const getGraph = () => {
1931
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1932
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
1933
+ return buildGraphFromStorage(storage);
1934
+ };
1935
+ return (args) => {
1936
+ try {
1937
+ const parsed = InputSchema13.safeParse(args);
1938
+ if (!parsed.success) {
1939
+ return {
1940
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1941
+ };
1942
+ }
1943
+ const { limit, kind, filePattern, damping } = parsed.data;
1944
+ const graph = getGraph();
1945
+ const result = calculator.calculate(graph, { damping, limit: graph.nodeCount, maxIterations: 100 });
1946
+ let filtered = result.rankings;
1947
+ if (kind) {
1948
+ filtered = filtered.filter((e) => e.kind === kind);
1949
+ }
1950
+ if (filePattern) {
1951
+ const lowerPattern = filePattern.toLowerCase();
1952
+ filtered = filtered.filter((e) => e.file.toLowerCase().includes(lowerPattern));
1953
+ }
1954
+ filtered = filtered.slice(0, limit);
1955
+ const payload = masking.mask(JSON.stringify(wrapResponse({
1956
+ rankings: filtered,
1957
+ totalSymbols: result.totalSymbols,
1958
+ iterations: result.iterations,
1959
+ converged: result.converged,
1960
+ damping
1961
+ })));
1962
+ const content = [];
1963
+ if (staleness) {
1964
+ const warning = staleness.check(storage.listIndexedFiles());
1965
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
1966
+ }
1967
+ content.push({ type: "text", text: payload });
1968
+ return { content };
1969
+ } catch (err) {
1970
+ return {
1971
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
1972
+ };
1973
+ }
1974
+ };
1975
+ }
1976
+
1977
+ // src/adapters/mcp/get-pr-impact.ts
1978
+ import { z as z14 } from "zod";
1979
+ import { readFileSync, existsSync } from "fs";
1980
+ import { join } from "path";
1981
+ var InputSchema14 = z14.object({
1982
+ since: z14.string().optional().default("HEAD~1"),
1983
+ maxFiles: z14.number().int().min(1).optional().default(50),
1984
+ confidence: z14.enum(["confirmed", "likely", "potential"]).optional()
1985
+ });
1986
+ function loadCoChanges(ctxoRoot) {
1987
+ const path = join(ctxoRoot, "index", "co-changes.json");
1988
+ if (!existsSync(path)) return void 0;
1989
+ try {
1990
+ const matrix = JSON.parse(readFileSync(path, "utf-8"));
1991
+ return loadCoChangeMap(matrix);
1992
+ } catch {
1993
+ return void 0;
1994
+ }
1995
+ }
1996
+ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
1997
+ const calculator = new BlastRadiusCalculator();
1998
+ const getGraph = () => {
1999
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
2000
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
2001
+ return buildGraphFromStorage(storage);
2002
+ };
2003
+ return async (args) => {
2004
+ try {
2005
+ const parsed = InputSchema14.safeParse(args);
2006
+ if (!parsed.success) {
2007
+ return {
2008
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
2009
+ };
2010
+ }
2011
+ const { since, maxFiles, confidence: confFilter } = parsed.data;
2012
+ const changedPaths = await git.getChangedFiles(since);
2013
+ if (changedPaths.length === 0) {
2014
+ return {
2015
+ content: [{ type: "text", text: JSON.stringify({ since, changedFiles: 0, changedSymbols: 0, totalImpact: 0, riskLevel: "low", files: [], summary: { confirmedTotal: 0, likelyTotal: 0, potentialTotal: 0, highRiskSymbols: [] } }) }]
2016
+ };
2017
+ }
2018
+ const graph = getGraph();
2019
+ const coChangeMap = loadCoChanges(ctxoRoot);
2020
+ const allNodes = graph.allNodes();
2021
+ const nodesByFile = /* @__PURE__ */ new Map();
2022
+ for (const node of allNodes) {
2023
+ const file = node.symbolId.split("::")[0];
2024
+ let list = nodesByFile.get(file);
2025
+ if (!list) {
2026
+ list = [];
2027
+ nodesByFile.set(file, list);
2028
+ }
2029
+ list.push(node);
2030
+ }
2031
+ const limitedPaths = changedPaths.slice(0, maxFiles);
2032
+ let totalImpact = 0;
2033
+ let confirmedTotal = 0;
2034
+ let likelyTotal = 0;
2035
+ let potentialTotal = 0;
2036
+ const highRiskSymbols = [];
2037
+ const files = [];
2038
+ for (const filePath of limitedPaths) {
2039
+ const symbols = nodesByFile.get(filePath);
2040
+ if (!symbols || symbols.length === 0) continue;
2041
+ const fileSymbols = [];
2042
+ for (const sym of symbols) {
2043
+ const result = calculator.calculate(graph, sym.symbolId, coChangeMap);
2044
+ let impacted = result.impactedSymbols;
2045
+ if (confFilter) {
2046
+ impacted = impacted.filter((s) => s.confidence === confFilter);
2047
+ }
2048
+ const impactScore = impacted.length;
2049
+ const confirmed = impacted.filter((s) => s.confidence === "confirmed").length;
2050
+ const likely = impacted.filter((s) => s.confidence === "likely").length;
2051
+ const potential = impacted.filter((s) => s.confidence === "potential").length;
2052
+ totalImpact += impactScore;
2053
+ confirmedTotal += confirmed;
2054
+ likelyTotal += likely;
2055
+ potentialTotal += potential;
2056
+ if (result.overallRiskScore > 0.7) {
2057
+ highRiskSymbols.push(sym.symbolId);
2058
+ }
2059
+ const topImpacted = impacted.sort((a, b) => b.riskScore - a.riskScore).slice(0, 10);
2060
+ fileSymbols.push({
2061
+ symbolId: sym.symbolId,
2062
+ name: sym.name,
2063
+ kind: sym.kind,
2064
+ blast: {
2065
+ impactScore,
2066
+ confirmedCount: confirmed,
2067
+ likelyCount: likely,
2068
+ potentialCount: potential,
2069
+ riskScore: result.overallRiskScore,
2070
+ topImpacted
2071
+ }
2072
+ });
2073
+ }
2074
+ let coChangedWith;
2075
+ if (coChangeMap) {
2076
+ const entries = coChangeMap.get(filePath);
2077
+ if (entries && entries.length > 0) {
2078
+ coChangedWith = entries.sort((a, b) => b.frequency - a.frequency).slice(0, 5).map((e) => e.file1 === filePath ? e.file2 : e.file1);
2079
+ }
2080
+ }
2081
+ files.push({
2082
+ file: filePath,
2083
+ symbols: fileSymbols,
2084
+ ...coChangedWith ? { coChangedWith } : {}
2085
+ });
2086
+ }
2087
+ const maxRisk = files.reduce((max, f) => {
2088
+ const fileMax = f.symbols.reduce((m, s) => Math.max(m, s.blast.riskScore), 0);
2089
+ return Math.max(max, fileMax);
2090
+ }, 0);
2091
+ const riskLevel = maxRisk > 0.7 ? "high" : maxRisk > 0.3 ? "medium" : "low";
2092
+ const changedSymbols = files.reduce((sum, f) => sum + f.symbols.length, 0);
2093
+ const payload = masking.mask(JSON.stringify(wrapResponse({
2094
+ since,
2095
+ changedFiles: files.length,
2096
+ changedSymbols,
2097
+ totalImpact,
2098
+ riskLevel,
2099
+ files,
2100
+ summary: {
2101
+ confirmedTotal,
2102
+ likelyTotal,
2103
+ potentialTotal,
2104
+ highRiskSymbols: highRiskSymbols.slice(0, 10)
2105
+ }
2106
+ })));
2107
+ const content = [];
2108
+ if (staleness) {
2109
+ const warning = staleness.check(storage.listIndexedFiles());
2110
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
2111
+ }
2112
+ content.push({ type: "text", text: payload });
2113
+ return { content };
2114
+ } catch (err) {
2115
+ return {
2116
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
2117
+ };
2118
+ }
2119
+ };
2120
+ }
2121
+
691
2122
  // src/index.ts
2123
+ var log = createLogger("ctxo:mcp");
692
2124
  function loadMaskingConfig(ctxoRoot) {
693
- const jsonConfigPath = join(ctxoRoot, "masking.json");
694
- if (existsSync(jsonConfigPath)) {
2125
+ const jsonConfigPath = join2(ctxoRoot, "masking.json");
2126
+ if (existsSync2(jsonConfigPath)) {
695
2127
  try {
696
- const raw = readFileSync(jsonConfigPath, "utf-8");
2128
+ const raw = readFileSync2(jsonConfigPath, "utf-8");
697
2129
  const patterns = JSON.parse(raw);
698
- console.error(`[ctxo] Loaded ${patterns.length} custom masking pattern(s)`);
2130
+ log.info(`Loaded ${patterns.length} custom masking pattern(s)`);
699
2131
  return MaskingPipeline.fromConfig(patterns);
700
2132
  } catch (err) {
701
- console.error(`[ctxo] Failed to load masking config: ${err.message}`);
2133
+ log.error(`Failed to load masking config: ${err.message}`);
702
2134
  }
703
2135
  }
704
2136
  return new MaskingPipeline();
@@ -706,7 +2138,7 @@ function loadMaskingConfig(ctxoRoot) {
706
2138
  async function main() {
707
2139
  const args = process.argv.slice(2);
708
2140
  if (args.length > 0) {
709
- const { CliRouter } = await import("./cli-router-PIWHLS5F.js");
2141
+ const { CliRouter } = await import("./cli-router-NRUGPICL.js");
710
2142
  const router = new CliRouter(process.cwd());
711
2143
  await router.route(args);
712
2144
  return;
@@ -717,40 +2149,51 @@ async function main() {
717
2149
  const masking = loadMaskingConfig(ctxoRoot);
718
2150
  const git = new SimpleGitAdapter(process.cwd());
719
2151
  const server = new McpServer({ name: "ctxo", version: "0.1.0" });
720
- const { StalenessDetector } = await import("./staleness-detector-5AN223FM.js");
2152
+ const { StalenessDetector } = await import("./staleness-detector-VSDPTPX7.js");
721
2153
  const staleness = new StalenessDetector(process.cwd(), ctxoRoot);
2154
+ const toolAnnotations = {
2155
+ readOnlyHint: true,
2156
+ destructiveHint: false,
2157
+ idempotentHint: true,
2158
+ openWorldHint: false
2159
+ };
722
2160
  const logicSliceHandler = handleGetLogicSlice(storage, masking, staleness, ctxoRoot);
723
2161
  const whyContextHandler = handleGetWhyContext(storage, git, masking, staleness, ctxoRoot);
724
2162
  const changeIntelligenceHandler = handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot);
725
2163
  server.registerTool(
726
2164
  "get_logic_slice",
727
2165
  {
728
- description: "Retrieve a Logic-Slice for a named symbol \u2014 the symbol plus all transitive dependencies",
2166
+ description: 'Retrieve a symbol and all its transitive dependencies as a Logic-Slice. Use this when you need to UNDERSTAND what a symbol depends on (downstream view). L1=signature only, L2=direct deps, L3=full closure, L4=with token budget. Use `intent` to filter dependencies by keyword (e.g., "core", "adapter"). All responses include `_meta` with item counts and truncation info. For impact analysis (what BREAKS if this changes), use get_blast_radius instead. For task-specific context, use get_context_for_task.',
729
2167
  inputSchema: {
730
- symbolId: z6.string().optional().describe("Single symbol ID (format: file::name::kind)"),
731
- symbolIds: z6.array(z6.string()).optional().describe("Batch: array of symbol IDs"),
732
- level: z6.number().min(1).max(4).optional().default(3).describe("Detail level (L1=signature, L2=direct deps, L3=full closure, L4=with token budget)")
733
- }
2168
+ symbolId: z15.string().optional().describe("Single symbol ID (format: file::name::kind)"),
2169
+ symbolIds: z15.array(z15.string()).optional().describe("Batch: array of symbol IDs"),
2170
+ level: z15.number().min(1).max(4).optional().default(3).describe("Detail level (L1=signature, L2=direct deps, L3=full closure, L4=with token budget)"),
2171
+ intent: z15.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter")')
2172
+ },
2173
+ annotations: toolAnnotations
734
2174
  },
735
2175
  (args2) => logicSliceHandler(args2)
736
2176
  );
737
2177
  server.registerTool(
738
2178
  "get_why_context",
739
2179
  {
740
- description: "Retrieve git commit intent, anti-pattern warnings from revert history for a symbol",
2180
+ description: "Retrieve git commit history intent and anti-pattern warnings (reverts, rollbacks) for a symbol. Use this when you need to understand WHY code was written this way or whether it has a history of problems. Pair with get_change_intelligence for complexity/churn scores.",
741
2181
  inputSchema: {
742
- symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
743
- }
2182
+ symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)"),
2183
+ maxCommits: z15.number().int().min(1).optional().describe("Limit commit history to N most recent commits")
2184
+ },
2185
+ annotations: toolAnnotations
744
2186
  },
745
2187
  (args2) => whyContextHandler(args2)
746
2188
  );
747
2189
  server.registerTool(
748
2190
  "get_change_intelligence",
749
2191
  {
750
- description: "Retrieve complexity x churn composite score for a symbol",
2192
+ description: "Retrieve complexity x churn composite score for a symbol \u2014 identifies hotspots that are both complex and frequently changed. Use this to prioritize refactoring targets or assess risk before modifying code. For git history details, use get_why_context. For impact scope, use get_blast_radius.",
751
2193
  inputSchema: {
752
- symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
753
- }
2194
+ symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)")
2195
+ },
2196
+ annotations: toolAnnotations
754
2197
  },
755
2198
  (args2) => changeIntelligenceHandler(args2)
756
2199
  );
@@ -758,10 +2201,13 @@ async function main() {
758
2201
  server.registerTool(
759
2202
  "get_blast_radius",
760
2203
  {
761
- description: "Retrieve the blast radius for a symbol \u2014 symbols that would break if it changed",
2204
+ description: "BEFORE modifying any function or class, call this to understand impact. Returns all symbols that would break if the target changes, split into confirmed (direct importers), likely (co-changed), and potential (transitive) tiers with risk scores. Use `confidence` to filter by tier, `intent` to filter by keyword. Large results are auto-truncated with `_meta.hint` for drill-in. For what a symbol DEPENDS ON (downstream), use get_logic_slice instead. For full PR-level analysis, use get_pr_impact.",
762
2205
  inputSchema: {
763
- symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
764
- }
2206
+ symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)"),
2207
+ confidence: z15.enum(["confirmed", "likely", "potential"]).optional().describe("Filter by confidence tier"),
2208
+ intent: z15.string().optional().describe('Filter impacted symbols by intent keywords (e.g., "test", "adapter")')
2209
+ },
2210
+ annotations: toolAnnotations
765
2211
  },
766
2212
  (args2) => blastRadiusHandler(args2)
767
2213
  );
@@ -769,18 +2215,149 @@ async function main() {
769
2215
  server.registerTool(
770
2216
  "get_architectural_overlay",
771
2217
  {
772
- description: "Retrieve an architectural overlay \u2014 layer map identifying Domain, Infrastructure, and Adapter boundaries",
2218
+ description: "Get the project architectural layer map \u2014 identifies which symbols belong to Domain, Infrastructure, and Adapter layers. Use this when onboarding to a new codebase or validating that a change respects layer boundaries. For symbol-level analysis, use get_logic_slice or get_blast_radius.",
773
2219
  inputSchema: {
774
- layer: z6.string().optional().describe("Filter by specific layer name")
775
- }
2220
+ layer: z15.string().optional().describe("Filter by specific layer name")
2221
+ },
2222
+ annotations: toolAnnotations
776
2223
  },
777
2224
  (args2) => overlayHandler(args2)
778
2225
  );
2226
+ const deadCodeHandler = handleFindDeadCode(storage, masking, staleness, ctxoRoot);
2227
+ server.registerTool(
2228
+ "find_dead_code",
2229
+ {
2230
+ description: 'Find unreachable symbols and files that are never imported or called anywhere. Use this during cleanup, refactoring, or before deleting code to confirm it is truly unused. Use `intent` to filter results by keyword (e.g., "adapter", "function"). Large results are auto-truncated with `_meta`. For reverse dependency lookup of a specific symbol, use find_importers instead.',
2231
+ inputSchema: {
2232
+ includeTests: z15.boolean().optional().default(false).describe("Include test files in analysis (default: exclude)"),
2233
+ intent: z15.string().optional().describe('Filter dead code results by intent keywords (e.g., "adapter", "function")')
2234
+ },
2235
+ annotations: toolAnnotations
2236
+ },
2237
+ (args2) => deadCodeHandler(args2)
2238
+ );
2239
+ const contextForTaskHandler = handleGetContextForTask(storage, masking, staleness, ctxoRoot);
2240
+ server.registerTool(
2241
+ "get_context_for_task",
2242
+ {
2243
+ description: 'Get task-optimized context for a symbol based on what you are about to do. Specify taskType: "fix" (bug investigation \u2014 includes history + anti-patterns), "extend" (add feature \u2014 includes deps + blast radius), "refactor" (restructure \u2014 includes importers + complexity), or "understand" (learn \u2014 includes full slice + architecture). This is the BEST starting point when you know both the symbol and your intent.',
2244
+ inputSchema: {
2245
+ symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)"),
2246
+ taskType: z15.enum(["fix", "extend", "refactor", "understand"]).describe("Task type determines which context is most relevant"),
2247
+ tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for context (default 4000)")
2248
+ },
2249
+ annotations: toolAnnotations
2250
+ },
2251
+ (args2) => contextForTaskHandler(args2)
2252
+ );
2253
+ const rankedContextHandler = handleGetRankedContext(storage, masking, staleness, ctxoRoot);
2254
+ server.registerTool(
2255
+ "get_ranked_context",
2256
+ {
2257
+ description: "Search and rank symbols by relevance to a natural language query, packed within a token budget. Uses BM25 text matching + PageRank importance scoring. Results include `_meta` with truncation info. Use this when you have a question or topic but do not know which specific symbol to look at. For exact symbol name search, use search_symbols instead.",
2258
+ inputSchema: {
2259
+ query: z15.string().min(1).describe("Search query (matches symbol names)"),
2260
+ tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for results (default 4000)"),
2261
+ strategy: z15.enum(["combined", "dependency", "importance"]).optional().default("combined").describe("Ranking strategy")
2262
+ },
2263
+ annotations: toolAnnotations
2264
+ },
2265
+ (args2) => rankedContextHandler(args2)
2266
+ );
2267
+ const searchSymbolsHandler = handleSearchSymbols(storage, masking, staleness, ctxoRoot);
2268
+ server.registerTool(
2269
+ "search_symbols",
2270
+ {
2271
+ description: "Search symbols by exact name or regex pattern across the codebase index. Use this when you know (part of) the symbol name and need to find its ID for use with other tools. For semantic/relevance-based search, use get_ranked_context instead.",
2272
+ inputSchema: {
2273
+ pattern: z15.string().min(1).describe("Search pattern (substring or regex)"),
2274
+ kind: z15.enum(["function", "class", "interface", "method", "variable", "type"]).optional().describe("Filter by symbol kind"),
2275
+ filePattern: z15.string().optional().describe("Filter by file path substring"),
2276
+ limit: z15.number().int().min(1).max(100).optional().default(25).describe("Max results (default 25)")
2277
+ },
2278
+ annotations: toolAnnotations
2279
+ },
2280
+ (args2) => searchSymbolsHandler(args2)
2281
+ );
2282
+ const changedSymbolsHandler = handleGetChangedSymbols(storage, git, masking, staleness, ctxoRoot);
2283
+ server.registerTool(
2284
+ "get_changed_symbols",
2285
+ {
2286
+ description: "Get symbols in recently changed files based on git diff. Use this to see what was modified in recent commits. For full PR risk assessment (changed symbols + blast radius + co-changes), use get_pr_impact instead.",
2287
+ inputSchema: {
2288
+ since: z15.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
2289
+ maxFiles: z15.number().int().min(1).optional().default(50).describe("Max changed files to process (default 50)")
2290
+ },
2291
+ annotations: toolAnnotations
2292
+ },
2293
+ (args2) => changedSymbolsHandler(args2)
2294
+ );
2295
+ const findImportersHandler = handleFindImporters(storage, masking, staleness, ctxoRoot);
2296
+ server.registerTool(
2297
+ "find_importers",
2298
+ {
2299
+ description: 'Find all symbols that import or depend on a given symbol (reverse dependency / "who uses this?"). Use this to check if a symbol is safe to modify or delete. Supports transitive traversal for deep impact chains. Use `intent` to filter by keyword (e.g., "test", "core"). For forward dependencies (what this symbol uses), use get_logic_slice. For aggregated impact with risk scores, use get_blast_radius.',
2300
+ inputSchema: {
2301
+ symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)"),
2302
+ edgeKinds: z15.array(z15.enum(["imports", "calls", "extends", "implements", "uses"])).optional().describe("Filter by edge kinds"),
2303
+ transitive: z15.boolean().optional().default(false).describe("Follow transitive reverse edges (default false)"),
2304
+ maxDepth: z15.number().int().min(1).max(10).optional().default(5).describe("Max BFS depth for transitive mode (default 5)"),
2305
+ intent: z15.string().optional().describe('Filter importers by intent keywords (e.g., "test", "core", "adapter")')
2306
+ },
2307
+ annotations: toolAnnotations
2308
+ },
2309
+ (args2) => findImportersHandler(args2)
2310
+ );
2311
+ const classHierarchyHandler = handleGetClassHierarchy(storage, masking, staleness, ctxoRoot);
2312
+ server.registerTool(
2313
+ "get_class_hierarchy",
2314
+ {
2315
+ description: "Get class inheritance hierarchy \u2014 ancestors (extends/implements chain) and descendants (subclasses). Use this when working with OOP code to understand type relationships before modifying a base class or interface. For dependency-based relationships (imports/calls), use get_logic_slice or find_importers.",
2316
+ inputSchema: {
2317
+ symbolId: z15.string().min(1).optional().describe("Root symbol ID (omit for full project hierarchy)"),
2318
+ direction: z15.enum(["ancestors", "descendants", "both"]).optional().default("both").describe("Traversal direction (default both)")
2319
+ },
2320
+ annotations: toolAnnotations
2321
+ },
2322
+ (args2) => classHierarchyHandler(args2)
2323
+ );
2324
+ const symbolImportanceHandler = handleGetSymbolImportance(storage, masking, staleness, ctxoRoot);
2325
+ server.registerTool(
2326
+ "get_symbol_importance",
2327
+ {
2328
+ description: "Rank symbols by structural importance using PageRank centrality on the dependency graph. Use this to identify the most critical symbols in the codebase \u2014 high-PageRank symbols are heavily depended upon and risky to change. For finding unused/unimportant code, use find_dead_code instead.",
2329
+ inputSchema: {
2330
+ limit: z15.number().int().min(1).max(200).optional().default(25).describe("Max results (default 25)"),
2331
+ kind: z15.enum(["function", "class", "interface", "method", "variable", "type"]).optional().describe("Filter by symbol kind"),
2332
+ filePattern: z15.string().optional().describe("Filter by file path substring"),
2333
+ damping: z15.number().min(0).max(1).optional().default(0.85).describe("PageRank damping factor (default 0.85)")
2334
+ },
2335
+ annotations: toolAnnotations
2336
+ },
2337
+ (args2) => symbolImportanceHandler(args2)
2338
+ );
2339
+ const prImpactHandler = handleGetPrImpact(storage, git, masking, staleness, ctxoRoot);
2340
+ server.registerTool(
2341
+ "get_pr_impact",
2342
+ {
2343
+ description: "Analyze the full impact of a PR or recent changes in a SINGLE call \u2014 combines changed symbols + blast radius + co-change history into one risk assessment. Results include `_meta` with truncation info. Use this FIRST when reviewing a PR or evaluating recent commits. For single-symbol analysis, use get_blast_radius instead.",
2344
+ inputSchema: {
2345
+ since: z15.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
2346
+ maxFiles: z15.number().int().min(1).optional().default(50).describe("Max changed files to analyze"),
2347
+ confidence: z15.enum(["confirmed", "likely", "potential"]).optional().describe("Filter impacted symbols by confidence tier")
2348
+ },
2349
+ annotations: toolAnnotations
2350
+ },
2351
+ (args2) => prImpactHandler(args2)
2352
+ );
2353
+ server.resource("ctxo-status", "ctxo://status", async (uri) => ({
2354
+ contents: [{ uri: uri.href, text: "Ctxo MCP server is running." }]
2355
+ }));
779
2356
  const transport = new StdioServerTransport();
780
2357
  await server.connect(transport);
781
2358
  }
782
2359
  main().catch((err) => {
783
- console.error("[ctxo] Fatal:", err.message);
2360
+ log.error("Fatal: %s", err.message);
784
2361
  process.exit(1);
785
2362
  });
786
2363
  //# sourceMappingURL=index.js.map