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.
@@ -2041,6 +2041,9 @@ function findKnowledgeBasePathIndex(knowledgeBases, inputPath, projectRoot3) {
2041
2041
  }
2042
2042
 
2043
2043
  // src/tools/contracts.ts
2044
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
2045
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
2046
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
2044
2047
  var CODE_COMMUNITIES_MIN_SIZE = 1;
2045
2048
  var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
2046
2049
  var CODE_COMMUNITIES_MAX_LIMIT = 100;
@@ -2664,6 +2667,16 @@ function compactEvidenceValue(value, maxChars) {
2664
2667
  }
2665
2668
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
2666
2669
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
2670
+ function toContextPackTraceCandidate(result) {
2671
+ return {
2672
+ filePath: result.filePath,
2673
+ startLine: result.startLine,
2674
+ endLine: result.endLine,
2675
+ score: result.score,
2676
+ chunkType: result.chunkType,
2677
+ name: result.name
2678
+ };
2679
+ }
2667
2680
  function formatExactSearchHandoff(results) {
2668
2681
  const suggestedNames = [];
2669
2682
  const seen = /* @__PURE__ */ new Set();
@@ -2712,13 +2725,13 @@ function buildContextPack(results, options = {}) {
2712
2725
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
2713
2726
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
2714
2727
  const candidateCount = results.length;
2715
- const deduplicated = deduplicateContextCandidates(
2716
- rankContextCandidates(
2717
- results,
2718
- options.preferImplementationPaths ?? false
2719
- )
2720
- );
2721
- const diversified = diversifyContextCandidates(deduplicated);
2728
+ const preserveInputOrder = options.preserveInputOrder ?? false;
2729
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
2730
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
2731
+ const deduplicated = deduplicateContextCandidates(ranked);
2732
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
2733
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
2734
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
2722
2735
  const duplicateCount = candidateCount - deduplicated.length;
2723
2736
  const selectable = diversified.slice(0, maxResults);
2724
2737
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -2751,6 +2764,15 @@ function buildContextPack(results, options = {}) {
2751
2764
  const fitted = fitTextToContextBudget(text3, tokenBudget);
2752
2765
  const budgetOmittedCount = selectable.length - selected.length;
2753
2766
  const omittedCount = candidateCount - selected.length;
2767
+ if (options.trace) {
2768
+ options.trace({
2769
+ inputCandidates: results.map(toContextPackTraceCandidate),
2770
+ rankedCandidates,
2771
+ deduplicatedCandidates,
2772
+ diversifiedCandidates,
2773
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
2774
+ });
2775
+ }
2754
2776
  return {
2755
2777
  requestedTokenBudget,
2756
2778
  tokenBudget,
@@ -8575,12 +8597,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
8575
8597
  return [...primary, ...remainder];
8576
8598
  }
8577
8599
  function buildDiversityKey(metadata) {
8578
- const normalizedPath = metadata.filePath.toLowerCase();
8600
+ const normalizedPath2 = metadata.filePath.toLowerCase();
8579
8601
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
8580
8602
  if (normalizedName.length > 0) {
8581
- return `${normalizedPath}#${normalizedName}`;
8603
+ return `${normalizedPath2}#${normalizedName}`;
8582
8604
  }
8583
- return normalizedPath;
8605
+ return normalizedPath2;
8584
8606
  }
8585
8607
  function rankHybridResults(query, semanticResults, keywordResults, options) {
8586
8608
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -8986,6 +9008,29 @@ function extractPrimaryIdentifierQueryHint(query) {
8986
9008
  const best = codeTerms.find((term) => term.length >= 6);
8987
9009
  return best ?? null;
8988
9010
  }
9011
+ function pathSegmentsForAffinityMatch(filePath) {
9012
+ const normalizedPath2 = normalizeRankingText(filePath).replace(/\\/g, "/");
9013
+ const segments = normalizedPath2.split("/").filter((segment) => segment.length > 0);
9014
+ if (segments.length === 0) {
9015
+ return [];
9016
+ }
9017
+ const basename5 = segments[segments.length - 1] ?? "";
9018
+ const basenameWithoutExt = basename5.replace(/\.[^/.]+$/u, "");
9019
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
9020
+ return Array.from(/* @__PURE__ */ new Set([
9021
+ ...normalizedSegments,
9022
+ basenameWithoutExt.toLowerCase()
9023
+ ]));
9024
+ }
9025
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
9026
+ const haystack = pathSegmentsForAffinityMatch(filePath);
9027
+ return exactIdentifierVariants.some((variant) => {
9028
+ if (!variant || variant.length < 2) {
9029
+ return false;
9030
+ }
9031
+ return haystack.includes(variant);
9032
+ });
9033
+ }
8989
9034
  var FILE_PATH_HINT_EXTENSIONS = [
8990
9035
  "ts",
8991
9036
  "tsx",
@@ -9029,9 +9074,9 @@ function normalizeFilePathForHintMatch(filePath) {
9029
9074
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9030
9075
  }
9031
9076
  function pathMatchesHint(filePath, hint) {
9032
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9077
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9033
9078
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9034
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9079
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9035
9080
  }
9036
9081
  function extractFilePathHint(query) {
9037
9082
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -9061,10 +9106,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9061
9106
  ).map((candidate) => {
9062
9107
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
9063
9108
  const pathLower = candidate.metadata.filePath.toLowerCase();
9064
- let maxMatch = 0;
9065
- const nameMatchesPrimary = primaryVariants.some(
9109
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
9110
+ const exactMatch = exactIdentifierVariants.some(
9066
9111
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
9067
9112
  );
9113
+ let maxMatch = 0;
9114
+ const nameMatchesPrimary = exactMatch;
9115
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
9068
9116
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
9069
9117
  for (const hint of hints) {
9070
9118
  const variants = normalizeIdentifierVariants(hint);
@@ -9085,12 +9133,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9085
9133
  candidate,
9086
9134
  maxMatch,
9087
9135
  pathMatchesFileHint,
9088
- nameMatchesPrimary
9136
+ nameMatchesPrimary,
9137
+ pathAffinity
9089
9138
  };
9090
9139
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
9091
9140
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
9092
9141
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
9093
9142
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
9143
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
9144
+ return b.nameMatchesPrimary ? 1 : -1;
9145
+ }
9146
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
9094
9147
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
9095
9148
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
9096
9149
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -12904,6 +12957,20 @@ var Indexer = class _Indexer {
12904
12957
  requestedLimit = nextLimit;
12905
12958
  }
12906
12959
  }
12960
+ buildCandidateSnapshot(candidate) {
12961
+ return {
12962
+ id: candidate.id,
12963
+ filePath: candidate.metadata.filePath,
12964
+ startLine: candidate.metadata.startLine,
12965
+ endLine: candidate.metadata.endLine,
12966
+ score: candidate.score,
12967
+ chunkType: candidate.metadata.chunkType,
12968
+ name: candidate.metadata.name
12969
+ };
12970
+ }
12971
+ buildCandidateSnapshotList(candidates) {
12972
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12973
+ }
12907
12974
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12908
12975
  return this.searchCandidatesWithBranchPrefilter(
12909
12976
  initialLimit,
@@ -13095,6 +13162,16 @@ var Indexer = class _Indexer {
13095
13162
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13096
13163
  fusionMs: Math.round(fusionMs * 100) / 100
13097
13164
  });
13165
+ if (options?.trace) {
13166
+ options.trace({
13167
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13168
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13169
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13170
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13171
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13172
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13173
+ });
13174
+ }
13098
13175
  const metadataOnly = options?.metadataOnly ?? false;
13099
13176
  return Promise.all(
13100
13177
  finalResults.map(async (r) => {
@@ -14355,7 +14432,8 @@ async function searchCodebase(projectRoot3, host, query, options = {}) {
14355
14432
  definitionIntent: options.definitionIntent,
14356
14433
  blameAuthor: options.blameAuthor,
14357
14434
  blameSha: options.blameSha,
14358
- blameSince: options.blameSince
14435
+ blameSince: options.blameSince,
14436
+ trace: options.trace
14359
14437
  });
14360
14438
  }
14361
14439
  async function searchCodebaseWithEffectiveness(projectRoot3, host, route, query, options, render) {
@@ -14409,15 +14487,19 @@ async function implementationLookup(projectRoot3, host, query, options = {}) {
14409
14487
  return indexer.search(query, options.limit, {
14410
14488
  fileType: options.fileType,
14411
14489
  directory: options.directory,
14412
- definitionIntent: true
14490
+ definitionIntent: true,
14491
+ trace: options.trace
14413
14492
  });
14414
14493
  }
14415
14494
  async function getCallGraphData(projectRoot3, host, params) {
14416
14495
  await ensureAutoIndexReadyForRetrieval(projectRoot3, host);
14417
14496
  const root = getProjectRoot(projectRoot3, host);
14418
14497
  const indexer = getIndexerForProject(root, host);
14498
+ return getCallGraphDataForIndexer(indexer, root, params);
14499
+ }
14500
+ async function getCallGraphDataForIndexer(indexer, projectRoot3, params) {
14419
14501
  const symbols = await indexer.getCallGraphSymbols();
14420
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14502
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot3, params.name, params.filePath, params.symbolId);
14421
14503
  const direction = params.direction === "callees" ? "callees" : "callers";
14422
14504
  if (resolution.status !== "resolved") {
14423
14505
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14645,17 +14727,17 @@ async function getIndexLogs(projectRoot3, host, args) {
14645
14727
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14646
14728
  const root = getProjectRoot(projectRoot3, host);
14647
14729
  const inputPath = knowledgeBasePath.trim();
14648
- const normalizedPath = path20.resolve(
14730
+ const normalizedPath2 = path20.resolve(
14649
14731
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14650
14732
  );
14651
- if (!existsSync12(normalizedPath)) {
14652
- return `Error: Directory does not exist: ${normalizedPath}`;
14733
+ if (!existsSync12(normalizedPath2)) {
14734
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14653
14735
  }
14654
14736
  let realPath;
14655
14737
  try {
14656
- realPath = realpathSync5(normalizedPath);
14738
+ realPath = realpathSync5(normalizedPath2);
14657
14739
  } catch {
14658
- return `Error: Cannot resolve path: ${normalizedPath}`;
14740
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14659
14741
  }
14660
14742
  const blockedPrefixes = [
14661
14743
  "/etc",
@@ -14678,34 +14760,34 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14678
14760
  ];
14679
14761
  for (const prefix of blockedPrefixes) {
14680
14762
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14681
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14763
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14682
14764
  }
14683
14765
  }
14684
14766
  for (const dotDir of sensitiveDotDirs) {
14685
14767
  const sensitiveDir = path20.join(homeDir, dotDir);
14686
14768
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14687
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14769
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14688
14770
  }
14689
14771
  }
14690
14772
  try {
14691
- const stat = statSync5(normalizedPath);
14773
+ const stat = statSync5(normalizedPath2);
14692
14774
  if (!stat.isDirectory()) {
14693
- return `Error: Path is not a directory: ${normalizedPath}`;
14775
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14694
14776
  }
14695
14777
  } catch (error) {
14696
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14778
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14697
14779
  }
14698
14780
  const config = loadEditableConfig(root, host);
14699
14781
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14700
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14782
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14701
14783
  if (alreadyExists) {
14702
- return `Knowledge base already configured: ${normalizedPath}`;
14784
+ return `Knowledge base already configured: ${normalizedPath2}`;
14703
14785
  }
14704
- knowledgeBases.push(normalizedPath);
14786
+ knowledgeBases.push(normalizedPath2);
14705
14787
  config.knowledgeBases = knowledgeBases;
14706
14788
  saveConfig(root, config, host);
14707
14789
  refreshIndexerForDirectory(root, host);
14708
- let result = `${normalizedPath}
14790
+ let result = `${normalizedPath2}
14709
14791
  `;
14710
14792
  result += `Total knowledge bases: ${knowledgeBases.length}
14711
14793
  `;
@@ -14880,6 +14962,27 @@ function buildRecoveryDetails(attempts, successIndex) {
14880
14962
  successfulAttemptIndex: successIndex
14881
14963
  };
14882
14964
  }
14965
+ function serializeAttempts(attempts) {
14966
+ return attempts.map((attempt) => ({
14967
+ kind: attempt.kind,
14968
+ scope: attempt.scope,
14969
+ resultCount: attempt.resultCount,
14970
+ relaxedFields: attempt.relaxedFields
14971
+ }));
14972
+ }
14973
+ function buildSearchDiagnostic(attempt) {
14974
+ if (!attempt) {
14975
+ return void 0;
14976
+ }
14977
+ return {
14978
+ route: attempt.kind,
14979
+ routedQuery: attempt.query,
14980
+ searchQuery: attempt.query,
14981
+ searchScope: attempt.scopeFilter,
14982
+ searchTrace: attempt.searchTrace,
14983
+ contextPackTrace: attempt.contextPackTrace
14984
+ };
14985
+ }
14883
14986
  function trimOrUndefined2(value) {
14884
14987
  const normalized = value?.trim();
14885
14988
  if (!normalized) {
@@ -14919,6 +15022,7 @@ async function resolveSearchContext(input, operations) {
14919
15022
  const hasFilters = Boolean(fileType || directory);
14920
15023
  const relaxedFields = relaxedHintFields(fileType, directory);
14921
15024
  const attempts = [];
15025
+ const attemptStates = [];
14922
15026
  const decisions = {
14923
15027
  inferredDefinitionMiss: false,
14924
15028
  fallbackFromOriginalConceptualToInferred: false,
@@ -14947,8 +15051,20 @@ async function resolveSearchContext(input, operations) {
14947
15051
  if (seenAttempts.has(key)) {
14948
15052
  return [];
14949
15053
  }
14950
- const results = await runAttempt();
15054
+ const attemptState = {
15055
+ kind,
15056
+ scope: describeScope(scope.fileType, scope.directory),
15057
+ resultCount: 0,
15058
+ relaxedFields: [...relaxedFieldsForAttempt],
15059
+ query: attemptQuery,
15060
+ scopeFilter: scope
15061
+ };
15062
+ const results = await runAttempt((trace) => {
15063
+ attemptState.searchTrace = trace;
15064
+ });
15065
+ attemptState.resultCount = results.length;
14951
15066
  seenAttempts.add(key);
15067
+ attemptStates.push(attemptState);
14952
15068
  attempts.push({
14953
15069
  kind,
14954
15070
  scope: describeScope(scope.fileType, scope.directory),
@@ -14968,7 +15084,7 @@ async function resolveSearchContext(input, operations) {
14968
15084
  symbol,
14969
15085
  scope,
14970
15086
  relaxedFieldsForAttempt,
14971
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15087
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14972
15088
  );
14973
15089
  };
14974
15090
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -14977,13 +15093,23 @@ async function resolveSearchContext(input, operations) {
14977
15093
  searchQuery,
14978
15094
  scope,
14979
15095
  relaxedFieldsForAttempt,
14980
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15096
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14981
15097
  );
14982
15098
  };
14983
- const toResult = (route, routedQuery, pack) => {
15099
+ const findSuccessfulAttemptState = (route) => {
15100
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15101
+ const attempt = attemptStates[index];
15102
+ if (attempt.kind === route && attempt.resultCount > 0) {
15103
+ return attempt;
15104
+ }
15105
+ }
15106
+ return void 0;
15107
+ };
15108
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
14984
15109
  const base = packedResult(route, routedQuery, pack);
14985
15110
  const baseDetails = base.details;
14986
15111
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15112
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
14987
15113
  return {
14988
15114
  text: base.text,
14989
15115
  details: {
@@ -14991,7 +15117,10 @@ async function resolveSearchContext(input, operations) {
14991
15117
  tokenBudget: baseDetails.tokenBudget,
14992
15118
  tokenEstimate: baseDetails.tokenEstimate,
14993
15119
  truncated: false,
14994
- recovery: buildRecoveryDetails(attempts, successIndex)
15120
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15121
+ ...input.diagnostic && {
15122
+ diagnostic: buildSearchDiagnostic(successState)
15123
+ }
14995
15124
  }
14996
15125
  };
14997
15126
  };
@@ -15005,8 +15134,18 @@ async function resolveSearchContext(input, operations) {
15005
15134
  buildContextPack(scopedDefinitionResults, {
15006
15135
  tokenBudget,
15007
15136
  maxResults: limit,
15008
- heading
15009
- })
15137
+ heading,
15138
+ preserveInputOrder: true,
15139
+ ...input.diagnostic ? {
15140
+ trace: (trace) => {
15141
+ const attemptState = findSuccessfulAttemptState("definition");
15142
+ if (attemptState) {
15143
+ attemptState.contextPackTrace = trace;
15144
+ }
15145
+ }
15146
+ } : void 0
15147
+ }),
15148
+ findSuccessfulAttemptState("definition")
15010
15149
  );
15011
15150
  }
15012
15151
  if (explicitSymbol) {
@@ -15024,8 +15163,18 @@ async function resolveSearchContext(input, operations) {
15024
15163
  buildContextPack(unscopedDefinitionResults, {
15025
15164
  tokenBudget,
15026
15165
  maxResults: limit,
15027
- heading: heading2
15028
- })
15166
+ heading: heading2,
15167
+ preserveInputOrder: true,
15168
+ ...input.diagnostic ? {
15169
+ trace: (trace) => {
15170
+ const attemptState = findSuccessfulAttemptState("definition");
15171
+ if (attemptState) {
15172
+ attemptState.contextPackTrace = trace;
15173
+ }
15174
+ }
15175
+ } : void 0
15176
+ }),
15177
+ findSuccessfulAttemptState("definition")
15029
15178
  );
15030
15179
  }
15031
15180
  }
@@ -15044,7 +15193,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15044
15193
  tokenBudget: heading.tokenBudget,
15045
15194
  tokenEstimate: heading.tokenEstimate,
15046
15195
  truncated: heading.truncated,
15047
- recovery: buildRecoveryDetails(attempts, null)
15196
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15197
+ ...input.diagnostic && {
15198
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15199
+ }
15048
15200
  }
15049
15201
  };
15050
15202
  }
@@ -15084,8 +15236,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15084
15236
  maxResults: limit,
15085
15237
  heading,
15086
15238
  includeExactSearchHandoff: true,
15087
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15088
- })
15239
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15240
+ ...input.diagnostic ? {
15241
+ trace: (trace) => {
15242
+ const attemptState = findSuccessfulAttemptState("conceptual");
15243
+ if (attemptState) {
15244
+ attemptState.contextPackTrace = trace;
15245
+ }
15246
+ }
15247
+ } : void 0
15248
+ }),
15249
+ findSuccessfulAttemptState("conceptual")
15089
15250
  );
15090
15251
  }
15091
15252
  }
@@ -15101,7 +15262,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15101
15262
  tokenBudget: fallbackText.tokenBudget,
15102
15263
  tokenEstimate: fallbackText.tokenEstimate,
15103
15264
  truncated: fallbackText.truncated,
15104
- recovery: buildRecoveryDetails(attempts, null)
15265
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15266
+ ...input.diagnostic && {
15267
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15268
+ }
15105
15269
  }
15106
15270
  };
15107
15271
  }
@@ -15182,17 +15346,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot3, host, input) {
15182
15346
  details: fittedDetails("path", fitted, 0)
15183
15347
  };
15184
15348
  }
15185
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15186
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot3, host, lookupSymbol, {
15349
+ return resolveSearchContext({
15350
+ query: input.query,
15351
+ symbol,
15352
+ limit,
15353
+ tokenBudget,
15354
+ fileType,
15355
+ directory,
15356
+ diagnostic: input.diagnostic
15357
+ }, {
15358
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot3, host, lookupSymbol, {
15187
15359
  limit: retrievalLimit,
15188
15360
  fileType: scope.fileType,
15189
- directory: scope.directory
15361
+ directory: scope.directory,
15362
+ trace
15190
15363
  }),
15191
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot3, host, queryText, {
15364
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot3, host, queryText, {
15192
15365
  limit: retrievalLimit,
15193
15366
  fileType: scope.fileType,
15194
15367
  directory: scope.directory,
15195
- metadataOnly: true
15368
+ metadataOnly: true,
15369
+ trace
15196
15370
  })
15197
15371
  });
15198
15372
  }
@@ -15257,12 +15431,188 @@ async function resolveCodebaseContext(projectRoot3, host, input) {
15257
15431
  }
15258
15432
  }
15259
15433
 
15434
+ // src/tools/edit-context.ts
15435
+ function edgeLimit(value) {
15436
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15437
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15438
+ }
15439
+ return Math.min(
15440
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15441
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15442
+ );
15443
+ }
15444
+ function normalizedPath(value) {
15445
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15446
+ }
15447
+ function pathsMatch(left, right) {
15448
+ const normalizedLeft = normalizedPath(left);
15449
+ const normalizedRight = normalizedPath(right);
15450
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15451
+ }
15452
+ function targetSource(results, resolution) {
15453
+ 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);
15454
+ }
15455
+ function formatSource(result) {
15456
+ const name = result.name ? ` ${result.name}` : "";
15457
+ return [
15458
+ "## Target implementation",
15459
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15460
+ "```",
15461
+ result.content,
15462
+ "```"
15463
+ ].join("\n");
15464
+ }
15465
+ function formatCallers(edges) {
15466
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15467
+ return [
15468
+ "## Direct callers",
15469
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15470
+ ].join("\n");
15471
+ }
15472
+ function formatCallees(edges, sourceFilePath) {
15473
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15474
+ return [
15475
+ "## Direct callees",
15476
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15477
+ ].join("\n");
15478
+ }
15479
+ function formatResolutionRisk(resolution) {
15480
+ if (resolution.status === "ambiguous") {
15481
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15482
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15483
+ }
15484
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15485
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15486
+ }
15487
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15488
+ }
15489
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15490
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15491
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15492
+ tokenBudget: tokenBudget ?? void 0,
15493
+ heading: "## Conceptual evidence",
15494
+ maxResults: 5,
15495
+ includeExactSearchHandoff: false,
15496
+ preferImplementationPaths: true
15497
+ });
15498
+ const fitted = fitTextToContextBudget(`${risk}
15499
+
15500
+ ${pack.text}`, tokenBudget ?? void 0);
15501
+ return {
15502
+ text: fitted.text,
15503
+ details: {
15504
+ resolution,
15505
+ tokenBudget: fitted.tokenBudget,
15506
+ tokenEstimate: fitted.tokenEstimate,
15507
+ truncated: fitted.truncated,
15508
+ sourceIncluded: candidateSource.length > 0,
15509
+ callerCount: 0,
15510
+ calleeCount: 0
15511
+ }
15512
+ };
15513
+ }
15514
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15515
+ const symbol = input.symbol?.trim();
15516
+ if (!symbol) {
15517
+ return fallbackPack(
15518
+ dependencies,
15519
+ input.query,
15520
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15521
+ "not_requested",
15522
+ input.tokenBudget
15523
+ );
15524
+ }
15525
+ let callersResult;
15526
+ try {
15527
+ callersResult = await dependencies.getCallGraphData({
15528
+ name: symbol,
15529
+ filePath: input.filePath ?? void 0,
15530
+ direction: "callers"
15531
+ });
15532
+ } catch (error) {
15533
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15534
+ const message = error instanceof Error ? error.message : String(error);
15535
+ return fallbackPack(
15536
+ dependencies,
15537
+ input.query,
15538
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15539
+ "graph_unavailable",
15540
+ input.tokenBudget,
15541
+ candidates
15542
+ );
15543
+ }
15544
+ if (callersResult.resolution.status !== "resolved") {
15545
+ return fallbackPack(
15546
+ dependencies,
15547
+ input.query,
15548
+ formatResolutionRisk(callersResult.resolution),
15549
+ callersResult.resolution.status,
15550
+ input.tokenBudget
15551
+ );
15552
+ }
15553
+ const resolution = callersResult.resolution;
15554
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15555
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15556
+ dependencies.getCallGraphData({
15557
+ name: symbol,
15558
+ filePath: input.filePath ?? resolution.filePath,
15559
+ direction: "callees"
15560
+ })
15561
+ ]);
15562
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15563
+ let graphRisk;
15564
+ let callees = [];
15565
+ if (calleesResult.status === "rejected") {
15566
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15567
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15568
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15569
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15570
+ } else {
15571
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15572
+ }
15573
+ const source = targetSource(definitionsResult.value, resolution);
15574
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15575
+ const sourceBudget = Math.max(
15576
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15577
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15578
+ );
15579
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15580
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15581
+ const fitted = fitTextToContextBudget([
15582
+ `# Pre-edit context for ${resolution.name}`,
15583
+ graphRisk,
15584
+ sourceText,
15585
+ formatCallers(callers),
15586
+ formatCallees(callees, resolution.filePath)
15587
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15588
+ return {
15589
+ text: fitted.text,
15590
+ details: {
15591
+ resolution: "resolved",
15592
+ tokenBudget: fitted.tokenBudget,
15593
+ tokenEstimate: fitted.tokenEstimate,
15594
+ truncated: fitted.truncated,
15595
+ sourceIncluded: source !== void 0,
15596
+ callerCount: callers.length,
15597
+ calleeCount: callees.length
15598
+ }
15599
+ };
15600
+ }
15601
+ async function resolveCodebaseEditContext(projectRoot3, host, input) {
15602
+ return resolveCodebaseEditContextWithDependencies(input, {
15603
+ searchCodebase: (query, options) => searchCodebase(projectRoot3, host, query, options),
15604
+ implementationLookup: (query, options) => implementationLookup(projectRoot3, host, query, options),
15605
+ getCallGraphData: (params) => getCallGraphData(projectRoot3, host, params)
15606
+ });
15607
+ }
15608
+
15260
15609
  // src/adapters/pi/call-graph.ts
15261
15610
  import { Type } from "typebox";
15262
15611
 
15263
15612
  // src/tools/tool-names.ts
15264
15613
  var TOOL_NAME = {
15265
15614
  CODEBASE_CONTEXT: "codebase_context",
15615
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
15266
15616
  CODEBASE_SEARCH: "codebase_search",
15267
15617
  CODEBASE_PEEK: "codebase_peek",
15268
15618
  FIND_SIMILAR: "find_similar",
@@ -15286,6 +15636,7 @@ var TOOL_NAME = {
15286
15636
  };
15287
15637
  var PORTABLE_TOOL_NAMES = [
15288
15638
  TOOL_NAME.CODEBASE_CONTEXT,
15639
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15289
15640
  TOOL_NAME.CODEBASE_SEARCH,
15290
15641
  TOOL_NAME.CODEBASE_PEEK,
15291
15642
  TOOL_NAME.INDEX_CODEBASE,
@@ -15302,6 +15653,7 @@ var PORTABLE_TOOL_NAMES = [
15302
15653
  ];
15303
15654
  var OPENCODE_TOOL_NAMES = [
15304
15655
  TOOL_NAME.CODEBASE_CONTEXT,
15656
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15305
15657
  TOOL_NAME.CODEBASE_SEARCH,
15306
15658
  TOOL_NAME.CODEBASE_PEEK,
15307
15659
  TOOL_NAME.INDEX_CODEBASE,
@@ -15322,6 +15674,7 @@ var OPENCODE_TOOL_NAMES = [
15322
15674
  ];
15323
15675
  var PI_TOOL_NAMES = [
15324
15676
  TOOL_NAME.CODEBASE_CONTEXT,
15677
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15325
15678
  TOOL_NAME.CODEBASE_SEARCH,
15326
15679
  TOOL_NAME.CODEBASE_PEEK,
15327
15680
  TOOL_NAME.FIND_SIMILAR,
@@ -15436,6 +15789,7 @@ function codebaseIndexPiExtension(pi) {
15436
15789
  Type2.Integer({ minimum: MIN_CONTEXT_PATH_DEPTH, maximum: MAX_CONTEXT_PATH_DEPTH }),
15437
15790
  Type2.Null()
15438
15791
  ], { default: 10, description: `Maximum call-graph traversal depth (${MIN_CONTEXT_PATH_DEPTH}-${MAX_CONTEXT_PATH_DEPTH})` })),
15792
+ diagnostic: Type2.Optional(Type2.Union([Type2.Boolean(), Type2.Null()], { description: "Collect diagnostic routing and search traces without changing normal output." })),
15439
15793
  fileType: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()], { description: "Filter by file extension, e.g., ts, py, rs" })),
15440
15794
  directory: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()], { description: "Filter by directory path" })),
15441
15795
  tokenBudget: Type2.Optional(Type2.Union([
@@ -15447,10 +15801,40 @@ function codebaseIndexPiExtension(pi) {
15447
15801
  }))
15448
15802
  }),
15449
15803
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15450
- const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, params);
15804
+ const normalizedParams = {
15805
+ ...params,
15806
+ diagnostic: params.diagnostic ?? void 0
15807
+ };
15808
+ const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, normalizedParams);
15451
15809
  return text2(result.text, result.details);
15452
15810
  }
15453
15811
  });
15812
+ pi.registerTool({
15813
+ name: TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15814
+ label: "Codebase Edit Context",
15815
+ 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.",
15816
+ parameters: Type2.Object({
15817
+ query: Type2.String({ description: "The requested change or target behavior" }),
15818
+ symbol: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
15819
+ filePath: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
15820
+ callerLimit: Type2.Optional(Type2.Union([
15821
+ Type2.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15822
+ Type2.Null()
15823
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15824
+ calleeLimit: Type2.Optional(Type2.Union([
15825
+ Type2.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15826
+ Type2.Null()
15827
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15828
+ tokenBudget: Type2.Optional(Type2.Union([
15829
+ Type2.Integer({ minimum: MIN_CONTEXT_PACK_TOKEN_BUDGET, maximum: MAX_CONTEXT_PACK_TOKEN_BUDGET }),
15830
+ Type2.Null()
15831
+ ], { default: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET }))
15832
+ }),
15833
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15834
+ const result = await resolveCodebaseEditContext(projectRoot2(ctx), HOST2, params);
15835
+ return text2(result.text);
15836
+ }
15837
+ });
15454
15838
  pi.registerTool({
15455
15839
  name: TOOL_NAME.CODEBASE_SEARCH,
15456
15840
  label: "Codebase Search",