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.js CHANGED
@@ -1277,6 +1277,7 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1277
1277
  lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
1278
1278
  lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
1279
1279
  lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
1280
+ lines.push(`| Graph-neighbor recall | ${(summary.metrics.graphNeighborRecall ?? 0).toFixed(4)} |`);
1280
1281
  lines.push(`| Distinct Top@3 | ${formatPct(summary.metrics.distinctTop3Ratio)} |`);
1281
1282
  lines.push(`| Raw Distinct Top@3 | ${formatPct(summary.metrics.rawDistinctTop3Ratio)} |`);
1282
1283
  lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
@@ -6640,12 +6641,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
6640
6641
  return [...primary, ...remainder];
6641
6642
  }
6642
6643
  function buildDiversityKey(metadata) {
6643
- const normalizedPath = metadata.filePath.toLowerCase();
6644
+ const normalizedPath3 = metadata.filePath.toLowerCase();
6644
6645
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
6645
6646
  if (normalizedName.length > 0) {
6646
- return `${normalizedPath}#${normalizedName}`;
6647
+ return `${normalizedPath3}#${normalizedName}`;
6647
6648
  }
6648
- return normalizedPath;
6649
+ return normalizedPath3;
6649
6650
  }
6650
6651
  function rankHybridResults(query, semanticResults, keywordResults, options) {
6651
6652
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -7051,6 +7052,29 @@ function extractPrimaryIdentifierQueryHint(query) {
7051
7052
  const best = codeTerms.find((term) => term.length >= 6);
7052
7053
  return best ?? null;
7053
7054
  }
7055
+ function pathSegmentsForAffinityMatch(filePath) {
7056
+ const normalizedPath3 = normalizeRankingText(filePath).replace(/\\/g, "/");
7057
+ const segments = normalizedPath3.split("/").filter((segment) => segment.length > 0);
7058
+ if (segments.length === 0) {
7059
+ return [];
7060
+ }
7061
+ const basename8 = segments[segments.length - 1] ?? "";
7062
+ const basenameWithoutExt = basename8.replace(/\.[^/.]+$/u, "");
7063
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
7064
+ return Array.from(/* @__PURE__ */ new Set([
7065
+ ...normalizedSegments,
7066
+ basenameWithoutExt.toLowerCase()
7067
+ ]));
7068
+ }
7069
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
7070
+ const haystack = pathSegmentsForAffinityMatch(filePath);
7071
+ return exactIdentifierVariants.some((variant) => {
7072
+ if (!variant || variant.length < 2) {
7073
+ return false;
7074
+ }
7075
+ return haystack.includes(variant);
7076
+ });
7077
+ }
7054
7078
  var FILE_PATH_HINT_EXTENSIONS = [
7055
7079
  "ts",
7056
7080
  "tsx",
@@ -7094,9 +7118,9 @@ function normalizeFilePathForHintMatch(filePath) {
7094
7118
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
7095
7119
  }
7096
7120
  function pathMatchesHint(filePath, hint) {
7097
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
7121
+ const normalizedPath3 = normalizeFilePathForHintMatch(filePath);
7098
7122
  const normalizedHint = normalizeFilePathForHintMatch(hint);
7099
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
7123
+ return normalizedPath3.endsWith(normalizedHint) || normalizedPath3.includes(`/${normalizedHint}`) || normalizedPath3.includes(normalizedHint);
7100
7124
  }
7101
7125
  function extractFilePathHint(query) {
7102
7126
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -7126,10 +7150,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
7126
7150
  ).map((candidate) => {
7127
7151
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
7128
7152
  const pathLower = candidate.metadata.filePath.toLowerCase();
7129
- let maxMatch = 0;
7130
- const nameMatchesPrimary = primaryVariants.some(
7153
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
7154
+ const exactMatch = exactIdentifierVariants.some(
7131
7155
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
7132
7156
  );
7157
+ let maxMatch = 0;
7158
+ const nameMatchesPrimary = exactMatch;
7159
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
7133
7160
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
7134
7161
  for (const hint of hints) {
7135
7162
  const variants = normalizeIdentifierVariants(hint);
@@ -7150,12 +7177,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
7150
7177
  candidate,
7151
7178
  maxMatch,
7152
7179
  pathMatchesFileHint,
7153
- nameMatchesPrimary
7180
+ nameMatchesPrimary,
7181
+ pathAffinity
7154
7182
  };
7155
7183
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
7156
7184
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
7157
7185
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
7158
7186
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
7187
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
7188
+ return b.nameMatchesPrimary ? 1 : -1;
7189
+ }
7190
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
7159
7191
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
7160
7192
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
7161
7193
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -11370,6 +11402,20 @@ var Indexer = class _Indexer {
11370
11402
  requestedLimit = nextLimit;
11371
11403
  }
11372
11404
  }
11405
+ buildCandidateSnapshot(candidate) {
11406
+ return {
11407
+ id: candidate.id,
11408
+ filePath: candidate.metadata.filePath,
11409
+ startLine: candidate.metadata.startLine,
11410
+ endLine: candidate.metadata.endLine,
11411
+ score: candidate.score,
11412
+ chunkType: candidate.metadata.chunkType,
11413
+ name: candidate.metadata.name
11414
+ };
11415
+ }
11416
+ buildCandidateSnapshotList(candidates) {
11417
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
11418
+ }
11373
11419
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
11374
11420
  return this.searchCandidatesWithBranchPrefilter(
11375
11421
  initialLimit,
@@ -11561,6 +11607,16 @@ var Indexer = class _Indexer {
11561
11607
  prefilterMs: Math.round(prefilterMs * 100) / 100,
11562
11608
  fusionMs: Math.round(fusionMs * 100) / 100
11563
11609
  });
11610
+ if (options?.trace) {
11611
+ options.trace({
11612
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
11613
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
11614
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
11615
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
11616
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
11617
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
11618
+ });
11619
+ }
11564
11620
  const metadataOnly = options?.metadataOnly ?? false;
11565
11621
  return Promise.all(
11566
11622
  finalResults.map(async (r) => {
@@ -12609,6 +12665,42 @@ var Indexer = class _Indexer {
12609
12665
  }
12610
12666
  };
12611
12667
 
12668
+ // src/tools/contracts.ts
12669
+ var CHUNK_TYPES = [
12670
+ "function",
12671
+ "class",
12672
+ "method",
12673
+ "interface",
12674
+ "type",
12675
+ "enum",
12676
+ "struct",
12677
+ "impl",
12678
+ "trait",
12679
+ "module",
12680
+ "other"
12681
+ ];
12682
+ var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12683
+ var RELATIONSHIP_TYPES = [
12684
+ "Call",
12685
+ "MethodCall",
12686
+ "Constructor",
12687
+ "Import",
12688
+ "Inherits",
12689
+ "Implements"
12690
+ ];
12691
+ var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12692
+ var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12693
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
12694
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
12695
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
12696
+ var CODE_COMMUNITIES_MIN_SIZE = 1;
12697
+ var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12698
+ var CODE_COMMUNITIES_MAX_LIMIT = 100;
12699
+ var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12700
+ var CODE_COMMUNITIES_MIN_COUPLING = 1;
12701
+ var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12702
+ var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12703
+
12612
12704
  // src/tools/operations.ts
12613
12705
  import { existsSync as existsSync12, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
12614
12706
  import * as path21 from "path";
@@ -12737,39 +12829,6 @@ function formatCodeCommunities(result) {
12737
12829
  return lines.join("\n");
12738
12830
  }
12739
12831
 
12740
- // src/tools/contracts.ts
12741
- var CHUNK_TYPES = [
12742
- "function",
12743
- "class",
12744
- "method",
12745
- "interface",
12746
- "type",
12747
- "enum",
12748
- "struct",
12749
- "impl",
12750
- "trait",
12751
- "module",
12752
- "other"
12753
- ];
12754
- var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12755
- var RELATIONSHIP_TYPES = [
12756
- "Call",
12757
- "MethodCall",
12758
- "Constructor",
12759
- "Import",
12760
- "Inherits",
12761
- "Implements"
12762
- ];
12763
- var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12764
- var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12765
- var CODE_COMMUNITIES_MIN_SIZE = 1;
12766
- var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12767
- var CODE_COMMUNITIES_MAX_LIMIT = 100;
12768
- var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12769
- var CODE_COMMUNITIES_MIN_COUPLING = 1;
12770
- var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12771
- var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12772
-
12773
12832
  // src/tools/context-pack.ts
12774
12833
  import { get_encoding } from "tiktoken";
12775
12834
  var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
@@ -12871,6 +12930,16 @@ function compactEvidenceValue(value, maxChars) {
12871
12930
  }
12872
12931
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
12873
12932
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
12933
+ function toContextPackTraceCandidate(result) {
12934
+ return {
12935
+ filePath: result.filePath,
12936
+ startLine: result.startLine,
12937
+ endLine: result.endLine,
12938
+ score: result.score,
12939
+ chunkType: result.chunkType,
12940
+ name: result.name
12941
+ };
12942
+ }
12874
12943
  function formatExactSearchHandoff(results) {
12875
12944
  const suggestedNames = [];
12876
12945
  const seen = /* @__PURE__ */ new Set();
@@ -12919,13 +12988,13 @@ function buildContextPack(results, options = {}) {
12919
12988
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
12920
12989
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
12921
12990
  const candidateCount = results.length;
12922
- const deduplicated = deduplicateContextCandidates(
12923
- rankContextCandidates(
12924
- results,
12925
- options.preferImplementationPaths ?? false
12926
- )
12927
- );
12928
- const diversified = diversifyContextCandidates(deduplicated);
12991
+ const preserveInputOrder = options.preserveInputOrder ?? false;
12992
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
12993
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
12994
+ const deduplicated = deduplicateContextCandidates(ranked);
12995
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
12996
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
12997
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
12929
12998
  const duplicateCount = candidateCount - deduplicated.length;
12930
12999
  const selectable = diversified.slice(0, maxResults);
12931
13000
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -12958,6 +13027,15 @@ function buildContextPack(results, options = {}) {
12958
13027
  const fitted = fitTextToContextBudget(text, tokenBudget);
12959
13028
  const budgetOmittedCount = selectable.length - selected.length;
12960
13029
  const omittedCount = candidateCount - selected.length;
13030
+ if (options.trace) {
13031
+ options.trace({
13032
+ inputCandidates: results.map(toContextPackTraceCandidate),
13033
+ rankedCandidates,
13034
+ deduplicatedCandidates,
13035
+ diversifiedCandidates,
13036
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
13037
+ });
13038
+ }
12961
13039
  return {
12962
13040
  requestedTokenBudget,
12963
13041
  tokenBudget,
@@ -14705,7 +14783,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14705
14783
  definitionIntent: options.definitionIntent,
14706
14784
  blameAuthor: options.blameAuthor,
14707
14785
  blameSha: options.blameSha,
14708
- blameSince: options.blameSince
14786
+ blameSince: options.blameSince,
14787
+ trace: options.trace
14709
14788
  });
14710
14789
  }
14711
14790
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14759,15 +14838,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14759
14838
  return indexer.search(query, options.limit, {
14760
14839
  fileType: options.fileType,
14761
14840
  directory: options.directory,
14762
- definitionIntent: true
14841
+ definitionIntent: true,
14842
+ trace: options.trace
14763
14843
  });
14764
14844
  }
14765
14845
  async function getCallGraphData(projectRoot, host, params) {
14766
14846
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14767
14847
  const root = getProjectRoot(projectRoot, host);
14768
14848
  const indexer = getIndexerForProject(root, host);
14849
+ return getCallGraphDataForIndexer(indexer, root, params);
14850
+ }
14851
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14769
14852
  const symbols = await indexer.getCallGraphSymbols();
14770
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14853
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14771
14854
  const direction = params.direction === "callees" ? "callees" : "callers";
14772
14855
  if (resolution.status !== "resolved") {
14773
14856
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -15095,6 +15178,27 @@ function buildRecoveryDetails(attempts, successIndex) {
15095
15178
  successfulAttemptIndex: successIndex
15096
15179
  };
15097
15180
  }
15181
+ function serializeAttempts(attempts) {
15182
+ return attempts.map((attempt) => ({
15183
+ kind: attempt.kind,
15184
+ scope: attempt.scope,
15185
+ resultCount: attempt.resultCount,
15186
+ relaxedFields: attempt.relaxedFields
15187
+ }));
15188
+ }
15189
+ function buildSearchDiagnostic(attempt) {
15190
+ if (!attempt) {
15191
+ return void 0;
15192
+ }
15193
+ return {
15194
+ route: attempt.kind,
15195
+ routedQuery: attempt.query,
15196
+ searchQuery: attempt.query,
15197
+ searchScope: attempt.scopeFilter,
15198
+ searchTrace: attempt.searchTrace,
15199
+ contextPackTrace: attempt.contextPackTrace
15200
+ };
15201
+ }
15098
15202
  function trimOrUndefined2(value) {
15099
15203
  const normalized = value?.trim();
15100
15204
  if (!normalized) {
@@ -15134,6 +15238,7 @@ async function resolveSearchContext(input, operations) {
15134
15238
  const hasFilters = Boolean(fileType || directory);
15135
15239
  const relaxedFields = relaxedHintFields(fileType, directory);
15136
15240
  const attempts = [];
15241
+ const attemptStates = [];
15137
15242
  const decisions = {
15138
15243
  inferredDefinitionMiss: false,
15139
15244
  fallbackFromOriginalConceptualToInferred: false,
@@ -15162,8 +15267,20 @@ async function resolveSearchContext(input, operations) {
15162
15267
  if (seenAttempts.has(key)) {
15163
15268
  return [];
15164
15269
  }
15165
- const results = await runAttempt();
15270
+ const attemptState = {
15271
+ kind,
15272
+ scope: describeScope(scope.fileType, scope.directory),
15273
+ resultCount: 0,
15274
+ relaxedFields: [...relaxedFieldsForAttempt],
15275
+ query: attemptQuery,
15276
+ scopeFilter: scope
15277
+ };
15278
+ const results = await runAttempt((trace) => {
15279
+ attemptState.searchTrace = trace;
15280
+ });
15281
+ attemptState.resultCount = results.length;
15166
15282
  seenAttempts.add(key);
15283
+ attemptStates.push(attemptState);
15167
15284
  attempts.push({
15168
15285
  kind,
15169
15286
  scope: describeScope(scope.fileType, scope.directory),
@@ -15183,7 +15300,7 @@ async function resolveSearchContext(input, operations) {
15183
15300
  symbol,
15184
15301
  scope,
15185
15302
  relaxedFieldsForAttempt,
15186
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15303
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15187
15304
  );
15188
15305
  };
15189
15306
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -15192,13 +15309,23 @@ async function resolveSearchContext(input, operations) {
15192
15309
  searchQuery,
15193
15310
  scope,
15194
15311
  relaxedFieldsForAttempt,
15195
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15312
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15196
15313
  );
15197
15314
  };
15198
- const toResult = (route, routedQuery, pack) => {
15315
+ const findSuccessfulAttemptState = (route) => {
15316
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15317
+ const attempt = attemptStates[index];
15318
+ if (attempt.kind === route && attempt.resultCount > 0) {
15319
+ return attempt;
15320
+ }
15321
+ }
15322
+ return void 0;
15323
+ };
15324
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
15199
15325
  const base = packedResult(route, routedQuery, pack);
15200
15326
  const baseDetails = base.details;
15201
15327
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15328
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
15202
15329
  return {
15203
15330
  text: base.text,
15204
15331
  details: {
@@ -15206,7 +15333,10 @@ async function resolveSearchContext(input, operations) {
15206
15333
  tokenBudget: baseDetails.tokenBudget,
15207
15334
  tokenEstimate: baseDetails.tokenEstimate,
15208
15335
  truncated: false,
15209
- recovery: buildRecoveryDetails(attempts, successIndex)
15336
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15337
+ ...input.diagnostic && {
15338
+ diagnostic: buildSearchDiagnostic(successState)
15339
+ }
15210
15340
  }
15211
15341
  };
15212
15342
  };
@@ -15220,8 +15350,18 @@ async function resolveSearchContext(input, operations) {
15220
15350
  buildContextPack(scopedDefinitionResults, {
15221
15351
  tokenBudget,
15222
15352
  maxResults: limit,
15223
- heading
15224
- })
15353
+ heading,
15354
+ preserveInputOrder: true,
15355
+ ...input.diagnostic ? {
15356
+ trace: (trace) => {
15357
+ const attemptState = findSuccessfulAttemptState("definition");
15358
+ if (attemptState) {
15359
+ attemptState.contextPackTrace = trace;
15360
+ }
15361
+ }
15362
+ } : void 0
15363
+ }),
15364
+ findSuccessfulAttemptState("definition")
15225
15365
  );
15226
15366
  }
15227
15367
  if (explicitSymbol) {
@@ -15239,8 +15379,18 @@ async function resolveSearchContext(input, operations) {
15239
15379
  buildContextPack(unscopedDefinitionResults, {
15240
15380
  tokenBudget,
15241
15381
  maxResults: limit,
15242
- heading: heading2
15243
- })
15382
+ heading: heading2,
15383
+ preserveInputOrder: true,
15384
+ ...input.diagnostic ? {
15385
+ trace: (trace) => {
15386
+ const attemptState = findSuccessfulAttemptState("definition");
15387
+ if (attemptState) {
15388
+ attemptState.contextPackTrace = trace;
15389
+ }
15390
+ }
15391
+ } : void 0
15392
+ }),
15393
+ findSuccessfulAttemptState("definition")
15244
15394
  );
15245
15395
  }
15246
15396
  }
@@ -15259,7 +15409,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15259
15409
  tokenBudget: heading.tokenBudget,
15260
15410
  tokenEstimate: heading.tokenEstimate,
15261
15411
  truncated: heading.truncated,
15262
- recovery: buildRecoveryDetails(attempts, null)
15412
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15413
+ ...input.diagnostic && {
15414
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15415
+ }
15263
15416
  }
15264
15417
  };
15265
15418
  }
@@ -15299,8 +15452,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15299
15452
  maxResults: limit,
15300
15453
  heading,
15301
15454
  includeExactSearchHandoff: true,
15302
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15303
- })
15455
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15456
+ ...input.diagnostic ? {
15457
+ trace: (trace) => {
15458
+ const attemptState = findSuccessfulAttemptState("conceptual");
15459
+ if (attemptState) {
15460
+ attemptState.contextPackTrace = trace;
15461
+ }
15462
+ }
15463
+ } : void 0
15464
+ }),
15465
+ findSuccessfulAttemptState("conceptual")
15304
15466
  );
15305
15467
  }
15306
15468
  }
@@ -15316,7 +15478,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15316
15478
  tokenBudget: fallbackText.tokenBudget,
15317
15479
  tokenEstimate: fallbackText.tokenEstimate,
15318
15480
  truncated: fallbackText.truncated,
15319
- recovery: buildRecoveryDetails(attempts, null)
15481
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15482
+ ...input.diagnostic && {
15483
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15484
+ }
15320
15485
  }
15321
15486
  };
15322
15487
  }
@@ -15397,17 +15562,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
15397
15562
  details: fittedDetails("path", fitted, 0)
15398
15563
  };
15399
15564
  }
15400
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15401
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
15565
+ return resolveSearchContext({
15566
+ query: input.query,
15567
+ symbol,
15568
+ limit,
15569
+ tokenBudget,
15570
+ fileType,
15571
+ directory,
15572
+ diagnostic: input.diagnostic
15573
+ }, {
15574
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
15402
15575
  limit: retrievalLimit,
15403
15576
  fileType: scope.fileType,
15404
- directory: scope.directory
15577
+ directory: scope.directory,
15578
+ trace
15405
15579
  }),
15406
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
15580
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
15407
15581
  limit: retrievalLimit,
15408
15582
  fileType: scope.fileType,
15409
15583
  directory: scope.directory,
15410
- metadataOnly: true
15584
+ metadataOnly: true,
15585
+ trace
15411
15586
  })
15412
15587
  });
15413
15588
  }
@@ -15472,6 +15647,181 @@ async function resolveCodebaseContext(projectRoot, host, input) {
15472
15647
  }
15473
15648
  }
15474
15649
 
15650
+ // src/tools/edit-context.ts
15651
+ function edgeLimit(value) {
15652
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15653
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15654
+ }
15655
+ return Math.min(
15656
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15657
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15658
+ );
15659
+ }
15660
+ function normalizedPath(value) {
15661
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15662
+ }
15663
+ function pathsMatch(left, right) {
15664
+ const normalizedLeft = normalizedPath(left);
15665
+ const normalizedRight = normalizedPath(right);
15666
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15667
+ }
15668
+ function targetSource(results, resolution) {
15669
+ 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);
15670
+ }
15671
+ function formatSource(result) {
15672
+ const name = result.name ? ` ${result.name}` : "";
15673
+ return [
15674
+ "## Target implementation",
15675
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15676
+ "```",
15677
+ result.content,
15678
+ "```"
15679
+ ].join("\n");
15680
+ }
15681
+ function formatCallers(edges) {
15682
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15683
+ return [
15684
+ "## Direct callers",
15685
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15686
+ ].join("\n");
15687
+ }
15688
+ function formatCallees(edges, sourceFilePath) {
15689
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15690
+ return [
15691
+ "## Direct callees",
15692
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15693
+ ].join("\n");
15694
+ }
15695
+ function formatResolutionRisk(resolution) {
15696
+ if (resolution.status === "ambiguous") {
15697
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15698
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15699
+ }
15700
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15701
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15702
+ }
15703
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15704
+ }
15705
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15706
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15707
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15708
+ tokenBudget: tokenBudget ?? void 0,
15709
+ heading: "## Conceptual evidence",
15710
+ maxResults: 5,
15711
+ includeExactSearchHandoff: false,
15712
+ preferImplementationPaths: true
15713
+ });
15714
+ const fitted = fitTextToContextBudget(`${risk}
15715
+
15716
+ ${pack.text}`, tokenBudget ?? void 0);
15717
+ return {
15718
+ text: fitted.text,
15719
+ details: {
15720
+ resolution,
15721
+ tokenBudget: fitted.tokenBudget,
15722
+ tokenEstimate: fitted.tokenEstimate,
15723
+ truncated: fitted.truncated,
15724
+ sourceIncluded: candidateSource.length > 0,
15725
+ callerCount: 0,
15726
+ calleeCount: 0
15727
+ }
15728
+ };
15729
+ }
15730
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15731
+ const symbol = input.symbol?.trim();
15732
+ if (!symbol) {
15733
+ return fallbackPack(
15734
+ dependencies,
15735
+ input.query,
15736
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15737
+ "not_requested",
15738
+ input.tokenBudget
15739
+ );
15740
+ }
15741
+ let callersResult;
15742
+ try {
15743
+ callersResult = await dependencies.getCallGraphData({
15744
+ name: symbol,
15745
+ filePath: input.filePath ?? void 0,
15746
+ direction: "callers"
15747
+ });
15748
+ } catch (error) {
15749
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15750
+ const message = error instanceof Error ? error.message : String(error);
15751
+ return fallbackPack(
15752
+ dependencies,
15753
+ input.query,
15754
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15755
+ "graph_unavailable",
15756
+ input.tokenBudget,
15757
+ candidates
15758
+ );
15759
+ }
15760
+ if (callersResult.resolution.status !== "resolved") {
15761
+ return fallbackPack(
15762
+ dependencies,
15763
+ input.query,
15764
+ formatResolutionRisk(callersResult.resolution),
15765
+ callersResult.resolution.status,
15766
+ input.tokenBudget
15767
+ );
15768
+ }
15769
+ const resolution = callersResult.resolution;
15770
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15771
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15772
+ dependencies.getCallGraphData({
15773
+ name: symbol,
15774
+ filePath: input.filePath ?? resolution.filePath,
15775
+ direction: "callees"
15776
+ })
15777
+ ]);
15778
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15779
+ let graphRisk;
15780
+ let callees = [];
15781
+ if (calleesResult.status === "rejected") {
15782
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15783
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15784
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15785
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15786
+ } else {
15787
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15788
+ }
15789
+ const source = targetSource(definitionsResult.value, resolution);
15790
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15791
+ const sourceBudget = Math.max(
15792
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15793
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15794
+ );
15795
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15796
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15797
+ const fitted = fitTextToContextBudget([
15798
+ `# Pre-edit context for ${resolution.name}`,
15799
+ graphRisk,
15800
+ sourceText,
15801
+ formatCallers(callers),
15802
+ formatCallees(callees, resolution.filePath)
15803
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15804
+ return {
15805
+ text: fitted.text,
15806
+ details: {
15807
+ resolution: "resolved",
15808
+ tokenBudget: fitted.tokenBudget,
15809
+ tokenEstimate: fitted.tokenEstimate,
15810
+ truncated: fitted.truncated,
15811
+ sourceIncluded: source !== void 0,
15812
+ callerCount: callers.length,
15813
+ calleeCount: callees.length
15814
+ }
15815
+ };
15816
+ }
15817
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
15818
+ return resolveCodebaseEditContextWithDependencies(input, {
15819
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
15820
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
15821
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
15822
+ });
15823
+ }
15824
+
15475
15825
  // src/eval/budget.ts
15476
15826
  function evaluateBudgetGate(budget, summary, comparison) {
15477
15827
  const BASELINE_P95_EPSILON_MS = 1e-3;
@@ -15495,6 +15845,24 @@ function evaluateBudgetGate(budget, summary, comparison) {
15495
15845
  message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
15496
15846
  });
15497
15847
  }
15848
+ if (thresholds.minGraphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall < thresholds.minGraphNeighborRecall) {
15849
+ violations.push({
15850
+ metric: "minGraphNeighborRecall",
15851
+ message: `Graph-neighbor recall ${summary.metrics.graphNeighborRecall.toFixed(4)} is below minimum ${thresholds.minGraphNeighborRecall.toFixed(4)}`
15852
+ });
15853
+ }
15854
+ if (thresholds.minRouteAccuracy !== void 0 && summary.metrics.routeAccuracy < thresholds.minRouteAccuracy) {
15855
+ violations.push({
15856
+ metric: "minRouteAccuracy",
15857
+ message: `Route accuracy ${summary.metrics.routeAccuracy.toFixed(4)} is below minimum ${thresholds.minRouteAccuracy.toFixed(4)}`
15858
+ });
15859
+ }
15860
+ if (thresholds.minOutcomeAccuracy !== void 0 && summary.metrics.outcomeAccuracy < thresholds.minOutcomeAccuracy) {
15861
+ violations.push({
15862
+ metric: "minOutcomeAccuracy",
15863
+ message: `Outcome accuracy ${summary.metrics.outcomeAccuracy.toFixed(4)} is below minimum ${thresholds.minOutcomeAccuracy.toFixed(4)}`
15864
+ });
15865
+ }
15498
15866
  if (comparison) {
15499
15867
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
15500
15868
  violations.push({
@@ -15672,6 +16040,14 @@ function isSymbolIntended(query) {
15672
16040
  function isExpectedFile(filePath, relevant) {
15673
16041
  return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
15674
16042
  }
16043
+ function graphNeighborMatches(query, result) {
16044
+ const expected = query.expected.graphNeighbor;
16045
+ if (!expected || result.graphDirection !== expected.direction) return false;
16046
+ if (expected.filePath !== void 0 && !pathMatchesExpected(result.filePath, expected.filePath)) {
16047
+ return false;
16048
+ }
16049
+ return expected.symbol === void 0 || result.name === expected.symbol;
16050
+ }
15675
16051
  function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
15676
16052
  let relevance = 0;
15677
16053
  for (const entry of relevant) {
@@ -15819,6 +16195,7 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
15819
16195
  routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
15820
16196
  outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
15821
16197
  recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
16198
+ graphNeighborMatched: query.expected.graphNeighbor === void 0 ? void 0 : results.some((result) => graphNeighborMatches(query, result)),
15822
16199
  language: query.language,
15823
16200
  difficulty: query.difficulty,
15824
16201
  tags: query.tags,
@@ -15868,7 +16245,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15868
16245
  "no-relevant-hit-top-k": 0
15869
16246
  };
15870
16247
  const latencies = perQuery.map((item) => item.latencyMs);
15871
- const contextQueries = perQuery.filter((item) => item.retrievalMode === "context");
16248
+ const contextQueries = perQuery.filter((item) => item.retrievalMode !== "search");
15872
16249
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
15873
16250
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
15874
16251
  const contextTokenUnits = totalContextResponseTokens / 1e3;
@@ -15878,6 +16255,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15878
16255
  let outcomeExpectedCount = 0;
15879
16256
  let recoveryMatchedCount = 0;
15880
16257
  let recoveryExpectedCount = 0;
16258
+ let graphNeighborMatchedCount = 0;
16259
+ let graphNeighborExpectedCount = 0;
15881
16260
  for (const query of perQuery) {
15882
16261
  if (positiveQueryIds.has(query.id)) {
15883
16262
  if (query.hitAt1) sum.hitAt1 += 1;
@@ -15906,6 +16285,10 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15906
16285
  recoveryExpectedCount += 1;
15907
16286
  if (query.recoveryMatched) recoveryMatchedCount += 1;
15908
16287
  }
16288
+ if (query.graphNeighborMatched !== void 0) {
16289
+ graphNeighborExpectedCount += 1;
16290
+ if (query.graphNeighborMatched) graphNeighborMatchedCount += 1;
16291
+ }
15909
16292
  }
15910
16293
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
15911
16294
  return {
@@ -15918,6 +16301,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15918
16301
  routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
15919
16302
  outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
15920
16303
  recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
16304
+ graphNeighborRecall: graphNeighborExpectedCount === 0 ? 0 : graphNeighborMatchedCount / graphNeighborExpectedCount,
15921
16305
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
15922
16306
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
15923
16307
  latencyMs: {
@@ -16183,14 +16567,29 @@ function parseQueryArgs(value, path31) {
16183
16567
  throw new Error(`${path31} must be an object`);
16184
16568
  }
16185
16569
  const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16570
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16186
16571
  const fileType = parseStringOrUndefined(value.fileType, `${path31}.fileType`);
16187
16572
  const directory = parseStringOrUndefined(value.directory, `${path31}.directory`);
16573
+ const callerLimit = parsePositiveIntegerOrUndefined(value.callerLimit, `${path31}.callerLimit`);
16574
+ const calleeLimit = parsePositiveIntegerOrUndefined(value.calleeLimit, `${path31}.calleeLimit`);
16575
+ const tokenBudget = parsePositiveIntegerOrUndefined(value.tokenBudget, `${path31}.tokenBudget`);
16188
16576
  return {
16189
16577
  ...symbol !== void 0 ? { symbol } : {},
16578
+ ...filePath !== void 0 ? { filePath } : {},
16190
16579
  ...fileType !== void 0 ? { fileType } : {},
16191
- ...directory !== void 0 ? { directory } : {}
16580
+ ...directory !== void 0 ? { directory } : {},
16581
+ ...callerLimit !== void 0 ? { callerLimit } : {},
16582
+ ...calleeLimit !== void 0 ? { calleeLimit } : {},
16583
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
16192
16584
  };
16193
16585
  }
16586
+ function parsePositiveIntegerOrUndefined(value, path31) {
16587
+ if (value === void 0 || value === null) return void 0;
16588
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
16589
+ throw new Error(`${path31} must be a positive integer`);
16590
+ }
16591
+ return value;
16592
+ }
16194
16593
  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-]+)*)?$/;
16195
16594
  function parseSemanticVersion(value, path31) {
16196
16595
  if (!isNonEmptyString(value)) {
@@ -16203,8 +16602,8 @@ function parseSemanticVersion(value, path31) {
16203
16602
  }
16204
16603
  function parseRetrievalMode(value, path31) {
16205
16604
  if (value === void 0 || value === "search") return "search";
16206
- if (value === "context") return value;
16207
- throw new Error(`${path31} must be one of: search, context`);
16605
+ if (value === "context" || value === "edit-context") return value;
16606
+ throw new Error(`${path31} must be one of: search, context, edit-context`);
16208
16607
  }
16209
16608
  function parseStringOrUndefined(value, path31) {
16210
16609
  if (value === void 0 || value === null) return void 0;
@@ -16244,6 +16643,25 @@ function parseEvidenceRelevance(value, path31) {
16244
16643
  }
16245
16644
  return value;
16246
16645
  }
16646
+ function parseExpectedGraphNeighbor(value, path31) {
16647
+ if (value === void 0) return void 0;
16648
+ if (!isRecord3(value)) {
16649
+ throw new Error(`${path31} must be an object`);
16650
+ }
16651
+ if (value.direction !== "caller" && value.direction !== "callee") {
16652
+ throw new Error(`${path31}.direction must be one of: caller, callee`);
16653
+ }
16654
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16655
+ const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16656
+ if (filePath === void 0 && symbol === void 0) {
16657
+ throw new Error(`${path31} must include filePath or symbol`);
16658
+ }
16659
+ return {
16660
+ direction: value.direction,
16661
+ ...filePath !== void 0 ? { filePath } : {},
16662
+ ...symbol !== void 0 ? { symbol } : {}
16663
+ };
16664
+ }
16247
16665
  function parseExpected(input, path31) {
16248
16666
  if (!isRecord3(input)) {
16249
16667
  throw new Error(`${path31} must be an object`);
@@ -16256,9 +16674,11 @@ function parseExpected(input, path31) {
16256
16674
  const expectedOutcomeRaw = input.expectedOutcome;
16257
16675
  const recoveryExpectationRaw = input.recoveryExpectation;
16258
16676
  const gradedEvidenceRaw = input.gradedEvidence;
16677
+ const graphNeighborRaw = input.graphNeighbor;
16259
16678
  const filePath = parseStringOrUndefined(filePathRaw, `${path31}.filePath`);
16260
16679
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
16261
16680
  const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path31}.gradedEvidence`);
16681
+ const graphNeighbor = parseExpectedGraphNeighbor(graphNeighborRaw, `${path31}.graphNeighbor`);
16262
16682
  const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path31}.expectedOutcome`);
16263
16683
  if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
16264
16684
  throw new Error(
@@ -16287,7 +16707,8 @@ function parseExpected(input, path31) {
16287
16707
  expectedRoute,
16288
16708
  expectedOutcome,
16289
16709
  recoveryExpectation,
16290
- ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
16710
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {},
16711
+ ...graphNeighbor !== void 0 ? { graphNeighbor } : {}
16291
16712
  };
16292
16713
  }
16293
16714
  function parseQueryLanguage(value, path31) {
@@ -16422,6 +16843,21 @@ function parseBudget(raw, sourceLabel) {
16422
16843
  "minRawDistinctTop3Ratio",
16423
16844
  sourceLabel
16424
16845
  ),
16846
+ minGraphNeighborRecall: parseThresholdValue(
16847
+ thresholds.minGraphNeighborRecall,
16848
+ "minGraphNeighborRecall",
16849
+ sourceLabel
16850
+ ),
16851
+ minRouteAccuracy: parseThresholdValue(
16852
+ thresholds.minRouteAccuracy,
16853
+ "minRouteAccuracy",
16854
+ sourceLabel
16855
+ ),
16856
+ minOutcomeAccuracy: parseThresholdValue(
16857
+ thresholds.minOutcomeAccuracy,
16858
+ "minOutcomeAccuracy",
16859
+ sourceLabel
16860
+ ),
16425
16861
  maxContextResponseTokensAverage: parseThresholdValue(
16426
16862
  thresholds.maxContextResponseTokensAverage,
16427
16863
  "maxContextResponseTokensAverage",
@@ -16486,6 +16922,120 @@ function buildDatasetFingerprint(dataset) {
16486
16922
  const canonical = JSON.stringify(normalizeForFingerprint(dataset));
16487
16923
  return crypto2.createHash("sha256").update(canonical).digest("hex");
16488
16924
  }
16925
+ function normalizedPath2(value) {
16926
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
16927
+ }
16928
+ function pathsMatch2(left, right) {
16929
+ const normalizedLeft = normalizedPath2(left);
16930
+ const normalizedRight = normalizedPath2(right);
16931
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
16932
+ }
16933
+ function toEvalSearchResult(result) {
16934
+ return {
16935
+ filePath: result.filePath,
16936
+ startLine: result.startLine,
16937
+ endLine: result.endLine,
16938
+ score: result.score,
16939
+ chunkType: result.chunkType,
16940
+ name: result.name
16941
+ };
16942
+ }
16943
+ function selectResolvedTarget(definitions, resolution) {
16944
+ if (resolution?.status !== "resolved") return definitions[0];
16945
+ 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];
16946
+ }
16947
+ function callerResult(edge) {
16948
+ if (!edge.fromSymbolFilePath) return void 0;
16949
+ return {
16950
+ filePath: edge.fromSymbolFilePath,
16951
+ startLine: edge.line,
16952
+ endLine: edge.line,
16953
+ score: 0,
16954
+ chunkType: "graph-caller",
16955
+ name: edge.fromSymbolName,
16956
+ graphDirection: "caller"
16957
+ };
16958
+ }
16959
+ function calleeResult(edge, symbols) {
16960
+ 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;
16961
+ if (!symbol) return void 0;
16962
+ return {
16963
+ filePath: symbol.filePath,
16964
+ startLine: symbol.startLine,
16965
+ endLine: symbol.endLine,
16966
+ score: 0,
16967
+ chunkType: "graph-callee",
16968
+ name: symbol.name,
16969
+ graphDirection: "callee"
16970
+ };
16971
+ }
16972
+ async function runEditContextQuery(indexer, projectRoot, query) {
16973
+ let definitions = [];
16974
+ let conceptual = [];
16975
+ let callers;
16976
+ let callees;
16977
+ const editContext = await resolveCodebaseEditContextWithDependencies({
16978
+ query: query.query,
16979
+ symbol: query.args?.symbol,
16980
+ filePath: query.args?.filePath ?? query.expected.filePath,
16981
+ callerLimit: query.args?.callerLimit,
16982
+ calleeLimit: query.args?.calleeLimit,
16983
+ tokenBudget: query.args?.tokenBudget
16984
+ }, {
16985
+ searchCodebase: async (searchQuery, options) => {
16986
+ conceptual = await indexer.search(searchQuery, options?.limit, {
16987
+ filterByBranch: !!query.expected.branch
16988
+ });
16989
+ return conceptual;
16990
+ },
16991
+ implementationLookup: async (symbol, options) => {
16992
+ definitions = await indexer.search(symbol, options?.limit, {
16993
+ filterByBranch: !!query.expected.branch,
16994
+ definitionIntent: true
16995
+ });
16996
+ return definitions;
16997
+ },
16998
+ getCallGraphData: async (params) => {
16999
+ const result = await getCallGraphDataForIndexer(indexer, projectRoot, params);
17000
+ if (params.direction === "callers") callers = result;
17001
+ else callees = result;
17002
+ return result;
17003
+ }
17004
+ });
17005
+ const resolution = callers?.resolution;
17006
+ const target = selectResolvedTarget(definitions, resolution);
17007
+ const targetCandidates = target ? [target] : [...definitions, ...conceptual];
17008
+ const results = targetCandidates.filter((candidate) => (resolution?.status !== "resolved" || editContext.details.sourceIncluded) && editContext.text.includes(
17009
+ `${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
17010
+ )).map(toEvalSearchResult);
17011
+ if (query.expected.graphNeighbor) {
17012
+ const symbols = await indexer.getCallGraphSymbols();
17013
+ const callerLimit = query.args?.callerLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17014
+ const calleeLimit = query.args?.calleeLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17015
+ const publishedCallers = (callers?.callers ?? []).slice(0, callerLimit).filter((edge) => editContext.text.includes(
17016
+ `${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
17017
+ ));
17018
+ const publishedCallees = (callees?.callees ?? []).slice(0, calleeLimit).filter((edge) => resolution?.status === "resolved" && editContext.text.includes(
17019
+ `${edge.targetName} from ${resolution.filePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
17020
+ ));
17021
+ results.push(
17022
+ ...publishedCallers.map(callerResult).filter((item) => item !== void 0),
17023
+ ...publishedCallees.map((edge) => calleeResult(edge, symbols)).filter((item) => item !== void 0)
17024
+ );
17025
+ }
17026
+ return {
17027
+ results,
17028
+ resolvedRoute: resolution?.status === "resolved" ? "definition" : "search",
17029
+ routedQuery: query.args?.symbol ?? query.query,
17030
+ context: {
17031
+ tokenBudget: editContext.details.tokenBudget,
17032
+ responseTokens: editContext.details.tokenEstimate,
17033
+ candidateCount: results.length,
17034
+ deduplicatedCount: results.length,
17035
+ omittedCount: 0
17036
+ }
17037
+ };
17038
+ }
16489
17039
  async function runEvaluation(options) {
16490
17040
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
16491
17041
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -16517,6 +17067,7 @@ async function runEvaluation(options) {
16517
17067
  );
16518
17068
  }
16519
17069
  const start = performance3.now();
17070
+ const editContextResult = query.retrievalMode === "edit-context" ? await runEditContextQuery(indexer, options.projectRoot, query) : void 0;
16520
17071
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
16521
17072
  query: query.query,
16522
17073
  symbol: query.args?.symbol,
@@ -16540,31 +17091,32 @@ async function runEvaluation(options) {
16540
17091
  directory: scope.directory
16541
17092
  })
16542
17093
  }) : void 0;
16543
- const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
17094
+ const result = editContextResult?.results ?? contextResult?.details?.results ?? await indexer.search(query.query, 10, {
16544
17095
  metadataOnly: true,
16545
17096
  filterByBranch: !!query.expected.branch,
16546
17097
  fileType: query.args?.fileType,
16547
17098
  directory: query.args?.directory
16548
17099
  });
16549
17100
  const elapsed = performance3.now() - start;
16550
- const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
16551
- const routedQuery = contextResult?.details?.routedQuery ?? query.query;
17101
+ const resolvedRoute = editContextResult?.resolvedRoute ?? (contextResult?.details?.route === "definition" ? "definition" : "search");
17102
+ const routedQuery = editContextResult?.routedQuery ?? contextResult?.details?.routedQuery ?? query.query;
16552
17103
  const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
16553
17104
  const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
16554
17105
  const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
16555
17106
  const recoveryUsed = recoveryAttempts.length > 1 || recoveryAttempts.some((attempt) => attempt.relaxedFields.length > 0);
16556
- const materialized = result.map((item) => ({
16557
- filePath: item.filePath,
16558
- startLine: item.startLine,
16559
- endLine: item.endLine,
16560
- score: item.score,
16561
- chunkType: item.chunkType,
16562
- name: item.name
16563
- }));
16564
- perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
16565
- resolvedRoute,
16566
- routedQuery
16567
- }, contextResult?.details ? {
17107
+ const materialized = result.map((item) => {
17108
+ const graphDirection = "graphDirection" in item && (item.graphDirection === "caller" || item.graphDirection === "callee") ? item.graphDirection : void 0;
17109
+ return {
17110
+ filePath: item.filePath,
17111
+ startLine: item.startLine,
17112
+ endLine: item.endLine,
17113
+ score: item.score,
17114
+ chunkType: item.chunkType,
17115
+ name: item.name,
17116
+ graphDirection
17117
+ };
17118
+ });
17119
+ const contextMeasurement = editContextResult?.context ?? (contextResult?.details ? {
16568
17120
  tokenBudget: contextResult.details.tokenBudget,
16569
17121
  responseTokens: contextResult.details.tokenEstimate,
16570
17122
  candidateCount: contextResult.details.candidateCount ?? 0,
@@ -16572,7 +17124,11 @@ async function runEvaluation(options) {
16572
17124
  omittedCount: contextResult.details.omittedCount ?? 0,
16573
17125
  recoveryUsed,
16574
17126
  recoveryRelaxed
16575
- } : void 0));
17127
+ } : void 0);
17128
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
17129
+ resolvedRoute,
17130
+ routedQuery
17131
+ }, contextMeasurement));
16576
17132
  }
16577
17133
  const logger = indexer.getLogger();
16578
17134
  const metricSnapshot = logger.getMetrics();
@@ -17108,7 +17664,11 @@ import { z as z2 } from "zod";
17108
17664
 
17109
17665
  // src/tools/execute-common.ts
17110
17666
  async function executeCodebaseContext(projectRoot, host, args) {
17111
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17667
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17668
+ return { text: result.text, details: result.details };
17669
+ }
17670
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17671
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17112
17672
  }
17113
17673
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17114
17674
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17224,6 +17784,7 @@ function formatPrImpact(result) {
17224
17784
  // src/tools/tool-names.ts
17225
17785
  var TOOL_NAME = {
17226
17786
  CODEBASE_CONTEXT: "codebase_context",
17787
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
17227
17788
  CODEBASE_SEARCH: "codebase_search",
17228
17789
  CODEBASE_PEEK: "codebase_peek",
17229
17790
  FIND_SIMILAR: "find_similar",
@@ -17247,6 +17808,7 @@ var TOOL_NAME = {
17247
17808
  };
17248
17809
  var PORTABLE_TOOL_NAMES = [
17249
17810
  TOOL_NAME.CODEBASE_CONTEXT,
17811
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17250
17812
  TOOL_NAME.CODEBASE_SEARCH,
17251
17813
  TOOL_NAME.CODEBASE_PEEK,
17252
17814
  TOOL_NAME.INDEX_CODEBASE,
@@ -17263,6 +17825,7 @@ var PORTABLE_TOOL_NAMES = [
17263
17825
  ];
17264
17826
  var OPENCODE_TOOL_NAMES = [
17265
17827
  TOOL_NAME.CODEBASE_CONTEXT,
17828
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17266
17829
  TOOL_NAME.CODEBASE_SEARCH,
17267
17830
  TOOL_NAME.CODEBASE_PEEK,
17268
17831
  TOOL_NAME.INDEX_CODEBASE,
@@ -17283,6 +17846,7 @@ var OPENCODE_TOOL_NAMES = [
17283
17846
  ];
17284
17847
  var PI_TOOL_NAMES = [
17285
17848
  TOOL_NAME.CODEBASE_CONTEXT,
17849
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17286
17850
  TOOL_NAME.CODEBASE_SEARCH,
17287
17851
  TOOL_NAME.CODEBASE_PEEK,
17288
17852
  TOOL_NAME.FIND_SIMILAR,
@@ -17326,10 +17890,30 @@ function registerMcpTools(server, runtime) {
17326
17890
  directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
17327
17891
  tokenBudget: allowNullAsUndefined(
17328
17892
  z2.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
17329
- ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`)
17893
+ ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
17894
+ diagnostic: z2.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
17330
17895
  },
17331
17896
  async (args) => {
17332
17897
  const result = await executeCodebaseContext(runtime.projectRoot, runtime.host, args);
17898
+ return {
17899
+ content: [{ type: "text", text: result.text }],
17900
+ ...args.diagnostic ? { structuredContent: result.details } : {}
17901
+ };
17902
+ }
17903
+ );
17904
+ server.tool(
17905
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17906
+ "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.",
17907
+ {
17908
+ query: z2.string().describe("The requested change or target behavior."),
17909
+ symbol: allowNullAsUndefined(z2.string().optional()).describe("Authoritative target symbol when known."),
17910
+ filePath: allowNullAsUndefined(z2.string().optional()).describe("Optional file path used to disambiguate duplicate symbol names."),
17911
+ callerLimit: allowNullAsUndefined(z2.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT)),
17912
+ calleeLimit: allowNullAsUndefined(z2.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT)),
17913
+ tokenBudget: allowNullAsUndefined(z2.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET))
17914
+ },
17915
+ async (args) => {
17916
+ const result = await executeCodebaseEditContext(runtime.projectRoot, runtime.host, args);
17333
17917
  return { content: [{ type: "text", text: result.text }] };
17334
17918
  }
17335
17919
  );
@@ -19786,9 +20370,9 @@ function parseGitActivity(output) {
19786
20370
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
19787
20371
  const churn = Number(addedRaw) + Number(deletedRaw);
19788
20372
  if (!Number.isFinite(churn) || churn <= 0) continue;
19789
- const normalizedPath = normalizePath4(filePath);
19790
- const previous = activity.get(normalizedPath);
19791
- activity.set(normalizedPath, {
20373
+ const normalizedPath3 = normalizePath4(filePath);
20374
+ const previous = activity.get(normalizedPath3);
20375
+ activity.set(normalizedPath3, {
19792
20376
  churn: (previous?.churn ?? 0) + churn,
19793
20377
  commits: (previous?.commits ?? 0) + 1,
19794
20378
  latestDate: previous?.latestDate ?? latestDate,
@@ -20378,8 +20962,8 @@ function transformForVisualization(symbols, edges, options = {}) {
20378
20962
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
20379
20963
  filteredSymbols = symbols.filter(
20380
20964
  (s) => {
20381
- const normalizedPath = s.filePath.replace(/\\/g, "/");
20382
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
20965
+ const normalizedPath3 = s.filePath.replace(/\\/g, "/");
20966
+ return normalizedPath3 === normalizedDir || normalizedPath3.startsWith(normalizedDirWithSlash) || normalizedPath3.endsWith(`/${normalizedDir}`) || normalizedPath3.includes(normalizedAbsoluteSuffix);
20383
20967
  }
20384
20968
  );
20385
20969
  }