opencode-codebase-index 0.19.1 → 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");
@@ -4037,9 +4052,21 @@ function parseFiles(files) {
4037
4052
  return result.map((f) => ({
4038
4053
  path: f.path,
4039
4054
  chunks: f.chunks.map(mapChunk),
4055
+ symbols: (f.symbols ?? []).map(mapParsedSymbol),
4040
4056
  hash: f.hash
4041
4057
  }));
4042
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
+ }
4043
4070
  function mapChunk(c) {
4044
4071
  return {
4045
4072
  content: c.content,
@@ -6850,6 +6877,7 @@ var INDEX_METADATA_VERSION = "1";
6850
6877
  var EMBEDDING_STRATEGY_VERSION = "2";
6851
6878
  var SWIFT_PARSER_VERSION = "1";
6852
6879
  var METAL_PARSER_VERSION = "1";
6880
+ var SYMBOL_EXTRACTOR_VERSION = "1";
6853
6881
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
6854
6882
  var RANK_HYBRID_CACHE_LIMIT = 256;
6855
6883
  function createPendingChunkStorageText(texts) {
@@ -7151,7 +7179,7 @@ function classifyQueryIntentRaw(query) {
7151
7179
  return "neutral";
7152
7180
  }
7153
7181
  function isImplementationChunkType(chunkType) {
7154
- return [
7182
+ return CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunkType) || [
7155
7183
  "export_statement",
7156
7184
  "function",
7157
7185
  "function_declaration",
@@ -7596,7 +7624,7 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
7596
7624
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
7597
7625
  return [...promoted, ...remainder];
7598
7626
  }
7599
- function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7627
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7600
7628
  if (!prioritizeSourcePaths) {
7601
7629
  return [];
7602
7630
  }
@@ -7610,14 +7638,14 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7610
7638
  const primaryHint = extractPrimaryIdentifierQueryHint(query);
7611
7639
  const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
7612
7640
  if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
7613
- return;
7641
+ return false;
7614
7642
  }
7615
7643
  const chunkType = chunk.nodeType ?? "other";
7616
7644
  if (!isImplementationChunkType(chunkType)) {
7617
- return;
7645
+ return false;
7618
7646
  }
7619
7647
  if (!isLikelyImplementationPath2(chunk.filePath)) {
7620
- return;
7648
+ return false;
7621
7649
  }
7622
7650
  const nameLower = (chunk.name ?? "").toLowerCase();
7623
7651
  const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
@@ -7639,6 +7667,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7639
7667
  }
7640
7668
  });
7641
7669
  }
7670
+ return true;
7642
7671
  };
7643
7672
  const normalizedHints = identifierHints.flatMap((hint) => [
7644
7673
  hint,
@@ -7660,12 +7689,46 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7660
7689
  dedupSymbols.set(symbol.id, symbol);
7661
7690
  }
7662
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
+ }
7663
7698
  const chunks = database.getChunksByFile(symbol.filePath);
7699
+ let foundCoveringChunk = false;
7664
7700
  for (const chunk of chunks) {
7665
7701
  if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
7666
7702
  continue;
7667
7703
  }
7668
- 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
+ });
7669
7732
  }
7670
7733
  }
7671
7734
  const dedupChunksByName = /* @__PURE__ */ new Map();
@@ -7673,6 +7736,9 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7673
7736
  dedupChunksByName.set(chunk.chunkId, chunk);
7674
7737
  }
7675
7738
  for (const chunk of dedupChunksByName.values()) {
7739
+ if (filePathHint && !pathMatchesHint(chunk.filePath, filePathHint)) {
7740
+ continue;
7741
+ }
7676
7742
  upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7677
7743
  }
7678
7744
  }
@@ -8214,6 +8280,10 @@ var Indexer = class _Indexer {
8214
8280
  const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
8215
8281
  return `${key}.${projectHash}`;
8216
8282
  }
8283
+ getSymbolExtractorVersionMetadataKey(catalogIdentity = this.getBranchCatalogIdentity()) {
8284
+ const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
8285
+ return `index.symbolExtractorVersion.${hashContent(branchKey).slice(0, 24)}`;
8286
+ }
8217
8287
  hasProjectForceReembedPending() {
8218
8288
  return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
8219
8289
  }
@@ -9557,7 +9627,8 @@ var Indexer = class _Indexer {
9557
9627
  }
9558
9628
  const branchKey = this.getBranchCatalogKey();
9559
9629
  const alreadyIndexed = database.getBranchChunkIds(branchKey).length > 0 && database.getBranchSymbolIds(branchKey).length > 0;
9560
- if (alreadyIndexed && this.getStoredBranchCommit(database) === normalizedCommit) {
9630
+ const symbolsCurrent = database.getMetadata(this.getSymbolExtractorVersionMetadataKey()) === SYMBOL_EXTRACTOR_VERSION;
9631
+ if (alreadyIndexed && symbolsCurrent && this.getStoredBranchCommit(database) === normalizedCommit) {
9561
9632
  return { prepared: false };
9562
9633
  }
9563
9634
  const stats = await this.indexUnlocked(onProgress, [], true);
@@ -9616,6 +9687,8 @@ var Indexer = class _Indexer {
9616
9687
  const reparseCachedSwiftFiles = database.getMetadata(swiftParserMetadataKey) !== SWIFT_PARSER_VERSION;
9617
9688
  const metalParserMetadataKey = this.getMetalParserVersionMetadataKey();
9618
9689
  const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
9690
+ const symbolExtractorMetadataKey = this.getSymbolExtractorVersionMetadataKey();
9691
+ const refreshCachedSymbols = database.getMetadata(symbolExtractorMetadataKey) !== SYMBOL_EXTRACTOR_VERSION;
9619
9692
  if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
9620
9693
  (filePath) => path13.extname(filePath).toLowerCase() === ".swift"
9621
9694
  )) {
@@ -9659,7 +9732,7 @@ var Indexer = class _Indexer {
9659
9732
  );
9660
9733
  const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path13.extname(canonicalPath).toLowerCase() === ".swift";
9661
9734
  const requiresMetalParserUpgrade = reparseCachedMetalFiles && path13.extname(canonicalPath).toLowerCase() === ".metal";
9662
- if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade) {
9735
+ if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
9663
9736
  unchangedFilePaths.add(canonicalPath);
9664
9737
  this.logger.recordCacheHit();
9665
9738
  } else {
@@ -9870,37 +9943,27 @@ var Indexer = class _Indexer {
9870
9943
  const parsed = parsedFiles[i];
9871
9944
  const changedFile = changedFiles[i];
9872
9945
  const fileSymbols = [];
9873
- for (const chunk of parsed.chunks) {
9874
- if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
9875
- const existingMetalSymbol = chunk.language === "metal" ? fileSymbols.find(
9876
- (symbol2) => symbol2.name === chunk.name && symbol2.kind === chunk.chunkType && symbol2.startLine <= chunk.endLine && chunk.startLine <= symbol2.endLine
9877
- ) : void 0;
9878
- if (existingMetalSymbol) {
9879
- existingMetalSymbol.startLine = Math.min(existingMetalSymbol.startLine, chunk.startLine);
9880
- existingMetalSymbol.endLine = Math.max(existingMetalSymbol.endLine, chunk.endLine);
9881
- existingMetalSymbol.startCol = Math.min(existingMetalSymbol.startCol, chunk.startCol ?? 0);
9882
- existingMetalSymbol.endCol = Math.max(existingMetalSymbol.endCol, chunk.endCol ?? 0);
9883
- continue;
9884
- }
9946
+ for (const parsedSymbol of parsed.symbols) {
9947
+ if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) continue;
9885
9948
  const preparedNamespace = this.getPreparedBranchNamespace();
9886
9949
  const symbolId = `sym_${hashContent(
9887
- (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
9888
9951
  ).slice(0, 16)}`;
9889
9952
  const symbol = {
9890
9953
  id: symbolId,
9891
9954
  filePath: parsed.path,
9892
- name: chunk.name,
9893
- kind: chunk.chunkType,
9894
- startLine: chunk.startLine,
9895
- startCol: chunk.startCol ?? 0,
9896
- endLine: chunk.endLine,
9897
- endCol: chunk.endCol ?? 0,
9898
- 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
9899
9962
  };
9900
9963
  fileSymbols.push(symbol);
9901
9964
  allSymbolIds.add(symbolId);
9902
9965
  }
9903
- const fileLanguage = parsed.chunks[0]?.language;
9966
+ const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
9904
9967
  const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
9905
9968
  const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
9906
9969
  const symbolsByName = /* @__PURE__ */ new Map();
@@ -10016,6 +10079,7 @@ var Indexer = class _Indexer {
10016
10079
  }
10017
10080
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10018
10081
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10082
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10019
10083
  this.saveBranchCommit(database, indexedCommit);
10020
10084
  this.saveIndexMetadata(configuredProviderInfo);
10021
10085
  this.indexCompatibility = { compatible: true };
@@ -10052,6 +10116,7 @@ var Indexer = class _Indexer {
10052
10116
  }
10053
10117
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10054
10118
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10119
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10055
10120
  this.saveBranchCommit(database, indexedCommit);
10056
10121
  this.saveIndexMetadata(configuredProviderInfo);
10057
10122
  this.indexCompatibility = { compatible: true };
@@ -10331,6 +10396,7 @@ var Indexer = class _Indexer {
10331
10396
  }
10332
10397
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10333
10398
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10399
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10334
10400
  this.saveBranchCommit(database, indexedCommit);
10335
10401
  this.saveIndexMetadata(configuredProviderInfo);
10336
10402
  this.indexCompatibility = { compatible: true };
@@ -10469,10 +10535,11 @@ var Indexer = class _Indexer {
10469
10535
  const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
10470
10536
  const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
10471
10537
  let branchChunkIds = null;
10538
+ let branchSymbolIds = null;
10472
10539
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
10473
- branchChunkIds = new Set(
10474
- this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
10475
- );
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)));
10476
10543
  }
10477
10544
  const prefilterStartTime = import_perf_hooks.performance.now();
10478
10545
  const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
@@ -10544,6 +10611,7 @@ var Indexer = class _Indexer {
10544
10611
  query,
10545
10612
  database,
10546
10613
  branchChunkIds,
10614
+ branchSymbolIds,
10547
10615
  maxResults,
10548
10616
  union,
10549
10617
  sourceIntent
@@ -10557,7 +10625,7 @@ var Indexer = class _Indexer {
10557
10625
  (r) => isLikelyImplementationPath2(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
10558
10626
  );
10559
10627
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
10560
- 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) : [];
10561
10629
  const finalResults = filtered.length > 0 ? filtered : identifierFallback;
10562
10630
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
10563
10631
  this.logger.recordSearch(totalSearchMs, {
@@ -10714,7 +10782,7 @@ var Indexer = class _Indexer {
10714
10782
  const extension = path13.extname(filePath).toLowerCase();
10715
10783
  return extension === ".php" || extension === ".c" || extension === ".cc" || extension === ".cpp" || extension === ".cxx";
10716
10784
  });
10717
- 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) {
10718
10786
  return { readable: true, current: false, reason: "migration-required" };
10719
10787
  }
10720
10788
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -11422,7 +11490,10 @@ var Indexer = class _Indexer {
11422
11490
  const requestedRef = opts.pr !== void 0 ? expectedCommit : headRefName ?? resolvedBranch;
11423
11491
  const storedCommit = this.getStoredBranchCommit(database, catalogIdentity);
11424
11492
  const catalogIdentityMatches = storedCommit === expectedCommit;
11425
- 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) {
11426
11497
  if (!resolvedBranch || resolvedBranch === "default") {
11427
11498
  throw new Error("Run index_codebase first to build the call graph and symbol index for this project.");
11428
11499
  }
@@ -11738,8 +11809,17 @@ function fitTextToContextBudget(text, tokenBudget) {
11738
11809
  function normalizedLineRange(result) {
11739
11810
  return result.startLine <= result.endLine ? { start: result.startLine, end: result.endLine } : { start: result.endLine, end: result.startLine };
11740
11811
  }
11741
- function rankContextCandidates(results) {
11742
- 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
+ });
11743
11823
  }
11744
11824
  function deduplicateContextCandidates(candidates) {
11745
11825
  const acceptedByFile = /* @__PURE__ */ new Map();
@@ -11828,7 +11908,12 @@ function buildContextPack(results, options = {}) {
11828
11908
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
11829
11909
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
11830
11910
  const candidateCount = results.length;
11831
- const deduplicated = deduplicateContextCandidates(rankContextCandidates(results));
11911
+ const deduplicated = deduplicateContextCandidates(
11912
+ rankContextCandidates(
11913
+ results,
11914
+ options.preferImplementationPaths ?? false
11915
+ )
11916
+ );
11832
11917
  const diversified = diversifyContextCandidates(deduplicated);
11833
11918
  const duplicateCount = candidateCount - deduplicated.length;
11834
11919
  const selectable = diversified.slice(0, maxResults);
@@ -12200,7 +12285,7 @@ ${truncateContent(r.content)}
12200
12285
  }
12201
12286
 
12202
12287
  // src/utils/effectiveness-metrics.ts
12203
- var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 2;
12288
+ var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 3;
12204
12289
  var MAX_EFFECTIVENESS_COUNTER = 1e9;
12205
12290
  var EFFECTIVENESS_TOOL_ROUTES = [
12206
12291
  "context-conceptual",
@@ -12246,6 +12331,9 @@ var EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS = ["none", "directory", "file-type",
12246
12331
  function emptyCounterMap(values) {
12247
12332
  return Object.fromEntries(values.map((value) => [value, 0]));
12248
12333
  }
12334
+ function emptyRouteCounterMap(routes, values) {
12335
+ return Object.fromEntries(routes.map((route) => [route, emptyCounterMap(values)]));
12336
+ }
12249
12337
  function boundedNumber(value) {
12250
12338
  if (value === void 0 || !Number.isFinite(value)) return 0;
12251
12339
  return Math.max(0, Math.floor(value));
@@ -12294,6 +12382,11 @@ function allowedValue(value, allowed, fallback) {
12294
12382
  function cloneCounterMap(counters) {
12295
12383
  return { ...counters };
12296
12384
  }
12385
+ function cloneRouteCounterMap(counters) {
12386
+ return Object.fromEntries(
12387
+ EFFECTIVENESS_TOOL_ROUTES.map((route) => [route, cloneCounterMap(counters[route])])
12388
+ );
12389
+ }
12297
12390
  var EffectivenessMetrics = class {
12298
12391
  constructor(counterCap = MAX_EFFECTIVENESS_COUNTER) {
12299
12392
  this.counterCap = counterCap;
@@ -12309,7 +12402,7 @@ var EffectivenessMetrics = class {
12309
12402
  lifetime: "process",
12310
12403
  reset: "index_metrics-reset-or-process-exit",
12311
12404
  maxCounterValue: this.counterCap,
12312
- dimensions: "bounded-host-and-category-only"
12405
+ dimensions: "bounded-route-and-bucketed-performance-only"
12313
12406
  },
12314
12407
  totalCalls: 0,
12315
12408
  toolRoute: emptyCounterMap(EFFECTIVENESS_TOOL_ROUTES),
@@ -12321,7 +12414,14 @@ var EffectivenessMetrics = class {
12321
12414
  tokenBudget: emptyCounterMap(EFFECTIVENESS_TOKEN_BUDGET_BUCKETS),
12322
12415
  returnedTokenEstimate: emptyCounterMap(EFFECTIVENESS_RETURNED_TOKEN_BUCKETS),
12323
12416
  exactHandoffEmitted: emptyCounterMap(EFFECTIVENESS_BOOLEAN_BUCKETS),
12324
- 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
+ )
12325
12425
  };
12326
12426
  }
12327
12427
  increment(counters, key) {
@@ -12343,12 +12443,19 @@ var EffectivenessMetrics = class {
12343
12443
  this.increment(this.snapshot.hostMode, host);
12344
12444
  this.increment(this.snapshot.outcome, outcome);
12345
12445
  this.increment(this.snapshot.recoveryUsed, recoveryUsed);
12346
- this.increment(this.snapshot.resultCount, resultCountBucket(event.resultCount));
12347
- 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);
12348
12451
  this.increment(this.snapshot.tokenBudget, tokenBudgetBucket(event.tokenBudget));
12349
- this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucket(event.returnedTokenEstimate));
12452
+ this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucketValue);
12350
12453
  this.increment(this.snapshot.exactHandoffEmitted, exactHandoffEmitted);
12351
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);
12352
12459
  }
12353
12460
  getSnapshot() {
12354
12461
  return {
@@ -12363,7 +12470,11 @@ var EffectivenessMetrics = class {
12363
12470
  tokenBudget: cloneCounterMap(this.snapshot.tokenBudget),
12364
12471
  returnedTokenEstimate: cloneCounterMap(this.snapshot.returnedTokenEstimate),
12365
12472
  exactHandoffEmitted: cloneCounterMap(this.snapshot.exactHandoffEmitted),
12366
- 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)
12367
12478
  };
12368
12479
  }
12369
12480
  reset() {
@@ -12382,6 +12493,7 @@ function resetProcessEffectivenessMetrics() {
12382
12493
  }
12383
12494
  function formatEffectivenessMetrics(snapshot) {
12384
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("; ");
12385
12497
  const lines = [
12386
12498
  `Privacy-safe effectiveness (schema v${snapshot.schemaVersion}):`,
12387
12499
  ` Retention: ${snapshot.retention.storage}, ${snapshot.retention.lifetime}-lifetime; reset with index_metrics(reset=true) or process exit`,
@@ -12396,6 +12508,10 @@ function formatEffectivenessMetrics(snapshot) {
12396
12508
  ` Latency bucket: ${formatCounters(snapshot.latency)}`,
12397
12509
  ` Token-budget bucket: ${formatCounters(snapshot.tokenBudget)}`,
12398
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)}`,
12399
12515
  ` Exact handoff emitted: ${formatCounters(snapshot.exactHandoffEmitted)}`,
12400
12516
  ` Scope relaxation: ${formatCounters(snapshot.scopeRelaxation)}`,
12401
12517
  " Privacy: no queries, response text, source, symbols, paths, repository names, user identity, or stable identifiers are retained."
@@ -12407,6 +12523,103 @@ function formatEffectivenessMetrics(snapshot) {
12407
12523
  var import_fs11 = require("fs");
12408
12524
  var os6 = __toESM(require("os"), 1);
12409
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
12410
12623
  var MAX_RETRY_DELAY_MS = 1e4;
12411
12624
  var SHUTDOWN_WAIT_MS = 2e3;
12412
12625
  var coordinators = /* @__PURE__ */ new Map();
@@ -12523,7 +12736,13 @@ var AutoIndexCoordinator = class {
12523
12736
  activation = Promise.resolve();
12524
12737
  inFlight = null;
12525
12738
  activeRequest = null;
12739
+ batteryCheck = null;
12740
+ batteryIndexJob = null;
12741
+ batteryDeferredRequest = null;
12742
+ batteryRetryTimer = null;
12743
+ resolveBatteryRetry = null;
12526
12744
  pendingRequest = null;
12745
+ pendingFollowUp = null;
12527
12746
  abortController = null;
12528
12747
  stopped = false;
12529
12748
  constructor(registration) {
@@ -12536,6 +12755,7 @@ var AutoIndexCoordinator = class {
12536
12755
  };
12537
12756
  }
12538
12757
  update(registration) {
12758
+ const pauseOnBatteryChanged = registration.config.indexing.pauseBackgroundIndexingOnBattery !== this.registration.config.indexing.pauseBackgroundIndexingOnBattery;
12539
12759
  this.registration = registration;
12540
12760
  this.status.enabled = registration.config.indexing.autoIndex;
12541
12761
  this.status.blockedReason = registration.blockedReason;
@@ -12551,6 +12771,9 @@ var AutoIndexCoordinator = class {
12551
12771
  this.setState("idle", { source: void 0 });
12552
12772
  }
12553
12773
  }
12774
+ if (pauseOnBatteryChanged) {
12775
+ this.cancelBatteryRetry();
12776
+ }
12554
12777
  }
12555
12778
  activateAfter(activation) {
12556
12779
  this.activation = activation;
@@ -12572,7 +12795,26 @@ var AutoIndexCoordinator = class {
12572
12795
  if (this.stopped) {
12573
12796
  return Promise.resolve({ outcome: "stopped" });
12574
12797
  }
12575
- 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;
12576
12818
  }
12577
12819
  enqueueRequest(request) {
12578
12820
  if (this.stopped || !this.canRun(request)) {
@@ -12590,6 +12832,11 @@ var AutoIndexCoordinator = class {
12590
12832
  }
12591
12833
  if (request.source === "watcher") {
12592
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
+ });
12593
12840
  }
12594
12841
  return this.inFlight;
12595
12842
  }
@@ -12606,6 +12853,8 @@ var AutoIndexCoordinator = class {
12606
12853
  }
12607
12854
  async stop(waitForCompletion = false) {
12608
12855
  this.stopped = true;
12856
+ this.batteryDeferredRequest = null;
12857
+ this.cancelBatteryRetry();
12609
12858
  this.pendingRequest = null;
12610
12859
  this.abortController?.abort();
12611
12860
  this.setState("stopped", {
@@ -12635,10 +12884,20 @@ var AutoIndexCoordinator = class {
12635
12884
  this.inFlight = null;
12636
12885
  this.activeRequest = null;
12637
12886
  this.abortController = null;
12887
+ if (this.batteryIndexJob === job) {
12888
+ this.batteryIndexJob = null;
12889
+ this.batteryCheck = null;
12890
+ }
12638
12891
  const pending = this.pendingRequest;
12639
12892
  this.pendingRequest = null;
12640
12893
  if (pending && !this.stopped) {
12641
- 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
+ });
12642
12901
  }
12643
12902
  });
12644
12903
  return job;
@@ -12802,6 +13061,68 @@ var AutoIndexCoordinator = class {
12802
13061
  }
12803
13062
  return this.registration.safeToRun && this.registration.config.indexing.autoIndex;
12804
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
+ }
12805
13126
  };
12806
13127
  function getCoordinator(projectRoot, host) {
12807
13128
  const key = coordinatorKeysByProject.get(projectLookupKey(projectRoot, host));
@@ -12811,6 +13132,9 @@ function configureAutoIndex(projectRoot, host, config, getIndexer) {
12811
13132
  const projectKey = projectLookupKey(projectRoot, host);
12812
13133
  const safety = getProjectSafety(projectRoot, config);
12813
13134
  const registration = {
13135
+ backgroundIndexingPolicy: createBackgroundIndexingPolicy(
13136
+ config.indexing.pauseBackgroundIndexingOnBattery
13137
+ ),
12814
13138
  config,
12815
13139
  getIndexer,
12816
13140
  projectRoot,
@@ -14034,6 +14358,7 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14034
14358
  const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed);
14035
14359
  if (results.length > 0) {
14036
14360
  const heading = buildPackHeading("conceptual", decisions);
14361
+ const intent = analyzeQueryIntent(attempt.queryText);
14037
14362
  return toResult(
14038
14363
  "conceptual",
14039
14364
  attempt.queryText,
@@ -14041,7 +14366,8 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14041
14366
  tokenBudget,
14042
14367
  maxResults: limit,
14043
14368
  heading,
14044
- includeExactSearchHandoff: true
14369
+ includeExactSearchHandoff: true,
14370
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
14045
14371
  })
14046
14372
  );
14047
14373
  }
@@ -14342,17 +14668,26 @@ function percentile(values, p) {
14342
14668
  function normalizePath2(input) {
14343
14669
  return normalizePathSeparators(input);
14344
14670
  }
14345
- function uniqueResultsByPath(results) {
14671
+ function uniqueResultsByEvidence(results) {
14346
14672
  const seen = /* @__PURE__ */ new Set();
14347
14673
  const unique = [];
14348
14674
  for (const result of results) {
14349
- const normalized = normalizePath2(result.filePath);
14350
- if (seen.has(normalized)) continue;
14351
- seen.add(normalized);
14675
+ const key = `${normalizePath2(result.filePath)}::${result.name ?? ""}`;
14676
+ if (seen.has(key)) continue;
14677
+ seen.add(key);
14352
14678
  unique.push(result);
14353
14679
  }
14354
14680
  return unique;
14355
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
+ }
14356
14691
  function distinctTopKRatio(results, k) {
14357
14692
  const top = results.slice(0, k);
14358
14693
  if (top.length === 0) return 0;
@@ -14363,60 +14698,169 @@ function pathMatchesExpected(actualPath, expectedPath) {
14363
14698
  const actual = normalizePath2(actualPath);
14364
14699
  const expected = normalizePath2(expectedPath);
14365
14700
  if (actual === expected) return true;
14366
- 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;
14367
14767
  }
14368
- function getRelevantPaths(query) {
14369
- const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
14370
- const fromAcceptable = query.expected.acceptableFiles ?? [];
14371
- 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;
14372
14779
  }
14373
- function isRelevantResult(filePath, relevantPaths) {
14374
- return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
14780
+ function hasGradeBasedEvidence(query) {
14781
+ return (query.expected.gradedEvidence?.length ?? 0) > 0;
14375
14782
  }
14376
- function reciprocalRankAtK(results, relevantPaths, k) {
14377
- 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);
14378
14795
  for (let i = 0; i < top.length; i += 1) {
14379
- if (isRelevantResult(top[i].filePath, relevantPaths)) {
14796
+ if (isRelevantResult(top[i].filePath, top[i].name, relevant, isSymbolIntendedQuery)) {
14380
14797
  return 1 / (i + 1);
14381
14798
  }
14382
14799
  }
14383
14800
  return 0;
14384
14801
  }
14385
- function ndcgAtK(results, relevantPaths, k) {
14386
- 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
+ );
14387
14807
  const dcg = top.reduce((sum, result, i) => {
14388
- const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
14389
- 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);
14390
14822
  }, 0);
14391
- const idealLen = Math.min(k, relevantPaths.length);
14392
- const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
14393
- (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),
14394
14829
  0
14395
14830
  );
14396
- 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;
14397
14839
  }
14398
14840
  function isDocsOrTestsPath(filePath) {
14399
14841
  const lowered = normalizePath2(filePath).toLowerCase();
14400
14842
  return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
14401
14843
  }
14402
14844
  function classifyFailureBucket(query, results, k) {
14403
- const relevantPaths = getRelevantPaths(query);
14404
- const top = uniqueResultsByPath(results).slice(0, k);
14405
- 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
+ );
14406
14852
  if (!hasRelevantTopK) {
14853
+ const hasExpectedFileTopK = top.some((result) => isExpectedFile(result.filePath, relevant));
14854
+ if (hasExpectedFileTopK && hasSymbolRequirement(query)) {
14855
+ return "wrong-symbol";
14856
+ }
14407
14857
  return "no-relevant-hit-top-k";
14408
14858
  }
14409
- if (query.expected.symbol) {
14410
- const hasSymbol = top.some(
14411
- (result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
14412
- );
14413
- if (!hasSymbol) return "wrong-symbol";
14414
- }
14415
14859
  const top1 = top[0];
14416
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
14860
+ if (top1 && !isExpectedFile(top1.filePath, relevant) && isDocsOrTestsPath(top1.filePath)) {
14417
14861
  return "docs-tests-outranking-source";
14418
14862
  }
14419
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
14863
+ if (top1 && !isExpectedFile(top1.filePath, relevant)) {
14420
14864
  return "wrong-file";
14421
14865
  }
14422
14866
  return void 0;
@@ -14425,9 +14869,12 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14425
14869
  resolvedRoute: "search",
14426
14870
  routedQuery: query.query
14427
14871
  }, context) {
14428
- const relevantPaths = getRelevantPaths(query);
14429
- const deduped = uniqueResultsByPath(results);
14430
- 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
+ );
14431
14878
  const perQuery = {
14432
14879
  id: query.id,
14433
14880
  query: query.query,
@@ -14435,13 +14882,19 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14435
14882
  retrievalMode: query.retrievalMode ?? "search",
14436
14883
  resolvedRoute: route.resolvedRoute,
14437
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,
14438
14891
  latencyMs,
14439
14892
  hitAt1: hitAt(1),
14440
14893
  hitAt3: hitAt(3),
14441
14894
  hitAt5: hitAt(5),
14442
14895
  hitAt10: hitAt(10),
14443
- reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
14444
- ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
14896
+ reciprocalRankAt10: reciprocalRankAtK(deduped, relevant, isSymbolIntendedQuery, 10),
14897
+ ndcgAt10: ndcgAtK(query, deduped, relevant, isSymbolIntendedQuery, 10),
14445
14898
  failureBucket: classifyFailureBucket(query, results, k),
14446
14899
  rawTop3DistinctRatio: distinctTopKRatio(results, 3),
14447
14900
  tokenBudget: context?.tokenBudget,
@@ -14459,6 +14912,11 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14459
14912
  function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
14460
14913
  const count = perQuery.length;
14461
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;
14462
14920
  const sum = {
14463
14921
  hitAt1: 0,
14464
14922
  hitAt3: 0,
@@ -14480,27 +14938,52 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
14480
14938
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
14481
14939
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
14482
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;
14483
14947
  for (const query of perQuery) {
14484
- if (query.hitAt1) sum.hitAt1 += 1;
14485
- if (query.hitAt3) sum.hitAt3 += 1;
14486
- if (query.hitAt5) sum.hitAt5 += 1;
14487
- if (query.hitAt10) sum.hitAt10 += 1;
14488
- sum.mrrAt10 += query.reciprocalRankAt10;
14489
- sum.ndcgAt10 += query.ndcgAt10;
14490
- 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);
14491
14957
  sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
14492
14958
  if (query.failureBucket) {
14493
14959
  failureBuckets[query.failureBucket] += 1;
14494
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
+ }
14495
14975
  }
14496
14976
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
14497
14977
  return {
14498
- hitAt1: safeDiv(sum.hitAt1),
14499
- hitAt3: safeDiv(sum.hitAt3),
14500
- hitAt5: safeDiv(sum.hitAt5),
14501
- hitAt10: safeDiv(sum.hitAt10),
14502
- mrrAt10: safeDiv(sum.mrrAt10),
14503
- 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,
14504
14987
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
14505
14988
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
14506
14989
  latencyMs: {
@@ -14707,6 +15190,9 @@ function isRecord3(value) {
14707
15190
  function isStringArray4(value) {
14708
15191
  return Array.isArray(value) && value.every((item) => typeof item === "string");
14709
15192
  }
15193
+ function isNonEmptyString(value) {
15194
+ return typeof value === "string" && value.trim().length > 0;
15195
+ }
14710
15196
  function asPositiveNumber(value, path29) {
14711
15197
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
14712
15198
  throw new Error(`${path29} must be a non-negative number`);
@@ -14721,11 +15207,109 @@ function parseQueryType(value, path29) {
14721
15207
  `${path29} must be one of: definition, implementation-intent, similarity, keyword-heavy, conceptual`
14722
15208
  );
14723
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
+ }
14724
15270
  function parseRetrievalMode(value, path29) {
14725
15271
  if (value === void 0 || value === "search") return "search";
14726
15272
  if (value === "context") return value;
14727
15273
  throw new Error(`${path29} must be one of: search, context`);
14728
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
+ }
14729
15313
  function parseExpected(input, path29) {
14730
15314
  if (!isRecord3(input)) {
14731
15315
  throw new Error(`${path29} must be an object`);
@@ -14734,10 +15318,18 @@ function parseExpected(input, path29) {
14734
15318
  const acceptableFilesRaw = input.acceptableFiles;
14735
15319
  const symbolRaw = input.symbol;
14736
15320
  const branchRaw = input.branch;
14737
- 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`);
14738
15326
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
14739
- if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
14740
- 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
+ );
14741
15333
  }
14742
15334
  if (acceptableFilesRaw !== void 0 && !isStringArray4(acceptableFilesRaw)) {
14743
15335
  throw new Error(`${path29}.acceptableFiles must be an array of strings`);
@@ -14748,13 +15340,25 @@ function parseExpected(input, path29) {
14748
15340
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
14749
15341
  throw new Error(`${path29}.branch must be a string when provided`);
14750
15342
  }
15343
+ const expectedRoute = parseExpectedRoute(expectedRouteRaw, `${path29}.expectedRoute`);
15344
+ const recoveryExpectation = parseRecoveryExpectation(
15345
+ recoveryExpectationRaw,
15346
+ `${path29}.recoveryExpectation`
15347
+ );
14751
15348
  return {
14752
15349
  filePath,
14753
15350
  acceptableFiles,
14754
15351
  symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
14755
- 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 } : {}
14756
15357
  };
14757
15358
  }
15359
+ function parseQueryLanguage(value, path29) {
15360
+ return parseStringOrUndefined(value, path29);
15361
+ }
14758
15362
  function parseQuery(input, index) {
14759
15363
  const path29 = `queries[${index}]`;
14760
15364
  if (!isRecord3(input)) {
@@ -14765,6 +15369,10 @@ function parseQuery(input, index) {
14765
15369
  const queryType = input.queryType;
14766
15370
  const retrievalMode = input.retrievalMode;
14767
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;
14768
15376
  if (typeof id !== "string" || id.trim().length === 0) {
14769
15377
  throw new Error(`${path29}.id must be a non-empty string`);
14770
15378
  }
@@ -14776,6 +15384,10 @@ function parseQuery(input, index) {
14776
15384
  query,
14777
15385
  queryType: parseQueryType(queryType, `${path29}.queryType`),
14778
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`),
14779
15391
  expected: parseExpected(expected, `${path29}.expected`)
14780
15392
  };
14781
15393
  }
@@ -14787,9 +15399,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14787
15399
  const name = raw.name;
14788
15400
  const description = raw.description;
14789
15401
  const queriesRaw = raw.queries;
14790
- if (typeof version !== "string" || version.trim().length === 0) {
14791
- throw new Error(`${sourceLabel}.version must be a non-empty string`);
14792
- }
15402
+ const validatedVersion = parseSemanticVersion(version, `${sourceLabel}.version`);
14793
15403
  if (typeof name !== "string" || name.trim().length === 0) {
14794
15404
  throw new Error(`${sourceLabel}.name must be a non-empty string`);
14795
15405
  }
@@ -14811,7 +15421,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14811
15421
  idSet.add(query.id);
14812
15422
  }
14813
15423
  return {
14814
- version,
15424
+ version: validatedVersion,
14815
15425
  name,
14816
15426
  description: typeof description === "string" ? description : void 0,
14817
15427
  queries
@@ -14871,16 +15481,8 @@ function parseBudget(raw, sourceLabel) {
14871
15481
  "p95LatencyMaxAbsoluteMs",
14872
15482
  sourceLabel
14873
15483
  ),
14874
- minHitAt5: parseThresholdValue(
14875
- thresholds.minHitAt5,
14876
- "minHitAt5",
14877
- sourceLabel
14878
- ),
14879
- minMrrAt10: parseThresholdValue(
14880
- thresholds.minMrrAt10,
14881
- "minMrrAt10",
14882
- sourceLabel
14883
- ),
15484
+ minHitAt5: parseThresholdValue(thresholds.minHitAt5, "minHitAt5", sourceLabel),
15485
+ minMrrAt10: parseThresholdValue(thresholds.minMrrAt10, "minMrrAt10", sourceLabel),
14884
15486
  minRawDistinctTop3Ratio: parseThresholdValue(
14885
15487
  thresholds.minRawDistinctTop3Ratio,
14886
15488
  "minRawDistinctTop3Ratio",
@@ -14930,6 +15532,26 @@ function loadBudget(budgetPath) {
14930
15532
  }
14931
15533
 
14932
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
+ }
14933
15555
  async function runEvaluation(options) {
14934
15556
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
14935
15557
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -14954,27 +15576,40 @@ async function runEvaluation(options) {
14954
15576
  const start = import_perf_hooks2.performance.now();
14955
15577
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
14956
15578
  query: query.query,
15579
+ symbol: query.args?.symbol,
15580
+ fileType: query.args?.fileType,
15581
+ directory: query.args?.directory,
14957
15582
  limit: 10,
14958
15583
  tokenBudget: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET
14959
15584
  }, {
14960
- lookup: (symbol, limit, _scope) => indexer.search(symbol, limit, {
15585
+ lookup: (symbol, limit, scope) => indexer.search(symbol, limit, {
14961
15586
  metadataOnly: true,
14962
15587
  filterByBranch: !!query.expected.branch,
14963
- definitionIntent: true
15588
+ definitionIntent: true,
15589
+ fileType: scope.fileType,
15590
+ directory: scope.directory
14964
15591
  }),
14965
- search: (searchQuery, limit, _scope) => indexer.search(searchQuery, limit, {
15592
+ search: (searchQuery, limit, scope) => indexer.search(searchQuery, limit, {
14966
15593
  metadataOnly: true,
14967
15594
  filterByBranch: !!query.expected.branch,
14968
- definitionIntent: false
15595
+ definitionIntent: false,
15596
+ fileType: scope.fileType,
15597
+ directory: scope.directory
14969
15598
  })
14970
15599
  }) : void 0;
14971
15600
  const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
14972
15601
  metadataOnly: true,
14973
- filterByBranch: !!query.expected.branch
15602
+ filterByBranch: !!query.expected.branch,
15603
+ fileType: query.args?.fileType,
15604
+ directory: query.args?.directory
14974
15605
  });
14975
15606
  const elapsed = import_perf_hooks2.performance.now() - start;
14976
15607
  const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
14977
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);
14978
15613
  const materialized = result.map((item) => ({
14979
15614
  filePath: item.filePath,
14980
15615
  startLine: item.startLine,
@@ -14991,7 +15626,9 @@ async function runEvaluation(options) {
14991
15626
  responseTokens: contextResult.details.tokenEstimate,
14992
15627
  candidateCount: contextResult.details.candidateCount ?? 0,
14993
15628
  deduplicatedCount: contextResult.details.deduplicatedCount ?? 0,
14994
- omittedCount: contextResult.details.omittedCount ?? 0
15629
+ omittedCount: contextResult.details.omittedCount ?? 0,
15630
+ recoveryUsed,
15631
+ recoveryRelaxed
14995
15632
  } : void 0));
14996
15633
  }
14997
15634
  const logger = indexer.getLogger();
@@ -15003,6 +15640,7 @@ async function runEvaluation(options) {
15003
15640
  datasetPath,
15004
15641
  datasetName: dataset.name,
15005
15642
  datasetVersion: dataset.version,
15643
+ datasetFingerprint: buildDatasetFingerprint(dataset),
15006
15644
  queryCount: dataset.queries.length,
15007
15645
  topK: 10,
15008
15646
  searchConfig: {
@@ -17920,7 +18558,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
17920
18558
  if (isGitRepo(projectRoot)) {
17921
18559
  gitWatcher = new GitHeadWatcher(projectRoot);
17922
18560
  gitWatcher.start(async (oldBranch, newBranch) => {
17923
- console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
18561
+ getIndexer().getLogger().branch("info", "Branch changed", {
18562
+ oldBranch,
18563
+ newBranch
18564
+ });
17924
18565
  requestReindex();
17925
18566
  });
17926
18567
  }