opencode-codebase-index 0.19.0 → 0.20.0

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/cli.cjs CHANGED
@@ -793,6 +793,7 @@ function getDefaultIndexingConfig() {
793
793
  autoIndexMaxRetries: 5,
794
794
  autoIndexRetryDelayMs: 100,
795
795
  watchFiles: true,
796
+ pauseBackgroundIndexingOnBattery: false,
796
797
  maxFileSize: 1048576,
797
798
  maxChunksPerFile: 100,
798
799
  semanticOnly: false,
@@ -924,6 +925,7 @@ function parseConfig(raw) {
924
925
  autoIndexMaxRetries: typeof rawIndexing.autoIndexMaxRetries === "number" ? Math.min(10, Math.max(0, Math.floor(rawIndexing.autoIndexMaxRetries))) : defaultIndexing.autoIndexMaxRetries,
925
926
  autoIndexRetryDelayMs: typeof rawIndexing.autoIndexRetryDelayMs === "number" ? Math.min(1e4, Math.max(10, Math.floor(rawIndexing.autoIndexRetryDelayMs))) : defaultIndexing.autoIndexRetryDelayMs,
926
927
  watchFiles: typeof rawIndexing.watchFiles === "boolean" ? rawIndexing.watchFiles : defaultIndexing.watchFiles,
928
+ pauseBackgroundIndexingOnBattery: typeof rawIndexing.pauseBackgroundIndexingOnBattery === "boolean" ? rawIndexing.pauseBackgroundIndexingOnBattery : defaultIndexing.pauseBackgroundIndexingOnBattery,
927
929
  maxFileSize: typeof rawIndexing.maxFileSize === "number" ? rawIndexing.maxFileSize : defaultIndexing.maxFileSize,
928
930
  maxChunksPerFile: typeof rawIndexing.maxChunksPerFile === "number" ? Math.max(1, rawIndexing.maxChunksPerFile) : defaultIndexing.maxChunksPerFile,
929
931
  semanticOnly: typeof rawIndexing.semanticOnly === "boolean" ? rawIndexing.semanticOnly : defaultIndexing.semanticOnly,
@@ -1097,6 +1099,18 @@ function metricDelta(current, baseline) {
1097
1099
  };
1098
1100
  }
1099
1101
  function compareSummaries(current, baseline, againstPath) {
1102
+ const hasCurrentFingerprint = current.datasetFingerprint !== void 0;
1103
+ const hasBaselineFingerprint = baseline.datasetFingerprint !== void 0;
1104
+ if (hasCurrentFingerprint !== hasBaselineFingerprint) {
1105
+ throw new Error(
1106
+ `Cannot compare evaluation summaries with mismatched dataset fingerprint presence: current=${hasCurrentFingerprint ? "present" : "missing"}, baseline=${hasBaselineFingerprint ? "present" : "missing"} at ${againstPath}`
1107
+ );
1108
+ }
1109
+ if (hasCurrentFingerprint && hasBaselineFingerprint && current.datasetFingerprint !== baseline.datasetFingerprint) {
1110
+ throw new Error(
1111
+ `Cannot compare incompatible evaluation datasets by fingerprint: current=${current.datasetFingerprint}, baseline=${baseline.datasetFingerprint} at ${againstPath}`
1112
+ );
1113
+ }
1100
1114
  if (current.datasetName !== baseline.datasetName || current.datasetVersion !== baseline.datasetVersion || current.queryCount !== baseline.queryCount) {
1101
1115
  throw new Error(
1102
1116
  `Cannot compare incompatible evaluation datasets: current=${current.datasetName}@${current.datasetVersion} (${current.queryCount} queries), baseline=${baseline.datasetName}@${baseline.datasetVersion} (${baseline.queryCount} queries) at ${againstPath}`
@@ -1400,6 +1414,7 @@ function buildPerQueryArtifact(perQuery) {
1400
1414
  }
1401
1415
 
1402
1416
  // src/eval/runner.ts
1417
+ var crypto = __toESM(require("crypto"), 1);
1403
1418
  var import_fs17 = require("fs");
1404
1419
  var path21 = __toESM(require("path"), 1);
1405
1420
  var import_perf_hooks2 = require("perf_hooks");
@@ -3342,9 +3357,7 @@ var PROJECT_MARKERS = [
3342
3357
  "pom.xml",
3343
3358
  "build.gradle",
3344
3359
  "CMakeLists.txt",
3345
- "Makefile",
3346
- ".opencode",
3347
- ".codebase-index"
3360
+ "Makefile"
3348
3361
  ];
3349
3362
  function hasProjectMarker(projectRoot) {
3350
3363
  for (const marker of PROJECT_MARKERS) {
@@ -4039,9 +4052,21 @@ function parseFiles(files) {
4039
4052
  return result.map((f) => ({
4040
4053
  path: f.path,
4041
4054
  chunks: f.chunks.map(mapChunk),
4055
+ symbols: (f.symbols ?? []).map(mapParsedSymbol),
4042
4056
  hash: f.hash
4043
4057
  }));
4044
4058
  }
4059
+ function mapParsedSymbol(symbol) {
4060
+ return {
4061
+ name: symbol.name,
4062
+ kind: symbol.kind,
4063
+ startLine: symbol.startLine ?? symbol.start_line,
4064
+ startCol: symbol.startCol ?? symbol.start_col,
4065
+ endLine: symbol.endLine ?? symbol.end_line,
4066
+ endCol: symbol.endCol ?? symbol.end_col,
4067
+ language: symbol.language
4068
+ };
4069
+ }
4045
4070
  function mapChunk(c) {
4046
4071
  return {
4047
4072
  content: c.content,
@@ -6852,6 +6877,7 @@ var INDEX_METADATA_VERSION = "1";
6852
6877
  var EMBEDDING_STRATEGY_VERSION = "2";
6853
6878
  var SWIFT_PARSER_VERSION = "1";
6854
6879
  var METAL_PARSER_VERSION = "1";
6880
+ var SYMBOL_EXTRACTOR_VERSION = "1";
6855
6881
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
6856
6882
  var RANK_HYBRID_CACHE_LIMIT = 256;
6857
6883
  function createPendingChunkStorageText(texts) {
@@ -7153,7 +7179,7 @@ function classifyQueryIntentRaw(query) {
7153
7179
  return "neutral";
7154
7180
  }
7155
7181
  function isImplementationChunkType(chunkType) {
7156
- return [
7182
+ return CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunkType) || [
7157
7183
  "export_statement",
7158
7184
  "function",
7159
7185
  "function_declaration",
@@ -7598,7 +7624,7 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
7598
7624
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
7599
7625
  return [...promoted, ...remainder];
7600
7626
  }
7601
- function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7627
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7602
7628
  if (!prioritizeSourcePaths) {
7603
7629
  return [];
7604
7630
  }
@@ -7612,14 +7638,14 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7612
7638
  const primaryHint = extractPrimaryIdentifierQueryHint(query);
7613
7639
  const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
7614
7640
  if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
7615
- return;
7641
+ return false;
7616
7642
  }
7617
7643
  const chunkType = chunk.nodeType ?? "other";
7618
7644
  if (!isImplementationChunkType(chunkType)) {
7619
- return;
7645
+ return false;
7620
7646
  }
7621
7647
  if (!isLikelyImplementationPath2(chunk.filePath)) {
7622
- return;
7648
+ return false;
7623
7649
  }
7624
7650
  const nameLower = (chunk.name ?? "").toLowerCase();
7625
7651
  const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
@@ -7641,6 +7667,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7641
7667
  }
7642
7668
  });
7643
7669
  }
7670
+ return true;
7644
7671
  };
7645
7672
  const normalizedHints = identifierHints.flatMap((hint) => [
7646
7673
  hint,
@@ -7662,12 +7689,46 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7662
7689
  dedupSymbols.set(symbol.id, symbol);
7663
7690
  }
7664
7691
  for (const symbol of dedupSymbols.values()) {
7692
+ if (branchSymbolIds && !branchSymbolIds.has(symbol.id)) {
7693
+ continue;
7694
+ }
7695
+ if (filePathHint && !pathMatchesHint(symbol.filePath, filePathHint)) {
7696
+ continue;
7697
+ }
7665
7698
  const chunks = database.getChunksByFile(symbol.filePath);
7699
+ let foundCoveringChunk = false;
7666
7700
  for (const chunk of chunks) {
7667
7701
  if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
7668
7702
  continue;
7669
7703
  }
7670
- upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7704
+ const chunkName = (chunk.name ?? "").toLowerCase();
7705
+ const symbolName2 = symbol.name.toLowerCase();
7706
+ if (chunkName !== symbolName2 && chunkName.replace(/_/g, "") !== symbolName2.replace(/_/g, "")) {
7707
+ continue;
7708
+ }
7709
+ foundCoveringChunk = upsertChunkCandidate(chunk, identifier, normalizedIdentifier) || foundCoveringChunk;
7710
+ }
7711
+ if (foundCoveringChunk || !isLikelyImplementationPath2(symbol.filePath)) {
7712
+ continue;
7713
+ }
7714
+ const symbolName = symbol.name.toLowerCase();
7715
+ const exactName = symbolName === identifier || symbolName.replace(/_/g, "") === normalizedIdentifier;
7716
+ const score = exactName ? 0.99 : 0.88;
7717
+ const existing = symbolCandidates.get(symbol.id);
7718
+ if (!existing || score > existing.score) {
7719
+ symbolCandidates.set(symbol.id, {
7720
+ id: symbol.id,
7721
+ score,
7722
+ metadata: {
7723
+ filePath: symbol.filePath,
7724
+ startLine: symbol.startLine,
7725
+ endLine: symbol.endLine,
7726
+ chunkType: symbol.kind,
7727
+ name: symbol.name,
7728
+ language: symbol.language,
7729
+ hash: symbol.id
7730
+ }
7731
+ });
7671
7732
  }
7672
7733
  }
7673
7734
  const dedupChunksByName = /* @__PURE__ */ new Map();
@@ -7675,6 +7736,9 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7675
7736
  dedupChunksByName.set(chunk.chunkId, chunk);
7676
7737
  }
7677
7738
  for (const chunk of dedupChunksByName.values()) {
7739
+ if (filePathHint && !pathMatchesHint(chunk.filePath, filePathHint)) {
7740
+ continue;
7741
+ }
7678
7742
  upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7679
7743
  }
7680
7744
  }
@@ -8216,6 +8280,10 @@ var Indexer = class _Indexer {
8216
8280
  const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
8217
8281
  return `${key}.${projectHash}`;
8218
8282
  }
8283
+ getSymbolExtractorVersionMetadataKey(catalogIdentity = this.getBranchCatalogIdentity()) {
8284
+ const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
8285
+ return `index.symbolExtractorVersion.${hashContent(branchKey).slice(0, 24)}`;
8286
+ }
8219
8287
  hasProjectForceReembedPending() {
8220
8288
  return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
8221
8289
  }
@@ -9559,7 +9627,8 @@ var Indexer = class _Indexer {
9559
9627
  }
9560
9628
  const branchKey = this.getBranchCatalogKey();
9561
9629
  const alreadyIndexed = database.getBranchChunkIds(branchKey).length > 0 && database.getBranchSymbolIds(branchKey).length > 0;
9562
- if (alreadyIndexed && this.getStoredBranchCommit(database) === normalizedCommit) {
9630
+ const symbolsCurrent = database.getMetadata(this.getSymbolExtractorVersionMetadataKey()) === SYMBOL_EXTRACTOR_VERSION;
9631
+ if (alreadyIndexed && symbolsCurrent && this.getStoredBranchCommit(database) === normalizedCommit) {
9563
9632
  return { prepared: false };
9564
9633
  }
9565
9634
  const stats = await this.indexUnlocked(onProgress, [], true);
@@ -9618,6 +9687,8 @@ var Indexer = class _Indexer {
9618
9687
  const reparseCachedSwiftFiles = database.getMetadata(swiftParserMetadataKey) !== SWIFT_PARSER_VERSION;
9619
9688
  const metalParserMetadataKey = this.getMetalParserVersionMetadataKey();
9620
9689
  const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
9690
+ const symbolExtractorMetadataKey = this.getSymbolExtractorVersionMetadataKey();
9691
+ const refreshCachedSymbols = database.getMetadata(symbolExtractorMetadataKey) !== SYMBOL_EXTRACTOR_VERSION;
9621
9692
  if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
9622
9693
  (filePath) => path13.extname(filePath).toLowerCase() === ".swift"
9623
9694
  )) {
@@ -9661,7 +9732,7 @@ var Indexer = class _Indexer {
9661
9732
  );
9662
9733
  const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path13.extname(canonicalPath).toLowerCase() === ".swift";
9663
9734
  const requiresMetalParserUpgrade = reparseCachedMetalFiles && path13.extname(canonicalPath).toLowerCase() === ".metal";
9664
- if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade) {
9735
+ if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
9665
9736
  unchangedFilePaths.add(canonicalPath);
9666
9737
  this.logger.recordCacheHit();
9667
9738
  } else {
@@ -9872,37 +9943,27 @@ var Indexer = class _Indexer {
9872
9943
  const parsed = parsedFiles[i];
9873
9944
  const changedFile = changedFiles[i];
9874
9945
  const fileSymbols = [];
9875
- for (const chunk of parsed.chunks) {
9876
- if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
9877
- const existingMetalSymbol = chunk.language === "metal" ? fileSymbols.find(
9878
- (symbol2) => symbol2.name === chunk.name && symbol2.kind === chunk.chunkType && symbol2.startLine <= chunk.endLine && chunk.startLine <= symbol2.endLine
9879
- ) : void 0;
9880
- if (existingMetalSymbol) {
9881
- existingMetalSymbol.startLine = Math.min(existingMetalSymbol.startLine, chunk.startLine);
9882
- existingMetalSymbol.endLine = Math.max(existingMetalSymbol.endLine, chunk.endLine);
9883
- existingMetalSymbol.startCol = Math.min(existingMetalSymbol.startCol, chunk.startCol ?? 0);
9884
- existingMetalSymbol.endCol = Math.max(existingMetalSymbol.endCol, chunk.endCol ?? 0);
9885
- continue;
9886
- }
9946
+ for (const parsedSymbol of parsed.symbols) {
9947
+ if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) continue;
9887
9948
  const preparedNamespace = this.getPreparedBranchNamespace();
9888
9949
  const symbolId = `sym_${hashContent(
9889
- (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine + ":" + (chunk.startCol ?? 0) + ":" + changedFile.hash
9950
+ (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + parsedSymbol.name + ":" + parsedSymbol.kind + ":" + parsedSymbol.startLine + ":" + parsedSymbol.startCol + ":" + changedFile.hash
9890
9951
  ).slice(0, 16)}`;
9891
9952
  const symbol = {
9892
9953
  id: symbolId,
9893
9954
  filePath: parsed.path,
9894
- name: chunk.name,
9895
- kind: chunk.chunkType,
9896
- startLine: chunk.startLine,
9897
- startCol: chunk.startCol ?? 0,
9898
- endLine: chunk.endLine,
9899
- endCol: chunk.endCol ?? 0,
9900
- language: chunk.language
9955
+ name: parsedSymbol.name,
9956
+ kind: parsedSymbol.kind,
9957
+ startLine: parsedSymbol.startLine,
9958
+ startCol: parsedSymbol.startCol,
9959
+ endLine: parsedSymbol.endLine,
9960
+ endCol: parsedSymbol.endCol,
9961
+ language: parsedSymbol.language
9901
9962
  };
9902
9963
  fileSymbols.push(symbol);
9903
9964
  allSymbolIds.add(symbolId);
9904
9965
  }
9905
- const fileLanguage = parsed.chunks[0]?.language;
9966
+ const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
9906
9967
  const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
9907
9968
  const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
9908
9969
  const symbolsByName = /* @__PURE__ */ new Map();
@@ -10018,6 +10079,7 @@ var Indexer = class _Indexer {
10018
10079
  }
10019
10080
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10020
10081
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10082
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10021
10083
  this.saveBranchCommit(database, indexedCommit);
10022
10084
  this.saveIndexMetadata(configuredProviderInfo);
10023
10085
  this.indexCompatibility = { compatible: true };
@@ -10054,6 +10116,7 @@ var Indexer = class _Indexer {
10054
10116
  }
10055
10117
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10056
10118
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10119
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10057
10120
  this.saveBranchCommit(database, indexedCommit);
10058
10121
  this.saveIndexMetadata(configuredProviderInfo);
10059
10122
  this.indexCompatibility = { compatible: true };
@@ -10333,6 +10396,7 @@ var Indexer = class _Indexer {
10333
10396
  }
10334
10397
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10335
10398
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10399
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10336
10400
  this.saveBranchCommit(database, indexedCommit);
10337
10401
  this.saveIndexMetadata(configuredProviderInfo);
10338
10402
  this.indexCompatibility = { compatible: true };
@@ -10471,10 +10535,11 @@ var Indexer = class _Indexer {
10471
10535
  const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
10472
10536
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
10473
10537
  let branchChunkIds = null;
10538
+ let branchSymbolIds = null;
10474
10539
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
10475
- branchChunkIds = new Set(
10476
- this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
10477
- );
10540
+ const branchCatalogKeys = this.getBranchCatalogKeys();
10541
+ branchChunkIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchChunkIds(branchKey)));
10542
+ branchSymbolIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchSymbolIds(branchKey)));
10478
10543
  }
10479
10544
  const prefilterStartTime = import_perf_hooks.performance.now();
10480
10545
  const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
@@ -10546,6 +10611,7 @@ var Indexer = class _Indexer {
10546
10611
  query,
10547
10612
  database,
10548
10613
  branchChunkIds,
10614
+ branchSymbolIds,
10549
10615
  maxResults,
10550
10616
  union,
10551
10617
  sourceIntent
@@ -10559,7 +10625,7 @@ var Indexer = class _Indexer {
10559
10625
  (r) => isLikelyImplementationPath2(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
10560
10626
  );
10561
10627
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
10562
- const identifierFallback = !options?.definitionIntent && filtered.length === 0 && identifierHints.length > 0 ? buildSymbolDefinitionLane(query, database, branchChunkIds, maxResults, union, true).filter((r) => matchesSearchFilters(r, options, this.config.search.minScore)).slice(0, maxResults) : [];
10628
+ const identifierFallback = !options?.definitionIntent && filtered.length === 0 && identifierHints.length > 0 ? buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, maxResults, union, true).filter((r) => matchesSearchFilters(r, options, this.config.search.minScore)).slice(0, maxResults) : [];
10563
10629
  const finalResults = filtered.length > 0 ? filtered : identifierFallback;
10564
10630
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
10565
10631
  this.logger.recordSearch(totalSearchMs, {
@@ -10716,7 +10782,7 @@ var Indexer = class _Indexer {
10716
10782
  const extension = path13.extname(filePath).toLowerCase();
10717
10783
  return extension === ".php" || extension === ".c" || extension === ".cc" || extension === ".cpp" || extension === ".cxx";
10718
10784
  });
10719
- if (hasSwiftFiles && database.getMetadata(this.getSwiftParserVersionMetadataKey()) !== SWIFT_PARSER_VERSION || hasMetalFiles && database.getMetadata(this.getMetalParserVersionMetadataKey()) !== METAL_PARSER_VERSION || hasCallGraphMigrationFiles && database.getMetadata(this.getCallGraphResolutionMetadataKey()) !== CALL_GRAPH_RESOLUTION_VERSION) {
10785
+ if (hasSwiftFiles && database.getMetadata(this.getSwiftParserVersionMetadataKey()) !== SWIFT_PARSER_VERSION || hasMetalFiles && database.getMetadata(this.getMetalParserVersionMetadataKey()) !== METAL_PARSER_VERSION || database.getMetadata(this.getSymbolExtractorVersionMetadataKey()) !== SYMBOL_EXTRACTOR_VERSION || hasCallGraphMigrationFiles && database.getMetadata(this.getCallGraphResolutionMetadataKey()) !== CALL_GRAPH_RESOLUTION_VERSION) {
10720
10786
  return { readable: true, current: false, reason: "migration-required" };
10721
10787
  }
10722
10788
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -11424,7 +11490,10 @@ var Indexer = class _Indexer {
11424
11490
  const requestedRef = opts.pr !== void 0 ? expectedCommit : headRefName ?? resolvedBranch;
11425
11491
  const storedCommit = this.getStoredBranchCommit(database, catalogIdentity);
11426
11492
  const catalogIdentityMatches = storedCommit === expectedCommit;
11427
- if (branchSymbols.length === 0 || !catalogIdentityMatches) {
11493
+ const symbolsCurrent = database.getMetadata(
11494
+ this.getSymbolExtractorVersionMetadataKey(catalogIdentity)
11495
+ ) === SYMBOL_EXTRACTOR_VERSION;
11496
+ if (branchSymbols.length === 0 || !catalogIdentityMatches || !symbolsCurrent) {
11428
11497
  if (!resolvedBranch || resolvedBranch === "default") {
11429
11498
  throw new Error("Run index_codebase first to build the call graph and symbol index for this project.");
11430
11499
  }
@@ -11740,8 +11809,17 @@ function fitTextToContextBudget(text, tokenBudget) {
11740
11809
  function normalizedLineRange(result) {
11741
11810
  return result.startLine <= result.endLine ? { start: result.startLine, end: result.endLine } : { start: result.endLine, end: result.startLine };
11742
11811
  }
11743
- function rankContextCandidates(results) {
11744
- return results.map((result, originalIndex) => ({ result, originalIndex })).sort((left, right) => right.result.score - left.result.score || left.originalIndex - right.originalIndex);
11812
+ function rankContextCandidates(results, preferImplementationPaths) {
11813
+ return results.map((result, originalIndex) => ({ result, originalIndex })).sort((left, right) => {
11814
+ if (preferImplementationPaths) {
11815
+ const leftIsImplementation = isLikelyImplementationPath(left.result.filePath);
11816
+ const rightIsImplementation = isLikelyImplementationPath(right.result.filePath);
11817
+ if (leftIsImplementation !== rightIsImplementation) {
11818
+ return leftIsImplementation ? -1 : 1;
11819
+ }
11820
+ }
11821
+ return right.result.score - left.result.score || left.originalIndex - right.originalIndex;
11822
+ });
11745
11823
  }
11746
11824
  function deduplicateContextCandidates(candidates) {
11747
11825
  const acceptedByFile = /* @__PURE__ */ new Map();
@@ -11830,7 +11908,12 @@ function buildContextPack(results, options = {}) {
11830
11908
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
11831
11909
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
11832
11910
  const candidateCount = results.length;
11833
- const deduplicated = deduplicateContextCandidates(rankContextCandidates(results));
11911
+ const deduplicated = deduplicateContextCandidates(
11912
+ rankContextCandidates(
11913
+ results,
11914
+ options.preferImplementationPaths ?? false
11915
+ )
11916
+ );
11834
11917
  const diversified = diversifyContextCandidates(deduplicated);
11835
11918
  const duplicateCount = candidateCount - deduplicated.length;
11836
11919
  const selectable = diversified.slice(0, maxResults);
@@ -12202,7 +12285,7 @@ ${truncateContent(r.content)}
12202
12285
  }
12203
12286
 
12204
12287
  // src/utils/effectiveness-metrics.ts
12205
- var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 2;
12288
+ var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 3;
12206
12289
  var MAX_EFFECTIVENESS_COUNTER = 1e9;
12207
12290
  var EFFECTIVENESS_TOOL_ROUTES = [
12208
12291
  "context-conceptual",
@@ -12248,6 +12331,9 @@ var EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS = ["none", "directory", "file-type",
12248
12331
  function emptyCounterMap(values) {
12249
12332
  return Object.fromEntries(values.map((value) => [value, 0]));
12250
12333
  }
12334
+ function emptyRouteCounterMap(routes, values) {
12335
+ return Object.fromEntries(routes.map((route) => [route, emptyCounterMap(values)]));
12336
+ }
12251
12337
  function boundedNumber(value) {
12252
12338
  if (value === void 0 || !Number.isFinite(value)) return 0;
12253
12339
  return Math.max(0, Math.floor(value));
@@ -12296,6 +12382,11 @@ function allowedValue(value, allowed, fallback) {
12296
12382
  function cloneCounterMap(counters) {
12297
12383
  return { ...counters };
12298
12384
  }
12385
+ function cloneRouteCounterMap(counters) {
12386
+ return Object.fromEntries(
12387
+ EFFECTIVENESS_TOOL_ROUTES.map((route) => [route, cloneCounterMap(counters[route])])
12388
+ );
12389
+ }
12299
12390
  var EffectivenessMetrics = class {
12300
12391
  constructor(counterCap = MAX_EFFECTIVENESS_COUNTER) {
12301
12392
  this.counterCap = counterCap;
@@ -12311,7 +12402,7 @@ var EffectivenessMetrics = class {
12311
12402
  lifetime: "process",
12312
12403
  reset: "index_metrics-reset-or-process-exit",
12313
12404
  maxCounterValue: this.counterCap,
12314
- dimensions: "bounded-host-and-category-only"
12405
+ dimensions: "bounded-route-and-bucketed-performance-only"
12315
12406
  },
12316
12407
  totalCalls: 0,
12317
12408
  toolRoute: emptyCounterMap(EFFECTIVENESS_TOOL_ROUTES),
@@ -12323,7 +12414,14 @@ var EffectivenessMetrics = class {
12323
12414
  tokenBudget: emptyCounterMap(EFFECTIVENESS_TOKEN_BUDGET_BUCKETS),
12324
12415
  returnedTokenEstimate: emptyCounterMap(EFFECTIVENESS_RETURNED_TOKEN_BUCKETS),
12325
12416
  exactHandoffEmitted: emptyCounterMap(EFFECTIVENESS_BOOLEAN_BUCKETS),
12326
- scopeRelaxation: emptyCounterMap(EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS)
12417
+ scopeRelaxation: emptyCounterMap(EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS),
12418
+ routeOutcome: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_OUTCOMES),
12419
+ routeLatency: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_LATENCY_BUCKETS),
12420
+ routeResultCount: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_RESULT_COUNT_BUCKETS),
12421
+ routeReturnedTokenEstimate: emptyRouteCounterMap(
12422
+ EFFECTIVENESS_TOOL_ROUTES,
12423
+ EFFECTIVENESS_RETURNED_TOKEN_BUCKETS
12424
+ )
12327
12425
  };
12328
12426
  }
12329
12427
  increment(counters, key) {
@@ -12345,12 +12443,19 @@ var EffectivenessMetrics = class {
12345
12443
  this.increment(this.snapshot.hostMode, host);
12346
12444
  this.increment(this.snapshot.outcome, outcome);
12347
12445
  this.increment(this.snapshot.recoveryUsed, recoveryUsed);
12348
- this.increment(this.snapshot.resultCount, resultCountBucket(event.resultCount));
12349
- this.increment(this.snapshot.latency, latencyBucket(event.latencyMs));
12446
+ const resultCountBucketValue = resultCountBucket(event.resultCount);
12447
+ const latencyBucketValue = latencyBucket(event.latencyMs);
12448
+ const returnedTokenBucketValue = returnedTokenBucket(event.returnedTokenEstimate);
12449
+ this.increment(this.snapshot.resultCount, resultCountBucketValue);
12450
+ this.increment(this.snapshot.latency, latencyBucketValue);
12350
12451
  this.increment(this.snapshot.tokenBudget, tokenBudgetBucket(event.tokenBudget));
12351
- this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucket(event.returnedTokenEstimate));
12452
+ this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucketValue);
12352
12453
  this.increment(this.snapshot.exactHandoffEmitted, exactHandoffEmitted);
12353
12454
  this.increment(this.snapshot.scopeRelaxation, scopeRelaxation);
12455
+ this.increment(this.snapshot.routeOutcome[route], outcome);
12456
+ this.increment(this.snapshot.routeLatency[route], latencyBucketValue);
12457
+ this.increment(this.snapshot.routeResultCount[route], resultCountBucketValue);
12458
+ this.increment(this.snapshot.routeReturnedTokenEstimate[route], returnedTokenBucketValue);
12354
12459
  }
12355
12460
  getSnapshot() {
12356
12461
  return {
@@ -12365,7 +12470,11 @@ var EffectivenessMetrics = class {
12365
12470
  tokenBudget: cloneCounterMap(this.snapshot.tokenBudget),
12366
12471
  returnedTokenEstimate: cloneCounterMap(this.snapshot.returnedTokenEstimate),
12367
12472
  exactHandoffEmitted: cloneCounterMap(this.snapshot.exactHandoffEmitted),
12368
- scopeRelaxation: cloneCounterMap(this.snapshot.scopeRelaxation)
12473
+ scopeRelaxation: cloneCounterMap(this.snapshot.scopeRelaxation),
12474
+ routeOutcome: cloneRouteCounterMap(this.snapshot.routeOutcome),
12475
+ routeLatency: cloneRouteCounterMap(this.snapshot.routeLatency),
12476
+ routeResultCount: cloneRouteCounterMap(this.snapshot.routeResultCount),
12477
+ routeReturnedTokenEstimate: cloneRouteCounterMap(this.snapshot.routeReturnedTokenEstimate)
12369
12478
  };
12370
12479
  }
12371
12480
  reset() {
@@ -12384,6 +12493,7 @@ function resetProcessEffectivenessMetrics() {
12384
12493
  }
12385
12494
  function formatEffectivenessMetrics(snapshot) {
12386
12495
  const formatCounters = (counters) => Object.entries(counters).map(([bucket, count]) => `${bucket}=${count}`).join(", ");
12496
+ const formatRouteCounters = (counters) => EFFECTIVENESS_TOOL_ROUTES.map((route) => `${route} => ${formatCounters(counters[route])}`).join("; ");
12387
12497
  const lines = [
12388
12498
  `Privacy-safe effectiveness (schema v${snapshot.schemaVersion}):`,
12389
12499
  ` Retention: ${snapshot.retention.storage}, ${snapshot.retention.lifetime}-lifetime; reset with index_metrics(reset=true) or process exit`,
@@ -12398,6 +12508,10 @@ function formatEffectivenessMetrics(snapshot) {
12398
12508
  ` Latency bucket: ${formatCounters(snapshot.latency)}`,
12399
12509
  ` Token-budget bucket: ${formatCounters(snapshot.tokenBudget)}`,
12400
12510
  ` Returned-token estimate: ${formatCounters(snapshot.returnedTokenEstimate)}`,
12511
+ ` Route outcome buckets: ${formatRouteCounters(snapshot.routeOutcome)}`,
12512
+ ` Route latency buckets: ${formatRouteCounters(snapshot.routeLatency)}`,
12513
+ ` Route result-count buckets: ${formatRouteCounters(snapshot.routeResultCount)}`,
12514
+ ` Route returned-token buckets: ${formatRouteCounters(snapshot.routeReturnedTokenEstimate)}`,
12401
12515
  ` Exact handoff emitted: ${formatCounters(snapshot.exactHandoffEmitted)}`,
12402
12516
  ` Scope relaxation: ${formatCounters(snapshot.scopeRelaxation)}`,
12403
12517
  " Privacy: no queries, response text, source, symbols, paths, repository names, user identity, or stable identifiers are retained."
@@ -12409,6 +12523,103 @@ function formatEffectivenessMetrics(snapshot) {
12409
12523
  var import_fs11 = require("fs");
12410
12524
  var os6 = __toESM(require("os"), 1);
12411
12525
  var path15 = __toESM(require("path"), 1);
12526
+
12527
+ // src/utils/power-source.ts
12528
+ var childProcess = __toESM(require("child_process"), 1);
12529
+ var POWER_SOURCE_RECHECK_DELAY_MS = 6e4;
12530
+ var PMSET_TIMEOUT_MS = 5e3;
12531
+ function getErrorMessage4(error) {
12532
+ return error instanceof Error ? error.message : String(error);
12533
+ }
12534
+ function runCommand(file, args, options) {
12535
+ return new Promise((resolve17, reject) => {
12536
+ childProcess.execFile(
12537
+ file,
12538
+ args,
12539
+ { encoding: "utf8", timeout: options.timeoutMs },
12540
+ (error, stdout) => {
12541
+ if (error) {
12542
+ reject(error);
12543
+ return;
12544
+ }
12545
+ resolve17(stdout);
12546
+ }
12547
+ );
12548
+ });
12549
+ }
12550
+ function parseMacOsPowerSource(output) {
12551
+ const match = output.match(/Now drawing from '([^']+)'/i);
12552
+ if (!match) {
12553
+ return "unknown";
12554
+ }
12555
+ const source = match[1].toLowerCase();
12556
+ if (source === "battery power") {
12557
+ return "battery";
12558
+ }
12559
+ if (source === "ac power") {
12560
+ return "ac";
12561
+ }
12562
+ return "unknown";
12563
+ }
12564
+ async function readMacOsPowerSource(commandRunner = runCommand) {
12565
+ const output = await commandRunner(
12566
+ "/usr/bin/pmset",
12567
+ ["-g", "batt"],
12568
+ { timeoutMs: PMSET_TIMEOUT_MS }
12569
+ );
12570
+ return parseMacOsPowerSource(output);
12571
+ }
12572
+ var MacOsBackgroundIndexingPolicy = class {
12573
+ constructor(readPowerSource, recheckDelayMs) {
12574
+ this.readPowerSource = readPowerSource;
12575
+ this.recheckDelayMs = recheckDelayMs;
12576
+ }
12577
+ readPowerSource;
12578
+ recheckDelayMs;
12579
+ lastPaused = null;
12580
+ reportedFailure = false;
12581
+ isPaused() {
12582
+ return this.checkPowerSource();
12583
+ }
12584
+ async checkPowerSource() {
12585
+ try {
12586
+ const source = await this.readPowerSource();
12587
+ if (source === "unknown") {
12588
+ throw new Error("pmset returned an unrecognized power source");
12589
+ }
12590
+ this.reportedFailure = false;
12591
+ const paused = source === "battery";
12592
+ if (paused && this.lastPaused !== true) {
12593
+ console.warn("[codebase-index] Background indexing paused while macOS is using battery power.");
12594
+ } else if (!paused && this.lastPaused === true) {
12595
+ console.warn("[codebase-index] AC power detected; resuming pending background indexing.");
12596
+ }
12597
+ this.lastPaused = paused;
12598
+ return paused;
12599
+ } catch (error) {
12600
+ if (!this.reportedFailure) {
12601
+ console.error(
12602
+ `[codebase-index] Failed to determine the macOS power source; background indexing will continue: ${getErrorMessage4(error)}`
12603
+ );
12604
+ this.reportedFailure = true;
12605
+ }
12606
+ this.lastPaused = false;
12607
+ return false;
12608
+ }
12609
+ }
12610
+ };
12611
+ function createBackgroundIndexingPolicy(pauseOnBattery, options = {}) {
12612
+ const platform2 = options.platform ?? process.platform;
12613
+ if (!pauseOnBattery || platform2 !== "darwin") {
12614
+ return null;
12615
+ }
12616
+ return new MacOsBackgroundIndexingPolicy(
12617
+ options.readPowerSource ?? readMacOsPowerSource,
12618
+ options.recheckDelayMs ?? POWER_SOURCE_RECHECK_DELAY_MS
12619
+ );
12620
+ }
12621
+
12622
+ // src/utils/auto-index.ts
12412
12623
  var MAX_RETRY_DELAY_MS = 1e4;
12413
12624
  var SHUTDOWN_WAIT_MS = 2e3;
12414
12625
  var coordinators = /* @__PURE__ */ new Map();
@@ -12525,7 +12736,13 @@ var AutoIndexCoordinator = class {
12525
12736
  activation = Promise.resolve();
12526
12737
  inFlight = null;
12527
12738
  activeRequest = null;
12739
+ batteryCheck = null;
12740
+ batteryIndexJob = null;
12741
+ batteryDeferredRequest = null;
12742
+ batteryRetryTimer = null;
12743
+ resolveBatteryRetry = null;
12528
12744
  pendingRequest = null;
12745
+ pendingFollowUp = null;
12529
12746
  abortController = null;
12530
12747
  stopped = false;
12531
12748
  constructor(registration) {
@@ -12538,6 +12755,7 @@ var AutoIndexCoordinator = class {
12538
12755
  };
12539
12756
  }
12540
12757
  update(registration) {
12758
+ const pauseOnBatteryChanged = registration.config.indexing.pauseBackgroundIndexingOnBattery !== this.registration.config.indexing.pauseBackgroundIndexingOnBattery;
12541
12759
  this.registration = registration;
12542
12760
  this.status.enabled = registration.config.indexing.autoIndex;
12543
12761
  this.status.blockedReason = registration.blockedReason;
@@ -12553,6 +12771,9 @@ var AutoIndexCoordinator = class {
12553
12771
  this.setState("idle", { source: void 0 });
12554
12772
  }
12555
12773
  }
12774
+ if (pauseOnBatteryChanged) {
12775
+ this.cancelBatteryRetry();
12776
+ }
12556
12777
  }
12557
12778
  activateAfter(activation) {
12558
12779
  this.activation = activation;
@@ -12574,7 +12795,26 @@ var AutoIndexCoordinator = class {
12574
12795
  if (this.stopped) {
12575
12796
  return Promise.resolve({ outcome: "stopped" });
12576
12797
  }
12577
- return this.activation.then(() => this.enqueueRequest(request));
12798
+ return this.activation.then(() => this.enqueueBatteryAwareRequest(request));
12799
+ }
12800
+ enqueueBatteryAwareRequest(request) {
12801
+ if (!this.shouldDeferForBattery(request)) {
12802
+ return this.enqueueRequest(request);
12803
+ }
12804
+ if (this.batteryCheck && this.batteryIndexJob !== null && this.batteryIndexJob === this.inFlight) {
12805
+ return this.enqueueRequest(request);
12806
+ }
12807
+ this.batteryDeferredRequest = mergeRequests(this.batteryDeferredRequest, request);
12808
+ if (this.batteryCheck) {
12809
+ return this.batteryCheck;
12810
+ }
12811
+ const batteryCheck = this.waitForACPower();
12812
+ this.batteryCheck = batteryCheck;
12813
+ void batteryCheck.then(
12814
+ () => this.finishBatteryCheck(batteryCheck),
12815
+ () => this.finishBatteryCheck(batteryCheck)
12816
+ );
12817
+ return batteryCheck;
12578
12818
  }
12579
12819
  enqueueRequest(request) {
12580
12820
  if (this.stopped || !this.canRun(request)) {
@@ -12592,6 +12832,11 @@ var AutoIndexCoordinator = class {
12592
12832
  }
12593
12833
  if (request.source === "watcher") {
12594
12834
  this.pendingRequest = mergeRequests(this.pendingRequest, request);
12835
+ const active = this.inFlight;
12836
+ return active.then(() => {
12837
+ if (this.stopped) return { outcome: "stopped" };
12838
+ return this.pendingFollowUp ?? { outcome: "stopped" };
12839
+ });
12595
12840
  }
12596
12841
  return this.inFlight;
12597
12842
  }
@@ -12608,6 +12853,8 @@ var AutoIndexCoordinator = class {
12608
12853
  }
12609
12854
  async stop(waitForCompletion = false) {
12610
12855
  this.stopped = true;
12856
+ this.batteryDeferredRequest = null;
12857
+ this.cancelBatteryRetry();
12611
12858
  this.pendingRequest = null;
12612
12859
  this.abortController?.abort();
12613
12860
  this.setState("stopped", {
@@ -12637,10 +12884,20 @@ var AutoIndexCoordinator = class {
12637
12884
  this.inFlight = null;
12638
12885
  this.activeRequest = null;
12639
12886
  this.abortController = null;
12887
+ if (this.batteryIndexJob === job) {
12888
+ this.batteryIndexJob = null;
12889
+ this.batteryCheck = null;
12890
+ }
12640
12891
  const pending = this.pendingRequest;
12641
12892
  this.pendingRequest = null;
12642
12893
  if (pending && !this.stopped) {
12643
- this.startRequest(pending);
12894
+ const followUp = this.request(pending);
12895
+ this.pendingFollowUp = followUp;
12896
+ void followUp.then(() => {
12897
+ if (this.pendingFollowUp === followUp) {
12898
+ this.pendingFollowUp = null;
12899
+ }
12900
+ });
12644
12901
  }
12645
12902
  });
12646
12903
  return job;
@@ -12804,6 +13061,68 @@ var AutoIndexCoordinator = class {
12804
13061
  }
12805
13062
  return this.registration.safeToRun && this.registration.config.indexing.autoIndex;
12806
13063
  }
13064
+ shouldDeferForBattery(request) {
13065
+ return this.registration.backgroundIndexingPolicy !== null && (request.source === "startup" || request.source === "watcher");
13066
+ }
13067
+ async waitForACPower() {
13068
+ while (!this.stopped) {
13069
+ const policy = this.registration.backgroundIndexingPolicy;
13070
+ if (!policy || !await this.isBatteryPauseActive(policy)) {
13071
+ const request = this.batteryDeferredRequest;
13072
+ this.batteryDeferredRequest = null;
13073
+ if (!request) return { outcome: "stopped" };
13074
+ const job = this.enqueueRequest(request);
13075
+ if (this.inFlight === job) {
13076
+ this.batteryIndexJob = job;
13077
+ }
13078
+ return job;
13079
+ }
13080
+ await this.waitForBatteryRetry(policy.recheckDelayMs);
13081
+ }
13082
+ return { outcome: "stopped" };
13083
+ }
13084
+ async isBatteryPauseActive(policy) {
13085
+ try {
13086
+ return await policy.isPaused();
13087
+ } catch (error) {
13088
+ console.error(
13089
+ `[codebase-index] Failed to apply the background indexing power policy; background indexing will continue: ${safeFailureMessage(error)}`
13090
+ );
13091
+ return false;
13092
+ }
13093
+ }
13094
+ waitForBatteryRetry(delayMs) {
13095
+ return new Promise((resolve17) => {
13096
+ const timer = setTimeout(() => {
13097
+ if (this.batteryRetryTimer === timer) {
13098
+ this.batteryRetryTimer = null;
13099
+ this.resolveBatteryRetry = null;
13100
+ }
13101
+ resolve17();
13102
+ }, delayMs);
13103
+ timer.unref?.();
13104
+ this.batteryRetryTimer = timer;
13105
+ this.resolveBatteryRetry = resolve17;
13106
+ });
13107
+ }
13108
+ cancelBatteryRetry() {
13109
+ if (this.batteryRetryTimer) {
13110
+ clearTimeout(this.batteryRetryTimer);
13111
+ this.batteryRetryTimer = null;
13112
+ }
13113
+ const resolve17 = this.resolveBatteryRetry;
13114
+ this.resolveBatteryRetry = null;
13115
+ resolve17?.();
13116
+ }
13117
+ finishBatteryCheck(batteryCheck) {
13118
+ if (this.batteryCheck !== batteryCheck) return;
13119
+ this.batteryCheck = null;
13120
+ const deferredRequest = this.batteryDeferredRequest;
13121
+ this.batteryDeferredRequest = null;
13122
+ if (deferredRequest && !this.stopped) {
13123
+ void this.request(deferredRequest);
13124
+ }
13125
+ }
12807
13126
  };
12808
13127
  function getCoordinator(projectRoot, host) {
12809
13128
  const key = coordinatorKeysByProject.get(projectLookupKey(projectRoot, host));
@@ -12813,6 +13132,9 @@ function configureAutoIndex(projectRoot, host, config, getIndexer) {
12813
13132
  const projectKey = projectLookupKey(projectRoot, host);
12814
13133
  const safety = getProjectSafety(projectRoot, config);
12815
13134
  const registration = {
13135
+ backgroundIndexingPolicy: createBackgroundIndexingPolicy(
13136
+ config.indexing.pauseBackgroundIndexingOnBattery
13137
+ ),
12816
13138
  config,
12817
13139
  getIndexer,
12818
13140
  projectRoot,
@@ -14036,6 +14358,7 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14036
14358
  const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed);
14037
14359
  if (results.length > 0) {
14038
14360
  const heading = buildPackHeading("conceptual", decisions);
14361
+ const intent = analyzeQueryIntent(attempt.queryText);
14039
14362
  return toResult(
14040
14363
  "conceptual",
14041
14364
  attempt.queryText,
@@ -14043,7 +14366,8 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14043
14366
  tokenBudget,
14044
14367
  maxResults: limit,
14045
14368
  heading,
14046
- includeExactSearchHandoff: true
14369
+ includeExactSearchHandoff: true,
14370
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
14047
14371
  })
14048
14372
  );
14049
14373
  }
@@ -14344,17 +14668,26 @@ function percentile(values, p) {
14344
14668
  function normalizePath2(input) {
14345
14669
  return normalizePathSeparators(input);
14346
14670
  }
14347
- function uniqueResultsByPath(results) {
14671
+ function uniqueResultsByEvidence(results) {
14348
14672
  const seen = /* @__PURE__ */ new Set();
14349
14673
  const unique = [];
14350
14674
  for (const result of results) {
14351
- const normalized = normalizePath2(result.filePath);
14352
- if (seen.has(normalized)) continue;
14353
- seen.add(normalized);
14675
+ const key = `${normalizePath2(result.filePath)}::${result.name ?? ""}`;
14676
+ if (seen.has(key)) continue;
14677
+ seen.add(key);
14354
14678
  unique.push(result);
14355
14679
  }
14356
14680
  return unique;
14357
14681
  }
14682
+ function uniqueResultsByPath(results) {
14683
+ const seen = /* @__PURE__ */ new Set();
14684
+ return results.filter((result) => {
14685
+ const key = normalizePath2(result.filePath);
14686
+ if (seen.has(key)) return false;
14687
+ seen.add(key);
14688
+ return true;
14689
+ });
14690
+ }
14358
14691
  function distinctTopKRatio(results, k) {
14359
14692
  const top = results.slice(0, k);
14360
14693
  if (top.length === 0) return 0;
@@ -14365,60 +14698,169 @@ function pathMatchesExpected(actualPath, expectedPath) {
14365
14698
  const actual = normalizePath2(actualPath);
14366
14699
  const expected = normalizePath2(expectedPath);
14367
14700
  if (actual === expected) return true;
14368
- return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
14701
+ return actual.endsWith(`/${expected}`);
14702
+ }
14703
+ function getRelevantEvidence(query) {
14704
+ const legacyEvidence = [];
14705
+ if (query.expected.filePath !== void 0) {
14706
+ legacyEvidence.push({
14707
+ path: query.expected.filePath,
14708
+ ...query.expected.symbol !== void 0 ? { symbol: query.expected.symbol } : {},
14709
+ relevance: 1
14710
+ });
14711
+ }
14712
+ if (query.expected.acceptableFiles) {
14713
+ for (const path29 of query.expected.acceptableFiles) {
14714
+ legacyEvidence.push({
14715
+ path: path29,
14716
+ ...query.expected.symbol !== void 0 ? { symbol: query.expected.symbol } : {},
14717
+ relevance: 1
14718
+ });
14719
+ }
14720
+ }
14721
+ const gradedEvidence = query.expected.gradedEvidence ?? [];
14722
+ const allEvidence = [...legacyEvidence, ...gradedEvidence];
14723
+ const dedupeKey = (entry) => {
14724
+ return `${normalizePath2(entry.path)}::${entry.symbol ?? ""}`;
14725
+ };
14726
+ const unique = /* @__PURE__ */ new Map();
14727
+ for (const entry of allEvidence) {
14728
+ unique.set(dedupeKey(entry), entry);
14729
+ }
14730
+ return Array.from(unique.values());
14731
+ }
14732
+ function hasSymbolRequirement(query) {
14733
+ return isSymbolIntended(query);
14734
+ }
14735
+ function isSymbolIntended(query) {
14736
+ return query.expected.symbol !== void 0 || query.args?.symbol !== void 0 || query.expected.gradedEvidence?.some((entry) => entry.symbol !== void 0) === true;
14737
+ }
14738
+ function isExpectedFile(filePath, relevant) {
14739
+ return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
14740
+ }
14741
+ function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
14742
+ let relevance = 0;
14743
+ for (const entry of relevant) {
14744
+ if (!pathMatchesExpected(filePath, entry.path)) {
14745
+ continue;
14746
+ }
14747
+ if (isSymbolIntendedQuery) {
14748
+ if (symbol === void 0 || entry.symbol === void 0 || symbol !== entry.symbol) {
14749
+ continue;
14750
+ }
14751
+ } else if (entry.symbol !== void 0 && symbol !== void 0 && symbol !== entry.symbol) {
14752
+ continue;
14753
+ }
14754
+ if (entry.symbol === void 0) {
14755
+ relevance = Math.max(relevance, entry.relevance);
14756
+ continue;
14757
+ }
14758
+ if (isSymbolIntendedQuery && entry.symbol !== void 0 && symbol === entry.symbol) {
14759
+ relevance = Math.max(relevance, entry.relevance);
14760
+ continue;
14761
+ }
14762
+ if (!isSymbolIntendedQuery && symbol !== void 0 && symbol === entry.symbol) {
14763
+ relevance = Math.max(relevance, entry.relevance);
14764
+ }
14765
+ }
14766
+ return relevance;
14369
14767
  }
14370
- function getRelevantPaths(query) {
14371
- const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
14372
- const fromAcceptable = query.expected.acceptableFiles ?? [];
14373
- return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
14768
+ function isRelevantResult(filePath, symbol, relevant, isSymbolIntendedQuery) {
14769
+ return resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) > 0;
14770
+ }
14771
+ function evidenceMatchesResult(entry, filePath, symbol, isSymbolIntendedQuery) {
14772
+ if (!pathMatchesExpected(filePath, entry.path)) {
14773
+ return false;
14774
+ }
14775
+ if (isSymbolIntendedQuery) {
14776
+ return entry.symbol !== void 0 && symbol === entry.symbol;
14777
+ }
14778
+ return entry.symbol === void 0 || symbol === entry.symbol;
14374
14779
  }
14375
- function isRelevantResult(filePath, relevantPaths) {
14376
- return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
14780
+ function hasGradeBasedEvidence(query) {
14781
+ return (query.expected.gradedEvidence?.length ?? 0) > 0;
14377
14782
  }
14378
- function reciprocalRankAtK(results, relevantPaths, k) {
14379
- const top = uniqueResultsByPath(results).slice(0, k);
14783
+ function dedupeRelevantEvidence(relevant) {
14784
+ const deduped = /* @__PURE__ */ new Map();
14785
+ for (const entry of relevant) {
14786
+ const key = `${normalizePath2(entry.path)}::${entry.symbol ?? ""}`;
14787
+ if (!deduped.has(key)) {
14788
+ deduped.set(key, entry);
14789
+ }
14790
+ }
14791
+ return [...deduped.values()];
14792
+ }
14793
+ function reciprocalRankAtK(results, relevant, isSymbolIntendedQuery, k) {
14794
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14380
14795
  for (let i = 0; i < top.length; i += 1) {
14381
- if (isRelevantResult(top[i].filePath, relevantPaths)) {
14796
+ if (isRelevantResult(top[i].filePath, top[i].name, relevant, isSymbolIntendedQuery)) {
14382
14797
  return 1 / (i + 1);
14383
14798
  }
14384
14799
  }
14385
14800
  return 0;
14386
14801
  }
14387
- function ndcgAtK(results, relevantPaths, k) {
14388
- const top = uniqueResultsByPath(results).slice(0, k);
14802
+ function ndcgAtK(query, results, relevant, isSymbolIntendedQuery, k) {
14803
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14804
+ const availableEvidence = dedupeRelevantEvidence(relevant).filter(
14805
+ (entry) => !isSymbolIntendedQuery || entry.symbol !== void 0
14806
+ );
14389
14807
  const dcg = top.reduce((sum, result, i) => {
14390
- const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
14391
- return sum + rel / Math.log2(i + 2);
14808
+ let bestEvidenceIndex = -1;
14809
+ let rel = 0;
14810
+ for (let evidenceIndex = 0; evidenceIndex < availableEvidence.length; evidenceIndex += 1) {
14811
+ const entry = availableEvidence[evidenceIndex];
14812
+ if (entry.relevance > rel && evidenceMatchesResult(entry, result.filePath, result.name, isSymbolIntendedQuery)) {
14813
+ bestEvidenceIndex = evidenceIndex;
14814
+ rel = entry.relevance;
14815
+ }
14816
+ }
14817
+ if (bestEvidenceIndex < 0) {
14818
+ return sum;
14819
+ }
14820
+ availableEvidence.splice(bestEvidenceIndex, 1);
14821
+ return sum + (2 ** rel - 1) / Math.log2(i + 2);
14392
14822
  }, 0);
14393
- const idealLen = Math.min(k, relevantPaths.length);
14394
- const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
14395
- (sum, value) => sum + value,
14823
+ const dedupedRelevant = dedupeRelevantEvidence(relevant).filter(
14824
+ (entry) => !isSymbolIntendedQuery || entry.symbol !== void 0
14825
+ );
14826
+ const idealRelevances = hasGradeBasedEvidence(query) ? dedupedRelevant.map((entry) => entry.relevance).sort((a, b) => b - a).slice(0, k) : relevant.length > 0 ? [1] : [];
14827
+ const idcg = idealRelevances.reduce(
14828
+ (sum, rel, index) => sum + (2 ** rel - 1) / Math.log2(index + 2),
14396
14829
  0
14397
14830
  );
14398
- return idcg === 0 ? 0 : dcg / idcg;
14831
+ if (idcg === 0) {
14832
+ return 0;
14833
+ }
14834
+ const ndcg = dcg / idcg;
14835
+ if (ndcg <= 0) {
14836
+ return 0;
14837
+ }
14838
+ return ndcg > 1 ? 1 : ndcg;
14399
14839
  }
14400
14840
  function isDocsOrTestsPath(filePath) {
14401
14841
  const lowered = normalizePath2(filePath).toLowerCase();
14402
14842
  return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
14403
14843
  }
14404
14844
  function classifyFailureBucket(query, results, k) {
14405
- const relevantPaths = getRelevantPaths(query);
14406
- const top = uniqueResultsByPath(results).slice(0, k);
14407
- const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
14845
+ const relevant = getRelevantEvidence(query);
14846
+ const isSymbolIntendedQuery = isSymbolIntended(query);
14847
+ if (query.expected.expectedOutcome === "no-results") return void 0;
14848
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14849
+ const hasRelevantTopK = top.some(
14850
+ (result) => isRelevantResult(result.filePath, result.name, relevant, isSymbolIntendedQuery)
14851
+ );
14408
14852
  if (!hasRelevantTopK) {
14853
+ const hasExpectedFileTopK = top.some((result) => isExpectedFile(result.filePath, relevant));
14854
+ if (hasExpectedFileTopK && hasSymbolRequirement(query)) {
14855
+ return "wrong-symbol";
14856
+ }
14409
14857
  return "no-relevant-hit-top-k";
14410
14858
  }
14411
- if (query.expected.symbol) {
14412
- const hasSymbol = top.some(
14413
- (result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
14414
- );
14415
- if (!hasSymbol) return "wrong-symbol";
14416
- }
14417
14859
  const top1 = top[0];
14418
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
14860
+ if (top1 && !isExpectedFile(top1.filePath, relevant) && isDocsOrTestsPath(top1.filePath)) {
14419
14861
  return "docs-tests-outranking-source";
14420
14862
  }
14421
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
14863
+ if (top1 && !isExpectedFile(top1.filePath, relevant)) {
14422
14864
  return "wrong-file";
14423
14865
  }
14424
14866
  return void 0;
@@ -14427,9 +14869,12 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14427
14869
  resolvedRoute: "search",
14428
14870
  routedQuery: query.query
14429
14871
  }, context) {
14430
- const relevantPaths = getRelevantPaths(query);
14431
- const deduped = uniqueResultsByPath(results);
14432
- const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
14872
+ const relevant = getRelevantEvidence(query);
14873
+ const isSymbolIntendedQuery = isSymbolIntended(query);
14874
+ const deduped = uniqueResultsByEvidence(results);
14875
+ const hitAt = (cutoff) => deduped.slice(0, cutoff).some(
14876
+ (result) => isRelevantResult(result.filePath, result.name, relevant, isSymbolIntendedQuery)
14877
+ );
14433
14878
  const perQuery = {
14434
14879
  id: query.id,
14435
14880
  query: query.query,
@@ -14437,13 +14882,19 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14437
14882
  retrievalMode: query.retrievalMode ?? "search",
14438
14883
  resolvedRoute: route.resolvedRoute,
14439
14884
  routedQuery: route.routedQuery,
14885
+ routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
14886
+ outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
14887
+ recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
14888
+ language: query.language,
14889
+ difficulty: query.difficulty,
14890
+ tags: query.tags,
14440
14891
  latencyMs,
14441
14892
  hitAt1: hitAt(1),
14442
14893
  hitAt3: hitAt(3),
14443
14894
  hitAt5: hitAt(5),
14444
14895
  hitAt10: hitAt(10),
14445
- reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
14446
- ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
14896
+ reciprocalRankAt10: reciprocalRankAtK(deduped, relevant, isSymbolIntendedQuery, 10),
14897
+ ndcgAt10: ndcgAtK(query, deduped, relevant, isSymbolIntendedQuery, 10),
14447
14898
  failureBucket: classifyFailureBucket(query, results, k),
14448
14899
  rawTop3DistinctRatio: distinctTopKRatio(results, 3),
14449
14900
  tokenBudget: context?.tokenBudget,
@@ -14461,6 +14912,11 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14461
14912
  function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
14462
14913
  const count = perQuery.length;
14463
14914
  const safeDiv = (value) => count === 0 ? 0 : value / count;
14915
+ const positiveQueryIds = new Set(
14916
+ queries.filter((query) => query.expected.expectedOutcome !== "no-results").map((query) => query.id)
14917
+ );
14918
+ const positiveCount = perQuery.filter((query) => positiveQueryIds.has(query.id)).length;
14919
+ const safePositiveDiv = (value) => positiveCount === 0 ? 0 : value / positiveCount;
14464
14920
  const sum = {
14465
14921
  hitAt1: 0,
14466
14922
  hitAt3: 0,
@@ -14482,27 +14938,52 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
14482
14938
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
14483
14939
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
14484
14940
  const contextTokenUnits = totalContextResponseTokens / 1e3;
14941
+ let routeMatchedCount = 0;
14942
+ let routeExpectedCount = 0;
14943
+ let outcomeMatchedCount = 0;
14944
+ let outcomeExpectedCount = 0;
14945
+ let recoveryMatchedCount = 0;
14946
+ let recoveryExpectedCount = 0;
14485
14947
  for (const query of perQuery) {
14486
- if (query.hitAt1) sum.hitAt1 += 1;
14487
- if (query.hitAt3) sum.hitAt3 += 1;
14488
- if (query.hitAt5) sum.hitAt5 += 1;
14489
- if (query.hitAt10) sum.hitAt10 += 1;
14490
- sum.mrrAt10 += query.reciprocalRankAt10;
14491
- sum.ndcgAt10 += query.ndcgAt10;
14492
- sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
14948
+ if (positiveQueryIds.has(query.id)) {
14949
+ if (query.hitAt1) sum.hitAt1 += 1;
14950
+ if (query.hitAt3) sum.hitAt3 += 1;
14951
+ if (query.hitAt5) sum.hitAt5 += 1;
14952
+ if (query.hitAt10) sum.hitAt10 += 1;
14953
+ sum.mrrAt10 += query.reciprocalRankAt10;
14954
+ sum.ndcgAt10 += query.ndcgAt10;
14955
+ }
14956
+ sum.distinctTop3Ratio += distinctTopKRatio(uniqueResultsByPath(query.results), 3);
14493
14957
  sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
14494
14958
  if (query.failureBucket) {
14495
14959
  failureBuckets[query.failureBucket] += 1;
14496
14960
  }
14961
+ if (query.routeMatched !== void 0) {
14962
+ routeExpectedCount += 1;
14963
+ if (query.routeMatched) {
14964
+ routeMatchedCount += 1;
14965
+ }
14966
+ }
14967
+ if (query.outcomeMatched !== void 0) {
14968
+ outcomeExpectedCount += 1;
14969
+ if (query.outcomeMatched) outcomeMatchedCount += 1;
14970
+ }
14971
+ if (query.recoveryMatched !== void 0) {
14972
+ recoveryExpectedCount += 1;
14973
+ if (query.recoveryMatched) recoveryMatchedCount += 1;
14974
+ }
14497
14975
  }
14498
14976
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
14499
14977
  return {
14500
- hitAt1: safeDiv(sum.hitAt1),
14501
- hitAt3: safeDiv(sum.hitAt3),
14502
- hitAt5: safeDiv(sum.hitAt5),
14503
- hitAt10: safeDiv(sum.hitAt10),
14504
- mrrAt10: safeDiv(sum.mrrAt10),
14505
- ndcgAt10: safeDiv(sum.ndcgAt10),
14978
+ hitAt1: safePositiveDiv(sum.hitAt1),
14979
+ hitAt3: safePositiveDiv(sum.hitAt3),
14980
+ hitAt5: safePositiveDiv(sum.hitAt5),
14981
+ hitAt10: safePositiveDiv(sum.hitAt10),
14982
+ mrrAt10: safePositiveDiv(sum.mrrAt10),
14983
+ ndcgAt10: safePositiveDiv(sum.ndcgAt10),
14984
+ routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
14985
+ outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
14986
+ recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
14506
14987
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
14507
14988
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
14508
14989
  latencyMs: {
@@ -14709,6 +15190,9 @@ function isRecord3(value) {
14709
15190
  function isStringArray4(value) {
14710
15191
  return Array.isArray(value) && value.every((item) => typeof item === "string");
14711
15192
  }
15193
+ function isNonEmptyString(value) {
15194
+ return typeof value === "string" && value.trim().length > 0;
15195
+ }
14712
15196
  function asPositiveNumber(value, path29) {
14713
15197
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
14714
15198
  throw new Error(`${path29} must be a non-negative number`);
@@ -14723,11 +15207,109 @@ function parseQueryType(value, path29) {
14723
15207
  `${path29} must be one of: definition, implementation-intent, similarity, keyword-heavy, conceptual`
14724
15208
  );
14725
15209
  }
15210
+ function parseExpectedRoute(value, path29) {
15211
+ if (value === void 0) return void 0;
15212
+ if (value === "search" || value === "definition") return value;
15213
+ throw new Error(`${path29} must be one of: search, definition`);
15214
+ }
15215
+ function parseExpectedOutcome(value, path29) {
15216
+ if (value === void 0) return void 0;
15217
+ if (value === "results" || value === "no-results") {
15218
+ return value;
15219
+ }
15220
+ throw new Error(`${path29} must be one of: results, no-results`);
15221
+ }
15222
+ function parseRecoveryExpectation(value, path29) {
15223
+ if (value === void 0) return void 0;
15224
+ if (value === "none" || value === "filter-relaxed") {
15225
+ return value;
15226
+ }
15227
+ throw new Error(`${path29} must be one of: none, filter-relaxed`);
15228
+ }
15229
+ function parseQueryDifficulty(value, path29) {
15230
+ if (value === void 0) return void 0;
15231
+ if (value === "easy" || value === "medium" || value === "hard") {
15232
+ return value;
15233
+ }
15234
+ throw new Error(`${path29} must be one of: easy, medium, hard`);
15235
+ }
15236
+ function parseQueryTags(value, path29) {
15237
+ if (value === void 0) return void 0;
15238
+ if (!isStringArray4(value) || value.some((tag) => tag.trim().length === 0)) {
15239
+ throw new Error(`${path29} must be an array of non-empty strings`);
15240
+ }
15241
+ if (value.length > 16) {
15242
+ throw new Error(`${path29} must contain at most 16 tags`);
15243
+ }
15244
+ return value;
15245
+ }
15246
+ function parseQueryArgs(value, path29) {
15247
+ if (value === void 0) return void 0;
15248
+ if (!isRecord3(value)) {
15249
+ throw new Error(`${path29} must be an object`);
15250
+ }
15251
+ const symbol = parseStringOrUndefined(value.symbol, `${path29}.symbol`);
15252
+ const fileType = parseStringOrUndefined(value.fileType, `${path29}.fileType`);
15253
+ const directory = parseStringOrUndefined(value.directory, `${path29}.directory`);
15254
+ return {
15255
+ ...symbol !== void 0 ? { symbol } : {},
15256
+ ...fileType !== void 0 ? { fileType } : {},
15257
+ ...directory !== void 0 ? { directory } : {}
15258
+ };
15259
+ }
15260
+ var SEMVER_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
15261
+ function parseSemanticVersion(value, path29) {
15262
+ if (!isNonEmptyString(value)) {
15263
+ throw new Error(`${path29} must be a non-empty string`);
15264
+ }
15265
+ if (!SEMVER_VERSION_PATTERN.test(value)) {
15266
+ throw new Error(`${path29} must be a valid semantic version (MAJOR.MINOR.PATCH)`);
15267
+ }
15268
+ return value;
15269
+ }
14726
15270
  function parseRetrievalMode(value, path29) {
14727
15271
  if (value === void 0 || value === "search") return "search";
14728
15272
  if (value === "context") return value;
14729
15273
  throw new Error(`${path29} must be one of: search, context`);
14730
15274
  }
15275
+ function parseStringOrUndefined(value, path29) {
15276
+ if (value === void 0 || value === null) return void 0;
15277
+ if (!isNonEmptyString(value)) {
15278
+ throw new Error(`${path29} must be a non-empty string`);
15279
+ }
15280
+ return value;
15281
+ }
15282
+ function parseGradedEvidence(value, path29) {
15283
+ if (value === void 0) return [];
15284
+ if (!Array.isArray(value)) {
15285
+ throw new Error(`${path29} must be an array`);
15286
+ }
15287
+ return value.map((entry, index) => {
15288
+ if (!isRecord3(entry)) {
15289
+ throw new Error(`${path29}[${index}] must be an object`);
15290
+ }
15291
+ const evidencePath = parseStringOrUndefined(entry.path, `${path29}[${index}].path`);
15292
+ if (evidencePath === void 0) {
15293
+ throw new Error(`${path29}[${index}].path is required`);
15294
+ }
15295
+ const symbol = parseStringOrUndefined(entry.symbol, `${path29}[${index}].symbol`);
15296
+ const relevance = parseEvidenceRelevance(entry.relevance, `${path29}[${index}].relevance`);
15297
+ return {
15298
+ path: evidencePath,
15299
+ ...symbol !== void 0 ? { symbol } : {},
15300
+ relevance
15301
+ };
15302
+ });
15303
+ }
15304
+ function parseEvidenceRelevance(value, path29) {
15305
+ if (value === void 0) {
15306
+ throw new Error(`${path29} is required`);
15307
+ }
15308
+ if (value !== 1 && value !== 2 && value !== 3) {
15309
+ throw new Error(`${path29} must be 1, 2, or 3`);
15310
+ }
15311
+ return value;
15312
+ }
14731
15313
  function parseExpected(input, path29) {
14732
15314
  if (!isRecord3(input)) {
14733
15315
  throw new Error(`${path29} must be an object`);
@@ -14736,10 +15318,18 @@ function parseExpected(input, path29) {
14736
15318
  const acceptableFilesRaw = input.acceptableFiles;
14737
15319
  const symbolRaw = input.symbol;
14738
15320
  const branchRaw = input.branch;
14739
- const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
15321
+ const expectedRouteRaw = input.expectedRoute;
15322
+ const expectedOutcomeRaw = input.expectedOutcome;
15323
+ const recoveryExpectationRaw = input.recoveryExpectation;
15324
+ const gradedEvidenceRaw = input.gradedEvidence;
15325
+ const filePath = parseStringOrUndefined(filePathRaw, `${path29}.filePath`);
14740
15326
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
14741
- if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
14742
- throw new Error(`${path29} must include either expected.filePath or expected.acceptableFiles`);
15327
+ const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path29}.gradedEvidence`);
15328
+ const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path29}.expectedOutcome`);
15329
+ if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
15330
+ throw new Error(
15331
+ `${path29} must include expected.filePath, expected.acceptableFiles, or expected.gradedEvidence`
15332
+ );
14743
15333
  }
14744
15334
  if (acceptableFilesRaw !== void 0 && !isStringArray4(acceptableFilesRaw)) {
14745
15335
  throw new Error(`${path29}.acceptableFiles must be an array of strings`);
@@ -14750,13 +15340,25 @@ function parseExpected(input, path29) {
14750
15340
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
14751
15341
  throw new Error(`${path29}.branch must be a string when provided`);
14752
15342
  }
15343
+ const expectedRoute = parseExpectedRoute(expectedRouteRaw, `${path29}.expectedRoute`);
15344
+ const recoveryExpectation = parseRecoveryExpectation(
15345
+ recoveryExpectationRaw,
15346
+ `${path29}.recoveryExpectation`
15347
+ );
14753
15348
  return {
14754
15349
  filePath,
14755
15350
  acceptableFiles,
14756
15351
  symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
14757
- branch: typeof branchRaw === "string" ? branchRaw : void 0
15352
+ branch: typeof branchRaw === "string" ? branchRaw : void 0,
15353
+ expectedRoute,
15354
+ expectedOutcome,
15355
+ recoveryExpectation,
15356
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
14758
15357
  };
14759
15358
  }
15359
+ function parseQueryLanguage(value, path29) {
15360
+ return parseStringOrUndefined(value, path29);
15361
+ }
14760
15362
  function parseQuery(input, index) {
14761
15363
  const path29 = `queries[${index}]`;
14762
15364
  if (!isRecord3(input)) {
@@ -14767,6 +15369,10 @@ function parseQuery(input, index) {
14767
15369
  const queryType = input.queryType;
14768
15370
  const retrievalMode = input.retrievalMode;
14769
15371
  const expected = input.expected;
15372
+ const language = input.language;
15373
+ const difficulty = input.difficulty;
15374
+ const tags = input.tags;
15375
+ const args = input.args;
14770
15376
  if (typeof id !== "string" || id.trim().length === 0) {
14771
15377
  throw new Error(`${path29}.id must be a non-empty string`);
14772
15378
  }
@@ -14778,6 +15384,10 @@ function parseQuery(input, index) {
14778
15384
  query,
14779
15385
  queryType: parseQueryType(queryType, `${path29}.queryType`),
14780
15386
  retrievalMode: parseRetrievalMode(retrievalMode, `${path29}.retrievalMode`),
15387
+ language: parseQueryLanguage(language, `${path29}.language`),
15388
+ difficulty: parseQueryDifficulty(difficulty, `${path29}.difficulty`),
15389
+ args: parseQueryArgs(args, `${path29}.args`),
15390
+ tags: parseQueryTags(tags, `${path29}.tags`),
14781
15391
  expected: parseExpected(expected, `${path29}.expected`)
14782
15392
  };
14783
15393
  }
@@ -14789,9 +15399,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14789
15399
  const name = raw.name;
14790
15400
  const description = raw.description;
14791
15401
  const queriesRaw = raw.queries;
14792
- if (typeof version !== "string" || version.trim().length === 0) {
14793
- throw new Error(`${sourceLabel}.version must be a non-empty string`);
14794
- }
15402
+ const validatedVersion = parseSemanticVersion(version, `${sourceLabel}.version`);
14795
15403
  if (typeof name !== "string" || name.trim().length === 0) {
14796
15404
  throw new Error(`${sourceLabel}.name must be a non-empty string`);
14797
15405
  }
@@ -14813,7 +15421,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14813
15421
  idSet.add(query.id);
14814
15422
  }
14815
15423
  return {
14816
- version,
15424
+ version: validatedVersion,
14817
15425
  name,
14818
15426
  description: typeof description === "string" ? description : void 0,
14819
15427
  queries
@@ -14873,16 +15481,8 @@ function parseBudget(raw, sourceLabel) {
14873
15481
  "p95LatencyMaxAbsoluteMs",
14874
15482
  sourceLabel
14875
15483
  ),
14876
- minHitAt5: parseThresholdValue(
14877
- thresholds.minHitAt5,
14878
- "minHitAt5",
14879
- sourceLabel
14880
- ),
14881
- minMrrAt10: parseThresholdValue(
14882
- thresholds.minMrrAt10,
14883
- "minMrrAt10",
14884
- sourceLabel
14885
- ),
15484
+ minHitAt5: parseThresholdValue(thresholds.minHitAt5, "minHitAt5", sourceLabel),
15485
+ minMrrAt10: parseThresholdValue(thresholds.minMrrAt10, "minMrrAt10", sourceLabel),
14886
15486
  minRawDistinctTop3Ratio: parseThresholdValue(
14887
15487
  thresholds.minRawDistinctTop3Ratio,
14888
15488
  "minRawDistinctTop3Ratio",
@@ -14932,6 +15532,26 @@ function loadBudget(budgetPath) {
14932
15532
  }
14933
15533
 
14934
15534
  // src/eval/runner.ts
15535
+ function normalizeForFingerprint(value) {
15536
+ if (Array.isArray(value)) {
15537
+ return value.map((entry) => normalizeForFingerprint(entry));
15538
+ }
15539
+ if (value && typeof value === "object") {
15540
+ const normalized = {};
15541
+ for (const key of Object.keys(value).sort()) {
15542
+ const normalizedValue = normalizeForFingerprint(value[key]);
15543
+ if (normalizedValue !== void 0) {
15544
+ normalized[key] = normalizedValue;
15545
+ }
15546
+ }
15547
+ return normalized;
15548
+ }
15549
+ return value;
15550
+ }
15551
+ function buildDatasetFingerprint(dataset) {
15552
+ const canonical = JSON.stringify(normalizeForFingerprint(dataset));
15553
+ return crypto.createHash("sha256").update(canonical).digest("hex");
15554
+ }
14935
15555
  async function runEvaluation(options) {
14936
15556
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
14937
15557
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -14956,27 +15576,40 @@ async function runEvaluation(options) {
14956
15576
  const start = import_perf_hooks2.performance.now();
14957
15577
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
14958
15578
  query: query.query,
15579
+ symbol: query.args?.symbol,
15580
+ fileType: query.args?.fileType,
15581
+ directory: query.args?.directory,
14959
15582
  limit: 10,
14960
15583
  tokenBudget: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET
14961
15584
  }, {
14962
- lookup: (symbol, limit, _scope) => indexer.search(symbol, limit, {
15585
+ lookup: (symbol, limit, scope) => indexer.search(symbol, limit, {
14963
15586
  metadataOnly: true,
14964
15587
  filterByBranch: !!query.expected.branch,
14965
- definitionIntent: true
15588
+ definitionIntent: true,
15589
+ fileType: scope.fileType,
15590
+ directory: scope.directory
14966
15591
  }),
14967
- search: (searchQuery, limit, _scope) => indexer.search(searchQuery, limit, {
15592
+ search: (searchQuery, limit, scope) => indexer.search(searchQuery, limit, {
14968
15593
  metadataOnly: true,
14969
15594
  filterByBranch: !!query.expected.branch,
14970
- definitionIntent: false
15595
+ definitionIntent: false,
15596
+ fileType: scope.fileType,
15597
+ directory: scope.directory
14971
15598
  })
14972
15599
  }) : void 0;
14973
15600
  const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
14974
15601
  metadataOnly: true,
14975
- filterByBranch: !!query.expected.branch
15602
+ filterByBranch: !!query.expected.branch,
15603
+ fileType: query.args?.fileType,
15604
+ directory: query.args?.directory
14976
15605
  });
14977
15606
  const elapsed = import_perf_hooks2.performance.now() - start;
14978
15607
  const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
14979
15608
  const routedQuery = contextResult?.details?.routedQuery ?? query.query;
15609
+ const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
15610
+ const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
15611
+ const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
15612
+ const recoveryUsed = recoveryAttempts.length > 1 || recoveryAttempts.some((attempt) => attempt.relaxedFields.length > 0);
14980
15613
  const materialized = result.map((item) => ({
14981
15614
  filePath: item.filePath,
14982
15615
  startLine: item.startLine,
@@ -14993,7 +15626,9 @@ async function runEvaluation(options) {
14993
15626
  responseTokens: contextResult.details.tokenEstimate,
14994
15627
  candidateCount: contextResult.details.candidateCount ?? 0,
14995
15628
  deduplicatedCount: contextResult.details.deduplicatedCount ?? 0,
14996
- omittedCount: contextResult.details.omittedCount ?? 0
15629
+ omittedCount: contextResult.details.omittedCount ?? 0,
15630
+ recoveryUsed,
15631
+ recoveryRelaxed
14997
15632
  } : void 0));
14998
15633
  }
14999
15634
  const logger = indexer.getLogger();
@@ -15005,6 +15640,7 @@ async function runEvaluation(options) {
15005
15640
  datasetPath,
15006
15641
  datasetName: dataset.name,
15007
15642
  datasetVersion: dataset.version,
15643
+ datasetFingerprint: buildDatasetFingerprint(dataset),
15008
15644
  queryCount: dataset.queries.length,
15009
15645
  topK: 10,
15010
15646
  searchConfig: {
@@ -17922,7 +18558,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
17922
18558
  if (isGitRepo(projectRoot)) {
17923
18559
  gitWatcher = new GitHeadWatcher(projectRoot);
17924
18560
  gitWatcher.start(async (oldBranch, newBranch) => {
17925
- console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
18561
+ getIndexer().getLogger().branch("info", "Branch changed", {
18562
+ oldBranch,
18563
+ newBranch
18564
+ });
17926
18565
  requestReindex();
17927
18566
  });
17928
18567
  }