opencode-codebase-index 0.22.3 → 0.22.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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";
@@ -9024,6 +9046,29 @@ function extractPrimaryIdentifierQueryHint(query) {
9024
9046
  const best = codeTerms.find((term) => term.length >= 6);
9025
9047
  return best ?? null;
9026
9048
  }
9049
+ function pathSegmentsForAffinityMatch(filePath) {
9050
+ const normalizedPath2 = normalizeRankingText(filePath).replace(/\\/g, "/");
9051
+ const segments = normalizedPath2.split("/").filter((segment) => segment.length > 0);
9052
+ if (segments.length === 0) {
9053
+ return [];
9054
+ }
9055
+ const basename9 = segments[segments.length - 1] ?? "";
9056
+ const basenameWithoutExt = basename9.replace(/\.[^/.]+$/u, "");
9057
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
9058
+ return Array.from(/* @__PURE__ */ new Set([
9059
+ ...normalizedSegments,
9060
+ basenameWithoutExt.toLowerCase()
9061
+ ]));
9062
+ }
9063
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
9064
+ const haystack = pathSegmentsForAffinityMatch(filePath);
9065
+ return exactIdentifierVariants.some((variant) => {
9066
+ if (!variant || variant.length < 2) {
9067
+ return false;
9068
+ }
9069
+ return haystack.includes(variant);
9070
+ });
9071
+ }
9027
9072
  var FILE_PATH_HINT_EXTENSIONS = [
9028
9073
  "ts",
9029
9074
  "tsx",
@@ -9067,9 +9112,9 @@ function normalizeFilePathForHintMatch(filePath) {
9067
9112
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9068
9113
  }
9069
9114
  function pathMatchesHint(filePath, hint) {
9070
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9115
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9071
9116
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9072
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9117
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9073
9118
  }
9074
9119
  function extractFilePathHint(query) {
9075
9120
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -9099,10 +9144,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9099
9144
  ).map((candidate) => {
9100
9145
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
9101
9146
  const pathLower = candidate.metadata.filePath.toLowerCase();
9102
- let maxMatch = 0;
9103
- const nameMatchesPrimary = primaryVariants.some(
9147
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
9148
+ const exactMatch = exactIdentifierVariants.some(
9104
9149
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
9105
9150
  );
9151
+ let maxMatch = 0;
9152
+ const nameMatchesPrimary = exactMatch;
9153
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
9106
9154
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
9107
9155
  for (const hint of hints) {
9108
9156
  const variants = normalizeIdentifierVariants(hint);
@@ -9123,12 +9171,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9123
9171
  candidate,
9124
9172
  maxMatch,
9125
9173
  pathMatchesFileHint,
9126
- nameMatchesPrimary
9174
+ nameMatchesPrimary,
9175
+ pathAffinity
9127
9176
  };
9128
9177
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
9129
9178
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
9130
9179
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
9131
9180
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
9181
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
9182
+ return b.nameMatchesPrimary ? 1 : -1;
9183
+ }
9184
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
9132
9185
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
9133
9186
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
9134
9187
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -12942,6 +12995,20 @@ var Indexer = class _Indexer {
12942
12995
  requestedLimit = nextLimit;
12943
12996
  }
12944
12997
  }
12998
+ buildCandidateSnapshot(candidate) {
12999
+ return {
13000
+ id: candidate.id,
13001
+ filePath: candidate.metadata.filePath,
13002
+ startLine: candidate.metadata.startLine,
13003
+ endLine: candidate.metadata.endLine,
13004
+ score: candidate.score,
13005
+ chunkType: candidate.metadata.chunkType,
13006
+ name: candidate.metadata.name
13007
+ };
13008
+ }
13009
+ buildCandidateSnapshotList(candidates) {
13010
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
13011
+ }
12945
13012
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12946
13013
  return this.searchCandidatesWithBranchPrefilter(
12947
13014
  initialLimit,
@@ -13133,6 +13200,16 @@ var Indexer = class _Indexer {
13133
13200
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13134
13201
  fusionMs: Math.round(fusionMs * 100) / 100
13135
13202
  });
13203
+ if (options?.trace) {
13204
+ options.trace({
13205
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13206
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13207
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13208
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13209
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13210
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13211
+ });
13212
+ }
13136
13213
  const metadataOnly = options?.metadataOnly ?? false;
13137
13214
  return Promise.all(
13138
13215
  finalResults.map(async (r) => {
@@ -14400,7 +14477,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14400
14477
  definitionIntent: options.definitionIntent,
14401
14478
  blameAuthor: options.blameAuthor,
14402
14479
  blameSha: options.blameSha,
14403
- blameSince: options.blameSince
14480
+ blameSince: options.blameSince,
14481
+ trace: options.trace
14404
14482
  });
14405
14483
  }
14406
14484
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14454,15 +14532,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14454
14532
  return indexer.search(query, options.limit, {
14455
14533
  fileType: options.fileType,
14456
14534
  directory: options.directory,
14457
- definitionIntent: true
14535
+ definitionIntent: true,
14536
+ trace: options.trace
14458
14537
  });
14459
14538
  }
14460
14539
  async function getCallGraphData(projectRoot, host, params) {
14461
14540
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14462
14541
  const root = getProjectRoot(projectRoot, host);
14463
14542
  const indexer = getIndexerForProject(root, host);
14543
+ return getCallGraphDataForIndexer(indexer, root, params);
14544
+ }
14545
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14464
14546
  const symbols = await indexer.getCallGraphSymbols();
14465
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14547
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14466
14548
  const direction = params.direction === "callees" ? "callees" : "callers";
14467
14549
  if (resolution.status !== "resolved") {
14468
14550
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14678,17 +14760,17 @@ async function getIndexLogs(projectRoot, host, args) {
14678
14760
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14679
14761
  const root = getProjectRoot(projectRoot, host);
14680
14762
  const inputPath = knowledgeBasePath.trim();
14681
- const normalizedPath = path20.resolve(
14763
+ const normalizedPath2 = path20.resolve(
14682
14764
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14683
14765
  );
14684
- if (!(0, import_fs13.existsSync)(normalizedPath)) {
14685
- return `Error: Directory does not exist: ${normalizedPath}`;
14766
+ if (!(0, import_fs13.existsSync)(normalizedPath2)) {
14767
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14686
14768
  }
14687
14769
  let realPath;
14688
14770
  try {
14689
- realPath = (0, import_fs13.realpathSync)(normalizedPath);
14771
+ realPath = (0, import_fs13.realpathSync)(normalizedPath2);
14690
14772
  } catch {
14691
- return `Error: Cannot resolve path: ${normalizedPath}`;
14773
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14692
14774
  }
14693
14775
  const blockedPrefixes = [
14694
14776
  "/etc",
@@ -14711,34 +14793,34 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14711
14793
  ];
14712
14794
  for (const prefix of blockedPrefixes) {
14713
14795
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14714
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14796
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14715
14797
  }
14716
14798
  }
14717
14799
  for (const dotDir of sensitiveDotDirs) {
14718
14800
  const sensitiveDir = path20.join(homeDir, dotDir);
14719
14801
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14720
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14802
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14721
14803
  }
14722
14804
  }
14723
14805
  try {
14724
- const stat4 = (0, import_fs13.statSync)(normalizedPath);
14806
+ const stat4 = (0, import_fs13.statSync)(normalizedPath2);
14725
14807
  if (!stat4.isDirectory()) {
14726
- return `Error: Path is not a directory: ${normalizedPath}`;
14808
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14727
14809
  }
14728
14810
  } catch (error) {
14729
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14811
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14730
14812
  }
14731
14813
  const config = loadEditableConfig(root, host);
14732
14814
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14733
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14815
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14734
14816
  if (alreadyExists) {
14735
- return `Knowledge base already configured: ${normalizedPath}`;
14817
+ return `Knowledge base already configured: ${normalizedPath2}`;
14736
14818
  }
14737
- knowledgeBases.push(normalizedPath);
14819
+ knowledgeBases.push(normalizedPath2);
14738
14820
  config.knowledgeBases = knowledgeBases;
14739
14821
  saveConfig(root, config, host);
14740
14822
  refreshIndexerForDirectory(root, host);
14741
- let result = `${normalizedPath}
14823
+ let result = `${normalizedPath2}
14742
14824
  `;
14743
14825
  result += `Total knowledge bases: ${knowledgeBases.length}
14744
14826
  `;
@@ -17051,6 +17133,181 @@ var pr_impact = tool({
17051
17133
  }
17052
17134
  });
17053
17135
 
17136
+ // src/tools/edit-context.ts
17137
+ function edgeLimit(value) {
17138
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
17139
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17140
+ }
17141
+ return Math.min(
17142
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
17143
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
17144
+ );
17145
+ }
17146
+ function normalizedPath(value) {
17147
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
17148
+ }
17149
+ function pathsMatch(left, right) {
17150
+ const normalizedLeft = normalizedPath(left);
17151
+ const normalizedRight = normalizedPath(right);
17152
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
17153
+ }
17154
+ function targetSource(results, resolution) {
17155
+ 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);
17156
+ }
17157
+ function formatSource(result) {
17158
+ const name = result.name ? ` ${result.name}` : "";
17159
+ return [
17160
+ "## Target implementation",
17161
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
17162
+ "```",
17163
+ result.content,
17164
+ "```"
17165
+ ].join("\n");
17166
+ }
17167
+ function formatCallers(edges) {
17168
+ if (edges.length === 0) return "## Direct callers\nNone found.";
17169
+ return [
17170
+ "## Direct callers",
17171
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17172
+ ].join("\n");
17173
+ }
17174
+ function formatCallees(edges, sourceFilePath) {
17175
+ if (edges.length === 0) return "## Direct callees\nNone found.";
17176
+ return [
17177
+ "## Direct callees",
17178
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17179
+ ].join("\n");
17180
+ }
17181
+ function formatResolutionRisk(resolution) {
17182
+ if (resolution.status === "ambiguous") {
17183
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
17184
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
17185
+ }
17186
+ if (resolution.filePath && resolution.totalCandidates > 0) {
17187
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
17188
+ }
17189
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
17190
+ }
17191
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
17192
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
17193
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
17194
+ tokenBudget: tokenBudget ?? void 0,
17195
+ heading: "## Conceptual evidence",
17196
+ maxResults: 5,
17197
+ includeExactSearchHandoff: false,
17198
+ preferImplementationPaths: true
17199
+ });
17200
+ const fitted = fitTextToContextBudget(`${risk}
17201
+
17202
+ ${pack.text}`, tokenBudget ?? void 0);
17203
+ return {
17204
+ text: fitted.text,
17205
+ details: {
17206
+ resolution,
17207
+ tokenBudget: fitted.tokenBudget,
17208
+ tokenEstimate: fitted.tokenEstimate,
17209
+ truncated: fitted.truncated,
17210
+ sourceIncluded: candidateSource.length > 0,
17211
+ callerCount: 0,
17212
+ calleeCount: 0
17213
+ }
17214
+ };
17215
+ }
17216
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
17217
+ const symbol = input.symbol?.trim();
17218
+ if (!symbol) {
17219
+ return fallbackPack(
17220
+ dependencies,
17221
+ input.query,
17222
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
17223
+ "not_requested",
17224
+ input.tokenBudget
17225
+ );
17226
+ }
17227
+ let callersResult;
17228
+ try {
17229
+ callersResult = await dependencies.getCallGraphData({
17230
+ name: symbol,
17231
+ filePath: input.filePath ?? void 0,
17232
+ direction: "callers"
17233
+ });
17234
+ } catch (error) {
17235
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
17236
+ const message = error instanceof Error ? error.message : String(error);
17237
+ return fallbackPack(
17238
+ dependencies,
17239
+ input.query,
17240
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
17241
+ "graph_unavailable",
17242
+ input.tokenBudget,
17243
+ candidates
17244
+ );
17245
+ }
17246
+ if (callersResult.resolution.status !== "resolved") {
17247
+ return fallbackPack(
17248
+ dependencies,
17249
+ input.query,
17250
+ formatResolutionRisk(callersResult.resolution),
17251
+ callersResult.resolution.status,
17252
+ input.tokenBudget
17253
+ );
17254
+ }
17255
+ const resolution = callersResult.resolution;
17256
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
17257
+ dependencies.implementationLookup(symbol, { limit: 10 }),
17258
+ dependencies.getCallGraphData({
17259
+ name: symbol,
17260
+ filePath: input.filePath ?? resolution.filePath,
17261
+ direction: "callees"
17262
+ })
17263
+ ]);
17264
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
17265
+ let graphRisk;
17266
+ let callees = [];
17267
+ if (calleesResult.status === "rejected") {
17268
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
17269
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
17270
+ } else if (calleesResult.value.resolution.status !== "resolved") {
17271
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
17272
+ } else {
17273
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
17274
+ }
17275
+ const source = targetSource(definitionsResult.value, resolution);
17276
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
17277
+ const sourceBudget = Math.max(
17278
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
17279
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
17280
+ );
17281
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
17282
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
17283
+ const fitted = fitTextToContextBudget([
17284
+ `# Pre-edit context for ${resolution.name}`,
17285
+ graphRisk,
17286
+ sourceText,
17287
+ formatCallers(callers),
17288
+ formatCallees(callees, resolution.filePath)
17289
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
17290
+ return {
17291
+ text: fitted.text,
17292
+ details: {
17293
+ resolution: "resolved",
17294
+ tokenBudget: fitted.tokenBudget,
17295
+ tokenEstimate: fitted.tokenEstimate,
17296
+ truncated: fitted.truncated,
17297
+ sourceIncluded: source !== void 0,
17298
+ callerCount: callers.length,
17299
+ calleeCount: callees.length
17300
+ }
17301
+ };
17302
+ }
17303
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
17304
+ return resolveCodebaseEditContextWithDependencies(input, {
17305
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
17306
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
17307
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
17308
+ });
17309
+ }
17310
+
17054
17311
  // src/tools/context-search.ts
17055
17312
  var MIN_CONTEXT_RESULT_LIMIT = 1;
17056
17313
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -17153,6 +17410,27 @@ function buildRecoveryDetails(attempts, successIndex) {
17153
17410
  successfulAttemptIndex: successIndex
17154
17411
  };
17155
17412
  }
17413
+ function serializeAttempts(attempts) {
17414
+ return attempts.map((attempt) => ({
17415
+ kind: attempt.kind,
17416
+ scope: attempt.scope,
17417
+ resultCount: attempt.resultCount,
17418
+ relaxedFields: attempt.relaxedFields
17419
+ }));
17420
+ }
17421
+ function buildSearchDiagnostic(attempt) {
17422
+ if (!attempt) {
17423
+ return void 0;
17424
+ }
17425
+ return {
17426
+ route: attempt.kind,
17427
+ routedQuery: attempt.query,
17428
+ searchQuery: attempt.query,
17429
+ searchScope: attempt.scopeFilter,
17430
+ searchTrace: attempt.searchTrace,
17431
+ contextPackTrace: attempt.contextPackTrace
17432
+ };
17433
+ }
17156
17434
  function trimOrUndefined2(value) {
17157
17435
  const normalized = value?.trim();
17158
17436
  if (!normalized) {
@@ -17192,6 +17470,7 @@ async function resolveSearchContext(input, operations) {
17192
17470
  const hasFilters = Boolean(fileType || directory);
17193
17471
  const relaxedFields = relaxedHintFields(fileType, directory);
17194
17472
  const attempts = [];
17473
+ const attemptStates = [];
17195
17474
  const decisions = {
17196
17475
  inferredDefinitionMiss: false,
17197
17476
  fallbackFromOriginalConceptualToInferred: false,
@@ -17220,8 +17499,20 @@ async function resolveSearchContext(input, operations) {
17220
17499
  if (seenAttempts.has(key)) {
17221
17500
  return [];
17222
17501
  }
17223
- const results = await runAttempt();
17502
+ const attemptState = {
17503
+ kind,
17504
+ scope: describeScope(scope.fileType, scope.directory),
17505
+ resultCount: 0,
17506
+ relaxedFields: [...relaxedFieldsForAttempt],
17507
+ query: attemptQuery,
17508
+ scopeFilter: scope
17509
+ };
17510
+ const results = await runAttempt((trace) => {
17511
+ attemptState.searchTrace = trace;
17512
+ });
17513
+ attemptState.resultCount = results.length;
17224
17514
  seenAttempts.add(key);
17515
+ attemptStates.push(attemptState);
17225
17516
  attempts.push({
17226
17517
  kind,
17227
17518
  scope: describeScope(scope.fileType, scope.directory),
@@ -17241,7 +17532,7 @@ async function resolveSearchContext(input, operations) {
17241
17532
  symbol,
17242
17533
  scope,
17243
17534
  relaxedFieldsForAttempt,
17244
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
17535
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17245
17536
  );
17246
17537
  };
17247
17538
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -17250,13 +17541,23 @@ async function resolveSearchContext(input, operations) {
17250
17541
  searchQuery,
17251
17542
  scope,
17252
17543
  relaxedFieldsForAttempt,
17253
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
17544
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17254
17545
  );
17255
17546
  };
17256
- const toResult = (route, routedQuery, pack) => {
17547
+ const findSuccessfulAttemptState = (route) => {
17548
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
17549
+ const attempt = attemptStates[index];
17550
+ if (attempt.kind === route && attempt.resultCount > 0) {
17551
+ return attempt;
17552
+ }
17553
+ }
17554
+ return void 0;
17555
+ };
17556
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
17257
17557
  const base = packedResult(route, routedQuery, pack);
17258
17558
  const baseDetails = base.details;
17259
17559
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
17560
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
17260
17561
  return {
17261
17562
  text: base.text,
17262
17563
  details: {
@@ -17264,7 +17565,10 @@ async function resolveSearchContext(input, operations) {
17264
17565
  tokenBudget: baseDetails.tokenBudget,
17265
17566
  tokenEstimate: baseDetails.tokenEstimate,
17266
17567
  truncated: false,
17267
- recovery: buildRecoveryDetails(attempts, successIndex)
17568
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
17569
+ ...input.diagnostic && {
17570
+ diagnostic: buildSearchDiagnostic(successState)
17571
+ }
17268
17572
  }
17269
17573
  };
17270
17574
  };
@@ -17278,8 +17582,18 @@ async function resolveSearchContext(input, operations) {
17278
17582
  buildContextPack(scopedDefinitionResults, {
17279
17583
  tokenBudget,
17280
17584
  maxResults: limit,
17281
- heading
17282
- })
17585
+ heading,
17586
+ preserveInputOrder: true,
17587
+ ...input.diagnostic ? {
17588
+ trace: (trace) => {
17589
+ const attemptState = findSuccessfulAttemptState("definition");
17590
+ if (attemptState) {
17591
+ attemptState.contextPackTrace = trace;
17592
+ }
17593
+ }
17594
+ } : void 0
17595
+ }),
17596
+ findSuccessfulAttemptState("definition")
17283
17597
  );
17284
17598
  }
17285
17599
  if (explicitSymbol) {
@@ -17297,8 +17611,18 @@ async function resolveSearchContext(input, operations) {
17297
17611
  buildContextPack(unscopedDefinitionResults, {
17298
17612
  tokenBudget,
17299
17613
  maxResults: limit,
17300
- heading: heading2
17301
- })
17614
+ heading: heading2,
17615
+ preserveInputOrder: true,
17616
+ ...input.diagnostic ? {
17617
+ trace: (trace) => {
17618
+ const attemptState = findSuccessfulAttemptState("definition");
17619
+ if (attemptState) {
17620
+ attemptState.contextPackTrace = trace;
17621
+ }
17622
+ }
17623
+ } : void 0
17624
+ }),
17625
+ findSuccessfulAttemptState("definition")
17302
17626
  );
17303
17627
  }
17304
17628
  }
@@ -17317,7 +17641,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17317
17641
  tokenBudget: heading.tokenBudget,
17318
17642
  tokenEstimate: heading.tokenEstimate,
17319
17643
  truncated: heading.truncated,
17320
- recovery: buildRecoveryDetails(attempts, null)
17644
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17645
+ ...input.diagnostic && {
17646
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17647
+ }
17321
17648
  }
17322
17649
  };
17323
17650
  }
@@ -17357,8 +17684,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17357
17684
  maxResults: limit,
17358
17685
  heading,
17359
17686
  includeExactSearchHandoff: true,
17360
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
17361
- })
17687
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
17688
+ ...input.diagnostic ? {
17689
+ trace: (trace) => {
17690
+ const attemptState = findSuccessfulAttemptState("conceptual");
17691
+ if (attemptState) {
17692
+ attemptState.contextPackTrace = trace;
17693
+ }
17694
+ }
17695
+ } : void 0
17696
+ }),
17697
+ findSuccessfulAttemptState("conceptual")
17362
17698
  );
17363
17699
  }
17364
17700
  }
@@ -17374,7 +17710,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17374
17710
  tokenBudget: fallbackText.tokenBudget,
17375
17711
  tokenEstimate: fallbackText.tokenEstimate,
17376
17712
  truncated: fallbackText.truncated,
17377
- recovery: buildRecoveryDetails(attempts, null)
17713
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17714
+ ...input.diagnostic && {
17715
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17716
+ }
17378
17717
  }
17379
17718
  };
17380
17719
  }
@@ -17455,17 +17794,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
17455
17794
  details: fittedDetails("path", fitted, 0)
17456
17795
  };
17457
17796
  }
17458
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
17459
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
17797
+ return resolveSearchContext({
17798
+ query: input.query,
17799
+ symbol,
17800
+ limit,
17801
+ tokenBudget,
17802
+ fileType,
17803
+ directory,
17804
+ diagnostic: input.diagnostic
17805
+ }, {
17806
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
17460
17807
  limit: retrievalLimit,
17461
17808
  fileType: scope.fileType,
17462
- directory: scope.directory
17809
+ directory: scope.directory,
17810
+ trace
17463
17811
  }),
17464
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
17812
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
17465
17813
  limit: retrievalLimit,
17466
17814
  fileType: scope.fileType,
17467
17815
  directory: scope.directory,
17468
- metadataOnly: true
17816
+ metadataOnly: true,
17817
+ trace
17469
17818
  })
17470
17819
  });
17471
17820
  }
@@ -17532,7 +17881,11 @@ async function resolveCodebaseContext(projectRoot, host, input) {
17532
17881
 
17533
17882
  // src/tools/execute-common.ts
17534
17883
  async function executeCodebaseContext(projectRoot, host, args) {
17535
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17884
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17885
+ return { text: result.text, details: result.details };
17886
+ }
17887
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17888
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17536
17889
  }
17537
17890
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17538
17891
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17635,9 +17988,9 @@ function parseGitActivity(output) {
17635
17988
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
17636
17989
  const churn = Number(addedRaw) + Number(deletedRaw);
17637
17990
  if (!Number.isFinite(churn) || churn <= 0) continue;
17638
- const normalizedPath = normalizePath3(filePath);
17639
- const previous = activity.get(normalizedPath);
17640
- activity.set(normalizedPath, {
17991
+ const normalizedPath2 = normalizePath3(filePath);
17992
+ const previous = activity.get(normalizedPath2);
17993
+ activity.set(normalizedPath2, {
17641
17994
  churn: (previous?.churn ?? 0) + churn,
17642
17995
  commits: (previous?.commits ?? 0) + 1,
17643
17996
  latestDate: previous?.latestDate ?? latestDate,
@@ -18227,8 +18580,8 @@ function transformForVisualization(symbols, edges, options = {}) {
18227
18580
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
18228
18581
  filteredSymbols = symbols.filter(
18229
18582
  (s) => {
18230
- const normalizedPath = s.filePath.replace(/\\/g, "/");
18231
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
18583
+ const normalizedPath2 = s.filePath.replace(/\\/g, "/");
18584
+ return normalizedPath2 === normalizedDir || normalizedPath2.startsWith(normalizedDirWithSlash) || normalizedPath2.endsWith(`/${normalizedDir}`) || normalizedPath2.includes(normalizedAbsoluteSuffix);
18232
18585
  }
18233
18586
  );
18234
18587
  }
@@ -18302,6 +18655,26 @@ var z3 = tool.schema;
18302
18655
  var DEFAULT_HOST = "opencode";
18303
18656
  var CHUNK_TYPE_VALUES = CHUNK_TYPES;
18304
18657
  var RELATIONSHIP_TYPE_VALUES = RELATIONSHIP_TYPES;
18658
+ function stableSortedDiagnosticValue(value) {
18659
+ if (Array.isArray(value)) {
18660
+ return value.map((item) => stableSortedDiagnosticValue(item));
18661
+ }
18662
+ if (value === null || typeof value !== "object") {
18663
+ return value;
18664
+ }
18665
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
18666
+ const sorted = {};
18667
+ for (const [key, item] of entries) {
18668
+ sorted[key] = stableSortedDiagnosticValue(item);
18669
+ }
18670
+ return sorted;
18671
+ }
18672
+ function formatCodebaseContextDiagnostic(details) {
18673
+ const sorted = stableSortedDiagnosticValue(details);
18674
+ return `
18675
+ Diagnostics:
18676
+ ${JSON.stringify(sorted, null, 2)}`;
18677
+ }
18305
18678
  function initializeTools2(projectRoot, config) {
18306
18679
  initializeTools(projectRoot, config, DEFAULT_HOST);
18307
18680
  }
@@ -18321,10 +18694,29 @@ var codebase_context = tool({
18321
18694
  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})`),
18322
18695
  fileType: z3.string().nullable().optional().describe("Filter by file extension"),
18323
18696
  directory: z3.string().nullable().optional().describe("Filter by directory path"),
18324
- 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})`)
18697
+ 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})`),
18698
+ diagnostic: z3.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
18699
+ },
18700
+ async execute(args, context) {
18701
+ const result = await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args);
18702
+ if (!args.diagnostic || !result.details?.diagnostic) {
18703
+ return result.text;
18704
+ }
18705
+ return `${result.text}${formatCodebaseContextDiagnostic(result.details.diagnostic)}`;
18706
+ }
18707
+ });
18708
+ var codebase_edit_context = tool({
18709
+ 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.",
18710
+ args: {
18711
+ query: z3.string().describe("The requested change or target behavior"),
18712
+ symbol: z3.string().nullable().optional().describe("Authoritative target symbol when known"),
18713
+ filePath: z3.string().nullable().optional().describe("Optional file path used to disambiguate duplicate symbol names"),
18714
+ 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),
18715
+ 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),
18716
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
18325
18717
  },
18326
18718
  async execute(args, context) {
18327
- return (await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args)).text;
18719
+ return (await executeCodebaseEditContext(context?.worktree, DEFAULT_HOST, args)).text;
18328
18720
  }
18329
18721
  });
18330
18722
  var codebase_peek = tool({
@@ -18581,6 +18973,7 @@ var index_visualize = tool({
18581
18973
  // src/tools/tool-names.ts
18582
18974
  var TOOL_NAME = {
18583
18975
  CODEBASE_CONTEXT: "codebase_context",
18976
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
18584
18977
  CODEBASE_SEARCH: "codebase_search",
18585
18978
  CODEBASE_PEEK: "codebase_peek",
18586
18979
  FIND_SIMILAR: "find_similar",
@@ -18604,6 +18997,7 @@ var TOOL_NAME = {
18604
18997
  };
18605
18998
  var PORTABLE_TOOL_NAMES = [
18606
18999
  TOOL_NAME.CODEBASE_CONTEXT,
19000
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18607
19001
  TOOL_NAME.CODEBASE_SEARCH,
18608
19002
  TOOL_NAME.CODEBASE_PEEK,
18609
19003
  TOOL_NAME.INDEX_CODEBASE,
@@ -18620,6 +19014,7 @@ var PORTABLE_TOOL_NAMES = [
18620
19014
  ];
18621
19015
  var OPENCODE_TOOL_NAMES = [
18622
19016
  TOOL_NAME.CODEBASE_CONTEXT,
19017
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18623
19018
  TOOL_NAME.CODEBASE_SEARCH,
18624
19019
  TOOL_NAME.CODEBASE_PEEK,
18625
19020
  TOOL_NAME.INDEX_CODEBASE,
@@ -18640,6 +19035,7 @@ var OPENCODE_TOOL_NAMES = [
18640
19035
  ];
18641
19036
  var PI_TOOL_NAMES = [
18642
19037
  TOOL_NAME.CODEBASE_CONTEXT,
19038
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18643
19039
  TOOL_NAME.CODEBASE_SEARCH,
18644
19040
  TOOL_NAME.CODEBASE_PEEK,
18645
19041
  TOOL_NAME.FIND_SIMILAR,
@@ -19094,6 +19490,7 @@ var plugin = async ({ directory, worktree }) => {
19094
19490
  return {
19095
19491
  tool: {
19096
19492
  [TOOL_NAME.CODEBASE_CONTEXT]: codebase_context,
19493
+ [TOOL_NAME.CODEBASE_EDIT_CONTEXT]: codebase_edit_context,
19097
19494
  [TOOL_NAME.CODEBASE_SEARCH]: codebase_search,
19098
19495
  [TOOL_NAME.CODEBASE_PEEK]: codebase_peek,
19099
19496
  [TOOL_NAME.INDEX_CODEBASE]: index_codebase,