open-codebase-index 0.22.3 → 0.22.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1291,6 +1291,7 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1291
1291
  lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
1292
1292
  lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
1293
1293
  lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
1294
+ lines.push(`| Graph-neighbor recall | ${(summary.metrics.graphNeighborRecall ?? 0).toFixed(4)} |`);
1294
1295
  lines.push(`| Distinct Top@3 | ${formatPct(summary.metrics.distinctTop3Ratio)} |`);
1295
1296
  lines.push(`| Raw Distinct Top@3 | ${formatPct(summary.metrics.rawDistinctTop3Ratio)} |`);
1296
1297
  lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
@@ -6655,12 +6656,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
6655
6656
  return [...primary, ...remainder];
6656
6657
  }
6657
6658
  function buildDiversityKey(metadata) {
6658
- const normalizedPath = metadata.filePath.toLowerCase();
6659
+ const normalizedPath3 = metadata.filePath.toLowerCase();
6659
6660
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
6660
6661
  if (normalizedName.length > 0) {
6661
- return `${normalizedPath}#${normalizedName}`;
6662
+ return `${normalizedPath3}#${normalizedName}`;
6662
6663
  }
6663
- return normalizedPath;
6664
+ return normalizedPath3;
6664
6665
  }
6665
6666
  function rankHybridResults(query, semanticResults, keywordResults, options) {
6666
6667
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -7066,6 +7067,29 @@ function extractPrimaryIdentifierQueryHint(query) {
7066
7067
  const best = codeTerms.find((term) => term.length >= 6);
7067
7068
  return best ?? null;
7068
7069
  }
7070
+ function pathSegmentsForAffinityMatch(filePath) {
7071
+ const normalizedPath3 = normalizeRankingText(filePath).replace(/\\/g, "/");
7072
+ const segments = normalizedPath3.split("/").filter((segment) => segment.length > 0);
7073
+ if (segments.length === 0) {
7074
+ return [];
7075
+ }
7076
+ const basename8 = segments[segments.length - 1] ?? "";
7077
+ const basenameWithoutExt = basename8.replace(/\.[^/.]+$/u, "");
7078
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
7079
+ return Array.from(/* @__PURE__ */ new Set([
7080
+ ...normalizedSegments,
7081
+ basenameWithoutExt.toLowerCase()
7082
+ ]));
7083
+ }
7084
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
7085
+ const haystack = pathSegmentsForAffinityMatch(filePath);
7086
+ return exactIdentifierVariants.some((variant) => {
7087
+ if (!variant || variant.length < 2) {
7088
+ return false;
7089
+ }
7090
+ return haystack.includes(variant);
7091
+ });
7092
+ }
7069
7093
  var FILE_PATH_HINT_EXTENSIONS = [
7070
7094
  "ts",
7071
7095
  "tsx",
@@ -7109,9 +7133,9 @@ function normalizeFilePathForHintMatch(filePath) {
7109
7133
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
7110
7134
  }
7111
7135
  function pathMatchesHint(filePath, hint) {
7112
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
7136
+ const normalizedPath3 = normalizeFilePathForHintMatch(filePath);
7113
7137
  const normalizedHint = normalizeFilePathForHintMatch(hint);
7114
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
7138
+ return normalizedPath3.endsWith(normalizedHint) || normalizedPath3.includes(`/${normalizedHint}`) || normalizedPath3.includes(normalizedHint);
7115
7139
  }
7116
7140
  function extractFilePathHint(query) {
7117
7141
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -7141,10 +7165,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
7141
7165
  ).map((candidate) => {
7142
7166
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
7143
7167
  const pathLower = candidate.metadata.filePath.toLowerCase();
7144
- let maxMatch = 0;
7145
- const nameMatchesPrimary = primaryVariants.some(
7168
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
7169
+ const exactMatch = exactIdentifierVariants.some(
7146
7170
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
7147
7171
  );
7172
+ let maxMatch = 0;
7173
+ const nameMatchesPrimary = exactMatch;
7174
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
7148
7175
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
7149
7176
  for (const hint of hints) {
7150
7177
  const variants = normalizeIdentifierVariants(hint);
@@ -7165,12 +7192,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
7165
7192
  candidate,
7166
7193
  maxMatch,
7167
7194
  pathMatchesFileHint,
7168
- nameMatchesPrimary
7195
+ nameMatchesPrimary,
7196
+ pathAffinity
7169
7197
  };
7170
7198
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
7171
7199
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
7172
7200
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
7173
7201
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
7202
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
7203
+ return b.nameMatchesPrimary ? 1 : -1;
7204
+ }
7205
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
7174
7206
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
7175
7207
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
7176
7208
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -11375,6 +11407,20 @@ var Indexer = class _Indexer {
11375
11407
  requestedLimit = nextLimit;
11376
11408
  }
11377
11409
  }
11410
+ buildCandidateSnapshot(candidate) {
11411
+ return {
11412
+ id: candidate.id,
11413
+ filePath: candidate.metadata.filePath,
11414
+ startLine: candidate.metadata.startLine,
11415
+ endLine: candidate.metadata.endLine,
11416
+ score: candidate.score,
11417
+ chunkType: candidate.metadata.chunkType,
11418
+ name: candidate.metadata.name
11419
+ };
11420
+ }
11421
+ buildCandidateSnapshotList(candidates) {
11422
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
11423
+ }
11378
11424
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
11379
11425
  return this.searchCandidatesWithBranchPrefilter(
11380
11426
  initialLimit,
@@ -11566,6 +11612,16 @@ var Indexer = class _Indexer {
11566
11612
  prefilterMs: Math.round(prefilterMs * 100) / 100,
11567
11613
  fusionMs: Math.round(fusionMs * 100) / 100
11568
11614
  });
11615
+ if (options?.trace) {
11616
+ options.trace({
11617
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
11618
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
11619
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
11620
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
11621
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
11622
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
11623
+ });
11624
+ }
11569
11625
  const metadataOnly = options?.metadataOnly ?? false;
11570
11626
  return Promise.all(
11571
11627
  finalResults.map(async (r) => {
@@ -12614,6 +12670,42 @@ var Indexer = class _Indexer {
12614
12670
  }
12615
12671
  };
12616
12672
 
12673
+ // src/tools/contracts.ts
12674
+ var CHUNK_TYPES = [
12675
+ "function",
12676
+ "class",
12677
+ "method",
12678
+ "interface",
12679
+ "type",
12680
+ "enum",
12681
+ "struct",
12682
+ "impl",
12683
+ "trait",
12684
+ "module",
12685
+ "other"
12686
+ ];
12687
+ var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12688
+ var RELATIONSHIP_TYPES = [
12689
+ "Call",
12690
+ "MethodCall",
12691
+ "Constructor",
12692
+ "Import",
12693
+ "Inherits",
12694
+ "Implements"
12695
+ ];
12696
+ var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12697
+ var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12698
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
12699
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
12700
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
12701
+ var CODE_COMMUNITIES_MIN_SIZE = 1;
12702
+ var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12703
+ var CODE_COMMUNITIES_MAX_LIMIT = 100;
12704
+ var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12705
+ var CODE_COMMUNITIES_MIN_COUPLING = 1;
12706
+ var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12707
+ var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12708
+
12617
12709
  // src/tools/operations.ts
12618
12710
  var import_fs14 = require("fs");
12619
12711
  var path21 = __toESM(require("path"), 1);
@@ -12742,39 +12834,6 @@ function formatCodeCommunities(result) {
12742
12834
  return lines.join("\n");
12743
12835
  }
12744
12836
 
12745
- // src/tools/contracts.ts
12746
- var CHUNK_TYPES = [
12747
- "function",
12748
- "class",
12749
- "method",
12750
- "interface",
12751
- "type",
12752
- "enum",
12753
- "struct",
12754
- "impl",
12755
- "trait",
12756
- "module",
12757
- "other"
12758
- ];
12759
- var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12760
- var RELATIONSHIP_TYPES = [
12761
- "Call",
12762
- "MethodCall",
12763
- "Constructor",
12764
- "Import",
12765
- "Inherits",
12766
- "Implements"
12767
- ];
12768
- var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12769
- var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12770
- var CODE_COMMUNITIES_MIN_SIZE = 1;
12771
- var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12772
- var CODE_COMMUNITIES_MAX_LIMIT = 100;
12773
- var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12774
- var CODE_COMMUNITIES_MIN_COUPLING = 1;
12775
- var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12776
- var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12777
-
12778
12837
  // src/tools/context-pack.ts
12779
12838
  var import_tiktoken = require("tiktoken");
12780
12839
  var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
@@ -12876,6 +12935,16 @@ function compactEvidenceValue(value, maxChars) {
12876
12935
  }
12877
12936
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
12878
12937
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
12938
+ function toContextPackTraceCandidate(result) {
12939
+ return {
12940
+ filePath: result.filePath,
12941
+ startLine: result.startLine,
12942
+ endLine: result.endLine,
12943
+ score: result.score,
12944
+ chunkType: result.chunkType,
12945
+ name: result.name
12946
+ };
12947
+ }
12879
12948
  function formatExactSearchHandoff(results) {
12880
12949
  const suggestedNames = [];
12881
12950
  const seen = /* @__PURE__ */ new Set();
@@ -12924,13 +12993,13 @@ function buildContextPack(results, options = {}) {
12924
12993
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
12925
12994
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
12926
12995
  const candidateCount = results.length;
12927
- const deduplicated = deduplicateContextCandidates(
12928
- rankContextCandidates(
12929
- results,
12930
- options.preferImplementationPaths ?? false
12931
- )
12932
- );
12933
- const diversified = diversifyContextCandidates(deduplicated);
12996
+ const preserveInputOrder = options.preserveInputOrder ?? false;
12997
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
12998
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
12999
+ const deduplicated = deduplicateContextCandidates(ranked);
13000
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
13001
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
13002
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
12934
13003
  const duplicateCount = candidateCount - deduplicated.length;
12935
13004
  const selectable = diversified.slice(0, maxResults);
12936
13005
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -12963,6 +13032,15 @@ function buildContextPack(results, options = {}) {
12963
13032
  const fitted = fitTextToContextBudget(text, tokenBudget);
12964
13033
  const budgetOmittedCount = selectable.length - selected.length;
12965
13034
  const omittedCount = candidateCount - selected.length;
13035
+ if (options.trace) {
13036
+ options.trace({
13037
+ inputCandidates: results.map(toContextPackTraceCandidate),
13038
+ rankedCandidates,
13039
+ deduplicatedCandidates,
13040
+ diversifiedCandidates,
13041
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
13042
+ });
13043
+ }
12966
13044
  return {
12967
13045
  requestedTokenBudget,
12968
13046
  tokenBudget,
@@ -14710,7 +14788,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14710
14788
  definitionIntent: options.definitionIntent,
14711
14789
  blameAuthor: options.blameAuthor,
14712
14790
  blameSha: options.blameSha,
14713
- blameSince: options.blameSince
14791
+ blameSince: options.blameSince,
14792
+ trace: options.trace
14714
14793
  });
14715
14794
  }
14716
14795
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14764,15 +14843,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14764
14843
  return indexer.search(query, options.limit, {
14765
14844
  fileType: options.fileType,
14766
14845
  directory: options.directory,
14767
- definitionIntent: true
14846
+ definitionIntent: true,
14847
+ trace: options.trace
14768
14848
  });
14769
14849
  }
14770
14850
  async function getCallGraphData(projectRoot, host, params) {
14771
14851
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14772
14852
  const root = getProjectRoot(projectRoot, host);
14773
14853
  const indexer = getIndexerForProject(root, host);
14854
+ return getCallGraphDataForIndexer(indexer, root, params);
14855
+ }
14856
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14774
14857
  const symbols = await indexer.getCallGraphSymbols();
14775
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14858
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14776
14859
  const direction = params.direction === "callees" ? "callees" : "callers";
14777
14860
  if (resolution.status !== "resolved") {
14778
14861
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -15100,6 +15183,27 @@ function buildRecoveryDetails(attempts, successIndex) {
15100
15183
  successfulAttemptIndex: successIndex
15101
15184
  };
15102
15185
  }
15186
+ function serializeAttempts(attempts) {
15187
+ return attempts.map((attempt) => ({
15188
+ kind: attempt.kind,
15189
+ scope: attempt.scope,
15190
+ resultCount: attempt.resultCount,
15191
+ relaxedFields: attempt.relaxedFields
15192
+ }));
15193
+ }
15194
+ function buildSearchDiagnostic(attempt) {
15195
+ if (!attempt) {
15196
+ return void 0;
15197
+ }
15198
+ return {
15199
+ route: attempt.kind,
15200
+ routedQuery: attempt.query,
15201
+ searchQuery: attempt.query,
15202
+ searchScope: attempt.scopeFilter,
15203
+ searchTrace: attempt.searchTrace,
15204
+ contextPackTrace: attempt.contextPackTrace
15205
+ };
15206
+ }
15103
15207
  function trimOrUndefined2(value) {
15104
15208
  const normalized = value?.trim();
15105
15209
  if (!normalized) {
@@ -15139,6 +15243,7 @@ async function resolveSearchContext(input, operations) {
15139
15243
  const hasFilters = Boolean(fileType || directory);
15140
15244
  const relaxedFields = relaxedHintFields(fileType, directory);
15141
15245
  const attempts = [];
15246
+ const attemptStates = [];
15142
15247
  const decisions = {
15143
15248
  inferredDefinitionMiss: false,
15144
15249
  fallbackFromOriginalConceptualToInferred: false,
@@ -15167,8 +15272,20 @@ async function resolveSearchContext(input, operations) {
15167
15272
  if (seenAttempts.has(key)) {
15168
15273
  return [];
15169
15274
  }
15170
- const results = await runAttempt();
15275
+ const attemptState = {
15276
+ kind,
15277
+ scope: describeScope(scope.fileType, scope.directory),
15278
+ resultCount: 0,
15279
+ relaxedFields: [...relaxedFieldsForAttempt],
15280
+ query: attemptQuery,
15281
+ scopeFilter: scope
15282
+ };
15283
+ const results = await runAttempt((trace) => {
15284
+ attemptState.searchTrace = trace;
15285
+ });
15286
+ attemptState.resultCount = results.length;
15171
15287
  seenAttempts.add(key);
15288
+ attemptStates.push(attemptState);
15172
15289
  attempts.push({
15173
15290
  kind,
15174
15291
  scope: describeScope(scope.fileType, scope.directory),
@@ -15188,7 +15305,7 @@ async function resolveSearchContext(input, operations) {
15188
15305
  symbol,
15189
15306
  scope,
15190
15307
  relaxedFieldsForAttempt,
15191
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15308
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15192
15309
  );
15193
15310
  };
15194
15311
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -15197,13 +15314,23 @@ async function resolveSearchContext(input, operations) {
15197
15314
  searchQuery,
15198
15315
  scope,
15199
15316
  relaxedFieldsForAttempt,
15200
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15317
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15201
15318
  );
15202
15319
  };
15203
- const toResult = (route, routedQuery, pack) => {
15320
+ const findSuccessfulAttemptState = (route) => {
15321
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15322
+ const attempt = attemptStates[index];
15323
+ if (attempt.kind === route && attempt.resultCount > 0) {
15324
+ return attempt;
15325
+ }
15326
+ }
15327
+ return void 0;
15328
+ };
15329
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
15204
15330
  const base = packedResult(route, routedQuery, pack);
15205
15331
  const baseDetails = base.details;
15206
15332
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15333
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
15207
15334
  return {
15208
15335
  text: base.text,
15209
15336
  details: {
@@ -15211,7 +15338,10 @@ async function resolveSearchContext(input, operations) {
15211
15338
  tokenBudget: baseDetails.tokenBudget,
15212
15339
  tokenEstimate: baseDetails.tokenEstimate,
15213
15340
  truncated: false,
15214
- recovery: buildRecoveryDetails(attempts, successIndex)
15341
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15342
+ ...input.diagnostic && {
15343
+ diagnostic: buildSearchDiagnostic(successState)
15344
+ }
15215
15345
  }
15216
15346
  };
15217
15347
  };
@@ -15225,8 +15355,18 @@ async function resolveSearchContext(input, operations) {
15225
15355
  buildContextPack(scopedDefinitionResults, {
15226
15356
  tokenBudget,
15227
15357
  maxResults: limit,
15228
- heading
15229
- })
15358
+ heading,
15359
+ preserveInputOrder: true,
15360
+ ...input.diagnostic ? {
15361
+ trace: (trace) => {
15362
+ const attemptState = findSuccessfulAttemptState("definition");
15363
+ if (attemptState) {
15364
+ attemptState.contextPackTrace = trace;
15365
+ }
15366
+ }
15367
+ } : void 0
15368
+ }),
15369
+ findSuccessfulAttemptState("definition")
15230
15370
  );
15231
15371
  }
15232
15372
  if (explicitSymbol) {
@@ -15244,8 +15384,18 @@ async function resolveSearchContext(input, operations) {
15244
15384
  buildContextPack(unscopedDefinitionResults, {
15245
15385
  tokenBudget,
15246
15386
  maxResults: limit,
15247
- heading: heading2
15248
- })
15387
+ heading: heading2,
15388
+ preserveInputOrder: true,
15389
+ ...input.diagnostic ? {
15390
+ trace: (trace) => {
15391
+ const attemptState = findSuccessfulAttemptState("definition");
15392
+ if (attemptState) {
15393
+ attemptState.contextPackTrace = trace;
15394
+ }
15395
+ }
15396
+ } : void 0
15397
+ }),
15398
+ findSuccessfulAttemptState("definition")
15249
15399
  );
15250
15400
  }
15251
15401
  }
@@ -15264,7 +15414,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15264
15414
  tokenBudget: heading.tokenBudget,
15265
15415
  tokenEstimate: heading.tokenEstimate,
15266
15416
  truncated: heading.truncated,
15267
- recovery: buildRecoveryDetails(attempts, null)
15417
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15418
+ ...input.diagnostic && {
15419
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15420
+ }
15268
15421
  }
15269
15422
  };
15270
15423
  }
@@ -15304,8 +15457,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15304
15457
  maxResults: limit,
15305
15458
  heading,
15306
15459
  includeExactSearchHandoff: true,
15307
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15308
- })
15460
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15461
+ ...input.diagnostic ? {
15462
+ trace: (trace) => {
15463
+ const attemptState = findSuccessfulAttemptState("conceptual");
15464
+ if (attemptState) {
15465
+ attemptState.contextPackTrace = trace;
15466
+ }
15467
+ }
15468
+ } : void 0
15469
+ }),
15470
+ findSuccessfulAttemptState("conceptual")
15309
15471
  );
15310
15472
  }
15311
15473
  }
@@ -15321,7 +15483,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15321
15483
  tokenBudget: fallbackText.tokenBudget,
15322
15484
  tokenEstimate: fallbackText.tokenEstimate,
15323
15485
  truncated: fallbackText.truncated,
15324
- recovery: buildRecoveryDetails(attempts, null)
15486
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15487
+ ...input.diagnostic && {
15488
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15489
+ }
15325
15490
  }
15326
15491
  };
15327
15492
  }
@@ -15402,17 +15567,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
15402
15567
  details: fittedDetails("path", fitted, 0)
15403
15568
  };
15404
15569
  }
15405
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15406
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
15570
+ return resolveSearchContext({
15571
+ query: input.query,
15572
+ symbol,
15573
+ limit,
15574
+ tokenBudget,
15575
+ fileType,
15576
+ directory,
15577
+ diagnostic: input.diagnostic
15578
+ }, {
15579
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
15407
15580
  limit: retrievalLimit,
15408
15581
  fileType: scope.fileType,
15409
- directory: scope.directory
15582
+ directory: scope.directory,
15583
+ trace
15410
15584
  }),
15411
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
15585
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
15412
15586
  limit: retrievalLimit,
15413
15587
  fileType: scope.fileType,
15414
15588
  directory: scope.directory,
15415
- metadataOnly: true
15589
+ metadataOnly: true,
15590
+ trace
15416
15591
  })
15417
15592
  });
15418
15593
  }
@@ -15477,6 +15652,181 @@ async function resolveCodebaseContext(projectRoot, host, input) {
15477
15652
  }
15478
15653
  }
15479
15654
 
15655
+ // src/tools/edit-context.ts
15656
+ function edgeLimit(value) {
15657
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15658
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15659
+ }
15660
+ return Math.min(
15661
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15662
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15663
+ );
15664
+ }
15665
+ function normalizedPath(value) {
15666
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15667
+ }
15668
+ function pathsMatch(left, right) {
15669
+ const normalizedLeft = normalizedPath(left);
15670
+ const normalizedRight = normalizedPath(right);
15671
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15672
+ }
15673
+ function targetSource(results, resolution) {
15674
+ return results.find((result) => pathsMatch(result.filePath, resolution.filePath) && result.startLine <= resolution.startLine && result.endLine >= resolution.startLine) ?? results.find((result) => pathsMatch(result.filePath, resolution.filePath) && result.name === resolution.name);
15675
+ }
15676
+ function formatSource(result) {
15677
+ const name = result.name ? ` ${result.name}` : "";
15678
+ return [
15679
+ "## Target implementation",
15680
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15681
+ "```",
15682
+ result.content,
15683
+ "```"
15684
+ ].join("\n");
15685
+ }
15686
+ function formatCallers(edges) {
15687
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15688
+ return [
15689
+ "## Direct callers",
15690
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15691
+ ].join("\n");
15692
+ }
15693
+ function formatCallees(edges, sourceFilePath) {
15694
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15695
+ return [
15696
+ "## Direct callees",
15697
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15698
+ ].join("\n");
15699
+ }
15700
+ function formatResolutionRisk(resolution) {
15701
+ if (resolution.status === "ambiguous") {
15702
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15703
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15704
+ }
15705
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15706
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15707
+ }
15708
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15709
+ }
15710
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15711
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15712
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15713
+ tokenBudget: tokenBudget ?? void 0,
15714
+ heading: "## Conceptual evidence",
15715
+ maxResults: 5,
15716
+ includeExactSearchHandoff: false,
15717
+ preferImplementationPaths: true
15718
+ });
15719
+ const fitted = fitTextToContextBudget(`${risk}
15720
+
15721
+ ${pack.text}`, tokenBudget ?? void 0);
15722
+ return {
15723
+ text: fitted.text,
15724
+ details: {
15725
+ resolution,
15726
+ tokenBudget: fitted.tokenBudget,
15727
+ tokenEstimate: fitted.tokenEstimate,
15728
+ truncated: fitted.truncated,
15729
+ sourceIncluded: candidateSource.length > 0,
15730
+ callerCount: 0,
15731
+ calleeCount: 0
15732
+ }
15733
+ };
15734
+ }
15735
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15736
+ const symbol = input.symbol?.trim();
15737
+ if (!symbol) {
15738
+ return fallbackPack(
15739
+ dependencies,
15740
+ input.query,
15741
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15742
+ "not_requested",
15743
+ input.tokenBudget
15744
+ );
15745
+ }
15746
+ let callersResult;
15747
+ try {
15748
+ callersResult = await dependencies.getCallGraphData({
15749
+ name: symbol,
15750
+ filePath: input.filePath ?? void 0,
15751
+ direction: "callers"
15752
+ });
15753
+ } catch (error) {
15754
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15755
+ const message = error instanceof Error ? error.message : String(error);
15756
+ return fallbackPack(
15757
+ dependencies,
15758
+ input.query,
15759
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15760
+ "graph_unavailable",
15761
+ input.tokenBudget,
15762
+ candidates
15763
+ );
15764
+ }
15765
+ if (callersResult.resolution.status !== "resolved") {
15766
+ return fallbackPack(
15767
+ dependencies,
15768
+ input.query,
15769
+ formatResolutionRisk(callersResult.resolution),
15770
+ callersResult.resolution.status,
15771
+ input.tokenBudget
15772
+ );
15773
+ }
15774
+ const resolution = callersResult.resolution;
15775
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15776
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15777
+ dependencies.getCallGraphData({
15778
+ name: symbol,
15779
+ filePath: input.filePath ?? resolution.filePath,
15780
+ direction: "callees"
15781
+ })
15782
+ ]);
15783
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15784
+ let graphRisk;
15785
+ let callees = [];
15786
+ if (calleesResult.status === "rejected") {
15787
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15788
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15789
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15790
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15791
+ } else {
15792
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15793
+ }
15794
+ const source = targetSource(definitionsResult.value, resolution);
15795
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15796
+ const sourceBudget = Math.max(
15797
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15798
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15799
+ );
15800
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15801
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15802
+ const fitted = fitTextToContextBudget([
15803
+ `# Pre-edit context for ${resolution.name}`,
15804
+ graphRisk,
15805
+ sourceText,
15806
+ formatCallers(callers),
15807
+ formatCallees(callees, resolution.filePath)
15808
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15809
+ return {
15810
+ text: fitted.text,
15811
+ details: {
15812
+ resolution: "resolved",
15813
+ tokenBudget: fitted.tokenBudget,
15814
+ tokenEstimate: fitted.tokenEstimate,
15815
+ truncated: fitted.truncated,
15816
+ sourceIncluded: source !== void 0,
15817
+ callerCount: callers.length,
15818
+ calleeCount: callees.length
15819
+ }
15820
+ };
15821
+ }
15822
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
15823
+ return resolveCodebaseEditContextWithDependencies(input, {
15824
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
15825
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
15826
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
15827
+ });
15828
+ }
15829
+
15480
15830
  // src/eval/budget.ts
15481
15831
  function evaluateBudgetGate(budget, summary, comparison) {
15482
15832
  const BASELINE_P95_EPSILON_MS = 1e-3;
@@ -15500,6 +15850,24 @@ function evaluateBudgetGate(budget, summary, comparison) {
15500
15850
  message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
15501
15851
  });
15502
15852
  }
15853
+ if (thresholds.minGraphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall < thresholds.minGraphNeighborRecall) {
15854
+ violations.push({
15855
+ metric: "minGraphNeighborRecall",
15856
+ message: `Graph-neighbor recall ${summary.metrics.graphNeighborRecall.toFixed(4)} is below minimum ${thresholds.minGraphNeighborRecall.toFixed(4)}`
15857
+ });
15858
+ }
15859
+ if (thresholds.minRouteAccuracy !== void 0 && summary.metrics.routeAccuracy < thresholds.minRouteAccuracy) {
15860
+ violations.push({
15861
+ metric: "minRouteAccuracy",
15862
+ message: `Route accuracy ${summary.metrics.routeAccuracy.toFixed(4)} is below minimum ${thresholds.minRouteAccuracy.toFixed(4)}`
15863
+ });
15864
+ }
15865
+ if (thresholds.minOutcomeAccuracy !== void 0 && summary.metrics.outcomeAccuracy < thresholds.minOutcomeAccuracy) {
15866
+ violations.push({
15867
+ metric: "minOutcomeAccuracy",
15868
+ message: `Outcome accuracy ${summary.metrics.outcomeAccuracy.toFixed(4)} is below minimum ${thresholds.minOutcomeAccuracy.toFixed(4)}`
15869
+ });
15870
+ }
15503
15871
  if (comparison) {
15504
15872
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
15505
15873
  violations.push({
@@ -15677,6 +16045,14 @@ function isSymbolIntended(query) {
15677
16045
  function isExpectedFile(filePath, relevant) {
15678
16046
  return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
15679
16047
  }
16048
+ function graphNeighborMatches(query, result) {
16049
+ const expected = query.expected.graphNeighbor;
16050
+ if (!expected || result.graphDirection !== expected.direction) return false;
16051
+ if (expected.filePath !== void 0 && !pathMatchesExpected(result.filePath, expected.filePath)) {
16052
+ return false;
16053
+ }
16054
+ return expected.symbol === void 0 || result.name === expected.symbol;
16055
+ }
15680
16056
  function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
15681
16057
  let relevance = 0;
15682
16058
  for (const entry of relevant) {
@@ -15824,6 +16200,7 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
15824
16200
  routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
15825
16201
  outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
15826
16202
  recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
16203
+ graphNeighborMatched: query.expected.graphNeighbor === void 0 ? void 0 : results.some((result) => graphNeighborMatches(query, result)),
15827
16204
  language: query.language,
15828
16205
  difficulty: query.difficulty,
15829
16206
  tags: query.tags,
@@ -15873,7 +16250,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15873
16250
  "no-relevant-hit-top-k": 0
15874
16251
  };
15875
16252
  const latencies = perQuery.map((item) => item.latencyMs);
15876
- const contextQueries = perQuery.filter((item) => item.retrievalMode === "context");
16253
+ const contextQueries = perQuery.filter((item) => item.retrievalMode !== "search");
15877
16254
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
15878
16255
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
15879
16256
  const contextTokenUnits = totalContextResponseTokens / 1e3;
@@ -15883,6 +16260,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15883
16260
  let outcomeExpectedCount = 0;
15884
16261
  let recoveryMatchedCount = 0;
15885
16262
  let recoveryExpectedCount = 0;
16263
+ let graphNeighborMatchedCount = 0;
16264
+ let graphNeighborExpectedCount = 0;
15886
16265
  for (const query of perQuery) {
15887
16266
  if (positiveQueryIds.has(query.id)) {
15888
16267
  if (query.hitAt1) sum.hitAt1 += 1;
@@ -15911,6 +16290,10 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15911
16290
  recoveryExpectedCount += 1;
15912
16291
  if (query.recoveryMatched) recoveryMatchedCount += 1;
15913
16292
  }
16293
+ if (query.graphNeighborMatched !== void 0) {
16294
+ graphNeighborExpectedCount += 1;
16295
+ if (query.graphNeighborMatched) graphNeighborMatchedCount += 1;
16296
+ }
15914
16297
  }
15915
16298
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
15916
16299
  return {
@@ -15923,6 +16306,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15923
16306
  routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
15924
16307
  outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
15925
16308
  recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
16309
+ graphNeighborRecall: graphNeighborExpectedCount === 0 ? 0 : graphNeighborMatchedCount / graphNeighborExpectedCount,
15926
16310
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
15927
16311
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
15928
16312
  latencyMs: {
@@ -16188,14 +16572,29 @@ function parseQueryArgs(value, path31) {
16188
16572
  throw new Error(`${path31} must be an object`);
16189
16573
  }
16190
16574
  const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16575
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16191
16576
  const fileType = parseStringOrUndefined(value.fileType, `${path31}.fileType`);
16192
16577
  const directory = parseStringOrUndefined(value.directory, `${path31}.directory`);
16578
+ const callerLimit = parsePositiveIntegerOrUndefined(value.callerLimit, `${path31}.callerLimit`);
16579
+ const calleeLimit = parsePositiveIntegerOrUndefined(value.calleeLimit, `${path31}.calleeLimit`);
16580
+ const tokenBudget = parsePositiveIntegerOrUndefined(value.tokenBudget, `${path31}.tokenBudget`);
16193
16581
  return {
16194
16582
  ...symbol !== void 0 ? { symbol } : {},
16583
+ ...filePath !== void 0 ? { filePath } : {},
16195
16584
  ...fileType !== void 0 ? { fileType } : {},
16196
- ...directory !== void 0 ? { directory } : {}
16585
+ ...directory !== void 0 ? { directory } : {},
16586
+ ...callerLimit !== void 0 ? { callerLimit } : {},
16587
+ ...calleeLimit !== void 0 ? { calleeLimit } : {},
16588
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
16197
16589
  };
16198
16590
  }
16591
+ function parsePositiveIntegerOrUndefined(value, path31) {
16592
+ if (value === void 0 || value === null) return void 0;
16593
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
16594
+ throw new Error(`${path31} must be a positive integer`);
16595
+ }
16596
+ return value;
16597
+ }
16199
16598
  var SEMVER_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
16200
16599
  function parseSemanticVersion(value, path31) {
16201
16600
  if (!isNonEmptyString(value)) {
@@ -16208,8 +16607,8 @@ function parseSemanticVersion(value, path31) {
16208
16607
  }
16209
16608
  function parseRetrievalMode(value, path31) {
16210
16609
  if (value === void 0 || value === "search") return "search";
16211
- if (value === "context") return value;
16212
- throw new Error(`${path31} must be one of: search, context`);
16610
+ if (value === "context" || value === "edit-context") return value;
16611
+ throw new Error(`${path31} must be one of: search, context, edit-context`);
16213
16612
  }
16214
16613
  function parseStringOrUndefined(value, path31) {
16215
16614
  if (value === void 0 || value === null) return void 0;
@@ -16249,6 +16648,25 @@ function parseEvidenceRelevance(value, path31) {
16249
16648
  }
16250
16649
  return value;
16251
16650
  }
16651
+ function parseExpectedGraphNeighbor(value, path31) {
16652
+ if (value === void 0) return void 0;
16653
+ if (!isRecord3(value)) {
16654
+ throw new Error(`${path31} must be an object`);
16655
+ }
16656
+ if (value.direction !== "caller" && value.direction !== "callee") {
16657
+ throw new Error(`${path31}.direction must be one of: caller, callee`);
16658
+ }
16659
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16660
+ const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16661
+ if (filePath === void 0 && symbol === void 0) {
16662
+ throw new Error(`${path31} must include filePath or symbol`);
16663
+ }
16664
+ return {
16665
+ direction: value.direction,
16666
+ ...filePath !== void 0 ? { filePath } : {},
16667
+ ...symbol !== void 0 ? { symbol } : {}
16668
+ };
16669
+ }
16252
16670
  function parseExpected(input, path31) {
16253
16671
  if (!isRecord3(input)) {
16254
16672
  throw new Error(`${path31} must be an object`);
@@ -16261,9 +16679,11 @@ function parseExpected(input, path31) {
16261
16679
  const expectedOutcomeRaw = input.expectedOutcome;
16262
16680
  const recoveryExpectationRaw = input.recoveryExpectation;
16263
16681
  const gradedEvidenceRaw = input.gradedEvidence;
16682
+ const graphNeighborRaw = input.graphNeighbor;
16264
16683
  const filePath = parseStringOrUndefined(filePathRaw, `${path31}.filePath`);
16265
16684
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
16266
16685
  const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path31}.gradedEvidence`);
16686
+ const graphNeighbor = parseExpectedGraphNeighbor(graphNeighborRaw, `${path31}.graphNeighbor`);
16267
16687
  const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path31}.expectedOutcome`);
16268
16688
  if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
16269
16689
  throw new Error(
@@ -16292,7 +16712,8 @@ function parseExpected(input, path31) {
16292
16712
  expectedRoute,
16293
16713
  expectedOutcome,
16294
16714
  recoveryExpectation,
16295
- ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
16715
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {},
16716
+ ...graphNeighbor !== void 0 ? { graphNeighbor } : {}
16296
16717
  };
16297
16718
  }
16298
16719
  function parseQueryLanguage(value, path31) {
@@ -16427,6 +16848,21 @@ function parseBudget(raw, sourceLabel) {
16427
16848
  "minRawDistinctTop3Ratio",
16428
16849
  sourceLabel
16429
16850
  ),
16851
+ minGraphNeighborRecall: parseThresholdValue(
16852
+ thresholds.minGraphNeighborRecall,
16853
+ "minGraphNeighborRecall",
16854
+ sourceLabel
16855
+ ),
16856
+ minRouteAccuracy: parseThresholdValue(
16857
+ thresholds.minRouteAccuracy,
16858
+ "minRouteAccuracy",
16859
+ sourceLabel
16860
+ ),
16861
+ minOutcomeAccuracy: parseThresholdValue(
16862
+ thresholds.minOutcomeAccuracy,
16863
+ "minOutcomeAccuracy",
16864
+ sourceLabel
16865
+ ),
16430
16866
  maxContextResponseTokensAverage: parseThresholdValue(
16431
16867
  thresholds.maxContextResponseTokensAverage,
16432
16868
  "maxContextResponseTokensAverage",
@@ -16491,6 +16927,120 @@ function buildDatasetFingerprint(dataset) {
16491
16927
  const canonical = JSON.stringify(normalizeForFingerprint(dataset));
16492
16928
  return crypto2.createHash("sha256").update(canonical).digest("hex");
16493
16929
  }
16930
+ function normalizedPath2(value) {
16931
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
16932
+ }
16933
+ function pathsMatch2(left, right) {
16934
+ const normalizedLeft = normalizedPath2(left);
16935
+ const normalizedRight = normalizedPath2(right);
16936
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
16937
+ }
16938
+ function toEvalSearchResult(result) {
16939
+ return {
16940
+ filePath: result.filePath,
16941
+ startLine: result.startLine,
16942
+ endLine: result.endLine,
16943
+ score: result.score,
16944
+ chunkType: result.chunkType,
16945
+ name: result.name
16946
+ };
16947
+ }
16948
+ function selectResolvedTarget(definitions, resolution) {
16949
+ if (resolution?.status !== "resolved") return definitions[0];
16950
+ return definitions.find((result) => pathsMatch2(result.filePath, resolution.filePath) && result.startLine <= resolution.startLine && result.endLine >= resolution.startLine) ?? definitions.find((result) => pathsMatch2(result.filePath, resolution.filePath) && result.name === resolution.name) ?? definitions[0];
16951
+ }
16952
+ function callerResult(edge) {
16953
+ if (!edge.fromSymbolFilePath) return void 0;
16954
+ return {
16955
+ filePath: edge.fromSymbolFilePath,
16956
+ startLine: edge.line,
16957
+ endLine: edge.line,
16958
+ score: 0,
16959
+ chunkType: "graph-caller",
16960
+ name: edge.fromSymbolName,
16961
+ graphDirection: "caller"
16962
+ };
16963
+ }
16964
+ function calleeResult(edge, symbols) {
16965
+ const symbol = edge.toSymbolId ? symbols.find((candidate) => candidate.id === edge.toSymbolId) : symbols.filter((candidate) => candidate.name === edge.targetName).length === 1 ? symbols.find((candidate) => candidate.name === edge.targetName) : void 0;
16966
+ if (!symbol) return void 0;
16967
+ return {
16968
+ filePath: symbol.filePath,
16969
+ startLine: symbol.startLine,
16970
+ endLine: symbol.endLine,
16971
+ score: 0,
16972
+ chunkType: "graph-callee",
16973
+ name: symbol.name,
16974
+ graphDirection: "callee"
16975
+ };
16976
+ }
16977
+ async function runEditContextQuery(indexer, projectRoot, query) {
16978
+ let definitions = [];
16979
+ let conceptual = [];
16980
+ let callers;
16981
+ let callees;
16982
+ const editContext = await resolveCodebaseEditContextWithDependencies({
16983
+ query: query.query,
16984
+ symbol: query.args?.symbol,
16985
+ filePath: query.args?.filePath ?? query.expected.filePath,
16986
+ callerLimit: query.args?.callerLimit,
16987
+ calleeLimit: query.args?.calleeLimit,
16988
+ tokenBudget: query.args?.tokenBudget
16989
+ }, {
16990
+ searchCodebase: async (searchQuery, options) => {
16991
+ conceptual = await indexer.search(searchQuery, options?.limit, {
16992
+ filterByBranch: !!query.expected.branch
16993
+ });
16994
+ return conceptual;
16995
+ },
16996
+ implementationLookup: async (symbol, options) => {
16997
+ definitions = await indexer.search(symbol, options?.limit, {
16998
+ filterByBranch: !!query.expected.branch,
16999
+ definitionIntent: true
17000
+ });
17001
+ return definitions;
17002
+ },
17003
+ getCallGraphData: async (params) => {
17004
+ const result = await getCallGraphDataForIndexer(indexer, projectRoot, params);
17005
+ if (params.direction === "callers") callers = result;
17006
+ else callees = result;
17007
+ return result;
17008
+ }
17009
+ });
17010
+ const resolution = callers?.resolution;
17011
+ const target = selectResolvedTarget(definitions, resolution);
17012
+ const targetCandidates = target ? [target] : [...definitions, ...conceptual];
17013
+ const results = targetCandidates.filter((candidate) => (resolution?.status !== "resolved" || editContext.details.sourceIncluded) && editContext.text.includes(
17014
+ `${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
17015
+ )).map(toEvalSearchResult);
17016
+ if (query.expected.graphNeighbor) {
17017
+ const symbols = await indexer.getCallGraphSymbols();
17018
+ const callerLimit = query.args?.callerLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17019
+ const calleeLimit = query.args?.calleeLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17020
+ const publishedCallers = (callers?.callers ?? []).slice(0, callerLimit).filter((edge) => editContext.text.includes(
17021
+ `${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
17022
+ ));
17023
+ const publishedCallees = (callees?.callees ?? []).slice(0, calleeLimit).filter((edge) => resolution?.status === "resolved" && editContext.text.includes(
17024
+ `${edge.targetName} from ${resolution.filePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
17025
+ ));
17026
+ results.push(
17027
+ ...publishedCallers.map(callerResult).filter((item) => item !== void 0),
17028
+ ...publishedCallees.map((edge) => calleeResult(edge, symbols)).filter((item) => item !== void 0)
17029
+ );
17030
+ }
17031
+ return {
17032
+ results,
17033
+ resolvedRoute: resolution?.status === "resolved" ? "definition" : "search",
17034
+ routedQuery: query.args?.symbol ?? query.query,
17035
+ context: {
17036
+ tokenBudget: editContext.details.tokenBudget,
17037
+ responseTokens: editContext.details.tokenEstimate,
17038
+ candidateCount: results.length,
17039
+ deduplicatedCount: results.length,
17040
+ omittedCount: 0
17041
+ }
17042
+ };
17043
+ }
16494
17044
  async function runEvaluation(options) {
16495
17045
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
16496
17046
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -16522,6 +17072,7 @@ async function runEvaluation(options) {
16522
17072
  );
16523
17073
  }
16524
17074
  const start = import_perf_hooks2.performance.now();
17075
+ const editContextResult = query.retrievalMode === "edit-context" ? await runEditContextQuery(indexer, options.projectRoot, query) : void 0;
16525
17076
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
16526
17077
  query: query.query,
16527
17078
  symbol: query.args?.symbol,
@@ -16545,31 +17096,32 @@ async function runEvaluation(options) {
16545
17096
  directory: scope.directory
16546
17097
  })
16547
17098
  }) : void 0;
16548
- const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
17099
+ const result = editContextResult?.results ?? contextResult?.details?.results ?? await indexer.search(query.query, 10, {
16549
17100
  metadataOnly: true,
16550
17101
  filterByBranch: !!query.expected.branch,
16551
17102
  fileType: query.args?.fileType,
16552
17103
  directory: query.args?.directory
16553
17104
  });
16554
17105
  const elapsed = import_perf_hooks2.performance.now() - start;
16555
- const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
16556
- const routedQuery = contextResult?.details?.routedQuery ?? query.query;
17106
+ const resolvedRoute = editContextResult?.resolvedRoute ?? (contextResult?.details?.route === "definition" ? "definition" : "search");
17107
+ const routedQuery = editContextResult?.routedQuery ?? contextResult?.details?.routedQuery ?? query.query;
16557
17108
  const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
16558
17109
  const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
16559
17110
  const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
16560
17111
  const recoveryUsed = recoveryAttempts.length > 1 || recoveryAttempts.some((attempt) => attempt.relaxedFields.length > 0);
16561
- const materialized = result.map((item) => ({
16562
- filePath: item.filePath,
16563
- startLine: item.startLine,
16564
- endLine: item.endLine,
16565
- score: item.score,
16566
- chunkType: item.chunkType,
16567
- name: item.name
16568
- }));
16569
- perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
16570
- resolvedRoute,
16571
- routedQuery
16572
- }, contextResult?.details ? {
17112
+ const materialized = result.map((item) => {
17113
+ const graphDirection = "graphDirection" in item && (item.graphDirection === "caller" || item.graphDirection === "callee") ? item.graphDirection : void 0;
17114
+ return {
17115
+ filePath: item.filePath,
17116
+ startLine: item.startLine,
17117
+ endLine: item.endLine,
17118
+ score: item.score,
17119
+ chunkType: item.chunkType,
17120
+ name: item.name,
17121
+ graphDirection
17122
+ };
17123
+ });
17124
+ const contextMeasurement = editContextResult?.context ?? (contextResult?.details ? {
16573
17125
  tokenBudget: contextResult.details.tokenBudget,
16574
17126
  responseTokens: contextResult.details.tokenEstimate,
16575
17127
  candidateCount: contextResult.details.candidateCount ?? 0,
@@ -16577,7 +17129,11 @@ async function runEvaluation(options) {
16577
17129
  omittedCount: contextResult.details.omittedCount ?? 0,
16578
17130
  recoveryUsed,
16579
17131
  recoveryRelaxed
16580
- } : void 0));
17132
+ } : void 0);
17133
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
17134
+ resolvedRoute,
17135
+ routedQuery
17136
+ }, contextMeasurement));
16581
17137
  }
16582
17138
  const logger = indexer.getLogger();
16583
17139
  const metricSnapshot = logger.getMetrics();
@@ -17114,7 +17670,11 @@ var import_zod2 = require("zod");
17114
17670
 
17115
17671
  // src/tools/execute-common.ts
17116
17672
  async function executeCodebaseContext(projectRoot, host, args) {
17117
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17673
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17674
+ return { text: result.text, details: result.details };
17675
+ }
17676
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17677
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17118
17678
  }
17119
17679
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17120
17680
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17230,6 +17790,7 @@ function formatPrImpact(result) {
17230
17790
  // src/tools/tool-names.ts
17231
17791
  var TOOL_NAME = {
17232
17792
  CODEBASE_CONTEXT: "codebase_context",
17793
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
17233
17794
  CODEBASE_SEARCH: "codebase_search",
17234
17795
  CODEBASE_PEEK: "codebase_peek",
17235
17796
  FIND_SIMILAR: "find_similar",
@@ -17253,6 +17814,7 @@ var TOOL_NAME = {
17253
17814
  };
17254
17815
  var PORTABLE_TOOL_NAMES = [
17255
17816
  TOOL_NAME.CODEBASE_CONTEXT,
17817
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17256
17818
  TOOL_NAME.CODEBASE_SEARCH,
17257
17819
  TOOL_NAME.CODEBASE_PEEK,
17258
17820
  TOOL_NAME.INDEX_CODEBASE,
@@ -17269,6 +17831,7 @@ var PORTABLE_TOOL_NAMES = [
17269
17831
  ];
17270
17832
  var OPENCODE_TOOL_NAMES = [
17271
17833
  TOOL_NAME.CODEBASE_CONTEXT,
17834
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17272
17835
  TOOL_NAME.CODEBASE_SEARCH,
17273
17836
  TOOL_NAME.CODEBASE_PEEK,
17274
17837
  TOOL_NAME.INDEX_CODEBASE,
@@ -17289,6 +17852,7 @@ var OPENCODE_TOOL_NAMES = [
17289
17852
  ];
17290
17853
  var PI_TOOL_NAMES = [
17291
17854
  TOOL_NAME.CODEBASE_CONTEXT,
17855
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17292
17856
  TOOL_NAME.CODEBASE_SEARCH,
17293
17857
  TOOL_NAME.CODEBASE_PEEK,
17294
17858
  TOOL_NAME.FIND_SIMILAR,
@@ -17332,10 +17896,30 @@ function registerMcpTools(server, runtime) {
17332
17896
  directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
17333
17897
  tokenBudget: allowNullAsUndefined(
17334
17898
  import_zod2.z.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
17335
- ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`)
17899
+ ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
17900
+ diagnostic: import_zod2.z.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
17336
17901
  },
17337
17902
  async (args) => {
17338
17903
  const result = await executeCodebaseContext(runtime.projectRoot, runtime.host, args);
17904
+ return {
17905
+ content: [{ type: "text", text: result.text }],
17906
+ ...args.diagnostic ? { structuredContent: result.details } : {}
17907
+ };
17908
+ }
17909
+ );
17910
+ server.tool(
17911
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17912
+ "PRE-EDIT TOOL for a known or suspected symbol. Returns token-bounded target source, direct callers and callees, or a risk-marked conceptual fallback when resolution is unsafe.",
17913
+ {
17914
+ query: import_zod2.z.string().describe("The requested change or target behavior."),
17915
+ symbol: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Authoritative target symbol when known."),
17916
+ filePath: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Optional file path used to disambiguate duplicate symbol names."),
17917
+ callerLimit: allowNullAsUndefined(import_zod2.z.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT)),
17918
+ calleeLimit: allowNullAsUndefined(import_zod2.z.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT)),
17919
+ tokenBudget: allowNullAsUndefined(import_zod2.z.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET))
17920
+ },
17921
+ async (args) => {
17922
+ const result = await executeCodebaseEditContext(runtime.projectRoot, runtime.host, args);
17339
17923
  return { content: [{ type: "text", text: result.text }] };
17340
17924
  }
17341
17925
  );
@@ -19792,9 +20376,9 @@ function parseGitActivity(output) {
19792
20376
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
19793
20377
  const churn = Number(addedRaw) + Number(deletedRaw);
19794
20378
  if (!Number.isFinite(churn) || churn <= 0) continue;
19795
- const normalizedPath = normalizePath4(filePath);
19796
- const previous = activity.get(normalizedPath);
19797
- activity.set(normalizedPath, {
20379
+ const normalizedPath3 = normalizePath4(filePath);
20380
+ const previous = activity.get(normalizedPath3);
20381
+ activity.set(normalizedPath3, {
19798
20382
  churn: (previous?.churn ?? 0) + churn,
19799
20383
  commits: (previous?.commits ?? 0) + 1,
19800
20384
  latestDate: previous?.latestDate ?? latestDate,
@@ -20384,8 +20968,8 @@ function transformForVisualization(symbols, edges, options = {}) {
20384
20968
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
20385
20969
  filteredSymbols = symbols.filter(
20386
20970
  (s) => {
20387
- const normalizedPath = s.filePath.replace(/\\/g, "/");
20388
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
20971
+ const normalizedPath3 = s.filePath.replace(/\\/g, "/");
20972
+ return normalizedPath3 === normalizedDir || normalizedPath3.startsWith(normalizedDirWithSlash) || normalizedPath3.endsWith(`/${normalizedDir}`) || normalizedPath3.includes(normalizedAbsoluteSuffix);
20389
20973
  }
20390
20974
  );
20391
20975
  }