open-codebase-index 0.22.3 → 0.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -9064,9 +9086,9 @@ function normalizeFilePathForHintMatch(filePath) {
9064
9086
  return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
9065
9087
  }
9066
9088
  function pathMatchesHint(filePath, hint) {
9067
- const normalizedPath = normalizeFilePathForHintMatch(filePath);
9089
+ const normalizedPath2 = normalizeFilePathForHintMatch(filePath);
9068
9090
  const normalizedHint = normalizeFilePathForHintMatch(hint);
9069
- return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
9091
+ return normalizedPath2.endsWith(normalizedHint) || normalizedPath2.includes(`/${normalizedHint}`) || normalizedPath2.includes(normalizedHint);
9070
9092
  }
9071
9093
  function extractFilePathHint(query) {
9072
9094
  const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
@@ -12939,6 +12961,20 @@ var Indexer = class _Indexer {
12939
12961
  requestedLimit = nextLimit;
12940
12962
  }
12941
12963
  }
12964
+ buildCandidateSnapshot(candidate) {
12965
+ return {
12966
+ id: candidate.id,
12967
+ filePath: candidate.metadata.filePath,
12968
+ startLine: candidate.metadata.startLine,
12969
+ endLine: candidate.metadata.endLine,
12970
+ score: candidate.score,
12971
+ chunkType: candidate.metadata.chunkType,
12972
+ name: candidate.metadata.name
12973
+ };
12974
+ }
12975
+ buildCandidateSnapshotList(candidates) {
12976
+ return candidates.map((candidate) => this.buildCandidateSnapshot(candidate));
12977
+ }
12942
12978
  searchSemanticCandidates(store, embedding, initialLimit, branchChunkIds, shouldPrefilterByBranch) {
12943
12979
  return this.searchCandidatesWithBranchPrefilter(
12944
12980
  initialLimit,
@@ -13130,6 +13166,16 @@ var Indexer = class _Indexer {
13130
13166
  prefilterMs: Math.round(prefilterMs * 100) / 100,
13131
13167
  fusionMs: Math.round(fusionMs * 100) / 100
13132
13168
  });
13169
+ if (options?.trace) {
13170
+ options.trace({
13171
+ semanticCandidates: this.buildCandidateSnapshotList(scopedSemanticCandidates),
13172
+ keywordCandidates: this.buildCandidateSnapshotList(scopedKeywordCandidates),
13173
+ hybridCandidates: this.buildCandidateSnapshotList(combined),
13174
+ postExternalRerankCandidates: this.buildCandidateSnapshotList(rerankedCombined),
13175
+ tieredCandidates: this.buildCandidateSnapshotList(tiered),
13176
+ finalCandidates: this.buildCandidateSnapshotList(finalResults)
13177
+ });
13178
+ }
13133
13179
  const metadataOnly = options?.metadataOnly ?? false;
13134
13180
  return Promise.all(
13135
13181
  finalResults.map(async (r) => {
@@ -14397,7 +14443,8 @@ async function searchCodebase(projectRoot, host, query, options = {}) {
14397
14443
  definitionIntent: options.definitionIntent,
14398
14444
  blameAuthor: options.blameAuthor,
14399
14445
  blameSha: options.blameSha,
14400
- blameSince: options.blameSince
14446
+ blameSince: options.blameSince,
14447
+ trace: options.trace
14401
14448
  });
14402
14449
  }
14403
14450
  async function searchCodebaseWithEffectiveness(projectRoot, host, route, query, options, render) {
@@ -14451,15 +14498,19 @@ async function implementationLookup(projectRoot, host, query, options = {}) {
14451
14498
  return indexer.search(query, options.limit, {
14452
14499
  fileType: options.fileType,
14453
14500
  directory: options.directory,
14454
- definitionIntent: true
14501
+ definitionIntent: true,
14502
+ trace: options.trace
14455
14503
  });
14456
14504
  }
14457
14505
  async function getCallGraphData(projectRoot, host, params) {
14458
14506
  await ensureAutoIndexReadyForRetrieval(projectRoot, host);
14459
14507
  const root = getProjectRoot(projectRoot, host);
14460
14508
  const indexer = getIndexerForProject(root, host);
14509
+ return getCallGraphDataForIndexer(indexer, root, params);
14510
+ }
14511
+ async function getCallGraphDataForIndexer(indexer, projectRoot, params) {
14461
14512
  const symbols = await indexer.getCallGraphSymbols();
14462
- const resolution = resolveCallGraphSymbol(symbols, root, params.name, params.filePath, params.symbolId);
14513
+ const resolution = resolveCallGraphSymbol(symbols, projectRoot, params.name, params.filePath, params.symbolId);
14463
14514
  const direction = params.direction === "callees" ? "callees" : "callers";
14464
14515
  if (resolution.status !== "resolved") {
14465
14516
  return { direction, resolution, callers: [], callees: [], relationshipType: params.relationshipType };
@@ -14675,17 +14726,17 @@ async function getIndexLogs(projectRoot, host, args) {
14675
14726
  function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14676
14727
  const root = getProjectRoot(projectRoot, host);
14677
14728
  const inputPath = knowledgeBasePath.trim();
14678
- const normalizedPath = path20.resolve(
14729
+ const normalizedPath2 = path20.resolve(
14679
14730
  path20.isAbsolute(inputPath) ? inputPath : resolveKnowledgeBasePath(inputPath, root)
14680
14731
  );
14681
- if (!existsSync12(normalizedPath)) {
14682
- return `Error: Directory does not exist: ${normalizedPath}`;
14732
+ if (!existsSync12(normalizedPath2)) {
14733
+ return `Error: Directory does not exist: ${normalizedPath2}`;
14683
14734
  }
14684
14735
  let realPath;
14685
14736
  try {
14686
- realPath = realpathSync5(normalizedPath);
14737
+ realPath = realpathSync5(normalizedPath2);
14687
14738
  } catch {
14688
- return `Error: Cannot resolve path: ${normalizedPath}`;
14739
+ return `Error: Cannot resolve path: ${normalizedPath2}`;
14689
14740
  }
14690
14741
  const blockedPrefixes = [
14691
14742
  "/etc",
@@ -14708,34 +14759,34 @@ function addKnowledgeBase(projectRoot, host, knowledgeBasePath) {
14708
14759
  ];
14709
14760
  for (const prefix of blockedPrefixes) {
14710
14761
  if (realPath === prefix || realPath.startsWith(`${prefix}/`)) {
14711
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14762
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14712
14763
  }
14713
14764
  }
14714
14765
  for (const dotDir of sensitiveDotDirs) {
14715
14766
  const sensitiveDir = path20.join(homeDir, dotDir);
14716
14767
  if (sensitiveDir && (realPath === sensitiveDir || realPath.startsWith(`${sensitiveDir}/`))) {
14717
- return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath}`;
14768
+ return `Error: Adding sensitive directory as knowledge base is not allowed: ${normalizedPath2}`;
14718
14769
  }
14719
14770
  }
14720
14771
  try {
14721
- const stat4 = statSync5(normalizedPath);
14772
+ const stat4 = statSync5(normalizedPath2);
14722
14773
  if (!stat4.isDirectory()) {
14723
- return `Error: Path is not a directory: ${normalizedPath}`;
14774
+ return `Error: Path is not a directory: ${normalizedPath2}`;
14724
14775
  }
14725
14776
  } catch (error) {
14726
- return `Error: Cannot access directory: ${normalizedPath} - ${error instanceof Error ? error.message : String(error)}`;
14777
+ return `Error: Cannot access directory: ${normalizedPath2} - ${error instanceof Error ? error.message : String(error)}`;
14727
14778
  }
14728
14779
  const config = loadEditableConfig(root, host);
14729
14780
  const knowledgeBases = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : [];
14730
- const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath, root);
14781
+ const alreadyExists = hasMatchingKnowledgeBasePath(knowledgeBases, normalizedPath2, root);
14731
14782
  if (alreadyExists) {
14732
- return `Knowledge base already configured: ${normalizedPath}`;
14783
+ return `Knowledge base already configured: ${normalizedPath2}`;
14733
14784
  }
14734
- knowledgeBases.push(normalizedPath);
14785
+ knowledgeBases.push(normalizedPath2);
14735
14786
  config.knowledgeBases = knowledgeBases;
14736
14787
  saveConfig(root, config, host);
14737
14788
  refreshIndexerForDirectory(root, host);
14738
- let result = `${normalizedPath}
14789
+ let result = `${normalizedPath2}
14739
14790
  `;
14740
14791
  result += `Total knowledge bases: ${knowledgeBases.length}
14741
14792
  `;
@@ -17048,6 +17099,181 @@ var pr_impact = tool({
17048
17099
  }
17049
17100
  });
17050
17101
 
17102
+ // src/tools/edit-context.ts
17103
+ function edgeLimit(value) {
17104
+ if (value === null || value === void 0 || !Number.isFinite(value)) {
17105
+ return DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT;
17106
+ }
17107
+ return Math.min(
17108
+ MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT,
17109
+ Math.max(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT, Math.floor(value))
17110
+ );
17111
+ }
17112
+ function normalizedPath(value) {
17113
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
17114
+ }
17115
+ function pathsMatch(left, right) {
17116
+ const normalizedLeft = normalizedPath(left);
17117
+ const normalizedRight = normalizedPath(right);
17118
+ return normalizedLeft === normalizedRight || normalizedLeft.endsWith(`/${normalizedRight}`) || normalizedRight.endsWith(`/${normalizedLeft}`);
17119
+ }
17120
+ function targetSource(results, resolution) {
17121
+ return results.find((result) => pathsMatch(result.filePath, resolution.filePath) && result.startLine <= resolution.startLine && result.endLine >= resolution.startLine) ?? results.find((result) => pathsMatch(result.filePath, resolution.filePath) && result.name === resolution.name);
17122
+ }
17123
+ function formatSource(result) {
17124
+ const name = result.name ? ` ${result.name}` : "";
17125
+ return [
17126
+ "## Target implementation",
17127
+ `${result.filePath}:${result.startLine}-${result.endLine} (${result.chunkType}${name})`,
17128
+ "```",
17129
+ result.content,
17130
+ "```"
17131
+ ].join("\n");
17132
+ }
17133
+ function formatCallers(edges) {
17134
+ if (edges.length === 0) return "## Direct callers\nNone found.";
17135
+ return [
17136
+ "## Direct callers",
17137
+ ...edges.map((edge) => `- ${edge.fromSymbolName ?? "<unknown>"} at ${edge.fromSymbolFilePath ?? "<unknown file>"}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17138
+ ].join("\n");
17139
+ }
17140
+ function formatCallees(edges, sourceFilePath) {
17141
+ if (edges.length === 0) return "## Direct callees\nNone found.";
17142
+ return [
17143
+ "## Direct callees",
17144
+ ...edges.map((edge) => `- ${edge.targetName} from ${sourceFilePath}:${edge.line} (${edge.callType}, ${edge.isResolved ? "resolved" : "unresolved"})`)
17145
+ ].join("\n");
17146
+ }
17147
+ function formatResolutionRisk(resolution) {
17148
+ if (resolution.status === "ambiguous") {
17149
+ const candidates = resolution.candidates.map((candidate) => `${candidate.filePath}:${candidate.startLine}`).join(", ");
17150
+ return `Risk: symbol "${resolution.name}" is ambiguous. Pass filePath to select a target.${candidates ? ` Candidates: ${candidates}.` : ""}`;
17151
+ }
17152
+ if (resolution.filePath && resolution.totalCandidates > 0) {
17153
+ return `Risk: symbol "${resolution.name}" did not resolve at filePath="${resolution.filePath}". Review the conceptual evidence before editing.`;
17154
+ }
17155
+ return `Risk: symbol "${resolution.name}" could not be resolved. Review the conceptual evidence before editing.`;
17156
+ }
17157
+ async function fallbackPack(dependencies, query, risk, resolution, tokenBudget, candidateSource = []) {
17158
+ const conceptual = await dependencies.searchCodebase(query, { limit: 5 });
17159
+ const pack = buildContextPack([...candidateSource, ...conceptual], {
17160
+ tokenBudget: tokenBudget ?? void 0,
17161
+ heading: "## Conceptual evidence",
17162
+ maxResults: 5,
17163
+ includeExactSearchHandoff: false,
17164
+ preferImplementationPaths: true
17165
+ });
17166
+ const fitted = fitTextToContextBudget(`${risk}
17167
+
17168
+ ${pack.text}`, tokenBudget ?? void 0);
17169
+ return {
17170
+ text: fitted.text,
17171
+ details: {
17172
+ resolution,
17173
+ tokenBudget: fitted.tokenBudget,
17174
+ tokenEstimate: fitted.tokenEstimate,
17175
+ truncated: fitted.truncated,
17176
+ sourceIncluded: candidateSource.length > 0,
17177
+ callerCount: 0,
17178
+ calleeCount: 0
17179
+ }
17180
+ };
17181
+ }
17182
+ async function resolveCodebaseEditContextWithDependencies(input, dependencies) {
17183
+ const symbol = input.symbol?.trim();
17184
+ if (!symbol) {
17185
+ return fallbackPack(
17186
+ dependencies,
17187
+ input.query,
17188
+ "Risk: no authoritative symbol was supplied. Review the conceptual evidence before editing.",
17189
+ "not_requested",
17190
+ input.tokenBudget
17191
+ );
17192
+ }
17193
+ let callersResult;
17194
+ try {
17195
+ callersResult = await dependencies.getCallGraphData({
17196
+ name: symbol,
17197
+ filePath: input.filePath ?? void 0,
17198
+ direction: "callers"
17199
+ });
17200
+ } catch (error) {
17201
+ const candidates = await dependencies.implementationLookup(symbol, { limit: 5 });
17202
+ const message = error instanceof Error ? error.message : String(error);
17203
+ return fallbackPack(
17204
+ dependencies,
17205
+ input.query,
17206
+ `Risk: graph data is unavailable (${message}). Target and dependencies are not graph-verified.`,
17207
+ "graph_unavailable",
17208
+ input.tokenBudget,
17209
+ candidates
17210
+ );
17211
+ }
17212
+ if (callersResult.resolution.status !== "resolved") {
17213
+ return fallbackPack(
17214
+ dependencies,
17215
+ input.query,
17216
+ formatResolutionRisk(callersResult.resolution),
17217
+ callersResult.resolution.status,
17218
+ input.tokenBudget
17219
+ );
17220
+ }
17221
+ const resolution = callersResult.resolution;
17222
+ const [definitionsResult, calleesResult] = await Promise.allSettled([
17223
+ dependencies.implementationLookup(symbol, { limit: 10 }),
17224
+ dependencies.getCallGraphData({
17225
+ name: symbol,
17226
+ filePath: input.filePath ?? resolution.filePath,
17227
+ direction: "callees"
17228
+ })
17229
+ ]);
17230
+ if (definitionsResult.status === "rejected") throw definitionsResult.reason;
17231
+ let graphRisk;
17232
+ let callees = [];
17233
+ if (calleesResult.status === "rejected") {
17234
+ const message = calleesResult.reason instanceof Error ? calleesResult.reason.message : String(calleesResult.reason);
17235
+ graphRisk = `Risk: callee graph data is unavailable (${message}). Dependency evidence is incomplete.`;
17236
+ } else if (calleesResult.value.resolution.status !== "resolved") {
17237
+ graphRisk = "Risk: the target resolved for callers but not callees. Dependency evidence is incomplete.";
17238
+ } else {
17239
+ callees = calleesResult.value.callees.slice(0, edgeLimit(input.calleeLimit));
17240
+ }
17241
+ const source = targetSource(definitionsResult.value, resolution);
17242
+ const callers = callersResult.callers.slice(0, edgeLimit(input.callerLimit));
17243
+ const sourceBudget = Math.max(
17244
+ MIN_CONTEXT_PACK_TOKEN_BUDGET,
17245
+ Math.floor((input.tokenBudget ?? DEFAULT_CONTEXT_PACK_TOKEN_BUDGET) * 0.6)
17246
+ );
17247
+ const sourceText = source ? fitTextToContextBudget(formatSource(source), sourceBudget).text : `## Target implementation
17248
+ Risk: no implementation source matched the resolved target at ${resolution.filePath}:${resolution.startLine}.`;
17249
+ const fitted = fitTextToContextBudget([
17250
+ `# Pre-edit context for ${resolution.name}`,
17251
+ graphRisk,
17252
+ sourceText,
17253
+ formatCallers(callers),
17254
+ formatCallees(callees, resolution.filePath)
17255
+ ].filter((section) => section !== void 0).join("\n\n"), input.tokenBudget ?? void 0);
17256
+ return {
17257
+ text: fitted.text,
17258
+ details: {
17259
+ resolution: "resolved",
17260
+ tokenBudget: fitted.tokenBudget,
17261
+ tokenEstimate: fitted.tokenEstimate,
17262
+ truncated: fitted.truncated,
17263
+ sourceIncluded: source !== void 0,
17264
+ callerCount: callers.length,
17265
+ calleeCount: callees.length
17266
+ }
17267
+ };
17268
+ }
17269
+ async function resolveCodebaseEditContext(projectRoot, host, input) {
17270
+ return resolveCodebaseEditContextWithDependencies(input, {
17271
+ searchCodebase: (query, options) => searchCodebase(projectRoot, host, query, options),
17272
+ implementationLookup: (query, options) => implementationLookup(projectRoot, host, query, options),
17273
+ getCallGraphData: (params) => getCallGraphData(projectRoot, host, params)
17274
+ });
17275
+ }
17276
+
17051
17277
  // src/tools/context-search.ts
17052
17278
  var MIN_CONTEXT_RESULT_LIMIT = 1;
17053
17279
  var MAX_CONTEXT_RESULT_LIMIT = 100;
@@ -17150,6 +17376,27 @@ function buildRecoveryDetails(attempts, successIndex) {
17150
17376
  successfulAttemptIndex: successIndex
17151
17377
  };
17152
17378
  }
17379
+ function serializeAttempts(attempts) {
17380
+ return attempts.map((attempt) => ({
17381
+ kind: attempt.kind,
17382
+ scope: attempt.scope,
17383
+ resultCount: attempt.resultCount,
17384
+ relaxedFields: attempt.relaxedFields
17385
+ }));
17386
+ }
17387
+ function buildSearchDiagnostic(attempt) {
17388
+ if (!attempt) {
17389
+ return void 0;
17390
+ }
17391
+ return {
17392
+ route: attempt.kind,
17393
+ routedQuery: attempt.query,
17394
+ searchQuery: attempt.query,
17395
+ searchScope: attempt.scopeFilter,
17396
+ searchTrace: attempt.searchTrace,
17397
+ contextPackTrace: attempt.contextPackTrace
17398
+ };
17399
+ }
17153
17400
  function trimOrUndefined2(value) {
17154
17401
  const normalized = value?.trim();
17155
17402
  if (!normalized) {
@@ -17189,6 +17436,7 @@ async function resolveSearchContext(input, operations) {
17189
17436
  const hasFilters = Boolean(fileType || directory);
17190
17437
  const relaxedFields = relaxedHintFields(fileType, directory);
17191
17438
  const attempts = [];
17439
+ const attemptStates = [];
17192
17440
  const decisions = {
17193
17441
  inferredDefinitionMiss: false,
17194
17442
  fallbackFromOriginalConceptualToInferred: false,
@@ -17217,8 +17465,20 @@ async function resolveSearchContext(input, operations) {
17217
17465
  if (seenAttempts.has(key)) {
17218
17466
  return [];
17219
17467
  }
17220
- const results = await runAttempt();
17468
+ const attemptState = {
17469
+ kind,
17470
+ scope: describeScope(scope.fileType, scope.directory),
17471
+ resultCount: 0,
17472
+ relaxedFields: [...relaxedFieldsForAttempt],
17473
+ query: attemptQuery,
17474
+ scopeFilter: scope
17475
+ };
17476
+ const results = await runAttempt((trace) => {
17477
+ attemptState.searchTrace = trace;
17478
+ });
17479
+ attemptState.resultCount = results.length;
17221
17480
  seenAttempts.add(key);
17481
+ attemptStates.push(attemptState);
17222
17482
  attempts.push({
17223
17483
  kind,
17224
17484
  scope: describeScope(scope.fileType, scope.directory),
@@ -17238,7 +17498,7 @@ async function resolveSearchContext(input, operations) {
17238
17498
  symbol,
17239
17499
  scope,
17240
17500
  relaxedFieldsForAttempt,
17241
- () => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope)
17501
+ (trace) => operations.lookup(symbol, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17242
17502
  );
17243
17503
  };
17244
17504
  const tryConceptualSearch = async (searchQuery, scope, relaxedFieldsForAttempt) => {
@@ -17247,13 +17507,23 @@ async function resolveSearchContext(input, operations) {
17247
17507
  searchQuery,
17248
17508
  scope,
17249
17509
  relaxedFieldsForAttempt,
17250
- () => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope)
17510
+ (trace) => operations.search(searchQuery, MAX_CONTEXT_RESULT_LIMIT, scope, input.diagnostic ? trace : void 0)
17251
17511
  );
17252
17512
  };
17253
- const toResult = (route, routedQuery, pack) => {
17513
+ const findSuccessfulAttemptState = (route) => {
17514
+ for (let index = attemptStates.length - 1; index >= 0; index -= 1) {
17515
+ const attempt = attemptStates[index];
17516
+ if (attempt.kind === route && attempt.resultCount > 0) {
17517
+ return attempt;
17518
+ }
17519
+ }
17520
+ return void 0;
17521
+ };
17522
+ const toResult = (route, routedQuery, pack, successfulAttempt) => {
17254
17523
  const base = packedResult(route, routedQuery, pack);
17255
17524
  const baseDetails = base.details;
17256
17525
  const successIndex = findSuccessfulAttemptIndex(route, attempts);
17526
+ const successState = successfulAttempt ?? findSuccessfulAttemptState(route);
17257
17527
  return {
17258
17528
  text: base.text,
17259
17529
  details: {
@@ -17261,7 +17531,10 @@ async function resolveSearchContext(input, operations) {
17261
17531
  tokenBudget: baseDetails.tokenBudget,
17262
17532
  tokenEstimate: baseDetails.tokenEstimate,
17263
17533
  truncated: false,
17264
- recovery: buildRecoveryDetails(attempts, successIndex)
17534
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), successIndex),
17535
+ ...input.diagnostic && {
17536
+ diagnostic: buildSearchDiagnostic(successState)
17537
+ }
17265
17538
  }
17266
17539
  };
17267
17540
  };
@@ -17275,8 +17548,18 @@ async function resolveSearchContext(input, operations) {
17275
17548
  buildContextPack(scopedDefinitionResults, {
17276
17549
  tokenBudget,
17277
17550
  maxResults: limit,
17278
- heading
17279
- })
17551
+ heading,
17552
+ preserveInputOrder: true,
17553
+ ...input.diagnostic ? {
17554
+ trace: (trace) => {
17555
+ const attemptState = findSuccessfulAttemptState("definition");
17556
+ if (attemptState) {
17557
+ attemptState.contextPackTrace = trace;
17558
+ }
17559
+ }
17560
+ } : void 0
17561
+ }),
17562
+ findSuccessfulAttemptState("definition")
17280
17563
  );
17281
17564
  }
17282
17565
  if (explicitSymbol) {
@@ -17294,8 +17577,18 @@ async function resolveSearchContext(input, operations) {
17294
17577
  buildContextPack(unscopedDefinitionResults, {
17295
17578
  tokenBudget,
17296
17579
  maxResults: limit,
17297
- heading: heading2
17298
- })
17580
+ heading: heading2,
17581
+ preserveInputOrder: true,
17582
+ ...input.diagnostic ? {
17583
+ trace: (trace) => {
17584
+ const attemptState = findSuccessfulAttemptState("definition");
17585
+ if (attemptState) {
17586
+ attemptState.contextPackTrace = trace;
17587
+ }
17588
+ }
17589
+ } : void 0
17590
+ }),
17591
+ findSuccessfulAttemptState("definition")
17299
17592
  );
17300
17593
  }
17301
17594
  }
@@ -17314,7 +17607,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17314
17607
  tokenBudget: heading.tokenBudget,
17315
17608
  tokenEstimate: heading.tokenEstimate,
17316
17609
  truncated: heading.truncated,
17317
- recovery: buildRecoveryDetails(attempts, null)
17610
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17611
+ ...input.diagnostic && {
17612
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17613
+ }
17318
17614
  }
17319
17615
  };
17320
17616
  }
@@ -17354,8 +17650,17 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17354
17650
  maxResults: limit,
17355
17651
  heading,
17356
17652
  includeExactSearchHandoff: true,
17357
- preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
17358
- })
17653
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test",
17654
+ ...input.diagnostic ? {
17655
+ trace: (trace) => {
17656
+ const attemptState = findSuccessfulAttemptState("conceptual");
17657
+ if (attemptState) {
17658
+ attemptState.contextPackTrace = trace;
17659
+ }
17660
+ }
17661
+ } : void 0
17662
+ }),
17663
+ findSuccessfulAttemptState("conceptual")
17359
17664
  );
17360
17665
  }
17361
17666
  }
@@ -17371,7 +17676,10 @@ Explicit symbol lookup only; conceptual search was not attempted.`
17371
17676
  tokenBudget: fallbackText.tokenBudget,
17372
17677
  tokenEstimate: fallbackText.tokenEstimate,
17373
17678
  truncated: fallbackText.truncated,
17374
- recovery: buildRecoveryDetails(attempts, null)
17679
+ recovery: buildRecoveryDetails(serializeAttempts(attemptStates), null),
17680
+ ...input.diagnostic && {
17681
+ diagnostic: buildSearchDiagnostic(attemptStates[attemptStates.length - 1])
17682
+ }
17375
17683
  }
17376
17684
  };
17377
17685
  }
@@ -17452,17 +17760,27 @@ async function resolveCodebaseContextUnmeasured(projectRoot, host, input) {
17452
17760
  details: fittedDetails("path", fitted, 0)
17453
17761
  };
17454
17762
  }
17455
- return resolveSearchContext({ query: input.query, symbol, limit, tokenBudget, fileType, directory }, {
17456
- lookup: (lookupSymbol, retrievalLimit, scope) => implementationLookup(projectRoot, host, lookupSymbol, {
17763
+ return resolveSearchContext({
17764
+ query: input.query,
17765
+ symbol,
17766
+ limit,
17767
+ tokenBudget,
17768
+ fileType,
17769
+ directory,
17770
+ diagnostic: input.diagnostic
17771
+ }, {
17772
+ lookup: (lookupSymbol, retrievalLimit, scope, trace) => implementationLookup(projectRoot, host, lookupSymbol, {
17457
17773
  limit: retrievalLimit,
17458
17774
  fileType: scope.fileType,
17459
- directory: scope.directory
17775
+ directory: scope.directory,
17776
+ trace
17460
17777
  }),
17461
- search: (queryText, retrievalLimit, scope) => searchCodebase(projectRoot, host, queryText, {
17778
+ search: (queryText, retrievalLimit, scope, trace) => searchCodebase(projectRoot, host, queryText, {
17462
17779
  limit: retrievalLimit,
17463
17780
  fileType: scope.fileType,
17464
17781
  directory: scope.directory,
17465
- metadataOnly: true
17782
+ metadataOnly: true,
17783
+ trace
17466
17784
  })
17467
17785
  });
17468
17786
  }
@@ -17529,7 +17847,11 @@ async function resolveCodebaseContext(projectRoot, host, input) {
17529
17847
 
17530
17848
  // src/tools/execute-common.ts
17531
17849
  async function executeCodebaseContext(projectRoot, host, args) {
17532
- return { text: (await resolveCodebaseContext(projectRoot, host, args)).text };
17850
+ const result = await resolveCodebaseContext(projectRoot, host, args);
17851
+ return { text: result.text, details: result.details };
17852
+ }
17853
+ async function executeCodebaseEditContext(projectRoot, host, args) {
17854
+ return { text: (await resolveCodebaseEditContext(projectRoot, host, args)).text };
17533
17855
  }
17534
17856
  async function executeIndexCodebase(projectRoot, host, args, onProgress) {
17535
17857
  const result = await runIndexCodebase(projectRoot, host, args, onProgress);
@@ -17632,9 +17954,9 @@ function parseGitActivity(output) {
17632
17954
  if (!filePath || addedRaw === "-" || deletedRaw === "-") continue;
17633
17955
  const churn = Number(addedRaw) + Number(deletedRaw);
17634
17956
  if (!Number.isFinite(churn) || churn <= 0) continue;
17635
- const normalizedPath = normalizePath3(filePath);
17636
- const previous = activity.get(normalizedPath);
17637
- activity.set(normalizedPath, {
17957
+ const normalizedPath2 = normalizePath3(filePath);
17958
+ const previous = activity.get(normalizedPath2);
17959
+ activity.set(normalizedPath2, {
17638
17960
  churn: (previous?.churn ?? 0) + churn,
17639
17961
  commits: (previous?.commits ?? 0) + 1,
17640
17962
  latestDate: previous?.latestDate ?? latestDate,
@@ -18224,8 +18546,8 @@ function transformForVisualization(symbols, edges, options = {}) {
18224
18546
  const normalizedAbsoluteSuffix = `/${normalizedDirWithSlash}`;
18225
18547
  filteredSymbols = symbols.filter(
18226
18548
  (s) => {
18227
- const normalizedPath = s.filePath.replace(/\\/g, "/");
18228
- return normalizedPath === normalizedDir || normalizedPath.startsWith(normalizedDirWithSlash) || normalizedPath.endsWith(`/${normalizedDir}`) || normalizedPath.includes(normalizedAbsoluteSuffix);
18549
+ const normalizedPath2 = s.filePath.replace(/\\/g, "/");
18550
+ return normalizedPath2 === normalizedDir || normalizedPath2.startsWith(normalizedDirWithSlash) || normalizedPath2.endsWith(`/${normalizedDir}`) || normalizedPath2.includes(normalizedAbsoluteSuffix);
18229
18551
  }
18230
18552
  );
18231
18553
  }
@@ -18299,6 +18621,26 @@ var z3 = tool.schema;
18299
18621
  var DEFAULT_HOST = "opencode";
18300
18622
  var CHUNK_TYPE_VALUES = CHUNK_TYPES;
18301
18623
  var RELATIONSHIP_TYPE_VALUES = RELATIONSHIP_TYPES;
18624
+ function stableSortedDiagnosticValue(value) {
18625
+ if (Array.isArray(value)) {
18626
+ return value.map((item) => stableSortedDiagnosticValue(item));
18627
+ }
18628
+ if (value === null || typeof value !== "object") {
18629
+ return value;
18630
+ }
18631
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
18632
+ const sorted = {};
18633
+ for (const [key, item] of entries) {
18634
+ sorted[key] = stableSortedDiagnosticValue(item);
18635
+ }
18636
+ return sorted;
18637
+ }
18638
+ function formatCodebaseContextDiagnostic(details) {
18639
+ const sorted = stableSortedDiagnosticValue(details);
18640
+ return `
18641
+ Diagnostics:
18642
+ ${JSON.stringify(sorted, null, 2)}`;
18643
+ }
18302
18644
  function initializeTools2(projectRoot, config) {
18303
18645
  initializeTools(projectRoot, config, DEFAULT_HOST);
18304
18646
  }
@@ -18318,10 +18660,29 @@ var codebase_context = tool({
18318
18660
  maxDepth: z3.number().int().min(MIN_CONTEXT_PATH_DEPTH).max(MAX_CONTEXT_PATH_DEPTH).nullable().optional().default(10).describe(`Maximum call-path traversal depth (${MIN_CONTEXT_PATH_DEPTH}-${MAX_CONTEXT_PATH_DEPTH})`),
18319
18661
  fileType: z3.string().nullable().optional().describe("Filter by file extension"),
18320
18662
  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})`)
18663
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET).describe(`Maximum response tokens (${MIN_CONTEXT_PACK_TOKEN_BUDGET}-${MAX_CONTEXT_PACK_TOKEN_BUDGET})`),
18664
+ diagnostic: z3.boolean().optional().describe("Collect diagnostic routing and search traces without changing normal text output.")
18665
+ },
18666
+ async execute(args, context) {
18667
+ const result = await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args);
18668
+ if (!args.diagnostic || !result.details?.diagnostic) {
18669
+ return result.text;
18670
+ }
18671
+ return `${result.text}${formatCodebaseContextDiagnostic(result.details.diagnostic)}`;
18672
+ }
18673
+ });
18674
+ var codebase_edit_context = tool({
18675
+ description: "PRE-EDIT TOOL for a known or suspected symbol. Returns token-bounded target source, direct callers and callees, or a risk-marked conceptual fallback when the target cannot be resolved.",
18676
+ args: {
18677
+ query: z3.string().describe("The requested change or target behavior"),
18678
+ symbol: z3.string().nullable().optional().describe("Authoritative target symbol when known"),
18679
+ filePath: z3.string().nullable().optional().describe("Optional file path used to disambiguate duplicate symbol names"),
18680
+ callerLimit: z3.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).nullable().optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT),
18681
+ calleeLimit: z3.number().int().min(MIN_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).max(MAX_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT).nullable().optional().default(DEFAULT_CODEBASE_EDIT_CONTEXT_EDGE_LIMIT),
18682
+ tokenBudget: z3.number().int().min(MIN_CONTEXT_PACK_TOKEN_BUDGET).max(MAX_CONTEXT_PACK_TOKEN_BUDGET).nullable().optional().default(DEFAULT_CONTEXT_PACK_TOKEN_BUDGET)
18322
18683
  },
18323
18684
  async execute(args, context) {
18324
- return (await executeCodebaseContext(context?.worktree, DEFAULT_HOST, args)).text;
18685
+ return (await executeCodebaseEditContext(context?.worktree, DEFAULT_HOST, args)).text;
18325
18686
  }
18326
18687
  });
18327
18688
  var codebase_peek = tool({
@@ -18578,6 +18939,7 @@ var index_visualize = tool({
18578
18939
  // src/tools/tool-names.ts
18579
18940
  var TOOL_NAME = {
18580
18941
  CODEBASE_CONTEXT: "codebase_context",
18942
+ CODEBASE_EDIT_CONTEXT: "codebase_edit_context",
18581
18943
  CODEBASE_SEARCH: "codebase_search",
18582
18944
  CODEBASE_PEEK: "codebase_peek",
18583
18945
  FIND_SIMILAR: "find_similar",
@@ -18601,6 +18963,7 @@ var TOOL_NAME = {
18601
18963
  };
18602
18964
  var PORTABLE_TOOL_NAMES = [
18603
18965
  TOOL_NAME.CODEBASE_CONTEXT,
18966
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18604
18967
  TOOL_NAME.CODEBASE_SEARCH,
18605
18968
  TOOL_NAME.CODEBASE_PEEK,
18606
18969
  TOOL_NAME.INDEX_CODEBASE,
@@ -18617,6 +18980,7 @@ var PORTABLE_TOOL_NAMES = [
18617
18980
  ];
18618
18981
  var OPENCODE_TOOL_NAMES = [
18619
18982
  TOOL_NAME.CODEBASE_CONTEXT,
18983
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18620
18984
  TOOL_NAME.CODEBASE_SEARCH,
18621
18985
  TOOL_NAME.CODEBASE_PEEK,
18622
18986
  TOOL_NAME.INDEX_CODEBASE,
@@ -18637,6 +19001,7 @@ var OPENCODE_TOOL_NAMES = [
18637
19001
  ];
18638
19002
  var PI_TOOL_NAMES = [
18639
19003
  TOOL_NAME.CODEBASE_CONTEXT,
19004
+ TOOL_NAME.CODEBASE_EDIT_CONTEXT,
18640
19005
  TOOL_NAME.CODEBASE_SEARCH,
18641
19006
  TOOL_NAME.CODEBASE_PEEK,
18642
19007
  TOOL_NAME.FIND_SIMILAR,
@@ -19090,6 +19455,7 @@ var plugin = async ({ directory, worktree }) => {
19090
19455
  return {
19091
19456
  tool: {
19092
19457
  [TOOL_NAME.CODEBASE_CONTEXT]: codebase_context,
19458
+ [TOOL_NAME.CODEBASE_EDIT_CONTEXT]: codebase_edit_context,
19093
19459
  [TOOL_NAME.CODEBASE_SEARCH]: codebase_search,
19094
19460
  [TOOL_NAME.CODEBASE_PEEK]: codebase_peek,
19095
19461
  [TOOL_NAME.INDEX_CODEBASE]: index_codebase,