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.js CHANGED
@@ -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";
@@ -9021,6 +9043,29 @@ function extractPrimaryIdentifierQueryHint(query) {
9021
9043
  const best = codeTerms.find((term) => term.length >= 6);
9022
9044
  return best ?? null;
9023
9045
  }
9046
+ function pathSegmentsForAffinityMatch(filePath) {
9047
+ const normalizedPath2 = normalizeRankingText(filePath).replace(/\\/g, "/");
9048
+ const segments = normalizedPath2.split("/").filter((segment) => segment.length > 0);
9049
+ if (segments.length === 0) {
9050
+ return [];
9051
+ }
9052
+ const basename9 = segments[segments.length - 1] ?? "";
9053
+ const basenameWithoutExt = basename9.replace(/\.[^/.]+$/u, "");
9054
+ const normalizedSegments = segments.map((segment) => segment.toLowerCase());
9055
+ return Array.from(/* @__PURE__ */ new Set([
9056
+ ...normalizedSegments,
9057
+ basenameWithoutExt.toLowerCase()
9058
+ ]));
9059
+ }
9060
+ function hasModuleAffinity(filePath, exactIdentifierVariants) {
9061
+ const haystack = pathSegmentsForAffinityMatch(filePath);
9062
+ return exactIdentifierVariants.some((variant) => {
9063
+ if (!variant || variant.length < 2) {
9064
+ return false;
9065
+ }
9066
+ return haystack.includes(variant);
9067
+ });
9068
+ }
9024
9069
  var FILE_PATH_HINT_EXTENSIONS = [
9025
9070
  "ts",
9026
9071
  "tsx",
@@ -9064,9 +9109,9 @@ function normalizeFilePathForHintMatch(filePath) {
9064
9109
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9065
9110
  }
9066
9111
  function pathMatchesHint(filePath, hint) {
9067
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9112
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9068
9113
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9069
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9114
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9070
9115
  }
9071
9116
  function extractFilePathHint(query) {
9072
9117
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -9096,10 +9141,13 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9096
9141
  ).map((candidate) => {
9097
9142
  const nameLower = (candidate.metadata.name ?? "").toLowerCase();
9098
9143
  const pathLower = candidate.metadata.filePath.toLowerCase();
9099
- let maxMatch = 0;
9100
- const nameMatchesPrimary = primaryVariants.some(
9144
+ const exactIdentifierVariants = primaryVariants.filter((value) => value.length >= 2);
9145
+ const exactMatch = exactIdentifierVariants.some(
9101
9146
  (variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
9102
9147
  );
9148
+ let maxMatch = 0;
9149
+ const nameMatchesPrimary = exactMatch;
9150
+ const pathAffinity = exactMatch ? hasModuleAffinity(candidate.metadata.filePath, exactIdentifierVariants) : false;
9103
9151
  const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
9104
9152
  for (const hint of hints) {
9105
9153
  const variants = normalizeIdentifierVariants(hint);
@@ -9120,12 +9168,17 @@ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSo
9120
9168
  candidate,
9121
9169
  maxMatch,
9122
9170
  pathMatchesFileHint,
9123
- nameMatchesPrimary
9171
+ nameMatchesPrimary,
9172
+ pathAffinity
9124
9173
  };
9125
9174
  }).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
9126
9175
  const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
9127
9176
  const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
9128
9177
  if (aAnchored !== bAnchored) return bAnchored - aAnchored;
9178
+ if (a.nameMatchesPrimary !== b.nameMatchesPrimary) {
9179
+ return b.nameMatchesPrimary ? 1 : -1;
9180
+ }
9181
+ if (a.pathAffinity !== b.pathAffinity) return b.pathAffinity ? 1 : -1;
9129
9182
  if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
9130
9183
  if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
9131
9184
  return a.candidate.id.localeCompare(b.candidate.id);
@@ -12939,6 +12992,20 @@ var Indexer = class _Indexer {
12939
12992
  requestedLimit = nextLimit;
12940
12993
  }
12941
12994
  }
12995
+ buildCandidateSnapshot(candidate) {
12996
+ return {
12997
+ id: candidate.id,
12998
+ filePath: candidate.metadata.filePath,
12999
+ startLine: candidate.metadata.startLine,
13000
+ endLine: candidate.metadata.endLine,
13001
+ score: candidate.score,
13002
+ chunkType: candidate.metadata.chunkType,
13003
+ name: candidate.metadata.name
13004
+ };
13005
+ }
13006
+ buildCandidateSnapshotList(candidates) {
13007
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
13008
+ }
12942
13009
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12943
13010
  return this.searchCandidatesWithBranchPrefilter(
12944
13011
  initialLimit,
@@ -13130,6 +13197,16 @@ var Indexer = class _Indexer {
13130
13197
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13131
13198
  fusionMs: Math.round(fusionMs * 100) / 100
13132
13199
  });
13200
+ if (options?.trace) {
13201
+ options.trace({
13202
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13203
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13204
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13205
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13206
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13207
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13208
+ });
13209
+ }
13133
13210
  const metadataOnly = options?.metadataOnly ?? false;
13134
13211
  return Promise.all(
13135
13212
  finalResults.map(async (r) => {
@@ -14397,7 +14474,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14397
14474
  definitionIntent: options.definitionIntent,
14398
14475
  blameAuthor: options.blameAuthor,
14399
14476
  blameSha: options.blameSha,
14400
- blameSince: options.blameSince
14477
+ blameSince: options.blameSince,
14478
+ trace: options.trace
14401
14479
  });
14402
14480
  }
14403
14481
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14451,15 +14529,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14451
14529
  return indexer.search(query, options.limit, {
14452
14530
  fileType: options.fileType,
14453
14531
  directory: options.directory,
14454
- definitionIntent: true
14532
+ definitionIntent: true,
14533
+ trace: options.trace
14455
14534
  });
14456
14535
  }
14457
14536
  async function getCallGraphData(projectRoot, host, params) {
14458
14537
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14459
14538
  const root = getProjectRoot(projectRoot, host);
14460
14539
  const indexer = getIndexerForProject(root, host);
14540
+ return getCallGraphDataForIndexer(indexer, root, params);
14541
+ }
14542
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14461
14543
  const symbols = await indexer.getCallGraphSymbols();
14462
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14544
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14463
14545
  const direction = params.direction === "callees" ? "callees" : "callers";
14464
14546
  if (resolution.status !== "resolved") {
14465
14547
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14675,17 +14757,17 @@ async function getIndexLogs(projectRoot, host, args) {
14675
14757
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14676
14758
  const root = getProjectRoot(projectRoot, host);
14677
14759
  const inputPath = knowledgeBasePath.trim();
14678
- const normalizedPath = path20.resolve(
14760
+ const normalizedPath2 = path20.resolve(
14679
14761
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14680
14762
  );
14681
- if (!existsSync12(normalizedPath)) {
14682
- return `Error: Directory does not exist: ${normalizedPath}`;
14763
+ if (!existsSync12(normalizedPath2)) {
14764
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14683
14765
  }
14684
14766
  let realPath;
14685
14767
  try {
14686
- realPath = realpathSync5(normalizedPath);
14768
+ realPath = realpathSync5(normalizedPath2);
14687
14769
  } catch {
14688
- return `Error: Cannot resolve path: ${normalizedPath}`;
14770
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14689
14771
  }
14690
14772
  const blockedPrefixes = [
14691
14773
  "/etc",
@@ -14708,34 +14790,34 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14708
14790
  ];
14709
14791
  for (const prefix of blockedPrefixes) {
14710
14792
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14711
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14793
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14712
14794
  }
14713
14795
  }
14714
14796
  for (const dotDir of sensitiveDotDirs) {
14715
14797
  const sensitiveDir = path20.join(homeDir, dotDir);
14716
14798
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14717
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14799
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14718
14800
  }
14719
14801
  }
14720
14802
  try {
14721
- const stat4 = statSync5(normalizedPath);
14803
+ const stat4 = statSync5(normalizedPath2);
14722
14804
  if (!stat4.isDirectory()) {
14723
- return `Error: Path is not a directory: ${normalizedPath}`;
14805
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14724
14806
  }
14725
14807
  } catch (error) {
14726
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14808
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14727
14809
  }
14728
14810
  const config = loadEditableConfig(root, host);
14729
14811
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14730
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14812
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14731
14813
  if (alreadyExists) {
14732
- return `Knowledge base already configured: ${normalizedPath}`;
14814
+ return `Knowledge base already configured: ${normalizedPath2}`;
14733
14815
  }
14734
- knowledgeBases.push(normalizedPath);
14816
+ knowledgeBases.push(normalizedPath2);
14735
14817
  config.knowledgeBases = knowledgeBases;
14736
14818
  saveConfig(root, config, host);
14737
14819
  refreshIndexerForDirectory(root, host);
14738
- let result = `${normalizedPath}
14820
+ let result = `${normalizedPath2}
14739
14821
  `;
14740
14822
  result += `Total knowledge bases: ${knowledgeBases.length}
14741
14823
  `;
@@ -17048,6 +17130,181 @@ var pr_impact = tool({
17048
17130
  }
17049
17131
  });
17050
17132
 
17133
+ // src/tools/edit-context.ts
17134
+ function edgeLimit(value) {
17135
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
17136
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17137
+ }
17138
+ return Math.min(
17139
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
17140
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
17141
+ );
17142
+ }
17143
+ function normalizedPath(value) {
17144
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
17145
+ }
17146
+ function pathsMatch(left, right) {
17147
+ const normalizedLeft = normalizedPath(left);
17148
+ const normalizedRight = normalizedPath(right);
17149
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
17150
+ }
17151
+ function targetSource(results, resolution) {
17152
+ 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);
17153
+ }
17154
+ function formatSource(result) {
17155
+ const name = result.name ? ` ${result.name}` : "";
17156
+ return [
17157
+ "## Target implementation",
17158
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
17159
+ "```",
17160
+ result.content,
17161
+ "```"
17162
+ ].join("\n");
17163
+ }
17164
+ function formatCallers(edges) {
17165
+ if (edges.length === 0) return "## Direct callers\nNone found.";
17166
+ return [
17167
+ "## Direct callers",
17168
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17169
+ ].join("\n");
17170
+ }
17171
+ function formatCallees(edges, sourceFilePath) {
17172
+ if (edges.length === 0) return "## Direct callees\nNone found.";
17173
+ return [
17174
+ "## Direct callees",
17175
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17176
+ ].join("\n");
17177
+ }
17178
+ function formatResolutionRisk(resolution) {
17179
+ if (resolution.status === "ambiguous") {
17180
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
17181
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
17182
+ }
17183
+ if (resolution.filePath && resolution.totalCandidates > 0) {
17184
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
17185
+ }
17186
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
17187
+ }
17188
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
17189
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
17190
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
17191
+ tokenBudget: tokenBudget ?? void 0,
17192
+ heading: "## Conceptual evidence",
17193
+ maxResults: 5,
17194
+ includeExactSearchHandoff: false,
17195
+ preferImplementationPaths: true
17196
+ });
17197
+ const fitted = fitTextToContextBudget(`${risk}
17198
+
17199
+ ${pack.text}`, tokenBudget ?? void 0);
17200
+ return {
17201
+ text: fitted.text,
17202
+ details: {
17203
+ resolution,
17204
+ tokenBudget: fitted.tokenBudget,
17205
+ tokenEstimate: fitted.tokenEstimate,
17206
+ truncated: fitted.truncated,
17207
+ sourceIncluded: candidateSource.length > 0,
17208
+ callerCount: 0,
17209
+ calleeCount: 0
17210
+ }
17211
+ };
17212
+ }
17213
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
17214
+ const symbol = input.symbol?.trim();
17215
+ if (!symbol) {
17216
+ return fallbackPack(
17217
+ dependencies,
17218
+ input.query,
17219
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
17220
+ "not_requested",
17221
+ input.tokenBudget
17222
+ );
17223
+ }
17224
+ let callersResult;
17225
+ try {
17226
+ callersResult = await dependencies.getCallGraphData({
17227
+ name: symbol,
17228
+ filePath: input.filePath ?? void 0,
17229
+ direction: "callers"
17230
+ });
17231
+ } catch (error) {
17232
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
17233
+ const message = error instanceof Error ? error.message : String(error);
17234
+ return fallbackPack(
17235
+ dependencies,
17236
+ input.query,
17237
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
17238
+ "graph_unavailable",
17239
+ input.tokenBudget,
17240
+ candidates
17241
+ );
17242
+ }
17243
+ if (callersResult.resolution.status !== "resolved") {
17244
+ return fallbackPack(
17245
+ dependencies,
17246
+ input.query,
17247
+ formatResolutionRisk(callersResult.resolution),
17248
+ callersResult.resolution.status,
17249
+ input.tokenBudget
17250
+ );
17251
+ }
17252
+ const resolution = callersResult.resolution;
17253
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
17254
+ dependencies.implementationLookup(symbol, { limit: 10 }),
17255
+ dependencies.getCallGraphData({
17256
+ name: symbol,
17257
+ filePath: input.filePath ?? resolution.filePath,
17258
+ direction: "callees"
17259
+ })
17260
+ ]);
17261
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
17262
+ let graphRisk;
17263
+ let callees = [];
17264
+ if (calleesResult.status === "rejected") {
17265
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
17266
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
17267
+ } else if (calleesResult.value.resolution.status !== "resolved") {
17268
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
17269
+ } else {
17270
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
17271
+ }
17272
+ const source = targetSource(definitionsResult.value, resolution);
17273
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
17274
+ const sourceBudget = Math.max(
17275
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
17276
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
17277
+ );
17278
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
17279
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
17280
+ const fitted = fitTextToContextBudget([
17281
+ `# Pre-edit context for ${resolution.name}`,
17282
+ graphRisk,
17283
+ sourceText,
17284
+ formatCallers(callers),
17285
+ formatCallees(callees, resolution.filePath)
17286
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
17287
+ return {
17288
+ text: fitted.text,
17289
+ details: {
17290
+ resolution: "resolved",
17291
+ tokenBudget: fitted.tokenBudget,
17292
+ tokenEstimate: fitted.tokenEstimate,
17293
+ truncated: fitted.truncated,
17294
+ sourceIncluded: source !== void 0,
17295
+ callerCount: callers.length,
17296
+ calleeCount: callees.length
17297
+ }
17298
+ };
17299
+ }
17300
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
17301
+ return resolveCodebaseEditContextWithDependencies(input, {
17302
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
17303
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
17304
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
17305
+ });
17306
+ }
17307
+
17051
17308
  // src/tools/context-search.ts
17052
17309
  var MIN_CONTEXT_RESULT_LIMIT = 1;
17053
17310
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -17150,6 +17407,27 @@ function buildRecoveryDetails(attempts, successIndex) {
17150
17407
  successfulAttemptIndex: successIndex
17151
17408
  };
17152
17409
  }
17410
+ function serializeAttempts(attempts) {
17411
+ return attempts.map((attempt) => ({
17412
+ kind: attempt.kind,
17413
+ scope: attempt.scope,
17414
+ resultCount: attempt.resultCount,
17415
+ relaxedFields: attempt.relaxedFields
17416
+ }));
17417
+ }
17418
+ function buildSearchDiagnostic(attempt) {
17419
+ if (!attempt) {
17420
+ return void 0;
17421
+ }
17422
+ return {
17423
+ route: attempt.kind,
17424
+ routedQuery: attempt.query,
17425
+ searchQuery: attempt.query,
17426
+ searchScope: attempt.scopeFilter,
17427
+ searchTrace: attempt.searchTrace,
17428
+ contextPackTrace: attempt.contextPackTrace
17429
+ };
17430
+ }
17153
17431
  function trimOrUndefined2(value) {
17154
17432
  const normalized = value?.trim();
17155
17433
  if (!normalized) {
@@ -17189,6 +17467,7 @@ async function resolveSearchContext(input, operations) {
17189
17467
  const hasFilters = Boolean(fileType || directory);
17190
17468
  const relaxedFields = relaxedHintFields(fileType, directory);
17191
17469
  const attempts = [];
17470
+ const attemptStates = [];
17192
17471
  const decisions = {
17193
17472
  inferredDefinitionMiss: false,
17194
17473
  fallbackFromOriginalConceptualToInferred: false,
@@ -17217,8 +17496,20 @@ async function resolveSearchContext(input, operations) {
17217
17496
  if (seenAttempts.has(key)) {
17218
17497
  return [];
17219
17498
  }
17220
- const results = await runAttempt();
17499
+ const attemptState = {
17500
+ kind,
17501
+ scope: describeScope(scope.fileType, scope.directory),
17502
+ resultCount: 0,
17503
+ relaxedFields: [...relaxedFieldsForAttempt],
17504
+ query: attemptQuery,
17505
+ scopeFilter: scope
17506
+ };
17507
+ const results = await runAttempt((trace) => {
17508
+ attemptState.searchTrace = trace;
17509
+ });
17510
+ attemptState.resultCount = results.length;
17221
17511
  seenAttempts.add(key);
17512
+ attemptStates.push(attemptState);
17222
17513
  attempts.push({
17223
17514
  kind,
17224
17515
  scope: describeScope(scope.fileType, scope.directory),
@@ -17238,7 +17529,7 @@ async function resolveSearchContext(input, operations) {
17238
17529
  symbol,
17239
17530
  scope,
17240
17531
  relaxedFieldsForAttempt,
17241
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
17532
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17242
17533
  );
17243
17534
  };
17244
17535
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -17247,13 +17538,23 @@ async function resolveSearchContext(input, operations) {
17247
17538
  searchQuery,
17248
17539
  scope,
17249
17540
  relaxedFieldsForAttempt,
17250
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
17541
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17251
17542
  );
17252
17543
  };
17253
- const toResult = (route, routedQuery, pack) => {
17544
+ const findSuccessfulAttemptState = (route) => {
17545
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
17546
+ const attempt = attemptStates[index];
17547
+ if (attempt.kind === route && attempt.resultCount > 0) {
17548
+ return attempt;
17549
+ }
17550
+ }
17551
+ return void 0;
17552
+ };
17553
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
17254
17554
  const base = packedResult(route, routedQuery, pack);
17255
17555
  const baseDetails = base.details;
17256
17556
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
17557
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
17257
17558
  return {
17258
17559
  text: base.text,
17259
17560
  details: {
@@ -17261,7 +17562,10 @@ async function resolveSearchContext(input, operations) {
17261
17562
  tokenBudget: baseDetails.tokenBudget,
17262
17563
  tokenEstimate: baseDetails.tokenEstimate,
17263
17564
  truncated: false,
17264
- recovery: buildRecoveryDetails(attempts, successIndex)
17565
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
17566
+ ...input.diagnostic && {
17567
+ diagnostic: buildSearchDiagnostic(successState)
17568
+ }
17265
17569
  }
17266
17570
  };
17267
17571
  };
@@ -17275,8 +17579,18 @@ async function resolveSearchContext(input, operations) {
17275
17579
  buildContextPack(scopedDefinitionResults, {
17276
17580
  tokenBudget,
17277
17581
  maxResults: limit,
17278
- heading
17279
- })
17582
+ heading,
17583
+ preserveInputOrder: true,
17584
+ ...input.diagnostic ? {
17585
+ trace: (trace) => {
17586
+ const attemptState = findSuccessfulAttemptState("definition");
17587
+ if (attemptState) {
17588
+ attemptState.contextPackTrace = trace;
17589
+ }
17590
+ }
17591
+ } : void 0
17592
+ }),
17593
+ findSuccessfulAttemptState("definition")
17280
17594
  );
17281
17595
  }
17282
17596
  if (explicitSymbol) {
@@ -17294,8 +17608,18 @@ async function resolveSearchContext(input, operations) {
17294
17608
  buildContextPack(unscopedDefinitionResults, {
17295
17609
  tokenBudget,
17296
17610
  maxResults: limit,
17297
- heading: heading2
17298
- })
17611
+ heading: heading2,
17612
+ preserveInputOrder: true,
17613
+ ...input.diagnostic ? {
17614
+ trace: (trace) => {
17615
+ const attemptState = findSuccessfulAttemptState("definition");
17616
+ if (attemptState) {
17617
+ attemptState.contextPackTrace = trace;
17618
+ }
17619
+ }
17620
+ } : void 0
17621
+ }),
17622
+ findSuccessfulAttemptState("definition")
17299
17623
  );
17300
17624
  }
17301
17625
  }
@@ -17314,7 +17638,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17314
17638
  tokenBudget: heading.tokenBudget,
17315
17639
  tokenEstimate: heading.tokenEstimate,
17316
17640
  truncated: heading.truncated,
17317
- recovery: buildRecoveryDetails(attempts, null)
17641
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17642
+ ...input.diagnostic && {
17643
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17644
+ }
17318
17645
  }
17319
17646
  };
17320
17647
  }
@@ -17354,8 +17681,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17354
17681
  maxResults: limit,
17355
17682
  heading,
17356
17683
  includeExactSearchHandoff: true,
17357
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
17358
- })
17684
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
17685
+ ...input.diagnostic ? {
17686
+ trace: (trace) => {
17687
+ const attemptState = findSuccessfulAttemptState("conceptual");
17688
+ if (attemptState) {
17689
+ attemptState.contextPackTrace = trace;
17690
+ }
17691
+ }
17692
+ } : void 0
17693
+ }),
17694
+ findSuccessfulAttemptState("conceptual")
17359
17695
  );
17360
17696
  }
17361
17697
  }
@@ -17371,7 +17707,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17371
17707
  tokenBudget: fallbackText.tokenBudget,
17372
17708
  tokenEstimate: fallbackText.tokenEstimate,
17373
17709
  truncated: fallbackText.truncated,
17374
- recovery: buildRecoveryDetails(attempts, null)
17710
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17711
+ ...input.diagnostic && {
17712
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17713
+ }
17375
17714
  }
17376
17715
  };
17377
17716
  }
@@ -17452,17 +17791,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
17452
17791
  details: fittedDetails("path", fitted, 0)
17453
17792
  };
17454
17793
  }
17455
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
17456
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
17794
+ return resolveSearchContext({
17795
+ query: input.query,
17796
+ symbol,
17797
+ limit,
17798
+ tokenBudget,
17799
+ fileType,
17800
+ directory,
17801
+ diagnostic: input.diagnostic
17802
+ }, {
17803
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
17457
17804
  limit: retrievalLimit,
17458
17805
  fileType: scope.fileType,
17459
- directory: scope.directory
17806
+ directory: scope.directory,
17807
+ trace
17460
17808
  }),
17461
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
17809
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
17462
17810
  limit: retrievalLimit,
17463
17811
  fileType: scope.fileType,
17464
17812
  directory: scope.directory,
17465
- metadataOnly: true
17813
+ metadataOnly: true,
17814
+ trace
17466
17815
  })
17467
17816
  });
17468
17817
  }
@@ -17529,7 +17878,11 @@ async function resolveCodebaseContext(projectRoot, host, input) {
17529
17878
 
17530
17879
  // src/tools/execute-common.ts
17531
17880
  async function executeCodebaseContext(projectRoot, host, args) {
17532
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17881
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17882
+ return { text: result.text, details: result.details };
17883
+ }
17884
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17885
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17533
17886
  }
17534
17887
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17535
17888
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17632,9 +17985,9 @@ function parseGitActivity(output) {
17632
17985
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
17633
17986
  const churn = Number(addedRaw) + Number(deletedRaw);
17634
17987
  if (!Number.isFinite(churn) || churn <= 0) continue;
17635
- const normalizedPath = normalizePath3(filePath);
17636
- const previous = activity.get(normalizedPath);
17637
- activity.set(normalizedPath, {
17988
+ const normalizedPath2 = normalizePath3(filePath);
17989
+ const previous = activity.get(normalizedPath2);
17990
+ activity.set(normalizedPath2, {
17638
17991
  churn: (previous?.churn ?? 0) + churn,
17639
17992
  commits: (previous?.commits ?? 0) + 1,
17640
17993
  latestDate: previous?.latestDate ?? latestDate,
@@ -18224,8 +18577,8 @@ function transformForVisualization(symbols, edges, options = {}) {
18224
18577
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
18225
18578
  filteredSymbols = symbols.filter(
18226
18579
  (s) => {
18227
- const normalizedPath = s.filePath.replace(/\\/g, "/");
18228
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
18580
+ const normalizedPath2 = s.filePath.replace(/\\/g, "/");
18581
+ return normalizedPath2 === normalizedDir || normalizedPath2.startsWith(normalizedDirWithSlash) || normalizedPath2.endsWith(`/${normalizedDir}`) || normalizedPath2.includes(normalizedAbsoluteSuffix);
18229
18582
  }
18230
18583
  );
18231
18584
  }
@@ -18299,6 +18652,26 @@ var z3 = tool.schema;
18299
18652
  var DEFAULT_HOST = "opencode";
18300
18653
  var CHUNK_TYPE_VALUES = CHUNK_TYPES;
18301
18654
  var RELATIONSHIP_TYPE_VALUES = RELATIONSHIP_TYPES;
18655
+ function stableSortedDiagnosticValue(value) {
18656
+ if (Array.isArray(value)) {
18657
+ return value.map((item) => stableSortedDiagnosticValue(item));
18658
+ }
18659
+ if (value === null || typeof value !== "object") {
18660
+ return value;
18661
+ }
18662
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
18663
+ const sorted = {};
18664
+ for (const [key, item] of entries) {
18665
+ sorted[key] = stableSortedDiagnosticValue(item);
18666
+ }
18667
+ return sorted;
18668
+ }
18669
+ function formatCodebaseContextDiagnostic(details) {
18670
+ const sorted = stableSortedDiagnosticValue(details);
18671
+ return `
18672
+ Diagnostics:
18673
+ ${JSON.stringify(sorted, null, 2)}`;
18674
+ }
18302
18675
  function initializeTools2(projectRoot, config) {
18303
18676
  initializeTools(projectRoot, config, DEFAULT_HOST);
18304
18677
  }
@@ -18318,10 +18691,29 @@ var codebase_context = tool({
18318
18691
  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})`),
18319
18692
  fileType: z3.string().nullable().optional().describe("Filter by file extension"),
18320
18693
  directory: z3.string().nullable().optional().describe("Filter by directory path"),
18321
- 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})`)
18694
+ 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})`),
18695
+ diagnostic: z3.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
18696
+ },
18697
+ async execute(args, context) {
18698
+ const result = await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args);
18699
+ if (!args.diagnostic || !result.details?.diagnostic) {
18700
+ return result.text;
18701
+ }
18702
+ return `${result.text}${formatCodebaseContextDiagnostic(result.details.diagnostic)}`;
18703
+ }
18704
+ });
18705
+ var codebase_edit_context = tool({
18706
+ 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.",
18707
+ args: {
18708
+ query: z3.string().describe("The requested change or target behavior"),
18709
+ symbol: z3.string().nullable().optional().describe("Authoritative target symbol when known"),
18710
+ filePath: z3.string().nullable().optional().describe("Optional file path used to disambiguate duplicate symbol names"),
18711
+ 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),
18712
+ 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),
18713
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
18322
18714
  },
18323
18715
  async execute(args, context) {
18324
- return (await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args)).text;
18716
+ return (await executeCodebaseEditContext(context?.worktree, DEFAULT_HOST, args)).text;
18325
18717
  }
18326
18718
  });
18327
18719
  var codebase_peek = tool({
@@ -18578,6 +18970,7 @@ var index_visualize = tool({
18578
18970
  // src/tools/tool-names.ts
18579
18971
  var TOOL_NAME = {
18580
18972
  CODEBASE_CONTEXT: "codebase_context",
18973
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
18581
18974
  CODEBASE_SEARCH: "codebase_search",
18582
18975
  CODEBASE_PEEK: "codebase_peek",
18583
18976
  FIND_SIMILAR: "find_similar",
@@ -18601,6 +18994,7 @@ var TOOL_NAME = {
18601
18994
  };
18602
18995
  var PORTABLE_TOOL_NAMES = [
18603
18996
  TOOL_NAME.CODEBASE_CONTEXT,
18997
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18604
18998
  TOOL_NAME.CODEBASE_SEARCH,
18605
18999
  TOOL_NAME.CODEBASE_PEEK,
18606
19000
  TOOL_NAME.INDEX_CODEBASE,
@@ -18617,6 +19011,7 @@ var PORTABLE_TOOL_NAMES = [
18617
19011
  ];
18618
19012
  var OPENCODE_TOOL_NAMES = [
18619
19013
  TOOL_NAME.CODEBASE_CONTEXT,
19014
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18620
19015
  TOOL_NAME.CODEBASE_SEARCH,
18621
19016
  TOOL_NAME.CODEBASE_PEEK,
18622
19017
  TOOL_NAME.INDEX_CODEBASE,
@@ -18637,6 +19032,7 @@ var OPENCODE_TOOL_NAMES = [
18637
19032
  ];
18638
19033
  var PI_TOOL_NAMES = [
18639
19034
  TOOL_NAME.CODEBASE_CONTEXT,
19035
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18640
19036
  TOOL_NAME.CODEBASE_SEARCH,
18641
19037
  TOOL_NAME.CODEBASE_PEEK,
18642
19038
  TOOL_NAME.FIND_SIMILAR,
@@ -19090,6 +19486,7 @@ var plugin = async ({ directory, worktree }) => {
19090
19486
  return {
19091
19487
  tool: {
19092
19488
  [TOOL_NAME.CODEBASE_CONTEXT]: codebase_context,
19489
+ [TOOL_NAME.CODEBASE_EDIT_CONTEXT]: codebase_edit_context,
19093
19490
  [TOOL_NAME.CODEBASE_SEARCH]: codebase_search,
19094
19491
  [TOOL_NAME.CODEBASE_PEEK]: codebase_peek,
19095
19492
  [TOOL_NAME.INDEX_CODEBASE]: index_codebase,