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.js CHANGED
@@ -781,6 +781,7 @@ function getDefaultIndexingConfig() {
781
781
  autoIndexMaxRetries: 5,
782
782
  autoIndexRetryDelayMs: 100,
783
783
  watchFiles: true,
784
+ pauseBackgroundIndexingOnBattery: false,
784
785
  maxFileSize: 1048576,
785
786
  maxChunksPerFile: 100,
786
787
  semanticOnly: false,
@@ -912,6 +913,7 @@ function parseConfig(raw) {
912
913
  autoIndexMaxRetries: typeof rawIndexing.autoIndexMaxRetries === "number" ? Math.min(10, Math.max(0, Math.floor(rawIndexing.autoIndexMaxRetries))) : defaultIndexing.autoIndexMaxRetries,
913
914
  autoIndexRetryDelayMs: typeof rawIndexing.autoIndexRetryDelayMs === "number" ? Math.min(1e4, Math.max(10, Math.floor(rawIndexing.autoIndexRetryDelayMs))) : defaultIndexing.autoIndexRetryDelayMs,
914
915
  watchFiles: typeof rawIndexing.watchFiles === "boolean" ? rawIndexing.watchFiles : defaultIndexing.watchFiles,
916
+ pauseBackgroundIndexingOnBattery: typeof rawIndexing.pauseBackgroundIndexingOnBattery === "boolean" ? rawIndexing.pauseBackgroundIndexingOnBattery : defaultIndexing.pauseBackgroundIndexingOnBattery,
915
917
  maxFileSize: typeof rawIndexing.maxFileSize === "number" ? rawIndexing.maxFileSize : defaultIndexing.maxFileSize,
916
918
  maxChunksPerFile: typeof rawIndexing.maxChunksPerFile === "number" ? Math.max(1, rawIndexing.maxChunksPerFile) : defaultIndexing.maxChunksPerFile,
917
919
  semanticOnly: typeof rawIndexing.semanticOnly === "boolean" ? rawIndexing.semanticOnly : defaultIndexing.semanticOnly,
@@ -1085,6 +1087,18 @@ function metricDelta(current, baseline) {
1085
1087
  };
1086
1088
  }
1087
1089
  function compareSummaries(current, baseline, againstPath) {
1090
+ const hasCurrentFingerprint = current.datasetFingerprint !== void 0;
1091
+ const hasBaselineFingerprint = baseline.datasetFingerprint !== void 0;
1092
+ if (hasCurrentFingerprint !== hasBaselineFingerprint) {
1093
+ throw new Error(
1094
+ `Cannot compare evaluation summaries with mismatched dataset fingerprint presence: current=${hasCurrentFingerprint ? "present" : "missing"}, baseline=${hasBaselineFingerprint ? "present" : "missing"} at ${againstPath}`
1095
+ );
1096
+ }
1097
+ if (hasCurrentFingerprint && hasBaselineFingerprint && current.datasetFingerprint !== baseline.datasetFingerprint) {
1098
+ throw new Error(
1099
+ `Cannot compare incompatible evaluation datasets by fingerprint: current=${current.datasetFingerprint}, baseline=${baseline.datasetFingerprint} at ${againstPath}`
1100
+ );
1101
+ }
1088
1102
  if (current.datasetName !== baseline.datasetName || current.datasetVersion !== baseline.datasetVersion || current.queryCount !== baseline.queryCount) {
1089
1103
  throw new Error(
1090
1104
  `Cannot compare incompatible evaluation datasets: current=${current.datasetName}@${current.datasetVersion} (${current.queryCount} queries), baseline=${baseline.datasetName}@${baseline.datasetVersion} (${baseline.queryCount} queries) at ${againstPath}`
@@ -1388,6 +1402,7 @@ function buildPerQueryArtifact(perQuery) {
1388
1402
  }
1389
1403
 
1390
1404
  // src/eval/runner.ts
1405
+ import * as crypto from "crypto";
1391
1406
  import { existsSync as existsSync13 } from "fs";
1392
1407
  import * as path21 from "path";
1393
1408
  import { performance as performance3 } from "perf_hooks";
@@ -3330,9 +3345,7 @@ var PROJECT_MARKERS = [
3330
3345
  "pom.xml",
3331
3346
  "build.gradle",
3332
3347
  "CMakeLists.txt",
3333
- "Makefile",
3334
- ".opencode",
3335
- ".codebase-index"
3348
+ "Makefile"
3336
3349
  ];
3337
3350
  function hasProjectMarker(projectRoot) {
3338
3351
  for (const marker of PROJECT_MARKERS) {
@@ -4026,9 +4039,21 @@ function parseFiles(files) {
4026
4039
  return result.map((f) => ({
4027
4040
  path: f.path,
4028
4041
  chunks: f.chunks.map(mapChunk),
4042
+ symbols: (f.symbols ?? []).map(mapParsedSymbol),
4029
4043
  hash: f.hash
4030
4044
  }));
4031
4045
  }
4046
+ function mapParsedSymbol(symbol) {
4047
+ return {
4048
+ name: symbol.name,
4049
+ kind: symbol.kind,
4050
+ startLine: symbol.startLine ?? symbol.start_line,
4051
+ startCol: symbol.startCol ?? symbol.start_col,
4052
+ endLine: symbol.endLine ?? symbol.end_line,
4053
+ endCol: symbol.endCol ?? symbol.end_col,
4054
+ language: symbol.language
4055
+ };
4056
+ }
4032
4057
  function mapChunk(c) {
4033
4058
  return {
4034
4059
  content: c.content,
@@ -6849,6 +6874,7 @@ var INDEX_METADATA_VERSION = "1";
6849
6874
  var EMBEDDING_STRATEGY_VERSION = "2";
6850
6875
  var SWIFT_PARSER_VERSION = "1";
6851
6876
  var METAL_PARSER_VERSION = "1";
6877
+ var SYMBOL_EXTRACTOR_VERSION = "1";
6852
6878
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
6853
6879
  var RANK_HYBRID_CACHE_LIMIT = 256;
6854
6880
  function createPendingChunkStorageText(texts) {
@@ -7150,7 +7176,7 @@ function classifyQueryIntentRaw(query) {
7150
7176
  return "neutral";
7151
7177
  }
7152
7178
  function isImplementationChunkType(chunkType) {
7153
- return [
7179
+ return CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunkType) || [
7154
7180
  "export_statement",
7155
7181
  "function",
7156
7182
  "function_declaration",
@@ -7595,7 +7621,7 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
7595
7621
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
7596
7622
  return [...promoted, ...remainder];
7597
7623
  }
7598
- function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7624
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7599
7625
  if (!prioritizeSourcePaths) {
7600
7626
  return [];
7601
7627
  }
@@ -7609,14 +7635,14 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7609
7635
  const primaryHint = extractPrimaryIdentifierQueryHint(query);
7610
7636
  const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
7611
7637
  if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
7612
- return;
7638
+ return false;
7613
7639
  }
7614
7640
  const chunkType = chunk.nodeType ?? "other";
7615
7641
  if (!isImplementationChunkType(chunkType)) {
7616
- return;
7642
+ return false;
7617
7643
  }
7618
7644
  if (!isLikelyImplementationPath2(chunk.filePath)) {
7619
- return;
7645
+ return false;
7620
7646
  }
7621
7647
  const nameLower = (chunk.name ?? "").toLowerCase();
7622
7648
  const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
@@ -7638,6 +7664,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7638
7664
  }
7639
7665
  });
7640
7666
  }
7667
+ return true;
7641
7668
  };
7642
7669
  const normalizedHints = identifierHints.flatMap((hint) => [
7643
7670
  hint,
@@ -7659,12 +7686,46 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7659
7686
  dedupSymbols.set(symbol.id, symbol);
7660
7687
  }
7661
7688
  for (const symbol of dedupSymbols.values()) {
7689
+ if (branchSymbolIds && !branchSymbolIds.has(symbol.id)) {
7690
+ continue;
7691
+ }
7692
+ if (filePathHint && !pathMatchesHint(symbol.filePath, filePathHint)) {
7693
+ continue;
7694
+ }
7662
7695
  const chunks = database.getChunksByFile(symbol.filePath);
7696
+ let foundCoveringChunk = false;
7663
7697
  for (const chunk of chunks) {
7664
7698
  if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
7665
7699
  continue;
7666
7700
  }
7667
- upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7701
+ const chunkName = (chunk.name ?? "").toLowerCase();
7702
+ const symbolName2 = symbol.name.toLowerCase();
7703
+ if (chunkName !== symbolName2 && chunkName.replace(/_/g, "") !== symbolName2.replace(/_/g, "")) {
7704
+ continue;
7705
+ }
7706
+ foundCoveringChunk = upsertChunkCandidate(chunk, identifier, normalizedIdentifier) || foundCoveringChunk;
7707
+ }
7708
+ if (foundCoveringChunk || !isLikelyImplementationPath2(symbol.filePath)) {
7709
+ continue;
7710
+ }
7711
+ const symbolName = symbol.name.toLowerCase();
7712
+ const exactName = symbolName === identifier || symbolName.replace(/_/g, "") === normalizedIdentifier;
7713
+ const score = exactName ? 0.99 : 0.88;
7714
+ const existing = symbolCandidates.get(symbol.id);
7715
+ if (!existing || score > existing.score) {
7716
+ symbolCandidates.set(symbol.id, {
7717
+ id: symbol.id,
7718
+ score,
7719
+ metadata: {
7720
+ filePath: symbol.filePath,
7721
+ startLine: symbol.startLine,
7722
+ endLine: symbol.endLine,
7723
+ chunkType: symbol.kind,
7724
+ name: symbol.name,
7725
+ language: symbol.language,
7726
+ hash: symbol.id
7727
+ }
7728
+ });
7668
7729
  }
7669
7730
  }
7670
7731
  const dedupChunksByName = /* @__PURE__ */ new Map();
@@ -7672,6 +7733,9 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7672
7733
  dedupChunksByName.set(chunk.chunkId, chunk);
7673
7734
  }
7674
7735
  for (const chunk of dedupChunksByName.values()) {
7736
+ if (filePathHint && !pathMatchesHint(chunk.filePath, filePathHint)) {
7737
+ continue;
7738
+ }
7675
7739
  upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7676
7740
  }
7677
7741
  }
@@ -8213,6 +8277,10 @@ var Indexer = class _Indexer {
8213
8277
  const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
8214
8278
  return `${key}.${projectHash}`;
8215
8279
  }
8280
+ getSymbolExtractorVersionMetadataKey(catalogIdentity = this.getBranchCatalogIdentity()) {
8281
+ const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
8282
+ return `index.symbolExtractorVersion.${hashContent(branchKey).slice(0, 24)}`;
8283
+ }
8216
8284
  hasProjectForceReembedPending() {
8217
8285
  return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
8218
8286
  }
@@ -9556,7 +9624,8 @@ var Indexer = class _Indexer {
9556
9624
  }
9557
9625
  const branchKey = this.getBranchCatalogKey();
9558
9626
  const alreadyIndexed = database.getBranchChunkIds(branchKey).length > 0 && database.getBranchSymbolIds(branchKey).length > 0;
9559
- if (alreadyIndexed && this.getStoredBranchCommit(database) === normalizedCommit) {
9627
+ const symbolsCurrent = database.getMetadata(this.getSymbolExtractorVersionMetadataKey()) === SYMBOL_EXTRACTOR_VERSION;
9628
+ if (alreadyIndexed && symbolsCurrent && this.getStoredBranchCommit(database) === normalizedCommit) {
9560
9629
  return { prepared: false };
9561
9630
  }
9562
9631
  const stats = await this.indexUnlocked(onProgress, [], true);
@@ -9615,6 +9684,8 @@ var Indexer = class _Indexer {
9615
9684
  const reparseCachedSwiftFiles = database.getMetadata(swiftParserMetadataKey) !== SWIFT_PARSER_VERSION;
9616
9685
  const metalParserMetadataKey = this.getMetalParserVersionMetadataKey();
9617
9686
  const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
9687
+ const symbolExtractorMetadataKey = this.getSymbolExtractorVersionMetadataKey();
9688
+ const refreshCachedSymbols = database.getMetadata(symbolExtractorMetadataKey) !== SYMBOL_EXTRACTOR_VERSION;
9618
9689
  if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
9619
9690
  (filePath) => path13.extname(filePath).toLowerCase() === ".swift"
9620
9691
  )) {
@@ -9658,7 +9729,7 @@ var Indexer = class _Indexer {
9658
9729
  );
9659
9730
  const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path13.extname(canonicalPath).toLowerCase() === ".swift";
9660
9731
  const requiresMetalParserUpgrade = reparseCachedMetalFiles && path13.extname(canonicalPath).toLowerCase() === ".metal";
9661
- if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade) {
9732
+ if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
9662
9733
  unchangedFilePaths.add(canonicalPath);
9663
9734
  this.logger.recordCacheHit();
9664
9735
  } else {
@@ -9869,37 +9940,27 @@ var Indexer = class _Indexer {
9869
9940
  const parsed = parsedFiles[i];
9870
9941
  const changedFile = changedFiles[i];
9871
9942
  const fileSymbols = [];
9872
- for (const chunk of parsed.chunks) {
9873
- if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
9874
- const existingMetalSymbol = chunk.language === "metal" ? fileSymbols.find(
9875
- (symbol2) => symbol2.name === chunk.name && symbol2.kind === chunk.chunkType && symbol2.startLine <= chunk.endLine && chunk.startLine <= symbol2.endLine
9876
- ) : void 0;
9877
- if (existingMetalSymbol) {
9878
- existingMetalSymbol.startLine = Math.min(existingMetalSymbol.startLine, chunk.startLine);
9879
- existingMetalSymbol.endLine = Math.max(existingMetalSymbol.endLine, chunk.endLine);
9880
- existingMetalSymbol.startCol = Math.min(existingMetalSymbol.startCol, chunk.startCol ?? 0);
9881
- existingMetalSymbol.endCol = Math.max(existingMetalSymbol.endCol, chunk.endCol ?? 0);
9882
- continue;
9883
- }
9943
+ for (const parsedSymbol of parsed.symbols) {
9944
+ if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) continue;
9884
9945
  const preparedNamespace = this.getPreparedBranchNamespace();
9885
9946
  const symbolId = `sym_${hashContent(
9886
- (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine + ":" + (chunk.startCol ?? 0) + ":" + changedFile.hash
9947
+ (preparedNamespace ? `${preparedNamespace}:` : "") + parsed.path + ":" + parsedSymbol.name + ":" + parsedSymbol.kind + ":" + parsedSymbol.startLine + ":" + parsedSymbol.startCol + ":" + changedFile.hash
9887
9948
  ).slice(0, 16)}`;
9888
9949
  const symbol = {
9889
9950
  id: symbolId,
9890
9951
  filePath: parsed.path,
9891
- name: chunk.name,
9892
- kind: chunk.chunkType,
9893
- startLine: chunk.startLine,
9894
- startCol: chunk.startCol ?? 0,
9895
- endLine: chunk.endLine,
9896
- endCol: chunk.endCol ?? 0,
9897
- language: chunk.language
9952
+ name: parsedSymbol.name,
9953
+ kind: parsedSymbol.kind,
9954
+ startLine: parsedSymbol.startLine,
9955
+ startCol: parsedSymbol.startCol,
9956
+ endLine: parsedSymbol.endLine,
9957
+ endCol: parsedSymbol.endCol,
9958
+ language: parsedSymbol.language
9898
9959
  };
9899
9960
  fileSymbols.push(symbol);
9900
9961
  allSymbolIds.add(symbolId);
9901
9962
  }
9902
- const fileLanguage = parsed.chunks[0]?.language;
9963
+ const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
9903
9964
  const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
9904
9965
  const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
9905
9966
  const symbolsByName = /* @__PURE__ */ new Map();
@@ -10015,6 +10076,7 @@ var Indexer = class _Indexer {
10015
10076
  }
10016
10077
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10017
10078
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10079
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10018
10080
  this.saveBranchCommit(database, indexedCommit);
10019
10081
  this.saveIndexMetadata(configuredProviderInfo);
10020
10082
  this.indexCompatibility = { compatible: true };
@@ -10051,6 +10113,7 @@ var Indexer = class _Indexer {
10051
10113
  }
10052
10114
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10053
10115
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10116
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10054
10117
  this.saveBranchCommit(database, indexedCommit);
10055
10118
  this.saveIndexMetadata(configuredProviderInfo);
10056
10119
  this.indexCompatibility = { compatible: true };
@@ -10330,6 +10393,7 @@ var Indexer = class _Indexer {
10330
10393
  }
10331
10394
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10332
10395
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10396
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10333
10397
  this.saveBranchCommit(database, indexedCommit);
10334
10398
  this.saveIndexMetadata(configuredProviderInfo);
10335
10399
  this.indexCompatibility = { compatible: true };
@@ -10468,10 +10532,11 @@ var Indexer = class _Indexer {
10468
10532
  const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
10469
10533
  const keywordMs = performance2.now() - keywordStartTime;
10470
10534
  let branchChunkIds = null;
10535
+ let branchSymbolIds = null;
10471
10536
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
10472
- branchChunkIds = new Set(
10473
- this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
10474
- );
10537
+ const branchCatalogKeys = this.getBranchCatalogKeys();
10538
+ branchChunkIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchChunkIds(branchKey)));
10539
+ branchSymbolIds = new Set(branchCatalogKeys.flatMap((branchKey) => database.getBranchSymbolIds(branchKey)));
10475
10540
  }
10476
10541
  const prefilterStartTime = performance2.now();
10477
10542
  const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
@@ -10543,6 +10608,7 @@ var Indexer = class _Indexer {
10543
10608
  query,
10544
10609
  database,
10545
10610
  branchChunkIds,
10611
+ branchSymbolIds,
10546
10612
  maxResults,
10547
10613
  union,
10548
10614
  sourceIntent
@@ -10556,7 +10622,7 @@ var Indexer = class _Indexer {
10556
10622
  (r) => isLikelyImplementationPath2(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
10557
10623
  );
10558
10624
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
10559
- 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) : [];
10625
+ 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) : [];
10560
10626
  const finalResults = filtered.length > 0 ? filtered : identifierFallback;
10561
10627
  const totalSearchMs = performance2.now() - searchStartTime;
10562
10628
  this.logger.recordSearch(totalSearchMs, {
@@ -10713,7 +10779,7 @@ var Indexer = class _Indexer {
10713
10779
  const extension = path13.extname(filePath).toLowerCase();
10714
10780
  return extension === ".php" || extension === ".c" || extension === ".cc" || extension === ".cpp" || extension === ".cxx";
10715
10781
  });
10716
- 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) {
10782
+ 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) {
10717
10783
  return { readable: true, current: false, reason: "migration-required" };
10718
10784
  }
10719
10785
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -11421,7 +11487,10 @@ var Indexer = class _Indexer {
11421
11487
  const requestedRef = opts.pr !== void 0 ? expectedCommit : headRefName ?? resolvedBranch;
11422
11488
  const storedCommit = this.getStoredBranchCommit(database, catalogIdentity);
11423
11489
  const catalogIdentityMatches = storedCommit === expectedCommit;
11424
- if (branchSymbols.length === 0 || !catalogIdentityMatches) {
11490
+ const symbolsCurrent = database.getMetadata(
11491
+ this.getSymbolExtractorVersionMetadataKey(catalogIdentity)
11492
+ ) === SYMBOL_EXTRACTOR_VERSION;
11493
+ if (branchSymbols.length === 0 || !catalogIdentityMatches || !symbolsCurrent) {
11425
11494
  if (!resolvedBranch || resolvedBranch === "default") {
11426
11495
  throw new Error("Run index_codebase first to build the call graph and symbol index for this project.");
11427
11496
  }
@@ -11737,8 +11806,17 @@ function fitTextToContextBudget(text, tokenBudget) {
11737
11806
  function normalizedLineRange(result) {
11738
11807
  return result.startLine <= result.endLine ? { start: result.startLine, end: result.endLine } : { start: result.endLine, end: result.startLine };
11739
11808
  }
11740
- function rankContextCandidates(results) {
11741
- return results.map((result, originalIndex) => ({ result, originalIndex })).sort((left, right) => right.result.score - left.result.score || left.originalIndex - right.originalIndex);
11809
+ function rankContextCandidates(results, preferImplementationPaths) {
11810
+ return results.map((result, originalIndex) => ({ result, originalIndex })).sort((left, right) => {
11811
+ if (preferImplementationPaths) {
11812
+ const leftIsImplementation = isLikelyImplementationPath(left.result.filePath);
11813
+ const rightIsImplementation = isLikelyImplementationPath(right.result.filePath);
11814
+ if (leftIsImplementation !== rightIsImplementation) {
11815
+ return leftIsImplementation ? -1 : 1;
11816
+ }
11817
+ }
11818
+ return right.result.score - left.result.score || left.originalIndex - right.originalIndex;
11819
+ });
11742
11820
  }
11743
11821
  function deduplicateContextCandidates(candidates) {
11744
11822
  const acceptedByFile = /* @__PURE__ */ new Map();
@@ -11827,7 +11905,12 @@ function buildContextPack(results, options = {}) {
11827
11905
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
11828
11906
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
11829
11907
  const candidateCount = results.length;
11830
- const deduplicated = deduplicateContextCandidates(rankContextCandidates(results));
11908
+ const deduplicated = deduplicateContextCandidates(
11909
+ rankContextCandidates(
11910
+ results,
11911
+ options.preferImplementationPaths ?? false
11912
+ )
11913
+ );
11831
11914
  const diversified = diversifyContextCandidates(deduplicated);
11832
11915
  const duplicateCount = candidateCount - deduplicated.length;
11833
11916
  const selectable = diversified.slice(0, maxResults);
@@ -12199,7 +12282,7 @@ ${truncateContent(r.content)}
12199
12282
  }
12200
12283
 
12201
12284
  // src/utils/effectiveness-metrics.ts
12202
- var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 2;
12285
+ var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 3;
12203
12286
  var MAX_EFFECTIVENESS_COUNTER = 1e9;
12204
12287
  var EFFECTIVENESS_TOOL_ROUTES = [
12205
12288
  "context-conceptual",
@@ -12245,6 +12328,9 @@ var EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS = ["none", "directory", "file-type",
12245
12328
  function emptyCounterMap(values) {
12246
12329
  return Object.fromEntries(values.map((value) => [value, 0]));
12247
12330
  }
12331
+ function emptyRouteCounterMap(routes, values) {
12332
+ return Object.fromEntries(routes.map((route) => [route, emptyCounterMap(values)]));
12333
+ }
12248
12334
  function boundedNumber(value) {
12249
12335
  if (value === void 0 || !Number.isFinite(value)) return 0;
12250
12336
  return Math.max(0, Math.floor(value));
@@ -12293,6 +12379,11 @@ function allowedValue(value, allowed, fallback) {
12293
12379
  function cloneCounterMap(counters) {
12294
12380
  return { ...counters };
12295
12381
  }
12382
+ function cloneRouteCounterMap(counters) {
12383
+ return Object.fromEntries(
12384
+ EFFECTIVENESS_TOOL_ROUTES.map((route) => [route, cloneCounterMap(counters[route])])
12385
+ );
12386
+ }
12296
12387
  var EffectivenessMetrics = class {
12297
12388
  constructor(counterCap = MAX_EFFECTIVENESS_COUNTER) {
12298
12389
  this.counterCap = counterCap;
@@ -12308,7 +12399,7 @@ var EffectivenessMetrics = class {
12308
12399
  lifetime: "process",
12309
12400
  reset: "index_metrics-reset-or-process-exit",
12310
12401
  maxCounterValue: this.counterCap,
12311
- dimensions: "bounded-host-and-category-only"
12402
+ dimensions: "bounded-route-and-bucketed-performance-only"
12312
12403
  },
12313
12404
  totalCalls: 0,
12314
12405
  toolRoute: emptyCounterMap(EFFECTIVENESS_TOOL_ROUTES),
@@ -12320,7 +12411,14 @@ var EffectivenessMetrics = class {
12320
12411
  tokenBudget: emptyCounterMap(EFFECTIVENESS_TOKEN_BUDGET_BUCKETS),
12321
12412
  returnedTokenEstimate: emptyCounterMap(EFFECTIVENESS_RETURNED_TOKEN_BUCKETS),
12322
12413
  exactHandoffEmitted: emptyCounterMap(EFFECTIVENESS_BOOLEAN_BUCKETS),
12323
- scopeRelaxation: emptyCounterMap(EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS)
12414
+ scopeRelaxation: emptyCounterMap(EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS),
12415
+ routeOutcome: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_OUTCOMES),
12416
+ routeLatency: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_LATENCY_BUCKETS),
12417
+ routeResultCount: emptyRouteCounterMap(EFFECTIVENESS_TOOL_ROUTES, EFFECTIVENESS_RESULT_COUNT_BUCKETS),
12418
+ routeReturnedTokenEstimate: emptyRouteCounterMap(
12419
+ EFFECTIVENESS_TOOL_ROUTES,
12420
+ EFFECTIVENESS_RETURNED_TOKEN_BUCKETS
12421
+ )
12324
12422
  };
12325
12423
  }
12326
12424
  increment(counters, key) {
@@ -12342,12 +12440,19 @@ var EffectivenessMetrics = class {
12342
12440
  this.increment(this.snapshot.hostMode, host);
12343
12441
  this.increment(this.snapshot.outcome, outcome);
12344
12442
  this.increment(this.snapshot.recoveryUsed, recoveryUsed);
12345
- this.increment(this.snapshot.resultCount, resultCountBucket(event.resultCount));
12346
- this.increment(this.snapshot.latency, latencyBucket(event.latencyMs));
12443
+ const resultCountBucketValue = resultCountBucket(event.resultCount);
12444
+ const latencyBucketValue = latencyBucket(event.latencyMs);
12445
+ const returnedTokenBucketValue = returnedTokenBucket(event.returnedTokenEstimate);
12446
+ this.increment(this.snapshot.resultCount, resultCountBucketValue);
12447
+ this.increment(this.snapshot.latency, latencyBucketValue);
12347
12448
  this.increment(this.snapshot.tokenBudget, tokenBudgetBucket(event.tokenBudget));
12348
- this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucket(event.returnedTokenEstimate));
12449
+ this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucketValue);
12349
12450
  this.increment(this.snapshot.exactHandoffEmitted, exactHandoffEmitted);
12350
12451
  this.increment(this.snapshot.scopeRelaxation, scopeRelaxation);
12452
+ this.increment(this.snapshot.routeOutcome[route], outcome);
12453
+ this.increment(this.snapshot.routeLatency[route], latencyBucketValue);
12454
+ this.increment(this.snapshot.routeResultCount[route], resultCountBucketValue);
12455
+ this.increment(this.snapshot.routeReturnedTokenEstimate[route], returnedTokenBucketValue);
12351
12456
  }
12352
12457
  getSnapshot() {
12353
12458
  return {
@@ -12362,7 +12467,11 @@ var EffectivenessMetrics = class {
12362
12467
  tokenBudget: cloneCounterMap(this.snapshot.tokenBudget),
12363
12468
  returnedTokenEstimate: cloneCounterMap(this.snapshot.returnedTokenEstimate),
12364
12469
  exactHandoffEmitted: cloneCounterMap(this.snapshot.exactHandoffEmitted),
12365
- scopeRelaxation: cloneCounterMap(this.snapshot.scopeRelaxation)
12470
+ scopeRelaxation: cloneCounterMap(this.snapshot.scopeRelaxation),
12471
+ routeOutcome: cloneRouteCounterMap(this.snapshot.routeOutcome),
12472
+ routeLatency: cloneRouteCounterMap(this.snapshot.routeLatency),
12473
+ routeResultCount: cloneRouteCounterMap(this.snapshot.routeResultCount),
12474
+ routeReturnedTokenEstimate: cloneRouteCounterMap(this.snapshot.routeReturnedTokenEstimate)
12366
12475
  };
12367
12476
  }
12368
12477
  reset() {
@@ -12381,6 +12490,7 @@ function resetProcessEffectivenessMetrics() {
12381
12490
  }
12382
12491
  function formatEffectivenessMetrics(snapshot) {
12383
12492
  const formatCounters = (counters) => Object.entries(counters).map(([bucket, count]) => `${bucket}=${count}`).join(", ");
12493
+ const formatRouteCounters = (counters) => EFFECTIVENESS_TOOL_ROUTES.map((route) => `${route} => ${formatCounters(counters[route])}`).join("; ");
12384
12494
  const lines = [
12385
12495
  `Privacy-safe effectiveness (schema v${snapshot.schemaVersion}):`,
12386
12496
  ` Retention: ${snapshot.retention.storage}, ${snapshot.retention.lifetime}-lifetime; reset with index_metrics(reset=true) or process exit`,
@@ -12395,6 +12505,10 @@ function formatEffectivenessMetrics(snapshot) {
12395
12505
  ` Latency bucket: ${formatCounters(snapshot.latency)}`,
12396
12506
  ` Token-budget bucket: ${formatCounters(snapshot.tokenBudget)}`,
12397
12507
  ` Returned-token estimate: ${formatCounters(snapshot.returnedTokenEstimate)}`,
12508
+ ` Route outcome buckets: ${formatRouteCounters(snapshot.routeOutcome)}`,
12509
+ ` Route latency buckets: ${formatRouteCounters(snapshot.routeLatency)}`,
12510
+ ` Route result-count buckets: ${formatRouteCounters(snapshot.routeResultCount)}`,
12511
+ ` Route returned-token buckets: ${formatRouteCounters(snapshot.routeReturnedTokenEstimate)}`,
12398
12512
  ` Exact handoff emitted: ${formatCounters(snapshot.exactHandoffEmitted)}`,
12399
12513
  ` Scope relaxation: ${formatCounters(snapshot.scopeRelaxation)}`,
12400
12514
  " Privacy: no queries, response text, source, symbols, paths, repository names, user identity, or stable identifiers are retained."
@@ -12406,6 +12520,103 @@ function formatEffectivenessMetrics(snapshot) {
12406
12520
  import { existsSync as existsSync8, realpathSync as realpathSync4 } from "fs";
12407
12521
  import * as os6 from "os";
12408
12522
  import * as path15 from "path";
12523
+
12524
+ // src/utils/power-source.ts
12525
+ import * as childProcess from "child_process";
12526
+ var POWER_SOURCE_RECHECK_DELAY_MS = 6e4;
12527
+ var PMSET_TIMEOUT_MS = 5e3;
12528
+ function getErrorMessage4(error) {
12529
+ return error instanceof Error ? error.message : String(error);
12530
+ }
12531
+ function runCommand(file, args, options) {
12532
+ return new Promise((resolve17, reject) => {
12533
+ childProcess.execFile(
12534
+ file,
12535
+ args,
12536
+ { encoding: "utf8", timeout: options.timeoutMs },
12537
+ (error, stdout) => {
12538
+ if (error) {
12539
+ reject(error);
12540
+ return;
12541
+ }
12542
+ resolve17(stdout);
12543
+ }
12544
+ );
12545
+ });
12546
+ }
12547
+ function parseMacOsPowerSource(output) {
12548
+ const match = output.match(/Now drawing from '([^']+)'/i);
12549
+ if (!match) {
12550
+ return "unknown";
12551
+ }
12552
+ const source = match[1].toLowerCase();
12553
+ if (source === "battery power") {
12554
+ return "battery";
12555
+ }
12556
+ if (source === "ac power") {
12557
+ return "ac";
12558
+ }
12559
+ return "unknown";
12560
+ }
12561
+ async function readMacOsPowerSource(commandRunner = runCommand) {
12562
+ const output = await commandRunner(
12563
+ "/usr/bin/pmset",
12564
+ ["-g", "batt"],
12565
+ { timeoutMs: PMSET_TIMEOUT_MS }
12566
+ );
12567
+ return parseMacOsPowerSource(output);
12568
+ }
12569
+ var MacOsBackgroundIndexingPolicy = class {
12570
+ constructor(readPowerSource, recheckDelayMs) {
12571
+ this.readPowerSource = readPowerSource;
12572
+ this.recheckDelayMs = recheckDelayMs;
12573
+ }
12574
+ readPowerSource;
12575
+ recheckDelayMs;
12576
+ lastPaused = null;
12577
+ reportedFailure = false;
12578
+ isPaused() {
12579
+ return this.checkPowerSource();
12580
+ }
12581
+ async checkPowerSource() {
12582
+ try {
12583
+ const source = await this.readPowerSource();
12584
+ if (source === "unknown") {
12585
+ throw new Error("pmset returned an unrecognized power source");
12586
+ }
12587
+ this.reportedFailure = false;
12588
+ const paused = source === "battery";
12589
+ if (paused && this.lastPaused !== true) {
12590
+ console.warn("[codebase-index] Background indexing paused while macOS is using battery power.");
12591
+ } else if (!paused && this.lastPaused === true) {
12592
+ console.warn("[codebase-index] AC power detected; resuming pending background indexing.");
12593
+ }
12594
+ this.lastPaused = paused;
12595
+ return paused;
12596
+ } catch (error) {
12597
+ if (!this.reportedFailure) {
12598
+ console.error(
12599
+ `[codebase-index] Failed to determine the macOS power source; background indexing will continue: ${getErrorMessage4(error)}`
12600
+ );
12601
+ this.reportedFailure = true;
12602
+ }
12603
+ this.lastPaused = false;
12604
+ return false;
12605
+ }
12606
+ }
12607
+ };
12608
+ function createBackgroundIndexingPolicy(pauseOnBattery, options = {}) {
12609
+ const platform2 = options.platform ?? process.platform;
12610
+ if (!pauseOnBattery || platform2 !== "darwin") {
12611
+ return null;
12612
+ }
12613
+ return new MacOsBackgroundIndexingPolicy(
12614
+ options.readPowerSource ?? readMacOsPowerSource,
12615
+ options.recheckDelayMs ?? POWER_SOURCE_RECHECK_DELAY_MS
12616
+ );
12617
+ }
12618
+
12619
+ // src/utils/auto-index.ts
12409
12620
  var MAX_RETRY_DELAY_MS = 1e4;
12410
12621
  var SHUTDOWN_WAIT_MS = 2e3;
12411
12622
  var coordinators = /* @__PURE__ */ new Map();
@@ -12522,7 +12733,13 @@ var AutoIndexCoordinator = class {
12522
12733
  activation = Promise.resolve();
12523
12734
  inFlight = null;
12524
12735
  activeRequest = null;
12736
+ batteryCheck = null;
12737
+ batteryIndexJob = null;
12738
+ batteryDeferredRequest = null;
12739
+ batteryRetryTimer = null;
12740
+ resolveBatteryRetry = null;
12525
12741
  pendingRequest = null;
12742
+ pendingFollowUp = null;
12526
12743
  abortController = null;
12527
12744
  stopped = false;
12528
12745
  constructor(registration) {
@@ -12535,6 +12752,7 @@ var AutoIndexCoordinator = class {
12535
12752
  };
12536
12753
  }
12537
12754
  update(registration) {
12755
+ const pauseOnBatteryChanged = registration.config.indexing.pauseBackgroundIndexingOnBattery !== this.registration.config.indexing.pauseBackgroundIndexingOnBattery;
12538
12756
  this.registration = registration;
12539
12757
  this.status.enabled = registration.config.indexing.autoIndex;
12540
12758
  this.status.blockedReason = registration.blockedReason;
@@ -12550,6 +12768,9 @@ var AutoIndexCoordinator = class {
12550
12768
  this.setState("idle", { source: void 0 });
12551
12769
  }
12552
12770
  }
12771
+ if (pauseOnBatteryChanged) {
12772
+ this.cancelBatteryRetry();
12773
+ }
12553
12774
  }
12554
12775
  activateAfter(activation) {
12555
12776
  this.activation = activation;
@@ -12571,7 +12792,26 @@ var AutoIndexCoordinator = class {
12571
12792
  if (this.stopped) {
12572
12793
  return Promise.resolve({ outcome: "stopped" });
12573
12794
  }
12574
- return this.activation.then(() => this.enqueueRequest(request));
12795
+ return this.activation.then(() => this.enqueueBatteryAwareRequest(request));
12796
+ }
12797
+ enqueueBatteryAwareRequest(request) {
12798
+ if (!this.shouldDeferForBattery(request)) {
12799
+ return this.enqueueRequest(request);
12800
+ }
12801
+ if (this.batteryCheck && this.batteryIndexJob !== null && this.batteryIndexJob === this.inFlight) {
12802
+ return this.enqueueRequest(request);
12803
+ }
12804
+ this.batteryDeferredRequest = mergeRequests(this.batteryDeferredRequest, request);
12805
+ if (this.batteryCheck) {
12806
+ return this.batteryCheck;
12807
+ }
12808
+ const batteryCheck = this.waitForACPower();
12809
+ this.batteryCheck = batteryCheck;
12810
+ void batteryCheck.then(
12811
+ () => this.finishBatteryCheck(batteryCheck),
12812
+ () => this.finishBatteryCheck(batteryCheck)
12813
+ );
12814
+ return batteryCheck;
12575
12815
  }
12576
12816
  enqueueRequest(request) {
12577
12817
  if (this.stopped || !this.canRun(request)) {
@@ -12589,6 +12829,11 @@ var AutoIndexCoordinator = class {
12589
12829
  }
12590
12830
  if (request.source === "watcher") {
12591
12831
  this.pendingRequest = mergeRequests(this.pendingRequest, request);
12832
+ const active = this.inFlight;
12833
+ return active.then(() => {
12834
+ if (this.stopped) return { outcome: "stopped" };
12835
+ return this.pendingFollowUp ?? { outcome: "stopped" };
12836
+ });
12592
12837
  }
12593
12838
  return this.inFlight;
12594
12839
  }
@@ -12605,6 +12850,8 @@ var AutoIndexCoordinator = class {
12605
12850
  }
12606
12851
  async stop(waitForCompletion = false) {
12607
12852
  this.stopped = true;
12853
+ this.batteryDeferredRequest = null;
12854
+ this.cancelBatteryRetry();
12608
12855
  this.pendingRequest = null;
12609
12856
  this.abortController?.abort();
12610
12857
  this.setState("stopped", {
@@ -12634,10 +12881,20 @@ var AutoIndexCoordinator = class {
12634
12881
  this.inFlight = null;
12635
12882
  this.activeRequest = null;
12636
12883
  this.abortController = null;
12884
+ if (this.batteryIndexJob === job) {
12885
+ this.batteryIndexJob = null;
12886
+ this.batteryCheck = null;
12887
+ }
12637
12888
  const pending = this.pendingRequest;
12638
12889
  this.pendingRequest = null;
12639
12890
  if (pending && !this.stopped) {
12640
- this.startRequest(pending);
12891
+ const followUp = this.request(pending);
12892
+ this.pendingFollowUp = followUp;
12893
+ void followUp.then(() => {
12894
+ if (this.pendingFollowUp === followUp) {
12895
+ this.pendingFollowUp = null;
12896
+ }
12897
+ });
12641
12898
  }
12642
12899
  });
12643
12900
  return job;
@@ -12801,6 +13058,68 @@ var AutoIndexCoordinator = class {
12801
13058
  }
12802
13059
  return this.registration.safeToRun && this.registration.config.indexing.autoIndex;
12803
13060
  }
13061
+ shouldDeferForBattery(request) {
13062
+ return this.registration.backgroundIndexingPolicy !== null && (request.source === "startup" || request.source === "watcher");
13063
+ }
13064
+ async waitForACPower() {
13065
+ while (!this.stopped) {
13066
+ const policy = this.registration.backgroundIndexingPolicy;
13067
+ if (!policy || !await this.isBatteryPauseActive(policy)) {
13068
+ const request = this.batteryDeferredRequest;
13069
+ this.batteryDeferredRequest = null;
13070
+ if (!request) return { outcome: "stopped" };
13071
+ const job = this.enqueueRequest(request);
13072
+ if (this.inFlight === job) {
13073
+ this.batteryIndexJob = job;
13074
+ }
13075
+ return job;
13076
+ }
13077
+ await this.waitForBatteryRetry(policy.recheckDelayMs);
13078
+ }
13079
+ return { outcome: "stopped" };
13080
+ }
13081
+ async isBatteryPauseActive(policy) {
13082
+ try {
13083
+ return await policy.isPaused();
13084
+ } catch (error) {
13085
+ console.error(
13086
+ `[codebase-index] Failed to apply the background indexing power policy; background indexing will continue: ${safeFailureMessage(error)}`
13087
+ );
13088
+ return false;
13089
+ }
13090
+ }
13091
+ waitForBatteryRetry(delayMs) {
13092
+ return new Promise((resolve17) => {
13093
+ const timer = setTimeout(() => {
13094
+ if (this.batteryRetryTimer === timer) {
13095
+ this.batteryRetryTimer = null;
13096
+ this.resolveBatteryRetry = null;
13097
+ }
13098
+ resolve17();
13099
+ }, delayMs);
13100
+ timer.unref?.();
13101
+ this.batteryRetryTimer = timer;
13102
+ this.resolveBatteryRetry = resolve17;
13103
+ });
13104
+ }
13105
+ cancelBatteryRetry() {
13106
+ if (this.batteryRetryTimer) {
13107
+ clearTimeout(this.batteryRetryTimer);
13108
+ this.batteryRetryTimer = null;
13109
+ }
13110
+ const resolve17 = this.resolveBatteryRetry;
13111
+ this.resolveBatteryRetry = null;
13112
+ resolve17?.();
13113
+ }
13114
+ finishBatteryCheck(batteryCheck) {
13115
+ if (this.batteryCheck !== batteryCheck) return;
13116
+ this.batteryCheck = null;
13117
+ const deferredRequest = this.batteryDeferredRequest;
13118
+ this.batteryDeferredRequest = null;
13119
+ if (deferredRequest && !this.stopped) {
13120
+ void this.request(deferredRequest);
13121
+ }
13122
+ }
12804
13123
  };
12805
13124
  function getCoordinator(projectRoot, host) {
12806
13125
  const key = coordinatorKeysByProject.get(projectLookupKey(projectRoot, host));
@@ -12810,6 +13129,9 @@ function configureAutoIndex(projectRoot, host, config, getIndexer) {
12810
13129
  const projectKey = projectLookupKey(projectRoot, host);
12811
13130
  const safety = getProjectSafety(projectRoot, config);
12812
13131
  const registration = {
13132
+ backgroundIndexingPolicy: createBackgroundIndexingPolicy(
13133
+ config.indexing.pauseBackgroundIndexingOnBattery
13134
+ ),
12813
13135
  config,
12814
13136
  getIndexer,
12815
13137
  projectRoot,
@@ -14033,6 +14355,7 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14033
14355
  const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed);
14034
14356
  if (results.length > 0) {
14035
14357
  const heading = buildPackHeading("conceptual", decisions);
14358
+ const intent = analyzeQueryIntent(attempt.queryText);
14036
14359
  return toResult(
14037
14360
  "conceptual",
14038
14361
  attempt.queryText,
@@ -14040,7 +14363,8 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14040
14363
  tokenBudget,
14041
14364
  maxResults: limit,
14042
14365
  heading,
14043
- includeExactSearchHandoff: true
14366
+ includeExactSearchHandoff: true,
14367
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
14044
14368
  })
14045
14369
  );
14046
14370
  }
@@ -14341,17 +14665,26 @@ function percentile(values, p) {
14341
14665
  function normalizePath2(input) {
14342
14666
  return normalizePathSeparators(input);
14343
14667
  }
14344
- function uniqueResultsByPath(results) {
14668
+ function uniqueResultsByEvidence(results) {
14345
14669
  const seen = /* @__PURE__ */ new Set();
14346
14670
  const unique = [];
14347
14671
  for (const result of results) {
14348
- const normalized = normalizePath2(result.filePath);
14349
- if (seen.has(normalized)) continue;
14350
- seen.add(normalized);
14672
+ const key = `${normalizePath2(result.filePath)}::${result.name ?? ""}`;
14673
+ if (seen.has(key)) continue;
14674
+ seen.add(key);
14351
14675
  unique.push(result);
14352
14676
  }
14353
14677
  return unique;
14354
14678
  }
14679
+ function uniqueResultsByPath(results) {
14680
+ const seen = /* @__PURE__ */ new Set();
14681
+ return results.filter((result) => {
14682
+ const key = normalizePath2(result.filePath);
14683
+ if (seen.has(key)) return false;
14684
+ seen.add(key);
14685
+ return true;
14686
+ });
14687
+ }
14355
14688
  function distinctTopKRatio(results, k) {
14356
14689
  const top = results.slice(0, k);
14357
14690
  if (top.length === 0) return 0;
@@ -14362,60 +14695,169 @@ function pathMatchesExpected(actualPath, expectedPath) {
14362
14695
  const actual = normalizePath2(actualPath);
14363
14696
  const expected = normalizePath2(expectedPath);
14364
14697
  if (actual === expected) return true;
14365
- return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
14698
+ return actual.endsWith(`/${expected}`);
14699
+ }
14700
+ function getRelevantEvidence(query) {
14701
+ const legacyEvidence = [];
14702
+ if (query.expected.filePath !== void 0) {
14703
+ legacyEvidence.push({
14704
+ path: query.expected.filePath,
14705
+ ...query.expected.symbol !== void 0 ? { symbol: query.expected.symbol } : {},
14706
+ relevance: 1
14707
+ });
14708
+ }
14709
+ if (query.expected.acceptableFiles) {
14710
+ for (const path29 of query.expected.acceptableFiles) {
14711
+ legacyEvidence.push({
14712
+ path: path29,
14713
+ ...query.expected.symbol !== void 0 ? { symbol: query.expected.symbol } : {},
14714
+ relevance: 1
14715
+ });
14716
+ }
14717
+ }
14718
+ const gradedEvidence = query.expected.gradedEvidence ?? [];
14719
+ const allEvidence = [...legacyEvidence, ...gradedEvidence];
14720
+ const dedupeKey = (entry) => {
14721
+ return `${normalizePath2(entry.path)}::${entry.symbol ?? ""}`;
14722
+ };
14723
+ const unique = /* @__PURE__ */ new Map();
14724
+ for (const entry of allEvidence) {
14725
+ unique.set(dedupeKey(entry), entry);
14726
+ }
14727
+ return Array.from(unique.values());
14728
+ }
14729
+ function hasSymbolRequirement(query) {
14730
+ return isSymbolIntended(query);
14731
+ }
14732
+ function isSymbolIntended(query) {
14733
+ return query.expected.symbol !== void 0 || query.args?.symbol !== void 0 || query.expected.gradedEvidence?.some((entry) => entry.symbol !== void 0) === true;
14734
+ }
14735
+ function isExpectedFile(filePath, relevant) {
14736
+ return relevant.some((entry) => pathMatchesExpected(filePath, entry.path));
14737
+ }
14738
+ function resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) {
14739
+ let relevance = 0;
14740
+ for (const entry of relevant) {
14741
+ if (!pathMatchesExpected(filePath, entry.path)) {
14742
+ continue;
14743
+ }
14744
+ if (isSymbolIntendedQuery) {
14745
+ if (symbol === void 0 || entry.symbol === void 0 || symbol !== entry.symbol) {
14746
+ continue;
14747
+ }
14748
+ } else if (entry.symbol !== void 0 && symbol !== void 0 && symbol !== entry.symbol) {
14749
+ continue;
14750
+ }
14751
+ if (entry.symbol === void 0) {
14752
+ relevance = Math.max(relevance, entry.relevance);
14753
+ continue;
14754
+ }
14755
+ if (isSymbolIntendedQuery && entry.symbol !== void 0 && symbol === entry.symbol) {
14756
+ relevance = Math.max(relevance, entry.relevance);
14757
+ continue;
14758
+ }
14759
+ if (!isSymbolIntendedQuery && symbol !== void 0 && symbol === entry.symbol) {
14760
+ relevance = Math.max(relevance, entry.relevance);
14761
+ }
14762
+ }
14763
+ return relevance;
14366
14764
  }
14367
- function getRelevantPaths(query) {
14368
- const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
14369
- const fromAcceptable = query.expected.acceptableFiles ?? [];
14370
- return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
14765
+ function isRelevantResult(filePath, symbol, relevant, isSymbolIntendedQuery) {
14766
+ return resultRelevance(filePath, symbol, relevant, isSymbolIntendedQuery) > 0;
14767
+ }
14768
+ function evidenceMatchesResult(entry, filePath, symbol, isSymbolIntendedQuery) {
14769
+ if (!pathMatchesExpected(filePath, entry.path)) {
14770
+ return false;
14771
+ }
14772
+ if (isSymbolIntendedQuery) {
14773
+ return entry.symbol !== void 0 && symbol === entry.symbol;
14774
+ }
14775
+ return entry.symbol === void 0 || symbol === entry.symbol;
14371
14776
  }
14372
- function isRelevantResult(filePath, relevantPaths) {
14373
- return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
14777
+ function hasGradeBasedEvidence(query) {
14778
+ return (query.expected.gradedEvidence?.length ?? 0) > 0;
14374
14779
  }
14375
- function reciprocalRankAtK(results, relevantPaths, k) {
14376
- const top = uniqueResultsByPath(results).slice(0, k);
14780
+ function dedupeRelevantEvidence(relevant) {
14781
+ const deduped = /* @__PURE__ */ new Map();
14782
+ for (const entry of relevant) {
14783
+ const key = `${normalizePath2(entry.path)}::${entry.symbol ?? ""}`;
14784
+ if (!deduped.has(key)) {
14785
+ deduped.set(key, entry);
14786
+ }
14787
+ }
14788
+ return [...deduped.values()];
14789
+ }
14790
+ function reciprocalRankAtK(results, relevant, isSymbolIntendedQuery, k) {
14791
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14377
14792
  for (let i = 0; i < top.length; i += 1) {
14378
- if (isRelevantResult(top[i].filePath, relevantPaths)) {
14793
+ if (isRelevantResult(top[i].filePath, top[i].name, relevant, isSymbolIntendedQuery)) {
14379
14794
  return 1 / (i + 1);
14380
14795
  }
14381
14796
  }
14382
14797
  return 0;
14383
14798
  }
14384
- function ndcgAtK(results, relevantPaths, k) {
14385
- const top = uniqueResultsByPath(results).slice(0, k);
14799
+ function ndcgAtK(query, results, relevant, isSymbolIntendedQuery, k) {
14800
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14801
+ const availableEvidence = dedupeRelevantEvidence(relevant).filter(
14802
+ (entry) => !isSymbolIntendedQuery || entry.symbol !== void 0
14803
+ );
14386
14804
  const dcg = top.reduce((sum, result, i) => {
14387
- const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
14388
- return sum + rel / Math.log2(i + 2);
14805
+ let bestEvidenceIndex = -1;
14806
+ let rel = 0;
14807
+ for (let evidenceIndex = 0; evidenceIndex < availableEvidence.length; evidenceIndex += 1) {
14808
+ const entry = availableEvidence[evidenceIndex];
14809
+ if (entry.relevance > rel && evidenceMatchesResult(entry, result.filePath, result.name, isSymbolIntendedQuery)) {
14810
+ bestEvidenceIndex = evidenceIndex;
14811
+ rel = entry.relevance;
14812
+ }
14813
+ }
14814
+ if (bestEvidenceIndex < 0) {
14815
+ return sum;
14816
+ }
14817
+ availableEvidence.splice(bestEvidenceIndex, 1);
14818
+ return sum + (2 ** rel - 1) / Math.log2(i + 2);
14389
14819
  }, 0);
14390
- const idealLen = Math.min(k, relevantPaths.length);
14391
- const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
14392
- (sum, value) => sum + value,
14820
+ const dedupedRelevant = dedupeRelevantEvidence(relevant).filter(
14821
+ (entry) => !isSymbolIntendedQuery || entry.symbol !== void 0
14822
+ );
14823
+ const idealRelevances = hasGradeBasedEvidence(query) ? dedupedRelevant.map((entry) => entry.relevance).sort((a, b) => b - a).slice(0, k) : relevant.length > 0 ? [1] : [];
14824
+ const idcg = idealRelevances.reduce(
14825
+ (sum, rel, index) => sum + (2 ** rel - 1) / Math.log2(index + 2),
14393
14826
  0
14394
14827
  );
14395
- return idcg === 0 ? 0 : dcg / idcg;
14828
+ if (idcg === 0) {
14829
+ return 0;
14830
+ }
14831
+ const ndcg = dcg / idcg;
14832
+ if (ndcg <= 0) {
14833
+ return 0;
14834
+ }
14835
+ return ndcg > 1 ? 1 : ndcg;
14396
14836
  }
14397
14837
  function isDocsOrTestsPath(filePath) {
14398
14838
  const lowered = normalizePath2(filePath).toLowerCase();
14399
14839
  return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
14400
14840
  }
14401
14841
  function classifyFailureBucket(query, results, k) {
14402
- const relevantPaths = getRelevantPaths(query);
14403
- const top = uniqueResultsByPath(results).slice(0, k);
14404
- const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
14842
+ const relevant = getRelevantEvidence(query);
14843
+ const isSymbolIntendedQuery = isSymbolIntended(query);
14844
+ if (query.expected.expectedOutcome === "no-results") return void 0;
14845
+ const top = uniqueResultsByEvidence(results).slice(0, k);
14846
+ const hasRelevantTopK = top.some(
14847
+ (result) => isRelevantResult(result.filePath, result.name, relevant, isSymbolIntendedQuery)
14848
+ );
14405
14849
  if (!hasRelevantTopK) {
14850
+ const hasExpectedFileTopK = top.some((result) => isExpectedFile(result.filePath, relevant));
14851
+ if (hasExpectedFileTopK && hasSymbolRequirement(query)) {
14852
+ return "wrong-symbol";
14853
+ }
14406
14854
  return "no-relevant-hit-top-k";
14407
14855
  }
14408
- if (query.expected.symbol) {
14409
- const hasSymbol = top.some(
14410
- (result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
14411
- );
14412
- if (!hasSymbol) return "wrong-symbol";
14413
- }
14414
14856
  const top1 = top[0];
14415
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
14857
+ if (top1 && !isExpectedFile(top1.filePath, relevant) && isDocsOrTestsPath(top1.filePath)) {
14416
14858
  return "docs-tests-outranking-source";
14417
14859
  }
14418
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
14860
+ if (top1 && !isExpectedFile(top1.filePath, relevant)) {
14419
14861
  return "wrong-file";
14420
14862
  }
14421
14863
  return void 0;
@@ -14424,9 +14866,12 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14424
14866
  resolvedRoute: "search",
14425
14867
  routedQuery: query.query
14426
14868
  }, context) {
14427
- const relevantPaths = getRelevantPaths(query);
14428
- const deduped = uniqueResultsByPath(results);
14429
- const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
14869
+ const relevant = getRelevantEvidence(query);
14870
+ const isSymbolIntendedQuery = isSymbolIntended(query);
14871
+ const deduped = uniqueResultsByEvidence(results);
14872
+ const hitAt = (cutoff) => deduped.slice(0, cutoff).some(
14873
+ (result) => isRelevantResult(result.filePath, result.name, relevant, isSymbolIntendedQuery)
14874
+ );
14430
14875
  const perQuery = {
14431
14876
  id: query.id,
14432
14877
  query: query.query,
@@ -14434,13 +14879,19 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14434
14879
  retrievalMode: query.retrievalMode ?? "search",
14435
14880
  resolvedRoute: route.resolvedRoute,
14436
14881
  routedQuery: route.routedQuery,
14882
+ routeMatched: query.expected.expectedRoute ? query.expected.expectedRoute === route.resolvedRoute : void 0,
14883
+ outcomeMatched: query.expected.expectedOutcome === void 0 ? void 0 : query.expected.expectedOutcome === "results" ? deduped.length > 0 : deduped.length === 0,
14884
+ recoveryMatched: query.expected.recoveryExpectation === void 0 ? void 0 : query.expected.recoveryExpectation === "filter-relaxed" ? context?.recoveryRelaxed === true : context?.recoveryUsed !== true,
14885
+ language: query.language,
14886
+ difficulty: query.difficulty,
14887
+ tags: query.tags,
14437
14888
  latencyMs,
14438
14889
  hitAt1: hitAt(1),
14439
14890
  hitAt3: hitAt(3),
14440
14891
  hitAt5: hitAt(5),
14441
14892
  hitAt10: hitAt(10),
14442
- reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
14443
- ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
14893
+ reciprocalRankAt10: reciprocalRankAtK(deduped, relevant, isSymbolIntendedQuery, 10),
14894
+ ndcgAt10: ndcgAtK(query, deduped, relevant, isSymbolIntendedQuery, 10),
14444
14895
  failureBucket: classifyFailureBucket(query, results, k),
14445
14896
  rawTop3DistinctRatio: distinctTopKRatio(results, 3),
14446
14897
  tokenBudget: context?.tokenBudget,
@@ -14458,6 +14909,11 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14458
14909
  function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
14459
14910
  const count = perQuery.length;
14460
14911
  const safeDiv = (value) => count === 0 ? 0 : value / count;
14912
+ const positiveQueryIds = new Set(
14913
+ queries.filter((query) => query.expected.expectedOutcome !== "no-results").map((query) => query.id)
14914
+ );
14915
+ const positiveCount = perQuery.filter((query) => positiveQueryIds.has(query.id)).length;
14916
+ const safePositiveDiv = (value) => positiveCount === 0 ? 0 : value / positiveCount;
14461
14917
  const sum = {
14462
14918
  hitAt1: 0,
14463
14919
  hitAt3: 0,
@@ -14479,27 +14935,52 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
14479
14935
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
14480
14936
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
14481
14937
  const contextTokenUnits = totalContextResponseTokens / 1e3;
14938
+ let routeMatchedCount = 0;
14939
+ let routeExpectedCount = 0;
14940
+ let outcomeMatchedCount = 0;
14941
+ let outcomeExpectedCount = 0;
14942
+ let recoveryMatchedCount = 0;
14943
+ let recoveryExpectedCount = 0;
14482
14944
  for (const query of perQuery) {
14483
- if (query.hitAt1) sum.hitAt1 += 1;
14484
- if (query.hitAt3) sum.hitAt3 += 1;
14485
- if (query.hitAt5) sum.hitAt5 += 1;
14486
- if (query.hitAt10) sum.hitAt10 += 1;
14487
- sum.mrrAt10 += query.reciprocalRankAt10;
14488
- sum.ndcgAt10 += query.ndcgAt10;
14489
- sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
14945
+ if (positiveQueryIds.has(query.id)) {
14946
+ if (query.hitAt1) sum.hitAt1 += 1;
14947
+ if (query.hitAt3) sum.hitAt3 += 1;
14948
+ if (query.hitAt5) sum.hitAt5 += 1;
14949
+ if (query.hitAt10) sum.hitAt10 += 1;
14950
+ sum.mrrAt10 += query.reciprocalRankAt10;
14951
+ sum.ndcgAt10 += query.ndcgAt10;
14952
+ }
14953
+ sum.distinctTop3Ratio += distinctTopKRatio(uniqueResultsByPath(query.results), 3);
14490
14954
  sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
14491
14955
  if (query.failureBucket) {
14492
14956
  failureBuckets[query.failureBucket] += 1;
14493
14957
  }
14958
+ if (query.routeMatched !== void 0) {
14959
+ routeExpectedCount += 1;
14960
+ if (query.routeMatched) {
14961
+ routeMatchedCount += 1;
14962
+ }
14963
+ }
14964
+ if (query.outcomeMatched !== void 0) {
14965
+ outcomeExpectedCount += 1;
14966
+ if (query.outcomeMatched) outcomeMatchedCount += 1;
14967
+ }
14968
+ if (query.recoveryMatched !== void 0) {
14969
+ recoveryExpectedCount += 1;
14970
+ if (query.recoveryMatched) recoveryMatchedCount += 1;
14971
+ }
14494
14972
  }
14495
14973
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
14496
14974
  return {
14497
- hitAt1: safeDiv(sum.hitAt1),
14498
- hitAt3: safeDiv(sum.hitAt3),
14499
- hitAt5: safeDiv(sum.hitAt5),
14500
- hitAt10: safeDiv(sum.hitAt10),
14501
- mrrAt10: safeDiv(sum.mrrAt10),
14502
- ndcgAt10: safeDiv(sum.ndcgAt10),
14975
+ hitAt1: safePositiveDiv(sum.hitAt1),
14976
+ hitAt3: safePositiveDiv(sum.hitAt3),
14977
+ hitAt5: safePositiveDiv(sum.hitAt5),
14978
+ hitAt10: safePositiveDiv(sum.hitAt10),
14979
+ mrrAt10: safePositiveDiv(sum.mrrAt10),
14980
+ ndcgAt10: safePositiveDiv(sum.ndcgAt10),
14981
+ routeAccuracy: routeExpectedCount === 0 ? 0 : routeMatchedCount / routeExpectedCount,
14982
+ outcomeAccuracy: outcomeExpectedCount === 0 ? 0 : outcomeMatchedCount / outcomeExpectedCount,
14983
+ recoveryAccuracy: recoveryExpectedCount === 0 ? 0 : recoveryMatchedCount / recoveryExpectedCount,
14503
14984
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
14504
14985
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
14505
14986
  latencyMs: {
@@ -14706,6 +15187,9 @@ function isRecord3(value) {
14706
15187
  function isStringArray4(value) {
14707
15188
  return Array.isArray(value) && value.every((item) => typeof item === "string");
14708
15189
  }
15190
+ function isNonEmptyString(value) {
15191
+ return typeof value === "string" && value.trim().length > 0;
15192
+ }
14709
15193
  function asPositiveNumber(value, path29) {
14710
15194
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
14711
15195
  throw new Error(`${path29} must be a non-negative number`);
@@ -14720,11 +15204,109 @@ function parseQueryType(value, path29) {
14720
15204
  `${path29} must be one of: definition, implementation-intent, similarity, keyword-heavy, conceptual`
14721
15205
  );
14722
15206
  }
15207
+ function parseExpectedRoute(value, path29) {
15208
+ if (value === void 0) return void 0;
15209
+ if (value === "search" || value === "definition") return value;
15210
+ throw new Error(`${path29} must be one of: search, definition`);
15211
+ }
15212
+ function parseExpectedOutcome(value, path29) {
15213
+ if (value === void 0) return void 0;
15214
+ if (value === "results" || value === "no-results") {
15215
+ return value;
15216
+ }
15217
+ throw new Error(`${path29} must be one of: results, no-results`);
15218
+ }
15219
+ function parseRecoveryExpectation(value, path29) {
15220
+ if (value === void 0) return void 0;
15221
+ if (value === "none" || value === "filter-relaxed") {
15222
+ return value;
15223
+ }
15224
+ throw new Error(`${path29} must be one of: none, filter-relaxed`);
15225
+ }
15226
+ function parseQueryDifficulty(value, path29) {
15227
+ if (value === void 0) return void 0;
15228
+ if (value === "easy" || value === "medium" || value === "hard") {
15229
+ return value;
15230
+ }
15231
+ throw new Error(`${path29} must be one of: easy, medium, hard`);
15232
+ }
15233
+ function parseQueryTags(value, path29) {
15234
+ if (value === void 0) return void 0;
15235
+ if (!isStringArray4(value) || value.some((tag) => tag.trim().length === 0)) {
15236
+ throw new Error(`${path29} must be an array of non-empty strings`);
15237
+ }
15238
+ if (value.length > 16) {
15239
+ throw new Error(`${path29} must contain at most 16 tags`);
15240
+ }
15241
+ return value;
15242
+ }
15243
+ function parseQueryArgs(value, path29) {
15244
+ if (value === void 0) return void 0;
15245
+ if (!isRecord3(value)) {
15246
+ throw new Error(`${path29} must be an object`);
15247
+ }
15248
+ const symbol = parseStringOrUndefined(value.symbol, `${path29}.symbol`);
15249
+ const fileType = parseStringOrUndefined(value.fileType, `${path29}.fileType`);
15250
+ const directory = parseStringOrUndefined(value.directory, `${path29}.directory`);
15251
+ return {
15252
+ ...symbol !== void 0 ? { symbol } : {},
15253
+ ...fileType !== void 0 ? { fileType } : {},
15254
+ ...directory !== void 0 ? { directory } : {}
15255
+ };
15256
+ }
15257
+ 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-]+)*)?$/;
15258
+ function parseSemanticVersion(value, path29) {
15259
+ if (!isNonEmptyString(value)) {
15260
+ throw new Error(`${path29} must be a non-empty string`);
15261
+ }
15262
+ if (!SEMVER_VERSION_PATTERN.test(value)) {
15263
+ throw new Error(`${path29} must be a valid semantic version (MAJOR.MINOR.PATCH)`);
15264
+ }
15265
+ return value;
15266
+ }
14723
15267
  function parseRetrievalMode(value, path29) {
14724
15268
  if (value === void 0 || value === "search") return "search";
14725
15269
  if (value === "context") return value;
14726
15270
  throw new Error(`${path29} must be one of: search, context`);
14727
15271
  }
15272
+ function parseStringOrUndefined(value, path29) {
15273
+ if (value === void 0 || value === null) return void 0;
15274
+ if (!isNonEmptyString(value)) {
15275
+ throw new Error(`${path29} must be a non-empty string`);
15276
+ }
15277
+ return value;
15278
+ }
15279
+ function parseGradedEvidence(value, path29) {
15280
+ if (value === void 0) return [];
15281
+ if (!Array.isArray(value)) {
15282
+ throw new Error(`${path29} must be an array`);
15283
+ }
15284
+ return value.map((entry, index) => {
15285
+ if (!isRecord3(entry)) {
15286
+ throw new Error(`${path29}[${index}] must be an object`);
15287
+ }
15288
+ const evidencePath = parseStringOrUndefined(entry.path, `${path29}[${index}].path`);
15289
+ if (evidencePath === void 0) {
15290
+ throw new Error(`${path29}[${index}].path is required`);
15291
+ }
15292
+ const symbol = parseStringOrUndefined(entry.symbol, `${path29}[${index}].symbol`);
15293
+ const relevance = parseEvidenceRelevance(entry.relevance, `${path29}[${index}].relevance`);
15294
+ return {
15295
+ path: evidencePath,
15296
+ ...symbol !== void 0 ? { symbol } : {},
15297
+ relevance
15298
+ };
15299
+ });
15300
+ }
15301
+ function parseEvidenceRelevance(value, path29) {
15302
+ if (value === void 0) {
15303
+ throw new Error(`${path29} is required`);
15304
+ }
15305
+ if (value !== 1 && value !== 2 && value !== 3) {
15306
+ throw new Error(`${path29} must be 1, 2, or 3`);
15307
+ }
15308
+ return value;
15309
+ }
14728
15310
  function parseExpected(input, path29) {
14729
15311
  if (!isRecord3(input)) {
14730
15312
  throw new Error(`${path29} must be an object`);
@@ -14733,10 +15315,18 @@ function parseExpected(input, path29) {
14733
15315
  const acceptableFilesRaw = input.acceptableFiles;
14734
15316
  const symbolRaw = input.symbol;
14735
15317
  const branchRaw = input.branch;
14736
- const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
15318
+ const expectedRouteRaw = input.expectedRoute;
15319
+ const expectedOutcomeRaw = input.expectedOutcome;
15320
+ const recoveryExpectationRaw = input.recoveryExpectation;
15321
+ const gradedEvidenceRaw = input.gradedEvidence;
15322
+ const filePath = parseStringOrUndefined(filePathRaw, `${path29}.filePath`);
14737
15323
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
14738
- if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
14739
- throw new Error(`${path29} must include either expected.filePath or expected.acceptableFiles`);
15324
+ const gradedEvidence = parseGradedEvidence(gradedEvidenceRaw, `${path29}.gradedEvidence`);
15325
+ const expectedOutcome = parseExpectedOutcome(expectedOutcomeRaw, `${path29}.expectedOutcome`);
15326
+ if (expectedOutcome !== "no-results" && !filePath && (!acceptableFiles || acceptableFiles.length === 0) && gradedEvidence.length === 0) {
15327
+ throw new Error(
15328
+ `${path29} must include expected.filePath, expected.acceptableFiles, or expected.gradedEvidence`
15329
+ );
14740
15330
  }
14741
15331
  if (acceptableFilesRaw !== void 0 && !isStringArray4(acceptableFilesRaw)) {
14742
15332
  throw new Error(`${path29}.acceptableFiles must be an array of strings`);
@@ -14747,13 +15337,25 @@ function parseExpected(input, path29) {
14747
15337
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
14748
15338
  throw new Error(`${path29}.branch must be a string when provided`);
14749
15339
  }
15340
+ const expectedRoute = parseExpectedRoute(expectedRouteRaw, `${path29}.expectedRoute`);
15341
+ const recoveryExpectation = parseRecoveryExpectation(
15342
+ recoveryExpectationRaw,
15343
+ `${path29}.recoveryExpectation`
15344
+ );
14750
15345
  return {
14751
15346
  filePath,
14752
15347
  acceptableFiles,
14753
15348
  symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
14754
- branch: typeof branchRaw === "string" ? branchRaw : void 0
15349
+ branch: typeof branchRaw === "string" ? branchRaw : void 0,
15350
+ expectedRoute,
15351
+ expectedOutcome,
15352
+ recoveryExpectation,
15353
+ ...gradedEvidence.length > 0 ? { gradedEvidence } : {}
14755
15354
  };
14756
15355
  }
15356
+ function parseQueryLanguage(value, path29) {
15357
+ return parseStringOrUndefined(value, path29);
15358
+ }
14757
15359
  function parseQuery(input, index) {
14758
15360
  const path29 = `queries[${index}]`;
14759
15361
  if (!isRecord3(input)) {
@@ -14764,6 +15366,10 @@ function parseQuery(input, index) {
14764
15366
  const queryType = input.queryType;
14765
15367
  const retrievalMode = input.retrievalMode;
14766
15368
  const expected = input.expected;
15369
+ const language = input.language;
15370
+ const difficulty = input.difficulty;
15371
+ const tags = input.tags;
15372
+ const args = input.args;
14767
15373
  if (typeof id !== "string" || id.trim().length === 0) {
14768
15374
  throw new Error(`${path29}.id must be a non-empty string`);
14769
15375
  }
@@ -14775,6 +15381,10 @@ function parseQuery(input, index) {
14775
15381
  query,
14776
15382
  queryType: parseQueryType(queryType, `${path29}.queryType`),
14777
15383
  retrievalMode: parseRetrievalMode(retrievalMode, `${path29}.retrievalMode`),
15384
+ language: parseQueryLanguage(language, `${path29}.language`),
15385
+ difficulty: parseQueryDifficulty(difficulty, `${path29}.difficulty`),
15386
+ args: parseQueryArgs(args, `${path29}.args`),
15387
+ tags: parseQueryTags(tags, `${path29}.tags`),
14778
15388
  expected: parseExpected(expected, `${path29}.expected`)
14779
15389
  };
14780
15390
  }
@@ -14786,9 +15396,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14786
15396
  const name = raw.name;
14787
15397
  const description = raw.description;
14788
15398
  const queriesRaw = raw.queries;
14789
- if (typeof version !== "string" || version.trim().length === 0) {
14790
- throw new Error(`${sourceLabel}.version must be a non-empty string`);
14791
- }
15399
+ const validatedVersion = parseSemanticVersion(version, `${sourceLabel}.version`);
14792
15400
  if (typeof name !== "string" || name.trim().length === 0) {
14793
15401
  throw new Error(`${sourceLabel}.name must be a non-empty string`);
14794
15402
  }
@@ -14810,7 +15418,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14810
15418
  idSet.add(query.id);
14811
15419
  }
14812
15420
  return {
14813
- version,
15421
+ version: validatedVersion,
14814
15422
  name,
14815
15423
  description: typeof description === "string" ? description : void 0,
14816
15424
  queries
@@ -14870,16 +15478,8 @@ function parseBudget(raw, sourceLabel) {
14870
15478
  "p95LatencyMaxAbsoluteMs",
14871
15479
  sourceLabel
14872
15480
  ),
14873
- minHitAt5: parseThresholdValue(
14874
- thresholds.minHitAt5,
14875
- "minHitAt5",
14876
- sourceLabel
14877
- ),
14878
- minMrrAt10: parseThresholdValue(
14879
- thresholds.minMrrAt10,
14880
- "minMrrAt10",
14881
- sourceLabel
14882
- ),
15481
+ minHitAt5: parseThresholdValue(thresholds.minHitAt5, "minHitAt5", sourceLabel),
15482
+ minMrrAt10: parseThresholdValue(thresholds.minMrrAt10, "minMrrAt10", sourceLabel),
14883
15483
  minRawDistinctTop3Ratio: parseThresholdValue(
14884
15484
  thresholds.minRawDistinctTop3Ratio,
14885
15485
  "minRawDistinctTop3Ratio",
@@ -14929,6 +15529,26 @@ function loadBudget(budgetPath) {
14929
15529
  }
14930
15530
 
14931
15531
  // src/eval/runner.ts
15532
+ function normalizeForFingerprint(value) {
15533
+ if (Array.isArray(value)) {
15534
+ return value.map((entry) => normalizeForFingerprint(entry));
15535
+ }
15536
+ if (value && typeof value === "object") {
15537
+ const normalized = {};
15538
+ for (const key of Object.keys(value).sort()) {
15539
+ const normalizedValue = normalizeForFingerprint(value[key]);
15540
+ if (normalizedValue !== void 0) {
15541
+ normalized[key] = normalizedValue;
15542
+ }
15543
+ }
15544
+ return normalized;
15545
+ }
15546
+ return value;
15547
+ }
15548
+ function buildDatasetFingerprint(dataset) {
15549
+ const canonical = JSON.stringify(normalizeForFingerprint(dataset));
15550
+ return crypto.createHash("sha256").update(canonical).digest("hex");
15551
+ }
14932
15552
  async function runEvaluation(options) {
14933
15553
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
14934
15554
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -14953,27 +15573,40 @@ async function runEvaluation(options) {
14953
15573
  const start = performance3.now();
14954
15574
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
14955
15575
  query: query.query,
15576
+ symbol: query.args?.symbol,
15577
+ fileType: query.args?.fileType,
15578
+ directory: query.args?.directory,
14956
15579
  limit: 10,
14957
15580
  tokenBudget: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET
14958
15581
  }, {
14959
- lookup: (symbol, limit, _scope) => indexer.search(symbol, limit, {
15582
+ lookup: (symbol, limit, scope) => indexer.search(symbol, limit, {
14960
15583
  metadataOnly: true,
14961
15584
  filterByBranch: !!query.expected.branch,
14962
- definitionIntent: true
15585
+ definitionIntent: true,
15586
+ fileType: scope.fileType,
15587
+ directory: scope.directory
14963
15588
  }),
14964
- search: (searchQuery, limit, _scope) => indexer.search(searchQuery, limit, {
15589
+ search: (searchQuery, limit, scope) => indexer.search(searchQuery, limit, {
14965
15590
  metadataOnly: true,
14966
15591
  filterByBranch: !!query.expected.branch,
14967
- definitionIntent: false
15592
+ definitionIntent: false,
15593
+ fileType: scope.fileType,
15594
+ directory: scope.directory
14968
15595
  })
14969
15596
  }) : void 0;
14970
15597
  const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
14971
15598
  metadataOnly: true,
14972
- filterByBranch: !!query.expected.branch
15599
+ filterByBranch: !!query.expected.branch,
15600
+ fileType: query.args?.fileType,
15601
+ directory: query.args?.directory
14973
15602
  });
14974
15603
  const elapsed = performance3.now() - start;
14975
15604
  const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
14976
15605
  const routedQuery = contextResult?.details?.routedQuery ?? query.query;
15606
+ const successfulRecoveryAttempt = contextResult?.details?.recovery?.successfulAttemptIndex;
15607
+ const recoveryAttempts = contextResult?.details?.recovery?.attempts ?? [];
15608
+ const recoveryRelaxed = successfulRecoveryAttempt === void 0 ? false : (recoveryAttempts[successfulRecoveryAttempt]?.relaxedFields.length ?? 0) > 0;
15609
+ const recoveryUsed = recoveryAttempts.length > 1 || recoveryAttempts.some((attempt) => attempt.relaxedFields.length > 0);
14977
15610
  const materialized = result.map((item) => ({
14978
15611
  filePath: item.filePath,
14979
15612
  startLine: item.startLine,
@@ -14990,7 +15623,9 @@ async function runEvaluation(options) {
14990
15623
  responseTokens: contextResult.details.tokenEstimate,
14991
15624
  candidateCount: contextResult.details.candidateCount ?? 0,
14992
15625
  deduplicatedCount: contextResult.details.deduplicatedCount ?? 0,
14993
- omittedCount: contextResult.details.omittedCount ?? 0
15626
+ omittedCount: contextResult.details.omittedCount ?? 0,
15627
+ recoveryUsed,
15628
+ recoveryRelaxed
14994
15629
  } : void 0));
14995
15630
  }
14996
15631
  const logger = indexer.getLogger();
@@ -15002,6 +15637,7 @@ async function runEvaluation(options) {
15002
15637
  datasetPath,
15003
15638
  datasetName: dataset.name,
15004
15639
  datasetVersion: dataset.version,
15640
+ datasetFingerprint: buildDatasetFingerprint(dataset),
15005
15641
  queryCount: dataset.queries.length,
15006
15642
  topK: 10,
15007
15643
  searchConfig: {
@@ -17918,7 +18554,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
17918
18554
  if (isGitRepo(projectRoot)) {
17919
18555
  gitWatcher = new GitHeadWatcher(projectRoot);
17920
18556
  gitWatcher.start(async (oldBranch, newBranch) => {
17921
- console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
18557
+ getIndexer().getLogger().branch("info", "Branch changed", {
18558
+ oldBranch,
18559
+ newBranch
18560
+ });
17922
18561
  requestReindex();
17923
18562
  });
17924
18563
  }