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.cjs 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
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -1925,6 +1925,9 @@ var RELATIONSHIP_TYPES = [
1925
1925
  ];
1926
1926
  var INDEX_LOG_CATEGORIES = ["search", "embedding", "cache", "gc", "branch", "general"];
1927
1927
  var INDEX_LOG_LEVELS = ["error", "warn", "info", "debug"];
1928
+ var MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 1;
1929
+ var MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 20;
1930
+ var DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT = 5;
1928
1931
  var CODE_COMMUNITIES_MIN_SIZE = 1;
1929
1932
  var CODE_COMMUNITIES_DEFAULT_LIMIT = 20;
1930
1933
  var CODE_COMMUNITIES_MAX_LIMIT = 100;
@@ -2548,6 +2551,16 @@ function compactEvidenceValue(value, maxChars) {
2548
2551
  }
2549
2552
  var MAX_EXACT_SEARCH_HANDOFF_NAMES = 3;
2550
2553
  var MAX_EXACT_SEARCH_HANDOFF_NAME_CHARS = 64;
2554
+ function toContextPackTraceCandidate(result) {
2555
+ return {
2556
+ filePath: result.filePath,
2557
+ startLine: result.startLine,
2558
+ endLine: result.endLine,
2559
+ score: result.score,
2560
+ chunkType: result.chunkType,
2561
+ name: result.name
2562
+ };
2563
+ }
2551
2564
  function formatExactSearchHandoff(results) {
2552
2565
  const suggestedNames = [];
2553
2566
  const seen = /* @__PURE__ */ new Set();
@@ -2596,13 +2609,13 @@ function buildContextPack(results, options = {}) {
2596
2609
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
2597
2610
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
2598
2611
  const candidateCount = results.length;
2599
- const deduplicated = deduplicateContextCandidates(
2600
- rankContextCandidates(
2601
- results,
2602
- options.preferImplementationPaths ?? false
2603
- )
2604
- );
2605
- const diversified = diversifyContextCandidates(deduplicated);
2612
+ const preserveInputOrder = options.preserveInputOrder ?? false;
2613
+ const ranked = preserveInputOrder ? results.map((result, originalIndex) => ({ result, originalIndex })) : rankContextCandidates(results, options.preferImplementationPaths ?? false);
2614
+ const rankedCandidates = ranked.map((entry) => toContextPackTraceCandidate(entry.result));
2615
+ const deduplicated = deduplicateContextCandidates(ranked);
2616
+ const deduplicatedCandidates = deduplicated.map((result) => toContextPackTraceCandidate(result));
2617
+ const diversified = preserveInputOrder ? deduplicated : diversifyContextCandidates(deduplicated);
2618
+ const diversifiedCandidates = diversified.map((result) => toContextPackTraceCandidate(result));
2606
2619
  const duplicateCount = candidateCount - deduplicated.length;
2607
2620
  const selectable = diversified.slice(0, maxResults);
2608
2621
  const limitOmittedCount = deduplicated.length - selectable.length;
@@ -2635,6 +2648,15 @@ function buildContextPack(results, options = {}) {
2635
2648
  const fitted = fitTextToContextBudget(text, tokenBudget);
2636
2649
  const budgetOmittedCount = selectable.length - selected.length;
2637
2650
  const omittedCount = candidateCount - selected.length;
2651
+ if (options.trace) {
2652
+ options.trace({
2653
+ inputCandidates: results.map(toContextPackTraceCandidate),
2654
+ rankedCandidates,
2655
+ deduplicatedCandidates,
2656
+ diversifiedCandidates,
2657
+ selectedCandidates: selected.map(toContextPackTraceCandidate)
2658
+ });
2659
+ }
2638
2660
  return {
2639
2661
  requestedTokenBudget,
2640
2662
  tokenBudget,
@@ -8613,12 +8635,12 @@ function diversifyGroupBySymbol(entries, getCandidate) {
8613
8635
  return [...primary, ...remainder];
8614
8636
  }
8615
8637
  function buildDiversityKey(metadata) {
8616
- const normalizedPath = metadata.filePath.toLowerCase();
8638
+ const normalizedPath2 = metadata.filePath.toLowerCase();
8617
8639
  const normalizedName = (metadata.name ?? "").trim().toLowerCase();
8618
8640
  if (normalizedName.length > 0) {
8619
- return `${normalizedPath}#${normalizedName}`;
8641
+ return `${normalizedPath2}#${normalizedName}`;
8620
8642
  }
8621
- return normalizedPath;
8643
+ return normalizedPath2;
8622
8644
  }
8623
8645
  function rankHybridResults(query, semanticResults, keywordResults, options) {
8624
8646
  const prioritizeSourcePaths = options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source";
@@ -9067,9 +9089,9 @@ function normalizeFilePathForHintMatch(filePath) {
9067
9089
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9068
9090
  }
9069
9091
  function pathMatchesHint(filePath, hint) {
9070
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9092
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9071
9093
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9072
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9094
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9073
9095
  }
9074
9096
  function extractFilePathHint(query) {
9075
9097
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -12942,6 +12964,20 @@ var Indexer = class _Indexer {
12942
12964
  requestedLimit = nextLimit;
12943
12965
  }
12944
12966
  }
12967
+ buildCandidateSnapshot(candidate) {
12968
+ return {
12969
+ id: candidate.id,
12970
+ filePath: candidate.metadata.filePath,
12971
+ startLine: candidate.metadata.startLine,
12972
+ endLine: candidate.metadata.endLine,
12973
+ score: candidate.score,
12974
+ chunkType: candidate.metadata.chunkType,
12975
+ name: candidate.metadata.name
12976
+ };
12977
+ }
12978
+ buildCandidateSnapshotList(candidates) {
12979
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12980
+ }
12945
12981
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12946
12982
  return this.searchCandidatesWithBranchPrefilter(
12947
12983
  initialLimit,
@@ -13133,6 +13169,16 @@ var Indexer = class _Indexer {
13133
13169
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13134
13170
  fusionMs: Math.round(fusionMs * 100) / 100
13135
13171
  });
13172
+ if (options?.trace) {
13173
+ options.trace({
13174
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13175
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13176
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13177
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13178
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13179
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13180
+ });
13181
+ }
13136
13182
  const metadataOnly = options?.metadataOnly ?? false;
13137
13183
  return Promise.all(
13138
13184
  finalResults.map(async (r) => {
@@ -14400,7 +14446,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14400
14446
  definitionIntent: options.definitionIntent,
14401
14447
  blameAuthor: options.blameAuthor,
14402
14448
  blameSha: options.blameSha,
14403
- blameSince: options.blameSince
14449
+ blameSince: options.blameSince,
14450
+ trace: options.trace
14404
14451
  });
14405
14452
  }
14406
14453
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14454,15 +14501,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14454
14501
  return indexer.search(query, options.limit, {
14455
14502
  fileType: options.fileType,
14456
14503
  directory: options.directory,
14457
- definitionIntent: true
14504
+ definitionIntent: true,
14505
+ trace: options.trace
14458
14506
  });
14459
14507
  }
14460
14508
  async function getCallGraphData(projectRoot, host, params) {
14461
14509
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14462
14510
  const root = getProjectRoot(projectRoot, host);
14463
14511
  const indexer = getIndexerForProject(root, host);
14512
+ return getCallGraphDataForIndexer(indexer, root, params);
14513
+ }
14514
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14464
14515
  const symbols = await indexer.getCallGraphSymbols();
14465
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14516
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14466
14517
  const direction = params.direction === "callees" ? "callees" : "callers";
14467
14518
  if (resolution.status !== "resolved") {
14468
14519
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14678,17 +14729,17 @@ async function getIndexLogs(projectRoot, host, args) {
14678
14729
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14679
14730
  const root = getProjectRoot(projectRoot, host);
14680
14731
  const inputPath = knowledgeBasePath.trim();
14681
- const normalizedPath = path20.resolve(
14732
+ const normalizedPath2 = path20.resolve(
14682
14733
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14683
14734
  );
14684
- if (!(0, import_fs13.existsSync)(normalizedPath)) {
14685
- return `Error: Directory does not exist: ${normalizedPath}`;
14735
+ if (!(0, import_fs13.existsSync)(normalizedPath2)) {
14736
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14686
14737
  }
14687
14738
  let realPath;
14688
14739
  try {
14689
- realPath = (0, import_fs13.realpathSync)(normalizedPath);
14740
+ realPath = (0, import_fs13.realpathSync)(normalizedPath2);
14690
14741
  } catch {
14691
- return `Error: Cannot resolve path: ${normalizedPath}`;
14742
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14692
14743
  }
14693
14744
  const blockedPrefixes = [
14694
14745
  "/etc",
@@ -14711,34 +14762,34 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14711
14762
  ];
14712
14763
  for (const prefix of blockedPrefixes) {
14713
14764
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14714
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14765
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14715
14766
  }
14716
14767
  }
14717
14768
  for (const dotDir of sensitiveDotDirs) {
14718
14769
  const sensitiveDir = path20.join(homeDir, dotDir);
14719
14770
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14720
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14771
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14721
14772
  }
14722
14773
  }
14723
14774
  try {
14724
- const stat4 = (0, import_fs13.statSync)(normalizedPath);
14775
+ const stat4 = (0, import_fs13.statSync)(normalizedPath2);
14725
14776
  if (!stat4.isDirectory()) {
14726
- return `Error: Path is not a directory: ${normalizedPath}`;
14777
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14727
14778
  }
14728
14779
  } catch (error) {
14729
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14780
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14730
14781
  }
14731
14782
  const config = loadEditableConfig(root, host);
14732
14783
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14733
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14784
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14734
14785
  if (alreadyExists) {
14735
- return `Knowledge base already configured: ${normalizedPath}`;
14786
+ return `Knowledge base already configured: ${normalizedPath2}`;
14736
14787
  }
14737
- knowledgeBases.push(normalizedPath);
14788
+ knowledgeBases.push(normalizedPath2);
14738
14789
  config.knowledgeBases = knowledgeBases;
14739
14790
  saveConfig(root, config, host);
14740
14791
  refreshIndexerForDirectory(root, host);
14741
- let result = `${normalizedPath}
14792
+ let result = `${normalizedPath2}
14742
14793
  `;
14743
14794
  result += `Total knowledge bases: ${knowledgeBases.length}
14744
14795
  `;
@@ -16783,6 +16834,8 @@ var GitHeadWatcher = class {
16783
16834
  debounceTimer = null;
16784
16835
  debounceMs = 100;
16785
16836
  // Short debounce for git operations
16837
+ readyPromise = Promise.resolve();
16838
+ resolveReady = null;
16786
16839
  constructor(projectRoot) {
16787
16840
  this.projectRoot = projectRoot;
16788
16841
  }
@@ -16791,8 +16844,12 @@ var GitHeadWatcher = class {
16791
16844
  return;
16792
16845
  }
16793
16846
  if (!isGitRepo(this.projectRoot)) {
16847
+ this.readyPromise = Promise.resolve();
16794
16848
  return;
16795
16849
  }
16850
+ this.readyPromise = new Promise((resolve15) => {
16851
+ this.resolveReady = resolve15;
16852
+ });
16796
16853
  this.onBranchChange = handler;
16797
16854
  this.currentBranch = getCurrentBranch(this.projectRoot);
16798
16855
  const headPath = getHeadPath(this.projectRoot);
@@ -16807,6 +16864,10 @@ var GitHeadWatcher = class {
16807
16864
  });
16808
16865
  this.watcher.on("change", () => this.handleHeadChange());
16809
16866
  this.watcher.on("add", () => this.handleHeadChange());
16867
+ this.watcher.once("ready", () => {
16868
+ this.resolveReady?.();
16869
+ this.resolveReady = null;
16870
+ });
16810
16871
  }
16811
16872
  handleHeadChange() {
16812
16873
  if (this.debounceTimer) {
@@ -16844,10 +16905,16 @@ var GitHeadWatcher = class {
16844
16905
  await watcher.close();
16845
16906
  }
16846
16907
  this.onBranchChange = null;
16908
+ this.resolveReady?.();
16909
+ this.resolveReady = null;
16910
+ this.readyPromise = Promise.resolve();
16847
16911
  }
16848
16912
  isRunning() {
16849
16913
  return this.watcher !== null;
16850
16914
  }
16915
+ async waitUntilReady() {
16916
+ await this.readyPromise;
16917
+ }
16851
16918
  };
16852
16919
 
16853
16920
  // src/watcher/index.ts
@@ -16897,7 +16964,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host, options
16897
16964
  fileWatcher,
16898
16965
  gitWatcher,
16899
16966
  whenReady() {
16900
- return fileWatcher.waitUntilReady();
16967
+ return Promise.all([
16968
+ fileWatcher.waitUntilReady(),
16969
+ gitWatcher?.waitUntilReady()
16970
+ ]).then(() => void 0);
16901
16971
  },
16902
16972
  async stop() {
16903
16973
  stopped = true;
@@ -17032,6 +17102,181 @@ var pr_impact = tool({
17032
17102
  }
17033
17103
  });
17034
17104
 
17105
+ // src/tools/edit-context.ts
17106
+ function edgeLimit(value) {
17107
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
17108
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17109
+ }
17110
+ return Math.min(
17111
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
17112
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
17113
+ );
17114
+ }
17115
+ function normalizedPath(value) {
17116
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
17117
+ }
17118
+ function pathsMatch(left, right) {
17119
+ const normalizedLeft = normalizedPath(left);
17120
+ const normalizedRight = normalizedPath(right);
17121
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
17122
+ }
17123
+ function targetSource(results, resolution) {
17124
+ 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);
17125
+ }
17126
+ function formatSource(result) {
17127
+ const name = result.name ? ` ${result.name}` : "";
17128
+ return [
17129
+ "## Target implementation",
17130
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
17131
+ "```",
17132
+ result.content,
17133
+ "```"
17134
+ ].join("\n");
17135
+ }
17136
+ function formatCallers(edges) {
17137
+ if (edges.length === 0) return "## Direct callers\nNone found.";
17138
+ return [
17139
+ "## Direct callers",
17140
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17141
+ ].join("\n");
17142
+ }
17143
+ function formatCallees(edges, sourceFilePath) {
17144
+ if (edges.length === 0) return "## Direct callees\nNone found.";
17145
+ return [
17146
+ "## Direct callees",
17147
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17148
+ ].join("\n");
17149
+ }
17150
+ function formatResolutionRisk(resolution) {
17151
+ if (resolution.status === "ambiguous") {
17152
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
17153
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
17154
+ }
17155
+ if (resolution.filePath && resolution.totalCandidates > 0) {
17156
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
17157
+ }
17158
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
17159
+ }
17160
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
17161
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
17162
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
17163
+ tokenBudget: tokenBudget ?? void 0,
17164
+ heading: "## Conceptual evidence",
17165
+ maxResults: 5,
17166
+ includeExactSearchHandoff: false,
17167
+ preferImplementationPaths: true
17168
+ });
17169
+ const fitted = fitTextToContextBudget(`${risk}
17170
+
17171
+ ${pack.text}`, tokenBudget ?? void 0);
17172
+ return {
17173
+ text: fitted.text,
17174
+ details: {
17175
+ resolution,
17176
+ tokenBudget: fitted.tokenBudget,
17177
+ tokenEstimate: fitted.tokenEstimate,
17178
+ truncated: fitted.truncated,
17179
+ sourceIncluded: candidateSource.length > 0,
17180
+ callerCount: 0,
17181
+ calleeCount: 0
17182
+ }
17183
+ };
17184
+ }
17185
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
17186
+ const symbol = input.symbol?.trim();
17187
+ if (!symbol) {
17188
+ return fallbackPack(
17189
+ dependencies,
17190
+ input.query,
17191
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
17192
+ "not_requested",
17193
+ input.tokenBudget
17194
+ );
17195
+ }
17196
+ let callersResult;
17197
+ try {
17198
+ callersResult = await dependencies.getCallGraphData({
17199
+ name: symbol,
17200
+ filePath: input.filePath ?? void 0,
17201
+ direction: "callers"
17202
+ });
17203
+ } catch (error) {
17204
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
17205
+ const message = error instanceof Error ? error.message : String(error);
17206
+ return fallbackPack(
17207
+ dependencies,
17208
+ input.query,
17209
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
17210
+ "graph_unavailable",
17211
+ input.tokenBudget,
17212
+ candidates
17213
+ );
17214
+ }
17215
+ if (callersResult.resolution.status !== "resolved") {
17216
+ return fallbackPack(
17217
+ dependencies,
17218
+ input.query,
17219
+ formatResolutionRisk(callersResult.resolution),
17220
+ callersResult.resolution.status,
17221
+ input.tokenBudget
17222
+ );
17223
+ }
17224
+ const resolution = callersResult.resolution;
17225
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
17226
+ dependencies.implementationLookup(symbol, { limit: 10 }),
17227
+ dependencies.getCallGraphData({
17228
+ name: symbol,
17229
+ filePath: input.filePath ?? resolution.filePath,
17230
+ direction: "callees"
17231
+ })
17232
+ ]);
17233
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
17234
+ let graphRisk;
17235
+ let callees = [];
17236
+ if (calleesResult.status === "rejected") {
17237
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
17238
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
17239
+ } else if (calleesResult.value.resolution.status !== "resolved") {
17240
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
17241
+ } else {
17242
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
17243
+ }
17244
+ const source = targetSource(definitionsResult.value, resolution);
17245
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
17246
+ const sourceBudget = Math.max(
17247
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
17248
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
17249
+ );
17250
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
17251
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
17252
+ const fitted = fitTextToContextBudget([
17253
+ `# Pre-edit context for ${resolution.name}`,
17254
+ graphRisk,
17255
+ sourceText,
17256
+ formatCallers(callers),
17257
+ formatCallees(callees, resolution.filePath)
17258
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
17259
+ return {
17260
+ text: fitted.text,
17261
+ details: {
17262
+ resolution: "resolved",
17263
+ tokenBudget: fitted.tokenBudget,
17264
+ tokenEstimate: fitted.tokenEstimate,
17265
+ truncated: fitted.truncated,
17266
+ sourceIncluded: source !== void 0,
17267
+ callerCount: callers.length,
17268
+ calleeCount: callees.length
17269
+ }
17270
+ };
17271
+ }
17272
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
17273
+ return resolveCodebaseEditContextWithDependencies(input, {
17274
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
17275
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
17276
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
17277
+ });
17278
+ }
17279
+
17035
17280
  // src/tools/context-search.ts
17036
17281
  var MIN_CONTEXT_RESULT_LIMIT = 1;
17037
17282
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -17134,6 +17379,27 @@ function buildRecoveryDetails(attempts, successIndex) {
17134
17379
  successfulAttemptIndex: successIndex
17135
17380
  };
17136
17381
  }
17382
+ function serializeAttempts(attempts) {
17383
+ return attempts.map((attempt) => ({
17384
+ kind: attempt.kind,
17385
+ scope: attempt.scope,
17386
+ resultCount: attempt.resultCount,
17387
+ relaxedFields: attempt.relaxedFields
17388
+ }));
17389
+ }
17390
+ function buildSearchDiagnostic(attempt) {
17391
+ if (!attempt) {
17392
+ return void 0;
17393
+ }
17394
+ return {
17395
+ route: attempt.kind,
17396
+ routedQuery: attempt.query,
17397
+ searchQuery: attempt.query,
17398
+ searchScope: attempt.scopeFilter,
17399
+ searchTrace: attempt.searchTrace,
17400
+ contextPackTrace: attempt.contextPackTrace
17401
+ };
17402
+ }
17137
17403
  function trimOrUndefined2(value) {
17138
17404
  const normalized = value?.trim();
17139
17405
  if (!normalized) {
@@ -17173,6 +17439,7 @@ async function resolveSearchContext(input, operations) {
17173
17439
  const hasFilters = Boolean(fileType || directory);
17174
17440
  const relaxedFields = relaxedHintFields(fileType, directory);
17175
17441
  const attempts = [];
17442
+ const attemptStates = [];
17176
17443
  const decisions = {
17177
17444
  inferredDefinitionMiss: false,
17178
17445
  fallbackFromOriginalConceptualToInferred: false,
@@ -17201,8 +17468,20 @@ async function resolveSearchContext(input, operations) {
17201
17468
  if (seenAttempts.has(key)) {
17202
17469
  return [];
17203
17470
  }
17204
- const results = await runAttempt();
17471
+ const attemptState = {
17472
+ kind,
17473
+ scope: describeScope(scope.fileType, scope.directory),
17474
+ resultCount: 0,
17475
+ relaxedFields: [...relaxedFieldsForAttempt],
17476
+ query: attemptQuery,
17477
+ scopeFilter: scope
17478
+ };
17479
+ const results = await runAttempt((trace) => {
17480
+ attemptState.searchTrace = trace;
17481
+ });
17482
+ attemptState.resultCount = results.length;
17205
17483
  seenAttempts.add(key);
17484
+ attemptStates.push(attemptState);
17206
17485
  attempts.push({
17207
17486
  kind,
17208
17487
  scope: describeScope(scope.fileType, scope.directory),
@@ -17222,7 +17501,7 @@ async function resolveSearchContext(input, operations) {
17222
17501
  symbol,
17223
17502
  scope,
17224
17503
  relaxedFieldsForAttempt,
17225
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
17504
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17226
17505
  );
17227
17506
  };
17228
17507
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -17231,13 +17510,23 @@ async function resolveSearchContext(input, operations) {
17231
17510
  searchQuery,
17232
17511
  scope,
17233
17512
  relaxedFieldsForAttempt,
17234
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
17513
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17235
17514
  );
17236
17515
  };
17237
- const toResult = (route, routedQuery, pack) => {
17516
+ const findSuccessfulAttemptState = (route) => {
17517
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
17518
+ const attempt = attemptStates[index];
17519
+ if (attempt.kind === route && attempt.resultCount > 0) {
17520
+ return attempt;
17521
+ }
17522
+ }
17523
+ return void 0;
17524
+ };
17525
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
17238
17526
  const base = packedResult(route, routedQuery, pack);
17239
17527
  const baseDetails = base.details;
17240
17528
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
17529
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
17241
17530
  return {
17242
17531
  text: base.text,
17243
17532
  details: {
@@ -17245,7 +17534,10 @@ async function resolveSearchContext(input, operations) {
17245
17534
  tokenBudget: baseDetails.tokenBudget,
17246
17535
  tokenEstimate: baseDetails.tokenEstimate,
17247
17536
  truncated: false,
17248
- recovery: buildRecoveryDetails(attempts, successIndex)
17537
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
17538
+ ...input.diagnostic && {
17539
+ diagnostic: buildSearchDiagnostic(successState)
17540
+ }
17249
17541
  }
17250
17542
  };
17251
17543
  };
@@ -17259,8 +17551,18 @@ async function resolveSearchContext(input, operations) {
17259
17551
  buildContextPack(scopedDefinitionResults, {
17260
17552
  tokenBudget,
17261
17553
  maxResults: limit,
17262
- heading
17263
- })
17554
+ heading,
17555
+ preserveInputOrder: true,
17556
+ ...input.diagnostic ? {
17557
+ trace: (trace) => {
17558
+ const attemptState = findSuccessfulAttemptState("definition");
17559
+ if (attemptState) {
17560
+ attemptState.contextPackTrace = trace;
17561
+ }
17562
+ }
17563
+ } : void 0
17564
+ }),
17565
+ findSuccessfulAttemptState("definition")
17264
17566
  );
17265
17567
  }
17266
17568
  if (explicitSymbol) {
@@ -17278,8 +17580,18 @@ async function resolveSearchContext(input, operations) {
17278
17580
  buildContextPack(unscopedDefinitionResults, {
17279
17581
  tokenBudget,
17280
17582
  maxResults: limit,
17281
- heading: heading2
17282
- })
17583
+ heading: heading2,
17584
+ preserveInputOrder: true,
17585
+ ...input.diagnostic ? {
17586
+ trace: (trace) => {
17587
+ const attemptState = findSuccessfulAttemptState("definition");
17588
+ if (attemptState) {
17589
+ attemptState.contextPackTrace = trace;
17590
+ }
17591
+ }
17592
+ } : void 0
17593
+ }),
17594
+ findSuccessfulAttemptState("definition")
17283
17595
  );
17284
17596
  }
17285
17597
  }
@@ -17298,7 +17610,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17298
17610
  tokenBudget: heading.tokenBudget,
17299
17611
  tokenEstimate: heading.tokenEstimate,
17300
17612
  truncated: heading.truncated,
17301
- recovery: buildRecoveryDetails(attempts, null)
17613
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17614
+ ...input.diagnostic && {
17615
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17616
+ }
17302
17617
  }
17303
17618
  };
17304
17619
  }
@@ -17338,8 +17653,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17338
17653
  maxResults: limit,
17339
17654
  heading,
17340
17655
  includeExactSearchHandoff: true,
17341
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
17342
- })
17656
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
17657
+ ...input.diagnostic ? {
17658
+ trace: (trace) => {
17659
+ const attemptState = findSuccessfulAttemptState("conceptual");
17660
+ if (attemptState) {
17661
+ attemptState.contextPackTrace = trace;
17662
+ }
17663
+ }
17664
+ } : void 0
17665
+ }),
17666
+ findSuccessfulAttemptState("conceptual")
17343
17667
  );
17344
17668
  }
17345
17669
  }
@@ -17355,7 +17679,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17355
17679
  tokenBudget: fallbackText.tokenBudget,
17356
17680
  tokenEstimate: fallbackText.tokenEstimate,
17357
17681
  truncated: fallbackText.truncated,
17358
- recovery: buildRecoveryDetails(attempts, null)
17682
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17683
+ ...input.diagnostic && {
17684
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17685
+ }
17359
17686
  }
17360
17687
  };
17361
17688
  }
@@ -17436,17 +17763,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
17436
17763
  details: fittedDetails("path", fitted, 0)
17437
17764
  };
17438
17765
  }
17439
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
17440
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
17766
+ return resolveSearchContext({
17767
+ query: input.query,
17768
+ symbol,
17769
+ limit,
17770
+ tokenBudget,
17771
+ fileType,
17772
+ directory,
17773
+ diagnostic: input.diagnostic
17774
+ }, {
17775
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
17441
17776
  limit: retrievalLimit,
17442
17777
  fileType: scope.fileType,
17443
- directory: scope.directory
17778
+ directory: scope.directory,
17779
+ trace
17444
17780
  }),
17445
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
17781
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
17446
17782
  limit: retrievalLimit,
17447
17783
  fileType: scope.fileType,
17448
17784
  directory: scope.directory,
17449
- metadataOnly: true
17785
+ metadataOnly: true,
17786
+ trace
17450
17787
  })
17451
17788
  });
17452
17789
  }
@@ -17513,7 +17850,11 @@ async function resolveCodebaseContext(projectRoot, host, input) {
17513
17850
 
17514
17851
  // src/tools/execute-common.ts
17515
17852
  async function executeCodebaseContext(projectRoot, host, args) {
17516
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17853
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17854
+ return { text: result.text, details: result.details };
17855
+ }
17856
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17857
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17517
17858
  }
17518
17859
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17519
17860
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17616,9 +17957,9 @@ function parseGitActivity(output) {
17616
17957
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
17617
17958
  const churn = Number(addedRaw) + Number(deletedRaw);
17618
17959
  if (!Number.isFinite(churn) || churn <= 0) continue;
17619
- const normalizedPath = normalizePath3(filePath);
17620
- const previous = activity.get(normalizedPath);
17621
- activity.set(normalizedPath, {
17960
+ const normalizedPath2 = normalizePath3(filePath);
17961
+ const previous = activity.get(normalizedPath2);
17962
+ activity.set(normalizedPath2, {
17622
17963
  churn: (previous?.churn ?? 0) + churn,
17623
17964
  commits: (previous?.commits ?? 0) + 1,
17624
17965
  latestDate: previous?.latestDate ?? latestDate,
@@ -18208,8 +18549,8 @@ function transformForVisualization(symbols, edges, options = {}) {
18208
18549
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
18209
18550
  filteredSymbols = symbols.filter(
18210
18551
  (s) => {
18211
- const normalizedPath = s.filePath.replace(/\\/g, "/");
18212
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
18552
+ const normalizedPath2 = s.filePath.replace(/\\/g, "/");
18553
+ return normalizedPath2 === normalizedDir || normalizedPath2.startsWith(normalizedDirWithSlash) || normalizedPath2.endsWith(`/${normalizedDir}`) || normalizedPath2.includes(normalizedAbsoluteSuffix);
18213
18554
  }
18214
18555
  );
18215
18556
  }
@@ -18283,6 +18624,26 @@ var z3 = tool.schema;
18283
18624
  var DEFAULT_HOST = "opencode";
18284
18625
  var CHUNK_TYPE_VALUES = CHUNK_TYPES;
18285
18626
  var RELATIONSHIP_TYPE_VALUES = RELATIONSHIP_TYPES;
18627
+ function stableSortedDiagnosticValue(value) {
18628
+ if (Array.isArray(value)) {
18629
+ return value.map((item) => stableSortedDiagnosticValue(item));
18630
+ }
18631
+ if (value === null || typeof value !== "object") {
18632
+ return value;
18633
+ }
18634
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
18635
+ const sorted = {};
18636
+ for (const [key, item] of entries) {
18637
+ sorted[key] = stableSortedDiagnosticValue(item);
18638
+ }
18639
+ return sorted;
18640
+ }
18641
+ function formatCodebaseContextDiagnostic(details) {
18642
+ const sorted = stableSortedDiagnosticValue(details);
18643
+ return `
18644
+ Diagnostics:
18645
+ ${JSON.stringify(sorted, null, 2)}`;
18646
+ }
18286
18647
  function initializeTools2(projectRoot, config) {
18287
18648
  initializeTools(projectRoot, config, DEFAULT_HOST);
18288
18649
  }
@@ -18302,10 +18663,29 @@ var codebase_context = tool({
18302
18663
  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})`),
18303
18664
  fileType: z3.string().nullable().optional().describe("Filter by file extension"),
18304
18665
  directory: z3.string().nullable().optional().describe("Filter by directory path"),
18305
- 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})`)
18666
+ 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})`),
18667
+ diagnostic: z3.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
18668
+ },
18669
+ async execute(args, context) {
18670
+ const result = await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args);
18671
+ if (!args.diagnostic || !result.details?.diagnostic) {
18672
+ return result.text;
18673
+ }
18674
+ return `${result.text}${formatCodebaseContextDiagnostic(result.details.diagnostic)}`;
18675
+ }
18676
+ });
18677
+ var codebase_edit_context = tool({
18678
+ 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.",
18679
+ args: {
18680
+ query: z3.string().describe("The requested change or target behavior"),
18681
+ symbol: z3.string().nullable().optional().describe("Authoritative target symbol when known"),
18682
+ filePath: z3.string().nullable().optional().describe("Optional file path used to disambiguate duplicate symbol names"),
18683
+ 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),
18684
+ 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),
18685
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
18306
18686
  },
18307
18687
  async execute(args, context) {
18308
- return (await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args)).text;
18688
+ return (await executeCodebaseEditContext(context?.worktree, DEFAULT_HOST, args)).text;
18309
18689
  }
18310
18690
  });
18311
18691
  var codebase_peek = tool({
@@ -18562,6 +18942,7 @@ var index_visualize = tool({
18562
18942
  // src/tools/tool-names.ts
18563
18943
  var TOOL_NAME = {
18564
18944
  CODEBASE_CONTEXT: "codebase_context",
18945
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
18565
18946
  CODEBASE_SEARCH: "codebase_search",
18566
18947
  CODEBASE_PEEK: "codebase_peek",
18567
18948
  FIND_SIMILAR: "find_similar",
@@ -18585,6 +18966,7 @@ var TOOL_NAME = {
18585
18966
  };
18586
18967
  var PORTABLE_TOOL_NAMES = [
18587
18968
  TOOL_NAME.CODEBASE_CONTEXT,
18969
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18588
18970
  TOOL_NAME.CODEBASE_SEARCH,
18589
18971
  TOOL_NAME.CODEBASE_PEEK,
18590
18972
  TOOL_NAME.INDEX_CODEBASE,
@@ -18601,6 +18983,7 @@ var PORTABLE_TOOL_NAMES = [
18601
18983
  ];
18602
18984
  var OPENCODE_TOOL_NAMES = [
18603
18985
  TOOL_NAME.CODEBASE_CONTEXT,
18986
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18604
18987
  TOOL_NAME.CODEBASE_SEARCH,
18605
18988
  TOOL_NAME.CODEBASE_PEEK,
18606
18989
  TOOL_NAME.INDEX_CODEBASE,
@@ -18621,6 +19004,7 @@ var OPENCODE_TOOL_NAMES = [
18621
19004
  ];
18622
19005
  var PI_TOOL_NAMES = [
18623
19006
  TOOL_NAME.CODEBASE_CONTEXT,
19007
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18624
19008
  TOOL_NAME.CODEBASE_SEARCH,
18625
19009
  TOOL_NAME.CODEBASE_PEEK,
18626
19010
  TOOL_NAME.FIND_SIMILAR,
@@ -19075,6 +19459,7 @@ var plugin = async ({ directory, worktree }) => {
19075
19459
  return {
19076
19460
  tool: {
19077
19461
  [TOOL_NAME.CODEBASE_CONTEXT]: codebase_context,
19462
+ [TOOL_NAME.CODEBASE_EDIT_CONTEXT]: codebase_edit_context,
19078
19463
  [TOOL_NAME.CODEBASE_SEARCH]: codebase_search,
19079
19464
  [TOOL_NAME.CODEBASE_PEEK]: codebase_peek,
19080
19465
  [TOOL_NAME.INDEX_CODEBASE]: index_codebase,