opencode-codebase-index 0.22.3 → 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.
@@ -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";
@@ -9029,9 +9051,9 @@ function normalizeFilePathForHintMatch(filePath) {
9029
9051
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9030
9052
  }
9031
9053
  function pathMatchesHint(filePath, hint) {
9032
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9054
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9033
9055
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9034
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9056
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9035
9057
  }
9036
9058
  function extractFilePathHint(query) {
9037
9059
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -12904,6 +12926,20 @@ var Indexer = class _Indexer {
12904
12926
  requestedLimit = nextLimit;
12905
12927
  }
12906
12928
  }
12929
+ buildCandidateSnapshot(candidate) {
12930
+ return {
12931
+ id: candidate.id,
12932
+ filePath: candidate.metadata.filePath,
12933
+ startLine: candidate.metadata.startLine,
12934
+ endLine: candidate.metadata.endLine,
12935
+ score: candidate.score,
12936
+ chunkType: candidate.metadata.chunkType,
12937
+ name: candidate.metadata.name
12938
+ };
12939
+ }
12940
+ buildCandidateSnapshotList(candidates) {
12941
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12942
+ }
12907
12943
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12908
12944
  return this.searchCandidatesWithBranchPrefilter(
12909
12945
  initialLimit,
@@ -13095,6 +13131,16 @@ var Indexer = class _Indexer {
13095
13131
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13096
13132
  fusionMs: Math.round(fusionMs * 100) / 100
13097
13133
  });
13134
+ if (options?.trace) {
13135
+ options.trace({
13136
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13137
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13138
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13139
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13140
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13141
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13142
+ });
13143
+ }
13098
13144
  const metadataOnly = options?.metadataOnly ?? false;
13099
13145
  return Promise.all(
13100
13146
  finalResults.map(async (r) => {
@@ -14355,7 +14401,8 @@ async function searchCodebase(projectRoot3, host, query, options = {}) {
14355
14401
  definitionIntent: options.definitionIntent,
14356
14402
  blameAuthor: options.blameAuthor,
14357
14403
  blameSha: options.blameSha,
14358
- blameSince: options.blameSince
14404
+ blameSince: options.blameSince,
14405
+ trace: options.trace
14359
14406
  });
14360
14407
  }
14361
14408
  async function searchCodebaseWithEffectiveness(projectRoot3, host, route, query, options, render) {
@@ -14409,15 +14456,19 @@ async function implementationLookup(projectRoot3, host, query, options = {}) {
14409
14456
  return indexer.search(query, options.limit, {
14410
14457
  fileType: options.fileType,
14411
14458
  directory: options.directory,
14412
- definitionIntent: true
14459
+ definitionIntent: true,
14460
+ trace: options.trace
14413
14461
  });
14414
14462
  }
14415
14463
  async function getCallGraphData(projectRoot3, host, params) {
14416
14464
  await ensureAutoIndexReadyForRetrieval(projectRoot3, host);
14417
14465
  const root = getProjectRoot(projectRoot3, host);
14418
14466
  const indexer = getIndexerForProject(root, host);
14467
+ return getCallGraphDataForIndexer(indexer, root, params);
14468
+ }
14469
+ async function getCallGraphDataForIndexer(indexer, projectRoot3, params) {
14419
14470
  const symbols = await indexer.getCallGraphSymbols();
14420
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14471
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot3, params.name, params.filePath, params.symbolId);
14421
14472
  const direction = params.direction === "callees" ? "callees" : "callers";
14422
14473
  if (resolution.status !== "resolved") {
14423
14474
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14645,17 +14696,17 @@ async function getIndexLogs(projectRoot3, host, args) {
14645
14696
  function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14646
14697
  const root = getProjectRoot(projectRoot3, host);
14647
14698
  const inputPath = knowledgeBasePath.trim();
14648
- const normalizedPath = path20.resolve(
14699
+ const normalizedPath2 = path20.resolve(
14649
14700
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14650
14701
  );
14651
- if (!existsSync12(normalizedPath)) {
14652
- return `Error: Directory does not exist: ${normalizedPath}`;
14702
+ if (!existsSync12(normalizedPath2)) {
14703
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14653
14704
  }
14654
14705
  let realPath;
14655
14706
  try {
14656
- realPath = realpathSync5(normalizedPath);
14707
+ realPath = realpathSync5(normalizedPath2);
14657
14708
  } catch {
14658
- return `Error: Cannot resolve path: ${normalizedPath}`;
14709
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14659
14710
  }
14660
14711
  const blockedPrefixes = [
14661
14712
  "/etc",
@@ -14678,34 +14729,34 @@ function addKnowledgeBase(projectRoot3, host, knowledgeBasePath) {
14678
14729
  ];
14679
14730
  for (const prefix of blockedPrefixes) {
14680
14731
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14681
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14732
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14682
14733
  }
14683
14734
  }
14684
14735
  for (const dotDir of sensitiveDotDirs) {
14685
14736
  const sensitiveDir = path20.join(homeDir, dotDir);
14686
14737
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14687
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14738
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14688
14739
  }
14689
14740
  }
14690
14741
  try {
14691
- const stat = statSync5(normalizedPath);
14742
+ const stat = statSync5(normalizedPath2);
14692
14743
  if (!stat.isDirectory()) {
14693
- return `Error: Path is not a directory: ${normalizedPath}`;
14744
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14694
14745
  }
14695
14746
  } catch (error) {
14696
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14747
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14697
14748
  }
14698
14749
  const config = loadEditableConfig(root, host);
14699
14750
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14700
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14751
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14701
14752
  if (alreadyExists) {
14702
- return `Knowledge base already configured: ${normalizedPath}`;
14753
+ return `Knowledge base already configured: ${normalizedPath2}`;
14703
14754
  }
14704
- knowledgeBases.push(normalizedPath);
14755
+ knowledgeBases.push(normalizedPath2);
14705
14756
  config.knowledgeBases = knowledgeBases;
14706
14757
  saveConfig(root, config, host);
14707
14758
  refreshIndexerForDirectory(root, host);
14708
- let result = `${normalizedPath}
14759
+ let result = `${normalizedPath2}
14709
14760
  `;
14710
14761
  result += `Total knowledge bases: ${knowledgeBases.length}
14711
14762
  `;
@@ -14880,6 +14931,27 @@ function buildRecoveryDetails(attempts, successIndex) {
14880
14931
  successfulAttemptIndex: successIndex
14881
14932
  };
14882
14933
  }
14934
+ function serializeAttempts(attempts) {
14935
+ return attempts.map((attempt) => ({
14936
+ kind: attempt.kind,
14937
+ scope: attempt.scope,
14938
+ resultCount: attempt.resultCount,
14939
+ relaxedFields: attempt.relaxedFields
14940
+ }));
14941
+ }
14942
+ function buildSearchDiagnostic(attempt) {
14943
+ if (!attempt) {
14944
+ return void 0;
14945
+ }
14946
+ return {
14947
+ route: attempt.kind,
14948
+ routedQuery: attempt.query,
14949
+ searchQuery: attempt.query,
14950
+ searchScope: attempt.scopeFilter,
14951
+ searchTrace: attempt.searchTrace,
14952
+ contextPackTrace: attempt.contextPackTrace
14953
+ };
14954
+ }
14883
14955
  function trimOrUndefined2(value) {
14884
14956
  const normalized = value?.trim();
14885
14957
  if (!normalized) {
@@ -14919,6 +14991,7 @@ async function resolveSearchContext(input, operations) {
14919
14991
  const hasFilters = Boolean(fileType || directory);
14920
14992
  const relaxedFields = relaxedHintFields(fileType, directory);
14921
14993
  const attempts = [];
14994
+ const attemptStates = [];
14922
14995
  const decisions = {
14923
14996
  inferredDefinitionMiss: false,
14924
14997
  fallbackFromOriginalConceptualToInferred: false,
@@ -14947,8 +15020,20 @@ async function resolveSearchContext(input, operations) {
14947
15020
  if (seenAttempts.has(key)) {
14948
15021
  return [];
14949
15022
  }
14950
- const results = await runAttempt();
15023
+ const attemptState = {
15024
+ kind,
15025
+ scope: describeScope(scope.fileType, scope.directory),
15026
+ resultCount: 0,
15027
+ relaxedFields: [...relaxedFieldsForAttempt],
15028
+ query: attemptQuery,
15029
+ scopeFilter: scope
15030
+ };
15031
+ const results = await runAttempt((trace) => {
15032
+ attemptState.searchTrace = trace;
15033
+ });
15034
+ attemptState.resultCount = results.length;
14951
15035
  seenAttempts.add(key);
15036
+ attemptStates.push(attemptState);
14952
15037
  attempts.push({
14953
15038
  kind,
14954
15039
  scope: describeScope(scope.fileType, scope.directory),
@@ -14968,7 +15053,7 @@ async function resolveSearchContext(input, operations) {
14968
15053
  symbol,
14969
15054
  scope,
14970
15055
  relaxedFieldsForAttempt,
14971
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
15056
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14972
15057
  );
14973
15058
  };
14974
15059
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -14977,13 +15062,23 @@ async function resolveSearchContext(input, operations) {
14977
15062
  searchQuery,
14978
15063
  scope,
14979
15064
  relaxedFieldsForAttempt,
14980
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
15065
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
14981
15066
  );
14982
15067
  };
14983
- const toResult = (route, routedQuery, pack) => {
15068
+ const findSuccessfulAttemptState = (route) => {
15069
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
15070
+ const attempt = attemptStates[index];
15071
+ if (attempt.kind === route && attempt.resultCount > 0) {
15072
+ return attempt;
15073
+ }
15074
+ }
15075
+ return void 0;
15076
+ };
15077
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
14984
15078
  const base = packedResult(route, routedQuery, pack);
14985
15079
  const baseDetails = base.details;
14986
15080
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
15081
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
14987
15082
  return {
14988
15083
  text: base.text,
14989
15084
  details: {
@@ -14991,7 +15086,10 @@ async function resolveSearchContext(input, operations) {
14991
15086
  tokenBudget: baseDetails.tokenBudget,
14992
15087
  tokenEstimate: baseDetails.tokenEstimate,
14993
15088
  truncated: false,
14994
- recovery: buildRecoveryDetails(attempts, successIndex)
15089
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
15090
+ ...input.diagnostic && {
15091
+ diagnostic: buildSearchDiagnostic(successState)
15092
+ }
14995
15093
  }
14996
15094
  };
14997
15095
  };
@@ -15005,8 +15103,18 @@ async function resolveSearchContext(input, operations) {
15005
15103
  buildContextPack(scopedDefinitionResults, {
15006
15104
  tokenBudget,
15007
15105
  maxResults: limit,
15008
- heading
15009
- })
15106
+ heading,
15107
+ preserveInputOrder: true,
15108
+ ...input.diagnostic ? {
15109
+ trace: (trace) => {
15110
+ const attemptState = findSuccessfulAttemptState("definition");
15111
+ if (attemptState) {
15112
+ attemptState.contextPackTrace = trace;
15113
+ }
15114
+ }
15115
+ } : void 0
15116
+ }),
15117
+ findSuccessfulAttemptState("definition")
15010
15118
  );
15011
15119
  }
15012
15120
  if (explicitSymbol) {
@@ -15024,8 +15132,18 @@ async function resolveSearchContext(input, operations) {
15024
15132
  buildContextPack(unscopedDefinitionResults, {
15025
15133
  tokenBudget,
15026
15134
  maxResults: limit,
15027
- heading: heading2
15028
- })
15135
+ heading: heading2,
15136
+ preserveInputOrder: true,
15137
+ ...input.diagnostic ? {
15138
+ trace: (trace) => {
15139
+ const attemptState = findSuccessfulAttemptState("definition");
15140
+ if (attemptState) {
15141
+ attemptState.contextPackTrace = trace;
15142
+ }
15143
+ }
15144
+ } : void 0
15145
+ }),
15146
+ findSuccessfulAttemptState("definition")
15029
15147
  );
15030
15148
  }
15031
15149
  }
@@ -15044,7 +15162,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15044
15162
  tokenBudget: heading.tokenBudget,
15045
15163
  tokenEstimate: heading.tokenEstimate,
15046
15164
  truncated: heading.truncated,
15047
- recovery: buildRecoveryDetails(attempts, null)
15165
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15166
+ ...input.diagnostic && {
15167
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15168
+ }
15048
15169
  }
15049
15170
  };
15050
15171
  }
@@ -15084,8 +15205,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15084
15205
  maxResults: limit,
15085
15206
  heading,
15086
15207
  includeExactSearchHandoff: true,
15087
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
15088
- })
15208
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
15209
+ ...input.diagnostic ? {
15210
+ trace: (trace) => {
15211
+ const attemptState = findSuccessfulAttemptState("conceptual");
15212
+ if (attemptState) {
15213
+ attemptState.contextPackTrace = trace;
15214
+ }
15215
+ }
15216
+ } : void 0
15217
+ }),
15218
+ findSuccessfulAttemptState("conceptual")
15089
15219
  );
15090
15220
  }
15091
15221
  }
@@ -15101,7 +15231,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
15101
15231
  tokenBudget: fallbackText.tokenBudget,
15102
15232
  tokenEstimate: fallbackText.tokenEstimate,
15103
15233
  truncated: fallbackText.truncated,
15104
- recovery: buildRecoveryDetails(attempts, null)
15234
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
15235
+ ...input.diagnostic && {
15236
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
15237
+ }
15105
15238
  }
15106
15239
  };
15107
15240
  }
@@ -15182,17 +15315,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot3, host, input) {
15182
15315
  details: fittedDetails("path", fitted, 0)
15183
15316
  };
15184
15317
  }
15185
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
15186
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot3, host, lookupSymbol, {
15318
+ return resolveSearchContext({
15319
+ query: input.query,
15320
+ symbol,
15321
+ limit,
15322
+ tokenBudget,
15323
+ fileType,
15324
+ directory,
15325
+ diagnostic: input.diagnostic
15326
+ }, {
15327
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot3, host, lookupSymbol, {
15187
15328
  limit: retrievalLimit,
15188
15329
  fileType: scope.fileType,
15189
- directory: scope.directory
15330
+ directory: scope.directory,
15331
+ trace
15190
15332
  }),
15191
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot3, host, queryText, {
15333
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot3, host, queryText, {
15192
15334
  limit: retrievalLimit,
15193
15335
  fileType: scope.fileType,
15194
15336
  directory: scope.directory,
15195
- metadataOnly: true
15337
+ metadataOnly: true,
15338
+ trace
15196
15339
  })
15197
15340
  });
15198
15341
  }
@@ -15257,12 +15400,188 @@ async function resolveCodebaseContext(projectRoot3, host, input) {
15257
15400
  }
15258
15401
  }
15259
15402
 
15403
+ // src/tools/edit-context.ts
15404
+ function edgeLimit(value) {
15405
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
15406
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
15407
+ }
15408
+ return Math.min(
15409
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
15410
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
15411
+ );
15412
+ }
15413
+ function normalizedPath(value) {
15414
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
15415
+ }
15416
+ function pathsMatch(left, right) {
15417
+ const normalizedLeft = normalizedPath(left);
15418
+ const normalizedRight = normalizedPath(right);
15419
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
15420
+ }
15421
+ function targetSource(results, resolution) {
15422
+ 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);
15423
+ }
15424
+ function formatSource(result) {
15425
+ const name = result.name ? ` ${result.name}` : "";
15426
+ return [
15427
+ "## Target implementation",
15428
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
15429
+ "```",
15430
+ result.content,
15431
+ "```"
15432
+ ].join("\n");
15433
+ }
15434
+ function formatCallers(edges) {
15435
+ if (edges.length === 0) return "## Direct callers\nNone found.";
15436
+ return [
15437
+ "## Direct callers",
15438
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15439
+ ].join("\n");
15440
+ }
15441
+ function formatCallees(edges, sourceFilePath) {
15442
+ if (edges.length === 0) return "## Direct callees\nNone found.";
15443
+ return [
15444
+ "## Direct callees",
15445
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
15446
+ ].join("\n");
15447
+ }
15448
+ function formatResolutionRisk(resolution) {
15449
+ if (resolution.status === "ambiguous") {
15450
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
15451
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
15452
+ }
15453
+ if (resolution.filePath && resolution.totalCandidates > 0) {
15454
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
15455
+ }
15456
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
15457
+ }
15458
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
15459
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
15460
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
15461
+ tokenBudget: tokenBudget ?? void 0,
15462
+ heading: "## Conceptual evidence",
15463
+ maxResults: 5,
15464
+ includeExactSearchHandoff: false,
15465
+ preferImplementationPaths: true
15466
+ });
15467
+ const fitted = fitTextToContextBudget(`${risk}
15468
+
15469
+ ${pack.text}`, tokenBudget ?? void 0);
15470
+ return {
15471
+ text: fitted.text,
15472
+ details: {
15473
+ resolution,
15474
+ tokenBudget: fitted.tokenBudget,
15475
+ tokenEstimate: fitted.tokenEstimate,
15476
+ truncated: fitted.truncated,
15477
+ sourceIncluded: candidateSource.length > 0,
15478
+ callerCount: 0,
15479
+ calleeCount: 0
15480
+ }
15481
+ };
15482
+ }
15483
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
15484
+ const symbol = input.symbol?.trim();
15485
+ if (!symbol) {
15486
+ return fallbackPack(
15487
+ dependencies,
15488
+ input.query,
15489
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
15490
+ "not_requested",
15491
+ input.tokenBudget
15492
+ );
15493
+ }
15494
+ let callersResult;
15495
+ try {
15496
+ callersResult = await dependencies.getCallGraphData({
15497
+ name: symbol,
15498
+ filePath: input.filePath ?? void 0,
15499
+ direction: "callers"
15500
+ });
15501
+ } catch (error) {
15502
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
15503
+ const message = error instanceof Error ? error.message : String(error);
15504
+ return fallbackPack(
15505
+ dependencies,
15506
+ input.query,
15507
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
15508
+ "graph_unavailable",
15509
+ input.tokenBudget,
15510
+ candidates
15511
+ );
15512
+ }
15513
+ if (callersResult.resolution.status !== "resolved") {
15514
+ return fallbackPack(
15515
+ dependencies,
15516
+ input.query,
15517
+ formatResolutionRisk(callersResult.resolution),
15518
+ callersResult.resolution.status,
15519
+ input.tokenBudget
15520
+ );
15521
+ }
15522
+ const resolution = callersResult.resolution;
15523
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
15524
+ dependencies.implementationLookup(symbol, { limit: 10 }),
15525
+ dependencies.getCallGraphData({
15526
+ name: symbol,
15527
+ filePath: input.filePath ?? resolution.filePath,
15528
+ direction: "callees"
15529
+ })
15530
+ ]);
15531
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
15532
+ let graphRisk;
15533
+ let callees = [];
15534
+ if (calleesResult.status === "rejected") {
15535
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
15536
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
15537
+ } else if (calleesResult.value.resolution.status !== "resolved") {
15538
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
15539
+ } else {
15540
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
15541
+ }
15542
+ const source = targetSource(definitionsResult.value, resolution);
15543
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
15544
+ const sourceBudget = Math.max(
15545
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
15546
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
15547
+ );
15548
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
15549
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
15550
+ const fitted = fitTextToContextBudget([
15551
+ `# Pre-edit context for ${resolution.name}`,
15552
+ graphRisk,
15553
+ sourceText,
15554
+ formatCallers(callers),
15555
+ formatCallees(callees, resolution.filePath)
15556
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
15557
+ return {
15558
+ text: fitted.text,
15559
+ details: {
15560
+ resolution: "resolved",
15561
+ tokenBudget: fitted.tokenBudget,
15562
+ tokenEstimate: fitted.tokenEstimate,
15563
+ truncated: fitted.truncated,
15564
+ sourceIncluded: source !== void 0,
15565
+ callerCount: callers.length,
15566
+ calleeCount: callees.length
15567
+ }
15568
+ };
15569
+ }
15570
+ async function resolveCodebaseEditContext(projectRoot3, host, input) {
15571
+ return resolveCodebaseEditContextWithDependencies(input, {
15572
+ searchCodebase: (query, options) => searchCodebase(projectRoot3, host, query, options),
15573
+ implementationLookup: (query, options) => implementationLookup(projectRoot3, host, query, options),
15574
+ getCallGraphData: (params) => getCallGraphData(projectRoot3, host, params)
15575
+ });
15576
+ }
15577
+
15260
15578
  // src/adapters/pi/call-graph.ts
15261
15579
  import { Type } from "typebox";
15262
15580
 
15263
15581
  // src/tools/tool-names.ts
15264
15582
  var TOOL_NAME = {
15265
15583
  CODEBASE_CONTEXT: "codebase_context",
15584
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
15266
15585
  CODEBASE_SEARCH: "codebase_search",
15267
15586
  CODEBASE_PEEK: "codebase_peek",
15268
15587
  FIND_SIMILAR: "find_similar",
@@ -15286,6 +15605,7 @@ var TOOL_NAME = {
15286
15605
  };
15287
15606
  var PORTABLE_TOOL_NAMES = [
15288
15607
  TOOL_NAME.CODEBASE_CONTEXT,
15608
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15289
15609
  TOOL_NAME.CODEBASE_SEARCH,
15290
15610
  TOOL_NAME.CODEBASE_PEEK,
15291
15611
  TOOL_NAME.INDEX_CODEBASE,
@@ -15302,6 +15622,7 @@ var PORTABLE_TOOL_NAMES = [
15302
15622
  ];
15303
15623
  var OPENCODE_TOOL_NAMES = [
15304
15624
  TOOL_NAME.CODEBASE_CONTEXT,
15625
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15305
15626
  TOOL_NAME.CODEBASE_SEARCH,
15306
15627
  TOOL_NAME.CODEBASE_PEEK,
15307
15628
  TOOL_NAME.INDEX_CODEBASE,
@@ -15322,6 +15643,7 @@ var OPENCODE_TOOL_NAMES = [
15322
15643
  ];
15323
15644
  var PI_TOOL_NAMES = [
15324
15645
  TOOL_NAME.CODEBASE_CONTEXT,
15646
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15325
15647
  TOOL_NAME.CODEBASE_SEARCH,
15326
15648
  TOOL_NAME.CODEBASE_PEEK,
15327
15649
  TOOL_NAME.FIND_SIMILAR,
@@ -15436,6 +15758,7 @@ function codebaseIndexPiExtension(pi) {
15436
15758
  Type2.Integer({ minimum: MIN_CONTEXT_PATH_DEPTH, maximum: MAX_CONTEXT_PATH_DEPTH }),
15437
15759
  Type2.Null()
15438
15760
  ], { default: 10, description: `Maximum call-graph traversal depth (${MIN_CONTEXT_PATH_DEPTH}-${MAX_CONTEXT_PATH_DEPTH})` })),
15761
+ diagnostic: Type2.Optional(Type2.Union([Type2.Boolean(), Type2.Null()], { description: "Collect diagnostic routing and search traces without changing normal output." })),
15439
15762
  fileType: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()], { description: "Filter by file extension, e.g., ts, py, rs" })),
15440
15763
  directory: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()], { description: "Filter by directory path" })),
15441
15764
  tokenBudget: Type2.Optional(Type2.Union([
@@ -15447,10 +15770,40 @@ function codebaseIndexPiExtension(pi) {
15447
15770
  }))
15448
15771
  }),
15449
15772
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15450
- const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, params);
15773
+ const normalizedParams = {
15774
+ ...params,
15775
+ diagnostic: params.diagnostic ?? void 0
15776
+ };
15777
+ const result = await resolveCodebaseContext(projectRoot2(ctx), HOST2, normalizedParams);
15451
15778
  return text2(result.text, result.details);
15452
15779
  }
15453
15780
  });
15781
+ pi.registerTool({
15782
+ name: TOOL_NAME.CODEBASE_EDIT_CONTEXT,
15783
+ label: "Codebase Edit Context",
15784
+ 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.",
15785
+ parameters: Type2.Object({
15786
+ query: Type2.String({ description: "The requested change or target behavior" }),
15787
+ symbol: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
15788
+ filePath: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
15789
+ callerLimit: Type2.Optional(Type2.Union([
15790
+ Type2.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15791
+ Type2.Null()
15792
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15793
+ calleeLimit: Type2.Optional(Type2.Union([
15794
+ Type2.Integer({ minimum: MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, maximum: MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT }),
15795
+ Type2.Null()
15796
+ ], { default: DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT })),
15797
+ tokenBudget: Type2.Optional(Type2.Union([
15798
+ Type2.Integer({ minimum: MIN_CONTEXT_PACK_TOKEN_BUDGET, maximum: MAX_CONTEXT_PACK_TOKEN_BUDGET }),
15799
+ Type2.Null()
15800
+ ], { default: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET }))
15801
+ }),
15802
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15803
+ const result = await resolveCodebaseEditContext(projectRoot2(ctx), HOST2, params);
15804
+ return text2(result.text);
15805
+ }
15806
+ });
15454
15807
  pi.registerTool({
15455
15808
  name: TOOL_NAME.CODEBASE_SEARCH,
15456
15809
  label: "Codebase Search",