@ctxo/cli 0.14.0 → 0.15.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 +10 -1
- package/dist/{chunk-ZL36YS2V.js → chunk-A4URKKPP.js} +3 -177
- package/dist/{chunk-SRQZDGFZ.js → chunk-BBSJB4CG.js} +2 -2
- package/dist/{chunk-X4TVFJ4C.js → chunk-EC6INGPV.js} +1 -1
- package/dist/{chunk-7HSA5JVP.js → chunk-I7RPXT6L.js} +193 -1
- package/dist/chunk-IT426P3F.js +1036 -0
- package/dist/{chunk-G4BMKCKG.js → chunk-JDL6GGTO.js} +16 -5
- package/dist/{cli-router-AZCDNX5R.js → cli-router-W56INOQ3.js} +10 -10
- package/dist/index.js +3 -3
- package/dist/internal.d.ts +90 -3
- package/dist/internal.js +4 -4
- package/dist/{json-index-reader-DBW32TCN.js → json-index-reader-DKEIP2QR.js} +2 -2
- package/dist/{mcp-server-6YG2QE6E.js → mcp-server-IYCCE74E.js} +178 -446
- package/dist/module-host-KTGDODES.js +13 -0
- package/llms-full.txt +68 -3
- package/llms.txt +14 -2
- package/package.json +4 -3
- package/dist/atomic-write-BAFH464Q.js.map +0 -1
- package/dist/chunk-2Q76LH2H.js.map +0 -1
- package/dist/chunk-5DSKZJNN.js.map +0 -1
- package/dist/chunk-72CEQEQD.js.map +0 -1
- package/dist/chunk-7HSA5JVP.js.map +0 -1
- package/dist/chunk-G4BMKCKG.js.map +0 -1
- package/dist/chunk-L2CAUER6.js +0 -508
- package/dist/chunk-L2CAUER6.js.map +0 -1
- package/dist/chunk-LAX6VVBU.js.map +0 -1
- package/dist/chunk-PSKDDLSF.js.map +0 -1
- package/dist/chunk-RFEXMBTG.js.map +0 -1
- package/dist/chunk-SRQZDGFZ.js.map +0 -1
- package/dist/chunk-X4TVFJ4C.js.map +0 -1
- package/dist/chunk-ZL36YS2V.js.map +0 -1
- package/dist/cli-router-AZCDNX5R.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/json-index-reader-DBW32TCN.js.map +0 -1
- package/dist/mcp-server-6YG2QE6E.js.map +0 -1
- package/dist/module-host-KNMKS62R.js +0 -13
- package/dist/module-host-KNMKS62R.js.map +0 -1
- package/dist/module-loader-B3LYF3W7.js.map +0 -1
- package/dist/plugin-discovery-XRXI3IJF.js.map +0 -1
- package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
|
@@ -11,19 +11,21 @@ import {
|
|
|
11
11
|
RevertDetector,
|
|
12
12
|
SessionRecorderAdapter,
|
|
13
13
|
SqliteStorageAdapter,
|
|
14
|
-
SymbolTokenizer,
|
|
15
14
|
detectWorkspace,
|
|
16
15
|
findCoChangedFilesForBlastRadius
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-A4URKKPP.js";
|
|
18
17
|
import {
|
|
19
18
|
loadModulesFor
|
|
20
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-BBSJB4CG.js";
|
|
21
20
|
import {
|
|
22
21
|
LogicSliceQuery,
|
|
22
|
+
SearchEngine,
|
|
23
23
|
applyEnricher,
|
|
24
|
+
applyEnricherAsync,
|
|
24
25
|
buildGraphFromIndices,
|
|
25
26
|
buildGraphFromStorage,
|
|
26
27
|
collectEnrichers,
|
|
28
|
+
collectTaskContextReservation,
|
|
27
29
|
describeError,
|
|
28
30
|
filterByIntent,
|
|
29
31
|
getGraphAndFiles,
|
|
@@ -32,14 +34,14 @@ import {
|
|
|
32
34
|
setWorkspaceMeta,
|
|
33
35
|
withoutAnnotations,
|
|
34
36
|
wrapResponse
|
|
35
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-IT426P3F.js";
|
|
36
38
|
import {
|
|
37
39
|
SimpleGitAdapter,
|
|
38
40
|
loadConfig,
|
|
39
41
|
makeGlobMatcher,
|
|
40
42
|
maskingClusterLabelPatterns,
|
|
41
43
|
statsEnabled
|
|
42
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-I7RPXT6L.js";
|
|
43
45
|
import {
|
|
44
46
|
compareCodepoint
|
|
45
47
|
} from "./chunk-72CEQEQD.js";
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
JsonIndexReader,
|
|
49
51
|
SYMBOL_KINDS,
|
|
50
52
|
SymbolKindSchema
|
|
51
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-EC6INGPV.js";
|
|
52
54
|
import "./chunk-RFEXMBTG.js";
|
|
53
55
|
import {
|
|
54
56
|
createLogger
|
|
@@ -130,7 +132,7 @@ async function startHttpTransport(serverFactory, port) {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// src/mcp-server.ts
|
|
133
|
-
import { z as
|
|
135
|
+
import { z as z15 } from "zod";
|
|
134
136
|
import { existsSync, readFileSync } from "fs";
|
|
135
137
|
import { join, dirname } from "path";
|
|
136
138
|
import { fileURLToPath } from "url";
|
|
@@ -299,7 +301,7 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
|
|
|
299
301
|
}
|
|
300
302
|
const stalenessMeta = await buildSnapshotStaleness(storage, git);
|
|
301
303
|
const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
|
|
302
|
-
const enriched =
|
|
304
|
+
const enriched = await applyEnricherAsync(enrich, responsePayload, parsed.data, "get_why_context");
|
|
303
305
|
const payload = masking.mask(JSON.stringify(wrapResponse(enriched, extras)));
|
|
304
306
|
const content = [];
|
|
305
307
|
if (staleness) {
|
|
@@ -489,7 +491,7 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", g
|
|
|
489
491
|
if (coChangedFiles.length > 0) body.coChangedFiles = coChangedFiles;
|
|
490
492
|
const stalenessMeta = git ? await buildSnapshotStaleness(storage, git) : void 0;
|
|
491
493
|
const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
|
|
492
|
-
const enriched =
|
|
494
|
+
const enriched = await applyEnricherAsync(enrich, body, parsed.data, "get_blast_radius");
|
|
493
495
|
const payload = masking.mask(JSON.stringify(wrapResponse(enriched, extras)));
|
|
494
496
|
const content = [];
|
|
495
497
|
if (staleness) {
|
|
@@ -995,13 +997,30 @@ function synthesizeNodeFromId(symbolId) {
|
|
|
995
997
|
}
|
|
996
998
|
|
|
997
999
|
// src/adapters/mcp/get-context-for-task.ts
|
|
1000
|
+
var log2 = createLogger("ctxo:modules");
|
|
998
1001
|
var TaskTypeSchema = z6.enum(["fix", "extend", "refactor", "understand"]);
|
|
999
1002
|
var InputSchema6 = z6.object({
|
|
1000
1003
|
symbolId: z6.string().min(1),
|
|
1001
1004
|
taskType: TaskTypeSchema,
|
|
1002
1005
|
tokenBudget: z6.number().min(100).optional().default(4e3)
|
|
1003
1006
|
});
|
|
1004
|
-
|
|
1007
|
+
var MAX_RESERVED_SHARE = 0.25;
|
|
1008
|
+
function reservedTokens(reserve, args, tokenBudget) {
|
|
1009
|
+
if (reserve === void 0) return 0;
|
|
1010
|
+
let value;
|
|
1011
|
+
try {
|
|
1012
|
+
value = reserve(args);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
log2.warn(`The get_context_for_task token reservation failed: ${describeError(err)}; nothing is reserved`);
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
1017
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1018
|
+
log2.warn("The get_context_for_task token reservation is not a finite number; nothing is reserved");
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
return Math.min(Math.max(Math.round(value), 0), Math.floor(tokenBudget * MAX_RESERVED_SHARE));
|
|
1022
|
+
}
|
|
1023
|
+
function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo", coChange, enrich, reserve) {
|
|
1005
1024
|
const assembler = new ContextAssembler();
|
|
1006
1025
|
const indexReader = new JsonIndexReader(ctxoRoot);
|
|
1007
1026
|
const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
|
|
@@ -1016,13 +1035,23 @@ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
1016
1035
|
const { symbolId, taskType, tokenBudget } = parsed.data;
|
|
1017
1036
|
const { graph, indexedFiles } = getGraph();
|
|
1018
1037
|
const indices = indexReader.readAll();
|
|
1019
|
-
const
|
|
1038
|
+
const reserved = reservedTokens(reserve, parsed.data, tokenBudget);
|
|
1039
|
+
const result = assembler.assembleForTask(
|
|
1040
|
+
graph,
|
|
1041
|
+
symbolId,
|
|
1042
|
+
taskType,
|
|
1043
|
+
indices,
|
|
1044
|
+
tokenBudget - reserved,
|
|
1045
|
+
coChange?.readCoChangeMap()
|
|
1046
|
+
);
|
|
1020
1047
|
if (!result) {
|
|
1021
1048
|
return {
|
|
1022
1049
|
content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index".' }) }]
|
|
1023
1050
|
};
|
|
1024
1051
|
}
|
|
1025
|
-
const
|
|
1052
|
+
const body = { ...result, tokenBudget };
|
|
1053
|
+
const enriched = applyEnricher(enrich, body, parsed.data, "get_context_for_task");
|
|
1054
|
+
const payload = masking.mask(JSON.stringify(wrapResponse(enriched)));
|
|
1026
1055
|
const content = [];
|
|
1027
1056
|
if (staleness) {
|
|
1028
1057
|
const warning = staleness.check(indexedFiles);
|
|
@@ -1040,385 +1069,6 @@ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo"
|
|
|
1040
1069
|
|
|
1041
1070
|
// src/adapters/mcp/get-ranked-context.ts
|
|
1042
1071
|
import { z as z7 } from "zod";
|
|
1043
|
-
|
|
1044
|
-
// src/core/search/fuzzy-corrector.ts
|
|
1045
|
-
var FuzzyCorrector = class {
|
|
1046
|
-
/** term → frequency (for tie-breaking) */
|
|
1047
|
-
vocabulary = /* @__PURE__ */ new Map();
|
|
1048
|
-
buildVocabulary(termFrequencies) {
|
|
1049
|
-
this.vocabulary = new Map(termFrequencies);
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Attempt to correct query tokens.
|
|
1053
|
-
* Returns null if no correction found or no improvement possible.
|
|
1054
|
-
*/
|
|
1055
|
-
correct(queryTokens) {
|
|
1056
|
-
if (this.vocabulary.size === 0) return null;
|
|
1057
|
-
const corrections = [];
|
|
1058
|
-
const correctedTokens = [];
|
|
1059
|
-
let anyChanged = false;
|
|
1060
|
-
for (const token of queryTokens) {
|
|
1061
|
-
if (token.length <= 2) {
|
|
1062
|
-
correctedTokens.push(token);
|
|
1063
|
-
continue;
|
|
1064
|
-
}
|
|
1065
|
-
if (this.vocabulary.has(token)) {
|
|
1066
|
-
correctedTokens.push(token);
|
|
1067
|
-
continue;
|
|
1068
|
-
}
|
|
1069
|
-
const maxDist = token.length <= 5 ? 1 : 2;
|
|
1070
|
-
const best = this.findClosest(token, maxDist);
|
|
1071
|
-
if (best) {
|
|
1072
|
-
corrections.push({
|
|
1073
|
-
original: token,
|
|
1074
|
-
corrected: best.term,
|
|
1075
|
-
distance: best.distance
|
|
1076
|
-
});
|
|
1077
|
-
correctedTokens.push(best.term);
|
|
1078
|
-
anyChanged = true;
|
|
1079
|
-
} else {
|
|
1080
|
-
correctedTokens.push(token);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
if (!anyChanged) return null;
|
|
1084
|
-
return {
|
|
1085
|
-
originalQuery: queryTokens.join(" "),
|
|
1086
|
-
correctedQuery: correctedTokens.join(" "),
|
|
1087
|
-
corrections
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Find the closest vocabulary term within maxDistance.
|
|
1092
|
-
* Tie-breaks by frequency (higher = preferred).
|
|
1093
|
-
*/
|
|
1094
|
-
findClosest(token, maxDistance) {
|
|
1095
|
-
let bestTerm = null;
|
|
1096
|
-
let bestDist = maxDistance + 1;
|
|
1097
|
-
let bestFreq = -1;
|
|
1098
|
-
for (const [vocabTerm, freq] of this.vocabulary) {
|
|
1099
|
-
if (Math.abs(vocabTerm.length - token.length) > maxDistance) continue;
|
|
1100
|
-
const dist = damerauLevenshtein(token, vocabTerm);
|
|
1101
|
-
if (dist <= maxDistance) {
|
|
1102
|
-
if (dist < bestDist || dist === bestDist && freq > bestFreq) {
|
|
1103
|
-
bestTerm = vocabTerm;
|
|
1104
|
-
bestDist = dist;
|
|
1105
|
-
bestFreq = freq;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return bestTerm !== null ? { term: bestTerm, distance: bestDist } : null;
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
function damerauLevenshtein(a, b) {
|
|
1113
|
-
const lenA = a.length;
|
|
1114
|
-
const lenB = b.length;
|
|
1115
|
-
if (lenA === 0) return lenB;
|
|
1116
|
-
if (lenB === 0) return lenA;
|
|
1117
|
-
const d = Array.from({ length: lenA + 1 }, () => new Array(lenB + 1).fill(0));
|
|
1118
|
-
for (let i = 0; i <= lenA; i++) d[i][0] = i;
|
|
1119
|
-
for (let j = 0; j <= lenB; j++) d[0][j] = j;
|
|
1120
|
-
for (let i = 1; i <= lenA; i++) {
|
|
1121
|
-
for (let j = 1; j <= lenB; j++) {
|
|
1122
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1123
|
-
d[i][j] = Math.min(
|
|
1124
|
-
d[i - 1][j] + 1,
|
|
1125
|
-
// deletion
|
|
1126
|
-
d[i][j - 1] + 1,
|
|
1127
|
-
// insertion
|
|
1128
|
-
d[i - 1][j - 1] + cost
|
|
1129
|
-
// substitution
|
|
1130
|
-
);
|
|
1131
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
1132
|
-
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
return d[lenA][lenB];
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// src/core/search/search-engine.ts
|
|
1140
|
-
var log2 = createLogger("ctxo:search");
|
|
1141
|
-
var DEFAULT_CONFIG = {
|
|
1142
|
-
bm25: { k1: 1.2, b: 0.25 },
|
|
1143
|
-
pageRankWeight: 0.5,
|
|
1144
|
-
trigramPenalty: 0.8,
|
|
1145
|
-
phase2Threshold: 3,
|
|
1146
|
-
fuzzyThreshold: 3,
|
|
1147
|
-
maxPerPhase: 50
|
|
1148
|
-
};
|
|
1149
|
-
var SearchEngine = class {
|
|
1150
|
-
config;
|
|
1151
|
-
tokenizer;
|
|
1152
|
-
fuzzy;
|
|
1153
|
-
// Primary index (exact + tokenized)
|
|
1154
|
-
documents = [];
|
|
1155
|
-
primaryIndex = /* @__PURE__ */ new Map();
|
|
1156
|
-
avgDocLength = 0;
|
|
1157
|
-
// Trigram index (character 3-grams)
|
|
1158
|
-
trigramIndex = /* @__PURE__ */ new Map();
|
|
1159
|
-
// PageRank scores
|
|
1160
|
-
pageRankScores = /* @__PURE__ */ new Map();
|
|
1161
|
-
constructor(config) {
|
|
1162
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1163
|
-
this.tokenizer = new SymbolTokenizer({ includeOriginal: true, includeFilePath: true });
|
|
1164
|
-
this.fuzzy = new FuzzyCorrector();
|
|
1165
|
-
}
|
|
1166
|
-
getTier() {
|
|
1167
|
-
return "in-memory";
|
|
1168
|
-
}
|
|
1169
|
-
buildIndex(symbols, pageRankScores) {
|
|
1170
|
-
const start = performance.now();
|
|
1171
|
-
this.documents = [];
|
|
1172
|
-
this.primaryIndex = /* @__PURE__ */ new Map();
|
|
1173
|
-
this.trigramIndex = /* @__PURE__ */ new Map();
|
|
1174
|
-
this.pageRankScores = pageRankScores ?? /* @__PURE__ */ new Map();
|
|
1175
|
-
let totalLength = 0;
|
|
1176
|
-
const vocabFreq = /* @__PURE__ */ new Map();
|
|
1177
|
-
for (const sym of symbols) {
|
|
1178
|
-
const filePath = sym.symbolId.split("::")[0] ?? "";
|
|
1179
|
-
const tokens = this.tokenizer.tokenize(sym.name, filePath);
|
|
1180
|
-
const doc = {
|
|
1181
|
-
symbolId: sym.symbolId,
|
|
1182
|
-
name: sym.name,
|
|
1183
|
-
kind: sym.kind,
|
|
1184
|
-
filePath,
|
|
1185
|
-
tokens,
|
|
1186
|
-
length: tokens.length
|
|
1187
|
-
};
|
|
1188
|
-
const docIndex = this.documents.length;
|
|
1189
|
-
this.documents.push(doc);
|
|
1190
|
-
totalLength += tokens.length;
|
|
1191
|
-
const termFreqs = /* @__PURE__ */ new Map();
|
|
1192
|
-
for (const token of tokens) {
|
|
1193
|
-
termFreqs.set(token, (termFreqs.get(token) ?? 0) + 1);
|
|
1194
|
-
vocabFreq.set(token, (vocabFreq.get(token) ?? 0) + 1);
|
|
1195
|
-
}
|
|
1196
|
-
for (const [term, tf] of termFreqs) {
|
|
1197
|
-
let postings = this.primaryIndex.get(term);
|
|
1198
|
-
if (!postings) {
|
|
1199
|
-
postings = [];
|
|
1200
|
-
this.primaryIndex.set(term, postings);
|
|
1201
|
-
}
|
|
1202
|
-
postings.push({ docIndex, termFrequency: tf });
|
|
1203
|
-
}
|
|
1204
|
-
const nameLower = sym.name.toLowerCase();
|
|
1205
|
-
const trigrams = this.extractTrigrams(nameLower);
|
|
1206
|
-
const trigramFreqs = /* @__PURE__ */ new Map();
|
|
1207
|
-
for (const tri of trigrams) {
|
|
1208
|
-
trigramFreqs.set(tri, (trigramFreqs.get(tri) ?? 0) + 1);
|
|
1209
|
-
}
|
|
1210
|
-
for (const [tri, tf] of trigramFreqs) {
|
|
1211
|
-
let postings = this.trigramIndex.get(tri);
|
|
1212
|
-
if (!postings) {
|
|
1213
|
-
postings = [];
|
|
1214
|
-
this.trigramIndex.set(tri, postings);
|
|
1215
|
-
}
|
|
1216
|
-
postings.push({ docIndex, termFrequency: tf });
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
this.avgDocLength = this.documents.length > 0 ? totalLength / this.documents.length : 1;
|
|
1220
|
-
this.fuzzy.buildVocabulary(vocabFreq);
|
|
1221
|
-
const elapsed = performance.now() - start;
|
|
1222
|
-
log2.info(`Index built: ${symbols.length} symbols, ${this.primaryIndex.size} terms, ${this.trigramIndex.size} trigrams (${elapsed.toFixed(0)}ms)`);
|
|
1223
|
-
}
|
|
1224
|
-
search(query, limit = 50) {
|
|
1225
|
-
const start = performance.now();
|
|
1226
|
-
const queryTokens = this.tokenizer.tokenizeQuery(query);
|
|
1227
|
-
if (queryTokens.length === 0) {
|
|
1228
|
-
return this.emptyResponse(query, start);
|
|
1229
|
-
}
|
|
1230
|
-
const metrics = {
|
|
1231
|
-
porterHits: 0,
|
|
1232
|
-
trigramHits: 0,
|
|
1233
|
-
phase2Activated: false,
|
|
1234
|
-
fuzzyApplied: false,
|
|
1235
|
-
latencyMs: 0
|
|
1236
|
-
};
|
|
1237
|
-
const phase1Scores = this.bm25Search(queryTokens, this.primaryIndex);
|
|
1238
|
-
metrics.porterHits = phase1Scores.size;
|
|
1239
|
-
let allScores = phase1Scores;
|
|
1240
|
-
if (phase1Scores.size < this.config.phase2Threshold) {
|
|
1241
|
-
metrics.phase2Activated = true;
|
|
1242
|
-
const queryLower = query.toLowerCase();
|
|
1243
|
-
if (queryLower.length >= 3) {
|
|
1244
|
-
const queryTrigrams = this.extractTrigrams(queryLower);
|
|
1245
|
-
const phase2Scores = this.bm25Search(queryTrigrams, this.trigramIndex);
|
|
1246
|
-
metrics.trigramHits = phase2Scores.size;
|
|
1247
|
-
allScores = this.mergeScores(phase1Scores, phase2Scores, this.config.trigramPenalty);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
let fuzzyCorrection;
|
|
1251
|
-
if (allScores.size < this.config.fuzzyThreshold) {
|
|
1252
|
-
const correction = this.fuzzy.correct(queryTokens);
|
|
1253
|
-
if (correction) {
|
|
1254
|
-
metrics.fuzzyApplied = true;
|
|
1255
|
-
fuzzyCorrection = correction;
|
|
1256
|
-
const correctedTokens = this.tokenizer.tokenizeQuery(correction.correctedQuery);
|
|
1257
|
-
const fuzzyScores = this.bm25Search(correctedTokens, this.primaryIndex);
|
|
1258
|
-
allScores = this.mergeScores(allScores, fuzzyScores, 0.9);
|
|
1259
|
-
if (allScores.size < this.config.fuzzyThreshold && correction.correctedQuery.length >= 3) {
|
|
1260
|
-
const correctedTrigrams = this.extractTrigrams(correction.correctedQuery.toLowerCase());
|
|
1261
|
-
const fuzzyTrigramScores = this.bm25Search(correctedTrigrams, this.trigramIndex);
|
|
1262
|
-
allScores = this.mergeScores(allScores, fuzzyTrigramScores, this.config.trigramPenalty * 0.9);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
const rawRelevance = new Map(allScores);
|
|
1267
|
-
const effectiveQueryTerms = fuzzyCorrection ? this.tokenizer.tokenizeQuery(fuzzyCorrection.correctedQuery) : queryTokens;
|
|
1268
|
-
if (effectiveQueryTerms.length >= 2) {
|
|
1269
|
-
for (const [docIdx, score] of allScores) {
|
|
1270
|
-
const doc = this.documents[docIdx];
|
|
1271
|
-
const boost = this.bigramBoost(effectiveQueryTerms, doc.tokens);
|
|
1272
|
-
allScores.set(docIdx, score * boost);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
for (const [docIdx, score] of allScores) {
|
|
1276
|
-
const doc = this.documents[docIdx];
|
|
1277
|
-
const pr = this.pageRankScores.get(doc.symbolId) ?? 0;
|
|
1278
|
-
allScores.set(docIdx, score * (1 + this.config.pageRankWeight * pr));
|
|
1279
|
-
}
|
|
1280
|
-
metrics.latencyMs = performance.now() - start;
|
|
1281
|
-
const results = this.buildResults(allScores, rawRelevance, limit);
|
|
1282
|
-
const phaseInfo = metrics.phase2Activated ? "phase2=yes" : "phase2=no";
|
|
1283
|
-
if (fuzzyCorrection) {
|
|
1284
|
-
log2.info(`search "${query}" \u2192 fuzzy "${fuzzyCorrection.correctedQuery}": ${results.length} results (${metrics.latencyMs.toFixed(0)}ms)`);
|
|
1285
|
-
} else {
|
|
1286
|
-
log2.info(`search "${query}": ${results.length} results, porter=${metrics.porterHits} trigram=${metrics.trigramHits} ${phaseInfo} (${metrics.latencyMs.toFixed(0)}ms)`);
|
|
1287
|
-
}
|
|
1288
|
-
return { query, results, metrics, ...fuzzyCorrection ? { fuzzyCorrection } : {} };
|
|
1289
|
-
}
|
|
1290
|
-
updateFile(filePath, symbols) {
|
|
1291
|
-
const oldIndices = /* @__PURE__ */ new Set();
|
|
1292
|
-
for (let i = 0; i < this.documents.length; i++) {
|
|
1293
|
-
if (this.documents[i].filePath === filePath) {
|
|
1294
|
-
oldIndices.add(i);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
if (oldIndices.size === 0 && symbols.length === 0) return;
|
|
1298
|
-
const allSymbols = [];
|
|
1299
|
-
for (let i = 0; i < this.documents.length; i++) {
|
|
1300
|
-
if (!oldIndices.has(i)) {
|
|
1301
|
-
const doc = this.documents[i];
|
|
1302
|
-
allSymbols.push({
|
|
1303
|
-
symbolId: doc.symbolId,
|
|
1304
|
-
name: doc.name,
|
|
1305
|
-
kind: doc.kind,
|
|
1306
|
-
startLine: 0,
|
|
1307
|
-
endLine: 0
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
allSymbols.push(...symbols);
|
|
1312
|
-
this.buildIndex(allSymbols, this.pageRankScores);
|
|
1313
|
-
}
|
|
1314
|
-
/**
|
|
1315
|
-
* BM25 scoring over an inverted index.
|
|
1316
|
-
* Returns Map<docIndex, score>
|
|
1317
|
-
*/
|
|
1318
|
-
bm25Search(queryTerms, index) {
|
|
1319
|
-
const scores = /* @__PURE__ */ new Map();
|
|
1320
|
-
const N = this.documents.length;
|
|
1321
|
-
const { k1, b } = this.config.bm25;
|
|
1322
|
-
for (const term of queryTerms) {
|
|
1323
|
-
const postings = index.get(term);
|
|
1324
|
-
if (!postings) continue;
|
|
1325
|
-
const df = postings.length;
|
|
1326
|
-
const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
|
|
1327
|
-
for (const { docIndex, termFrequency } of postings) {
|
|
1328
|
-
const doc = this.documents[docIndex];
|
|
1329
|
-
const tf = termFrequency;
|
|
1330
|
-
const docLen = doc.length;
|
|
1331
|
-
const numerator = tf * (k1 + 1);
|
|
1332
|
-
const denominator = tf + k1 * (1 - b + b * (docLen / this.avgDocLength));
|
|
1333
|
-
const termScore = idf * (numerator / denominator);
|
|
1334
|
-
scores.set(docIndex, (scores.get(docIndex) ?? 0) + termScore);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
const queryJoined = queryTerms.join("");
|
|
1338
|
-
for (const [docIdx, score] of scores) {
|
|
1339
|
-
const doc = this.documents[docIdx];
|
|
1340
|
-
const nameLower = doc.name.toLowerCase();
|
|
1341
|
-
if (nameLower === queryJoined || nameLower === queryTerms.join(" ")) {
|
|
1342
|
-
scores.set(docIdx, score * 5);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return scores;
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* Merge two score maps. Phase 2 scores are penalized.
|
|
1349
|
-
*/
|
|
1350
|
-
mergeScores(primary, secondary, penalty) {
|
|
1351
|
-
const merged = new Map(primary);
|
|
1352
|
-
for (const [docIdx, score] of secondary) {
|
|
1353
|
-
const existing = merged.get(docIdx);
|
|
1354
|
-
const penalized = score * penalty;
|
|
1355
|
-
if (existing !== void 0) {
|
|
1356
|
-
merged.set(docIdx, Math.max(existing, penalized));
|
|
1357
|
-
} else {
|
|
1358
|
-
merged.set(docIdx, penalized);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return merged;
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Bigram boost for multi-word queries.
|
|
1365
|
-
* Each adjacent query term pair found adjacent in symbol tokens → 2x boost.
|
|
1366
|
-
*/
|
|
1367
|
-
bigramBoost(queryTerms, symbolTokens) {
|
|
1368
|
-
if (queryTerms.length < 2) return 1;
|
|
1369
|
-
let adjacentPairs = 0;
|
|
1370
|
-
for (let i = 0; i < queryTerms.length - 1; i++) {
|
|
1371
|
-
const idxA = symbolTokens.findIndex((t) => t.includes(queryTerms[i]));
|
|
1372
|
-
const idxB = symbolTokens.findIndex((t) => t.includes(queryTerms[i + 1]));
|
|
1373
|
-
if (idxA >= 0 && idxB >= 0 && Math.abs(idxA - idxB) === 1) {
|
|
1374
|
-
adjacentPairs++;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
return 1 + adjacentPairs * 2;
|
|
1378
|
-
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Extract character trigrams from a string.
|
|
1381
|
-
* "sqlite" → ["sql", "qli", "lit", "ite"]
|
|
1382
|
-
*/
|
|
1383
|
-
extractTrigrams(text) {
|
|
1384
|
-
const trigrams = [];
|
|
1385
|
-
for (let i = 0; i <= text.length - 3; i++) {
|
|
1386
|
-
trigrams.push(text.substring(i, i + 3));
|
|
1387
|
-
}
|
|
1388
|
-
return trigrams;
|
|
1389
|
-
}
|
|
1390
|
-
buildResults(combinedScores, rawRelevance, limit) {
|
|
1391
|
-
const entries = [...combinedScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1392
|
-
return entries.map(([docIdx, combined]) => {
|
|
1393
|
-
const doc = this.documents[docIdx];
|
|
1394
|
-
const pr = this.pageRankScores.get(doc.symbolId) ?? 0;
|
|
1395
|
-
return {
|
|
1396
|
-
symbolId: doc.symbolId,
|
|
1397
|
-
name: doc.name,
|
|
1398
|
-
kind: doc.kind,
|
|
1399
|
-
filePath: doc.filePath,
|
|
1400
|
-
relevanceScore: rawRelevance.get(docIdx) ?? 0,
|
|
1401
|
-
importanceScore: pr,
|
|
1402
|
-
combinedScore: combined
|
|
1403
|
-
};
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
emptyResponse(query, start) {
|
|
1407
|
-
return {
|
|
1408
|
-
query,
|
|
1409
|
-
results: [],
|
|
1410
|
-
metrics: {
|
|
1411
|
-
porterHits: 0,
|
|
1412
|
-
trigramHits: 0,
|
|
1413
|
-
phase2Activated: false,
|
|
1414
|
-
fuzzyApplied: false,
|
|
1415
|
-
latencyMs: performance.now() - start
|
|
1416
|
-
}
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
|
|
1421
|
-
// src/adapters/mcp/get-ranked-context.ts
|
|
1422
1072
|
var log3 = createLogger("ctxo:search");
|
|
1423
1073
|
var InputSchema7 = z7.object({
|
|
1424
1074
|
query: z7.string().min(1),
|
|
@@ -2024,8 +1674,10 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
2024
1674
|
const { since, maxFiles, confidence: confFilter } = parsed.data;
|
|
2025
1675
|
const changedPaths = await git.getChangedFiles(since);
|
|
2026
1676
|
if (changedPaths.length === 0) {
|
|
1677
|
+
const empty = { since, changedFiles: 0, changedSymbols: 0, totalImpact: 0, riskLevel: "low", files: [], summary: { confirmedTotal: 0, likelyTotal: 0, potentialTotal: 0, highRiskSymbols: [] } };
|
|
1678
|
+
const enrichedEmpty = await applyEnricherAsync(enrich, empty, parsed.data, "get_pr_impact");
|
|
2027
1679
|
return {
|
|
2028
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
1680
|
+
content: [{ type: "text", text: masking.mask(JSON.stringify(wrapResponse(enrichedEmpty))) }]
|
|
2029
1681
|
};
|
|
2030
1682
|
}
|
|
2031
1683
|
const { graph, indexedFiles } = getGraph();
|
|
@@ -2145,7 +1797,7 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo",
|
|
|
2145
1797
|
if (clustersAffected && clustersAffected.length > 0) body.clustersAffected = clustersAffected;
|
|
2146
1798
|
const stalenessMeta = await buildSnapshotStaleness(storage, git);
|
|
2147
1799
|
const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
|
|
2148
|
-
const enriched =
|
|
1800
|
+
const enriched = await applyEnricherAsync(enrich, body, parsed.data, "get_pr_impact");
|
|
2149
1801
|
const payload = masking.mask(JSON.stringify(wrapResponse(enriched, extras)));
|
|
2150
1802
|
const content = [];
|
|
2151
1803
|
if (staleness) {
|
|
@@ -2282,6 +1934,80 @@ function handleModuleTool(tool, masking, staleness, indexedFiles) {
|
|
|
2282
1934
|
};
|
|
2283
1935
|
}
|
|
2284
1936
|
|
|
1937
|
+
// src/adapters/mcp/ranked-scope-handler.ts
|
|
1938
|
+
import { z as z14 } from "zod";
|
|
1939
|
+
var log5 = createLogger("ctxo:modules");
|
|
1940
|
+
var CODE_SCOPE = "code";
|
|
1941
|
+
var ScopeInputSchema = z14.object({
|
|
1942
|
+
query: z14.string().min(1),
|
|
1943
|
+
tokenBudget: z14.number().min(100).optional().default(4e3)
|
|
1944
|
+
});
|
|
1945
|
+
function collectRankedScopes(modules) {
|
|
1946
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1947
|
+
for (const module of modules) {
|
|
1948
|
+
let list;
|
|
1949
|
+
try {
|
|
1950
|
+
list = module.registration.rankedContextScopes;
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
log5.warn(`${module.name} ranked-context scopes could not be read: ${describeError(err)}`);
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (list === void 0) continue;
|
|
1956
|
+
if (!Array.isArray(list)) {
|
|
1957
|
+
log5.warn(`${module.name} rankedContextScopes is not a list; it was ignored`);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
for (const entry of list) {
|
|
1961
|
+
const candidate = entry;
|
|
1962
|
+
const name = candidate?.scope;
|
|
1963
|
+
if (typeof name !== "string" || name.length === 0 || typeof candidate?.description !== "string" || typeof candidate.search !== "function") {
|
|
1964
|
+
log5.warn(`${module.name} registered a ranked-context scope without a name, a description or a search; it was ignored`);
|
|
1965
|
+
} else if (name === CODE_SCOPE) {
|
|
1966
|
+
log5.warn(`${module.name} scope "${name}" is skipped: reserved`);
|
|
1967
|
+
} else if (scopes.has(name)) {
|
|
1968
|
+
log5.warn(`${module.name} scope "${name}" is skipped: already registered`);
|
|
1969
|
+
} else {
|
|
1970
|
+
scopes.set(name, candidate);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return scopes;
|
|
1975
|
+
}
|
|
1976
|
+
function rankedScopeNames(scopes) {
|
|
1977
|
+
return [...scopes.keys()].sort(compareCodepoint);
|
|
1978
|
+
}
|
|
1979
|
+
function rankedScopeDescription(scopes) {
|
|
1980
|
+
return ['Where to search. "code" (default): rank symbols.', ...rankedScopeNames(scopes).map((name) => scopes.get(name).description)].join(" ");
|
|
1981
|
+
}
|
|
1982
|
+
function errorResult(message, masking) {
|
|
1983
|
+
return { content: [{ type: "text", text: masking.mask(JSON.stringify({ error: true, message })) }] };
|
|
1984
|
+
}
|
|
1985
|
+
function withRankedScopes(code, scopes, masking) {
|
|
1986
|
+
if (scopes.size === 0) return code;
|
|
1987
|
+
const names = rankedScopeNames(scopes);
|
|
1988
|
+
return (args) => {
|
|
1989
|
+
const requested = args["scope"];
|
|
1990
|
+
if (requested === void 0 || requested === CODE_SCOPE) return code(args);
|
|
1991
|
+
const scope = typeof requested === "string" ? scopes.get(requested) : void 0;
|
|
1992
|
+
if (scope === void 0) {
|
|
1993
|
+
return errorResult(`Unknown scope "${String(requested)}"; use one of: ${[CODE_SCOPE, ...names].join(", ")}`, masking);
|
|
1994
|
+
}
|
|
1995
|
+
const parsed = ScopeInputSchema.safeParse(args);
|
|
1996
|
+
if (!parsed.success) return errorResult(parsed.error.message, masking);
|
|
1997
|
+
return (async () => {
|
|
1998
|
+
try {
|
|
1999
|
+
const result = await scope.search({ query: parsed.data.query, tokenBudget: parsed.data.tokenBudget });
|
|
2000
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
2001
|
+
throw new Error(`Scope ${scope.scope} returned no payload object`);
|
|
2002
|
+
}
|
|
2003
|
+
return { content: [{ type: "text", text: masking.mask(JSON.stringify(wrapResponse(result))) }] };
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
return errorResult(describeError(err), masking);
|
|
2006
|
+
}
|
|
2007
|
+
})();
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2285
2011
|
// src/core/overlay/cluster-label-masker.ts
|
|
2286
2012
|
var ClusterLabelMasker = class {
|
|
2287
2013
|
matcher;
|
|
@@ -2317,17 +2043,17 @@ var ClusterLabelMasker = class {
|
|
|
2317
2043
|
};
|
|
2318
2044
|
|
|
2319
2045
|
// src/mcp-server.ts
|
|
2320
|
-
var
|
|
2046
|
+
var log6 = createLogger("ctxo:mcp");
|
|
2321
2047
|
function loadMaskingConfig(ctxoRoot) {
|
|
2322
2048
|
const jsonConfigPath = join(ctxoRoot, "masking.json");
|
|
2323
2049
|
if (existsSync(jsonConfigPath)) {
|
|
2324
2050
|
try {
|
|
2325
2051
|
const raw = readFileSync(jsonConfigPath, "utf-8");
|
|
2326
2052
|
const patterns = JSON.parse(raw);
|
|
2327
|
-
|
|
2053
|
+
log6.info(`Loaded ${patterns.length} custom masking pattern(s)`);
|
|
2328
2054
|
return MaskingPipeline.fromConfig(patterns);
|
|
2329
2055
|
} catch (err) {
|
|
2330
|
-
|
|
2056
|
+
log6.error(`Failed to load masking config: ${err.message}`);
|
|
2331
2057
|
}
|
|
2332
2058
|
}
|
|
2333
2059
|
return new MaskingPipeline();
|
|
@@ -2376,6 +2102,8 @@ async function startMcpServer(options = {}) {
|
|
|
2376
2102
|
}
|
|
2377
2103
|
function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo", clusterLabelMasker, coChange, modules = []) {
|
|
2378
2104
|
const enrichers = collectEnrichers(modules);
|
|
2105
|
+
const taskContextReservation = collectTaskContextReservation(modules);
|
|
2106
|
+
const rankedScopes = collectRankedScopes(modules);
|
|
2379
2107
|
const toolAnnotations = {
|
|
2380
2108
|
readOnlyHint: true,
|
|
2381
2109
|
destructiveHint: false,
|
|
@@ -2391,10 +2119,10 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2391
2119
|
{
|
|
2392
2120
|
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. With @ctxo/domain and a registry, the root symbol carries `context` (its named context id).',
|
|
2393
2121
|
inputSchema: {
|
|
2394
|
-
symbolId:
|
|
2395
|
-
symbolIds:
|
|
2396
|
-
level:
|
|
2397
|
-
intent:
|
|
2122
|
+
symbolId: z15.string().optional().describe(`Single symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2123
|
+
symbolIds: z15.array(z15.string()).optional().describe("Batch: array of symbol IDs"),
|
|
2124
|
+
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)"),
|
|
2125
|
+
intent: z15.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter")')
|
|
2398
2126
|
},
|
|
2399
2127
|
annotations: toolAnnotations
|
|
2400
2128
|
},
|
|
@@ -2403,10 +2131,10 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2403
2131
|
server.registerTool(
|
|
2404
2132
|
"get_why_context",
|
|
2405
2133
|
{
|
|
2406
|
-
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. With @ctxo/domain and a registry, `contextDrift` reports the named context of the symbol: its share of cross-context edges over time and files that moved between contexts.",
|
|
2134
|
+
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. With @ctxo/domain and a registry, `contextDrift` reports the named context of the symbol: its share of cross-context edges over time and files that moved between contexts. With @ctxo/domain and a domain graph, `decisions` lists up to five card decisions of the context of the symbol with their provenance, those naming a rule linked to the symbol first.",
|
|
2407
2135
|
inputSchema: {
|
|
2408
|
-
symbolId:
|
|
2409
|
-
maxCommits:
|
|
2136
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2137
|
+
maxCommits: z15.number().int().min(1).optional().describe("Limit commit history to N most recent commits")
|
|
2410
2138
|
},
|
|
2411
2139
|
annotations: toolAnnotations
|
|
2412
2140
|
},
|
|
@@ -2417,7 +2145,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2417
2145
|
{
|
|
2418
2146
|
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.",
|
|
2419
2147
|
inputSchema: {
|
|
2420
|
-
symbolId:
|
|
2148
|
+
symbolId: z15.string().min(1).describe("The symbol ID (format: file::name::kind)")
|
|
2421
2149
|
},
|
|
2422
2150
|
annotations: toolAnnotations
|
|
2423
2151
|
},
|
|
@@ -2427,11 +2155,11 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2427
2155
|
server.registerTool(
|
|
2428
2156
|
"get_blast_radius",
|
|
2429
2157
|
{
|
|
2430
|
-
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. With @ctxo/domain and a registry, each impacted symbol carries `context` (its named context id).",
|
|
2158
|
+
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. With @ctxo/domain and a registry, each impacted symbol carries `context` (its named context id). With @ctxo/domain and a domain graph (built by `ctxo index`), `rulesAffected` lists the card rules linked to the symbol or its dependents, and `cardsToUpdate` the cards that hold them.",
|
|
2431
2159
|
inputSchema: {
|
|
2432
|
-
symbolId:
|
|
2433
|
-
confidence:
|
|
2434
|
-
intent:
|
|
2160
|
+
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`),
|
|
2161
|
+
confidence: z15.enum(["confirmed", "likely", "potential"]).optional().describe("Filter by confidence tier"),
|
|
2162
|
+
intent: z15.string().optional().describe('Filter impacted symbols by intent keywords (e.g., "test", "adapter")')
|
|
2435
2163
|
},
|
|
2436
2164
|
annotations: toolAnnotations
|
|
2437
2165
|
},
|
|
@@ -2443,9 +2171,9 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2443
2171
|
{
|
|
2444
2172
|
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.',
|
|
2445
2173
|
inputSchema: {
|
|
2446
|
-
layer:
|
|
2447
|
-
mode:
|
|
2448
|
-
candidate:
|
|
2174
|
+
layer: z15.string().optional().describe("Filter by specific layer name"),
|
|
2175
|
+
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)'),
|
|
2176
|
+
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')
|
|
2449
2177
|
},
|
|
2450
2178
|
annotations: toolAnnotations
|
|
2451
2179
|
},
|
|
@@ -2457,37 +2185,41 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2457
2185
|
{
|
|
2458
2186
|
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.',
|
|
2459
2187
|
inputSchema: {
|
|
2460
|
-
includeTests:
|
|
2461
|
-
intent:
|
|
2188
|
+
includeTests: z15.boolean().optional().default(false).describe("Include test files (default test globs and files with a test-framework annotation such as @Test) in analysis (default: exclude)"),
|
|
2189
|
+
intent: z15.string().optional().describe('Filter dead code results by intent keywords (e.g., "adapter", "function")')
|
|
2462
2190
|
},
|
|
2463
2191
|
annotations: toolAnnotations
|
|
2464
2192
|
},
|
|
2465
2193
|
(args) => deadCodeHandler(args)
|
|
2466
2194
|
);
|
|
2467
|
-
const contextForTaskHandler = withRecording("get_context_for_task", handleGetContextForTask(storage, masking, staleness, ctxoRoot, coChange), recorder);
|
|
2195
|
+
const contextForTaskHandler = withRecording("get_context_for_task", handleGetContextForTask(storage, masking, staleness, ctxoRoot, coChange, enrichers.get("get_context_for_task"), taskContextReservation), recorder);
|
|
2468
2196
|
server.registerTool(
|
|
2469
2197
|
"get_context_for_task",
|
|
2470
2198
|
{
|
|
2471
|
-
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.',
|
|
2199
|
+
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. With @ctxo/domain and a domain graph, `cardExcerpt` adds the card rules linked to the symbol and its neighbours, the terms of its context and its contracts within `tokenBudget` (left out when even an empty excerpt does not fit).',
|
|
2472
2200
|
inputSchema: {
|
|
2473
|
-
symbolId:
|
|
2474
|
-
taskType:
|
|
2475
|
-
tokenBudget:
|
|
2201
|
+
symbolId: z15.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
|
|
2202
|
+
taskType: z15.enum(["fix", "extend", "refactor", "understand"]).describe("Task type determines which context is most relevant"),
|
|
2203
|
+
tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for context (default 4000)")
|
|
2476
2204
|
},
|
|
2477
2205
|
annotations: toolAnnotations
|
|
2478
2206
|
},
|
|
2479
2207
|
(args) => contextForTaskHandler(args)
|
|
2480
2208
|
);
|
|
2481
|
-
const rankedContextHandler = withRecording("get_ranked_context", handleGetRankedContext(storage, masking, staleness, ctxoRoot), recorder);
|
|
2209
|
+
const rankedContextHandler = withRecording("get_ranked_context", withRankedScopes(handleGetRankedContext(storage, masking, staleness, ctxoRoot), rankedScopes, masking), recorder);
|
|
2210
|
+
const rankedScopeShape = rankedScopes.size === 0 ? {} : {
|
|
2211
|
+
scope: z15.enum([CODE_SCOPE, ...rankedScopeNames(rankedScopes)]).optional().default(CODE_SCOPE).describe(rankedScopeDescription(rankedScopes))
|
|
2212
|
+
};
|
|
2482
2213
|
server.registerTool(
|
|
2483
2214
|
"get_ranked_context",
|
|
2484
2215
|
{
|
|
2485
2216
|
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.",
|
|
2486
2217
|
inputSchema: {
|
|
2487
|
-
query:
|
|
2488
|
-
tokenBudget:
|
|
2489
|
-
strategy:
|
|
2490
|
-
searchMode:
|
|
2218
|
+
query: z15.string().min(1).describe("Search query (matches symbol names)"),
|
|
2219
|
+
tokenBudget: z15.number().optional().default(4e3).describe("Max tokens for results (default 4000)"),
|
|
2220
|
+
strategy: z15.enum(["combined", "dependency", "importance"]).optional().default("combined").describe("Ranking strategy"),
|
|
2221
|
+
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'),
|
|
2222
|
+
...rankedScopeShape
|
|
2491
2223
|
},
|
|
2492
2224
|
annotations: toolAnnotations
|
|
2493
2225
|
},
|
|
@@ -2499,11 +2231,11 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2499
2231
|
{
|
|
2500
2232
|
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. With @ctxo/domain and a registry, each result carries `context` (its named context id).",
|
|
2501
2233
|
inputSchema: {
|
|
2502
|
-
pattern:
|
|
2234
|
+
pattern: z15.string().min(1).describe("Search pattern (substring or regex)"),
|
|
2503
2235
|
kind: SymbolKindSchema.optional().describe("Filter by symbol kind (table, view and procedure come from SQL plugins)"),
|
|
2504
|
-
filePattern:
|
|
2505
|
-
limit:
|
|
2506
|
-
mode:
|
|
2236
|
+
filePattern: z15.string().optional().describe("Filter by file path substring"),
|
|
2237
|
+
limit: z15.number().int().min(1).max(100).optional().default(25).describe("Max results (default 25)"),
|
|
2238
|
+
mode: z15.enum(["regex", "fts"]).optional().default("regex").describe('"regex" (default): case-insensitive regex or substring match on names; "fts": BM25 relevance search')
|
|
2507
2239
|
},
|
|
2508
2240
|
annotations: toolAnnotations
|
|
2509
2241
|
},
|
|
@@ -2515,8 +2247,8 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2515
2247
|
{
|
|
2516
2248
|
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.",
|
|
2517
2249
|
inputSchema: {
|
|
2518
|
-
since:
|
|
2519
|
-
maxFiles:
|
|
2250
|
+
since: z15.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
|
|
2251
|
+
maxFiles: z15.number().int().min(1).optional().default(50).describe("Max changed files to process (default 50)")
|
|
2520
2252
|
},
|
|
2521
2253
|
annotations: toolAnnotations
|
|
2522
2254
|
},
|
|
@@ -2528,11 +2260,11 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2528
2260
|
{
|
|
2529
2261
|
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. With @ctxo/domain and a registry, each importer carries `context` (its named context id).',
|
|
2530
2262
|
inputSchema: {
|
|
2531
|
-
symbolId:
|
|
2532
|
-
edgeKinds:
|
|
2533
|
-
transitive:
|
|
2534
|
-
maxDepth:
|
|
2535
|
-
intent:
|
|
2263
|
+
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`),
|
|
2264
|
+
edgeKinds: z15.array(EdgeKindSchema).optional().describe("Filter by edge kinds (reads, writes and executes are data access)"),
|
|
2265
|
+
transitive: z15.boolean().optional().default(false).describe("Follow transitive reverse edges (default false)"),
|
|
2266
|
+
maxDepth: z15.number().int().min(1).max(10).optional().default(5).describe("Max BFS depth for transitive mode (default 5)"),
|
|
2267
|
+
intent: z15.string().optional().describe('Filter importers by intent keywords (e.g., "test", "core", "adapter")')
|
|
2536
2268
|
},
|
|
2537
2269
|
annotations: toolAnnotations
|
|
2538
2270
|
},
|
|
@@ -2544,8 +2276,8 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2544
2276
|
{
|
|
2545
2277
|
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.",
|
|
2546
2278
|
inputSchema: {
|
|
2547
|
-
symbolId:
|
|
2548
|
-
direction:
|
|
2279
|
+
symbolId: z15.string().min(1).optional().describe("Root symbol ID (omit for full project hierarchy)"),
|
|
2280
|
+
direction: z15.enum(["ancestors", "descendants", "both"]).optional().default("both").describe("Traversal direction (default both)")
|
|
2549
2281
|
},
|
|
2550
2282
|
annotations: toolAnnotations
|
|
2551
2283
|
},
|
|
@@ -2557,10 +2289,10 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2557
2289
|
{
|
|
2558
2290
|
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.",
|
|
2559
2291
|
inputSchema: {
|
|
2560
|
-
limit:
|
|
2292
|
+
limit: z15.number().int().min(1).max(200).optional().default(25).describe("Max results (default 25)"),
|
|
2561
2293
|
kind: SymbolKindSchema.optional().describe("Filter by symbol kind (table, view and procedure come from SQL plugins)"),
|
|
2562
|
-
filePattern:
|
|
2563
|
-
damping:
|
|
2294
|
+
filePattern: z15.string().optional().describe("Filter by file path substring"),
|
|
2295
|
+
damping: z15.number().min(0).max(1).optional().default(0.85).describe("PageRank damping factor (default 0.85)")
|
|
2564
2296
|
},
|
|
2565
2297
|
annotations: toolAnnotations
|
|
2566
2298
|
},
|
|
@@ -2570,11 +2302,11 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2570
2302
|
server.registerTool(
|
|
2571
2303
|
"get_pr_impact",
|
|
2572
2304
|
{
|
|
2573
|
-
description: "Analyze the full impact of a PR or recent changes in a SINGLE call
|
|
2305
|
+
description: "Analyze the full impact of a PR or recent changes in a SINGLE call: 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. With @ctxo/domain and a registry, changed symbols carry `context` and `contextBoundaryViolations` lists edges that touch them and cross a context boundary outside its contracts. With @ctxo/domain and a domain graph, `contextsTouched` (the commits after the merge base of `since` and HEAD, plus the working tree), `cardsChanged` and `rulesWithoutTests` report the named contexts the change touched, the cards it changed and their rules without a test link.",
|
|
2574
2306
|
inputSchema: {
|
|
2575
|
-
since:
|
|
2576
|
-
maxFiles:
|
|
2577
|
-
confidence:
|
|
2307
|
+
since: z15.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
|
|
2308
|
+
maxFiles: z15.number().int().min(1).optional().default(50).describe("Max changed files to analyze"),
|
|
2309
|
+
confidence: z15.enum(["confirmed", "likely", "potential"]).optional().describe("Filter impacted symbols by confidence tier")
|
|
2578
2310
|
},
|
|
2579
2311
|
annotations: toolAnnotations
|
|
2580
2312
|
},
|
|
@@ -2586,11 +2318,11 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
|
|
|
2586
2318
|
const moduleToolHandler = withRecording(tool.name, handleModuleTool(tool, masking, staleness, indexedFiles), recorder);
|
|
2587
2319
|
server.registerTool(
|
|
2588
2320
|
tool.name,
|
|
2589
|
-
{ description: tool.description, inputSchema: moduleInputShape(tool,
|
|
2321
|
+
{ description: tool.description, inputSchema: moduleInputShape(tool, z15), annotations: toolAnnotations },
|
|
2590
2322
|
(args) => moduleToolHandler(args)
|
|
2591
2323
|
);
|
|
2592
2324
|
} catch (err) {
|
|
2593
|
-
|
|
2325
|
+
log6.warn(`Module tool "${tool.name}" was not registered: ${err.message}`);
|
|
2594
2326
|
}
|
|
2595
2327
|
}
|
|
2596
2328
|
server.resource("ctxo-status", "ctxo://status", async (uri) => ({
|
|
@@ -2601,4 +2333,4 @@ export {
|
|
|
2601
2333
|
registerTools,
|
|
2602
2334
|
startMcpServer
|
|
2603
2335
|
};
|
|
2604
|
-
//# sourceMappingURL=mcp-server-
|
|
2336
|
+
//# sourceMappingURL=mcp-server-IYCCE74E.js.map
|