@ctxo/cli 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -5
- package/dist/{chunk-HZ4UGA6J.js → chunk-BYX3UFBJ.js} +1374 -324
- package/dist/chunk-BYX3UFBJ.js.map +1 -0
- package/dist/{chunk-CDGQGCVN.js → chunk-DT3ZH4AU.js} +90 -25
- package/dist/chunk-DT3ZH4AU.js.map +1 -0
- package/dist/{cli-router-LO2SWFC4.js → cli-router-RR4CM62C.js} +3717 -945
- package/dist/cli-router-RR4CM62C.js.map +1 -0
- package/dist/index.js +272 -273
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-OXTRTGE6.js +13 -0
- package/llms-full.txt +220 -28
- package/llms.txt +53 -10
- package/package.json +8 -6
- package/pages/visualize-template.html +8 -2
- package/skills/ctxo-review-pr/SKILL.md +2 -0
- package/skills/ctxo-safe-edit/SKILL.md +2 -0
- package/skills/ctxo-understand/SKILL.md +28 -4
- package/dist/chunk-CDGQGCVN.js.map +0 -1
- package/dist/chunk-HZ4UGA6J.js.map +0 -1
- package/dist/cli-router-LO2SWFC4.js.map +0 -1
- package/dist/json-index-reader-UYS74USB.js +0 -9
- /package/dist/{json-index-reader-UYS74USB.js.map → json-index-reader-OXTRTGE6.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
ArchitecturalOverlay,
|
|
4
4
|
BlastRadiusCalculator,
|
|
5
5
|
BoundaryViolationDetector,
|
|
6
|
+
CoChangeReader,
|
|
7
|
+
ContextsViewReader,
|
|
6
8
|
DeadCodeDetector,
|
|
7
9
|
DriftDetector,
|
|
8
10
|
PageRankCalculator,
|
|
@@ -11,18 +13,23 @@ import {
|
|
|
11
13
|
SimpleGitAdapter,
|
|
12
14
|
SqliteStorageAdapter,
|
|
13
15
|
SymbolGraph,
|
|
16
|
+
SymbolTokenizer,
|
|
17
|
+
compareCodepoint,
|
|
14
18
|
detectWorkspace,
|
|
15
|
-
|
|
19
|
+
findCoChangedFilesForBlastRadius,
|
|
16
20
|
loadConfig,
|
|
17
21
|
makeGlobMatcher,
|
|
18
22
|
maskingClusterLabelPatterns,
|
|
19
23
|
statsEnabled
|
|
20
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-BYX3UFBJ.js";
|
|
21
25
|
import "./chunk-RFEXMBTG.js";
|
|
22
26
|
import {
|
|
23
27
|
DetailLevelSchema,
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
EdgeKindSchema,
|
|
29
|
+
JsonIndexReader,
|
|
30
|
+
SYMBOL_KINDS,
|
|
31
|
+
SymbolKindSchema
|
|
32
|
+
} from "./chunk-DT3ZH4AU.js";
|
|
26
33
|
import {
|
|
27
34
|
createLogger
|
|
28
35
|
} from "./chunk-5DSKZJNN.js";
|
|
@@ -102,10 +109,20 @@ async function startHttpTransport(serverFactory, port) {
|
|
|
102
109
|
});
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
// src/cli/http-port.ts
|
|
113
|
+
var DEFAULT_HTTP_PORT = "3001";
|
|
114
|
+
function resolveHttpPort(args, env) {
|
|
115
|
+
const fromEnv = env["CTXO_HTTP_PORT"] || null;
|
|
116
|
+
if (args.length === 0) return fromEnv;
|
|
117
|
+
if (!args.includes("--http")) return null;
|
|
118
|
+
const portFlag = args.indexOf("--port");
|
|
119
|
+
return fromEnv || (portFlag === -1 ? void 0 : args[portFlag + 1]) || DEFAULT_HTTP_PORT;
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
// src/index.ts
|
|
106
123
|
import { z as z15 } from "zod";
|
|
107
|
-
import { existsSync
|
|
108
|
-
import { join
|
|
124
|
+
import { existsSync, readFileSync } from "fs";
|
|
125
|
+
import { join, dirname } from "path";
|
|
109
126
|
import { fileURLToPath } from "url";
|
|
110
127
|
|
|
111
128
|
// src/core/masking/masking-pipeline.ts
|
|
@@ -166,10 +183,10 @@ var LogicSliceQuery = class {
|
|
|
166
183
|
getLogicSlice(graph, symbolId, maxDepth = Infinity) {
|
|
167
184
|
const root = graph.getNode(symbolId);
|
|
168
185
|
if (!root) return void 0;
|
|
169
|
-
const visited = /* @__PURE__ */ new Set([symbolId]);
|
|
186
|
+
const visited = /* @__PURE__ */ new Set([root.symbolId]);
|
|
170
187
|
const dependencies = [];
|
|
171
188
|
const collectedEdges = [];
|
|
172
|
-
const queue = [{ id: symbolId, depth: 0 }];
|
|
189
|
+
const queue = [{ id: root.symbolId, depth: 0 }];
|
|
173
190
|
while (queue.length > 0) {
|
|
174
191
|
const current = queue.shift();
|
|
175
192
|
if (current.depth >= maxDepth) continue;
|
|
@@ -191,6 +208,11 @@ var LogicSliceQuery = class {
|
|
|
191
208
|
// src/core/detail-levels/detail-formatter.ts
|
|
192
209
|
var L1_MAX_LINES = 150;
|
|
193
210
|
var L4_TOKEN_BUDGET = 8e3;
|
|
211
|
+
function withoutAnnotations(node) {
|
|
212
|
+
if (node.annotations === void 0) return node;
|
|
213
|
+
const { annotations: _annotations, ...rest } = node;
|
|
214
|
+
return rest;
|
|
215
|
+
}
|
|
194
216
|
var DetailFormatter = class {
|
|
195
217
|
format(slice, level) {
|
|
196
218
|
switch (level) {
|
|
@@ -209,7 +231,7 @@ var DetailFormatter = class {
|
|
|
209
231
|
const lineCount = root.endLine - root.startLine + 1;
|
|
210
232
|
const clampedRoot = lineCount > L1_MAX_LINES ? { ...root, endLine: root.startLine + L1_MAX_LINES - 1 } : root;
|
|
211
233
|
return {
|
|
212
|
-
root: clampedRoot,
|
|
234
|
+
root: withoutAnnotations(clampedRoot),
|
|
213
235
|
dependencies: [],
|
|
214
236
|
edges: [],
|
|
215
237
|
level: 1,
|
|
@@ -222,8 +244,8 @@ var DetailFormatter = class {
|
|
|
222
244
|
const directDepIds = new Set(directEdges.map((e) => e.to));
|
|
223
245
|
const directDeps = slice.dependencies.filter((d) => directDepIds.has(d.symbolId));
|
|
224
246
|
return {
|
|
225
|
-
root: slice.root,
|
|
226
|
-
dependencies: directDeps,
|
|
247
|
+
root: withoutAnnotations(slice.root),
|
|
248
|
+
dependencies: directDeps.map(withoutAnnotations),
|
|
227
249
|
edges: directEdges,
|
|
228
250
|
level: 2,
|
|
229
251
|
levelDescription: "L2: Root + direct dependencies (depth 1)"
|
|
@@ -231,8 +253,8 @@ var DetailFormatter = class {
|
|
|
231
253
|
}
|
|
232
254
|
formatL3(slice) {
|
|
233
255
|
return {
|
|
234
|
-
root: slice.root,
|
|
235
|
-
dependencies: slice.dependencies,
|
|
256
|
+
root: withoutAnnotations(slice.root),
|
|
257
|
+
dependencies: slice.dependencies.map(withoutAnnotations),
|
|
236
258
|
edges: slice.edges,
|
|
237
259
|
level: 3,
|
|
238
260
|
levelDescription: "L3: Full transitive dependency closure"
|
|
@@ -313,6 +335,7 @@ var TRUNCATABLE_FIELDS = {
|
|
|
313
335
|
deadFiles: "Use find_dead_code with includeTests to adjust scope.",
|
|
314
336
|
files: "Use get_changed_symbols with a narrower since ref or smaller maxFiles.",
|
|
315
337
|
clusters: "Use get_architectural_overlay with mode=communities and layer filter for a focused view.",
|
|
338
|
+
candidates: "Use get_architectural_overlay with mode=contexts and candidate: <id> for one candidate in full, or ctxo contexts --json for the full contexts view.",
|
|
316
339
|
clustersAffected: "Use get_blast_radius on individual changed symbols for per-cluster impact."
|
|
317
340
|
};
|
|
318
341
|
function getThreshold() {
|
|
@@ -472,10 +495,11 @@ function buildGraphFromStorage(storage) {
|
|
|
472
495
|
function buildGraphAndFilesFromJsonIndex(ctxoRoot) {
|
|
473
496
|
const reader = new JsonIndexReader(ctxoRoot);
|
|
474
497
|
const indices = reader.readAll();
|
|
498
|
+
return { graph: buildGraphFromIndices(indices), indexedFiles: indices.map((fileIndex) => fileIndex.file) };
|
|
499
|
+
}
|
|
500
|
+
function buildGraphFromIndices(indices) {
|
|
475
501
|
const graph = new SymbolGraph();
|
|
476
|
-
const indexedFiles = [];
|
|
477
502
|
for (const fileIndex of indices) {
|
|
478
|
-
indexedFiles.push(fileIndex.file);
|
|
479
503
|
for (const sym of fileIndex.symbols) {
|
|
480
504
|
graph.addNode(sym);
|
|
481
505
|
}
|
|
@@ -485,7 +509,7 @@ function buildGraphAndFilesFromJsonIndex(ctxoRoot) {
|
|
|
485
509
|
graph.addEdge(edge);
|
|
486
510
|
}
|
|
487
511
|
}
|
|
488
|
-
return
|
|
512
|
+
return graph;
|
|
489
513
|
}
|
|
490
514
|
function getGraphAndFiles(ctxoRoot, storage) {
|
|
491
515
|
const json = buildGraphAndFilesFromJsonIndex(ctxoRoot);
|
|
@@ -585,6 +609,10 @@ async function buildSnapshotStaleness(storage, git) {
|
|
|
585
609
|
}
|
|
586
610
|
|
|
587
611
|
// src/adapters/mcp/get-why-context.ts
|
|
612
|
+
function graphOf(indices, storage) {
|
|
613
|
+
const graph = buildGraphFromIndices(indices);
|
|
614
|
+
return graph.nodeCount > 0 ? graph : buildGraphFromStorage(storage);
|
|
615
|
+
}
|
|
588
616
|
var InputSchema2 = z2.object({
|
|
589
617
|
symbolId: z2.string().min(1),
|
|
590
618
|
maxCommits: z2.number().int().min(1).optional()
|
|
@@ -601,15 +629,15 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
|
|
|
601
629
|
content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
|
|
602
630
|
};
|
|
603
631
|
}
|
|
604
|
-
const { symbolId, maxCommits } = parsed.data;
|
|
605
|
-
const
|
|
606
|
-
|
|
632
|
+
const { symbolId: inputId, maxCommits } = parsed.data;
|
|
633
|
+
const indices = indexReader.readAll();
|
|
634
|
+
const symbolId = storage.getSymbolById(inputId) ? inputId : graphOf(indices, storage).getNode(inputId)?.symbolId;
|
|
635
|
+
if (!symbolId) {
|
|
607
636
|
return {
|
|
608
637
|
content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
|
|
609
638
|
};
|
|
610
639
|
}
|
|
611
640
|
const filePath = symbolId.split("::")[0];
|
|
612
|
-
const indices = indexReader.readAll();
|
|
613
641
|
const fileIndex = indices.find((i) => i.file === filePath);
|
|
614
642
|
let commitHistory;
|
|
615
643
|
let antiPatterns;
|
|
@@ -645,6 +673,7 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
|
|
|
645
673
|
events: relevant,
|
|
646
674
|
confidence: drift.confidence,
|
|
647
675
|
snapshotsAvailable: drift.snapshotsAvailable,
|
|
676
|
+
distinctCommits: drift.distinctCommits,
|
|
648
677
|
stabilitySummary: drift.stability,
|
|
649
678
|
...drift.hint ? { hint: drift.hint } : {}
|
|
650
679
|
};
|
|
@@ -789,7 +818,7 @@ var InputSchema4 = z4.object({
|
|
|
789
818
|
confidence: z4.enum(["confirmed", "likely", "potential"]).optional(),
|
|
790
819
|
intent: z4.string().optional().describe('Filter results by intent keywords (e.g., "test", "adapter", "security")')
|
|
791
820
|
});
|
|
792
|
-
function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", git, clusterLabelMasker) {
|
|
821
|
+
function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", git, clusterLabelMasker, coChange) {
|
|
793
822
|
const calculator = new BlastRadiusCalculator();
|
|
794
823
|
const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
|
|
795
824
|
return async (args) => {
|
|
@@ -802,12 +831,15 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", g
|
|
|
802
831
|
}
|
|
803
832
|
const { symbolId } = parsed.data;
|
|
804
833
|
const { graph, indexedFiles } = getGraph();
|
|
805
|
-
|
|
834
|
+
const root = graph.getNode(symbolId);
|
|
835
|
+
if (!root) {
|
|
806
836
|
return {
|
|
807
837
|
content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
|
|
808
838
|
};
|
|
809
839
|
}
|
|
810
|
-
const
|
|
840
|
+
const rootId = root.symbolId;
|
|
841
|
+
const coChangeMap = coChange?.readCoChangeMap();
|
|
842
|
+
const result = calculator.calculate(graph, rootId, coChangeMap);
|
|
811
843
|
let symbols = result.impactedSymbols;
|
|
812
844
|
if (parsed.data.confidence) {
|
|
813
845
|
symbols = symbols.filter((s) => s.confidence === parsed.data.confidence);
|
|
@@ -818,7 +850,7 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", g
|
|
|
818
850
|
const potentialCount = symbols.filter((s) => s.confidence === "potential").length;
|
|
819
851
|
const rawSnapshot = storage.readCommunities();
|
|
820
852
|
const snapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
|
|
821
|
-
const clusterBreakdown = snapshot ? computeClusterBreakdown(snapshot,
|
|
853
|
+
const clusterBreakdown = snapshot ? computeClusterBreakdown(snapshot, rootId, symbols.map((s) => s.symbolId)) : void 0;
|
|
822
854
|
const body = {
|
|
823
855
|
symbolId,
|
|
824
856
|
impactScore: symbols.length,
|
|
@@ -836,6 +868,8 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", g
|
|
|
836
868
|
body.multiClusterHint = clusterBreakdown.multiClusterHint;
|
|
837
869
|
}
|
|
838
870
|
}
|
|
871
|
+
const coChangedFiles = findCoChangedFilesForBlastRadius(coChangeMap, rootId, result.impactedSymbols);
|
|
872
|
+
if (coChangedFiles.length > 0) body.coChangedFiles = coChangedFiles;
|
|
839
873
|
const stalenessMeta = git ? await buildSnapshotStaleness(storage, git) : void 0;
|
|
840
874
|
const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
|
|
841
875
|
const payload = masking.mask(JSON.stringify(wrapResponse(body, extras)));
|
|
@@ -856,10 +890,11 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", g
|
|
|
856
890
|
|
|
857
891
|
// src/adapters/mcp/get-architectural-overlay.ts
|
|
858
892
|
import { z as z5 } from "zod";
|
|
859
|
-
var OVERLAY_MODES = ["regex", "communities", "both"];
|
|
893
|
+
var OVERLAY_MODES = ["regex", "communities", "both", "contexts"];
|
|
860
894
|
var InputSchema5 = z5.object({
|
|
861
895
|
layer: z5.string().optional(),
|
|
862
|
-
mode: z5.enum(OVERLAY_MODES).optional()
|
|
896
|
+
mode: z5.enum(OVERLAY_MODES).optional(),
|
|
897
|
+
candidate: z5.number().int().positive().optional()
|
|
863
898
|
});
|
|
864
899
|
function buildCommunityOverlay(snapshot, options = {}) {
|
|
865
900
|
const previewLimit = options.memberPreviewLimit ?? 15;
|
|
@@ -904,7 +939,136 @@ function buildCommunityOverlay(snapshot, options = {}) {
|
|
|
904
939
|
commitSha: snapshot.commitSha
|
|
905
940
|
};
|
|
906
941
|
}
|
|
907
|
-
|
|
942
|
+
var HUB_COLLISION_LIMIT = 50;
|
|
943
|
+
var SUMMARY_TERM_LIMIT = 5;
|
|
944
|
+
var DETAIL_FILE_LIMIT = 15;
|
|
945
|
+
var DETAIL_COLLISION_LIMIT = 10;
|
|
946
|
+
var DETAIL_OCCURRENCE_LIMIT = 5;
|
|
947
|
+
var DETAIL_DATA_LIMIT = 15;
|
|
948
|
+
var DETAIL_ACCESS_LIMIT = 10;
|
|
949
|
+
var NO_VIEW_HINT = "No contexts view available. Run ctxo index to generate one.";
|
|
950
|
+
var SUMMARY_HINT = "Pass candidate: <id> for one candidate in full; run ctxo contexts --json for the whole view.";
|
|
951
|
+
var DETAIL_HINT = "Run ctxo contexts --json for full member sets.";
|
|
952
|
+
function labelMaskerFor(view, labelMasker) {
|
|
953
|
+
const masker = labelMasker?.isEnabled() ? labelMasker : void 0;
|
|
954
|
+
if (!masker) return (label) => label;
|
|
955
|
+
for (const label of [...new Set(view.candidates.map((c) => c.label))].sort(compareCodepoint)) masker.mask(label);
|
|
956
|
+
return (label) => masker.mask(label);
|
|
957
|
+
}
|
|
958
|
+
function collisionSummary(collision) {
|
|
959
|
+
return {
|
|
960
|
+
name: collision.name,
|
|
961
|
+
minJaccard: collision.minJaccard,
|
|
962
|
+
candidates: [...new Set(collision.occurrences.map((occurrence) => occurrence.candidate))].sort((a, b) => a - b),
|
|
963
|
+
occurrenceCount: collision.occurrences.length
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function contextsSummary(view, maskLabel) {
|
|
967
|
+
return {
|
|
968
|
+
commitSha: view.commitSha,
|
|
969
|
+
coverage: view.coverage,
|
|
970
|
+
candidates: view.candidates.map((candidate) => ({
|
|
971
|
+
id: candidate.id,
|
|
972
|
+
label: maskLabel(candidate.label),
|
|
973
|
+
fingerprint: candidate.fingerprint,
|
|
974
|
+
fileCount: candidate.fileCount,
|
|
975
|
+
cohesion: candidate.cohesion,
|
|
976
|
+
topTerms: candidate.topTerms.slice(0, SUMMARY_TERM_LIMIT),
|
|
977
|
+
...candidate.sharedTables.length > 0 ? { sharedTableCount: candidate.sharedTables.length } : {}
|
|
978
|
+
})),
|
|
979
|
+
hubs: view.hubs.slice(0, HUB_COLLISION_LIMIT).map((hub) => ({
|
|
980
|
+
file: hub.file,
|
|
981
|
+
reasons: hub.reasons,
|
|
982
|
+
...hub.candidate !== void 0 ? { candidate: hub.candidate } : {}
|
|
983
|
+
})),
|
|
984
|
+
hubsTotal: view.hubs.length,
|
|
985
|
+
collisions: view.collisions.slice(0, HUB_COLLISION_LIMIT).map(collisionSummary),
|
|
986
|
+
collisionsTotal: view.collisions.length,
|
|
987
|
+
unclustered: { count: view.unclustered.count },
|
|
988
|
+
dataResourcesTotal: view.dataResources.length,
|
|
989
|
+
sharedResourcesTotal: view.dataResources.filter((resource) => resource.accesses.length >= 2).length,
|
|
990
|
+
externalResourcesTotal: view.dataResources.filter((resource) => resource.external).length,
|
|
991
|
+
hint: SUMMARY_HINT
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function sharedTableDetail(table, id) {
|
|
995
|
+
const own = table.accesses.filter((access) => access.candidate === id);
|
|
996
|
+
const others = table.accesses.filter((access) => access.candidate !== id).slice(0, DETAIL_ACCESS_LIMIT - own.length);
|
|
997
|
+
return {
|
|
998
|
+
...table,
|
|
999
|
+
accesses: [...own, ...others].sort((a, b) => a.candidate - b.candidate),
|
|
1000
|
+
accessCount: table.accesses.length
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function ownDataResources(view, id) {
|
|
1004
|
+
const own = [];
|
|
1005
|
+
for (const resource of view.dataResources) {
|
|
1006
|
+
const access = resource.accesses.find((entry) => entry.candidate === id);
|
|
1007
|
+
if (!access) continue;
|
|
1008
|
+
own.push({
|
|
1009
|
+
name: resource.name,
|
|
1010
|
+
...resource.kind !== void 0 ? { kind: resource.kind } : {},
|
|
1011
|
+
external: resource.external,
|
|
1012
|
+
modes: access.modes,
|
|
1013
|
+
...resource.owner !== void 0 ? { owner: resource.owner } : {},
|
|
1014
|
+
writerCount: resource.writers.length
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return own;
|
|
1018
|
+
}
|
|
1019
|
+
function contextsDetail(view, id, maskLabel, extras) {
|
|
1020
|
+
const candidate = view.candidates.find((c) => c.id === id);
|
|
1021
|
+
if (!candidate) {
|
|
1022
|
+
return { found: false, hint: `No candidate ${id} in the contexts view. Call without candidate for the list.` };
|
|
1023
|
+
}
|
|
1024
|
+
const hubs = view.hubs.filter((hub) => hub.candidate === id);
|
|
1025
|
+
const collisions = view.collisions.filter(
|
|
1026
|
+
(collision) => collision.occurrences.some((occurrence) => occurrence.candidate === id)
|
|
1027
|
+
);
|
|
1028
|
+
const dataResources = ownDataResources(view, id);
|
|
1029
|
+
const shownTables = candidate.sharedTables.slice(0, DETAIL_DATA_LIMIT).map((table) => sharedTableDetail(table, id));
|
|
1030
|
+
const shownResources = dataResources.slice(0, DETAIL_DATA_LIMIT);
|
|
1031
|
+
const body = {
|
|
1032
|
+
commitSha: view.commitSha,
|
|
1033
|
+
candidate: {
|
|
1034
|
+
...candidate,
|
|
1035
|
+
label: maskLabel(candidate.label),
|
|
1036
|
+
files: candidate.files.slice(0, DETAIL_FILE_LIMIT),
|
|
1037
|
+
sharedTables: shownTables,
|
|
1038
|
+
sharedTableCount: candidate.sharedTables.length
|
|
1039
|
+
},
|
|
1040
|
+
hubs: hubs.slice(0, HUB_COLLISION_LIMIT).map((hub) => ({ file: hub.file, reasons: hub.reasons, dependents: hub.dependents, damping: hub.damping })),
|
|
1041
|
+
hubsTotal: hubs.length,
|
|
1042
|
+
collisions: collisions.slice(0, DETAIL_COLLISION_LIMIT).map((collision) => ({
|
|
1043
|
+
...collisionSummary(collision),
|
|
1044
|
+
occurrences: collision.occurrences.filter((occurrence) => occurrence.candidate === id).slice(0, DETAIL_OCCURRENCE_LIMIT).map((occurrence) => ({
|
|
1045
|
+
symbolId: occurrence.symbolId,
|
|
1046
|
+
members: occurrence.members.slice(0, DETAIL_OCCURRENCE_LIMIT),
|
|
1047
|
+
memberCount: occurrence.members.length
|
|
1048
|
+
}))
|
|
1049
|
+
})),
|
|
1050
|
+
collisionsTotal: collisions.length,
|
|
1051
|
+
dataResources: shownResources,
|
|
1052
|
+
dataResourcesTotal: dataResources.length,
|
|
1053
|
+
hint: DETAIL_HINT
|
|
1054
|
+
};
|
|
1055
|
+
fitDataToLimit(body, shownTables, shownResources, extras);
|
|
1056
|
+
return body;
|
|
1057
|
+
}
|
|
1058
|
+
function fitDataToLimit(body, tables, resources, extras) {
|
|
1059
|
+
const limit = getThreshold();
|
|
1060
|
+
const size = () => Buffer.byteLength(JSON.stringify(wrapResponse(body, extras)), "utf-8");
|
|
1061
|
+
while ((tables.length > 0 || resources.length > 0) && size() > limit) {
|
|
1062
|
+
if (resources.length >= tables.length) resources.pop();
|
|
1063
|
+
else tables.pop();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function buildContextsOverlay(view, options = {}) {
|
|
1067
|
+
if (!view) return { found: false, hint: NO_VIEW_HINT };
|
|
1068
|
+
const maskLabel = labelMaskerFor(view, options.labelMasker);
|
|
1069
|
+
return options.candidate === void 0 ? contextsSummary(view, maskLabel) : contextsDetail(view, options.candidate, maskLabel, options.extras);
|
|
1070
|
+
}
|
|
1071
|
+
function handleGetArchitecturalOverlay(storage, masking, staleness, git, clusterLabelMasker, contextsView) {
|
|
908
1072
|
const overlay = new ArchitecturalOverlay();
|
|
909
1073
|
return async (args) => {
|
|
910
1074
|
try {
|
|
@@ -935,6 +1099,15 @@ function handleGetArchitecturalOverlay(storage, masking, staleness, git, cluster
|
|
|
935
1099
|
);
|
|
936
1100
|
return { content: buildContent(payload2) };
|
|
937
1101
|
}
|
|
1102
|
+
if (mode === "contexts") {
|
|
1103
|
+
const body2 = buildContextsOverlay(contextsView?.readContextsView(), {
|
|
1104
|
+
candidate: parsed.data.candidate,
|
|
1105
|
+
labelMasker: clusterLabelMasker,
|
|
1106
|
+
extras
|
|
1107
|
+
});
|
|
1108
|
+
const payload2 = masking.mask(JSON.stringify(wrapResponse(body2, extras)));
|
|
1109
|
+
return { content: buildContent(payload2) };
|
|
1110
|
+
}
|
|
938
1111
|
const rawSnapshot = mode === "regex" ? void 0 : storage.readCommunities();
|
|
939
1112
|
const snapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
|
|
940
1113
|
const community = snapshot ? buildCommunityOverlay(snapshot) : void 0;
|
|
@@ -1036,12 +1209,18 @@ var DEFAULT_TOKEN_BUDGET = 4e3;
|
|
|
1036
1209
|
var ContextAssembler = class {
|
|
1037
1210
|
logicSlice = new LogicSliceQuery();
|
|
1038
1211
|
blastRadius = new BlastRadiusCalculator();
|
|
1039
|
-
|
|
1212
|
+
/**
|
|
1213
|
+
* `coChangeMap` is the same lookup `get_blast_radius` uses: with it, a potential dependent whose file
|
|
1214
|
+
* co-changes with the target's becomes likely. It changes the tier shown on an entry, not which entries
|
|
1215
|
+
* make the context or how they rank.
|
|
1216
|
+
*/
|
|
1217
|
+
assembleForTask(graph, symbolId, taskType, indices, tokenBudget = DEFAULT_TOKEN_BUDGET, coChangeMap) {
|
|
1040
1218
|
const target = graph.getNode(symbolId);
|
|
1041
1219
|
if (!target) return void 0;
|
|
1220
|
+
const targetId = target.symbolId;
|
|
1042
1221
|
const weights = getWeightsForTask(taskType);
|
|
1043
|
-
const slice = this.logicSlice.getLogicSlice(graph,
|
|
1044
|
-
const blastResult = this.blastRadius.calculate(graph,
|
|
1222
|
+
const slice = this.logicSlice.getLogicSlice(graph, targetId);
|
|
1223
|
+
const blastResult = this.blastRadius.calculate(graph, targetId, coChangeMap);
|
|
1045
1224
|
const candidateMap = /* @__PURE__ */ new Map();
|
|
1046
1225
|
if (slice) {
|
|
1047
1226
|
for (const dep of slice.dependencies) {
|
|
@@ -1057,10 +1236,11 @@ var ContextAssembler = class {
|
|
|
1057
1236
|
const existing = candidateMap.get(entry.symbolId);
|
|
1058
1237
|
if (existing) {
|
|
1059
1238
|
existing.signals.isDependent = true;
|
|
1239
|
+
existing.signals.dependentConfidence = entry.confidence;
|
|
1060
1240
|
} else {
|
|
1061
1241
|
candidateMap.set(entry.symbolId, {
|
|
1062
1242
|
node,
|
|
1063
|
-
signals: { isDirectDep: false, isInterfaceOrType: this.isInterfaceOrType(node.kind), isDependent: true, complexity: 0, hasAntiPattern: false }
|
|
1243
|
+
signals: { isDirectDep: false, isInterfaceOrType: this.isInterfaceOrType(node.kind), isDependent: true, dependentConfidence: entry.confidence, complexity: 0, hasAntiPattern: false }
|
|
1064
1244
|
});
|
|
1065
1245
|
}
|
|
1066
1246
|
}
|
|
@@ -1087,12 +1267,12 @@ var ContextAssembler = class {
|
|
|
1087
1267
|
totalTokens += entry.tokens;
|
|
1088
1268
|
}
|
|
1089
1269
|
const warnings = [];
|
|
1090
|
-
const targetFile =
|
|
1270
|
+
const targetFile = targetId.split("::")[0] ?? "";
|
|
1091
1271
|
const targetIndex = fileIndexMap.get(targetFile);
|
|
1092
1272
|
if (targetIndex && targetIndex.antiPatterns.length > 0) {
|
|
1093
1273
|
warnings.push(`\u26A0 Target symbol has ${targetIndex.antiPatterns.length} anti-pattern(s) in file history`);
|
|
1094
1274
|
}
|
|
1095
|
-
return { target, taskType, context, totalTokens, tokenBudget, warnings };
|
|
1275
|
+
return { target: withoutAnnotations(target), taskType, context, totalTokens, tokenBudget, warnings };
|
|
1096
1276
|
}
|
|
1097
1277
|
assembleRanked(graph, query, strategy = "combined", tokenBudget = DEFAULT_TOKEN_BUDGET) {
|
|
1098
1278
|
const allNodes = graph.allNodes();
|
|
@@ -1169,7 +1349,8 @@ var ContextAssembler = class {
|
|
|
1169
1349
|
relevanceScore: this.computeScore(signals, weights),
|
|
1170
1350
|
reason: reasons.join(", ") || "related symbol",
|
|
1171
1351
|
lines: node.endLine - node.startLine + 1,
|
|
1172
|
-
tokens: this.estimateTokens(node)
|
|
1352
|
+
tokens: this.estimateTokens(node),
|
|
1353
|
+
...signals.dependentConfidence ? { confidence: signals.dependentConfidence } : {}
|
|
1173
1354
|
};
|
|
1174
1355
|
}
|
|
1175
1356
|
isInterfaceOrType(kind) {
|
|
@@ -1185,14 +1366,7 @@ var ContextAssembler = class {
|
|
|
1185
1366
|
return (node.endLine - node.startLine + 1) * 10;
|
|
1186
1367
|
}
|
|
1187
1368
|
};
|
|
1188
|
-
var VALID_SYMBOL_KINDS =
|
|
1189
|
-
"function",
|
|
1190
|
-
"class",
|
|
1191
|
-
"interface",
|
|
1192
|
-
"method",
|
|
1193
|
-
"variable",
|
|
1194
|
-
"type"
|
|
1195
|
-
]);
|
|
1369
|
+
var VALID_SYMBOL_KINDS = new Set(SYMBOL_KINDS);
|
|
1196
1370
|
function synthesizeNodeFromId(symbolId) {
|
|
1197
1371
|
const parts = symbolId.split("::");
|
|
1198
1372
|
if (parts.length !== 3) return void 0;
|
|
@@ -1209,7 +1383,7 @@ var InputSchema7 = z7.object({
|
|
|
1209
1383
|
taskType: TaskTypeSchema,
|
|
1210
1384
|
tokenBudget: z7.number().min(100).optional().default(4e3)
|
|
1211
1385
|
});
|
|
1212
|
-
function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo") {
|
|
1386
|
+
function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo", coChange) {
|
|
1213
1387
|
const assembler = new ContextAssembler();
|
|
1214
1388
|
const indexReader = new JsonIndexReader(ctxoRoot);
|
|
1215
1389
|
const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
|
|
@@ -1224,7 +1398,7 @@ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
1224
1398
|
const { symbolId, taskType, tokenBudget } = parsed.data;
|
|
1225
1399
|
const { graph, indexedFiles } = getGraph();
|
|
1226
1400
|
const indices = indexReader.readAll();
|
|
1227
|
-
const result = assembler.assembleForTask(graph, symbolId, taskType, indices, tokenBudget);
|
|
1401
|
+
const result = assembler.assembleForTask(graph, symbolId, taskType, indices, tokenBudget, coChange?.readCoChangeMap());
|
|
1228
1402
|
if (!result) {
|
|
1229
1403
|
return {
|
|
1230
1404
|
content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index".' }) }]
|
|
@@ -1249,179 +1423,6 @@ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
1249
1423
|
// src/adapters/mcp/get-ranked-context.ts
|
|
1250
1424
|
import { z as z8 } from "zod";
|
|
1251
1425
|
|
|
1252
|
-
// src/core/search/symbol-tokenizer.ts
|
|
1253
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1254
|
-
"get",
|
|
1255
|
-
"set",
|
|
1256
|
-
"is",
|
|
1257
|
-
"has",
|
|
1258
|
-
"can",
|
|
1259
|
-
"do",
|
|
1260
|
-
"the",
|
|
1261
|
-
"a",
|
|
1262
|
-
"an",
|
|
1263
|
-
"of",
|
|
1264
|
-
"to",
|
|
1265
|
-
"in",
|
|
1266
|
-
"for",
|
|
1267
|
-
"on",
|
|
1268
|
-
"with",
|
|
1269
|
-
"at",
|
|
1270
|
-
"by",
|
|
1271
|
-
"from",
|
|
1272
|
-
"as",
|
|
1273
|
-
"into",
|
|
1274
|
-
"not",
|
|
1275
|
-
"no",
|
|
1276
|
-
"or"
|
|
1277
|
-
]);
|
|
1278
|
-
var DEFAULT_OPTIONS = {
|
|
1279
|
-
includeStopWords: false,
|
|
1280
|
-
includeOriginal: true,
|
|
1281
|
-
includeFilePath: false
|
|
1282
|
-
};
|
|
1283
|
-
var SymbolTokenizer = class _SymbolTokenizer {
|
|
1284
|
-
options;
|
|
1285
|
-
constructor(options) {
|
|
1286
|
-
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Tokenize a symbol name into searchable tokens.
|
|
1290
|
-
*
|
|
1291
|
-
* Examples:
|
|
1292
|
-
* "getCoChangeMetrics" → ["get", "co", "change", "metrics"] (or without stop words: ["co", "change", "metrics"])
|
|
1293
|
-
* "SqliteStorageAdapter" → ["sqlite", "storage", "adapter"]
|
|
1294
|
-
* "i_storage_port" → ["i", "storage", "port"]
|
|
1295
|
-
* "BM25Scorer" → ["bm", "25", "scorer"]
|
|
1296
|
-
* "DB_PASSWORD" → ["db", "password"]
|
|
1297
|
-
*/
|
|
1298
|
-
tokenize(symbolName, filePath) {
|
|
1299
|
-
const splitTokens = _SymbolTokenizer.splitName(symbolName);
|
|
1300
|
-
const lowered = splitTokens.map((t) => t.toLowerCase());
|
|
1301
|
-
const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
|
|
1302
|
-
if (this.options.includeOriginal) {
|
|
1303
|
-
const original = symbolName.toLowerCase();
|
|
1304
|
-
if (!tokens.includes(original)) {
|
|
1305
|
-
tokens.unshift(original);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
if (this.options.includeFilePath && filePath) {
|
|
1309
|
-
const pathTokens = _SymbolTokenizer.tokenizeFilePath(filePath);
|
|
1310
|
-
for (const pt of pathTokens) {
|
|
1311
|
-
if (!tokens.includes(pt)) {
|
|
1312
|
-
tokens.push(pt);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
return tokens;
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
|
-
* Tokenize a search query. Queries use stop words and don't include originals.
|
|
1320
|
-
*/
|
|
1321
|
-
tokenizeQuery(query) {
|
|
1322
|
-
const words = query.trim().split(/\s+/);
|
|
1323
|
-
const tokens = [];
|
|
1324
|
-
for (const word of words) {
|
|
1325
|
-
const split = _SymbolTokenizer.splitName(word);
|
|
1326
|
-
for (const t of split) {
|
|
1327
|
-
const lowered = t.toLowerCase();
|
|
1328
|
-
if (lowered.length > 0 && !tokens.includes(lowered)) {
|
|
1329
|
-
tokens.push(lowered);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
return tokens;
|
|
1334
|
-
}
|
|
1335
|
-
/**
|
|
1336
|
-
* Split a name into constituent words based on naming convention boundaries.
|
|
1337
|
-
*
|
|
1338
|
-
* Rules:
|
|
1339
|
-
* 1. Split on underscores and hyphens (snake_case, kebab-case)
|
|
1340
|
-
* 2. Split on camelCase/PascalCase boundaries (uppercase after lowercase)
|
|
1341
|
-
* 3. Split acronym boundaries (e.g., "HTMLParser" → ["HTML", "Parser"])
|
|
1342
|
-
* 4. Split on digit ↔ letter boundaries
|
|
1343
|
-
*/
|
|
1344
|
-
static splitName(name) {
|
|
1345
|
-
if (!name || name.length === 0) return [];
|
|
1346
|
-
const segments = name.split(/[_\-./\\]+/).filter((s) => s.length > 0);
|
|
1347
|
-
const tokens = [];
|
|
1348
|
-
for (const segment of segments) {
|
|
1349
|
-
tokens.push(..._SymbolTokenizer.splitCamelCase(segment));
|
|
1350
|
-
}
|
|
1351
|
-
return tokens.filter((t) => t.length > 0);
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Split a single segment on camelCase/PascalCase/digit boundaries.
|
|
1355
|
-
*/
|
|
1356
|
-
static splitCamelCase(segment) {
|
|
1357
|
-
const tokens = [];
|
|
1358
|
-
let current = "";
|
|
1359
|
-
for (let i = 0; i < segment.length; i++) {
|
|
1360
|
-
const ch = segment[i];
|
|
1361
|
-
const prev = i > 0 ? segment[i - 1] : "";
|
|
1362
|
-
if (current.length > 0) {
|
|
1363
|
-
const prevIsDigit = isDigit(prev);
|
|
1364
|
-
const currIsDigit = isDigit(ch);
|
|
1365
|
-
if (prevIsDigit !== currIsDigit) {
|
|
1366
|
-
tokens.push(current);
|
|
1367
|
-
current = ch;
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
if (current.length > 0 && isLower(prev) && isUpper(ch)) {
|
|
1372
|
-
tokens.push(current);
|
|
1373
|
-
current = ch;
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
|
|
1377
|
-
const lastChar = current[current.length - 1];
|
|
1378
|
-
if (isUpper(lastChar)) {
|
|
1379
|
-
tokens.push(current.slice(0, -1));
|
|
1380
|
-
current = lastChar + ch;
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
current += ch;
|
|
1385
|
-
}
|
|
1386
|
-
if (current.length > 0) {
|
|
1387
|
-
tokens.push(current);
|
|
1388
|
-
}
|
|
1389
|
-
return tokens;
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Extract searchable tokens from a file path.
|
|
1393
|
-
* "src/core/search/symbol-tokenizer.ts" → ["src", "core", "search", "symbol", "tokenizer", "ts"]
|
|
1394
|
-
*/
|
|
1395
|
-
static tokenizeFilePath(filePath) {
|
|
1396
|
-
const withoutExt = filePath.replace(/\.[^.]+$/, "");
|
|
1397
|
-
const segments = withoutExt.split(/[/\\]+/);
|
|
1398
|
-
const tokens = [];
|
|
1399
|
-
for (const seg of segments) {
|
|
1400
|
-
const parts = seg.split(/[_\-.]+/).filter((s) => s.length > 0);
|
|
1401
|
-
for (const p of parts) {
|
|
1402
|
-
const lowered = p.toLowerCase();
|
|
1403
|
-
if (lowered.length > 0 && !tokens.includes(lowered)) {
|
|
1404
|
-
tokens.push(lowered);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
return tokens;
|
|
1409
|
-
}
|
|
1410
|
-
/** Check if a token is a stop word */
|
|
1411
|
-
static isStopWord(token) {
|
|
1412
|
-
return STOP_WORDS.has(token.toLowerCase());
|
|
1413
|
-
}
|
|
1414
|
-
};
|
|
1415
|
-
function isUpper(ch) {
|
|
1416
|
-
return ch >= "A" && ch <= "Z";
|
|
1417
|
-
}
|
|
1418
|
-
function isLower(ch) {
|
|
1419
|
-
return ch >= "a" && ch <= "z";
|
|
1420
|
-
}
|
|
1421
|
-
function isDigit(ch) {
|
|
1422
|
-
return ch >= "0" && ch <= "9";
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
1426
|
// src/core/search/fuzzy-corrector.ts
|
|
1426
1427
|
var FuzzyCorrector = class {
|
|
1427
1428
|
/** term → frequency (for tie-breaking) */
|
|
@@ -1805,7 +1806,6 @@ var InputSchema8 = z8.object({
|
|
|
1805
1806
|
query: z8.string().min(1),
|
|
1806
1807
|
tokenBudget: z8.number().min(100).optional().default(4e3),
|
|
1807
1808
|
strategy: z8.enum(["combined", "dependency", "importance"]).optional().default("combined"),
|
|
1808
|
-
fuzzy: z8.boolean().optional().default(true),
|
|
1809
1809
|
searchMode: z8.enum(["fts", "legacy"]).optional().default("fts")
|
|
1810
1810
|
});
|
|
1811
1811
|
function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo", searchEngine) {
|
|
@@ -1898,7 +1898,7 @@ function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
1898
1898
|
import { z as z9 } from "zod";
|
|
1899
1899
|
var InputSchema9 = z9.object({
|
|
1900
1900
|
pattern: z9.string().min(1),
|
|
1901
|
-
kind:
|
|
1901
|
+
kind: SymbolKindSchema.optional(),
|
|
1902
1902
|
filePattern: z9.string().optional(),
|
|
1903
1903
|
limit: z9.number().int().min(1).max(100).optional().default(25),
|
|
1904
1904
|
mode: z9.enum(["regex", "fts"]).optional().default("regex")
|
|
@@ -2062,7 +2062,7 @@ function handleGetChangedSymbols(storage, git, masking, staleness, ctxoRoot = ".
|
|
|
2062
2062
|
import { z as z11 } from "zod";
|
|
2063
2063
|
var InputSchema11 = z11.object({
|
|
2064
2064
|
symbolId: z11.string().min(1),
|
|
2065
|
-
edgeKinds: z11.array(
|
|
2065
|
+
edgeKinds: z11.array(EdgeKindSchema).optional(),
|
|
2066
2066
|
transitive: z11.boolean().optional().default(false),
|
|
2067
2067
|
maxDepth: z11.number().int().min(1).max(10).optional().default(5),
|
|
2068
2068
|
intent: z11.string().optional().describe('Filter results by intent keywords (e.g., "test", "adapter", "core")')
|
|
@@ -2079,14 +2079,16 @@ function handleFindImporters(storage, masking, staleness, ctxoRoot = ".ctxo") {
|
|
|
2079
2079
|
}
|
|
2080
2080
|
const { symbolId, edgeKinds, transitive, maxDepth } = parsed.data;
|
|
2081
2081
|
const { graph, indexedFiles } = getGraph();
|
|
2082
|
-
|
|
2082
|
+
const root = graph.getNode(symbolId);
|
|
2083
|
+
if (!root) {
|
|
2083
2084
|
return {
|
|
2084
2085
|
content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
|
|
2085
2086
|
};
|
|
2086
2087
|
}
|
|
2088
|
+
const rootId = root.symbolId;
|
|
2087
2089
|
const importers = [];
|
|
2088
2090
|
if (!transitive) {
|
|
2089
|
-
const reverseEdges = graph.
|
|
2091
|
+
const reverseEdges = graph.getDependentEdges(rootId);
|
|
2090
2092
|
const seen = /* @__PURE__ */ new Map();
|
|
2091
2093
|
for (const edge of reverseEdges) {
|
|
2092
2094
|
if (edgeKinds && !edgeKinds.includes(edge.kind)) continue;
|
|
@@ -2113,12 +2115,13 @@ function handleFindImporters(storage, masking, staleness, ctxoRoot = ".ctxo") {
|
|
|
2113
2115
|
}
|
|
2114
2116
|
}
|
|
2115
2117
|
} else {
|
|
2116
|
-
const
|
|
2117
|
-
const
|
|
2118
|
+
const definitions = graph.dataResourceIdentity(rootId)?.definitions.map((definition) => definition.symbolId) ?? [];
|
|
2119
|
+
const visited = /* @__PURE__ */ new Set([rootId, ...definitions]);
|
|
2120
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
2118
2121
|
while (queue.length > 0) {
|
|
2119
2122
|
const current = queue.shift();
|
|
2120
2123
|
if (current.depth >= maxDepth) continue;
|
|
2121
|
-
const reverseEdges = graph.
|
|
2124
|
+
const reverseEdges = graph.getDependentEdges(current.id);
|
|
2122
2125
|
for (const edge of reverseEdges) {
|
|
2123
2126
|
if (edgeKinds && !edgeKinds.includes(edge.kind)) continue;
|
|
2124
2127
|
if (visited.has(edge.from)) continue;
|
|
@@ -2163,6 +2166,7 @@ function handleFindImporters(storage, masking, staleness, ctxoRoot = ".ctxo") {
|
|
|
2163
2166
|
|
|
2164
2167
|
// src/adapters/mcp/get-class-hierarchy.ts
|
|
2165
2168
|
import { z as z12 } from "zod";
|
|
2169
|
+
var HIERARCHY_EDGE_KINDS = /* @__PURE__ */ new Set(["extends", "implements"]);
|
|
2166
2170
|
var InputSchema12 = z12.object({
|
|
2167
2171
|
symbolId: z12.string().min(1).optional(),
|
|
2168
2172
|
direction: z12.enum(["ancestors", "descendants", "both"]).optional().default("both")
|
|
@@ -2197,7 +2201,7 @@ function handleGetClassHierarchy(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
2197
2201
|
}
|
|
2198
2202
|
const ancestors = [];
|
|
2199
2203
|
const descendants = [];
|
|
2200
|
-
const isHierarchyEdge = (kind) => kind
|
|
2204
|
+
const isHierarchyEdge = (kind) => HIERARCHY_EDGE_KINDS.has(kind);
|
|
2201
2205
|
if (direction === "ancestors" || direction === "both") {
|
|
2202
2206
|
const visited = /* @__PURE__ */ new Set([symbolId]);
|
|
2203
2207
|
const queue = [{ id: symbolId, depth: 0 }];
|
|
@@ -2261,7 +2265,7 @@ function handleGetClassHierarchy(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
2261
2265
|
return { content };
|
|
2262
2266
|
}
|
|
2263
2267
|
function handleFull(graph, maskingPort, stalenessCheck, indexedFiles) {
|
|
2264
|
-
const isHierarchyEdge = (kind) => kind
|
|
2268
|
+
const isHierarchyEdge = (kind) => HIERARCHY_EDGE_KINDS.has(kind);
|
|
2265
2269
|
const hierarchyEdges = graph.allEdges().filter((e) => isHierarchyEdge(e.kind));
|
|
2266
2270
|
const involved = /* @__PURE__ */ new Set();
|
|
2267
2271
|
for (const edge of hierarchyEdges) {
|
|
@@ -2319,7 +2323,7 @@ function handleGetClassHierarchy(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
2319
2323
|
import { z as z13 } from "zod";
|
|
2320
2324
|
var InputSchema13 = z13.object({
|
|
2321
2325
|
limit: z13.number().int().min(1).max(200).optional().default(25),
|
|
2322
|
-
kind:
|
|
2326
|
+
kind: SymbolKindSchema.optional(),
|
|
2323
2327
|
filePattern: z13.string().optional(),
|
|
2324
2328
|
damping: z13.number().min(0).max(1).optional().default(0.85)
|
|
2325
2329
|
});
|
|
@@ -2370,23 +2374,11 @@ function handleGetSymbolImportance(storage, masking, staleness, ctxoRoot = ".ctx
|
|
|
2370
2374
|
|
|
2371
2375
|
// src/adapters/mcp/get-pr-impact.ts
|
|
2372
2376
|
import { z as z14 } from "zod";
|
|
2373
|
-
import { readFileSync, existsSync } from "fs";
|
|
2374
|
-
import { join } from "path";
|
|
2375
2377
|
var InputSchema14 = z14.object({
|
|
2376
2378
|
since: z14.string().optional().default("HEAD~1"),
|
|
2377
2379
|
maxFiles: z14.number().int().min(1).optional().default(50),
|
|
2378
2380
|
confidence: z14.enum(["confirmed", "likely", "potential"]).optional()
|
|
2379
2381
|
});
|
|
2380
|
-
function loadCoChanges(ctxoRoot) {
|
|
2381
|
-
const path = join(ctxoRoot, "index", "co-changes.json");
|
|
2382
|
-
if (!existsSync(path)) return void 0;
|
|
2383
|
-
try {
|
|
2384
|
-
const matrix = JSON.parse(readFileSync(path, "utf-8"));
|
|
2385
|
-
return loadCoChangeMap(matrix);
|
|
2386
|
-
} catch {
|
|
2387
|
-
return void 0;
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
2382
|
function computeClustersAffected(snapshot, changedSymbolIds) {
|
|
2391
2383
|
const totals = /* @__PURE__ */ new Map();
|
|
2392
2384
|
for (const entry of snapshot.communities) {
|
|
@@ -2400,7 +2392,7 @@ function computeClustersAffected(snapshot, changedSymbolIds) {
|
|
|
2400
2392
|
}
|
|
2401
2393
|
return [...totals.entries()].map(([id, { label, count }]) => ({ id, label, symbolCount: count })).sort((a, b) => b.symbolCount - a.symbolCount || a.id - b.id);
|
|
2402
2394
|
}
|
|
2403
|
-
function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo", clusterLabelMasker) {
|
|
2395
|
+
function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo", clusterLabelMasker, coChange) {
|
|
2404
2396
|
const calculator = new BlastRadiusCalculator();
|
|
2405
2397
|
const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
|
|
2406
2398
|
return async (args) => {
|
|
@@ -2419,7 +2411,7 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
2419
2411
|
};
|
|
2420
2412
|
}
|
|
2421
2413
|
const { graph, indexedFiles } = getGraph();
|
|
2422
|
-
const coChangeMap =
|
|
2414
|
+
const coChangeMap = coChange?.readCoChangeMap();
|
|
2423
2415
|
const allNodes = graph.allNodes();
|
|
2424
2416
|
const nodesByFile = /* @__PURE__ */ new Map();
|
|
2425
2417
|
for (const node of allNodes) {
|
|
@@ -2478,7 +2470,7 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
2478
2470
|
if (coChangeMap) {
|
|
2479
2471
|
const entries = coChangeMap.get(filePath);
|
|
2480
2472
|
if (entries && entries.length > 0) {
|
|
2481
|
-
coChangedWith = entries.sort((a, b) => b.frequency - a.frequency).slice(0, 5).map((e) => e.file1 === filePath ? e.file2 : e.file1);
|
|
2473
|
+
coChangedWith = [...entries].sort((a, b) => b.frequency - a.frequency).slice(0, 5).map((e) => e.file1 === filePath ? e.file2 : e.file1);
|
|
2482
2474
|
}
|
|
2483
2475
|
}
|
|
2484
2476
|
files.push({
|
|
@@ -2512,6 +2504,7 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
2512
2504
|
violations: relevant,
|
|
2513
2505
|
confidence: violationResult.confidence,
|
|
2514
2506
|
snapshotsAvailable: violationResult.snapshotsAvailable,
|
|
2507
|
+
distinctCommits: violationResult.distinctCommits,
|
|
2515
2508
|
...violationResult.hint ? { hint: violationResult.hint } : {}
|
|
2516
2509
|
};
|
|
2517
2510
|
clustersAffected = computeClustersAffected(snapshot, changedSymbolIds);
|
|
@@ -2633,10 +2626,10 @@ var ClusterLabelMasker = class {
|
|
|
2633
2626
|
// src/index.ts
|
|
2634
2627
|
var log4 = createLogger("ctxo:mcp");
|
|
2635
2628
|
function loadMaskingConfig(ctxoRoot) {
|
|
2636
|
-
const jsonConfigPath =
|
|
2637
|
-
if (
|
|
2629
|
+
const jsonConfigPath = join(ctxoRoot, "masking.json");
|
|
2630
|
+
if (existsSync(jsonConfigPath)) {
|
|
2638
2631
|
try {
|
|
2639
|
-
const raw =
|
|
2632
|
+
const raw = readFileSync(jsonConfigPath, "utf-8");
|
|
2640
2633
|
const patterns = JSON.parse(raw);
|
|
2641
2634
|
log4.info(`Loaded ${patterns.length} custom masking pattern(s)`);
|
|
2642
2635
|
return MaskingPipeline.fromConfig(patterns);
|
|
@@ -2653,7 +2646,7 @@ var PKG_VERSION = (() => {
|
|
|
2653
2646
|
try {
|
|
2654
2647
|
const __filename = fileURLToPath(import.meta.url);
|
|
2655
2648
|
const __dirname = dirname(__filename);
|
|
2656
|
-
const pkg = JSON.parse(
|
|
2649
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
2657
2650
|
return pkg.version ?? "0.0.0";
|
|
2658
2651
|
} catch {
|
|
2659
2652
|
return "0.0.0";
|
|
@@ -2661,9 +2654,9 @@ var PKG_VERSION = (() => {
|
|
|
2661
2654
|
})();
|
|
2662
2655
|
async function main() {
|
|
2663
2656
|
const args = process.argv.slice(2);
|
|
2664
|
-
const httpPortStr =
|
|
2657
|
+
const httpPortStr = resolveHttpPort(args, process.env);
|
|
2665
2658
|
if (!httpPortStr && args.length > 0) {
|
|
2666
|
-
const { CliRouter } = await import("./cli-router-
|
|
2659
|
+
const { CliRouter } = await import("./cli-router-RR4CM62C.js");
|
|
2667
2660
|
const router = new CliRouter(process.cwd());
|
|
2668
2661
|
await router.route(args);
|
|
2669
2662
|
return;
|
|
@@ -2678,15 +2671,16 @@ async function main() {
|
|
|
2678
2671
|
maskingClusterLabelPatterns(loadConfig(ctxoRoot).config)
|
|
2679
2672
|
);
|
|
2680
2673
|
const git = new SimpleGitAdapter(process.cwd());
|
|
2674
|
+
const coChange = new CoChangeReader(ctxoRoot);
|
|
2681
2675
|
const recorder = readStatsEnabled(ctxoRoot) ? new SessionRecorderAdapter(storage.getDb(), () => storage.persist()) : null;
|
|
2682
2676
|
const server = new McpServer({ name: "ctxo", version: PKG_VERSION });
|
|
2683
2677
|
const { StalenessDetector } = await import("./staleness-detector-4HJYBQ7Z.js");
|
|
2684
2678
|
const staleness = new StalenessDetector(process.cwd(), ctxoRoot);
|
|
2685
|
-
registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker);
|
|
2679
|
+
registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker, coChange);
|
|
2686
2680
|
if (httpPortStr) {
|
|
2687
2681
|
await startHttpTransport(async () => {
|
|
2688
2682
|
const s = new McpServer({ name: "ctxo", version: PKG_VERSION });
|
|
2689
|
-
registerTools(s, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker);
|
|
2683
|
+
registerTools(s, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker, coChange);
|
|
2690
2684
|
return s;
|
|
2691
2685
|
}, parseInt(httpPortStr, 10));
|
|
2692
2686
|
} else {
|
|
@@ -2694,24 +2688,25 @@ async function main() {
|
|
|
2694
2688
|
await server.connect(transport);
|
|
2695
2689
|
}
|
|
2696
2690
|
}
|
|
2697
|
-
function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo", clusterLabelMasker) {
|
|
2691
|
+
function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo", clusterLabelMasker, coChange) {
|
|
2698
2692
|
const toolAnnotations = {
|
|
2699
2693
|
readOnlyHint: true,
|
|
2700
2694
|
destructiveHint: false,
|
|
2701
2695
|
idempotentHint: true,
|
|
2702
2696
|
openWorldHint: false
|
|
2703
2697
|
};
|
|
2698
|
+
const SYMBOL_ID_FORMAT = "format: file::name::kind; a SQL table, view or procedure also as name::kind, for example dbo.loans::table";
|
|
2704
2699
|
const logicSliceHandler = withRecording("get_logic_slice", handleGetLogicSlice(storage, masking, staleness, ctxoRoot), recorder);
|
|
2705
2700
|
const whyContextHandler = withRecording("get_why_context", handleGetWhyContext(storage, git, masking, staleness, ctxoRoot, clusterLabelMasker), recorder);
|
|
2706
2701
|
const changeIntelligenceHandler = withRecording("get_change_intelligence", handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot), recorder);
|
|
2707
2702
|
server.registerTool(
|
|
2708
2703
|
"get_logic_slice",
|
|
2709
2704
|
{
|
|
2710
|
-
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.',
|
|
2705
|
+
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 and symbol annotations (attributes, columns; only L4 includes them). 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.',
|
|
2711
2706
|
inputSchema: {
|
|
2712
|
-
symbolId: z15.string().optional().describe(
|
|
2707
|
+
symbolId: z15.string().optional().describe(`Single symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2713
2708
|
symbolIds: z15.array(z15.string()).optional().describe("Batch: array of symbol IDs"),
|
|
2714
|
-
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)"),
|
|
2709
|
+
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 and annotations)"),
|
|
2715
2710
|
intent: z15.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter")')
|
|
2716
2711
|
},
|
|
2717
2712
|
annotations: toolAnnotations
|
|
@@ -2723,7 +2718,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2723
2718
|
{
|
|
2724
2719
|
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.",
|
|
2725
2720
|
inputSchema: {
|
|
2726
|
-
symbolId: z15.string().min(1).describe(
|
|
2721
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2727
2722
|
maxCommits: z15.number().int().min(1).optional().describe("Limit commit history to N most recent commits")
|
|
2728
2723
|
},
|
|
2729
2724
|
annotations: toolAnnotations
|
|
@@ -2741,13 +2736,13 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2741
2736
|
},
|
|
2742
2737
|
(args) => changeIntelligenceHandler(args)
|
|
2743
2738
|
);
|
|
2744
|
-
const blastRadiusHandler = withRecording("get_blast_radius", handleGetBlastRadius(storage, masking, staleness, ctxoRoot, git, clusterLabelMasker), recorder);
|
|
2739
|
+
const blastRadiusHandler = withRecording("get_blast_radius", handleGetBlastRadius(storage, masking, staleness, ctxoRoot, git, clusterLabelMasker, coChange), recorder);
|
|
2745
2740
|
server.registerTool(
|
|
2746
2741
|
"get_blast_radius",
|
|
2747
2742
|
{
|
|
2748
|
-
description: "BEFORE modifying any function or
|
|
2743
|
+
description: "BEFORE modifying any symbol (a function, class, method or type, or a SQL table, view or procedure), call this to understand impact. Returns every symbol that depends on the target, directly or transitively, with risk scores that fall with depth. Each dependent is in the tier of its strongest edge: confirmed (`calls`, `extends`, `implements` and the data edges `reads`, `writes`, `executes`), likely (co-changed or `uses`), and potential (`imports`). When co-change history exists, `coChangedFiles` lists up to 10 files that often change together with the target's file but have no dependency path to it (informational: not counted in the tiers or 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.",
|
|
2749
2744
|
inputSchema: {
|
|
2750
|
-
symbolId: z15.string().min(1).describe(
|
|
2745
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT}). Every definition of a SQL object, and its name::kind id, has the same dependents`),
|
|
2751
2746
|
confidence: z15.enum(["confirmed", "likely", "potential"]).optional().describe("Filter by confidence tier"),
|
|
2752
2747
|
intent: z15.string().optional().describe('Filter impacted symbols by intent keywords (e.g., "test", "adapter")')
|
|
2753
2748
|
},
|
|
@@ -2755,13 +2750,15 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2755
2750
|
},
|
|
2756
2751
|
(args) => blastRadiusHandler(args)
|
|
2757
2752
|
);
|
|
2758
|
-
const overlayHandler = withRecording("get_architectural_overlay", handleGetArchitecturalOverlay(storage, masking, staleness, git, clusterLabelMasker), recorder);
|
|
2753
|
+
const overlayHandler = withRecording("get_architectural_overlay", handleGetArchitecturalOverlay(storage, masking, staleness, git, clusterLabelMasker, new ContextsViewReader(ctxoRoot)), recorder);
|
|
2759
2754
|
server.registerTool(
|
|
2760
2755
|
"get_architectural_overlay",
|
|
2761
2756
|
{
|
|
2762
|
-
description:
|
|
2757
|
+
description: 'Get the project architecture map. mode "both" (default) returns the regex layer map (Domain, Infrastructure, Adapter, Test, Composition, Configuration) plus data-driven Louvain communities; "regex" or "communities" returns one of them. mode "contexts" returns candidate bounded contexts computed by `ctxo index`: file-level clusters of dependencies and co-change with hub files damped. The summary lists every candidate with label, size, cohesion and top terms, plus hubs and name collisions (type names whose shapes differ between candidates). Use this when onboarding to a new codebase, validating layer boundaries, or preparing a context map. For symbol-level analysis, use get_logic_slice or get_blast_radius. Pass candidate: <id> for one candidate in full: its files, cross-candidate edges, co-change pairs, hubs and collisions. The summary also gives each candidate\'s sharedTableCount and the totals dataResourcesTotal, sharedResourcesTotal and externalResourcesTotal. With candidate: <id>, the detail adds sharedTables (SQL tables and views that files of this candidate and at least one other read or write, with access modes per candidate; up to the first 15 by name, each with up to 10 accesses) and the candidate\'s own dataResources (up to the first 15 by name, each with its access modes, writer count, the owning candidate when only one writes it, and an external flag for resources that no .sql file defines). Both lists hold fewer entries when the response limit requires; sharedTableCount and dataResourcesTotal keep the full counts, and ctxo contexts --json has the full lists.',
|
|
2763
2758
|
inputSchema: {
|
|
2764
|
-
layer: z15.string().optional().describe("Filter by specific layer name")
|
|
2759
|
+
layer: z15.string().optional().describe("Filter by specific layer name"),
|
|
2760
|
+
mode: z15.enum(OVERLAY_MODES).optional().describe('View to return: "both" (default) layers plus communities, "regex" layer map, "communities" Louvain clusters, "contexts" candidate contexts (file level)'),
|
|
2761
|
+
candidate: z15.number().int().positive().optional().describe('With mode "contexts": the id of one candidate to return in full (files, terms, cross edges, co-change pairs, its hubs and collisions); omit for the summary of all candidates')
|
|
2765
2762
|
},
|
|
2766
2763
|
annotations: toolAnnotations
|
|
2767
2764
|
},
|
|
@@ -2771,7 +2768,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2771
2768
|
server.registerTool(
|
|
2772
2769
|
"find_dead_code",
|
|
2773
2770
|
{
|
|
2774
|
-
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.',
|
|
2771
|
+
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. Symbols in .sql files are never reported. 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.',
|
|
2775
2772
|
inputSchema: {
|
|
2776
2773
|
includeTests: z15.boolean().optional().default(false).describe("Include test files in analysis (default: exclude)"),
|
|
2777
2774
|
intent: z15.string().optional().describe('Filter dead code results by intent keywords (e.g., "adapter", "function")')
|
|
@@ -2780,13 +2777,13 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2780
2777
|
},
|
|
2781
2778
|
(args) => deadCodeHandler(args)
|
|
2782
2779
|
);
|
|
2783
|
-
const contextForTaskHandler = withRecording("get_context_for_task", handleGetContextForTask(storage, masking, staleness, ctxoRoot), recorder);
|
|
2780
|
+
const contextForTaskHandler = withRecording("get_context_for_task", handleGetContextForTask(storage, masking, staleness, ctxoRoot, coChange), recorder);
|
|
2784
2781
|
server.registerTool(
|
|
2785
2782
|
"get_context_for_task",
|
|
2786
2783
|
{
|
|
2787
|
-
description: 'Get task-optimized context for a symbol based on what you are about to do. Specify taskType: "fix" (bug investigation
|
|
2784
|
+
description: 'Get task-optimized context for a symbol based on what you are about to do. Specify taskType: "fix" (bug investigation - includes history + anti-patterns), "extend" (add feature - includes deps + blast radius), "refactor" (restructure - includes importers + complexity), or "understand" (learn - includes full slice + architecture). Context entries that depend on the symbol (its blast radius) carry an optional `confidence` tier: confirmed, likely or potential, where co-change history can raise potential to likely. This is the BEST starting point when you know both the symbol and your intent.',
|
|
2788
2785
|
inputSchema: {
|
|
2789
|
-
symbolId: z15.string().min(1).describe(
|
|
2786
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2790
2787
|
taskType: z15.enum(["fix", "extend", "refactor", "understand"]).describe("Task type determines which context is most relevant"),
|
|
2791
2788
|
tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for context (default 4000)")
|
|
2792
2789
|
},
|
|
@@ -2802,7 +2799,8 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2802
2799
|
inputSchema: {
|
|
2803
2800
|
query: z15.string().min(1).describe("Search query (matches symbol names)"),
|
|
2804
2801
|
tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for results (default 4000)"),
|
|
2805
|
-
strategy: z15.enum(["combined", "dependency", "importance"]).optional().default("combined").describe("Ranking strategy")
|
|
2802
|
+
strategy: z15.enum(["combined", "dependency", "importance"]).optional().default("combined").describe("Ranking strategy"),
|
|
2803
|
+
searchMode: z15.enum(["fts", "legacy"]).optional().default("fts").describe('"fts" (default): BM25 ranking with camelCase splitting, trigram fallback and typo correction; "legacy": substring matching; strategy "importance" always uses legacy matching')
|
|
2806
2804
|
},
|
|
2807
2805
|
annotations: toolAnnotations
|
|
2808
2806
|
},
|
|
@@ -2815,9 +2813,10 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2815
2813
|
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.",
|
|
2816
2814
|
inputSchema: {
|
|
2817
2815
|
pattern: z15.string().min(1).describe("Search pattern (substring or regex)"),
|
|
2818
|
-
kind:
|
|
2816
|
+
kind: SymbolKindSchema.optional().describe("Filter by symbol kind (table, view and procedure come from SQL plugins)"),
|
|
2819
2817
|
filePattern: z15.string().optional().describe("Filter by file path substring"),
|
|
2820
|
-
limit: z15.number().int().min(1).max(100).optional().default(25).describe("Max results (default 25)")
|
|
2818
|
+
limit: z15.number().int().min(1).max(100).optional().default(25).describe("Max results (default 25)"),
|
|
2819
|
+
mode: z15.enum(["regex", "fts"]).optional().default("regex").describe('"regex" (default): case-insensitive regex or substring match on names; "fts": BM25 relevance search')
|
|
2821
2820
|
},
|
|
2822
2821
|
annotations: toolAnnotations
|
|
2823
2822
|
},
|
|
@@ -2842,8 +2841,8 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2842
2841
|
{
|
|
2843
2842
|
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.',
|
|
2844
2843
|
inputSchema: {
|
|
2845
|
-
symbolId: z15.string().min(1).describe(
|
|
2846
|
-
edgeKinds: z15.array(
|
|
2844
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT}). Every definition of a SQL object, and its name::kind id, has the same importers`),
|
|
2845
|
+
edgeKinds: z15.array(EdgeKindSchema).optional().describe("Filter by edge kinds (reads, writes and executes are data access)"),
|
|
2847
2846
|
transitive: z15.boolean().optional().default(false).describe("Follow transitive reverse edges (default false)"),
|
|
2848
2847
|
maxDepth: z15.number().int().min(1).max(10).optional().default(5).describe("Max BFS depth for transitive mode (default 5)"),
|
|
2849
2848
|
intent: z15.string().optional().describe('Filter importers by intent keywords (e.g., "test", "core", "adapter")')
|
|
@@ -2872,7 +2871,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2872
2871
|
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.",
|
|
2873
2872
|
inputSchema: {
|
|
2874
2873
|
limit: z15.number().int().min(1).max(200).optional().default(25).describe("Max results (default 25)"),
|
|
2875
|
-
kind:
|
|
2874
|
+
kind: SymbolKindSchema.optional().describe("Filter by symbol kind (table, view and procedure come from SQL plugins)"),
|
|
2876
2875
|
filePattern: z15.string().optional().describe("Filter by file path substring"),
|
|
2877
2876
|
damping: z15.number().min(0).max(1).optional().default(0.85).describe("PageRank damping factor (default 0.85)")
|
|
2878
2877
|
},
|
|
@@ -2880,7 +2879,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2880
2879
|
},
|
|
2881
2880
|
(args) => symbolImportanceHandler(args)
|
|
2882
2881
|
);
|
|
2883
|
-
const prImpactHandler = withRecording("get_pr_impact", handleGetPrImpact(storage, git, masking, staleness, ctxoRoot, clusterLabelMasker), recorder);
|
|
2882
|
+
const prImpactHandler = withRecording("get_pr_impact", handleGetPrImpact(storage, git, masking, staleness, ctxoRoot, clusterLabelMasker, coChange), recorder);
|
|
2884
2883
|
server.registerTool(
|
|
2885
2884
|
"get_pr_impact",
|
|
2886
2885
|
{
|