open-codebase-index 0.22.2 → 0.22.4

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-codebase-index - Semantic codebase search for OpenCode
2
+ // open-codebase-index - Semantic codebase indexing and search
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -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";
@@ -7109,9 +7110,9 @@ function normalizeFilePathForHintMatch(filePath) {
7109
7110
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
7110
7111
  }
7111
7112
  function pathMatchesHint(filePath, hint) {
7112
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
7113
+ const normalizedPath3 = normalizeFilePathForHintMatch(filePath);
7113
7114
  const normalizedHint = normalizeFilePathForHintMatch(hint);
7114
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
7115
+ return normalizedPath3.endsWith(normalizedHint) || normalizedPath3.includes(`/${normalizedHint}`) || normalizedPath3.includes(normalizedHint);
7115
7116
  }
7116
7117
  function extractFilePathHint(query) {
7117
7118
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -11375,6 +11376,20 @@ var Indexer = class _Indexer {
11375
11376
  requestedLimit = nextLimit;
11376
11377
  }
11377
11378
  }
11379
+ buildCandidateSnapshot(candidate) {
11380
+ return {
11381
+ id: candidate.id,
11382
+ filePath: candidate.metadata.filePath,
11383
+ startLine: candidate.metadata.startLine,
11384
+ endLine: candidate.metadata.endLine,
11385
+ score: candidate.score,
11386
+ chunkType: candidate.metadata.chunkType,
11387
+ name: candidate.metadata.name
11388
+ };
11389
+ }
11390
+ buildCandidateSnapshotList(candidates) {
11391
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
11392
+ }
11378
11393
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
11379
11394
  return this.searchCandidatesWithBranchPrefilter(
11380
11395
  initialLimit,
@@ -11566,6 +11581,16 @@ var Indexer = class _Indexer {
11566
11581
  prefilterMs: Math.round(prefilterMs * 100) / 100,
11567
11582
  fusionMs: Math.round(fusionMs * 100) / 100
11568
11583
  });
11584
+ if (options?.trace) {
11585
+ options.trace({
11586
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
11587
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
11588
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
11589
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
11590
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
11591
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
11592
+ });
11593
+ }
11569
11594
  const metadataOnly = options?.metadataOnly ?? false;
11570
11595
  return Promise.all(
11571
11596
  finalResults.map(async (r) => {
@@ -12614,6 +12639,42 @@ var Indexer = class _Indexer {
12614
12639
  }
12615
12640
  };
12616
12641
 
12642
+ // src/tools/contracts.ts
12643
+ var CHUNK_TYPES = [
12644
+ "function",
12645
+ "class",
12646
+ "method",
12647
+ "interface",
12648
+ "type",
12649
+ "enum",
12650
+ "struct",
12651
+ "impl",
12652
+ "trait",
12653
+ "module",
12654
+ "other"
12655
+ ];
12656
+ var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12657
+ var RELATIONSHIP_TYPES = [
12658
+ "Call",
12659
+ "MethodCall",
12660
+ "Constructor",
12661
+ "Import",
12662
+ "Inherits",
12663
+ "Implements"
12664
+ ];
12665
+ var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12666
+ var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12667
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
12668
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
12669
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
12670
+ var CODE_COMMUNITIES_MIN_SIZE = 1;
12671
+ var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12672
+ var CODE_COMMUNITIES_MAX_LIMIT = 100;
12673
+ var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12674
+ var CODE_COMMUNITIES_MIN_COUPLING = 1;
12675
+ var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12676
+ var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12677
+
12617
12678
  // src/tools/operations.ts
12618
12679
  var import_fs14 = require("fs");
12619
12680
  var path21 = __toESM(require("path"), 1);
@@ -12742,39 +12803,6 @@ function formatCodeCommunities(result) {
12742
12803
  return lines.join("\n");
12743
12804
  }
12744
12805
 
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
12806
  // src/tools/context-pack.ts
12779
12807
  var import_tiktoken = require("tiktoken");
12780
12808
  var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
@@ -12876,6 +12904,16 @@ function compactEvidenceValue(value, maxChars) {
12876
12904
  }
12877
12905
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
12878
12906
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
12907
+ function toContextPackTraceCandidate(result) {
12908
+ return {
12909
+ filePath: result.filePath,
12910
+ startLine: result.startLine,
12911
+ endLine: result.endLine,
12912
+ score: result.score,
12913
+ chunkType: result.chunkType,
12914
+ name: result.name
12915
+ };
12916
+ }
12879
12917
  function formatExactSearchHandoff(results) {
12880
12918
  const suggestedNames = [];
12881
12919
  const seen = /* @__PURE__ */ new Set();
@@ -12924,13 +12962,13 @@ function buildContextPack(results, options = {}) {
12924
12962
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
12925
12963
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
12926
12964
  const candidateCount = results.length;
12927
- const deduplicated = deduplicateContextCandidates(
12928
- rankContextCandidates(
12929
- results,
12930
- options.preferImplementationPaths ?? false
12931
- )
12932
- );
12933
- const diversified = diversifyContextCandidates(deduplicated);
12965
+ const preserveInputOrder = options.preserveInputOrder ?? false;
12966
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
12967
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
12968
+ const deduplicated = deduplicateContextCandidates(ranked);
12969
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
12970
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
12971
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
12934
12972
  const duplicateCount = candidateCount - deduplicated.length;
12935
12973
  const selectable = diversified.slice(0, maxResults);
12936
12974
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -12963,6 +13001,15 @@ function buildContextPack(results, options = {}) {
12963
13001
  const fitted = fitTextToContextBudget(text, tokenBudget);
12964
13002
  const budgetOmittedCount = selectable.length - selected.length;
12965
13003
  const omittedCount = candidateCount - selected.length;
13004
+ if (options.trace) {
13005
+ options.trace({
13006
+ inputCandidates: results.map(toContextPackTraceCandidate),
13007
+ rankedCandidates,
13008
+ deduplicatedCandidates,
13009
+ diversifiedCandidates,
13010
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
13011
+ });
13012
+ }
12966
13013
  return {
12967
13014
  requestedTokenBudget,
12968
13015
  tokenBudget,
@@ -14710,7 +14757,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14710
14757
  definitionIntent: options.definitionIntent,
14711
14758
  blameAuthor: options.blameAuthor,
14712
14759
  blameSha: options.blameSha,
14713
- blameSince: options.blameSince
14760
+ blameSince: options.blameSince,
14761
+ trace: options.trace
14714
14762
  });
14715
14763
  }
14716
14764
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14764,15 +14812,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14764
14812
  return indexer.search(query, options.limit, {
14765
14813
  fileType: options.fileType,
14766
14814
  directory: options.directory,
14767
- definitionIntent: true
14815
+ definitionIntent: true,
14816
+ trace: options.trace
14768
14817
  });
14769
14818
  }
14770
14819
  async function getCallGraphData(projectRoot, host, params) {
14771
14820
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14772
14821
  const root = getProjectRoot(projectRoot, host);
14773
14822
  const indexer = getIndexerForProject(root, host);
14823
+ return getCallGraphDataForIndexer(indexer, root, params);
14824
+ }
14825
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14774
14826
  const symbols = await indexer.getCallGraphSymbols();
14775
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14827
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14776
14828
  const direction = params.direction === "callees" ? "callees" : "callers";
14777
14829
  if (resolution.status !== "resolved") {
14778
14830
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -15100,6 +15152,27 @@ function buildRecoveryDetails(attempts, successIndex) {
15100
15152
  successfulAttemptIndex: successIndex
15101
15153
  };
15102
15154
  }
15155
+ function serializeAttempts(attempts) {
15156
+ return attempts.map((attempt) => ({
15157
+ kind: attempt.kind,
15158
+ scope: attempt.scope,
15159
+ resultCount: attempt.resultCount,
15160
+ relaxedFields: attempt.relaxedFields
15161
+ }));
15162
+ }
15163
+ function buildSearchDiagnostic(attempt) {
15164
+ if (!attempt) {
15165
+ return void 0;
15166
+ }
15167
+ return {
15168
+ route: attempt.kind,
15169
+ routedQuery: attempt.query,
15170
+ searchQuery: attempt.query,
15171
+ searchScope: attempt.scopeFilter,
15172
+ searchTrace: attempt.searchTrace,
15173
+ contextPackTrace: attempt.contextPackTrace
15174
+ };
15175
+ }
15103
15176
  function trimOrUndefined2(value) {
15104
15177
  const normalized = value?.trim();
15105
15178
  if (!normalized) {
@@ -15139,6 +15212,7 @@ async function resolveSearchContext(input, operations) {
15139
15212
  const hasFilters = Boolean(fileType || directory);
15140
15213
  const relaxedFields = relaxedHintFields(fileType, directory);
15141
15214
  const attempts = [];
15215
+ const attemptStates = [];
15142
15216
  const decisions = {
15143
15217
  inferredDefinitionMiss: false,
15144
15218
  fallbackFromOriginalConceptualToInferred: false,
@@ -15167,8 +15241,20 @@ async function resolveSearchContext(input, operations) {
15167
15241
  if (seenAttempts.has(key)) {
15168
15242
  return [];
15169
15243
  }
15170
- const results = await runAttempt();
15244
+ const attemptState = {
15245
+ kind,
15246
+ scope: describeScope(scope.fileType, scope.directory),
15247
+ resultCount: 0,
15248
+ relaxedFields: [...relaxedFieldsForAttempt],
15249
+ query: attemptQuery,
15250
+ scopeFilter: scope
15251
+ };
15252
+ const results = await runAttempt((trace) => {
15253
+ attemptState.searchTrace = trace;
15254
+ });
15255
+ attemptState.resultCount = results.length;
15171
15256
  seenAttempts.add(key);
15257
+ attemptStates.push(attemptState);
15172
15258
  attempts.push({
15173
15259
  kind,
15174
15260
  scope: describeScope(scope.fileType, scope.directory),
@@ -15188,7 +15274,7 @@ async function resolveSearchContext(input, operations) {
15188
15274
  symbol,
15189
15275
  scope,
15190
15276
  relaxedFieldsForAttempt,
15191
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15277
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15192
15278
  );
15193
15279
  };
15194
15280
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -15197,13 +15283,23 @@ async function resolveSearchContext(input, operations) {
15197
15283
  searchQuery,
15198
15284
  scope,
15199
15285
  relaxedFieldsForAttempt,
15200
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15286
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15201
15287
  );
15202
15288
  };
15203
- const toResult = (route, routedQuery, pack) => {
15289
+ const findSuccessfulAttemptState = (route) => {
15290
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15291
+ const attempt = attemptStates[index];
15292
+ if (attempt.kind === route && attempt.resultCount > 0) {
15293
+ return attempt;
15294
+ }
15295
+ }
15296
+ return void 0;
15297
+ };
15298
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
15204
15299
  const base = packedResult(route, routedQuery, pack);
15205
15300
  const baseDetails = base.details;
15206
15301
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15302
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
15207
15303
  return {
15208
15304
  text: base.text,
15209
15305
  details: {
@@ -15211,7 +15307,10 @@ async function resolveSearchContext(input, operations) {
15211
15307
  tokenBudget: baseDetails.tokenBudget,
15212
15308
  tokenEstimate: baseDetails.tokenEstimate,
15213
15309
  truncated: false,
15214
- recovery: buildRecoveryDetails(attempts, successIndex)
15310
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15311
+ ...input.diagnostic && {
15312
+ diagnostic: buildSearchDiagnostic(successState)
15313
+ }
15215
15314
  }
15216
15315
  };
15217
15316
  };
@@ -15225,8 +15324,18 @@ async function resolveSearchContext(input, operations) {
15225
15324
  buildContextPack(scopedDefinitionResults, {
15226
15325
  tokenBudget,
15227
15326
  maxResults: limit,
15228
- heading
15229
- })
15327
+ heading,
15328
+ preserveInputOrder: true,
15329
+ ...input.diagnostic ? {
15330
+ trace: (trace) => {
15331
+ const attemptState = findSuccessfulAttemptState("definition");
15332
+ if (attemptState) {
15333
+ attemptState.contextPackTrace = trace;
15334
+ }
15335
+ }
15336
+ } : void 0
15337
+ }),
15338
+ findSuccessfulAttemptState("definition")
15230
15339
  );
15231
15340
  }
15232
15341
  if (explicitSymbol) {
@@ -15244,8 +15353,18 @@ async function resolveSearchContext(input, operations) {
15244
15353
  buildContextPack(unscopedDefinitionResults, {
15245
15354
  tokenBudget,
15246
15355
  maxResults: limit,
15247
- heading: heading2
15248
- })
15356
+ heading: heading2,
15357
+ preserveInputOrder: true,
15358
+ ...input.diagnostic ? {
15359
+ trace: (trace) => {
15360
+ const attemptState = findSuccessfulAttemptState("definition");
15361
+ if (attemptState) {
15362
+ attemptState.contextPackTrace = trace;
15363
+ }
15364
+ }
15365
+ } : void 0
15366
+ }),
15367
+ findSuccessfulAttemptState("definition")
15249
15368
  );
15250
15369
  }
15251
15370
  }
@@ -15264,7 +15383,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15264
15383
  tokenBudget: heading.tokenBudget,
15265
15384
  tokenEstimate: heading.tokenEstimate,
15266
15385
  truncated: heading.truncated,
15267
- recovery: buildRecoveryDetails(attempts, null)
15386
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15387
+ ...input.diagnostic && {
15388
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15389
+ }
15268
15390
  }
15269
15391
  };
15270
15392
  }
@@ -15304,8 +15426,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15304
15426
  maxResults: limit,
15305
15427
  heading,
15306
15428
  includeExactSearchHandoff: true,
15307
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15308
- })
15429
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15430
+ ...input.diagnostic ? {
15431
+ trace: (trace) => {
15432
+ const attemptState = findSuccessfulAttemptState("conceptual");
15433
+ if (attemptState) {
15434
+ attemptState.contextPackTrace = trace;
15435
+ }
15436
+ }
15437
+ } : void 0
15438
+ }),
15439
+ findSuccessfulAttemptState("conceptual")
15309
15440
  );
15310
15441
  }
15311
15442
  }
@@ -15321,7 +15452,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15321
15452
  tokenBudget: fallbackText.tokenBudget,
15322
15453
  tokenEstimate: fallbackText.tokenEstimate,
15323
15454
  truncated: fallbackText.truncated,
15324
- recovery: buildRecoveryDetails(attempts, null)
15455
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15456
+ ...input.diagnostic && {
15457
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15458
+ }
15325
15459
  }
15326
15460
  };
15327
15461
  }
@@ -15402,17 +15536,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
15402
15536
  details: fittedDetails("path", fitted, 0)
15403
15537
  };
15404
15538
  }
15405
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15406
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
15539
+ return resolveSearchContext({
15540
+ query: input.query,
15541
+ symbol,
15542
+ limit,
15543
+ tokenBudget,
15544
+ fileType,
15545
+ directory,
15546
+ diagnostic: input.diagnostic
15547
+ }, {
15548
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
15407
15549
  limit: retrievalLimit,
15408
15550
  fileType: scope.fileType,
15409
- directory: scope.directory
15551
+ directory: scope.directory,
15552
+ trace
15410
15553
  }),
15411
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
15554
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
15412
15555
  limit: retrievalLimit,
15413
15556
  fileType: scope.fileType,
15414
15557
  directory: scope.directory,
15415
- metadataOnly: true
15558
+ metadataOnly: true,
15559
+ trace
15416
15560
  })
15417
15561
  });
15418
15562
  }
@@ -15477,6 +15621,181 @@ async function resolveCodebaseContext(projectRoot, host, input) {
15477
15621
  }
15478
15622
  }
15479
15623
 
15624
+ // src/tools/edit-context.ts
15625
+ function edgeLimit(value) {
15626
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15627
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15628
+ }
15629
+ return Math.min(
15630
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15631
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15632
+ );
15633
+ }
15634
+ function normalizedPath(value) {
15635
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15636
+ }
15637
+ function pathsMatch(left, right) {
15638
+ const normalizedLeft = normalizedPath(left);
15639
+ const normalizedRight = normalizedPath(right);
15640
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15641
+ }
15642
+ function targetSource(results, resolution) {
15643
+ 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);
15644
+ }
15645
+ function formatSource(result) {
15646
+ const name = result.name ? ` ${result.name}` : "";
15647
+ return [
15648
+ "## Target implementation",
15649
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15650
+ "```",
15651
+ result.content,
15652
+ "```"
15653
+ ].join("\n");
15654
+ }
15655
+ function formatCallers(edges) {
15656
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15657
+ return [
15658
+ "## Direct callers",
15659
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15660
+ ].join("\n");
15661
+ }
15662
+ function formatCallees(edges, sourceFilePath) {
15663
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15664
+ return [
15665
+ "## Direct callees",
15666
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15667
+ ].join("\n");
15668
+ }
15669
+ function formatResolutionRisk(resolution) {
15670
+ if (resolution.status === "ambiguous") {
15671
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15672
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15673
+ }
15674
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15675
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15676
+ }
15677
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15678
+ }
15679
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15680
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15681
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15682
+ tokenBudget: tokenBudget ?? void 0,
15683
+ heading: "## Conceptual evidence",
15684
+ maxResults: 5,
15685
+ includeExactSearchHandoff: false,
15686
+ preferImplementationPaths: true
15687
+ });
15688
+ const fitted = fitTextToContextBudget(`${risk}
15689
+
15690
+ ${pack.text}`, tokenBudget ?? void 0);
15691
+ return {
15692
+ text: fitted.text,
15693
+ details: {
15694
+ resolution,
15695
+ tokenBudget: fitted.tokenBudget,
15696
+ tokenEstimate: fitted.tokenEstimate,
15697
+ truncated: fitted.truncated,
15698
+ sourceIncluded: candidateSource.length > 0,
15699
+ callerCount: 0,
15700
+ calleeCount: 0
15701
+ }
15702
+ };
15703
+ }
15704
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15705
+ const symbol = input.symbol?.trim();
15706
+ if (!symbol) {
15707
+ return fallbackPack(
15708
+ dependencies,
15709
+ input.query,
15710
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15711
+ "not_requested",
15712
+ input.tokenBudget
15713
+ );
15714
+ }
15715
+ let callersResult;
15716
+ try {
15717
+ callersResult = await dependencies.getCallGraphData({
15718
+ name: symbol,
15719
+ filePath: input.filePath ?? void 0,
15720
+ direction: "callers"
15721
+ });
15722
+ } catch (error) {
15723
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15724
+ const message = error instanceof Error ? error.message : String(error);
15725
+ return fallbackPack(
15726
+ dependencies,
15727
+ input.query,
15728
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15729
+ "graph_unavailable",
15730
+ input.tokenBudget,
15731
+ candidates
15732
+ );
15733
+ }
15734
+ if (callersResult.resolution.status !== "resolved") {
15735
+ return fallbackPack(
15736
+ dependencies,
15737
+ input.query,
15738
+ formatResolutionRisk(callersResult.resolution),
15739
+ callersResult.resolution.status,
15740
+ input.tokenBudget
15741
+ );
15742
+ }
15743
+ const resolution = callersResult.resolution;
15744
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15745
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15746
+ dependencies.getCallGraphData({
15747
+ name: symbol,
15748
+ filePath: input.filePath ?? resolution.filePath,
15749
+ direction: "callees"
15750
+ })
15751
+ ]);
15752
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15753
+ let graphRisk;
15754
+ let callees = [];
15755
+ if (calleesResult.status === "rejected") {
15756
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15757
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15758
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15759
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15760
+ } else {
15761
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15762
+ }
15763
+ const source = targetSource(definitionsResult.value, resolution);
15764
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15765
+ const sourceBudget = Math.max(
15766
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15767
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15768
+ );
15769
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15770
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15771
+ const fitted = fitTextToContextBudget([
15772
+ `# Pre-edit context for ${resolution.name}`,
15773
+ graphRisk,
15774
+ sourceText,
15775
+ formatCallers(callers),
15776
+ formatCallees(callees, resolution.filePath)
15777
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15778
+ return {
15779
+ text: fitted.text,
15780
+ details: {
15781
+ resolution: "resolved",
15782
+ tokenBudget: fitted.tokenBudget,
15783
+ tokenEstimate: fitted.tokenEstimate,
15784
+ truncated: fitted.truncated,
15785
+ sourceIncluded: source !== void 0,
15786
+ callerCount: callers.length,
15787
+ calleeCount: callees.length
15788
+ }
15789
+ };
15790
+ }
15791
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
15792
+ return resolveCodebaseEditContextWithDependencies(input, {
15793
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
15794
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
15795
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
15796
+ });
15797
+ }
15798
+
15480
15799
  // src/eval/budget.ts
15481
15800
  function evaluateBudgetGate(budget, summary, comparison) {
15482
15801
  const BASELINE_P95_EPSILON_MS = 1e-3;
@@ -15500,6 +15819,24 @@ function evaluateBudgetGate(budget, summary, comparison) {
15500
15819
  message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
15501
15820
  });
15502
15821
  }
15822
+ if (thresholds.minGraphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall < thresholds.minGraphNeighborRecall) {
15823
+ violations.push({
15824
+ metric: "minGraphNeighborRecall",
15825
+ message: `Graph-neighbor recall ${summary.metrics.graphNeighborRecall.toFixed(4)} is below minimum ${thresholds.minGraphNeighborRecall.toFixed(4)}`
15826
+ });
15827
+ }
15828
+ if (thresholds.minRouteAccuracy !== void 0 && summary.metrics.routeAccuracy < thresholds.minRouteAccuracy) {
15829
+ violations.push({
15830
+ metric: "minRouteAccuracy",
15831
+ message: `Route accuracy ${summary.metrics.routeAccuracy.toFixed(4)} is below minimum ${thresholds.minRouteAccuracy.toFixed(4)}`
15832
+ });
15833
+ }
15834
+ if (thresholds.minOutcomeAccuracy !== void 0 && summary.metrics.outcomeAccuracy < thresholds.minOutcomeAccuracy) {
15835
+ violations.push({
15836
+ metric: "minOutcomeAccuracy",
15837
+ message: `Outcome accuracy ${summary.metrics.outcomeAccuracy.toFixed(4)} is below minimum ${thresholds.minOutcomeAccuracy.toFixed(4)}`
15838
+ });
15839
+ }
15503
15840
  if (comparison) {
15504
15841
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
15505
15842
  violations.push({
@@ -15677,6 +16014,14 @@ function isSymbolIntended(query) {
15677
16014
  function isExpectedFile(filePath, relevant) {
15678
16015
  return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
15679
16016
  }
16017
+ function graphNeighborMatches(query, result) {
16018
+ const expected = query.expected.graphNeighbor;
16019
+ if (!expected || result.graphDirection !== expected.direction) return false;
16020
+ if (expected.filePath !== void 0 && !pathMatchesExpected(result.filePath, expected.filePath)) {
16021
+ return false;
16022
+ }
16023
+ return expected.symbol === void 0 || result.name === expected.symbol;
16024
+ }
15680
16025
  function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
15681
16026
  let relevance = 0;
15682
16027
  for (const entry of relevant) {
@@ -15824,6 +16169,7 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
15824
16169
  routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
15825
16170
  outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
15826
16171
  recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
16172
+ graphNeighborMatched: query.expected.graphNeighbor === void 0 ? void 0 : results.some((result) => graphNeighborMatches(query, result)),
15827
16173
  language: query.language,
15828
16174
  difficulty: query.difficulty,
15829
16175
  tags: query.tags,
@@ -15873,7 +16219,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15873
16219
  "no-relevant-hit-top-k": 0
15874
16220
  };
15875
16221
  const latencies = perQuery.map((item) => item.latencyMs);
15876
- const contextQueries = perQuery.filter((item) => item.retrievalMode === "context");
16222
+ const contextQueries = perQuery.filter((item) => item.retrievalMode !== "search");
15877
16223
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
15878
16224
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
15879
16225
  const contextTokenUnits = totalContextResponseTokens / 1e3;
@@ -15883,6 +16229,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15883
16229
  let outcomeExpectedCount = 0;
15884
16230
  let recoveryMatchedCount = 0;
15885
16231
  let recoveryExpectedCount = 0;
16232
+ let graphNeighborMatchedCount = 0;
16233
+ let graphNeighborExpectedCount = 0;
15886
16234
  for (const query of perQuery) {
15887
16235
  if (positiveQueryIds.has(query.id)) {
15888
16236
  if (query.hitAt1) sum.hitAt1 += 1;
@@ -15911,6 +16259,10 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15911
16259
  recoveryExpectedCount += 1;
15912
16260
  if (query.recoveryMatched) recoveryMatchedCount += 1;
15913
16261
  }
16262
+ if (query.graphNeighborMatched !== void 0) {
16263
+ graphNeighborExpectedCount += 1;
16264
+ if (query.graphNeighborMatched) graphNeighborMatchedCount += 1;
16265
+ }
15914
16266
  }
15915
16267
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
15916
16268
  return {
@@ -15923,6 +16275,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15923
16275
  routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
15924
16276
  outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
15925
16277
  recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
16278
+ graphNeighborRecall: graphNeighborExpectedCount === 0 ? 0 : graphNeighborMatchedCount / graphNeighborExpectedCount,
15926
16279
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
15927
16280
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
15928
16281
  latencyMs: {
@@ -16188,14 +16541,29 @@ function parseQueryArgs(value, path31) {
16188
16541
  throw new Error(`${path31} must be an object`);
16189
16542
  }
16190
16543
  const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16544
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16191
16545
  const fileType = parseStringOrUndefined(value.fileType, `${path31}.fileType`);
16192
16546
  const directory = parseStringOrUndefined(value.directory, `${path31}.directory`);
16547
+ const callerLimit = parsePositiveIntegerOrUndefined(value.callerLimit, `${path31}.callerLimit`);
16548
+ const calleeLimit = parsePositiveIntegerOrUndefined(value.calleeLimit, `${path31}.calleeLimit`);
16549
+ const tokenBudget = parsePositiveIntegerOrUndefined(value.tokenBudget, `${path31}.tokenBudget`);
16193
16550
  return {
16194
16551
  ...symbol !== void 0 ? { symbol } : {},
16552
+ ...filePath !== void 0 ? { filePath } : {},
16195
16553
  ...fileType !== void 0 ? { fileType } : {},
16196
- ...directory !== void 0 ? { directory } : {}
16554
+ ...directory !== void 0 ? { directory } : {},
16555
+ ...callerLimit !== void 0 ? { callerLimit } : {},
16556
+ ...calleeLimit !== void 0 ? { calleeLimit } : {},
16557
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
16197
16558
  };
16198
16559
  }
16560
+ function parsePositiveIntegerOrUndefined(value, path31) {
16561
+ if (value === void 0 || value === null) return void 0;
16562
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
16563
+ throw new Error(`${path31} must be a positive integer`);
16564
+ }
16565
+ return value;
16566
+ }
16199
16567
  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
16568
  function parseSemanticVersion(value, path31) {
16201
16569
  if (!isNonEmptyString(value)) {
@@ -16208,8 +16576,8 @@ function parseSemanticVersion(value, path31) {
16208
16576
  }
16209
16577
  function parseRetrievalMode(value, path31) {
16210
16578
  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`);
16579
+ if (value === "context" || value === "edit-context") return value;
16580
+ throw new Error(`${path31} must be one of: search, context, edit-context`);
16213
16581
  }
16214
16582
  function parseStringOrUndefined(value, path31) {
16215
16583
  if (value === void 0 || value === null) return void 0;
@@ -16249,6 +16617,25 @@ function parseEvidenceRelevance(value, path31) {
16249
16617
  }
16250
16618
  return value;
16251
16619
  }
16620
+ function parseExpectedGraphNeighbor(value, path31) {
16621
+ if (value === void 0) return void 0;
16622
+ if (!isRecord3(value)) {
16623
+ throw new Error(`${path31} must be an object`);
16624
+ }
16625
+ if (value.direction !== "caller" && value.direction !== "callee") {
16626
+ throw new Error(`${path31}.direction must be one of: caller, callee`);
16627
+ }
16628
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16629
+ const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16630
+ if (filePath === void 0 && symbol === void 0) {
16631
+ throw new Error(`${path31} must include filePath or symbol`);
16632
+ }
16633
+ return {
16634
+ direction: value.direction,
16635
+ ...filePath !== void 0 ? { filePath } : {},
16636
+ ...symbol !== void 0 ? { symbol } : {}
16637
+ };
16638
+ }
16252
16639
  function parseExpected(input, path31) {
16253
16640
  if (!isRecord3(input)) {
16254
16641
  throw new Error(`${path31} must be an object`);
@@ -16261,9 +16648,11 @@ function parseExpected(input, path31) {
16261
16648
  const expectedOutcomeRaw = input.expectedOutcome;
16262
16649
  const recoveryExpectationRaw = input.recoveryExpectation;
16263
16650
  const gradedEvidenceRaw = input.gradedEvidence;
16651
+ const graphNeighborRaw = input.graphNeighbor;
16264
16652
  const filePath = parseStringOrUndefined(filePathRaw, `${path31}.filePath`);
16265
16653
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
16266
16654
  const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path31}.gradedEvidence`);
16655
+ const graphNeighbor = parseExpectedGraphNeighbor(graphNeighborRaw, `${path31}.graphNeighbor`);
16267
16656
  const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path31}.expectedOutcome`);
16268
16657
  if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
16269
16658
  throw new Error(
@@ -16292,7 +16681,8 @@ function parseExpected(input, path31) {
16292
16681
  expectedRoute,
16293
16682
  expectedOutcome,
16294
16683
  recoveryExpectation,
16295
- ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
16684
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {},
16685
+ ...graphNeighbor !== void 0 ? { graphNeighbor } : {}
16296
16686
  };
16297
16687
  }
16298
16688
  function parseQueryLanguage(value, path31) {
@@ -16427,6 +16817,21 @@ function parseBudget(raw, sourceLabel) {
16427
16817
  "minRawDistinctTop3Ratio",
16428
16818
  sourceLabel
16429
16819
  ),
16820
+ minGraphNeighborRecall: parseThresholdValue(
16821
+ thresholds.minGraphNeighborRecall,
16822
+ "minGraphNeighborRecall",
16823
+ sourceLabel
16824
+ ),
16825
+ minRouteAccuracy: parseThresholdValue(
16826
+ thresholds.minRouteAccuracy,
16827
+ "minRouteAccuracy",
16828
+ sourceLabel
16829
+ ),
16830
+ minOutcomeAccuracy: parseThresholdValue(
16831
+ thresholds.minOutcomeAccuracy,
16832
+ "minOutcomeAccuracy",
16833
+ sourceLabel
16834
+ ),
16430
16835
  maxContextResponseTokensAverage: parseThresholdValue(
16431
16836
  thresholds.maxContextResponseTokensAverage,
16432
16837
  "maxContextResponseTokensAverage",
@@ -16491,6 +16896,120 @@ function buildDatasetFingerprint(dataset) {
16491
16896
  const canonical = JSON.stringify(normalizeForFingerprint(dataset));
16492
16897
  return crypto2.createHash("sha256").update(canonical).digest("hex");
16493
16898
  }
16899
+ function normalizedPath2(value) {
16900
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
16901
+ }
16902
+ function pathsMatch2(left, right) {
16903
+ const normalizedLeft = normalizedPath2(left);
16904
+ const normalizedRight = normalizedPath2(right);
16905
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
16906
+ }
16907
+ function toEvalSearchResult(result) {
16908
+ return {
16909
+ filePath: result.filePath,
16910
+ startLine: result.startLine,
16911
+ endLine: result.endLine,
16912
+ score: result.score,
16913
+ chunkType: result.chunkType,
16914
+ name: result.name
16915
+ };
16916
+ }
16917
+ function selectResolvedTarget(definitions, resolution) {
16918
+ if (resolution?.status !== "resolved") return definitions[0];
16919
+ 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];
16920
+ }
16921
+ function callerResult(edge) {
16922
+ if (!edge.fromSymbolFilePath) return void 0;
16923
+ return {
16924
+ filePath: edge.fromSymbolFilePath,
16925
+ startLine: edge.line,
16926
+ endLine: edge.line,
16927
+ score: 0,
16928
+ chunkType: "graph-caller",
16929
+ name: edge.fromSymbolName,
16930
+ graphDirection: "caller"
16931
+ };
16932
+ }
16933
+ function calleeResult(edge, symbols) {
16934
+ 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;
16935
+ if (!symbol) return void 0;
16936
+ return {
16937
+ filePath: symbol.filePath,
16938
+ startLine: symbol.startLine,
16939
+ endLine: symbol.endLine,
16940
+ score: 0,
16941
+ chunkType: "graph-callee",
16942
+ name: symbol.name,
16943
+ graphDirection: "callee"
16944
+ };
16945
+ }
16946
+ async function runEditContextQuery(indexer, projectRoot, query) {
16947
+ let definitions = [];
16948
+ let conceptual = [];
16949
+ let callers;
16950
+ let callees;
16951
+ const editContext = await resolveCodebaseEditContextWithDependencies({
16952
+ query: query.query,
16953
+ symbol: query.args?.symbol,
16954
+ filePath: query.args?.filePath ?? query.expected.filePath,
16955
+ callerLimit: query.args?.callerLimit,
16956
+ calleeLimit: query.args?.calleeLimit,
16957
+ tokenBudget: query.args?.tokenBudget
16958
+ }, {
16959
+ searchCodebase: async (searchQuery, options) => {
16960
+ conceptual = await indexer.search(searchQuery, options?.limit, {
16961
+ filterByBranch: !!query.expected.branch
16962
+ });
16963
+ return conceptual;
16964
+ },
16965
+ implementationLookup: async (symbol, options) => {
16966
+ definitions = await indexer.search(symbol, options?.limit, {
16967
+ filterByBranch: !!query.expected.branch,
16968
+ definitionIntent: true
16969
+ });
16970
+ return definitions;
16971
+ },
16972
+ getCallGraphData: async (params) => {
16973
+ const result = await getCallGraphDataForIndexer(indexer, projectRoot, params);
16974
+ if (params.direction === "callers") callers = result;
16975
+ else callees = result;
16976
+ return result;
16977
+ }
16978
+ });
16979
+ const resolution = callers?.resolution;
16980
+ const target = selectResolvedTarget(definitions, resolution);
16981
+ const targetCandidates = target ? [target] : [...definitions, ...conceptual];
16982
+ const results = targetCandidates.filter((candidate) => (resolution?.status !== "resolved" || editContext.details.sourceIncluded) && editContext.text.includes(
16983
+ `${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
16984
+ )).map(toEvalSearchResult);
16985
+ if (query.expected.graphNeighbor) {
16986
+ const symbols = await indexer.getCallGraphSymbols();
16987
+ const callerLimit = query.args?.callerLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
16988
+ const calleeLimit = query.args?.calleeLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
16989
+ const publishedCallers = (callers?.callers ?? []).slice(0, callerLimit).filter((edge) => editContext.text.includes(
16990
+ `${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
16991
+ ));
16992
+ const publishedCallees = (callees?.callees ?? []).slice(0, calleeLimit).filter((edge) => resolution?.status === "resolved" && editContext.text.includes(
16993
+ `${edge.targetName} from ${resolution.filePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
16994
+ ));
16995
+ results.push(
16996
+ ...publishedCallers.map(callerResult).filter((item) => item !== void 0),
16997
+ ...publishedCallees.map((edge) => calleeResult(edge, symbols)).filter((item) => item !== void 0)
16998
+ );
16999
+ }
17000
+ return {
17001
+ results,
17002
+ resolvedRoute: resolution?.status === "resolved" ? "definition" : "search",
17003
+ routedQuery: query.args?.symbol ?? query.query,
17004
+ context: {
17005
+ tokenBudget: editContext.details.tokenBudget,
17006
+ responseTokens: editContext.details.tokenEstimate,
17007
+ candidateCount: results.length,
17008
+ deduplicatedCount: results.length,
17009
+ omittedCount: 0
17010
+ }
17011
+ };
17012
+ }
16494
17013
  async function runEvaluation(options) {
16495
17014
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
16496
17015
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -16522,6 +17041,7 @@ async function runEvaluation(options) {
16522
17041
  );
16523
17042
  }
16524
17043
  const start = import_perf_hooks2.performance.now();
17044
+ const editContextResult = query.retrievalMode === "edit-context" ? await runEditContextQuery(indexer, options.projectRoot, query) : void 0;
16525
17045
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
16526
17046
  query: query.query,
16527
17047
  symbol: query.args?.symbol,
@@ -16545,31 +17065,32 @@ async function runEvaluation(options) {
16545
17065
  directory: scope.directory
16546
17066
  })
16547
17067
  }) : void 0;
16548
- const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
17068
+ const result = editContextResult?.results ?? contextResult?.details?.results ?? await indexer.search(query.query, 10, {
16549
17069
  metadataOnly: true,
16550
17070
  filterByBranch: !!query.expected.branch,
16551
17071
  fileType: query.args?.fileType,
16552
17072
  directory: query.args?.directory
16553
17073
  });
16554
17074
  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;
17075
+ const resolvedRoute = editContextResult?.resolvedRoute ?? (contextResult?.details?.route === "definition" ? "definition" : "search");
17076
+ const routedQuery = editContextResult?.routedQuery ?? contextResult?.details?.routedQuery ?? query.query;
16557
17077
  const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
16558
17078
  const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
16559
17079
  const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
16560
17080
  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 ? {
17081
+ const materialized = result.map((item) => {
17082
+ const graphDirection = "graphDirection" in item && (item.graphDirection === "caller" || item.graphDirection === "callee") ? item.graphDirection : void 0;
17083
+ return {
17084
+ filePath: item.filePath,
17085
+ startLine: item.startLine,
17086
+ endLine: item.endLine,
17087
+ score: item.score,
17088
+ chunkType: item.chunkType,
17089
+ name: item.name,
17090
+ graphDirection
17091
+ };
17092
+ });
17093
+ const contextMeasurement = editContextResult?.context ?? (contextResult?.details ? {
16573
17094
  tokenBudget: contextResult.details.tokenBudget,
16574
17095
  responseTokens: contextResult.details.tokenEstimate,
16575
17096
  candidateCount: contextResult.details.candidateCount ?? 0,
@@ -16577,7 +17098,11 @@ async function runEvaluation(options) {
16577
17098
  omittedCount: contextResult.details.omittedCount ?? 0,
16578
17099
  recoveryUsed,
16579
17100
  recoveryRelaxed
16580
- } : void 0));
17101
+ } : void 0);
17102
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
17103
+ resolvedRoute,
17104
+ routedQuery
17105
+ }, contextMeasurement));
16581
17106
  }
16582
17107
  const logger = indexer.getLogger();
16583
17108
  const metricSnapshot = logger.getMetrics();
@@ -17114,7 +17639,11 @@ var import_zod2 = require("zod");
17114
17639
 
17115
17640
  // src/tools/execute-common.ts
17116
17641
  async function executeCodebaseContext(projectRoot, host, args) {
17117
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17642
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17643
+ return { text: result.text, details: result.details };
17644
+ }
17645
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17646
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17118
17647
  }
17119
17648
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17120
17649
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17230,6 +17759,7 @@ function formatPrImpact(result) {
17230
17759
  // src/tools/tool-names.ts
17231
17760
  var TOOL_NAME = {
17232
17761
  CODEBASE_CONTEXT: "codebase_context",
17762
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
17233
17763
  CODEBASE_SEARCH: "codebase_search",
17234
17764
  CODEBASE_PEEK: "codebase_peek",
17235
17765
  FIND_SIMILAR: "find_similar",
@@ -17253,6 +17783,7 @@ var TOOL_NAME = {
17253
17783
  };
17254
17784
  var PORTABLE_TOOL_NAMES = [
17255
17785
  TOOL_NAME.CODEBASE_CONTEXT,
17786
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17256
17787
  TOOL_NAME.CODEBASE_SEARCH,
17257
17788
  TOOL_NAME.CODEBASE_PEEK,
17258
17789
  TOOL_NAME.INDEX_CODEBASE,
@@ -17269,6 +17800,7 @@ var PORTABLE_TOOL_NAMES = [
17269
17800
  ];
17270
17801
  var OPENCODE_TOOL_NAMES = [
17271
17802
  TOOL_NAME.CODEBASE_CONTEXT,
17803
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17272
17804
  TOOL_NAME.CODEBASE_SEARCH,
17273
17805
  TOOL_NAME.CODEBASE_PEEK,
17274
17806
  TOOL_NAME.INDEX_CODEBASE,
@@ -17289,6 +17821,7 @@ var OPENCODE_TOOL_NAMES = [
17289
17821
  ];
17290
17822
  var PI_TOOL_NAMES = [
17291
17823
  TOOL_NAME.CODEBASE_CONTEXT,
17824
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17292
17825
  TOOL_NAME.CODEBASE_SEARCH,
17293
17826
  TOOL_NAME.CODEBASE_PEEK,
17294
17827
  TOOL_NAME.FIND_SIMILAR,
@@ -17332,10 +17865,30 @@ function registerMcpTools(server, runtime) {
17332
17865
  directory: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
17333
17866
  tokenBudget: allowNullAsUndefined(
17334
17867
  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})`)
17868
+ ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
17869
+ diagnostic: import_zod2.z.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
17336
17870
  },
17337
17871
  async (args) => {
17338
17872
  const result = await executeCodebaseContext(runtime.projectRoot, runtime.host, args);
17873
+ return {
17874
+ content: [{ type: "text", text: result.text }],
17875
+ ...args.diagnostic ? { structuredContent: result.details } : {}
17876
+ };
17877
+ }
17878
+ );
17879
+ server.tool(
17880
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17881
+ "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.",
17882
+ {
17883
+ query: import_zod2.z.string().describe("The requested change or target behavior."),
17884
+ symbol: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Authoritative target symbol when known."),
17885
+ filePath: allowNullAsUndefined(import_zod2.z.string().optional()).describe("Optional file path used to disambiguate duplicate symbol names."),
17886
+ 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)),
17887
+ 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)),
17888
+ 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))
17889
+ },
17890
+ async (args) => {
17891
+ const result = await executeCodebaseEditContext(runtime.projectRoot, runtime.host, args);
17339
17892
  return { content: [{ type: "text", text: result.text }] };
17340
17893
  }
17341
17894
  );
@@ -19598,6 +20151,8 @@ var GitHeadWatcher = class {
19598
20151
  debounceTimer = null;
19599
20152
  debounceMs = 100;
19600
20153
  // Short debounce for git operations
20154
+ readyPromise = Promise.resolve();
20155
+ resolveReady = null;
19601
20156
  constructor(projectRoot) {
19602
20157
  this.projectRoot = projectRoot;
19603
20158
  }
@@ -19606,8 +20161,12 @@ var GitHeadWatcher = class {
19606
20161
  return;
19607
20162
  }
19608
20163
  if (!isGitRepo(this.projectRoot)) {
20164
+ this.readyPromise = Promise.resolve();
19609
20165
  return;
19610
20166
  }
20167
+ this.readyPromise = new Promise((resolve18) => {
20168
+ this.resolveReady = resolve18;
20169
+ });
19611
20170
  this.onBranchChange = handler;
19612
20171
  this.currentBranch = getCurrentBranch(this.projectRoot);
19613
20172
  const headPath = getHeadPath(this.projectRoot);
@@ -19622,6 +20181,10 @@ var GitHeadWatcher = class {
19622
20181
  });
19623
20182
  this.watcher.on("change", () => this.handleHeadChange());
19624
20183
  this.watcher.on("add", () => this.handleHeadChange());
20184
+ this.watcher.once("ready", () => {
20185
+ this.resolveReady?.();
20186
+ this.resolveReady = null;
20187
+ });
19625
20188
  }
19626
20189
  handleHeadChange() {
19627
20190
  if (this.debounceTimer) {
@@ -19659,10 +20222,16 @@ var GitHeadWatcher = class {
19659
20222
  await watcher.close();
19660
20223
  }
19661
20224
  this.onBranchChange = null;
20225
+ this.resolveReady?.();
20226
+ this.resolveReady = null;
20227
+ this.readyPromise = Promise.resolve();
19662
20228
  }
19663
20229
  isRunning() {
19664
20230
  return this.watcher !== null;
19665
20231
  }
20232
+ async waitUntilReady() {
20233
+ await this.readyPromise;
20234
+ }
19666
20235
  };
19667
20236
 
19668
20237
  // src/watcher/index.ts
@@ -19712,7 +20281,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host, options
19712
20281
  fileWatcher,
19713
20282
  gitWatcher,
19714
20283
  whenReady() {
19715
- return fileWatcher.waitUntilReady();
20284
+ return Promise.all([
20285
+ fileWatcher.waitUntilReady(),
20286
+ gitWatcher?.waitUntilReady()
20287
+ ]).then(() => void 0);
19716
20288
  },
19717
20289
  async stop() {
19718
20290
  stopped = true;
@@ -19773,9 +20345,9 @@ function parseGitActivity(output) {
19773
20345
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
19774
20346
  const churn = Number(addedRaw) + Number(deletedRaw);
19775
20347
  if (!Number.isFinite(churn) || churn <= 0) continue;
19776
- const normalizedPath = normalizePath4(filePath);
19777
- const previous = activity.get(normalizedPath);
19778
- activity.set(normalizedPath, {
20348
+ const normalizedPath3 = normalizePath4(filePath);
20349
+ const previous = activity.get(normalizedPath3);
20350
+ activity.set(normalizedPath3, {
19779
20351
  churn: (previous?.churn ?? 0) + churn,
19780
20352
  commits: (previous?.commits ?? 0) + 1,
19781
20353
  latestDate: previous?.latestDate ?? latestDate,
@@ -20365,8 +20937,8 @@ function transformForVisualization(symbols, edges, options = {}) {
20365
20937
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
20366
20938
  filteredSymbols = symbols.filter(
20367
20939
  (s) => {
20368
- const normalizedPath = s.filePath.replace(/\\/g, "/");
20369
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
20940
+ const normalizedPath3 = s.filePath.replace(/\\/g, "/");
20941
+ return normalizedPath3 === normalizedDir || normalizedPath3.startsWith(normalizedDirWithSlash) || normalizedPath3.endsWith(`/${normalizedDir}`) || normalizedPath3.includes(normalizedAbsoluteSuffix);
20370
20942
  }
20371
20943
  );
20372
20944
  }