@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.
Files changed (41) hide show
  1. package/README.md +10 -1
  2. package/dist/{chunk-ZL36YS2V.js → chunk-A4URKKPP.js} +3 -177
  3. package/dist/{chunk-SRQZDGFZ.js → chunk-BBSJB4CG.js} +2 -2
  4. package/dist/{chunk-X4TVFJ4C.js → chunk-EC6INGPV.js} +1 -1
  5. package/dist/{chunk-7HSA5JVP.js → chunk-I7RPXT6L.js} +193 -1
  6. package/dist/chunk-IT426P3F.js +1036 -0
  7. package/dist/{chunk-G4BMKCKG.js → chunk-JDL6GGTO.js} +16 -5
  8. package/dist/{cli-router-AZCDNX5R.js → cli-router-W56INOQ3.js} +10 -10
  9. package/dist/index.js +3 -3
  10. package/dist/internal.d.ts +90 -3
  11. package/dist/internal.js +4 -4
  12. package/dist/{json-index-reader-DBW32TCN.js → json-index-reader-DKEIP2QR.js} +2 -2
  13. package/dist/{mcp-server-6YG2QE6E.js → mcp-server-IYCCE74E.js} +178 -446
  14. package/dist/module-host-KTGDODES.js +13 -0
  15. package/llms-full.txt +68 -3
  16. package/llms.txt +14 -2
  17. package/package.json +4 -3
  18. package/dist/atomic-write-BAFH464Q.js.map +0 -1
  19. package/dist/chunk-2Q76LH2H.js.map +0 -1
  20. package/dist/chunk-5DSKZJNN.js.map +0 -1
  21. package/dist/chunk-72CEQEQD.js.map +0 -1
  22. package/dist/chunk-7HSA5JVP.js.map +0 -1
  23. package/dist/chunk-G4BMKCKG.js.map +0 -1
  24. package/dist/chunk-L2CAUER6.js +0 -508
  25. package/dist/chunk-L2CAUER6.js.map +0 -1
  26. package/dist/chunk-LAX6VVBU.js.map +0 -1
  27. package/dist/chunk-PSKDDLSF.js.map +0 -1
  28. package/dist/chunk-RFEXMBTG.js.map +0 -1
  29. package/dist/chunk-SRQZDGFZ.js.map +0 -1
  30. package/dist/chunk-X4TVFJ4C.js.map +0 -1
  31. package/dist/chunk-ZL36YS2V.js.map +0 -1
  32. package/dist/cli-router-AZCDNX5R.js.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/internal.js.map +0 -1
  35. package/dist/json-index-reader-DBW32TCN.js.map +0 -1
  36. package/dist/mcp-server-6YG2QE6E.js.map +0 -1
  37. package/dist/module-host-KNMKS62R.js +0 -13
  38. package/dist/module-host-KNMKS62R.js.map +0 -1
  39. package/dist/module-loader-B3LYF3W7.js.map +0 -1
  40. package/dist/plugin-discovery-XRXI3IJF.js.map +0 -1
  41. 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-ZL36YS2V.js";
16
+ } from "./chunk-A4URKKPP.js";
18
17
  import {
19
18
  loadModulesFor
20
- } from "./chunk-SRQZDGFZ.js";
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-L2CAUER6.js";
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-7HSA5JVP.js";
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-X4TVFJ4C.js";
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 z14 } from "zod";
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 = applyEnricher(enrich, responsePayload, parsed.data, "get_why_context");
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 = applyEnricher(enrich, body, parsed.data, "get_blast_radius");
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
- function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo", coChange) {
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 result = assembler.assembleForTask(graph, symbolId, taskType, indices, tokenBudget, coChange?.readCoChangeMap());
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 payload = masking.mask(JSON.stringify(wrapResponse(result)));
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({ since, changedFiles: 0, changedSymbols: 0, totalImpact: 0, riskLevel: "low", files: [], summary: { confirmedTotal: 0, likelyTotal: 0, potentialTotal: 0, highRiskSymbols: [] } }) }]
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 = applyEnricher(enrich, body, parsed.data, "get_pr_impact");
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 log5 = createLogger("ctxo:mcp");
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
- log5.info(`Loaded ${patterns.length} custom masking pattern(s)`);
2053
+ log6.info(`Loaded ${patterns.length} custom masking pattern(s)`);
2328
2054
  return MaskingPipeline.fromConfig(patterns);
2329
2055
  } catch (err) {
2330
- log5.error(`Failed to load masking config: ${err.message}`);
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: z14.string().optional().describe(`Single symbol ID (${SYMBOL_ID_FORMAT})`),
2395
- symbolIds: z14.array(z14.string()).optional().describe("Batch: array of symbol IDs"),
2396
- level: z14.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)"),
2397
- intent: z14.string().optional().describe('Filter dependencies by intent keywords (e.g., "core", "adapter")')
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: z14.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
2409
- maxCommits: z14.number().int().min(1).optional().describe("Limit commit history to N most recent commits")
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: z14.string().min(1).describe("The symbol ID (format: file::name::kind)")
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: z14.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`),
2433
- confidence: z14.enum(["confirmed", "likely", "potential"]).optional().describe("Filter by confidence tier"),
2434
- intent: z14.string().optional().describe('Filter impacted symbols by intent keywords (e.g., "test", "adapter")')
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: z14.string().optional().describe("Filter by specific layer name"),
2447
- mode: z14.enum(OVERLAY_MODES).optional().describe('View to return: "both" (default) layers plus communities, "regex" layer map, "communities" Louvain clusters, "contexts" candidate contexts (file level)'),
2448
- candidate: z14.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')
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: z14.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)"),
2461
- intent: z14.string().optional().describe('Filter dead code results by intent keywords (e.g., "adapter", "function")')
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: z14.string().min(1).describe(`The symbol ID (${SYMBOL_ID_FORMAT})`),
2474
- taskType: z14.enum(["fix", "extend", "refactor", "understand"]).describe("Task type determines which context is most relevant"),
2475
- tokenBudget: z14.number().optional().default(4e3).describe("Max tokens for context (default 4000)")
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: z14.string().min(1).describe("Search query (matches symbol names)"),
2488
- tokenBudget: z14.number().optional().default(4e3).describe("Max tokens for results (default 4000)"),
2489
- strategy: z14.enum(["combined", "dependency", "importance"]).optional().default("combined").describe("Ranking strategy"),
2490
- searchMode: z14.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')
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: z14.string().min(1).describe("Search pattern (substring or regex)"),
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: z14.string().optional().describe("Filter by file path substring"),
2505
- limit: z14.number().int().min(1).max(100).optional().default(25).describe("Max results (default 25)"),
2506
- mode: z14.enum(["regex", "fts"]).optional().default("regex").describe('"regex" (default): case-insensitive regex or substring match on names; "fts": BM25 relevance search')
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: z14.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
2519
- maxFiles: z14.number().int().min(1).optional().default(50).describe("Max changed files to process (default 50)")
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: z14.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`),
2532
- edgeKinds: z14.array(EdgeKindSchema).optional().describe("Filter by edge kinds (reads, writes and executes are data access)"),
2533
- transitive: z14.boolean().optional().default(false).describe("Follow transitive reverse edges (default false)"),
2534
- maxDepth: z14.number().int().min(1).max(10).optional().default(5).describe("Max BFS depth for transitive mode (default 5)"),
2535
- intent: z14.string().optional().describe('Filter importers by intent keywords (e.g., "test", "core", "adapter")')
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: z14.string().min(1).optional().describe("Root symbol ID (omit for full project hierarchy)"),
2548
- direction: z14.enum(["ancestors", "descendants", "both"]).optional().default("both").describe("Traversal direction (default both)")
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: z14.number().int().min(1).max(200).optional().default(25).describe("Max results (default 25)"),
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: z14.string().optional().describe("Filter by file path substring"),
2563
- damping: z14.number().min(0).max(1).optional().default(0.85).describe("PageRank damping factor (default 0.85)")
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 \u2014 combines changed symbols + blast radius + co-change history into one risk assessment. Results include `_meta` with truncation info. Use this FIRST when reviewing a PR or evaluating recent commits. For single-symbol analysis, use get_blast_radius instead. 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.",
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: z14.string().optional().default("HEAD~1").describe("Git ref to diff against (default HEAD~1)"),
2576
- maxFiles: z14.number().int().min(1).optional().default(50).describe("Max changed files to analyze"),
2577
- confidence: z14.enum(["confirmed", "likely", "potential"]).optional().describe("Filter impacted symbols by confidence tier")
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, z14), annotations: toolAnnotations },
2321
+ { description: tool.description, inputSchema: moduleInputShape(tool, z15), annotations: toolAnnotations },
2590
2322
  (args) => moduleToolHandler(args)
2591
2323
  );
2592
2324
  } catch (err) {
2593
- log5.warn(`Module tool "${tool.name}" was not registered: ${err.message}`);
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-6YG2QE6E.js.map
2336
+ //# sourceMappingURL=mcp-server-IYCCE74E.js.map