opencode-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.js 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
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -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";
@@ -7094,9 +7095,9 @@ function normalizeFilePathForHintMatch(filePath) {
7094
7095
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
7095
7096
  }
7096
7097
  function pathMatchesHint(filePath, hint) {
7097
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
7098
+ const normalizedPath3 = normalizeFilePathForHintMatch(filePath);
7098
7099
  const normalizedHint = normalizeFilePathForHintMatch(hint);
7099
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
7100
+ return normalizedPath3.endsWith(normalizedHint) || normalizedPath3.includes(`/${normalizedHint}`) || normalizedPath3.includes(normalizedHint);
7100
7101
  }
7101
7102
  function extractFilePathHint(query) {
7102
7103
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -11370,6 +11371,20 @@ var Indexer = class _Indexer {
11370
11371
  requestedLimit = nextLimit;
11371
11372
  }
11372
11373
  }
11374
+ buildCandidateSnapshot(candidate) {
11375
+ return {
11376
+ id: candidate.id,
11377
+ filePath: candidate.metadata.filePath,
11378
+ startLine: candidate.metadata.startLine,
11379
+ endLine: candidate.metadata.endLine,
11380
+ score: candidate.score,
11381
+ chunkType: candidate.metadata.chunkType,
11382
+ name: candidate.metadata.name
11383
+ };
11384
+ }
11385
+ buildCandidateSnapshotList(candidates) {
11386
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
11387
+ }
11373
11388
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
11374
11389
  return this.searchCandidatesWithBranchPrefilter(
11375
11390
  initialLimit,
@@ -11561,6 +11576,16 @@ var Indexer = class _Indexer {
11561
11576
  prefilterMs: Math.round(prefilterMs * 100) / 100,
11562
11577
  fusionMs: Math.round(fusionMs * 100) / 100
11563
11578
  });
11579
+ if (options?.trace) {
11580
+ options.trace({
11581
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
11582
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
11583
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
11584
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
11585
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
11586
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
11587
+ });
11588
+ }
11564
11589
  const metadataOnly = options?.metadataOnly ?? false;
11565
11590
  return Promise.all(
11566
11591
  finalResults.map(async (r) => {
@@ -12609,6 +12634,42 @@ var Indexer = class _Indexer {
12609
12634
  }
12610
12635
  };
12611
12636
 
12637
+ // src/tools/contracts.ts
12638
+ var CHUNK_TYPES = [
12639
+ "function",
12640
+ "class",
12641
+ "method",
12642
+ "interface",
12643
+ "type",
12644
+ "enum",
12645
+ "struct",
12646
+ "impl",
12647
+ "trait",
12648
+ "module",
12649
+ "other"
12650
+ ];
12651
+ var CALL_GRAPH_DIRECTIONS = ["callers", "callees"];
12652
+ var RELATIONSHIP_TYPES = [
12653
+ "Call",
12654
+ "MethodCall",
12655
+ "Constructor",
12656
+ "Import",
12657
+ "Inherits",
12658
+ "Implements"
12659
+ ];
12660
+ var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
12661
+ var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
12662
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
12663
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
12664
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
12665
+ var CODE_COMMUNITIES_MIN_SIZE = 1;
12666
+ var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
12667
+ var CODE_COMMUNITIES_MAX_LIMIT = 100;
12668
+ var CODE_COMMUNITIES_DEFAULT_HUB_THRESHOLD = 5;
12669
+ var CODE_COMMUNITIES_MIN_COUPLING = 1;
12670
+ var CODE_COMMUNITIES_DEFAULT_COUPLING_LIMIT = 20;
12671
+ var CODE_COMMUNITIES_MAX_COUPLING_LIMIT = 100;
12672
+
12612
12673
  // src/tools/operations.ts
12613
12674
  import { existsSync as existsSync12, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
12614
12675
  import * as path21 from "path";
@@ -12737,39 +12798,6 @@ function formatCodeCommunities(result) {
12737
12798
  return lines.join("\n");
12738
12799
  }
12739
12800
 
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
12801
  // src/tools/context-pack.ts
12774
12802
  import { get_encoding } from "tiktoken";
12775
12803
  var MIN_CONTEXT_PACK_TOKEN_BUDGET = 128;
@@ -12871,6 +12899,16 @@ function compactEvidenceValue(value, maxChars) {
12871
12899
  }
12872
12900
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
12873
12901
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
12902
+ function toContextPackTraceCandidate(result) {
12903
+ return {
12904
+ filePath: result.filePath,
12905
+ startLine: result.startLine,
12906
+ endLine: result.endLine,
12907
+ score: result.score,
12908
+ chunkType: result.chunkType,
12909
+ name: result.name
12910
+ };
12911
+ }
12874
12912
  function formatExactSearchHandoff(results) {
12875
12913
  const suggestedNames = [];
12876
12914
  const seen = /* @__PURE__ */ new Set();
@@ -12919,13 +12957,13 @@ function buildContextPack(results, options = {}) {
12919
12957
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
12920
12958
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
12921
12959
  const candidateCount = results.length;
12922
- const deduplicated = deduplicateContextCandidates(
12923
- rankContextCandidates(
12924
- results,
12925
- options.preferImplementationPaths ?? false
12926
- )
12927
- );
12928
- const diversified = diversifyContextCandidates(deduplicated);
12960
+ const preserveInputOrder = options.preserveInputOrder ?? false;
12961
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
12962
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
12963
+ const deduplicated = deduplicateContextCandidates(ranked);
12964
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
12965
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
12966
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
12929
12967
  const duplicateCount = candidateCount - deduplicated.length;
12930
12968
  const selectable = diversified.slice(0, maxResults);
12931
12969
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -12958,6 +12996,15 @@ function buildContextPack(results, options = {}) {
12958
12996
  const fitted = fitTextToContextBudget(text, tokenBudget);
12959
12997
  const budgetOmittedCount = selectable.length - selected.length;
12960
12998
  const omittedCount = candidateCount - selected.length;
12999
+ if (options.trace) {
13000
+ options.trace({
13001
+ inputCandidates: results.map(toContextPackTraceCandidate),
13002
+ rankedCandidates,
13003
+ deduplicatedCandidates,
13004
+ diversifiedCandidates,
13005
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
13006
+ });
13007
+ }
12961
13008
  return {
12962
13009
  requestedTokenBudget,
12963
13010
  tokenBudget,
@@ -14705,7 +14752,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14705
14752
  definitionIntent: options.definitionIntent,
14706
14753
  blameAuthor: options.blameAuthor,
14707
14754
  blameSha: options.blameSha,
14708
- blameSince: options.blameSince
14755
+ blameSince: options.blameSince,
14756
+ trace: options.trace
14709
14757
  });
14710
14758
  }
14711
14759
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14759,15 +14807,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14759
14807
  return indexer.search(query, options.limit, {
14760
14808
  fileType: options.fileType,
14761
14809
  directory: options.directory,
14762
- definitionIntent: true
14810
+ definitionIntent: true,
14811
+ trace: options.trace
14763
14812
  });
14764
14813
  }
14765
14814
  async function getCallGraphData(projectRoot, host, params) {
14766
14815
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14767
14816
  const root = getProjectRoot(projectRoot, host);
14768
14817
  const indexer = getIndexerForProject(root, host);
14818
+ return getCallGraphDataForIndexer(indexer, root, params);
14819
+ }
14820
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14769
14821
  const symbols = await indexer.getCallGraphSymbols();
14770
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14822
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14771
14823
  const direction = params.direction === "callees" ? "callees" : "callers";
14772
14824
  if (resolution.status !== "resolved") {
14773
14825
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -15095,6 +15147,27 @@ function buildRecoveryDetails(attempts, successIndex) {
15095
15147
  successfulAttemptIndex: successIndex
15096
15148
  };
15097
15149
  }
15150
+ function serializeAttempts(attempts) {
15151
+ return attempts.map((attempt) => ({
15152
+ kind: attempt.kind,
15153
+ scope: attempt.scope,
15154
+ resultCount: attempt.resultCount,
15155
+ relaxedFields: attempt.relaxedFields
15156
+ }));
15157
+ }
15158
+ function buildSearchDiagnostic(attempt) {
15159
+ if (!attempt) {
15160
+ return void 0;
15161
+ }
15162
+ return {
15163
+ route: attempt.kind,
15164
+ routedQuery: attempt.query,
15165
+ searchQuery: attempt.query,
15166
+ searchScope: attempt.scopeFilter,
15167
+ searchTrace: attempt.searchTrace,
15168
+ contextPackTrace: attempt.contextPackTrace
15169
+ };
15170
+ }
15098
15171
  function trimOrUndefined2(value) {
15099
15172
  const normalized = value?.trim();
15100
15173
  if (!normalized) {
@@ -15134,6 +15207,7 @@ async function resolveSearchContext(input, operations) {
15134
15207
  const hasFilters = Boolean(fileType || directory);
15135
15208
  const relaxedFields = relaxedHintFields(fileType, directory);
15136
15209
  const attempts = [];
15210
+ const attemptStates = [];
15137
15211
  const decisions = {
15138
15212
  inferredDefinitionMiss: false,
15139
15213
  fallbackFromOriginalConceptualToInferred: false,
@@ -15162,8 +15236,20 @@ async function resolveSearchContext(input, operations) {
15162
15236
  if (seenAttempts.has(key)) {
15163
15237
  return [];
15164
15238
  }
15165
- const results = await runAttempt();
15239
+ const attemptState = {
15240
+ kind,
15241
+ scope: describeScope(scope.fileType, scope.directory),
15242
+ resultCount: 0,
15243
+ relaxedFields: [...relaxedFieldsForAttempt],
15244
+ query: attemptQuery,
15245
+ scopeFilter: scope
15246
+ };
15247
+ const results = await runAttempt((trace) => {
15248
+ attemptState.searchTrace = trace;
15249
+ });
15250
+ attemptState.resultCount = results.length;
15166
15251
  seenAttempts.add(key);
15252
+ attemptStates.push(attemptState);
15167
15253
  attempts.push({
15168
15254
  kind,
15169
15255
  scope: describeScope(scope.fileType, scope.directory),
@@ -15183,7 +15269,7 @@ async function resolveSearchContext(input, operations) {
15183
15269
  symbol,
15184
15270
  scope,
15185
15271
  relaxedFieldsForAttempt,
15186
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15272
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15187
15273
  );
15188
15274
  };
15189
15275
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -15192,13 +15278,23 @@ async function resolveSearchContext(input, operations) {
15192
15278
  searchQuery,
15193
15279
  scope,
15194
15280
  relaxedFieldsForAttempt,
15195
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15281
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
15196
15282
  );
15197
15283
  };
15198
- const toResult = (route, routedQuery, pack) => {
15284
+ const findSuccessfulAttemptState = (route) => {
15285
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15286
+ const attempt = attemptStates[index];
15287
+ if (attempt.kind === route && attempt.resultCount > 0) {
15288
+ return attempt;
15289
+ }
15290
+ }
15291
+ return void 0;
15292
+ };
15293
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
15199
15294
  const base = packedResult(route, routedQuery, pack);
15200
15295
  const baseDetails = base.details;
15201
15296
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15297
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
15202
15298
  return {
15203
15299
  text: base.text,
15204
15300
  details: {
@@ -15206,7 +15302,10 @@ async function resolveSearchContext(input, operations) {
15206
15302
  tokenBudget: baseDetails.tokenBudget,
15207
15303
  tokenEstimate: baseDetails.tokenEstimate,
15208
15304
  truncated: false,
15209
- recovery: buildRecoveryDetails(attempts, successIndex)
15305
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15306
+ ...input.diagnostic && {
15307
+ diagnostic: buildSearchDiagnostic(successState)
15308
+ }
15210
15309
  }
15211
15310
  };
15212
15311
  };
@@ -15220,8 +15319,18 @@ async function resolveSearchContext(input, operations) {
15220
15319
  buildContextPack(scopedDefinitionResults, {
15221
15320
  tokenBudget,
15222
15321
  maxResults: limit,
15223
- heading
15224
- })
15322
+ heading,
15323
+ preserveInputOrder: true,
15324
+ ...input.diagnostic ? {
15325
+ trace: (trace) => {
15326
+ const attemptState = findSuccessfulAttemptState("definition");
15327
+ if (attemptState) {
15328
+ attemptState.contextPackTrace = trace;
15329
+ }
15330
+ }
15331
+ } : void 0
15332
+ }),
15333
+ findSuccessfulAttemptState("definition")
15225
15334
  );
15226
15335
  }
15227
15336
  if (explicitSymbol) {
@@ -15239,8 +15348,18 @@ async function resolveSearchContext(input, operations) {
15239
15348
  buildContextPack(unscopedDefinitionResults, {
15240
15349
  tokenBudget,
15241
15350
  maxResults: limit,
15242
- heading: heading2
15243
- })
15351
+ heading: heading2,
15352
+ preserveInputOrder: true,
15353
+ ...input.diagnostic ? {
15354
+ trace: (trace) => {
15355
+ const attemptState = findSuccessfulAttemptState("definition");
15356
+ if (attemptState) {
15357
+ attemptState.contextPackTrace = trace;
15358
+ }
15359
+ }
15360
+ } : void 0
15361
+ }),
15362
+ findSuccessfulAttemptState("definition")
15244
15363
  );
15245
15364
  }
15246
15365
  }
@@ -15259,7 +15378,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15259
15378
  tokenBudget: heading.tokenBudget,
15260
15379
  tokenEstimate: heading.tokenEstimate,
15261
15380
  truncated: heading.truncated,
15262
- recovery: buildRecoveryDetails(attempts, null)
15381
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15382
+ ...input.diagnostic && {
15383
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15384
+ }
15263
15385
  }
15264
15386
  };
15265
15387
  }
@@ -15299,8 +15421,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15299
15421
  maxResults: limit,
15300
15422
  heading,
15301
15423
  includeExactSearchHandoff: true,
15302
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15303
- })
15424
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15425
+ ...input.diagnostic ? {
15426
+ trace: (trace) => {
15427
+ const attemptState = findSuccessfulAttemptState("conceptual");
15428
+ if (attemptState) {
15429
+ attemptState.contextPackTrace = trace;
15430
+ }
15431
+ }
15432
+ } : void 0
15433
+ }),
15434
+ findSuccessfulAttemptState("conceptual")
15304
15435
  );
15305
15436
  }
15306
15437
  }
@@ -15316,7 +15447,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15316
15447
  tokenBudget: fallbackText.tokenBudget,
15317
15448
  tokenEstimate: fallbackText.tokenEstimate,
15318
15449
  truncated: fallbackText.truncated,
15319
- recovery: buildRecoveryDetails(attempts, null)
15450
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15451
+ ...input.diagnostic && {
15452
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15453
+ }
15320
15454
  }
15321
15455
  };
15322
15456
  }
@@ -15397,17 +15531,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
15397
15531
  details: fittedDetails("path", fitted, 0)
15398
15532
  };
15399
15533
  }
15400
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15401
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
15534
+ return resolveSearchContext({
15535
+ query: input.query,
15536
+ symbol,
15537
+ limit,
15538
+ tokenBudget,
15539
+ fileType,
15540
+ directory,
15541
+ diagnostic: input.diagnostic
15542
+ }, {
15543
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
15402
15544
  limit: retrievalLimit,
15403
15545
  fileType: scope.fileType,
15404
- directory: scope.directory
15546
+ directory: scope.directory,
15547
+ trace
15405
15548
  }),
15406
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
15549
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
15407
15550
  limit: retrievalLimit,
15408
15551
  fileType: scope.fileType,
15409
15552
  directory: scope.directory,
15410
- metadataOnly: true
15553
+ metadataOnly: true,
15554
+ trace
15411
15555
  })
15412
15556
  });
15413
15557
  }
@@ -15472,6 +15616,181 @@ async function resolveCodebaseContext(projectRoot, host, input) {
15472
15616
  }
15473
15617
  }
15474
15618
 
15619
+ // src/tools/edit-context.ts
15620
+ function edgeLimit(value) {
15621
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15622
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15623
+ }
15624
+ return Math.min(
15625
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15626
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15627
+ );
15628
+ }
15629
+ function normalizedPath(value) {
15630
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15631
+ }
15632
+ function pathsMatch(left, right) {
15633
+ const normalizedLeft = normalizedPath(left);
15634
+ const normalizedRight = normalizedPath(right);
15635
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15636
+ }
15637
+ function targetSource(results, resolution) {
15638
+ 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);
15639
+ }
15640
+ function formatSource(result) {
15641
+ const name = result.name ? ` ${result.name}` : "";
15642
+ return [
15643
+ "## Target implementation",
15644
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15645
+ "```",
15646
+ result.content,
15647
+ "```"
15648
+ ].join("\n");
15649
+ }
15650
+ function formatCallers(edges) {
15651
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15652
+ return [
15653
+ "## Direct callers",
15654
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15655
+ ].join("\n");
15656
+ }
15657
+ function formatCallees(edges, sourceFilePath) {
15658
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15659
+ return [
15660
+ "## Direct callees",
15661
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15662
+ ].join("\n");
15663
+ }
15664
+ function formatResolutionRisk(resolution) {
15665
+ if (resolution.status === "ambiguous") {
15666
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15667
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15668
+ }
15669
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15670
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15671
+ }
15672
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15673
+ }
15674
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15675
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15676
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15677
+ tokenBudget: tokenBudget ?? void 0,
15678
+ heading: "## Conceptual evidence",
15679
+ maxResults: 5,
15680
+ includeExactSearchHandoff: false,
15681
+ preferImplementationPaths: true
15682
+ });
15683
+ const fitted = fitTextToContextBudget(`${risk}
15684
+
15685
+ ${pack.text}`, tokenBudget ?? void 0);
15686
+ return {
15687
+ text: fitted.text,
15688
+ details: {
15689
+ resolution,
15690
+ tokenBudget: fitted.tokenBudget,
15691
+ tokenEstimate: fitted.tokenEstimate,
15692
+ truncated: fitted.truncated,
15693
+ sourceIncluded: candidateSource.length > 0,
15694
+ callerCount: 0,
15695
+ calleeCount: 0
15696
+ }
15697
+ };
15698
+ }
15699
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15700
+ const symbol = input.symbol?.trim();
15701
+ if (!symbol) {
15702
+ return fallbackPack(
15703
+ dependencies,
15704
+ input.query,
15705
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15706
+ "not_requested",
15707
+ input.tokenBudget
15708
+ );
15709
+ }
15710
+ let callersResult;
15711
+ try {
15712
+ callersResult = await dependencies.getCallGraphData({
15713
+ name: symbol,
15714
+ filePath: input.filePath ?? void 0,
15715
+ direction: "callers"
15716
+ });
15717
+ } catch (error) {
15718
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15719
+ const message = error instanceof Error ? error.message : String(error);
15720
+ return fallbackPack(
15721
+ dependencies,
15722
+ input.query,
15723
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15724
+ "graph_unavailable",
15725
+ input.tokenBudget,
15726
+ candidates
15727
+ );
15728
+ }
15729
+ if (callersResult.resolution.status !== "resolved") {
15730
+ return fallbackPack(
15731
+ dependencies,
15732
+ input.query,
15733
+ formatResolutionRisk(callersResult.resolution),
15734
+ callersResult.resolution.status,
15735
+ input.tokenBudget
15736
+ );
15737
+ }
15738
+ const resolution = callersResult.resolution;
15739
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15740
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15741
+ dependencies.getCallGraphData({
15742
+ name: symbol,
15743
+ filePath: input.filePath ?? resolution.filePath,
15744
+ direction: "callees"
15745
+ })
15746
+ ]);
15747
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15748
+ let graphRisk;
15749
+ let callees = [];
15750
+ if (calleesResult.status === "rejected") {
15751
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15752
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15753
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15754
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15755
+ } else {
15756
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15757
+ }
15758
+ const source = targetSource(definitionsResult.value, resolution);
15759
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15760
+ const sourceBudget = Math.max(
15761
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15762
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15763
+ );
15764
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15765
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15766
+ const fitted = fitTextToContextBudget([
15767
+ `# Pre-edit context for ${resolution.name}`,
15768
+ graphRisk,
15769
+ sourceText,
15770
+ formatCallers(callers),
15771
+ formatCallees(callees, resolution.filePath)
15772
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15773
+ return {
15774
+ text: fitted.text,
15775
+ details: {
15776
+ resolution: "resolved",
15777
+ tokenBudget: fitted.tokenBudget,
15778
+ tokenEstimate: fitted.tokenEstimate,
15779
+ truncated: fitted.truncated,
15780
+ sourceIncluded: source !== void 0,
15781
+ callerCount: callers.length,
15782
+ calleeCount: callees.length
15783
+ }
15784
+ };
15785
+ }
15786
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
15787
+ return resolveCodebaseEditContextWithDependencies(input, {
15788
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
15789
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
15790
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
15791
+ });
15792
+ }
15793
+
15475
15794
  // src/eval/budget.ts
15476
15795
  function evaluateBudgetGate(budget, summary, comparison) {
15477
15796
  const BASELINE_P95_EPSILON_MS = 1e-3;
@@ -15495,6 +15814,24 @@ function evaluateBudgetGate(budget, summary, comparison) {
15495
15814
  message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
15496
15815
  });
15497
15816
  }
15817
+ if (thresholds.minGraphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall !== void 0 && summary.metrics.graphNeighborRecall < thresholds.minGraphNeighborRecall) {
15818
+ violations.push({
15819
+ metric: "minGraphNeighborRecall",
15820
+ message: `Graph-neighbor recall ${summary.metrics.graphNeighborRecall.toFixed(4)} is below minimum ${thresholds.minGraphNeighborRecall.toFixed(4)}`
15821
+ });
15822
+ }
15823
+ if (thresholds.minRouteAccuracy !== void 0 && summary.metrics.routeAccuracy < thresholds.minRouteAccuracy) {
15824
+ violations.push({
15825
+ metric: "minRouteAccuracy",
15826
+ message: `Route accuracy ${summary.metrics.routeAccuracy.toFixed(4)} is below minimum ${thresholds.minRouteAccuracy.toFixed(4)}`
15827
+ });
15828
+ }
15829
+ if (thresholds.minOutcomeAccuracy !== void 0 && summary.metrics.outcomeAccuracy < thresholds.minOutcomeAccuracy) {
15830
+ violations.push({
15831
+ metric: "minOutcomeAccuracy",
15832
+ message: `Outcome accuracy ${summary.metrics.outcomeAccuracy.toFixed(4)} is below minimum ${thresholds.minOutcomeAccuracy.toFixed(4)}`
15833
+ });
15834
+ }
15498
15835
  if (comparison) {
15499
15836
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
15500
15837
  violations.push({
@@ -15672,6 +16009,14 @@ function isSymbolIntended(query) {
15672
16009
  function isExpectedFile(filePath, relevant) {
15673
16010
  return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
15674
16011
  }
16012
+ function graphNeighborMatches(query, result) {
16013
+ const expected = query.expected.graphNeighbor;
16014
+ if (!expected || result.graphDirection !== expected.direction) return false;
16015
+ if (expected.filePath !== void 0 && !pathMatchesExpected(result.filePath, expected.filePath)) {
16016
+ return false;
16017
+ }
16018
+ return expected.symbol === void 0 || result.name === expected.symbol;
16019
+ }
15675
16020
  function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
15676
16021
  let relevance = 0;
15677
16022
  for (const entry of relevant) {
@@ -15819,6 +16164,7 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
15819
16164
  routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
15820
16165
  outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
15821
16166
  recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
16167
+ graphNeighborMatched: query.expected.graphNeighbor === void 0 ? void 0 : results.some((result) => graphNeighborMatches(query, result)),
15822
16168
  language: query.language,
15823
16169
  difficulty: query.difficulty,
15824
16170
  tags: query.tags,
@@ -15868,7 +16214,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15868
16214
  "no-relevant-hit-top-k": 0
15869
16215
  };
15870
16216
  const latencies = perQuery.map((item) => item.latencyMs);
15871
- const contextQueries = perQuery.filter((item) => item.retrievalMode === "context");
16217
+ const contextQueries = perQuery.filter((item) => item.retrievalMode !== "search");
15872
16218
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
15873
16219
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
15874
16220
  const contextTokenUnits = totalContextResponseTokens / 1e3;
@@ -15878,6 +16224,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15878
16224
  let outcomeExpectedCount = 0;
15879
16225
  let recoveryMatchedCount = 0;
15880
16226
  let recoveryExpectedCount = 0;
16227
+ let graphNeighborMatchedCount = 0;
16228
+ let graphNeighborExpectedCount = 0;
15881
16229
  for (const query of perQuery) {
15882
16230
  if (positiveQueryIds.has(query.id)) {
15883
16231
  if (query.hitAt1) sum.hitAt1 += 1;
@@ -15906,6 +16254,10 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15906
16254
  recoveryExpectedCount += 1;
15907
16255
  if (query.recoveryMatched) recoveryMatchedCount += 1;
15908
16256
  }
16257
+ if (query.graphNeighborMatched !== void 0) {
16258
+ graphNeighborExpectedCount += 1;
16259
+ if (query.graphNeighborMatched) graphNeighborMatchedCount += 1;
16260
+ }
15909
16261
  }
15910
16262
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
15911
16263
  return {
@@ -15918,6 +16270,7 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
15918
16270
  routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
15919
16271
  outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
15920
16272
  recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
16273
+ graphNeighborRecall: graphNeighborExpectedCount === 0 ? 0 : graphNeighborMatchedCount / graphNeighborExpectedCount,
15921
16274
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
15922
16275
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
15923
16276
  latencyMs: {
@@ -16183,14 +16536,29 @@ function parseQueryArgs(value, path31) {
16183
16536
  throw new Error(`${path31} must be an object`);
16184
16537
  }
16185
16538
  const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16539
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16186
16540
  const fileType = parseStringOrUndefined(value.fileType, `${path31}.fileType`);
16187
16541
  const directory = parseStringOrUndefined(value.directory, `${path31}.directory`);
16542
+ const callerLimit = parsePositiveIntegerOrUndefined(value.callerLimit, `${path31}.callerLimit`);
16543
+ const calleeLimit = parsePositiveIntegerOrUndefined(value.calleeLimit, `${path31}.calleeLimit`);
16544
+ const tokenBudget = parsePositiveIntegerOrUndefined(value.tokenBudget, `${path31}.tokenBudget`);
16188
16545
  return {
16189
16546
  ...symbol !== void 0 ? { symbol } : {},
16547
+ ...filePath !== void 0 ? { filePath } : {},
16190
16548
  ...fileType !== void 0 ? { fileType } : {},
16191
- ...directory !== void 0 ? { directory } : {}
16549
+ ...directory !== void 0 ? { directory } : {},
16550
+ ...callerLimit !== void 0 ? { callerLimit } : {},
16551
+ ...calleeLimit !== void 0 ? { calleeLimit } : {},
16552
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
16192
16553
  };
16193
16554
  }
16555
+ function parsePositiveIntegerOrUndefined(value, path31) {
16556
+ if (value === void 0 || value === null) return void 0;
16557
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
16558
+ throw new Error(`${path31} must be a positive integer`);
16559
+ }
16560
+ return value;
16561
+ }
16194
16562
  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
16563
  function parseSemanticVersion(value, path31) {
16196
16564
  if (!isNonEmptyString(value)) {
@@ -16203,8 +16571,8 @@ function parseSemanticVersion(value, path31) {
16203
16571
  }
16204
16572
  function parseRetrievalMode(value, path31) {
16205
16573
  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`);
16574
+ if (value === "context" || value === "edit-context") return value;
16575
+ throw new Error(`${path31} must be one of: search, context, edit-context`);
16208
16576
  }
16209
16577
  function parseStringOrUndefined(value, path31) {
16210
16578
  if (value === void 0 || value === null) return void 0;
@@ -16244,6 +16612,25 @@ function parseEvidenceRelevance(value, path31) {
16244
16612
  }
16245
16613
  return value;
16246
16614
  }
16615
+ function parseExpectedGraphNeighbor(value, path31) {
16616
+ if (value === void 0) return void 0;
16617
+ if (!isRecord3(value)) {
16618
+ throw new Error(`${path31} must be an object`);
16619
+ }
16620
+ if (value.direction !== "caller" && value.direction !== "callee") {
16621
+ throw new Error(`${path31}.direction must be one of: caller, callee`);
16622
+ }
16623
+ const filePath = parseStringOrUndefined(value.filePath, `${path31}.filePath`);
16624
+ const symbol = parseStringOrUndefined(value.symbol, `${path31}.symbol`);
16625
+ if (filePath === void 0 && symbol === void 0) {
16626
+ throw new Error(`${path31} must include filePath or symbol`);
16627
+ }
16628
+ return {
16629
+ direction: value.direction,
16630
+ ...filePath !== void 0 ? { filePath } : {},
16631
+ ...symbol !== void 0 ? { symbol } : {}
16632
+ };
16633
+ }
16247
16634
  function parseExpected(input, path31) {
16248
16635
  if (!isRecord3(input)) {
16249
16636
  throw new Error(`${path31} must be an object`);
@@ -16256,9 +16643,11 @@ function parseExpected(input, path31) {
16256
16643
  const expectedOutcomeRaw = input.expectedOutcome;
16257
16644
  const recoveryExpectationRaw = input.recoveryExpectation;
16258
16645
  const gradedEvidenceRaw = input.gradedEvidence;
16646
+ const graphNeighborRaw = input.graphNeighbor;
16259
16647
  const filePath = parseStringOrUndefined(filePathRaw, `${path31}.filePath`);
16260
16648
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
16261
16649
  const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path31}.gradedEvidence`);
16650
+ const graphNeighbor = parseExpectedGraphNeighbor(graphNeighborRaw, `${path31}.graphNeighbor`);
16262
16651
  const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path31}.expectedOutcome`);
16263
16652
  if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
16264
16653
  throw new Error(
@@ -16287,7 +16676,8 @@ function parseExpected(input, path31) {
16287
16676
  expectedRoute,
16288
16677
  expectedOutcome,
16289
16678
  recoveryExpectation,
16290
- ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
16679
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {},
16680
+ ...graphNeighbor !== void 0 ? { graphNeighbor } : {}
16291
16681
  };
16292
16682
  }
16293
16683
  function parseQueryLanguage(value, path31) {
@@ -16422,6 +16812,21 @@ function parseBudget(raw, sourceLabel) {
16422
16812
  "minRawDistinctTop3Ratio",
16423
16813
  sourceLabel
16424
16814
  ),
16815
+ minGraphNeighborRecall: parseThresholdValue(
16816
+ thresholds.minGraphNeighborRecall,
16817
+ "minGraphNeighborRecall",
16818
+ sourceLabel
16819
+ ),
16820
+ minRouteAccuracy: parseThresholdValue(
16821
+ thresholds.minRouteAccuracy,
16822
+ "minRouteAccuracy",
16823
+ sourceLabel
16824
+ ),
16825
+ minOutcomeAccuracy: parseThresholdValue(
16826
+ thresholds.minOutcomeAccuracy,
16827
+ "minOutcomeAccuracy",
16828
+ sourceLabel
16829
+ ),
16425
16830
  maxContextResponseTokensAverage: parseThresholdValue(
16426
16831
  thresholds.maxContextResponseTokensAverage,
16427
16832
  "maxContextResponseTokensAverage",
@@ -16486,6 +16891,120 @@ function buildDatasetFingerprint(dataset) {
16486
16891
  const canonical = JSON.stringify(normalizeForFingerprint(dataset));
16487
16892
  return crypto2.createHash("sha256").update(canonical).digest("hex");
16488
16893
  }
16894
+ function normalizedPath2(value) {
16895
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
16896
+ }
16897
+ function pathsMatch2(left, right) {
16898
+ const normalizedLeft = normalizedPath2(left);
16899
+ const normalizedRight = normalizedPath2(right);
16900
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
16901
+ }
16902
+ function toEvalSearchResult(result) {
16903
+ return {
16904
+ filePath: result.filePath,
16905
+ startLine: result.startLine,
16906
+ endLine: result.endLine,
16907
+ score: result.score,
16908
+ chunkType: result.chunkType,
16909
+ name: result.name
16910
+ };
16911
+ }
16912
+ function selectResolvedTarget(definitions, resolution) {
16913
+ if (resolution?.status !== "resolved") return definitions[0];
16914
+ 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];
16915
+ }
16916
+ function callerResult(edge) {
16917
+ if (!edge.fromSymbolFilePath) return void 0;
16918
+ return {
16919
+ filePath: edge.fromSymbolFilePath,
16920
+ startLine: edge.line,
16921
+ endLine: edge.line,
16922
+ score: 0,
16923
+ chunkType: "graph-caller",
16924
+ name: edge.fromSymbolName,
16925
+ graphDirection: "caller"
16926
+ };
16927
+ }
16928
+ function calleeResult(edge, symbols) {
16929
+ 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;
16930
+ if (!symbol) return void 0;
16931
+ return {
16932
+ filePath: symbol.filePath,
16933
+ startLine: symbol.startLine,
16934
+ endLine: symbol.endLine,
16935
+ score: 0,
16936
+ chunkType: "graph-callee",
16937
+ name: symbol.name,
16938
+ graphDirection: "callee"
16939
+ };
16940
+ }
16941
+ async function runEditContextQuery(indexer, projectRoot, query) {
16942
+ let definitions = [];
16943
+ let conceptual = [];
16944
+ let callers;
16945
+ let callees;
16946
+ const editContext = await resolveCodebaseEditContextWithDependencies({
16947
+ query: query.query,
16948
+ symbol: query.args?.symbol,
16949
+ filePath: query.args?.filePath ?? query.expected.filePath,
16950
+ callerLimit: query.args?.callerLimit,
16951
+ calleeLimit: query.args?.calleeLimit,
16952
+ tokenBudget: query.args?.tokenBudget
16953
+ }, {
16954
+ searchCodebase: async (searchQuery, options) => {
16955
+ conceptual = await indexer.search(searchQuery, options?.limit, {
16956
+ filterByBranch: !!query.expected.branch
16957
+ });
16958
+ return conceptual;
16959
+ },
16960
+ implementationLookup: async (symbol, options) => {
16961
+ definitions = await indexer.search(symbol, options?.limit, {
16962
+ filterByBranch: !!query.expected.branch,
16963
+ definitionIntent: true
16964
+ });
16965
+ return definitions;
16966
+ },
16967
+ getCallGraphData: async (params) => {
16968
+ const result = await getCallGraphDataForIndexer(indexer, projectRoot, params);
16969
+ if (params.direction === "callers") callers = result;
16970
+ else callees = result;
16971
+ return result;
16972
+ }
16973
+ });
16974
+ const resolution = callers?.resolution;
16975
+ const target = selectResolvedTarget(definitions, resolution);
16976
+ const targetCandidates = target ? [target] : [...definitions, ...conceptual];
16977
+ const results = targetCandidates.filter((candidate) => (resolution?.status !== "resolved" || editContext.details.sourceIncluded) && editContext.text.includes(
16978
+ `${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
16979
+ )).map(toEvalSearchResult);
16980
+ if (query.expected.graphNeighbor) {
16981
+ const symbols = await indexer.getCallGraphSymbols();
16982
+ const callerLimit = query.args?.callerLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
16983
+ const calleeLimit = query.args?.calleeLimit ?? DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
16984
+ const publishedCallers = (callers?.callers ?? []).slice(0, callerLimit).filter((edge) => editContext.text.includes(
16985
+ `${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
16986
+ ));
16987
+ const publishedCallees = (callees?.callees ?? []).slice(0, calleeLimit).filter((edge) => resolution?.status === "resolved" && editContext.text.includes(
16988
+ `${edge.targetName} from ${resolution.filePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`
16989
+ ));
16990
+ results.push(
16991
+ ...publishedCallers.map(callerResult).filter((item) => item !== void 0),
16992
+ ...publishedCallees.map((edge) => calleeResult(edge, symbols)).filter((item) => item !== void 0)
16993
+ );
16994
+ }
16995
+ return {
16996
+ results,
16997
+ resolvedRoute: resolution?.status === "resolved" ? "definition" : "search",
16998
+ routedQuery: query.args?.symbol ?? query.query,
16999
+ context: {
17000
+ tokenBudget: editContext.details.tokenBudget,
17001
+ responseTokens: editContext.details.tokenEstimate,
17002
+ candidateCount: results.length,
17003
+ deduplicatedCount: results.length,
17004
+ omittedCount: 0
17005
+ }
17006
+ };
17007
+ }
16489
17008
  async function runEvaluation(options) {
16490
17009
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
16491
17010
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -16517,6 +17036,7 @@ async function runEvaluation(options) {
16517
17036
  );
16518
17037
  }
16519
17038
  const start = performance3.now();
17039
+ const editContextResult = query.retrievalMode === "edit-context" ? await runEditContextQuery(indexer, options.projectRoot, query) : void 0;
16520
17040
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
16521
17041
  query: query.query,
16522
17042
  symbol: query.args?.symbol,
@@ -16540,31 +17060,32 @@ async function runEvaluation(options) {
16540
17060
  directory: scope.directory
16541
17061
  })
16542
17062
  }) : void 0;
16543
- const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
17063
+ const result = editContextResult?.results ?? contextResult?.details?.results ?? await indexer.search(query.query, 10, {
16544
17064
  metadataOnly: true,
16545
17065
  filterByBranch: !!query.expected.branch,
16546
17066
  fileType: query.args?.fileType,
16547
17067
  directory: query.args?.directory
16548
17068
  });
16549
17069
  const elapsed = performance3.now() - start;
16550
- const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
16551
- const routedQuery = contextResult?.details?.routedQuery ?? query.query;
17070
+ const resolvedRoute = editContextResult?.resolvedRoute ?? (contextResult?.details?.route === "definition" ? "definition" : "search");
17071
+ const routedQuery = editContextResult?.routedQuery ?? contextResult?.details?.routedQuery ?? query.query;
16552
17072
  const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
16553
17073
  const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
16554
17074
  const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
16555
17075
  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 ? {
17076
+ const materialized = result.map((item) => {
17077
+ const graphDirection = "graphDirection" in item && (item.graphDirection === "caller" || item.graphDirection === "callee") ? item.graphDirection : void 0;
17078
+ return {
17079
+ filePath: item.filePath,
17080
+ startLine: item.startLine,
17081
+ endLine: item.endLine,
17082
+ score: item.score,
17083
+ chunkType: item.chunkType,
17084
+ name: item.name,
17085
+ graphDirection
17086
+ };
17087
+ });
17088
+ const contextMeasurement = editContextResult?.context ?? (contextResult?.details ? {
16568
17089
  tokenBudget: contextResult.details.tokenBudget,
16569
17090
  responseTokens: contextResult.details.tokenEstimate,
16570
17091
  candidateCount: contextResult.details.candidateCount ?? 0,
@@ -16572,7 +17093,11 @@ async function runEvaluation(options) {
16572
17093
  omittedCount: contextResult.details.omittedCount ?? 0,
16573
17094
  recoveryUsed,
16574
17095
  recoveryRelaxed
16575
- } : void 0));
17096
+ } : void 0);
17097
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10, {
17098
+ resolvedRoute,
17099
+ routedQuery
17100
+ }, contextMeasurement));
16576
17101
  }
16577
17102
  const logger = indexer.getLogger();
16578
17103
  const metricSnapshot = logger.getMetrics();
@@ -17108,7 +17633,11 @@ import { z as z2 } from "zod";
17108
17633
 
17109
17634
  // src/tools/execute-common.ts
17110
17635
  async function executeCodebaseContext(projectRoot, host, args) {
17111
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17636
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17637
+ return { text: result.text, details: result.details };
17638
+ }
17639
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17640
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17112
17641
  }
17113
17642
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17114
17643
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17224,6 +17753,7 @@ function formatPrImpact(result) {
17224
17753
  // src/tools/tool-names.ts
17225
17754
  var TOOL_NAME = {
17226
17755
  CODEBASE_CONTEXT: "codebase_context",
17756
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
17227
17757
  CODEBASE_SEARCH: "codebase_search",
17228
17758
  CODEBASE_PEEK: "codebase_peek",
17229
17759
  FIND_SIMILAR: "find_similar",
@@ -17247,6 +17777,7 @@ var TOOL_NAME = {
17247
17777
  };
17248
17778
  var PORTABLE_TOOL_NAMES = [
17249
17779
  TOOL_NAME.CODEBASE_CONTEXT,
17780
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17250
17781
  TOOL_NAME.CODEBASE_SEARCH,
17251
17782
  TOOL_NAME.CODEBASE_PEEK,
17252
17783
  TOOL_NAME.INDEX_CODEBASE,
@@ -17263,6 +17794,7 @@ var PORTABLE_TOOL_NAMES = [
17263
17794
  ];
17264
17795
  var OPENCODE_TOOL_NAMES = [
17265
17796
  TOOL_NAME.CODEBASE_CONTEXT,
17797
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17266
17798
  TOOL_NAME.CODEBASE_SEARCH,
17267
17799
  TOOL_NAME.CODEBASE_PEEK,
17268
17800
  TOOL_NAME.INDEX_CODEBASE,
@@ -17283,6 +17815,7 @@ var OPENCODE_TOOL_NAMES = [
17283
17815
  ];
17284
17816
  var PI_TOOL_NAMES = [
17285
17817
  TOOL_NAME.CODEBASE_CONTEXT,
17818
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17286
17819
  TOOL_NAME.CODEBASE_SEARCH,
17287
17820
  TOOL_NAME.CODEBASE_PEEK,
17288
17821
  TOOL_NAME.FIND_SIMILAR,
@@ -17326,10 +17859,30 @@ function registerMcpTools(server, runtime) {
17326
17859
  directory: allowNullAsUndefined(z2.string().optional()).describe("Filter by directory path (e.g., 'src/utils', 'lib')"),
17327
17860
  tokenBudget: allowNullAsUndefined(
17328
17861
  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})`)
17862
+ ).describe(`Maximum response tokens for this context pack (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
17863
+ diagnostic: z2.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
17330
17864
  },
17331
17865
  async (args) => {
17332
17866
  const result = await executeCodebaseContext(runtime.projectRoot, runtime.host, args);
17867
+ return {
17868
+ content: [{ type: "text", text: result.text }],
17869
+ ...args.diagnostic ? { structuredContent: result.details } : {}
17870
+ };
17871
+ }
17872
+ );
17873
+ server.tool(
17874
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
17875
+ "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.",
17876
+ {
17877
+ query: z2.string().describe("The requested change or target behavior."),
17878
+ symbol: allowNullAsUndefined(z2.string().optional()).describe("Authoritative target symbol when known."),
17879
+ filePath: allowNullAsUndefined(z2.string().optional()).describe("Optional file path used to disambiguate duplicate symbol names."),
17880
+ 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)),
17881
+ 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)),
17882
+ tokenBudget: allowNullAsUndefined(z2.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET))
17883
+ },
17884
+ async (args) => {
17885
+ const result = await executeCodebaseEditContext(runtime.projectRoot, runtime.host, args);
17333
17886
  return { content: [{ type: "text", text: result.text }] };
17334
17887
  }
17335
17888
  );
@@ -19592,6 +20145,8 @@ var GitHeadWatcher = class {
19592
20145
  debounceTimer = null;
19593
20146
  debounceMs = 100;
19594
20147
  // Short debounce for git operations
20148
+ readyPromise = Promise.resolve();
20149
+ resolveReady = null;
19595
20150
  constructor(projectRoot) {
19596
20151
  this.projectRoot = projectRoot;
19597
20152
  }
@@ -19600,8 +20155,12 @@ var GitHeadWatcher = class {
19600
20155
  return;
19601
20156
  }
19602
20157
  if (!isGitRepo(this.projectRoot)) {
20158
+ this.readyPromise = Promise.resolve();
19603
20159
  return;
19604
20160
  }
20161
+ this.readyPromise = new Promise((resolve18) => {
20162
+ this.resolveReady = resolve18;
20163
+ });
19605
20164
  this.onBranchChange = handler;
19606
20165
  this.currentBranch = getCurrentBranch(this.projectRoot);
19607
20166
  const headPath = getHeadPath(this.projectRoot);
@@ -19616,6 +20175,10 @@ var GitHeadWatcher = class {
19616
20175
  });
19617
20176
  this.watcher.on("change", () => this.handleHeadChange());
19618
20177
  this.watcher.on("add", () => this.handleHeadChange());
20178
+ this.watcher.once("ready", () => {
20179
+ this.resolveReady?.();
20180
+ this.resolveReady = null;
20181
+ });
19619
20182
  }
19620
20183
  handleHeadChange() {
19621
20184
  if (this.debounceTimer) {
@@ -19653,10 +20216,16 @@ var GitHeadWatcher = class {
19653
20216
  await watcher.close();
19654
20217
  }
19655
20218
  this.onBranchChange = null;
20219
+ this.resolveReady?.();
20220
+ this.resolveReady = null;
20221
+ this.readyPromise = Promise.resolve();
19656
20222
  }
19657
20223
  isRunning() {
19658
20224
  return this.watcher !== null;
19659
20225
  }
20226
+ async waitUntilReady() {
20227
+ await this.readyPromise;
20228
+ }
19660
20229
  };
19661
20230
 
19662
20231
  // src/watcher/index.ts
@@ -19706,7 +20275,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host, options
19706
20275
  fileWatcher,
19707
20276
  gitWatcher,
19708
20277
  whenReady() {
19709
- return fileWatcher.waitUntilReady();
20278
+ return Promise.all([
20279
+ fileWatcher.waitUntilReady(),
20280
+ gitWatcher?.waitUntilReady()
20281
+ ]).then(() => void 0);
19710
20282
  },
19711
20283
  async stop() {
19712
20284
  stopped = true;
@@ -19767,9 +20339,9 @@ function parseGitActivity(output) {
19767
20339
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
19768
20340
  const churn = Number(addedRaw) + Number(deletedRaw);
19769
20341
  if (!Number.isFinite(churn) || churn <= 0) continue;
19770
- const normalizedPath = normalizePath4(filePath);
19771
- const previous = activity.get(normalizedPath);
19772
- activity.set(normalizedPath, {
20342
+ const normalizedPath3 = normalizePath4(filePath);
20343
+ const previous = activity.get(normalizedPath3);
20344
+ activity.set(normalizedPath3, {
19773
20345
  churn: (previous?.churn ?? 0) + churn,
19774
20346
  commits: (previous?.commits ?? 0) + 1,
19775
20347
  latestDate: previous?.latestDate ?? latestDate,
@@ -20359,8 +20931,8 @@ function transformForVisualization(symbols, edges, options = {}) {
20359
20931
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
20360
20932
  filteredSymbols = symbols.filter(
20361
20933
  (s) => {
20362
- const normalizedPath = s.filePath.replace(/\\/g, "/");
20363
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
20934
+ const normalizedPath3 = s.filePath.replace(/\\/g, "/");
20935
+ return normalizedPath3 === normalizedDir || normalizedPath3.startsWith(normalizedDirWithSlash) || normalizedPath3.endsWith(`/${normalizedDir}`) || normalizedPath3.includes(normalizedAbsoluteSuffix);
20364
20936
  }
20365
20937
  );
20366
20938
  }