open-codebase-index 0.22.2 → 0.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-codebase-index - Semantic codebase search for OpenCode
1
+ // open-codebase-index - Semantic codebase indexing and search
2
2
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -1913,6 +1913,9 @@ var RELATIONSHIP_TYPES = [
1913
1913
  ];
1914
1914
  var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
1915
1915
  var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
1916
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
1917
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
1918
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
1916
1919
  var CODE_COMMUNITIES_MIN_SIZE = 1;
1917
1920
  var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
1918
1921
  var CODE_COMMUNITIES_MAX_LIMIT = 100;
@@ -2536,6 +2539,16 @@ function compactEvidenceValue(value, maxChars) {
2536
2539
  }
2537
2540
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
2538
2541
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
2542
+ function toContextPackTraceCandidate(result) {
2543
+ return {
2544
+ filePath: result.filePath,
2545
+ startLine: result.startLine,
2546
+ endLine: result.endLine,
2547
+ score: result.score,
2548
+ chunkType: result.chunkType,
2549
+ name: result.name
2550
+ };
2551
+ }
2539
2552
  function formatExactSearchHandoff(results) {
2540
2553
  const suggestedNames = [];
2541
2554
  const seen = /* @__PURE__ */ new Set();
@@ -2584,13 +2597,13 @@ function buildContextPack(results, options = {}) {
2584
2597
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
2585
2598
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
2586
2599
  const candidateCount = results.length;
2587
- const deduplicated = deduplicateContextCandidates(
2588
- rankContextCandidates(
2589
- results,
2590
- options.preferImplementationPaths ?? false
2591
- )
2592
- );
2593
- const diversified = diversifyContextCandidates(deduplicated);
2600
+ const preserveInputOrder = options.preserveInputOrder ?? false;
2601
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
2602
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
2603
+ const deduplicated = deduplicateContextCandidates(ranked);
2604
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
2605
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
2606
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
2594
2607
  const duplicateCount = candidateCount - deduplicated.length;
2595
2608
  const selectable = diversified.slice(0, maxResults);
2596
2609
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -2623,6 +2636,15 @@ function buildContextPack(results, options = {}) {
2623
2636
  const fitted = fitTextToContextBudget(text, tokenBudget);
2624
2637
  const budgetOmittedCount = selectable.length - selected.length;
2625
2638
  const omittedCount = candidateCount - selected.length;
2639
+ if (options.trace) {
2640
+ options.trace({
2641
+ inputCandidates: results.map(toContextPackTraceCandidate),
2642
+ rankedCandidates,
2643
+ deduplicatedCandidates,
2644
+ diversifiedCandidates,
2645
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
2646
+ });
2647
+ }
2626
2648
  return {
2627
2649
  requestedTokenBudget,
2628
2650
  tokenBudget,
@@ -8610,12 +8632,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
8610
8632
  return [...primary, ...remainder];
8611
8633
  }
8612
8634
  function buildDiversityKey(metadata) {
8613
- const normalizedPath = metadata.filePath.toLowerCase();
8635
+ const normalizedPath2 = metadata.filePath.toLowerCase();
8614
8636
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
8615
8637
  if (normalizedName.length > 0) {
8616
- return `${normalizedPath}#${normalizedName}`;
8638
+ return `${normalizedPath2}#${normalizedName}`;
8617
8639
  }
8618
- return normalizedPath;
8640
+ return normalizedPath2;
8619
8641
  }
8620
8642
  function rankHybridResults(query, semanticResults, keywordResults, options) {
8621
8643
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -9064,9 +9086,9 @@ function normalizeFilePathForHintMatch(filePath) {
9064
9086
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9065
9087
  }
9066
9088
  function pathMatchesHint(filePath, hint) {
9067
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9089
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9068
9090
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9069
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9091
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9070
9092
  }
9071
9093
  function extractFilePathHint(query) {
9072
9094
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -12939,6 +12961,20 @@ var Indexer = class _Indexer {
12939
12961
  requestedLimit = nextLimit;
12940
12962
  }
12941
12963
  }
12964
+ buildCandidateSnapshot(candidate) {
12965
+ return {
12966
+ id: candidate.id,
12967
+ filePath: candidate.metadata.filePath,
12968
+ startLine: candidate.metadata.startLine,
12969
+ endLine: candidate.metadata.endLine,
12970
+ score: candidate.score,
12971
+ chunkType: candidate.metadata.chunkType,
12972
+ name: candidate.metadata.name
12973
+ };
12974
+ }
12975
+ buildCandidateSnapshotList(candidates) {
12976
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12977
+ }
12942
12978
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12943
12979
  return this.searchCandidatesWithBranchPrefilter(
12944
12980
  initialLimit,
@@ -13130,6 +13166,16 @@ var Indexer = class _Indexer {
13130
13166
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13131
13167
  fusionMs: Math.round(fusionMs * 100) / 100
13132
13168
  });
13169
+ if (options?.trace) {
13170
+ options.trace({
13171
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13172
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13173
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13174
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13175
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13176
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13177
+ });
13178
+ }
13133
13179
  const metadataOnly = options?.metadataOnly ?? false;
13134
13180
  return Promise.all(
13135
13181
  finalResults.map(async (r) => {
@@ -14397,7 +14443,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14397
14443
  definitionIntent: options.definitionIntent,
14398
14444
  blameAuthor: options.blameAuthor,
14399
14445
  blameSha: options.blameSha,
14400
- blameSince: options.blameSince
14446
+ blameSince: options.blameSince,
14447
+ trace: options.trace
14401
14448
  });
14402
14449
  }
14403
14450
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14451,15 +14498,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14451
14498
  return indexer.search(query, options.limit, {
14452
14499
  fileType: options.fileType,
14453
14500
  directory: options.directory,
14454
- definitionIntent: true
14501
+ definitionIntent: true,
14502
+ trace: options.trace
14455
14503
  });
14456
14504
  }
14457
14505
  async function getCallGraphData(projectRoot, host, params) {
14458
14506
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14459
14507
  const root = getProjectRoot(projectRoot, host);
14460
14508
  const indexer = getIndexerForProject(root, host);
14509
+ return getCallGraphDataForIndexer(indexer, root, params);
14510
+ }
14511
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14461
14512
  const symbols = await indexer.getCallGraphSymbols();
14462
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14513
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14463
14514
  const direction = params.direction === "callees" ? "callees" : "callers";
14464
14515
  if (resolution.status !== "resolved") {
14465
14516
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14675,17 +14726,17 @@ async function getIndexLogs(projectRoot, host, args) {
14675
14726
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14676
14727
  const root = getProjectRoot(projectRoot, host);
14677
14728
  const inputPath = knowledgeBasePath.trim();
14678
- const normalizedPath = path20.resolve(
14729
+ const normalizedPath2 = path20.resolve(
14679
14730
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14680
14731
  );
14681
- if (!existsSync12(normalizedPath)) {
14682
- return `Error: Directory does not exist: ${normalizedPath}`;
14732
+ if (!existsSync12(normalizedPath2)) {
14733
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14683
14734
  }
14684
14735
  let realPath;
14685
14736
  try {
14686
- realPath = realpathSync5(normalizedPath);
14737
+ realPath = realpathSync5(normalizedPath2);
14687
14738
  } catch {
14688
- return `Error: Cannot resolve path: ${normalizedPath}`;
14739
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14689
14740
  }
14690
14741
  const blockedPrefixes = [
14691
14742
  "/etc",
@@ -14708,34 +14759,34 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14708
14759
  ];
14709
14760
  for (const prefix of blockedPrefixes) {
14710
14761
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14711
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14762
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14712
14763
  }
14713
14764
  }
14714
14765
  for (const dotDir of sensitiveDotDirs) {
14715
14766
  const sensitiveDir = path20.join(homeDir, dotDir);
14716
14767
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14717
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14768
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14718
14769
  }
14719
14770
  }
14720
14771
  try {
14721
- const stat4 = statSync5(normalizedPath);
14772
+ const stat4 = statSync5(normalizedPath2);
14722
14773
  if (!stat4.isDirectory()) {
14723
- return `Error: Path is not a directory: ${normalizedPath}`;
14774
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14724
14775
  }
14725
14776
  } catch (error) {
14726
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14777
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14727
14778
  }
14728
14779
  const config = loadEditableConfig(root, host);
14729
14780
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14730
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14781
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14731
14782
  if (alreadyExists) {
14732
- return `Knowledge base already configured: ${normalizedPath}`;
14783
+ return `Knowledge base already configured: ${normalizedPath2}`;
14733
14784
  }
14734
- knowledgeBases.push(normalizedPath);
14785
+ knowledgeBases.push(normalizedPath2);
14735
14786
  config.knowledgeBases = knowledgeBases;
14736
14787
  saveConfig(root, config, host);
14737
14788
  refreshIndexerForDirectory(root, host);
14738
- let result = `${normalizedPath}
14789
+ let result = `${normalizedPath2}
14739
14790
  `;
14740
14791
  result += `Total knowledge bases: ${knowledgeBases.length}
14741
14792
  `;
@@ -16780,6 +16831,8 @@ var GitHeadWatcher = class {
16780
16831
  debounceTimer = null;
16781
16832
  debounceMs = 100;
16782
16833
  // Short debounce for git operations
16834
+ readyPromise = Promise.resolve();
16835
+ resolveReady = null;
16783
16836
  constructor(projectRoot) {
16784
16837
  this.projectRoot = projectRoot;
16785
16838
  }
@@ -16788,8 +16841,12 @@ var GitHeadWatcher = class {
16788
16841
  return;
16789
16842
  }
16790
16843
  if (!isGitRepo(this.projectRoot)) {
16844
+ this.readyPromise = Promise.resolve();
16791
16845
  return;
16792
16846
  }
16847
+ this.readyPromise = new Promise((resolve15) => {
16848
+ this.resolveReady = resolve15;
16849
+ });
16793
16850
  this.onBranchChange = handler;
16794
16851
  this.currentBranch = getCurrentBranch(this.projectRoot);
16795
16852
  const headPath = getHeadPath(this.projectRoot);
@@ -16804,6 +16861,10 @@ var GitHeadWatcher = class {
16804
16861
  });
16805
16862
  this.watcher.on("change", () => this.handleHeadChange());
16806
16863
  this.watcher.on("add", () => this.handleHeadChange());
16864
+ this.watcher.once("ready", () => {
16865
+ this.resolveReady?.();
16866
+ this.resolveReady = null;
16867
+ });
16807
16868
  }
16808
16869
  handleHeadChange() {
16809
16870
  if (this.debounceTimer) {
@@ -16841,10 +16902,16 @@ var GitHeadWatcher = class {
16841
16902
  await watcher.close();
16842
16903
  }
16843
16904
  this.onBranchChange = null;
16905
+ this.resolveReady?.();
16906
+ this.resolveReady = null;
16907
+ this.readyPromise = Promise.resolve();
16844
16908
  }
16845
16909
  isRunning() {
16846
16910
  return this.watcher !== null;
16847
16911
  }
16912
+ async waitUntilReady() {
16913
+ await this.readyPromise;
16914
+ }
16848
16915
  };
16849
16916
 
16850
16917
  // src/watcher/index.ts
@@ -16894,7 +16961,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host, options
16894
16961
  fileWatcher,
16895
16962
  gitWatcher,
16896
16963
  whenReady() {
16897
- return fileWatcher.waitUntilReady();
16964
+ return Promise.all([
16965
+ fileWatcher.waitUntilReady(),
16966
+ gitWatcher?.waitUntilReady()
16967
+ ]).then(() => void 0);
16898
16968
  },
16899
16969
  async stop() {
16900
16970
  stopped = true;
@@ -17029,6 +17099,181 @@ var pr_impact = tool({
17029
17099
  }
17030
17100
  });
17031
17101
 
17102
+ // src/tools/edit-context.ts
17103
+ function edgeLimit(value) {
17104
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
17105
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17106
+ }
17107
+ return Math.min(
17108
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
17109
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
17110
+ );
17111
+ }
17112
+ function normalizedPath(value) {
17113
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
17114
+ }
17115
+ function pathsMatch(left, right) {
17116
+ const normalizedLeft = normalizedPath(left);
17117
+ const normalizedRight = normalizedPath(right);
17118
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
17119
+ }
17120
+ function targetSource(results, resolution) {
17121
+ 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);
17122
+ }
17123
+ function formatSource(result) {
17124
+ const name = result.name ? ` ${result.name}` : "";
17125
+ return [
17126
+ "## Target implementation",
17127
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
17128
+ "```",
17129
+ result.content,
17130
+ "```"
17131
+ ].join("\n");
17132
+ }
17133
+ function formatCallers(edges) {
17134
+ if (edges.length === 0) return "## Direct callers\nNone found.";
17135
+ return [
17136
+ "## Direct callers",
17137
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17138
+ ].join("\n");
17139
+ }
17140
+ function formatCallees(edges, sourceFilePath) {
17141
+ if (edges.length === 0) return "## Direct callees\nNone found.";
17142
+ return [
17143
+ "## Direct callees",
17144
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17145
+ ].join("\n");
17146
+ }
17147
+ function formatResolutionRisk(resolution) {
17148
+ if (resolution.status === "ambiguous") {
17149
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
17150
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
17151
+ }
17152
+ if (resolution.filePath && resolution.totalCandidates > 0) {
17153
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
17154
+ }
17155
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
17156
+ }
17157
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
17158
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
17159
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
17160
+ tokenBudget: tokenBudget ?? void 0,
17161
+ heading: "## Conceptual evidence",
17162
+ maxResults: 5,
17163
+ includeExactSearchHandoff: false,
17164
+ preferImplementationPaths: true
17165
+ });
17166
+ const fitted = fitTextToContextBudget(`${risk}
17167
+
17168
+ ${pack.text}`, tokenBudget ?? void 0);
17169
+ return {
17170
+ text: fitted.text,
17171
+ details: {
17172
+ resolution,
17173
+ tokenBudget: fitted.tokenBudget,
17174
+ tokenEstimate: fitted.tokenEstimate,
17175
+ truncated: fitted.truncated,
17176
+ sourceIncluded: candidateSource.length > 0,
17177
+ callerCount: 0,
17178
+ calleeCount: 0
17179
+ }
17180
+ };
17181
+ }
17182
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
17183
+ const symbol = input.symbol?.trim();
17184
+ if (!symbol) {
17185
+ return fallbackPack(
17186
+ dependencies,
17187
+ input.query,
17188
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
17189
+ "not_requested",
17190
+ input.tokenBudget
17191
+ );
17192
+ }
17193
+ let callersResult;
17194
+ try {
17195
+ callersResult = await dependencies.getCallGraphData({
17196
+ name: symbol,
17197
+ filePath: input.filePath ?? void 0,
17198
+ direction: "callers"
17199
+ });
17200
+ } catch (error) {
17201
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
17202
+ const message = error instanceof Error ? error.message : String(error);
17203
+ return fallbackPack(
17204
+ dependencies,
17205
+ input.query,
17206
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
17207
+ "graph_unavailable",
17208
+ input.tokenBudget,
17209
+ candidates
17210
+ );
17211
+ }
17212
+ if (callersResult.resolution.status !== "resolved") {
17213
+ return fallbackPack(
17214
+ dependencies,
17215
+ input.query,
17216
+ formatResolutionRisk(callersResult.resolution),
17217
+ callersResult.resolution.status,
17218
+ input.tokenBudget
17219
+ );
17220
+ }
17221
+ const resolution = callersResult.resolution;
17222
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
17223
+ dependencies.implementationLookup(symbol, { limit: 10 }),
17224
+ dependencies.getCallGraphData({
17225
+ name: symbol,
17226
+ filePath: input.filePath ?? resolution.filePath,
17227
+ direction: "callees"
17228
+ })
17229
+ ]);
17230
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
17231
+ let graphRisk;
17232
+ let callees = [];
17233
+ if (calleesResult.status === "rejected") {
17234
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
17235
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
17236
+ } else if (calleesResult.value.resolution.status !== "resolved") {
17237
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
17238
+ } else {
17239
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
17240
+ }
17241
+ const source = targetSource(definitionsResult.value, resolution);
17242
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
17243
+ const sourceBudget = Math.max(
17244
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
17245
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
17246
+ );
17247
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
17248
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
17249
+ const fitted = fitTextToContextBudget([
17250
+ `# Pre-edit context for ${resolution.name}`,
17251
+ graphRisk,
17252
+ sourceText,
17253
+ formatCallers(callers),
17254
+ formatCallees(callees, resolution.filePath)
17255
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
17256
+ return {
17257
+ text: fitted.text,
17258
+ details: {
17259
+ resolution: "resolved",
17260
+ tokenBudget: fitted.tokenBudget,
17261
+ tokenEstimate: fitted.tokenEstimate,
17262
+ truncated: fitted.truncated,
17263
+ sourceIncluded: source !== void 0,
17264
+ callerCount: callers.length,
17265
+ calleeCount: callees.length
17266
+ }
17267
+ };
17268
+ }
17269
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
17270
+ return resolveCodebaseEditContextWithDependencies(input, {
17271
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
17272
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
17273
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
17274
+ });
17275
+ }
17276
+
17032
17277
  // src/tools/context-search.ts
17033
17278
  var MIN_CONTEXT_RESULT_LIMIT = 1;
17034
17279
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -17131,6 +17376,27 @@ function buildRecoveryDetails(attempts, successIndex) {
17131
17376
  successfulAttemptIndex: successIndex
17132
17377
  };
17133
17378
  }
17379
+ function serializeAttempts(attempts) {
17380
+ return attempts.map((attempt) => ({
17381
+ kind: attempt.kind,
17382
+ scope: attempt.scope,
17383
+ resultCount: attempt.resultCount,
17384
+ relaxedFields: attempt.relaxedFields
17385
+ }));
17386
+ }
17387
+ function buildSearchDiagnostic(attempt) {
17388
+ if (!attempt) {
17389
+ return void 0;
17390
+ }
17391
+ return {
17392
+ route: attempt.kind,
17393
+ routedQuery: attempt.query,
17394
+ searchQuery: attempt.query,
17395
+ searchScope: attempt.scopeFilter,
17396
+ searchTrace: attempt.searchTrace,
17397
+ contextPackTrace: attempt.contextPackTrace
17398
+ };
17399
+ }
17134
17400
  function trimOrUndefined2(value) {
17135
17401
  const normalized = value?.trim();
17136
17402
  if (!normalized) {
@@ -17170,6 +17436,7 @@ async function resolveSearchContext(input, operations) {
17170
17436
  const hasFilters = Boolean(fileType || directory);
17171
17437
  const relaxedFields = relaxedHintFields(fileType, directory);
17172
17438
  const attempts = [];
17439
+ const attemptStates = [];
17173
17440
  const decisions = {
17174
17441
  inferredDefinitionMiss: false,
17175
17442
  fallbackFromOriginalConceptualToInferred: false,
@@ -17198,8 +17465,20 @@ async function resolveSearchContext(input, operations) {
17198
17465
  if (seenAttempts.has(key)) {
17199
17466
  return [];
17200
17467
  }
17201
- const results = await runAttempt();
17468
+ const attemptState = {
17469
+ kind,
17470
+ scope: describeScope(scope.fileType, scope.directory),
17471
+ resultCount: 0,
17472
+ relaxedFields: [...relaxedFieldsForAttempt],
17473
+ query: attemptQuery,
17474
+ scopeFilter: scope
17475
+ };
17476
+ const results = await runAttempt((trace) => {
17477
+ attemptState.searchTrace = trace;
17478
+ });
17479
+ attemptState.resultCount = results.length;
17202
17480
  seenAttempts.add(key);
17481
+ attemptStates.push(attemptState);
17203
17482
  attempts.push({
17204
17483
  kind,
17205
17484
  scope: describeScope(scope.fileType, scope.directory),
@@ -17219,7 +17498,7 @@ async function resolveSearchContext(input, operations) {
17219
17498
  symbol,
17220
17499
  scope,
17221
17500
  relaxedFieldsForAttempt,
17222
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
17501
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17223
17502
  );
17224
17503
  };
17225
17504
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -17228,13 +17507,23 @@ async function resolveSearchContext(input, operations) {
17228
17507
  searchQuery,
17229
17508
  scope,
17230
17509
  relaxedFieldsForAttempt,
17231
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
17510
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17232
17511
  );
17233
17512
  };
17234
- const toResult = (route, routedQuery, pack) => {
17513
+ const findSuccessfulAttemptState = (route) => {
17514
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
17515
+ const attempt = attemptStates[index];
17516
+ if (attempt.kind === route && attempt.resultCount > 0) {
17517
+ return attempt;
17518
+ }
17519
+ }
17520
+ return void 0;
17521
+ };
17522
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
17235
17523
  const base = packedResult(route, routedQuery, pack);
17236
17524
  const baseDetails = base.details;
17237
17525
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
17526
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
17238
17527
  return {
17239
17528
  text: base.text,
17240
17529
  details: {
@@ -17242,7 +17531,10 @@ async function resolveSearchContext(input, operations) {
17242
17531
  tokenBudget: baseDetails.tokenBudget,
17243
17532
  tokenEstimate: baseDetails.tokenEstimate,
17244
17533
  truncated: false,
17245
- recovery: buildRecoveryDetails(attempts, successIndex)
17534
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
17535
+ ...input.diagnostic && {
17536
+ diagnostic: buildSearchDiagnostic(successState)
17537
+ }
17246
17538
  }
17247
17539
  };
17248
17540
  };
@@ -17256,8 +17548,18 @@ async function resolveSearchContext(input, operations) {
17256
17548
  buildContextPack(scopedDefinitionResults, {
17257
17549
  tokenBudget,
17258
17550
  maxResults: limit,
17259
- heading
17260
- })
17551
+ heading,
17552
+ preserveInputOrder: true,
17553
+ ...input.diagnostic ? {
17554
+ trace: (trace) => {
17555
+ const attemptState = findSuccessfulAttemptState("definition");
17556
+ if (attemptState) {
17557
+ attemptState.contextPackTrace = trace;
17558
+ }
17559
+ }
17560
+ } : void 0
17561
+ }),
17562
+ findSuccessfulAttemptState("definition")
17261
17563
  );
17262
17564
  }
17263
17565
  if (explicitSymbol) {
@@ -17275,8 +17577,18 @@ async function resolveSearchContext(input, operations) {
17275
17577
  buildContextPack(unscopedDefinitionResults, {
17276
17578
  tokenBudget,
17277
17579
  maxResults: limit,
17278
- heading: heading2
17279
- })
17580
+ heading: heading2,
17581
+ preserveInputOrder: true,
17582
+ ...input.diagnostic ? {
17583
+ trace: (trace) => {
17584
+ const attemptState = findSuccessfulAttemptState("definition");
17585
+ if (attemptState) {
17586
+ attemptState.contextPackTrace = trace;
17587
+ }
17588
+ }
17589
+ } : void 0
17590
+ }),
17591
+ findSuccessfulAttemptState("definition")
17280
17592
  );
17281
17593
  }
17282
17594
  }
@@ -17295,7 +17607,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17295
17607
  tokenBudget: heading.tokenBudget,
17296
17608
  tokenEstimate: heading.tokenEstimate,
17297
17609
  truncated: heading.truncated,
17298
- recovery: buildRecoveryDetails(attempts, null)
17610
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17611
+ ...input.diagnostic && {
17612
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17613
+ }
17299
17614
  }
17300
17615
  };
17301
17616
  }
@@ -17335,8 +17650,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17335
17650
  maxResults: limit,
17336
17651
  heading,
17337
17652
  includeExactSearchHandoff: true,
17338
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
17339
- })
17653
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
17654
+ ...input.diagnostic ? {
17655
+ trace: (trace) => {
17656
+ const attemptState = findSuccessfulAttemptState("conceptual");
17657
+ if (attemptState) {
17658
+ attemptState.contextPackTrace = trace;
17659
+ }
17660
+ }
17661
+ } : void 0
17662
+ }),
17663
+ findSuccessfulAttemptState("conceptual")
17340
17664
  );
17341
17665
  }
17342
17666
  }
@@ -17352,7 +17676,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17352
17676
  tokenBudget: fallbackText.tokenBudget,
17353
17677
  tokenEstimate: fallbackText.tokenEstimate,
17354
17678
  truncated: fallbackText.truncated,
17355
- recovery: buildRecoveryDetails(attempts, null)
17679
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17680
+ ...input.diagnostic && {
17681
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17682
+ }
17356
17683
  }
17357
17684
  };
17358
17685
  }
@@ -17433,17 +17760,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
17433
17760
  details: fittedDetails("path", fitted, 0)
17434
17761
  };
17435
17762
  }
17436
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
17437
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
17763
+ return resolveSearchContext({
17764
+ query: input.query,
17765
+ symbol,
17766
+ limit,
17767
+ tokenBudget,
17768
+ fileType,
17769
+ directory,
17770
+ diagnostic: input.diagnostic
17771
+ }, {
17772
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
17438
17773
  limit: retrievalLimit,
17439
17774
  fileType: scope.fileType,
17440
- directory: scope.directory
17775
+ directory: scope.directory,
17776
+ trace
17441
17777
  }),
17442
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
17778
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
17443
17779
  limit: retrievalLimit,
17444
17780
  fileType: scope.fileType,
17445
17781
  directory: scope.directory,
17446
- metadataOnly: true
17782
+ metadataOnly: true,
17783
+ trace
17447
17784
  })
17448
17785
  });
17449
17786
  }
@@ -17510,7 +17847,11 @@ async function resolveCodebaseContext(projectRoot, host, input) {
17510
17847
 
17511
17848
  // src/tools/execute-common.ts
17512
17849
  async function executeCodebaseContext(projectRoot, host, args) {
17513
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17850
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17851
+ return { text: result.text, details: result.details };
17852
+ }
17853
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17854
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17514
17855
  }
17515
17856
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17516
17857
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17613,9 +17954,9 @@ function parseGitActivity(output) {
17613
17954
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
17614
17955
  const churn = Number(addedRaw) + Number(deletedRaw);
17615
17956
  if (!Number.isFinite(churn) || churn <= 0) continue;
17616
- const normalizedPath = normalizePath3(filePath);
17617
- const previous = activity.get(normalizedPath);
17618
- activity.set(normalizedPath, {
17957
+ const normalizedPath2 = normalizePath3(filePath);
17958
+ const previous = activity.get(normalizedPath2);
17959
+ activity.set(normalizedPath2, {
17619
17960
  churn: (previous?.churn ?? 0) + churn,
17620
17961
  commits: (previous?.commits ?? 0) + 1,
17621
17962
  latestDate: previous?.latestDate ?? latestDate,
@@ -18205,8 +18546,8 @@ function transformForVisualization(symbols, edges, options = {}) {
18205
18546
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
18206
18547
  filteredSymbols = symbols.filter(
18207
18548
  (s) => {
18208
- const normalizedPath = s.filePath.replace(/\\/g, "/");
18209
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
18549
+ const normalizedPath2 = s.filePath.replace(/\\/g, "/");
18550
+ return normalizedPath2 === normalizedDir || normalizedPath2.startsWith(normalizedDirWithSlash) || normalizedPath2.endsWith(`/${normalizedDir}`) || normalizedPath2.includes(normalizedAbsoluteSuffix);
18210
18551
  }
18211
18552
  );
18212
18553
  }
@@ -18280,6 +18621,26 @@ var z3 = tool.schema;
18280
18621
  var DEFAULT_HOST = "opencode";
18281
18622
  var CHUNK_TYPE_VALUES = CHUNK_TYPES;
18282
18623
  var RELATIONSHIP_TYPE_VALUES = RELATIONSHIP_TYPES;
18624
+ function stableSortedDiagnosticValue(value) {
18625
+ if (Array.isArray(value)) {
18626
+ return value.map((item) => stableSortedDiagnosticValue(item));
18627
+ }
18628
+ if (value === null || typeof value !== "object") {
18629
+ return value;
18630
+ }
18631
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
18632
+ const sorted = {};
18633
+ for (const [key, item] of entries) {
18634
+ sorted[key] = stableSortedDiagnosticValue(item);
18635
+ }
18636
+ return sorted;
18637
+ }
18638
+ function formatCodebaseContextDiagnostic(details) {
18639
+ const sorted = stableSortedDiagnosticValue(details);
18640
+ return `
18641
+ Diagnostics:
18642
+ ${JSON.stringify(sorted, null, 2)}`;
18643
+ }
18283
18644
  function initializeTools2(projectRoot, config) {
18284
18645
  initializeTools(projectRoot, config, DEFAULT_HOST);
18285
18646
  }
@@ -18299,10 +18660,29 @@ var codebase_context = tool({
18299
18660
  maxDepth: z3.number().int().min(MIN_CONTEXT_PATH_DEPTH).max(MAX_CONTEXT_PATH_DEPTH).nullable().optional().default(10).describe(`Maximum call-path traversal depth (${MIN_CONTEXT_PATH_DEPTH}-${MAX_CONTEXT_PATH_DEPTH})`),
18300
18661
  fileType: z3.string().nullable().optional().describe("Filter by file extension"),
18301
18662
  directory: z3.string().nullable().optional().describe("Filter by directory path"),
18302
- tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET).describe(`Maximum response tokens (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`)
18663
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET).describe(`Maximum response tokens (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
18664
+ diagnostic: z3.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
18665
+ },
18666
+ async execute(args, context) {
18667
+ const result = await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args);
18668
+ if (!args.diagnostic || !result.details?.diagnostic) {
18669
+ return result.text;
18670
+ }
18671
+ return `${result.text}${formatCodebaseContextDiagnostic(result.details.diagnostic)}`;
18672
+ }
18673
+ });
18674
+ var codebase_edit_context = tool({
18675
+ description: "PRE-EDIT TOOL for a known or suspected symbol. Returns token-bounded target source, direct callers and callees, or a risk-marked conceptual fallback when the target cannot be resolved.",
18676
+ args: {
18677
+ query: z3.string().describe("The requested change or target behavior"),
18678
+ symbol: z3.string().nullable().optional().describe("Authoritative target symbol when known"),
18679
+ filePath: z3.string().nullable().optional().describe("Optional file path used to disambiguate duplicate symbol names"),
18680
+ callerLimit: z3.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).nullable().optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT),
18681
+ calleeLimit: z3.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).nullable().optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT),
18682
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
18303
18683
  },
18304
18684
  async execute(args, context) {
18305
- return (await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args)).text;
18685
+ return (await executeCodebaseEditContext(context?.worktree, DEFAULT_HOST, args)).text;
18306
18686
  }
18307
18687
  });
18308
18688
  var codebase_peek = tool({
@@ -18559,6 +18939,7 @@ var index_visualize = tool({
18559
18939
  // src/tools/tool-names.ts
18560
18940
  var TOOL_NAME = {
18561
18941
  CODEBASE_CONTEXT: "codebase_context",
18942
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
18562
18943
  CODEBASE_SEARCH: "codebase_search",
18563
18944
  CODEBASE_PEEK: "codebase_peek",
18564
18945
  FIND_SIMILAR: "find_similar",
@@ -18582,6 +18963,7 @@ var TOOL_NAME = {
18582
18963
  };
18583
18964
  var PORTABLE_TOOL_NAMES = [
18584
18965
  TOOL_NAME.CODEBASE_CONTEXT,
18966
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18585
18967
  TOOL_NAME.CODEBASE_SEARCH,
18586
18968
  TOOL_NAME.CODEBASE_PEEK,
18587
18969
  TOOL_NAME.INDEX_CODEBASE,
@@ -18598,6 +18980,7 @@ var PORTABLE_TOOL_NAMES = [
18598
18980
  ];
18599
18981
  var OPENCODE_TOOL_NAMES = [
18600
18982
  TOOL_NAME.CODEBASE_CONTEXT,
18983
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18601
18984
  TOOL_NAME.CODEBASE_SEARCH,
18602
18985
  TOOL_NAME.CODEBASE_PEEK,
18603
18986
  TOOL_NAME.INDEX_CODEBASE,
@@ -18618,6 +19001,7 @@ var OPENCODE_TOOL_NAMES = [
18618
19001
  ];
18619
19002
  var PI_TOOL_NAMES = [
18620
19003
  TOOL_NAME.CODEBASE_CONTEXT,
19004
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18621
19005
  TOOL_NAME.CODEBASE_SEARCH,
18622
19006
  TOOL_NAME.CODEBASE_PEEK,
18623
19007
  TOOL_NAME.FIND_SIMILAR,
@@ -19071,6 +19455,7 @@ var plugin = async ({ directory, worktree }) => {
19071
19455
  return {
19072
19456
  tool: {
19073
19457
  [TOOL_NAME.CODEBASE_CONTEXT]: codebase_context,
19458
+ [TOOL_NAME.CODEBASE_EDIT_CONTEXT]: codebase_edit_context,
19074
19459
  [TOOL_NAME.CODEBASE_SEARCH]: codebase_search,
19075
19460
  [TOOL_NAME.CODEBASE_PEEK]: codebase_peek,
19076
19461
  [TOOL_NAME.INDEX_CODEBASE]: index_codebase,