opencode-codebase-index 0.22.3 → 0.22.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2053,6 +2053,9 @@ function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot3) {
2053
2053
  }
2054
2054
 
2055
2055
  // src/tools/contracts.ts
2056
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
2057
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
2058
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
2056
2059
  var CODE_COMMUNITIES_MIN_SIZE = 1;
2057
2060
  var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
2058
2061
  var CODE_COMMUNITIES_MAX_LIMIT = 100;
@@ -2676,6 +2679,16 @@ function compactEvidenceValue(value, maxChars) {
2676
2679
  }
2677
2680
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
2678
2681
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
2682
+ function toContextPackTraceCandidate(result) {
2683
+ return {
2684
+ filePath: result.filePath,
2685
+ startLine: result.startLine,
2686
+ endLine: result.endLine,
2687
+ score: result.score,
2688
+ chunkType: result.chunkType,
2689
+ name: result.name
2690
+ };
2691
+ }
2679
2692
  function formatExactSearchHandoff(results) {
2680
2693
  const suggestedNames = [];
2681
2694
  const seen = /* @__PURE__ */ new Set();
@@ -2724,13 +2737,13 @@ function buildContextPack(results, options = {}) {
2724
2737
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
2725
2738
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
2726
2739
  const candidateCount = results.length;
2727
- const deduplicated = deduplicateContextCandidates(
2728
- rankContextCandidates(
2729
- results,
2730
- options.preferImplementationPaths ?? false
2731
- )
2732
- );
2733
- const diversified = diversifyContextCandidates(deduplicated);
2740
+ const preserveInputOrder = options.preserveInputOrder ?? false;
2741
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
2742
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
2743
+ const deduplicated = deduplicateContextCandidates(ranked);
2744
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
2745
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
2746
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
2734
2747
  const duplicateCount = candidateCount - deduplicated.length;
2735
2748
  const selectable = diversified.slice(0, maxResults);
2736
2749
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -2763,6 +2776,15 @@ function buildContextPack(results, options = {}) {
2763
2776
  const fitted = fitTextToContextBudget(text3, tokenBudget);
2764
2777
  const budgetOmittedCount = selectable.length - selected.length;
2765
2778
  const omittedCount = candidateCount - selected.length;
2779
+ if (options.trace) {
2780
+ options.trace({
2781
+ inputCandidates: results.map(toContextPackTraceCandidate),
2782
+ rankedCandidates,
2783
+ deduplicatedCandidates,
2784
+ diversifiedCandidates,
2785
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
2786
+ });
2787
+ }
2766
2788
  return {
2767
2789
  requestedTokenBudget,
2768
2790
  tokenBudget,
@@ -8578,12 +8600,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
8578
8600
  return [...primary, ...remainder];
8579
8601
  }
8580
8602
  function buildDiversityKey(metadata) {
8581
- const normalizedPath = metadata.filePath.toLowerCase();
8603
+ const normalizedPath2 = metadata.filePath.toLowerCase();
8582
8604
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
8583
8605
  if (normalizedName.length > 0) {
8584
- return `${normalizedPath}#${normalizedName}`;
8606
+ return `${normalizedPath2}#${normalizedName}`;
8585
8607
  }
8586
- return normalizedPath;
8608
+ return normalizedPath2;
8587
8609
  }
8588
8610
  function rankHybridResults(query, semanticResults, keywordResults, options) {
8589
8611
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -8989,6 +9011,29 @@ function extractPrimaryIdentifierQueryHint(query) {
8989
9011
  const best = codeTerms.find((term) => term.length >= 6);
8990
9012
  return best ?? null;
8991
9013
  }
9014
+ function pathSegmentsForAffinityMatch(filePath) {
9015
+ const normalizedPath2 = normalizeRankingText(filePath).replace(/\\/g, "/");
9016
+ const segments = normalizedPath2.split("/").filter((segment) => segment.length > 0);
9017
+ if (segments.length === 0) {
9018
+ return [];
9019
+ }
9020
+ const basename5 = segments[segments.length - 1] ?? "";
9021
+ const basenameWithoutExt = basename5.replace(/\.[^/.]+$/u, "");
9022
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
9023
+ return Array.from(/* @__PURE__ */ new Set([
9024
+ ...normalizedSegments,
9025
+ basenameWithoutExt.toLowerCase()
9026
+ ]));
9027
+ }
9028
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
9029
+ const haystack = pathSegmentsForAffinityMatch(filePath);
9030
+ return exactIdentifierVariants.some((variant) => {
9031
+ if (!variant || variant.length < 2) {
9032
+ return false;
9033
+ }
9034
+ return haystack.includes(variant);
9035
+ });
9036
+ }
8992
9037
  var FILE_PATH_HINT_EXTENSIONS = [
8993
9038
  "ts",
8994
9039
  "tsx",
@@ -9032,9 +9077,9 @@ function normalizeFilePathForHintMatch(filePath) {
9032
9077
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9033
9078
  }
9034
9079
  function pathMatchesHint(filePath, hint) {
9035
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9080
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9036
9081
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9037
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9082
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9038
9083
  }
9039
9084
  function extractFilePathHint(query) {
9040
9085
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -9064,10 +9109,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9064
9109
  ).map((candidate) => {
9065
9110
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
9066
9111
  const pathLower = candidate.metadata.filePath.toLowerCase();
9067
- let maxMatch = 0;
9068
- const nameMatchesPrimary = primaryVariants.some(
9112
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
9113
+ const exactMatch = exactIdentifierVariants.some(
9069
9114
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
9070
9115
  );
9116
+ let maxMatch = 0;
9117
+ const nameMatchesPrimary = exactMatch;
9118
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
9071
9119
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
9072
9120
  for (const hint of hints) {
9073
9121
  const variants = normalizeIdentifierVariants(hint);
@@ -9088,12 +9136,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9088
9136
  candidate,
9089
9137
  maxMatch,
9090
9138
  pathMatchesFileHint,
9091
- nameMatchesPrimary
9139
+ nameMatchesPrimary,
9140
+ pathAffinity
9092
9141
  };
9093
9142
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
9094
9143
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
9095
9144
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
9096
9145
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
9146
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
9147
+ return b.nameMatchesPrimary ? 1 : -1;
9148
+ }
9149
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
9097
9150
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
9098
9151
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
9099
9152
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -12907,6 +12960,20 @@ var Indexer = class _Indexer {
12907
12960
  requestedLimit = nextLimit;
12908
12961
  }
12909
12962
  }
12963
+ buildCandidateSnapshot(candidate) {
12964
+ return {
12965
+ id: candidate.id,
12966
+ filePath: candidate.metadata.filePath,
12967
+ startLine: candidate.metadata.startLine,
12968
+ endLine: candidate.metadata.endLine,
12969
+ score: candidate.score,
12970
+ chunkType: candidate.metadata.chunkType,
12971
+ name: candidate.metadata.name
12972
+ };
12973
+ }
12974
+ buildCandidateSnapshotList(candidates) {
12975
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12976
+ }
12910
12977
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12911
12978
  return this.searchCandidatesWithBranchPrefilter(
12912
12979
  initialLimit,
@@ -13098,6 +13165,16 @@ var Indexer = class _Indexer {
13098
13165
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13099
13166
  fusionMs: Math.round(fusionMs * 100) / 100
13100
13167
  });
13168
+ if (options?.trace) {
13169
+ options.trace({
13170
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13171
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13172
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13173
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13174
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13175
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13176
+ });
13177
+ }
13101
13178
  const metadataOnly = options?.metadataOnly ?? false;
13102
13179
  return Promise.all(
13103
13180
  finalResults.map(async (r) => {
@@ -14358,7 +14435,8 @@ async function searchCodebase(projectRoot3, host, query, options = {}) {
14358
14435
  definitionIntent: options.definitionIntent,
14359
14436
  blameAuthor: options.blameAuthor,
14360
14437
  blameSha: options.blameSha,
14361
- blameSince: options.blameSince
14438
+ blameSince: options.blameSince,
14439
+ trace: options.trace
14362
14440
  });
14363
14441
  }
14364
14442
  async function searchCodebaseWithEffectiveness(projectRoot3, host, route, query, options, render) {
@@ -14412,15 +14490,19 @@ async function implementationLookup(projectRoot3, host, query, options = {}) {
14412
14490
  return indexer.search(query, options.limit, {
14413
14491
  fileType: options.fileType,
14414
14492
  directory: options.directory,
14415
- definitionIntent: true
14493
+ definitionIntent: true,
14494
+ trace: options.trace
14416
14495
  });
14417
14496
  }
14418
14497
  async function getCallGraphData(projectRoot3, host, params) {
14419
14498
  await ensureAutoIndexReadyForRetrieval(projectRoot3, host);
14420
14499
  const root = getProjectRoot(projectRoot3, host);
14421
14500
  const indexer = getIndexerForProject(root, host);
14501
+ return getCallGraphDataForIndexer(indexer, root, params);
14502
+ }
14503
+ async function getCallGraphDataForIndexer(indexer, projectRoot3, params) {
14422
14504
  const symbols = await indexer.getCallGraphSymbols();
14423
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14505
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot3, params.name, params.filePath, params.symbolId);
14424
14506
  const direction = params.direction === "callees" ? "callees" : "callers";
14425
14507
  if (resolution.status !== "resolved") {
14426
14508
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14648,17 +14730,17 @@ async function getIndexLogs(projectRoot3, host, args) {
14648
14730
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14649
14731
  const root = getProjectRoot(projectRoot3, host);
14650
14732
  const inputPath = knowledgeBasePath.trim();
14651
- const normalizedPath = path20.resolve(
14733
+ const normalizedPath2 = path20.resolve(
14652
14734
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14653
14735
  );
14654
- if (!(0, import_fs13.existsSync)(normalizedPath)) {
14655
- return `Error: Directory does not exist: ${normalizedPath}`;
14736
+ if (!(0, import_fs13.existsSync)(normalizedPath2)) {
14737
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14656
14738
  }
14657
14739
  let realPath;
14658
14740
  try {
14659
- realPath = (0, import_fs13.realpathSync)(normalizedPath);
14741
+ realPath = (0, import_fs13.realpathSync)(normalizedPath2);
14660
14742
  } catch {
14661
- return `Error: Cannot resolve path: ${normalizedPath}`;
14743
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14662
14744
  }
14663
14745
  const blockedPrefixes = [
14664
14746
  "/etc",
@@ -14681,34 +14763,34 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14681
14763
  ];
14682
14764
  for (const prefix of blockedPrefixes) {
14683
14765
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14684
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14766
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14685
14767
  }
14686
14768
  }
14687
14769
  for (const dotDir of sensitiveDotDirs) {
14688
14770
  const sensitiveDir = path20.join(homeDir, dotDir);
14689
14771
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14690
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14772
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14691
14773
  }
14692
14774
  }
14693
14775
  try {
14694
- const stat = (0, import_fs13.statSync)(normalizedPath);
14776
+ const stat = (0, import_fs13.statSync)(normalizedPath2);
14695
14777
  if (!stat.isDirectory()) {
14696
- return `Error: Path is not a directory: ${normalizedPath}`;
14778
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14697
14779
  }
14698
14780
  } catch (error) {
14699
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14781
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14700
14782
  }
14701
14783
  const config = loadEditableConfig(root, host);
14702
14784
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14703
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14785
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14704
14786
  if (alreadyExists) {
14705
- return `Knowledge base already configured: ${normalizedPath}`;
14787
+ return `Knowledge base already configured: ${normalizedPath2}`;
14706
14788
  }
14707
- knowledgeBases.push(normalizedPath);
14789
+ knowledgeBases.push(normalizedPath2);
14708
14790
  config.knowledgeBases = knowledgeBases;
14709
14791
  saveConfig(root, config, host);
14710
14792
  refreshIndexerForDirectory(root, host);
14711
- let result = `${normalizedPath}
14793
+ let result = `${normalizedPath2}
14712
14794
  `;
14713
14795
  result += `Total knowledge bases: ${knowledgeBases.length}
14714
14796
  `;
@@ -14883,6 +14965,27 @@ function buildRecoveryDetails(attempts, successIndex) {
14883
14965
  successfulAttemptIndex: successIndex
14884
14966
  };
14885
14967
  }
14968
+ function serializeAttempts(attempts) {
14969
+ return attempts.map((attempt) => ({
14970
+ kind: attempt.kind,
14971
+ scope: attempt.scope,
14972
+ resultCount: attempt.resultCount,
14973
+ relaxedFields: attempt.relaxedFields
14974
+ }));
14975
+ }
14976
+ function buildSearchDiagnostic(attempt) {
14977
+ if (!attempt) {
14978
+ return void 0;
14979
+ }
14980
+ return {
14981
+ route: attempt.kind,
14982
+ routedQuery: attempt.query,
14983
+ searchQuery: attempt.query,
14984
+ searchScope: attempt.scopeFilter,
14985
+ searchTrace: attempt.searchTrace,
14986
+ contextPackTrace: attempt.contextPackTrace
14987
+ };
14988
+ }
14886
14989
  function trimOrUndefined2(value) {
14887
14990
  const normalized = value?.trim();
14888
14991
  if (!normalized) {
@@ -14922,6 +15025,7 @@ async function resolveSearchContext(input, operations) {
14922
15025
  const hasFilters = Boolean(fileType || directory);
14923
15026
  const relaxedFields = relaxedHintFields(fileType, directory);
14924
15027
  const attempts = [];
15028
+ const attemptStates = [];
14925
15029
  const decisions = {
14926
15030
  inferredDefinitionMiss: false,
14927
15031
  fallbackFromOriginalConceptualToInferred: false,
@@ -14950,8 +15054,20 @@ async function resolveSearchContext(input, operations) {
14950
15054
  if (seenAttempts.has(key)) {
14951
15055
  return [];
14952
15056
  }
14953
- const results = await runAttempt();
15057
+ const attemptState = {
15058
+ kind,
15059
+ scope: describeScope(scope.fileType, scope.directory),
15060
+ resultCount: 0,
15061
+ relaxedFields: [...relaxedFieldsForAttempt],
15062
+ query: attemptQuery,
15063
+ scopeFilter: scope
15064
+ };
15065
+ const results = await runAttempt((trace) => {
15066
+ attemptState.searchTrace = trace;
15067
+ });
15068
+ attemptState.resultCount = results.length;
14954
15069
  seenAttempts.add(key);
15070
+ attemptStates.push(attemptState);
14955
15071
  attempts.push({
14956
15072
  kind,
14957
15073
  scope: describeScope(scope.fileType, scope.directory),
@@ -14971,7 +15087,7 @@ async function resolveSearchContext(input, operations) {
14971
15087
  symbol,
14972
15088
  scope,
14973
15089
  relaxedFieldsForAttempt,
14974
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15090
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14975
15091
  );
14976
15092
  };
14977
15093
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -14980,13 +15096,23 @@ async function resolveSearchContext(input, operations) {
14980
15096
  searchQuery,
14981
15097
  scope,
14982
15098
  relaxedFieldsForAttempt,
14983
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15099
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14984
15100
  );
14985
15101
  };
14986
- const toResult = (route, routedQuery, pack) => {
15102
+ const findSuccessfulAttemptState = (route) => {
15103
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15104
+ const attempt = attemptStates[index];
15105
+ if (attempt.kind === route && attempt.resultCount > 0) {
15106
+ return attempt;
15107
+ }
15108
+ }
15109
+ return void 0;
15110
+ };
15111
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
14987
15112
  const base = packedResult(route, routedQuery, pack);
14988
15113
  const baseDetails = base.details;
14989
15114
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15115
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
14990
15116
  return {
14991
15117
  text: base.text,
14992
15118
  details: {
@@ -14994,7 +15120,10 @@ async function resolveSearchContext(input, operations) {
14994
15120
  tokenBudget: baseDetails.tokenBudget,
14995
15121
  tokenEstimate: baseDetails.tokenEstimate,
14996
15122
  truncated: false,
14997
- recovery: buildRecoveryDetails(attempts, successIndex)
15123
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15124
+ ...input.diagnostic && {
15125
+ diagnostic: buildSearchDiagnostic(successState)
15126
+ }
14998
15127
  }
14999
15128
  };
15000
15129
  };
@@ -15008,8 +15137,18 @@ async function resolveSearchContext(input, operations) {
15008
15137
  buildContextPack(scopedDefinitionResults, {
15009
15138
  tokenBudget,
15010
15139
  maxResults: limit,
15011
- heading
15012
- })
15140
+ heading,
15141
+ preserveInputOrder: true,
15142
+ ...input.diagnostic ? {
15143
+ trace: (trace) => {
15144
+ const attemptState = findSuccessfulAttemptState("definition");
15145
+ if (attemptState) {
15146
+ attemptState.contextPackTrace = trace;
15147
+ }
15148
+ }
15149
+ } : void 0
15150
+ }),
15151
+ findSuccessfulAttemptState("definition")
15013
15152
  );
15014
15153
  }
15015
15154
  if (explicitSymbol) {
@@ -15027,8 +15166,18 @@ async function resolveSearchContext(input, operations) {
15027
15166
  buildContextPack(unscopedDefinitionResults, {
15028
15167
  tokenBudget,
15029
15168
  maxResults: limit,
15030
- heading: heading2
15031
- })
15169
+ heading: heading2,
15170
+ preserveInputOrder: true,
15171
+ ...input.diagnostic ? {
15172
+ trace: (trace) => {
15173
+ const attemptState = findSuccessfulAttemptState("definition");
15174
+ if (attemptState) {
15175
+ attemptState.contextPackTrace = trace;
15176
+ }
15177
+ }
15178
+ } : void 0
15179
+ }),
15180
+ findSuccessfulAttemptState("definition")
15032
15181
  );
15033
15182
  }
15034
15183
  }
@@ -15047,7 +15196,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15047
15196
  tokenBudget: heading.tokenBudget,
15048
15197
  tokenEstimate: heading.tokenEstimate,
15049
15198
  truncated: heading.truncated,
15050
- recovery: buildRecoveryDetails(attempts, null)
15199
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15200
+ ...input.diagnostic && {
15201
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15202
+ }
15051
15203
  }
15052
15204
  };
15053
15205
  }
@@ -15087,8 +15239,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15087
15239
  maxResults: limit,
15088
15240
  heading,
15089
15241
  includeExactSearchHandoff: true,
15090
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15091
- })
15242
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15243
+ ...input.diagnostic ? {
15244
+ trace: (trace) => {
15245
+ const attemptState = findSuccessfulAttemptState("conceptual");
15246
+ if (attemptState) {
15247
+ attemptState.contextPackTrace = trace;
15248
+ }
15249
+ }
15250
+ } : void 0
15251
+ }),
15252
+ findSuccessfulAttemptState("conceptual")
15092
15253
  );
15093
15254
  }
15094
15255
  }
@@ -15104,7 +15265,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15104
15265
  tokenBudget: fallbackText.tokenBudget,
15105
15266
  tokenEstimate: fallbackText.tokenEstimate,
15106
15267
  truncated: fallbackText.truncated,
15107
- recovery: buildRecoveryDetails(attempts, null)
15268
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15269
+ ...input.diagnostic && {
15270
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15271
+ }
15108
15272
  }
15109
15273
  };
15110
15274
  }
@@ -15185,17 +15349,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot3, host, input) {
15185
15349
  details: fittedDetails("path", fitted, 0)
15186
15350
  };
15187
15351
  }
15188
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15189
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot3, host, lookupSymbol, {
15352
+ return resolveSearchContext({
15353
+ query: input.query,
15354
+ symbol,
15355
+ limit,
15356
+ tokenBudget,
15357
+ fileType,
15358
+ directory,
15359
+ diagnostic: input.diagnostic
15360
+ }, {
15361
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot3, host, lookupSymbol, {
15190
15362
  limit: retrievalLimit,
15191
15363
  fileType: scope.fileType,
15192
- directory: scope.directory
15364
+ directory: scope.directory,
15365
+ trace
15193
15366
  }),
15194
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot3, host, queryText, {
15367
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot3, host, queryText, {
15195
15368
  limit: retrievalLimit,
15196
15369
  fileType: scope.fileType,
15197
15370
  directory: scope.directory,
15198
- metadataOnly: true
15371
+ metadataOnly: true,
15372
+ trace
15199
15373
  })
15200
15374
  });
15201
15375
  }
@@ -15260,12 +15434,188 @@ async function resolveCodebaseContext(projectRoot3, host, input) {
15260
15434
  }
15261
15435
  }
15262
15436
 
15437
+ // src/tools/edit-context.ts
15438
+ function edgeLimit(value) {
15439
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15440
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15441
+ }
15442
+ return Math.min(
15443
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15444
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15445
+ );
15446
+ }
15447
+ function normalizedPath(value) {
15448
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15449
+ }
15450
+ function pathsMatch(left, right) {
15451
+ const normalizedLeft = normalizedPath(left);
15452
+ const normalizedRight = normalizedPath(right);
15453
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15454
+ }
15455
+ function targetSource(results, resolution) {
15456
+ 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);
15457
+ }
15458
+ function formatSource(result) {
15459
+ const name = result.name ? ` ${result.name}` : "";
15460
+ return [
15461
+ "## Target implementation",
15462
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15463
+ "```",
15464
+ result.content,
15465
+ "```"
15466
+ ].join("\n");
15467
+ }
15468
+ function formatCallers(edges) {
15469
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15470
+ return [
15471
+ "## Direct callers",
15472
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15473
+ ].join("\n");
15474
+ }
15475
+ function formatCallees(edges, sourceFilePath) {
15476
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15477
+ return [
15478
+ "## Direct callees",
15479
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15480
+ ].join("\n");
15481
+ }
15482
+ function formatResolutionRisk(resolution) {
15483
+ if (resolution.status === "ambiguous") {
15484
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15485
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15486
+ }
15487
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15488
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15489
+ }
15490
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15491
+ }
15492
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15493
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15494
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15495
+ tokenBudget: tokenBudget ?? void 0,
15496
+ heading: "## Conceptual evidence",
15497
+ maxResults: 5,
15498
+ includeExactSearchHandoff: false,
15499
+ preferImplementationPaths: true
15500
+ });
15501
+ const fitted = fitTextToContextBudget(`${risk}
15502
+
15503
+ ${pack.text}`, tokenBudget ?? void 0);
15504
+ return {
15505
+ text: fitted.text,
15506
+ details: {
15507
+ resolution,
15508
+ tokenBudget: fitted.tokenBudget,
15509
+ tokenEstimate: fitted.tokenEstimate,
15510
+ truncated: fitted.truncated,
15511
+ sourceIncluded: candidateSource.length > 0,
15512
+ callerCount: 0,
15513
+ calleeCount: 0
15514
+ }
15515
+ };
15516
+ }
15517
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15518
+ const symbol = input.symbol?.trim();
15519
+ if (!symbol) {
15520
+ return fallbackPack(
15521
+ dependencies,
15522
+ input.query,
15523
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15524
+ "not_requested",
15525
+ input.tokenBudget
15526
+ );
15527
+ }
15528
+ let callersResult;
15529
+ try {
15530
+ callersResult = await dependencies.getCallGraphData({
15531
+ name: symbol,
15532
+ filePath: input.filePath ?? void 0,
15533
+ direction: "callers"
15534
+ });
15535
+ } catch (error) {
15536
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15537
+ const message = error instanceof Error ? error.message : String(error);
15538
+ return fallbackPack(
15539
+ dependencies,
15540
+ input.query,
15541
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15542
+ "graph_unavailable",
15543
+ input.tokenBudget,
15544
+ candidates
15545
+ );
15546
+ }
15547
+ if (callersResult.resolution.status !== "resolved") {
15548
+ return fallbackPack(
15549
+ dependencies,
15550
+ input.query,
15551
+ formatResolutionRisk(callersResult.resolution),
15552
+ callersResult.resolution.status,
15553
+ input.tokenBudget
15554
+ );
15555
+ }
15556
+ const resolution = callersResult.resolution;
15557
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15558
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15559
+ dependencies.getCallGraphData({
15560
+ name: symbol,
15561
+ filePath: input.filePath ?? resolution.filePath,
15562
+ direction: "callees"
15563
+ })
15564
+ ]);
15565
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15566
+ let graphRisk;
15567
+ let callees = [];
15568
+ if (calleesResult.status === "rejected") {
15569
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15570
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15571
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15572
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15573
+ } else {
15574
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15575
+ }
15576
+ const source = targetSource(definitionsResult.value, resolution);
15577
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15578
+ const sourceBudget = Math.max(
15579
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15580
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15581
+ );
15582
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15583
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15584
+ const fitted = fitTextToContextBudget([
15585
+ `# Pre-edit context for ${resolution.name}`,
15586
+ graphRisk,
15587
+ sourceText,
15588
+ formatCallers(callers),
15589
+ formatCallees(callees, resolution.filePath)
15590
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15591
+ return {
15592
+ text: fitted.text,
15593
+ details: {
15594
+ resolution: "resolved",
15595
+ tokenBudget: fitted.tokenBudget,
15596
+ tokenEstimate: fitted.tokenEstimate,
15597
+ truncated: fitted.truncated,
15598
+ sourceIncluded: source !== void 0,
15599
+ callerCount: callers.length,
15600
+ calleeCount: callees.length
15601
+ }
15602
+ };
15603
+ }
15604
+ async function resolveCodebaseEditContext(projectRoot3, host, input) {
15605
+ return resolveCodebaseEditContextWithDependencies(input, {
15606
+ searchCodebase: (query, options) => searchCodebase(projectRoot3, host, query, options),
15607
+ implementationLookup: (query, options) => implementationLookup(projectRoot3, host, query, options),
15608
+ getCallGraphData: (params) => getCallGraphData(projectRoot3, host, params)
15609
+ });
15610
+ }
15611
+
15263
15612
  // src/adapters/pi/call-graph.ts
15264
15613
  var import_typebox = require("typebox");
15265
15614
 
15266
15615
  // src/tools/tool-names.ts
15267
15616
  var TOOL_NAME = {
15268
15617
  CODEBASE_CONTEXT: "codebase_context",
15618
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
15269
15619
  CODEBASE_SEARCH: "codebase_search",
15270
15620
  CODEBASE_PEEK: "codebase_peek",
15271
15621
  FIND_SIMILAR: "find_similar",
@@ -15289,6 +15639,7 @@ var TOOL_NAME = {
15289
15639
  };
15290
15640
  var PORTABLE_TOOL_NAMES = [
15291
15641
  TOOL_NAME.CODEBASE_CONTEXT,
15642
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15292
15643
  TOOL_NAME.CODEBASE_SEARCH,
15293
15644
  TOOL_NAME.CODEBASE_PEEK,
15294
15645
  TOOL_NAME.INDEX_CODEBASE,
@@ -15305,6 +15656,7 @@ var PORTABLE_TOOL_NAMES = [
15305
15656
  ];
15306
15657
  var OPENCODE_TOOL_NAMES = [
15307
15658
  TOOL_NAME.CODEBASE_CONTEXT,
15659
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15308
15660
  TOOL_NAME.CODEBASE_SEARCH,
15309
15661
  TOOL_NAME.CODEBASE_PEEK,
15310
15662
  TOOL_NAME.INDEX_CODEBASE,
@@ -15325,6 +15677,7 @@ var OPENCODE_TOOL_NAMES = [
15325
15677
  ];
15326
15678
  var PI_TOOL_NAMES = [
15327
15679
  TOOL_NAME.CODEBASE_CONTEXT,
15680
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15328
15681
  TOOL_NAME.CODEBASE_SEARCH,
15329
15682
  TOOL_NAME.CODEBASE_PEEK,
15330
15683
  TOOL_NAME.FIND_SIMILAR,
@@ -15439,6 +15792,7 @@ function codebaseIndexPiExtension(pi) {
15439
15792
  import_typebox2.Type.Integer({ minimum: MIN_CONTEXT_PATH_DEPTH, maximum: MAX_CONTEXT_PATH_DEPTH }),
15440
15793
  import_typebox2.Type.Null()
15441
15794
  ], { default: 10, description: `Maximum call-graph traversal depth (${MIN_CONTEXT_PATH_DEPTH}-${MAX_CONTEXT_PATH_DEPTH})` })),
15795
+ diagnostic: import_typebox2.Type.Optional(import_typebox2.Type.Union([import_typebox2.Type.Boolean(), import_typebox2.Type.Null()], { description: "Collect diagnostic routing and search traces without changing normal output." })),
15442
15796
  fileType: import_typebox2.Type.Optional(import_typebox2.Type.Union([import_typebox2.Type.String(), import_typebox2.Type.Null()], { description: "Filter by file extension, e.g., ts, py, rs" })),
15443
15797
  directory: import_typebox2.Type.Optional(import_typebox2.Type.Union([import_typebox2.Type.String(), import_typebox2.Type.Null()], { description: "Filter by directory path" })),
15444
15798
  tokenBudget: import_typebox2.Type.Optional(import_typebox2.Type.Union([
@@ -15450,10 +15804,40 @@ function codebaseIndexPiExtension(pi) {
15450
15804
  }))
15451
15805
  }),
15452
15806
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15453
- const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, params);
15807
+ const normalizedParams = {
15808
+ ...params,
15809
+ diagnostic: params.diagnostic ?? void 0
15810
+ };
15811
+ const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, normalizedParams);
15454
15812
  return text2(result.text, result.details);
15455
15813
  }
15456
15814
  });
15815
+ pi.registerTool({
15816
+ name: TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15817
+ label: "Codebase Edit Context",
15818
+ description: "PRE-EDIT TOOL for a known or suspected symbol. Returns bounded target source and direct graph evidence, with a risk-marked fallback when unresolved.",
15819
+ parameters: import_typebox2.Type.Object({
15820
+ query: import_typebox2.Type.String({ description: "The requested change or target behavior" }),
15821
+ symbol: import_typebox2.Type.Optional(import_typebox2.Type.Union([import_typebox2.Type.String(), import_typebox2.Type.Null()])),
15822
+ filePath: import_typebox2.Type.Optional(import_typebox2.Type.Union([import_typebox2.Type.String(), import_typebox2.Type.Null()])),
15823
+ callerLimit: import_typebox2.Type.Optional(import_typebox2.Type.Union([
15824
+ import_typebox2.Type.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15825
+ import_typebox2.Type.Null()
15826
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15827
+ calleeLimit: import_typebox2.Type.Optional(import_typebox2.Type.Union([
15828
+ import_typebox2.Type.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15829
+ import_typebox2.Type.Null()
15830
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15831
+ tokenBudget: import_typebox2.Type.Optional(import_typebox2.Type.Union([
15832
+ import_typebox2.Type.Integer({ minimum: MIN_CONTEXT_PACK_TOKEN_BUDGET, maximum: MAX_CONTEXT_PACK_TOKEN_BUDGET }),
15833
+ import_typebox2.Type.Null()
15834
+ ], { default: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET }))
15835
+ }),
15836
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15837
+ const result = await resolveCodebaseEditContext(projectRoot2(ctx), HOST2, params);
15838
+ return text2(result.text);
15839
+ }
15840
+ });
15457
15841
  pi.registerTool({
15458
15842
  name: TOOL_NAME.CODEBASE_SEARCH,
15459
15843
  label: "Codebase Search",