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.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";
@@ -4024,9 +4039,21 @@ function parseFiles(files) {
4024
4039
  return result.map((f) => ({
4025
4040
  path: f.path,
4026
4041
  chunks: f.chunks.map(mapChunk),
4042
+ symbols: (f.symbols ?? []).map(mapParsedSymbol),
4027
4043
  hash: f.hash
4028
4044
  }));
4029
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
+ }
4030
4057
  function mapChunk(c) {
4031
4058
  return {
4032
4059
  content: c.content,
@@ -6847,6 +6874,7 @@ var INDEX_METADATA_VERSION = "1";
6847
6874
  var EMBEDDING_STRATEGY_VERSION = "2";
6848
6875
  var SWIFT_PARSER_VERSION = "1";
6849
6876
  var METAL_PARSER_VERSION = "1";
6877
+ var SYMBOL_EXTRACTOR_VERSION = "1";
6850
6878
  var RANKING_TOKEN_CACHE_LIMIT = 4096;
6851
6879
  var RANK_HYBRID_CACHE_LIMIT = 256;
6852
6880
  function createPendingChunkStorageText(texts) {
@@ -7148,7 +7176,7 @@ function classifyQueryIntentRaw(query) {
7148
7176
  return "neutral";
7149
7177
  }
7150
7178
  function isImplementationChunkType(chunkType) {
7151
- return [
7179
+ return CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunkType) || [
7152
7180
  "export_statement",
7153
7181
  "function",
7154
7182
  "function_declaration",
@@ -7593,7 +7621,7 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
7593
7621
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
7594
7622
  return [...promoted, ...remainder];
7595
7623
  }
7596
- function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7624
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, branchSymbolIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
7597
7625
  if (!prioritizeSourcePaths) {
7598
7626
  return [];
7599
7627
  }
@@ -7607,14 +7635,14 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7607
7635
  const primaryHint = extractPrimaryIdentifierQueryHint(query);
7608
7636
  const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
7609
7637
  if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
7610
- return;
7638
+ return false;
7611
7639
  }
7612
7640
  const chunkType = chunk.nodeType ?? "other";
7613
7641
  if (!isImplementationChunkType(chunkType)) {
7614
- return;
7642
+ return false;
7615
7643
  }
7616
7644
  if (!isLikelyImplementationPath2(chunk.filePath)) {
7617
- return;
7645
+ return false;
7618
7646
  }
7619
7647
  const nameLower = (chunk.name ?? "").toLowerCase();
7620
7648
  const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
@@ -7636,6 +7664,7 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7636
7664
  }
7637
7665
  });
7638
7666
  }
7667
+ return true;
7639
7668
  };
7640
7669
  const normalizedHints = identifierHints.flatMap((hint) => [
7641
7670
  hint,
@@ -7657,12 +7686,46 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7657
7686
  dedupSymbols.set(symbol.id, symbol);
7658
7687
  }
7659
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
+ }
7660
7695
  const chunks = database.getChunksByFile(symbol.filePath);
7696
+ let foundCoveringChunk = false;
7661
7697
  for (const chunk of chunks) {
7662
7698
  if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
7663
7699
  continue;
7664
7700
  }
7665
- 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
+ });
7666
7729
  }
7667
7730
  }
7668
7731
  const dedupChunksByName = /* @__PURE__ */ new Map();
@@ -7670,6 +7733,9 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
7670
7733
  dedupChunksByName.set(chunk.chunkId, chunk);
7671
7734
  }
7672
7735
  for (const chunk of dedupChunksByName.values()) {
7736
+ if (filePathHint && !pathMatchesHint(chunk.filePath, filePathHint)) {
7737
+ continue;
7738
+ }
7673
7739
  upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
7674
7740
  }
7675
7741
  }
@@ -8211,6 +8277,10 @@ var Indexer = class _Indexer {
8211
8277
  const projectHash = hashContent(path13.resolve(this.projectRoot)).slice(0, 16);
8212
8278
  return `${key}.${projectHash}`;
8213
8279
  }
8280
+ getSymbolExtractorVersionMetadataKey(catalogIdentity = this.getBranchCatalogIdentity()) {
8281
+ const branchKey = this.getBranchCatalogKeyFor(catalogIdentity);
8282
+ return `index.symbolExtractorVersion.${hashContent(branchKey).slice(0, 24)}`;
8283
+ }
8214
8284
  hasProjectForceReembedPending() {
8215
8285
  return this.config.scope === "global" && this.database?.getMetadata(this.getProjectForceReembedMetadataKey()) === "true";
8216
8286
  }
@@ -9554,7 +9624,8 @@ var Indexer = class _Indexer {
9554
9624
  }
9555
9625
  const branchKey = this.getBranchCatalogKey();
9556
9626
  const alreadyIndexed = database.getBranchChunkIds(branchKey).length > 0 && database.getBranchSymbolIds(branchKey).length > 0;
9557
- if (alreadyIndexed && this.getStoredBranchCommit(database) === normalizedCommit) {
9627
+ const symbolsCurrent = database.getMetadata(this.getSymbolExtractorVersionMetadataKey()) === SYMBOL_EXTRACTOR_VERSION;
9628
+ if (alreadyIndexed && symbolsCurrent && this.getStoredBranchCommit(database) === normalizedCommit) {
9558
9629
  return { prepared: false };
9559
9630
  }
9560
9631
  const stats = await this.indexUnlocked(onProgress, [], true);
@@ -9613,6 +9684,8 @@ var Indexer = class _Indexer {
9613
9684
  const reparseCachedSwiftFiles = database.getMetadata(swiftParserMetadataKey) !== SWIFT_PARSER_VERSION;
9614
9685
  const metalParserMetadataKey = this.getMetalParserVersionMetadataKey();
9615
9686
  const reparseCachedMetalFiles = database.getMetadata(metalParserMetadataKey) !== METAL_PARSER_VERSION;
9687
+ const symbolExtractorMetadataKey = this.getSymbolExtractorVersionMetadataKey();
9688
+ const refreshCachedSymbols = database.getMetadata(symbolExtractorMetadataKey) !== SYMBOL_EXTRACTOR_VERSION;
9616
9689
  if (reparseCachedSwiftFiles && Array.from(this.fileHashCache.keys()).some(
9617
9690
  (filePath) => path13.extname(filePath).toLowerCase() === ".swift"
9618
9691
  )) {
@@ -9656,7 +9729,7 @@ var Indexer = class _Indexer {
9656
9729
  );
9657
9730
  const requiresSwiftParserUpgrade = reparseCachedSwiftFiles && path13.extname(canonicalPath).toLowerCase() === ".swift";
9658
9731
  const requiresMetalParserUpgrade = reparseCachedMetalFiles && path13.extname(canonicalPath).toLowerCase() === ".metal";
9659
- if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade) {
9732
+ if (cachedHashMatches && !needsCallGraphRefresh && !requiresSwiftParserUpgrade && !requiresMetalParserUpgrade && !refreshCachedSymbols) {
9660
9733
  unchangedFilePaths.add(canonicalPath);
9661
9734
  this.logger.recordCacheHit();
9662
9735
  } else {
@@ -9867,37 +9940,27 @@ var Indexer = class _Indexer {
9867
9940
  const parsed = parsedFiles[i];
9868
9941
  const changedFile = changedFiles[i];
9869
9942
  const fileSymbols = [];
9870
- for (const chunk of parsed.chunks) {
9871
- if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
9872
- const existingMetalSymbol = chunk.language === "metal" ? fileSymbols.find(
9873
- (symbol2) => symbol2.name === chunk.name && symbol2.kind === chunk.chunkType && symbol2.startLine <= chunk.endLine && chunk.startLine <= symbol2.endLine
9874
- ) : void 0;
9875
- if (existingMetalSymbol) {
9876
- existingMetalSymbol.startLine = Math.min(existingMetalSymbol.startLine, chunk.startLine);
9877
- existingMetalSymbol.endLine = Math.max(existingMetalSymbol.endLine, chunk.endLine);
9878
- existingMetalSymbol.startCol = Math.min(existingMetalSymbol.startCol, chunk.startCol ?? 0);
9879
- existingMetalSymbol.endCol = Math.max(existingMetalSymbol.endCol, chunk.endCol ?? 0);
9880
- continue;
9881
- }
9943
+ for (const parsedSymbol of parsed.symbols) {
9944
+ if (!CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(parsedSymbol.kind)) continue;
9882
9945
  const preparedNamespace = this.getPreparedBranchNamespace();
9883
9946
  const symbolId = `sym_${hashContent(
9884
- (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
9885
9948
  ).slice(0, 16)}`;
9886
9949
  const symbol = {
9887
9950
  id: symbolId,
9888
9951
  filePath: parsed.path,
9889
- name: chunk.name,
9890
- kind: chunk.chunkType,
9891
- startLine: chunk.startLine,
9892
- startCol: chunk.startCol ?? 0,
9893
- endLine: chunk.endLine,
9894
- endCol: chunk.endCol ?? 0,
9895
- 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
9896
9959
  };
9897
9960
  fileSymbols.push(symbol);
9898
9961
  allSymbolIds.add(symbolId);
9899
9962
  }
9900
- const fileLanguage = parsed.chunks[0]?.language;
9963
+ const fileLanguage = parsed.symbols[0]?.language ?? parsed.chunks[0]?.language;
9901
9964
  const isCaseInsensitiveLanguage = !!fileLanguage && CASE_INSENSITIVE_LANGUAGES.has(fileLanguage);
9902
9965
  const normalizeSymbolKey = (name) => isCaseInsensitiveLanguage ? name.toLowerCase() : name;
9903
9966
  const symbolsByName = /* @__PURE__ */ new Map();
@@ -10013,6 +10076,7 @@ var Indexer = class _Indexer {
10013
10076
  }
10014
10077
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10015
10078
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10079
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10016
10080
  this.saveBranchCommit(database, indexedCommit);
10017
10081
  this.saveIndexMetadata(configuredProviderInfo);
10018
10082
  this.indexCompatibility = { compatible: true };
@@ -10049,6 +10113,7 @@ var Indexer = class _Indexer {
10049
10113
  }
10050
10114
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10051
10115
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10116
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10052
10117
  this.saveBranchCommit(database, indexedCommit);
10053
10118
  this.saveIndexMetadata(configuredProviderInfo);
10054
10119
  this.indexCompatibility = { compatible: true };
@@ -10328,6 +10393,7 @@ var Indexer = class _Indexer {
10328
10393
  }
10329
10394
  database.setMetadata(swiftParserMetadataKey, SWIFT_PARSER_VERSION);
10330
10395
  database.setMetadata(metalParserMetadataKey, METAL_PARSER_VERSION);
10396
+ database.setMetadata(symbolExtractorMetadataKey, SYMBOL_EXTRACTOR_VERSION);
10331
10397
  this.saveBranchCommit(database, indexedCommit);
10332
10398
  this.saveIndexMetadata(configuredProviderInfo);
10333
10399
  this.indexCompatibility = { compatible: true };
@@ -10466,10 +10532,11 @@ var Indexer = class _Indexer {
10466
10532
  const keywordResults = await this.keywordSearch(query, maxResults * 4, store, invertedIndex);
10467
10533
  const keywordMs = performance2.now() - keywordStartTime;
10468
10534
  let branchChunkIds = null;
10535
+ let branchSymbolIds = null;
10469
10536
  if (filterByBranch && (this.config.scope === "global" || this.currentBranch !== "default")) {
10470
- branchChunkIds = new Set(
10471
- this.getBranchCatalogKeys().flatMap((branchKey) => database.getBranchChunkIds(branchKey))
10472
- );
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)));
10473
10540
  }
10474
10541
  const prefilterStartTime = performance2.now();
10475
10542
  const shouldPrefilterByBranch = branchChunkIds !== null && (this.config.scope === "global" || branchChunkIds.size > 0);
@@ -10541,6 +10608,7 @@ var Indexer = class _Indexer {
10541
10608
  query,
10542
10609
  database,
10543
10610
  branchChunkIds,
10611
+ branchSymbolIds,
10544
10612
  maxResults,
10545
10613
  union,
10546
10614
  sourceIntent
@@ -10554,7 +10622,7 @@ var Indexer = class _Indexer {
10554
10622
  (r) => isLikelyImplementationPath2(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
10555
10623
  );
10556
10624
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
10557
- 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) : [];
10558
10626
  const finalResults = filtered.length > 0 ? filtered : identifierFallback;
10559
10627
  const totalSearchMs = performance2.now() - searchStartTime;
10560
10628
  this.logger.recordSearch(totalSearchMs, {
@@ -10711,7 +10779,7 @@ var Indexer = class _Indexer {
10711
10779
  const extension = path13.extname(filePath).toLowerCase();
10712
10780
  return extension === ".php" || extension === ".c" || extension === ".cc" || extension === ".cpp" || extension === ".cxx";
10713
10781
  });
10714
- 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) {
10715
10783
  return { readable: true, current: false, reason: "migration-required" };
10716
10784
  }
10717
10785
  if (isGitRepo(this.materializedProjectRoot)) {
@@ -11419,7 +11487,10 @@ var Indexer = class _Indexer {
11419
11487
  const requestedRef = opts.pr !== void 0 ? expectedCommit : headRefName ?? resolvedBranch;
11420
11488
  const storedCommit = this.getStoredBranchCommit(database, catalogIdentity);
11421
11489
  const catalogIdentityMatches = storedCommit === expectedCommit;
11422
- 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) {
11423
11494
  if (!resolvedBranch || resolvedBranch === "default") {
11424
11495
  throw new Error("Run index_codebase first to build the call graph and symbol index for this project.");
11425
11496
  }
@@ -11735,8 +11806,17 @@ function fitTextToContextBudget(text, tokenBudget) {
11735
11806
  function normalizedLineRange(result) {
11736
11807
  return result.startLine <= result.endLine ? { start: result.startLine, end: result.endLine } : { start: result.endLine, end: result.startLine };
11737
11808
  }
11738
- function rankContextCandidates(results) {
11739
- 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
+ });
11740
11820
  }
11741
11821
  function deduplicateContextCandidates(candidates) {
11742
11822
  const acceptedByFile = /* @__PURE__ */ new Map();
@@ -11825,7 +11905,12 @@ function buildContextPack(results, options = {}) {
11825
11905
  const maxResults = Math.max(0, Math.floor(options.maxResults ?? results.length));
11826
11906
  const includeExactSearchHandoff = options.includeExactSearchHandoff ?? false;
11827
11907
  const candidateCount = results.length;
11828
- const deduplicated = deduplicateContextCandidates(rankContextCandidates(results));
11908
+ const deduplicated = deduplicateContextCandidates(
11909
+ rankContextCandidates(
11910
+ results,
11911
+ options.preferImplementationPaths ?? false
11912
+ )
11913
+ );
11829
11914
  const diversified = diversifyContextCandidates(deduplicated);
11830
11915
  const duplicateCount = candidateCount - deduplicated.length;
11831
11916
  const selectable = diversified.slice(0, maxResults);
@@ -12197,7 +12282,7 @@ ${truncateContent(r.content)}
12197
12282
  }
12198
12283
 
12199
12284
  // src/utils/effectiveness-metrics.ts
12200
- var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 2;
12285
+ var EFFECTIVENESS_METRICS_SCHEMA_VERSION = 3;
12201
12286
  var MAX_EFFECTIVENESS_COUNTER = 1e9;
12202
12287
  var EFFECTIVENESS_TOOL_ROUTES = [
12203
12288
  "context-conceptual",
@@ -12243,6 +12328,9 @@ var EFFECTIVENESS_SCOPE_RELAXATION_BUCKETS = ["none", "directory", "file-type",
12243
12328
  function emptyCounterMap(values) {
12244
12329
  return Object.fromEntries(values.map((value) => [value, 0]));
12245
12330
  }
12331
+ function emptyRouteCounterMap(routes, values) {
12332
+ return Object.fromEntries(routes.map((route) => [route, emptyCounterMap(values)]));
12333
+ }
12246
12334
  function boundedNumber(value) {
12247
12335
  if (value === void 0 || !Number.isFinite(value)) return 0;
12248
12336
  return Math.max(0, Math.floor(value));
@@ -12291,6 +12379,11 @@ function allowedValue(value, allowed, fallback) {
12291
12379
  function cloneCounterMap(counters) {
12292
12380
  return { ...counters };
12293
12381
  }
12382
+ function cloneRouteCounterMap(counters) {
12383
+ return Object.fromEntries(
12384
+ EFFECTIVENESS_TOOL_ROUTES.map((route) => [route, cloneCounterMap(counters[route])])
12385
+ );
12386
+ }
12294
12387
  var EffectivenessMetrics = class {
12295
12388
  constructor(counterCap = MAX_EFFECTIVENESS_COUNTER) {
12296
12389
  this.counterCap = counterCap;
@@ -12306,7 +12399,7 @@ var EffectivenessMetrics = class {
12306
12399
  lifetime: "process",
12307
12400
  reset: "index_metrics-reset-or-process-exit",
12308
12401
  maxCounterValue: this.counterCap,
12309
- dimensions: "bounded-host-and-category-only"
12402
+ dimensions: "bounded-route-and-bucketed-performance-only"
12310
12403
  },
12311
12404
  totalCalls: 0,
12312
12405
  toolRoute: emptyCounterMap(EFFECTIVENESS_TOOL_ROUTES),
@@ -12318,7 +12411,14 @@ var EffectivenessMetrics = class {
12318
12411
  tokenBudget: emptyCounterMap(EFFECTIVENESS_TOKEN_BUDGET_BUCKETS),
12319
12412
  returnedTokenEstimate: emptyCounterMap(EFFECTIVENESS_RETURNED_TOKEN_BUCKETS),
12320
12413
  exactHandoffEmitted: emptyCounterMap(EFFECTIVENESS_BOOLEAN_BUCKETS),
12321
- 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
+ )
12322
12422
  };
12323
12423
  }
12324
12424
  increment(counters, key) {
@@ -12340,12 +12440,19 @@ var EffectivenessMetrics = class {
12340
12440
  this.increment(this.snapshot.hostMode, host);
12341
12441
  this.increment(this.snapshot.outcome, outcome);
12342
12442
  this.increment(this.snapshot.recoveryUsed, recoveryUsed);
12343
- this.increment(this.snapshot.resultCount, resultCountBucket(event.resultCount));
12344
- 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);
12345
12448
  this.increment(this.snapshot.tokenBudget, tokenBudgetBucket(event.tokenBudget));
12346
- this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucket(event.returnedTokenEstimate));
12449
+ this.increment(this.snapshot.returnedTokenEstimate, returnedTokenBucketValue);
12347
12450
  this.increment(this.snapshot.exactHandoffEmitted, exactHandoffEmitted);
12348
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);
12349
12456
  }
12350
12457
  getSnapshot() {
12351
12458
  return {
@@ -12360,7 +12467,11 @@ var EffectivenessMetrics = class {
12360
12467
  tokenBudget: cloneCounterMap(this.snapshot.tokenBudget),
12361
12468
  returnedTokenEstimate: cloneCounterMap(this.snapshot.returnedTokenEstimate),
12362
12469
  exactHandoffEmitted: cloneCounterMap(this.snapshot.exactHandoffEmitted),
12363
- 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)
12364
12475
  };
12365
12476
  }
12366
12477
  reset() {
@@ -12379,6 +12490,7 @@ function resetProcessEffectivenessMetrics() {
12379
12490
  }
12380
12491
  function formatEffectivenessMetrics(snapshot) {
12381
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("; ");
12382
12494
  const lines = [
12383
12495
  `Privacy-safe effectiveness (schema v${snapshot.schemaVersion}):`,
12384
12496
  ` Retention: ${snapshot.retention.storage}, ${snapshot.retention.lifetime}-lifetime; reset with index_metrics(reset=true) or process exit`,
@@ -12393,6 +12505,10 @@ function formatEffectivenessMetrics(snapshot) {
12393
12505
  ` Latency bucket: ${formatCounters(snapshot.latency)}`,
12394
12506
  ` Token-budget bucket: ${formatCounters(snapshot.tokenBudget)}`,
12395
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)}`,
12396
12512
  ` Exact handoff emitted: ${formatCounters(snapshot.exactHandoffEmitted)}`,
12397
12513
  ` Scope relaxation: ${formatCounters(snapshot.scopeRelaxation)}`,
12398
12514
  " Privacy: no queries, response text, source, symbols, paths, repository names, user identity, or stable identifiers are retained."
@@ -12404,6 +12520,103 @@ function formatEffectivenessMetrics(snapshot) {
12404
12520
  import { existsSync as existsSync8, realpathSync as realpathSync4 } from "fs";
12405
12521
  import * as os6 from "os";
12406
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
12407
12620
  var MAX_RETRY_DELAY_MS = 1e4;
12408
12621
  var SHUTDOWN_WAIT_MS = 2e3;
12409
12622
  var coordinators = /* @__PURE__ */ new Map();
@@ -12520,7 +12733,13 @@ var AutoIndexCoordinator = class {
12520
12733
  activation = Promise.resolve();
12521
12734
  inFlight = null;
12522
12735
  activeRequest = null;
12736
+ batteryCheck = null;
12737
+ batteryIndexJob = null;
12738
+ batteryDeferredRequest = null;
12739
+ batteryRetryTimer = null;
12740
+ resolveBatteryRetry = null;
12523
12741
  pendingRequest = null;
12742
+ pendingFollowUp = null;
12524
12743
  abortController = null;
12525
12744
  stopped = false;
12526
12745
  constructor(registration) {
@@ -12533,6 +12752,7 @@ var AutoIndexCoordinator = class {
12533
12752
  };
12534
12753
  }
12535
12754
  update(registration) {
12755
+ const pauseOnBatteryChanged = registration.config.indexing.pauseBackgroundIndexingOnBattery !== this.registration.config.indexing.pauseBackgroundIndexingOnBattery;
12536
12756
  this.registration = registration;
12537
12757
  this.status.enabled = registration.config.indexing.autoIndex;
12538
12758
  this.status.blockedReason = registration.blockedReason;
@@ -12548,6 +12768,9 @@ var AutoIndexCoordinator = class {
12548
12768
  this.setState("idle", { source: void 0 });
12549
12769
  }
12550
12770
  }
12771
+ if (pauseOnBatteryChanged) {
12772
+ this.cancelBatteryRetry();
12773
+ }
12551
12774
  }
12552
12775
  activateAfter(activation) {
12553
12776
  this.activation = activation;
@@ -12569,7 +12792,26 @@ var AutoIndexCoordinator = class {
12569
12792
  if (this.stopped) {
12570
12793
  return Promise.resolve({ outcome: "stopped" });
12571
12794
  }
12572
- 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;
12573
12815
  }
12574
12816
  enqueueRequest(request) {
12575
12817
  if (this.stopped || !this.canRun(request)) {
@@ -12587,6 +12829,11 @@ var AutoIndexCoordinator = class {
12587
12829
  }
12588
12830
  if (request.source === "watcher") {
12589
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
+ });
12590
12837
  }
12591
12838
  return this.inFlight;
12592
12839
  }
@@ -12603,6 +12850,8 @@ var AutoIndexCoordinator = class {
12603
12850
  }
12604
12851
  async stop(waitForCompletion = false) {
12605
12852
  this.stopped = true;
12853
+ this.batteryDeferredRequest = null;
12854
+ this.cancelBatteryRetry();
12606
12855
  this.pendingRequest = null;
12607
12856
  this.abortController?.abort();
12608
12857
  this.setState("stopped", {
@@ -12632,10 +12881,20 @@ var AutoIndexCoordinator = class {
12632
12881
  this.inFlight = null;
12633
12882
  this.activeRequest = null;
12634
12883
  this.abortController = null;
12884
+ if (this.batteryIndexJob === job) {
12885
+ this.batteryIndexJob = null;
12886
+ this.batteryCheck = null;
12887
+ }
12635
12888
  const pending = this.pendingRequest;
12636
12889
  this.pendingRequest = null;
12637
12890
  if (pending && !this.stopped) {
12638
- 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
+ });
12639
12898
  }
12640
12899
  });
12641
12900
  return job;
@@ -12799,6 +13058,68 @@ var AutoIndexCoordinator = class {
12799
13058
  }
12800
13059
  return this.registration.safeToRun && this.registration.config.indexing.autoIndex;
12801
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
+ }
12802
13123
  };
12803
13124
  function getCoordinator(projectRoot, host) {
12804
13125
  const key = coordinatorKeysByProject.get(projectLookupKey(projectRoot, host));
@@ -12808,6 +13129,9 @@ function configureAutoIndex(projectRoot, host, config, getIndexer) {
12808
13129
  const projectKey = projectLookupKey(projectRoot, host);
12809
13130
  const safety = getProjectSafety(projectRoot, config);
12810
13131
  const registration = {
13132
+ backgroundIndexingPolicy: createBackgroundIndexingPolicy(
13133
+ config.indexing.pauseBackgroundIndexingOnBattery
13134
+ ),
12811
13135
  config,
12812
13136
  getIndexer,
12813
13137
  projectRoot,
@@ -14031,6 +14355,7 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14031
14355
  const results = await tryConceptualSearch(attempt.queryText, attempt.scope, attempt.relaxed);
14032
14356
  if (results.length > 0) {
14033
14357
  const heading = buildPackHeading("conceptual", decisions);
14358
+ const intent = analyzeQueryIntent(attempt.queryText);
14034
14359
  return toResult(
14035
14360
  "conceptual",
14036
14361
  attempt.queryText,
@@ -14038,7 +14363,8 @@ Explicit symbol lookup only; conceptual search was not attempted.`
14038
14363
  tokenBudget,
14039
14364
  maxResults: limit,
14040
14365
  heading,
14041
- includeExactSearchHandoff: true
14366
+ includeExactSearchHandoff: true,
14367
+ preferImplementationPaths: intent.preferSourcePaths || intent.primary !== "docs" && intent.primary !== "test"
14042
14368
  })
14043
14369
  );
14044
14370
  }
@@ -14339,17 +14665,26 @@ function percentile(values, p) {
14339
14665
  function normalizePath2(input) {
14340
14666
  return normalizePathSeparators(input);
14341
14667
  }
14342
- function uniqueResultsByPath(results) {
14668
+ function uniqueResultsByEvidence(results) {
14343
14669
  const seen = /* @__PURE__ */ new Set();
14344
14670
  const unique = [];
14345
14671
  for (const result of results) {
14346
- const normalized = normalizePath2(result.filePath);
14347
- if (seen.has(normalized)) continue;
14348
- seen.add(normalized);
14672
+ const key = `${normalizePath2(result.filePath)}::${result.name ?? ""}`;
14673
+ if (seen.has(key)) continue;
14674
+ seen.add(key);
14349
14675
  unique.push(result);
14350
14676
  }
14351
14677
  return unique;
14352
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
+ }
14353
14688
  function distinctTopKRatio(results, k) {
14354
14689
  const top = results.slice(0, k);
14355
14690
  if (top.length === 0) return 0;
@@ -14360,60 +14695,169 @@ function pathMatchesExpected(actualPath, expectedPath) {
14360
14695
  const actual = normalizePath2(actualPath);
14361
14696
  const expected = normalizePath2(expectedPath);
14362
14697
  if (actual === expected) return true;
14363
- 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;
14364
14764
  }
14365
- function getRelevantPaths(query) {
14366
- const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
14367
- const fromAcceptable = query.expected.acceptableFiles ?? [];
14368
- 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;
14369
14776
  }
14370
- function isRelevantResult(filePath, relevantPaths) {
14371
- return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
14777
+ function hasGradeBasedEvidence(query) {
14778
+ return (query.expected.gradedEvidence?.length ?? 0) > 0;
14372
14779
  }
14373
- function reciprocalRankAtK(results, relevantPaths, k) {
14374
- 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);
14375
14792
  for (let i = 0; i < top.length; i += 1) {
14376
- if (isRelevantResult(top[i].filePath, relevantPaths)) {
14793
+ if (isRelevantResult(top[i].filePath, top[i].name, relevant, isSymbolIntendedQuery)) {
14377
14794
  return 1 / (i + 1);
14378
14795
  }
14379
14796
  }
14380
14797
  return 0;
14381
14798
  }
14382
- function ndcgAtK(results, relevantPaths, k) {
14383
- 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
+ );
14384
14804
  const dcg = top.reduce((sum, result, i) => {
14385
- const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
14386
- 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);
14387
14819
  }, 0);
14388
- const idealLen = Math.min(k, relevantPaths.length);
14389
- const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
14390
- (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),
14391
14826
  0
14392
14827
  );
14393
- 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;
14394
14836
  }
14395
14837
  function isDocsOrTestsPath(filePath) {
14396
14838
  const lowered = normalizePath2(filePath).toLowerCase();
14397
14839
  return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
14398
14840
  }
14399
14841
  function classifyFailureBucket(query, results, k) {
14400
- const relevantPaths = getRelevantPaths(query);
14401
- const top = uniqueResultsByPath(results).slice(0, k);
14402
- 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
+ );
14403
14849
  if (!hasRelevantTopK) {
14850
+ const hasExpectedFileTopK = top.some((result) => isExpectedFile(result.filePath, relevant));
14851
+ if (hasExpectedFileTopK && hasSymbolRequirement(query)) {
14852
+ return "wrong-symbol";
14853
+ }
14404
14854
  return "no-relevant-hit-top-k";
14405
14855
  }
14406
- if (query.expected.symbol) {
14407
- const hasSymbol = top.some(
14408
- (result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
14409
- );
14410
- if (!hasSymbol) return "wrong-symbol";
14411
- }
14412
14856
  const top1 = top[0];
14413
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
14857
+ if (top1 && !isExpectedFile(top1.filePath, relevant) && isDocsOrTestsPath(top1.filePath)) {
14414
14858
  return "docs-tests-outranking-source";
14415
14859
  }
14416
- if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
14860
+ if (top1 && !isExpectedFile(top1.filePath, relevant)) {
14417
14861
  return "wrong-file";
14418
14862
  }
14419
14863
  return void 0;
@@ -14422,9 +14866,12 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14422
14866
  resolvedRoute: "search",
14423
14867
  routedQuery: query.query
14424
14868
  }, context) {
14425
- const relevantPaths = getRelevantPaths(query);
14426
- const deduped = uniqueResultsByPath(results);
14427
- 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
+ );
14428
14875
  const perQuery = {
14429
14876
  id: query.id,
14430
14877
  query: query.query,
@@ -14432,13 +14879,19 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14432
14879
  retrievalMode: query.retrievalMode ?? "search",
14433
14880
  resolvedRoute: route.resolvedRoute,
14434
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,
14435
14888
  latencyMs,
14436
14889
  hitAt1: hitAt(1),
14437
14890
  hitAt3: hitAt(3),
14438
14891
  hitAt5: hitAt(5),
14439
14892
  hitAt10: hitAt(10),
14440
- reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
14441
- ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
14893
+ reciprocalRankAt10: reciprocalRankAtK(deduped, relevant, isSymbolIntendedQuery, 10),
14894
+ ndcgAt10: ndcgAtK(query, deduped, relevant, isSymbolIntendedQuery, 10),
14442
14895
  failureBucket: classifyFailureBucket(query, results, k),
14443
14896
  rawTop3DistinctRatio: distinctTopKRatio(results, 3),
14444
14897
  tokenBudget: context?.tokenBudget,
@@ -14456,6 +14909,11 @@ function buildPerQueryResult(query, results, latencyMs, k, route = {
14456
14909
  function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
14457
14910
  const count = perQuery.length;
14458
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;
14459
14917
  const sum = {
14460
14918
  hitAt1: 0,
14461
14919
  hitAt3: 0,
@@ -14477,27 +14935,52 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
14477
14935
  const contextResponseTokens = contextQueries.map((item) => item.responseTokens);
14478
14936
  const totalContextResponseTokens = contextResponseTokens.reduce((sum2, value) => sum2 + value, 0);
14479
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;
14480
14944
  for (const query of perQuery) {
14481
- if (query.hitAt1) sum.hitAt1 += 1;
14482
- if (query.hitAt3) sum.hitAt3 += 1;
14483
- if (query.hitAt5) sum.hitAt5 += 1;
14484
- if (query.hitAt10) sum.hitAt10 += 1;
14485
- sum.mrrAt10 += query.reciprocalRankAt10;
14486
- sum.ndcgAt10 += query.ndcgAt10;
14487
- 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);
14488
14954
  sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
14489
14955
  if (query.failureBucket) {
14490
14956
  failureBuckets[query.failureBucket] += 1;
14491
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
+ }
14492
14972
  }
14493
14973
  const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
14494
14974
  return {
14495
- hitAt1: safeDiv(sum.hitAt1),
14496
- hitAt3: safeDiv(sum.hitAt3),
14497
- hitAt5: safeDiv(sum.hitAt5),
14498
- hitAt10: safeDiv(sum.hitAt10),
14499
- mrrAt10: safeDiv(sum.mrrAt10),
14500
- 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,
14501
14984
  distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
14502
14985
  rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
14503
14986
  latencyMs: {
@@ -14704,6 +15187,9 @@ function isRecord3(value) {
14704
15187
  function isStringArray4(value) {
14705
15188
  return Array.isArray(value) && value.every((item) => typeof item === "string");
14706
15189
  }
15190
+ function isNonEmptyString(value) {
15191
+ return typeof value === "string" && value.trim().length > 0;
15192
+ }
14707
15193
  function asPositiveNumber(value, path29) {
14708
15194
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
14709
15195
  throw new Error(`${path29} must be a non-negative number`);
@@ -14718,11 +15204,109 @@ function parseQueryType(value, path29) {
14718
15204
  `${path29} must be one of: definition, implementation-intent, similarity, keyword-heavy, conceptual`
14719
15205
  );
14720
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
+ }
14721
15267
  function parseRetrievalMode(value, path29) {
14722
15268
  if (value === void 0 || value === "search") return "search";
14723
15269
  if (value === "context") return value;
14724
15270
  throw new Error(`${path29} must be one of: search, context`);
14725
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
+ }
14726
15310
  function parseExpected(input, path29) {
14727
15311
  if (!isRecord3(input)) {
14728
15312
  throw new Error(`${path29} must be an object`);
@@ -14731,10 +15315,18 @@ function parseExpected(input, path29) {
14731
15315
  const acceptableFilesRaw = input.acceptableFiles;
14732
15316
  const symbolRaw = input.symbol;
14733
15317
  const branchRaw = input.branch;
14734
- 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`);
14735
15323
  const acceptableFiles = isStringArray4(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
14736
- if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
14737
- 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
+ );
14738
15330
  }
14739
15331
  if (acceptableFilesRaw !== void 0 && !isStringArray4(acceptableFilesRaw)) {
14740
15332
  throw new Error(`${path29}.acceptableFiles must be an array of strings`);
@@ -14745,13 +15337,25 @@ function parseExpected(input, path29) {
14745
15337
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
14746
15338
  throw new Error(`${path29}.branch must be a string when provided`);
14747
15339
  }
15340
+ const expectedRoute = parseExpectedRoute(expectedRouteRaw, `${path29}.expectedRoute`);
15341
+ const recoveryExpectation = parseRecoveryExpectation(
15342
+ recoveryExpectationRaw,
15343
+ `${path29}.recoveryExpectation`
15344
+ );
14748
15345
  return {
14749
15346
  filePath,
14750
15347
  acceptableFiles,
14751
15348
  symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
14752
- 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 } : {}
14753
15354
  };
14754
15355
  }
15356
+ function parseQueryLanguage(value, path29) {
15357
+ return parseStringOrUndefined(value, path29);
15358
+ }
14755
15359
  function parseQuery(input, index) {
14756
15360
  const path29 = `queries[${index}]`;
14757
15361
  if (!isRecord3(input)) {
@@ -14762,6 +15366,10 @@ function parseQuery(input, index) {
14762
15366
  const queryType = input.queryType;
14763
15367
  const retrievalMode = input.retrievalMode;
14764
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;
14765
15373
  if (typeof id !== "string" || id.trim().length === 0) {
14766
15374
  throw new Error(`${path29}.id must be a non-empty string`);
14767
15375
  }
@@ -14773,6 +15381,10 @@ function parseQuery(input, index) {
14773
15381
  query,
14774
15382
  queryType: parseQueryType(queryType, `${path29}.queryType`),
14775
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`),
14776
15388
  expected: parseExpected(expected, `${path29}.expected`)
14777
15389
  };
14778
15390
  }
@@ -14784,9 +15396,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14784
15396
  const name = raw.name;
14785
15397
  const description = raw.description;
14786
15398
  const queriesRaw = raw.queries;
14787
- if (typeof version !== "string" || version.trim().length === 0) {
14788
- throw new Error(`${sourceLabel}.version must be a non-empty string`);
14789
- }
15399
+ const validatedVersion = parseSemanticVersion(version, `${sourceLabel}.version`);
14790
15400
  if (typeof name !== "string" || name.trim().length === 0) {
14791
15401
  throw new Error(`${sourceLabel}.name must be a non-empty string`);
14792
15402
  }
@@ -14808,7 +15418,7 @@ function parseGoldenDataset(raw, sourceLabel) {
14808
15418
  idSet.add(query.id);
14809
15419
  }
14810
15420
  return {
14811
- version,
15421
+ version: validatedVersion,
14812
15422
  name,
14813
15423
  description: typeof description === "string" ? description : void 0,
14814
15424
  queries
@@ -14868,16 +15478,8 @@ function parseBudget(raw, sourceLabel) {
14868
15478
  "p95LatencyMaxAbsoluteMs",
14869
15479
  sourceLabel
14870
15480
  ),
14871
- minHitAt5: parseThresholdValue(
14872
- thresholds.minHitAt5,
14873
- "minHitAt5",
14874
- sourceLabel
14875
- ),
14876
- minMrrAt10: parseThresholdValue(
14877
- thresholds.minMrrAt10,
14878
- "minMrrAt10",
14879
- sourceLabel
14880
- ),
15481
+ minHitAt5: parseThresholdValue(thresholds.minHitAt5, "minHitAt5", sourceLabel),
15482
+ minMrrAt10: parseThresholdValue(thresholds.minMrrAt10, "minMrrAt10", sourceLabel),
14881
15483
  minRawDistinctTop3Ratio: parseThresholdValue(
14882
15484
  thresholds.minRawDistinctTop3Ratio,
14883
15485
  "minRawDistinctTop3Ratio",
@@ -14927,6 +15529,26 @@ function loadBudget(budgetPath) {
14927
15529
  }
14928
15530
 
14929
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
+ }
14930
15552
  async function runEvaluation(options) {
14931
15553
  const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
14932
15554
  const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
@@ -14951,27 +15573,40 @@ async function runEvaluation(options) {
14951
15573
  const start = performance3.now();
14952
15574
  const contextResult = query.retrievalMode === "context" ? await resolveSearchContext({
14953
15575
  query: query.query,
15576
+ symbol: query.args?.symbol,
15577
+ fileType: query.args?.fileType,
15578
+ directory: query.args?.directory,
14954
15579
  limit: 10,
14955
15580
  tokenBudget: DEFAULT_CONTEXT_PACK_TOKEN_BUDGET
14956
15581
  }, {
14957
- lookup: (symbol, limit, _scope) => indexer.search(symbol, limit, {
15582
+ lookup: (symbol, limit, scope) => indexer.search(symbol, limit, {
14958
15583
  metadataOnly: true,
14959
15584
  filterByBranch: !!query.expected.branch,
14960
- definitionIntent: true
15585
+ definitionIntent: true,
15586
+ fileType: scope.fileType,
15587
+ directory: scope.directory
14961
15588
  }),
14962
- search: (searchQuery, limit, _scope) => indexer.search(searchQuery, limit, {
15589
+ search: (searchQuery, limit, scope) => indexer.search(searchQuery, limit, {
14963
15590
  metadataOnly: true,
14964
15591
  filterByBranch: !!query.expected.branch,
14965
- definitionIntent: false
15592
+ definitionIntent: false,
15593
+ fileType: scope.fileType,
15594
+ directory: scope.directory
14966
15595
  })
14967
15596
  }) : void 0;
14968
15597
  const result = contextResult?.details?.results ?? await indexer.search(query.query, 10, {
14969
15598
  metadataOnly: true,
14970
- filterByBranch: !!query.expected.branch
15599
+ filterByBranch: !!query.expected.branch,
15600
+ fileType: query.args?.fileType,
15601
+ directory: query.args?.directory
14971
15602
  });
14972
15603
  const elapsed = performance3.now() - start;
14973
15604
  const resolvedRoute = contextResult?.details?.route === "definition" ? "definition" : "search";
14974
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);
14975
15610
  const materialized = result.map((item) => ({
14976
15611
  filePath: item.filePath,
14977
15612
  startLine: item.startLine,
@@ -14988,7 +15623,9 @@ async function runEvaluation(options) {
14988
15623
  responseTokens: contextResult.details.tokenEstimate,
14989
15624
  candidateCount: contextResult.details.candidateCount ?? 0,
14990
15625
  deduplicatedCount: contextResult.details.deduplicatedCount ?? 0,
14991
- omittedCount: contextResult.details.omittedCount ?? 0
15626
+ omittedCount: contextResult.details.omittedCount ?? 0,
15627
+ recoveryUsed,
15628
+ recoveryRelaxed
14992
15629
  } : void 0));
14993
15630
  }
14994
15631
  const logger = indexer.getLogger();
@@ -15000,6 +15637,7 @@ async function runEvaluation(options) {
15000
15637
  datasetPath,
15001
15638
  datasetName: dataset.name,
15002
15639
  datasetVersion: dataset.version,
15640
+ datasetFingerprint: buildDatasetFingerprint(dataset),
15003
15641
  queryCount: dataset.queries.length,
15004
15642
  topK: 10,
15005
15643
  searchConfig: {
@@ -17916,7 +18554,10 @@ function createWatcherWithIndexer(getIndexer, projectRoot, config, host = "openc
17916
18554
  if (isGitRepo(projectRoot)) {
17917
18555
  gitWatcher = new GitHeadWatcher(projectRoot);
17918
18556
  gitWatcher.start(async (oldBranch, newBranch) => {
17919
- console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
18557
+ getIndexer().getLogger().branch("info", "Branch changed", {
18558
+ oldBranch,
18559
+ newBranch
18560
+ });
17920
18561
  requestReindex();
17921
18562
  });
17922
18563
  }