opencode-codebase-index 0.6.1 → 0.7.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
@@ -487,7 +487,7 @@ var require_ignore = __commonJS({
487
487
  // path matching.
488
488
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
489
489
  // @returns {TestResult} true if a file is ignored
490
- test(path10, checkUnignored, mode) {
490
+ test(path11, checkUnignored, mode) {
491
491
  let ignored = false;
492
492
  let unignored = false;
493
493
  let matchedRule;
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
496
496
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
497
497
  return;
498
498
  }
499
- const matched = rule[mode].test(path10);
499
+ const matched = rule[mode].test(path11);
500
500
  if (!matched) {
501
501
  return;
502
502
  }
@@ -517,17 +517,17 @@ var require_ignore = __commonJS({
517
517
  var throwError = (message, Ctor) => {
518
518
  throw new Ctor(message);
519
519
  };
520
- var checkPath = (path10, originalPath, doThrow) => {
521
- if (!isString(path10)) {
520
+ var checkPath = (path11, originalPath, doThrow) => {
521
+ if (!isString(path11)) {
522
522
  return doThrow(
523
523
  `path must be a string, but got \`${originalPath}\``,
524
524
  TypeError
525
525
  );
526
526
  }
527
- if (!path10) {
527
+ if (!path11) {
528
528
  return doThrow(`path must not be empty`, TypeError);
529
529
  }
530
- if (checkPath.isNotRelative(path10)) {
530
+ if (checkPath.isNotRelative(path11)) {
531
531
  const r = "`path.relative()`d";
532
532
  return doThrow(
533
533
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -536,7 +536,7 @@ var require_ignore = __commonJS({
536
536
  }
537
537
  return true;
538
538
  };
539
- var isNotRelative = (path10) => REGEX_TEST_INVALID_PATH.test(path10);
539
+ var isNotRelative = (path11) => REGEX_TEST_INVALID_PATH.test(path11);
540
540
  checkPath.isNotRelative = isNotRelative;
541
541
  checkPath.convert = (p) => p;
542
542
  var Ignore2 = class {
@@ -566,19 +566,19 @@ var require_ignore = __commonJS({
566
566
  }
567
567
  // @returns {TestResult}
568
568
  _test(originalPath, cache, checkUnignored, slices) {
569
- const path10 = originalPath && checkPath.convert(originalPath);
569
+ const path11 = originalPath && checkPath.convert(originalPath);
570
570
  checkPath(
571
- path10,
571
+ path11,
572
572
  originalPath,
573
573
  this._strictPathCheck ? throwError : RETURN_FALSE
574
574
  );
575
- return this._t(path10, cache, checkUnignored, slices);
575
+ return this._t(path11, cache, checkUnignored, slices);
576
576
  }
577
- checkIgnore(path10) {
578
- if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
579
- return this.test(path10);
577
+ checkIgnore(path11) {
578
+ if (!REGEX_TEST_TRAILING_SLASH.test(path11)) {
579
+ return this.test(path11);
580
580
  }
581
- const slices = path10.split(SLASH).filter(Boolean);
581
+ const slices = path11.split(SLASH).filter(Boolean);
582
582
  slices.pop();
583
583
  if (slices.length) {
584
584
  const parent = this._t(
@@ -591,18 +591,18 @@ var require_ignore = __commonJS({
591
591
  return parent;
592
592
  }
593
593
  }
594
- return this._rules.test(path10, false, MODE_CHECK_IGNORE);
594
+ return this._rules.test(path11, false, MODE_CHECK_IGNORE);
595
595
  }
596
- _t(path10, cache, checkUnignored, slices) {
597
- if (path10 in cache) {
598
- return cache[path10];
596
+ _t(path11, cache, checkUnignored, slices) {
597
+ if (path11 in cache) {
598
+ return cache[path11];
599
599
  }
600
600
  if (!slices) {
601
- slices = path10.split(SLASH).filter(Boolean);
601
+ slices = path11.split(SLASH).filter(Boolean);
602
602
  }
603
603
  slices.pop();
604
604
  if (!slices.length) {
605
- return cache[path10] = this._rules.test(path10, checkUnignored, MODE_IGNORE);
605
+ return cache[path11] = this._rules.test(path11, checkUnignored, MODE_IGNORE);
606
606
  }
607
607
  const parent = this._t(
608
608
  slices.join(SLASH) + SLASH,
@@ -610,29 +610,29 @@ var require_ignore = __commonJS({
610
610
  checkUnignored,
611
611
  slices
612
612
  );
613
- return cache[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
613
+ return cache[path11] = parent.ignored ? parent : this._rules.test(path11, checkUnignored, MODE_IGNORE);
614
614
  }
615
- ignores(path10) {
616
- return this._test(path10, this._ignoreCache, false).ignored;
615
+ ignores(path11) {
616
+ return this._test(path11, this._ignoreCache, false).ignored;
617
617
  }
618
618
  createFilter() {
619
- return (path10) => !this.ignores(path10);
619
+ return (path11) => !this.ignores(path11);
620
620
  }
621
621
  filter(paths) {
622
622
  return makeArray(paths).filter(this.createFilter());
623
623
  }
624
624
  // @returns {TestResult}
625
- test(path10) {
626
- return this._test(path10, this._testCache, true);
625
+ test(path11) {
626
+ return this._test(path11, this._testCache, true);
627
627
  }
628
628
  };
629
629
  var factory = (options) => new Ignore2(options);
630
- var isPathValid = (path10) => checkPath(path10 && checkPath.convert(path10), path10, RETURN_FALSE);
630
+ var isPathValid = (path11) => checkPath(path11 && checkPath.convert(path11), path11, RETURN_FALSE);
631
631
  var setupWindows = () => {
632
632
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
633
633
  checkPath.convert = makePosix;
634
634
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
635
- checkPath.isNotRelative = (path10) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path10) || isNotRelative(path10);
635
+ checkPath.isNotRelative = (path11) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path11) || isNotRelative(path11);
636
636
  };
637
637
  if (
638
638
  // Detect `process` so that it can run in browsers.
@@ -649,9 +649,7 @@ var require_ignore = __commonJS({
649
649
 
650
650
  // src/cli.ts
651
651
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
652
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
653
- import * as path9 from "path";
654
- import * as os4 from "os";
652
+ import * as path10 from "path";
655
653
 
656
654
  // src/config/constants.ts
657
655
  var DEFAULT_INCLUDE = [
@@ -664,13 +662,15 @@ var DEFAULT_INCLUDE = [
664
662
  "**/*.{sql,graphql,proto}",
665
663
  "**/*.{yaml,yml,toml}",
666
664
  "**/*.{md,mdx}",
667
- "**/*.{sh,bash,zsh}"
665
+ "**/*.{sh,bash,zsh}",
666
+ "**/*.{txt,html,htm}"
668
667
  ];
669
668
  var DEFAULT_EXCLUDE = [
670
669
  "**/node_modules/**",
671
670
  "**/.git/**",
672
671
  "**/dist/**",
673
672
  "**/build/**",
673
+ "**/*build*/**",
674
674
  "**/*.min.js",
675
675
  "**/*.bundle.js",
676
676
  "**/vendor/**",
@@ -679,7 +679,9 @@ var DEFAULT_EXCLUDE = [
679
679
  "**/coverage/**",
680
680
  "**/.next/**",
681
681
  "**/.nuxt/**",
682
- "**/.opencode/**"
682
+ "**/.opencode/**",
683
+ "**/.*",
684
+ "**/.*/**"
683
685
  ];
684
686
  var EMBEDDING_MODELS = {
685
687
  "google": {
@@ -788,7 +790,10 @@ function getDefaultIndexingConfig() {
788
790
  autoGc: true,
789
791
  gcIntervalDays: 7,
790
792
  gcOrphanThreshold: 100,
791
- requireProjectMarker: true
793
+ requireProjectMarker: true,
794
+ maxDepth: 5,
795
+ maxFilesPerDirectory: 100,
796
+ fallbackToTextOnMaxChunks: true
792
797
  };
793
798
  }
794
799
  function getDefaultSearchConfig() {
@@ -800,12 +805,26 @@ function getDefaultSearchConfig() {
800
805
  fusionStrategy: "rrf",
801
806
  rrfK: 60,
802
807
  rerankTopN: 20,
803
- contextLines: 0
808
+ contextLines: 0,
809
+ routingHints: true
804
810
  };
805
811
  }
806
812
  function isValidFusionStrategy(value) {
807
813
  return value === "weighted" || value === "rrf";
808
814
  }
815
+ function isValidRerankerProvider(value) {
816
+ return value === "cohere" || value === "jina" || value === "custom";
817
+ }
818
+ function getDefaultRerankerBaseUrl(provider) {
819
+ switch (provider) {
820
+ case "cohere":
821
+ return "https://api.cohere.ai/v1";
822
+ case "jina":
823
+ return "https://api.jina.ai/v1";
824
+ case "custom":
825
+ return "";
826
+ }
827
+ }
809
828
  function getDefaultDebugConfig() {
810
829
  return {
811
830
  enabled: false,
@@ -868,7 +887,10 @@ function parseConfig(raw) {
868
887
  autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
869
888
  gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
870
889
  gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold,
871
- requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker
890
+ requireProjectMarker: typeof rawIndexing.requireProjectMarker === "boolean" ? rawIndexing.requireProjectMarker : defaultIndexing.requireProjectMarker,
891
+ maxDepth: typeof rawIndexing.maxDepth === "number" ? rawIndexing.maxDepth < -1 ? -1 : rawIndexing.maxDepth : defaultIndexing.maxDepth,
892
+ maxFilesPerDirectory: typeof rawIndexing.maxFilesPerDirectory === "number" ? Math.max(1, rawIndexing.maxFilesPerDirectory) : defaultIndexing.maxFilesPerDirectory,
893
+ fallbackToTextOnMaxChunks: typeof rawIndexing.fallbackToTextOnMaxChunks === "boolean" ? rawIndexing.fallbackToTextOnMaxChunks : defaultIndexing.fallbackToTextOnMaxChunks
872
894
  };
873
895
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
874
896
  const search = {
@@ -879,7 +901,8 @@ function parseConfig(raw) {
879
901
  fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
880
902
  rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
881
903
  rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
882
- contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
904
+ contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines,
905
+ routingHints: typeof rawSearch.routingHints === "boolean" ? rawSearch.routingHints : defaultSearch.routingHints
883
906
  };
884
907
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
885
908
  const debug = {
@@ -892,9 +915,14 @@ function parseConfig(raw) {
892
915
  logBranch: typeof rawDebug.logBranch === "boolean" ? rawDebug.logBranch : defaultDebug.logBranch,
893
916
  metrics: typeof rawDebug.metrics === "boolean" ? rawDebug.metrics : defaultDebug.metrics
894
917
  };
918
+ const rawKnowledgeBases = input.knowledgeBases;
919
+ const knowledgeBases = isStringArray(rawKnowledgeBases) ? rawKnowledgeBases.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim()) : [];
920
+ const rawAdditionalInclude = input.additionalInclude;
921
+ const additionalInclude = isStringArray(rawAdditionalInclude) ? rawAdditionalInclude.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim()) : [];
895
922
  let embeddingProvider;
896
923
  let embeddingModel = void 0;
897
924
  let customProvider = void 0;
925
+ let reranker = void 0;
898
926
  if (embeddingProviderValue === "custom") {
899
927
  embeddingProvider = "custom";
900
928
  const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
@@ -937,6 +965,33 @@ function parseConfig(raw) {
937
965
  } else {
938
966
  embeddingProvider = "auto";
939
967
  }
968
+ const rawReranker = input.reranker && typeof input.reranker === "object" ? input.reranker : {};
969
+ const rerankerEnabled = typeof rawReranker.enabled === "boolean" ? rawReranker.enabled : false;
970
+ if (rerankerEnabled) {
971
+ const provider = isValidRerankerProvider(rawReranker.provider) ? rawReranker.provider : "custom";
972
+ const model = getResolvedString(rawReranker.model, "$root.reranker.model");
973
+ if (!model || model.trim().length === 0) {
974
+ throw new Error("reranker is enabled but reranker.model is missing or invalid.");
975
+ }
976
+ const configuredBaseUrl = getResolvedString(rawReranker.baseUrl, "$root.reranker.baseUrl");
977
+ const baseUrl = configuredBaseUrl?.trim() || getDefaultRerankerBaseUrl(provider);
978
+ if (baseUrl.length === 0) {
979
+ throw new Error("reranker is enabled but reranker.baseUrl is missing or invalid for provider 'custom'.");
980
+ }
981
+ const apiKey = getResolvedString(rawReranker.apiKey, "$root.reranker.apiKey");
982
+ if ((provider === "cohere" || provider === "jina") && (!apiKey || apiKey.trim().length === 0)) {
983
+ throw new Error(`reranker provider '${provider}' requires reranker.apiKey when enabled.`);
984
+ }
985
+ reranker = {
986
+ enabled: true,
987
+ provider,
988
+ model: model.trim(),
989
+ baseUrl: baseUrl.replace(/\/+$/, ""),
990
+ apiKey: apiKey?.trim() || void 0,
991
+ topN: typeof rawReranker.topN === "number" ? Math.min(50, Math.max(1, Math.floor(rawReranker.topN))) : 15,
992
+ timeoutMs: typeof rawReranker.timeoutMs === "number" ? Math.max(1e3, Math.floor(rawReranker.timeoutMs)) : 1e4
993
+ };
994
+ }
940
995
  return {
941
996
  embeddingProvider,
942
997
  embeddingModel,
@@ -944,9 +999,12 @@ function parseConfig(raw) {
944
999
  scope: isValidScope(scopeValue) ? scopeValue : "project",
945
1000
  include: includeValue ?? DEFAULT_INCLUDE,
946
1001
  exclude: excludeValue ?? DEFAULT_EXCLUDE,
1002
+ additionalInclude,
947
1003
  indexing,
948
1004
  search,
949
- debug
1005
+ debug,
1006
+ reranker,
1007
+ knowledgeBases
950
1008
  };
951
1009
  }
952
1010
  function getDefaultModelForProvider(provider) {
@@ -980,6 +1038,8 @@ function compareSummaries(current, baseline, againstPath) {
980
1038
  hitAt10: metricDelta(current.metrics.hitAt10, baseline.metrics.hitAt10),
981
1039
  mrrAt10: metricDelta(current.metrics.mrrAt10, baseline.metrics.mrrAt10),
982
1040
  ndcgAt10: metricDelta(current.metrics.ndcgAt10, baseline.metrics.ndcgAt10),
1041
+ distinctTop3Ratio: metricDelta(current.metrics.distinctTop3Ratio, baseline.metrics.distinctTop3Ratio),
1042
+ rawDistinctTop3Ratio: metricDelta(current.metrics.rawDistinctTop3Ratio, baseline.metrics.rawDistinctTop3Ratio),
983
1043
  latencyP50Ms: metricDelta(current.metrics.latencyMs.p50, baseline.metrics.latencyMs.p50),
984
1044
  latencyP95Ms: metricDelta(current.metrics.latencyMs.p95, baseline.metrics.latencyMs.p95),
985
1045
  latencyP99Ms: metricDelta(current.metrics.latencyMs.p99, baseline.metrics.latencyMs.p99),
@@ -998,6 +1058,35 @@ function compareSummaries(current, baseline, againstPath) {
998
1058
  // src/eval/reports.ts
999
1059
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
1000
1060
  import * as path from "path";
1061
+ function assertFiniteNumber(value, path11) {
1062
+ if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1063
+ throw new Error(`${path11} must be a finite number`);
1064
+ }
1065
+ return value;
1066
+ }
1067
+ function validateSummary(summary, summaryPath, options) {
1068
+ assertFiniteNumber(summary.metrics.hitAt1, `${summaryPath}.metrics.hitAt1`);
1069
+ assertFiniteNumber(summary.metrics.hitAt3, `${summaryPath}.metrics.hitAt3`);
1070
+ assertFiniteNumber(summary.metrics.hitAt5, `${summaryPath}.metrics.hitAt5`);
1071
+ assertFiniteNumber(summary.metrics.hitAt10, `${summaryPath}.metrics.hitAt10`);
1072
+ assertFiniteNumber(summary.metrics.mrrAt10, `${summaryPath}.metrics.mrrAt10`);
1073
+ assertFiniteNumber(summary.metrics.ndcgAt10, `${summaryPath}.metrics.ndcgAt10`);
1074
+ const metrics = summary.metrics;
1075
+ if (metrics.distinctTop3Ratio === void 0 && options?.allowLegacyDiversityMetrics) {
1076
+ metrics.distinctTop3Ratio = 0;
1077
+ }
1078
+ if (metrics.rawDistinctTop3Ratio === void 0 && options?.allowLegacyDiversityMetrics) {
1079
+ metrics.rawDistinctTop3Ratio = 0;
1080
+ }
1081
+ assertFiniteNumber(metrics.distinctTop3Ratio, `${summaryPath}.metrics.distinctTop3Ratio`);
1082
+ assertFiniteNumber(metrics.rawDistinctTop3Ratio, `${summaryPath}.metrics.rawDistinctTop3Ratio`);
1083
+ assertFiniteNumber(summary.metrics.latencyMs.p50, `${summaryPath}.metrics.latencyMs.p50`);
1084
+ assertFiniteNumber(summary.metrics.latencyMs.p95, `${summaryPath}.metrics.latencyMs.p95`);
1085
+ assertFiniteNumber(summary.metrics.latencyMs.p99, `${summaryPath}.metrics.latencyMs.p99`);
1086
+ assertFiniteNumber(summary.metrics.embedding.callCount, `${summaryPath}.metrics.embedding.callCount`);
1087
+ assertFiniteNumber(summary.metrics.embedding.estimatedCostUsd, `${summaryPath}.metrics.embedding.estimatedCostUsd`);
1088
+ return summary;
1089
+ }
1001
1090
  function formatPct(value) {
1002
1091
  return `${(value * 100).toFixed(2)}%`;
1003
1092
  }
@@ -1011,9 +1100,9 @@ function signed(value, digits = 4) {
1011
1100
  const formatted = value.toFixed(digits);
1012
1101
  return value > 0 ? `+${formatted}` : formatted;
1013
1102
  }
1014
- function loadSummary(summaryPath) {
1103
+ function loadSummary(summaryPath, options) {
1015
1104
  const raw = readFileSync(summaryPath, "utf-8");
1016
- return JSON.parse(raw);
1105
+ return validateSummary(JSON.parse(raw), summaryPath, options);
1017
1106
  }
1018
1107
  function createRunDirectory(outputRoot, timestampOverride) {
1019
1108
  const timestamp = (timestampOverride ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
@@ -1048,6 +1137,8 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1048
1137
  lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
1049
1138
  lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
1050
1139
  lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
1140
+ lines.push(`| Distinct Top@3 | ${formatPct(summary.metrics.distinctTop3Ratio)} |`);
1141
+ lines.push(`| Raw Distinct Top@3 | ${formatPct(summary.metrics.rawDistinctTop3Ratio)} |`);
1051
1142
  lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
1052
1143
  lines.push(`| Latency p95 | ${formatMs(summary.metrics.latencyMs.p95)} |`);
1053
1144
  lines.push(`| Latency p99 | ${formatMs(summary.metrics.latencyMs.p99)} |`);
@@ -1088,6 +1179,12 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1088
1179
  lines.push(
1089
1180
  `| nDCG@10 | ${comparison.deltas.ndcgAt10.baseline.toFixed(4)} | ${comparison.deltas.ndcgAt10.current.toFixed(4)} | ${signed(comparison.deltas.ndcgAt10.absolute)} |`
1090
1181
  );
1182
+ lines.push(
1183
+ `| Distinct Top@3 | ${formatPct(comparison.deltas.distinctTop3Ratio.baseline)} | ${formatPct(comparison.deltas.distinctTop3Ratio.current)} | ${signed(comparison.deltas.distinctTop3Ratio.absolute)} |`
1184
+ );
1185
+ lines.push(
1186
+ `| Raw Distinct Top@3 | ${formatPct(comparison.deltas.rawDistinctTop3Ratio.baseline)} | ${formatPct(comparison.deltas.rawDistinctTop3Ratio.current)} | ${signed(comparison.deltas.rawDistinctTop3Ratio.absolute)} |`
1187
+ );
1091
1188
  lines.push(
1092
1189
  `| p95 latency (ms) | ${comparison.deltas.latencyP95Ms.baseline.toFixed(3)} | ${comparison.deltas.latencyP95Ms.current.toFixed(3)} | ${signed(comparison.deltas.latencyP95Ms.absolute, 3)} |`
1093
1190
  );
@@ -1171,7 +1268,7 @@ function pTimeout(promise, options) {
1171
1268
  } = options;
1172
1269
  let timer;
1173
1270
  let abortHandler;
1174
- const wrappedPromise = new Promise((resolve5, reject) => {
1271
+ const wrappedPromise = new Promise((resolve6, reject) => {
1175
1272
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
1176
1273
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
1177
1274
  }
@@ -1185,7 +1282,7 @@ function pTimeout(promise, options) {
1185
1282
  };
1186
1283
  signal.addEventListener("abort", abortHandler, { once: true });
1187
1284
  }
1188
- promise.then(resolve5, reject);
1285
+ promise.then(resolve6, reject);
1189
1286
  if (milliseconds === Number.POSITIVE_INFINITY) {
1190
1287
  return;
1191
1288
  }
@@ -1193,7 +1290,7 @@ function pTimeout(promise, options) {
1193
1290
  timer = customTimers.setTimeout.call(void 0, () => {
1194
1291
  if (fallback) {
1195
1292
  try {
1196
- resolve5(fallback());
1293
+ resolve6(fallback());
1197
1294
  } catch (error) {
1198
1295
  reject(error);
1199
1296
  }
@@ -1203,7 +1300,7 @@ function pTimeout(promise, options) {
1203
1300
  promise.cancel();
1204
1301
  }
1205
1302
  if (message === false) {
1206
- resolve5();
1303
+ resolve6();
1207
1304
  } else if (message instanceof Error) {
1208
1305
  reject(message);
1209
1306
  } else {
@@ -1267,6 +1364,17 @@ var PriorityQueue = class {
1267
1364
  const [item] = this.#queue.splice(index, 1);
1268
1365
  this.enqueue(item.run, { priority, id });
1269
1366
  }
1367
+ remove(idOrRun) {
1368
+ const index = this.#queue.findIndex((element) => {
1369
+ if (typeof idOrRun === "string") {
1370
+ return element.id === idOrRun;
1371
+ }
1372
+ return element.run === idOrRun;
1373
+ });
1374
+ if (index !== -1) {
1375
+ this.#queue.splice(index, 1);
1376
+ }
1377
+ }
1270
1378
  dequeue() {
1271
1379
  const item = this.#queue.shift();
1272
1380
  return item?.run;
@@ -1306,6 +1414,7 @@ var PQueue = class extends import_index.default {
1306
1414
  #idAssigner = 1n;
1307
1415
  // Track currently running tasks for debugging
1308
1416
  #runningTasks = /* @__PURE__ */ new Map();
1417
+ #queueAbortListenerCleanupFunctions = /* @__PURE__ */ new Set();
1309
1418
  /**
1310
1419
  Get or set the default timeout for all tasks. Can be changed at runtime.
1311
1420
 
@@ -1593,9 +1702,11 @@ var PQueue = class extends import_index.default {
1593
1702
  // Assign unique ID if not provided
1594
1703
  id: options.id ?? (this.#idAssigner++).toString()
1595
1704
  };
1596
- return new Promise((resolve5, reject) => {
1705
+ return new Promise((resolve6, reject) => {
1597
1706
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1598
- this.#queue.enqueue(async () => {
1707
+ let cleanupQueueAbortHandler = () => void 0;
1708
+ const run = async () => {
1709
+ cleanupQueueAbortHandler();
1599
1710
  this.#pending++;
1600
1711
  this.#runningTasks.set(taskSymbol, {
1601
1712
  id: options.id,
@@ -1631,7 +1742,7 @@ var PQueue = class extends import_index.default {
1631
1742
  })]);
1632
1743
  }
1633
1744
  const result = await operation;
1634
- resolve5(result);
1745
+ resolve6(result);
1635
1746
  this.emit("completed", result);
1636
1747
  } catch (error) {
1637
1748
  reject(error);
@@ -1645,7 +1756,35 @@ var PQueue = class extends import_index.default {
1645
1756
  this.#next();
1646
1757
  });
1647
1758
  }
1648
- }, options);
1759
+ };
1760
+ this.#queue.enqueue(run, options);
1761
+ const removeQueuedTask = () => {
1762
+ if (this.#queue instanceof PriorityQueue) {
1763
+ this.#queue.remove(run);
1764
+ return;
1765
+ }
1766
+ this.#queue.remove?.(options.id);
1767
+ };
1768
+ if (options.signal) {
1769
+ const { signal } = options;
1770
+ const queueAbortHandler = () => {
1771
+ cleanupQueueAbortHandler();
1772
+ removeQueuedTask();
1773
+ reject(signal.reason);
1774
+ this.#tryToStartAnother();
1775
+ this.emit("next");
1776
+ };
1777
+ cleanupQueueAbortHandler = () => {
1778
+ signal.removeEventListener("abort", queueAbortHandler);
1779
+ this.#queueAbortListenerCleanupFunctions.delete(cleanupQueueAbortHandler);
1780
+ };
1781
+ if (signal.aborted) {
1782
+ queueAbortHandler();
1783
+ return;
1784
+ }
1785
+ signal.addEventListener("abort", queueAbortHandler, { once: true });
1786
+ this.#queueAbortListenerCleanupFunctions.add(cleanupQueueAbortHandler);
1787
+ }
1649
1788
  this.emit("add");
1650
1789
  this.#tryToStartAnother();
1651
1790
  });
@@ -1674,6 +1813,9 @@ var PQueue = class extends import_index.default {
1674
1813
  Clear the queue.
1675
1814
  */
1676
1815
  clear() {
1816
+ for (const cleanupQueueAbortHandler of this.#queueAbortListenerCleanupFunctions) {
1817
+ cleanupQueueAbortHandler();
1818
+ }
1677
1819
  this.#queue = new this.#queueClass();
1678
1820
  this.#clearIntervalTimer();
1679
1821
  this.#updateRateLimitState();
@@ -1788,13 +1930,13 @@ var PQueue = class extends import_index.default {
1788
1930
  });
1789
1931
  }
1790
1932
  async #onEvent(event, filter) {
1791
- return new Promise((resolve5) => {
1933
+ return new Promise((resolve6) => {
1792
1934
  const listener = () => {
1793
1935
  if (filter && !filter()) {
1794
1936
  return;
1795
1937
  }
1796
1938
  this.off(event, listener);
1797
- resolve5();
1939
+ resolve6();
1798
1940
  };
1799
1941
  this.on(event, listener);
1800
1942
  });
@@ -1953,8 +2095,6 @@ var isError = (value) => objectToString.call(value) === "[object Error]";
1953
2095
  var errorMessages = /* @__PURE__ */ new Set([
1954
2096
  "network error",
1955
2097
  // Chrome
1956
- "Failed to fetch",
1957
- // Chrome
1958
2098
  "NetworkError when attempting to fetch resource.",
1959
2099
  // Firefox
1960
2100
  "The Internet connection appears to be offline.",
@@ -1982,6 +2122,9 @@ function isNetworkError(error) {
1982
2122
  if (message.startsWith("error sending request for url")) {
1983
2123
  return true;
1984
2124
  }
2125
+ if (message === "Failed to fetch" || message.startsWith("Failed to fetch (") && message.endsWith(")")) {
2126
+ return true;
2127
+ }
1985
2128
  return errorMessages.has(message);
1986
2129
  }
1987
2130
 
@@ -2079,7 +2222,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2079
2222
  const finalDelay = Math.min(delayTime, remainingTime);
2080
2223
  options.signal?.throwIfAborted();
2081
2224
  if (finalDelay > 0) {
2082
- await new Promise((resolve5, reject) => {
2225
+ await new Promise((resolve6, reject) => {
2083
2226
  const onAbort = () => {
2084
2227
  clearTimeout(timeoutToken);
2085
2228
  options.signal?.removeEventListener("abort", onAbort);
@@ -2087,7 +2230,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2087
2230
  };
2088
2231
  const timeoutToken = setTimeout(() => {
2089
2232
  options.signal?.removeEventListener("abort", onAbort);
2090
- resolve5();
2233
+ resolve6();
2091
2234
  }, finalDelay);
2092
2235
  if (options.unref) {
2093
2236
  timeoutToken.unref?.();
@@ -2690,6 +2833,82 @@ var CustomEmbeddingProvider = class {
2690
2833
  }
2691
2834
  };
2692
2835
 
2836
+ // src/rerank/index.ts
2837
+ function createReranker(config) {
2838
+ if (!config.enabled) {
2839
+ return new NoOpReranker();
2840
+ }
2841
+ return new SiliconFlowReranker(config);
2842
+ }
2843
+ var NoOpReranker = class {
2844
+ isAvailable() {
2845
+ return false;
2846
+ }
2847
+ async rerank(_query, documents, _topN) {
2848
+ return {
2849
+ results: documents.map((_, index) => ({ index, relevanceScore: 0 }))
2850
+ };
2851
+ }
2852
+ };
2853
+ var SiliconFlowReranker = class {
2854
+ config;
2855
+ constructor(config) {
2856
+ this.config = config;
2857
+ }
2858
+ isAvailable() {
2859
+ return this.config.enabled && !!this.config.baseUrl && !!this.config.model;
2860
+ }
2861
+ async rerank(query, documents, topN) {
2862
+ if (documents.length === 0) {
2863
+ return { results: [] };
2864
+ }
2865
+ const headers = {
2866
+ "Content-Type": "application/json"
2867
+ };
2868
+ if (this.config.apiKey) {
2869
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
2870
+ }
2871
+ const baseUrl = this.config.baseUrl ?? "https://api.siliconflow.cn/v1";
2872
+ const timeoutMs = this.config.timeoutMs ?? 3e4;
2873
+ const controller = new AbortController();
2874
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2875
+ try {
2876
+ const response = await fetch(`${baseUrl}/rerank`, {
2877
+ method: "POST",
2878
+ headers,
2879
+ body: JSON.stringify({
2880
+ model: this.config.model,
2881
+ query,
2882
+ documents,
2883
+ top_n: topN ?? this.config.topN ?? 20,
2884
+ return_documents: false
2885
+ }),
2886
+ signal: controller.signal
2887
+ });
2888
+ clearTimeout(timeout);
2889
+ if (!response.ok) {
2890
+ const errorText = await response.text();
2891
+ throw new Error(`Rerank API error: ${response.status} - ${errorText}`);
2892
+ }
2893
+ const data = await response.json();
2894
+ return {
2895
+ results: data.results.map((r) => ({
2896
+ index: r.index,
2897
+ relevanceScore: r.relevance_score,
2898
+ document: r.document?.text
2899
+ })),
2900
+ tokensUsed: data.meta?.tokens?.input_tokens
2901
+ };
2902
+ } catch (error) {
2903
+ clearTimeout(timeout);
2904
+ if (error instanceof Error && error.name === "AbortError") {
2905
+ throw new Error(`Rerank API request timed out after ${timeoutMs}ms`);
2906
+ }
2907
+ throw error;
2908
+ }
2909
+ }
2910
+ };
2911
+
2693
2912
  // src/utils/files.ts
2694
2913
  var import_ignore = __toESM(require_ignore(), 1);
2695
2914
  import { existsSync as existsSync2, readFileSync as readFileSync3, promises as fsPromises } from "fs";
@@ -2707,7 +2926,11 @@ function createIgnoreFilter(projectRoot) {
2707
2926
  "__pycache__",
2708
2927
  "target",
2709
2928
  "vendor",
2710
- ".opencode"
2929
+ ".opencode",
2930
+ ".*",
2931
+ "**/.*",
2932
+ "**/.*/**",
2933
+ "**/*build*/**"
2711
2934
  ];
2712
2935
  ig.add(defaultIgnores);
2713
2936
  const gitignorePath = path3.join(projectRoot, ".gitignore");
@@ -2718,18 +2941,37 @@ function createIgnoreFilter(projectRoot) {
2718
2941
  return ig;
2719
2942
  }
2720
2943
  function matchGlob(filePath, pattern) {
2721
- let regexPattern = pattern.replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTAR>>>/g, ".*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, p1) => `(${p1.split(",").join("|")})`);
2944
+ if (pattern.startsWith("**/")) {
2945
+ const withoutPrefix = pattern.slice(3);
2946
+ if (withoutPrefix && matchGlob(filePath, withoutPrefix)) {
2947
+ return true;
2948
+ }
2949
+ }
2950
+ const escapedPattern = pattern.replace(/[.+^$()|[\]\\]/g, "\\$&");
2951
+ let regexPattern = escapedPattern.replace(/\*\*/g, "<<<DOUBLESTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<DOUBLESTAR>>>/g, ".*").replace(/\?/g, ".").replace(/\{([^}]+)\}/g, (_, p1) => `(${p1.split(",").join("|")})`);
2722
2952
  if (regexPattern.startsWith(".*/")) {
2723
2953
  regexPattern = `(.*\\/)?${regexPattern.slice(3)}`;
2724
2954
  }
2725
2955
  const regex = new RegExp(`^${regexPattern}$`);
2726
2956
  return regex.test(filePath);
2727
2957
  }
2728
- async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
2958
+ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped, options, currentDepth = 0) {
2729
2959
  const entries = await fsPromises.readdir(dir, { withFileTypes: true });
2960
+ const filesInDir = [];
2961
+ const subdirs = [];
2730
2962
  for (const entry of entries) {
2731
2963
  const fullPath = path3.join(dir, entry.name);
2732
2964
  const relativePath = path3.relative(projectRoot, fullPath);
2965
+ if (entry.name.startsWith(".") && entry.name !== "." && entry.name !== "..") {
2966
+ if (entry.isDirectory()) {
2967
+ skipped.push({ path: relativePath, reason: "excluded" });
2968
+ }
2969
+ continue;
2970
+ }
2971
+ if (entry.isDirectory() && entry.name.toLowerCase().includes("build")) {
2972
+ skipped.push({ path: relativePath, reason: "excluded" });
2973
+ continue;
2974
+ }
2733
2975
  if (ignoreFilter.ignores(relativePath)) {
2734
2976
  if (entry.isFile()) {
2735
2977
  skipped.push({ path: relativePath, reason: "gitignore" });
@@ -2737,15 +2979,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2737
2979
  continue;
2738
2980
  }
2739
2981
  if (entry.isDirectory()) {
2740
- yield* walkDirectory(
2741
- fullPath,
2742
- projectRoot,
2743
- includePatterns,
2744
- excludePatterns,
2745
- ignoreFilter,
2746
- maxFileSize,
2747
- skipped
2748
- );
2982
+ subdirs.push({ fullPath, relativePath });
2749
2983
  } else if (entry.isFile()) {
2750
2984
  const stat = await fsPromises.stat(fullPath);
2751
2985
  if (stat.size > maxFileSize) {
@@ -2766,12 +3000,37 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2766
3000
  }
2767
3001
  }
2768
3002
  if (matched) {
2769
- yield { path: fullPath, size: stat.size };
3003
+ filesInDir.push({ path: fullPath, size: stat.size });
2770
3004
  }
2771
3005
  }
2772
3006
  }
3007
+ filesInDir.sort((a, b) => a.size - b.size);
3008
+ const limitedFiles = filesInDir.slice(0, options.maxFilesPerDirectory);
3009
+ for (const f of limitedFiles) {
3010
+ yield f;
3011
+ }
3012
+ for (let i = options.maxFilesPerDirectory; i < filesInDir.length; i++) {
3013
+ skipped.push({ path: path3.relative(projectRoot, filesInDir[i].path), reason: "excluded" });
3014
+ }
3015
+ const canRecurse = options.maxDepth === -1 || currentDepth < options.maxDepth;
3016
+ if (canRecurse) {
3017
+ for (const sub of subdirs) {
3018
+ yield* walkDirectory(
3019
+ sub.fullPath,
3020
+ projectRoot,
3021
+ includePatterns,
3022
+ excludePatterns,
3023
+ ignoreFilter,
3024
+ maxFileSize,
3025
+ skipped,
3026
+ options,
3027
+ currentDepth + 1
3028
+ );
3029
+ }
3030
+ }
2773
3031
  }
2774
- async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFileSize) {
3032
+ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFileSize, additionalRoots, walkOptions) {
3033
+ const opts = walkOptions ?? { maxDepth: 5, maxFilesPerDirectory: 100 };
2775
3034
  const ignoreFilter = createIgnoreFilter(projectRoot);
2776
3035
  const files = [];
2777
3036
  const skipped = [];
@@ -2782,10 +3041,46 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
2782
3041
  excludePatterns,
2783
3042
  ignoreFilter,
2784
3043
  maxFileSize,
2785
- skipped
3044
+ skipped,
3045
+ opts,
3046
+ 0
2786
3047
  )) {
2787
3048
  files.push(file);
2788
3049
  }
3050
+ if (additionalRoots && additionalRoots.length > 0) {
3051
+ const normalizedRoots = /* @__PURE__ */ new Set();
3052
+ for (const kbRoot of additionalRoots) {
3053
+ const resolved = path3.normalize(
3054
+ path3.isAbsolute(kbRoot) ? kbRoot : path3.resolve(projectRoot, kbRoot)
3055
+ );
3056
+ normalizedRoots.add(resolved);
3057
+ }
3058
+ for (const resolvedKbRoot of normalizedRoots) {
3059
+ try {
3060
+ const stat = await fsPromises.stat(resolvedKbRoot);
3061
+ if (!stat.isDirectory()) {
3062
+ skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3063
+ continue;
3064
+ }
3065
+ const kbIgnoreFilter = createIgnoreFilter(resolvedKbRoot);
3066
+ for await (const file of walkDirectory(
3067
+ resolvedKbRoot,
3068
+ resolvedKbRoot,
3069
+ includePatterns,
3070
+ excludePatterns,
3071
+ kbIgnoreFilter,
3072
+ maxFileSize,
3073
+ skipped,
3074
+ opts,
3075
+ 0
3076
+ )) {
3077
+ files.push(file);
3078
+ }
3079
+ } catch {
3080
+ skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3081
+ }
3082
+ }
3083
+ }
2789
3084
  return { files, skipped };
2790
3085
  }
2791
3086
 
@@ -3074,7 +3369,6 @@ var Logger = class {
3074
3369
  formatMetrics() {
3075
3370
  const m = this.metrics;
3076
3371
  const lines = [];
3077
- lines.push("=== Metrics ===");
3078
3372
  if (m.indexingStartTime && m.indexingEndTime) {
3079
3373
  const duration = m.indexingEndTime - m.indexingStartTime;
3080
3374
  lines.push(`Indexing duration: ${(duration / 1e3).toFixed(2)}s`);
@@ -3186,7 +3480,52 @@ function getNativeBinding() {
3186
3480
  const require2 = module.createRequire(requireTarget);
3187
3481
  return require2(nativePath);
3188
3482
  }
3189
- var native = getNativeBinding();
3483
+ function createMockNativeBinding() {
3484
+ const error = new Error("Native module not available. Please rebuild with 'npm run build:native'.");
3485
+ return {
3486
+ parseFile: () => {
3487
+ throw error;
3488
+ },
3489
+ parseFiles: () => {
3490
+ throw error;
3491
+ },
3492
+ hashContent: () => {
3493
+ throw error;
3494
+ },
3495
+ hashFile: () => {
3496
+ throw error;
3497
+ },
3498
+ extractCalls: () => {
3499
+ throw error;
3500
+ },
3501
+ VectorStore: class {
3502
+ constructor() {
3503
+ throw error;
3504
+ }
3505
+ },
3506
+ InvertedIndex: class {
3507
+ constructor() {
3508
+ throw error;
3509
+ }
3510
+ },
3511
+ Database: class {
3512
+ constructor() {
3513
+ throw error;
3514
+ }
3515
+ }
3516
+ };
3517
+ }
3518
+ var native;
3519
+ try {
3520
+ native = getNativeBinding();
3521
+ } catch (e) {
3522
+ console.error("[codebase-index] Failed to load native module:", e);
3523
+ native = createMockNativeBinding();
3524
+ }
3525
+ function parseFileAsText(filePath, content) {
3526
+ const result = native.parseFileAsText(filePath, content);
3527
+ return result.map(mapChunk);
3528
+ }
3190
3529
  function parseFiles(files) {
3191
3530
  const result = native.parseFiles(files);
3192
3531
  return result.map((f) => ({
@@ -4012,6 +4351,32 @@ function isLikelyImplementationPath(filePath) {
4012
4351
  }
4013
4352
  return true;
4014
4353
  }
4354
+ function isDocumentationPath(filePath) {
4355
+ const lowered = filePath.toLowerCase();
4356
+ const ext = lowered.split(".").pop() ?? "";
4357
+ return lowered.includes("readme") || ["md", "mdx", "rst", "adoc", "txt"].includes(ext);
4358
+ }
4359
+ function classifyExternalRerankBand(candidate, preferSourcePaths, docIntent) {
4360
+ const isDocOrTest = isTestOrDocPath(candidate.metadata.filePath);
4361
+ const isDocumentation = isDocumentationPath(candidate.metadata.filePath);
4362
+ const isImplementation = isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType);
4363
+ if (preferSourcePaths) {
4364
+ if (isImplementation) return "implementation";
4365
+ if (isDocumentation) return "documentation";
4366
+ if (isDocOrTest) return "test";
4367
+ return "other";
4368
+ }
4369
+ if (docIntent) {
4370
+ if (isDocumentation) return "documentation";
4371
+ if (isImplementation) return "implementation";
4372
+ if (isDocOrTest) return "test";
4373
+ return "other";
4374
+ }
4375
+ if (isImplementation) return "implementation";
4376
+ if (isDocumentation) return "documentation";
4377
+ if (isDocOrTest) return "test";
4378
+ return "other";
4379
+ }
4015
4380
  function classifyQueryIntent(tokens) {
4016
4381
  const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
4017
4382
  const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
@@ -4386,8 +4751,74 @@ function rerankResults(query, candidates, rerankTopN, options) {
4386
4751
  return 0;
4387
4752
  });
4388
4753
  }
4754
+ const shouldDiversify = !(preferSourcePaths && identifierHints.length > 0);
4755
+ const diversifiedHead = diversifyEntriesByFileAndSymbol(head, (entry) => entry.candidate, shouldDiversify);
4389
4756
  const tail = candidates.slice(topN);
4390
- return [...head.map((entry) => entry.candidate), ...tail];
4757
+ return [...diversifiedHead.map((entry) => entry.candidate), ...tail];
4758
+ }
4759
+ function diversifyEntriesByFileAndSymbol(entries, getCandidate, enabled) {
4760
+ if (!enabled || entries.length <= 2) {
4761
+ return entries;
4762
+ }
4763
+ const groups = /* @__PURE__ */ new Map();
4764
+ const groupOrder = [];
4765
+ for (const entry of entries) {
4766
+ const candidate = getCandidate(entry);
4767
+ const filePath = candidate.metadata.filePath;
4768
+ if (!groups.has(filePath)) {
4769
+ groups.set(filePath, []);
4770
+ groupOrder.push(filePath);
4771
+ }
4772
+ groups.get(filePath)?.push(entry);
4773
+ }
4774
+ const diversifiedGroups = groupOrder.map((filePath) => {
4775
+ const group = groups.get(filePath) ?? [];
4776
+ return diversifyGroupBySymbol(group, getCandidate);
4777
+ });
4778
+ const result = [];
4779
+ let added = true;
4780
+ let round = 0;
4781
+ while (added) {
4782
+ added = false;
4783
+ for (const group of diversifiedGroups) {
4784
+ const entry = group[round];
4785
+ if (entry !== void 0) {
4786
+ result.push(entry);
4787
+ added = true;
4788
+ }
4789
+ }
4790
+ round += 1;
4791
+ }
4792
+ return result;
4793
+ }
4794
+ function diversifyCandidatesByFile(candidates, enabled) {
4795
+ return diversifyEntriesByFileAndSymbol(candidates, (candidate) => candidate, enabled);
4796
+ }
4797
+ function diversifyGroupBySymbol(entries, getCandidate) {
4798
+ if (entries.length <= 2) {
4799
+ return entries;
4800
+ }
4801
+ const seenKeys = /* @__PURE__ */ new Set();
4802
+ const primary = [];
4803
+ const remainder = [];
4804
+ for (const entry of entries) {
4805
+ const key = buildDiversityKey(getCandidate(entry).metadata);
4806
+ if (!seenKeys.has(key)) {
4807
+ seenKeys.add(key);
4808
+ primary.push(entry);
4809
+ } else {
4810
+ remainder.push(entry);
4811
+ }
4812
+ }
4813
+ return [...primary, ...remainder];
4814
+ }
4815
+ function buildDiversityKey(metadata) {
4816
+ const normalizedPath = metadata.filePath.toLowerCase();
4817
+ const normalizedName = (metadata.name ?? "").trim().toLowerCase();
4818
+ if (normalizedName.length > 0) {
4819
+ return `${normalizedPath}#${normalizedName}`;
4820
+ }
4821
+ return normalizedPath;
4391
4822
  }
4392
4823
  function rankHybridResults(query, semanticResults, keywordResults, options) {
4393
4824
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
@@ -4715,6 +5146,7 @@ var Indexer = class {
4715
5146
  database = null;
4716
5147
  provider = null;
4717
5148
  configuredProviderInfo = null;
5149
+ reranker = null;
4718
5150
  fileHashCache = /* @__PURE__ */ new Map();
4719
5151
  fileHashCachePath = "";
4720
5152
  failedBatchesPath = "";
@@ -4844,6 +5276,152 @@ var Indexer = class {
4844
5276
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
4845
5277
  }
4846
5278
  }
5279
+ async rerankCandidatesWithApi(query, candidates, options) {
5280
+ const reranker = this.config.reranker;
5281
+ if (!reranker || !reranker.enabled || candidates.length <= 1) {
5282
+ return candidates;
5283
+ }
5284
+ const queryTokens = Array.from(tokenizeTextForRanking(query));
5285
+ const preferSourcePaths = classifyQueryIntentRaw(query) === "source";
5286
+ const docIntent = classifyDocIntent(queryTokens) === "docs";
5287
+ if (options?.definitionIntent === true) {
5288
+ return candidates;
5289
+ }
5290
+ if (options?.hasIdentifierHints === true && preferSourcePaths && !docIntent) {
5291
+ return candidates;
5292
+ }
5293
+ const topN = Math.min(reranker.topN, candidates.length);
5294
+ const head = candidates.slice(0, topN);
5295
+ const tail = candidates.slice(topN);
5296
+ const grouped = /* @__PURE__ */ new Map([
5297
+ ["implementation", []],
5298
+ ["documentation", []],
5299
+ ["test", []],
5300
+ ["other", []]
5301
+ ]);
5302
+ for (const candidate of head) {
5303
+ const band = classifyExternalRerankBand(candidate, preferSourcePaths, docIntent);
5304
+ grouped.get(band)?.push(candidate);
5305
+ }
5306
+ const orderedBands = preferSourcePaths ? ["implementation", "other", "documentation", "test"] : docIntent ? ["documentation", "implementation", "other", "test"] : ["implementation", "other", "documentation", "test"];
5307
+ try {
5308
+ const rerankedHead = [];
5309
+ for (const band of orderedBands) {
5310
+ const bandCandidates = grouped.get(band) ?? [];
5311
+ if (bandCandidates.length <= 1) {
5312
+ rerankedHead.push(...bandCandidates);
5313
+ continue;
5314
+ }
5315
+ const documents = await Promise.all(
5316
+ bandCandidates.map(async (candidate) => ({
5317
+ id: candidate.id,
5318
+ text: await this.createRerankerDocumentText(candidate)
5319
+ }))
5320
+ );
5321
+ const rankedIds = await this.callExternalReranker(query, documents, reranker);
5322
+ if (rankedIds.length === 0) {
5323
+ rerankedHead.push(...bandCandidates);
5324
+ continue;
5325
+ }
5326
+ const order = new Map(rankedIds.map((id, index) => [id, index]));
5327
+ const bandReranked = [...bandCandidates].sort((a, b) => {
5328
+ const aRank = order.get(a.id) ?? Number.MAX_SAFE_INTEGER;
5329
+ const bRank = order.get(b.id) ?? Number.MAX_SAFE_INTEGER;
5330
+ if (aRank !== bRank) {
5331
+ return aRank - bRank;
5332
+ }
5333
+ if (b.score !== a.score) {
5334
+ return b.score - a.score;
5335
+ }
5336
+ return a.id.localeCompare(b.id);
5337
+ });
5338
+ const shouldDiversifyBand = !options?.hasIdentifierHints;
5339
+ rerankedHead.push(...diversifyCandidatesByFile(bandReranked, shouldDiversifyBand));
5340
+ }
5341
+ this.logger.search("debug", "Applied external reranker", {
5342
+ provider: reranker.provider,
5343
+ model: reranker.model,
5344
+ candidateCount: head.length,
5345
+ bands: orderedBands
5346
+ });
5347
+ return [...rerankedHead, ...tail];
5348
+ } catch (error) {
5349
+ this.logger.search("warn", "External reranker failed; using deterministic order", {
5350
+ provider: reranker.provider,
5351
+ model: reranker.model,
5352
+ error: getErrorMessage(error)
5353
+ });
5354
+ return candidates;
5355
+ }
5356
+ }
5357
+ async callExternalReranker(query, documents, reranker) {
5358
+ const headers = {
5359
+ "Content-Type": "application/json"
5360
+ };
5361
+ if (reranker.apiKey) {
5362
+ headers.Authorization = `Bearer ${reranker.apiKey}`;
5363
+ }
5364
+ const controller = new AbortController();
5365
+ const timeout = setTimeout(() => controller.abort(), reranker.timeoutMs);
5366
+ try {
5367
+ const response = await fetch(`${reranker.baseUrl}/rerank`, {
5368
+ method: "POST",
5369
+ headers,
5370
+ body: JSON.stringify({
5371
+ model: reranker.model,
5372
+ query,
5373
+ documents: documents.map((document) => document.text),
5374
+ top_n: documents.length,
5375
+ return_documents: false
5376
+ }),
5377
+ signal: controller.signal
5378
+ });
5379
+ if (!response.ok) {
5380
+ throw new Error(`Reranker API error: ${response.status} - ${await response.text()}`);
5381
+ }
5382
+ const body = await response.json();
5383
+ if (!Array.isArray(body.results)) {
5384
+ throw new Error("Reranker API returned unexpected response format.");
5385
+ }
5386
+ return body.results.map((result) => {
5387
+ const index = typeof result.index === "number" ? result.index : -1;
5388
+ return documents[index]?.id;
5389
+ }).filter((id) => typeof id === "string");
5390
+ } catch (error) {
5391
+ if (error instanceof Error && error.name === "AbortError") {
5392
+ throw new Error(`Reranker request timed out after ${reranker.timeoutMs}ms`);
5393
+ }
5394
+ throw error;
5395
+ } finally {
5396
+ clearTimeout(timeout);
5397
+ }
5398
+ }
5399
+ async createRerankerDocumentText(candidate) {
5400
+ const parts = [
5401
+ `path: ${candidate.metadata.filePath}`,
5402
+ `chunk_type: ${candidate.metadata.chunkType}`,
5403
+ `language: ${candidate.metadata.language}`,
5404
+ `lines: ${candidate.metadata.startLine}-${candidate.metadata.endLine}`
5405
+ ];
5406
+ if (candidate.metadata.name) {
5407
+ parts.push(`name: ${candidate.metadata.name}`);
5408
+ }
5409
+ const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
5410
+ parts.push(`intent_hint: ${intent}`);
5411
+ try {
5412
+ const fileContent = await fsPromises2.readFile(candidate.metadata.filePath, "utf-8");
5413
+ const lines = fileContent.split("\n");
5414
+ const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
5415
+ const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
5416
+ const snippet = lines.slice(snippetStartLine - 1, snippetEndLine).join("\n").trim();
5417
+ parts.push("snippet:");
5418
+ parts.push(snippet.length > 0 ? snippet : "[empty]");
5419
+ } catch {
5420
+ parts.push("snippet:");
5421
+ parts.push("[unavailable]");
5422
+ }
5423
+ return parts.join("\n");
5424
+ }
4847
5425
  async initialize() {
4848
5426
  if (this.config.embeddingProvider === "custom") {
4849
5427
  if (!this.config.customProvider) {
@@ -4863,9 +5441,19 @@ var Indexer = class {
4863
5441
  this.logger.info("Initializing indexer", {
4864
5442
  provider: this.configuredProviderInfo.provider,
4865
5443
  model: this.configuredProviderInfo.modelInfo.model,
4866
- scope: this.config.scope
5444
+ scope: this.config.scope,
5445
+ rerankerEnabled: this.config.reranker?.enabled ?? false
4867
5446
  });
4868
5447
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
5448
+ if (this.config.reranker?.enabled) {
5449
+ this.reranker = createReranker(this.config.reranker);
5450
+ if (this.reranker.isAvailable()) {
5451
+ this.logger.info("Reranker initialized", {
5452
+ model: this.config.reranker.model,
5453
+ baseUrl: this.config.reranker.baseUrl
5454
+ });
5455
+ }
5456
+ }
4869
5457
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
4870
5458
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
4871
5459
  const storePath = path6.join(this.indexPath, "vectors");
@@ -5055,11 +5643,14 @@ var Indexer = class {
5055
5643
  }
5056
5644
  async estimateCost() {
5057
5645
  const { configuredProviderInfo } = await this.ensureInitialized();
5646
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5058
5647
  const { files } = await collectFiles(
5059
5648
  this.projectRoot,
5060
- this.config.include,
5649
+ includePatterns,
5061
5650
  this.config.exclude,
5062
- this.config.indexing.maxFileSize
5651
+ this.config.indexing.maxFileSize,
5652
+ this.config.knowledgeBases,
5653
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5063
5654
  );
5064
5655
  return createCostEstimate(files, configuredProviderInfo);
5065
5656
  }
@@ -5094,11 +5685,14 @@ var Indexer = class {
5094
5685
  totalChunks: 0
5095
5686
  });
5096
5687
  this.loadFileHashCache();
5688
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5097
5689
  const { files, skipped } = await collectFiles(
5098
5690
  this.projectRoot,
5099
- this.config.include,
5691
+ includePatterns,
5100
5692
  this.config.exclude,
5101
- this.config.indexing.maxFileSize
5693
+ this.config.indexing.maxFileSize,
5694
+ this.config.knowledgeBases,
5695
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5102
5696
  );
5103
5697
  stats.totalFiles = files.length;
5104
5698
  stats.skippedFiles = skipped;
@@ -5167,7 +5761,15 @@ var Indexer = class {
5167
5761
  stats.parseFailures.push(relativePath);
5168
5762
  }
5169
5763
  let fileChunkCount = 0;
5170
- for (const chunk of parsed.chunks) {
5764
+ let chunksToProcess = parsed.chunks;
5765
+ if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
5766
+ const changedFile = changedFiles.find((f) => f.path === parsed.path);
5767
+ if (changedFile) {
5768
+ const textChunks = parseFileAsText(parsed.path, changedFile.content);
5769
+ chunksToProcess = textChunks;
5770
+ }
5771
+ }
5772
+ for (const chunk of chunksToProcess) {
5171
5773
  if (fileChunkCount >= this.config.indexing.maxChunksPerFile) {
5172
5774
  break;
5173
5775
  }
@@ -5374,7 +5976,7 @@ var Indexer = class {
5374
5976
  for (const batch of dynamicBatches) {
5375
5977
  queue.add(async () => {
5376
5978
  if (rateLimitBackoffMs > 0) {
5377
- await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
5979
+ await new Promise((resolve6) => setTimeout(resolve6, rateLimitBackoffMs));
5378
5980
  }
5379
5981
  try {
5380
5982
  const result = await pRetry(
@@ -5579,6 +6181,7 @@ var Indexer = class {
5579
6181
  const rerankTopN = this.config.search.rerankTopN;
5580
6182
  const filterByBranch = options?.filterByBranch ?? true;
5581
6183
  const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
6184
+ const identifierHints = extractIdentifierHints(query);
5582
6185
  this.logger.search("debug", "Starting search", {
5583
6186
  query,
5584
6187
  maxResults,
@@ -5633,10 +6236,14 @@ var Indexer = class {
5633
6236
  hybridWeight,
5634
6237
  prioritizeSourcePaths: sourceIntent
5635
6238
  });
6239
+ const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
6240
+ definitionIntent: options?.definitionIntent === true,
6241
+ hasIdentifierHints: identifierHints.length > 0
6242
+ });
5636
6243
  const fusionMs = performance2.now() - fusionStartTime;
5637
6244
  const rescued = promoteIdentifierMatches(
5638
6245
  query,
5639
- combined,
6246
+ rerankedCombined,
5640
6247
  semanticCandidates,
5641
6248
  keywordCandidates,
5642
6249
  database,
@@ -5667,7 +6274,7 @@ var Indexer = class {
5667
6274
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5668
6275
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5669
6276
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5670
- const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
6277
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
5671
6278
  const baseFiltered = tiered.filter((r) => {
5672
6279
  if (r.score < this.config.search.minScore) return false;
5673
6280
  if (options?.fileType) {
@@ -5687,6 +6294,7 @@ var Indexer = class {
5687
6294
  (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5688
6295
  );
5689
6296
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
6297
+ const finalResults = filtered;
5690
6298
  const totalSearchMs = performance2.now() - searchStartTime;
5691
6299
  this.logger.recordSearch(totalSearchMs, {
5692
6300
  embeddingMs,
@@ -5696,7 +6304,7 @@ var Indexer = class {
5696
6304
  });
5697
6305
  this.logger.search("info", "Search complete", {
5698
6306
  query,
5699
- results: filtered.length,
6307
+ results: finalResults.length,
5700
6308
  totalMs: Math.round(totalSearchMs * 100) / 100,
5701
6309
  embeddingMs: Math.round(embeddingMs * 100) / 100,
5702
6310
  vectorMs: Math.round(vectorMs * 100) / 100,
@@ -5706,7 +6314,7 @@ var Indexer = class {
5706
6314
  });
5707
6315
  const metadataOnly = options?.metadataOnly ?? false;
5708
6316
  return Promise.all(
5709
- filtered.map(async (r) => {
6317
+ finalResults.map(async (r) => {
5710
6318
  let content = "";
5711
6319
  let contextStartLine = r.metadata.startLine;
5712
6320
  let contextEndLine = r.metadata.endLine;
@@ -6040,6 +6648,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
6040
6648
  message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
6041
6649
  });
6042
6650
  }
6651
+ if (thresholds.minRawDistinctTop3Ratio !== void 0 && summary.metrics.rawDistinctTop3Ratio < thresholds.minRawDistinctTop3Ratio) {
6652
+ violations.push({
6653
+ metric: "minRawDistinctTop3Ratio",
6654
+ message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
6655
+ });
6656
+ }
6043
6657
  if (comparison) {
6044
6658
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
6045
6659
  violations.push({
@@ -6053,6 +6667,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
6053
6667
  message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
6054
6668
  });
6055
6669
  }
6670
+ if (thresholds.rawDistinctTop3RatioMaxDrop !== void 0 && comparison.deltas.rawDistinctTop3Ratio.absolute < -thresholds.rawDistinctTop3RatioMaxDrop) {
6671
+ violations.push({
6672
+ metric: "rawDistinctTop3RatioMaxDrop",
6673
+ message: `Raw Distinct Top@3 drop ${comparison.deltas.rawDistinctTop3Ratio.absolute.toFixed(4)} exceeds allowed -${thresholds.rawDistinctTop3RatioMaxDrop.toFixed(4)}`
6674
+ });
6675
+ }
6056
6676
  if (thresholds.p95LatencyMaxMultiplier !== void 0) {
6057
6677
  const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
6058
6678
  if (baselineP95 > BASELINE_P95_EPSILON_MS) {
@@ -6107,6 +6727,12 @@ function uniqueResultsByPath(results) {
6107
6727
  }
6108
6728
  return unique;
6109
6729
  }
6730
+ function distinctTopKRatio(results, k) {
6731
+ const top = results.slice(0, k);
6732
+ if (top.length === 0) return 0;
6733
+ const distinct = new Set(top.map((result) => normalizePath(result.filePath))).size;
6734
+ return distinct / top.length;
6735
+ }
6110
6736
  function pathMatchesExpected(actualPath, expectedPath) {
6111
6737
  const actual = normalizePath(actualPath);
6112
6738
  const expected = normalizePath(expectedPath);
@@ -6185,6 +6811,7 @@ function buildPerQueryResult(query, results, latencyMs, k) {
6185
6811
  reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
6186
6812
  ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
6187
6813
  failureBucket: classifyFailureBucket(query, results, k),
6814
+ rawTop3DistinctRatio: distinctTopKRatio(results, 3),
6188
6815
  results: deduped
6189
6816
  };
6190
6817
  return perQuery;
@@ -6198,7 +6825,9 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6198
6825
  hitAt5: 0,
6199
6826
  hitAt10: 0,
6200
6827
  mrrAt10: 0,
6201
- ndcgAt10: 0
6828
+ ndcgAt10: 0,
6829
+ distinctTop3Ratio: 0,
6830
+ rawDistinctTop3Ratio: 0
6202
6831
  };
6203
6832
  const failureBuckets = {
6204
6833
  "wrong-file": 0,
@@ -6214,6 +6843,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6214
6843
  if (query.hitAt10) sum.hitAt10 += 1;
6215
6844
  sum.mrrAt10 += query.reciprocalRankAt10;
6216
6845
  sum.ndcgAt10 += query.ndcgAt10;
6846
+ sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
6847
+ sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
6217
6848
  if (query.failureBucket) {
6218
6849
  failureBuckets[query.failureBucket] += 1;
6219
6850
  }
@@ -6226,6 +6857,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6226
6857
  hitAt10: safeDiv(sum.hitAt10),
6227
6858
  mrrAt10: safeDiv(sum.mrrAt10),
6228
6859
  ndcgAt10: safeDiv(sum.ndcgAt10),
6860
+ distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
6861
+ rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
6229
6862
  latencyMs: {
6230
6863
  p50: percentile(latencies, 0.5),
6231
6864
  p95: percentile(latencies, 0.95),
@@ -6256,23 +6889,23 @@ function isRecord(value) {
6256
6889
  function isStringArray2(value) {
6257
6890
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6258
6891
  }
6259
- function asPositiveNumber(value, path10) {
6892
+ function asPositiveNumber(value, path11) {
6260
6893
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6261
- throw new Error(`${path10} must be a non-negative number`);
6894
+ throw new Error(`${path11} must be a non-negative number`);
6262
6895
  }
6263
6896
  return value;
6264
6897
  }
6265
- function parseQueryType(value, path10) {
6898
+ function parseQueryType(value, path11) {
6266
6899
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6267
6900
  return value;
6268
6901
  }
6269
6902
  throw new Error(
6270
- `${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6903
+ `${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6271
6904
  );
6272
6905
  }
6273
- function parseExpected(input, path10) {
6906
+ function parseExpected(input, path11) {
6274
6907
  if (!isRecord(input)) {
6275
- throw new Error(`${path10} must be an object`);
6908
+ throw new Error(`${path11} must be an object`);
6276
6909
  }
6277
6910
  const filePathRaw = input.filePath;
6278
6911
  const acceptableFilesRaw = input.acceptableFiles;
@@ -6281,16 +6914,16 @@ function parseExpected(input, path10) {
6281
6914
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6282
6915
  const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6283
6916
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6284
- throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
6917
+ throw new Error(`${path11} must include either expected.filePath or expected.acceptableFiles`);
6285
6918
  }
6286
6919
  if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6287
- throw new Error(`${path10}.acceptableFiles must be an array of strings`);
6920
+ throw new Error(`${path11}.acceptableFiles must be an array of strings`);
6288
6921
  }
6289
6922
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6290
- throw new Error(`${path10}.symbol must be a string when provided`);
6923
+ throw new Error(`${path11}.symbol must be a string when provided`);
6291
6924
  }
6292
6925
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6293
- throw new Error(`${path10}.branch must be a string when provided`);
6926
+ throw new Error(`${path11}.branch must be a string when provided`);
6294
6927
  }
6295
6928
  return {
6296
6929
  filePath,
@@ -6300,25 +6933,25 @@ function parseExpected(input, path10) {
6300
6933
  };
6301
6934
  }
6302
6935
  function parseQuery(input, index) {
6303
- const path10 = `queries[${index}]`;
6936
+ const path11 = `queries[${index}]`;
6304
6937
  if (!isRecord(input)) {
6305
- throw new Error(`${path10} must be an object`);
6938
+ throw new Error(`${path11} must be an object`);
6306
6939
  }
6307
6940
  const id = input.id;
6308
6941
  const query = input.query;
6309
6942
  const queryType = input.queryType;
6310
6943
  const expected = input.expected;
6311
6944
  if (typeof id !== "string" || id.trim().length === 0) {
6312
- throw new Error(`${path10}.id must be a non-empty string`);
6945
+ throw new Error(`${path11}.id must be a non-empty string`);
6313
6946
  }
6314
6947
  if (typeof query !== "string" || query.trim().length === 0) {
6315
- throw new Error(`${path10}.query must be a non-empty string`);
6948
+ throw new Error(`${path11}.query must be a non-empty string`);
6316
6949
  }
6317
6950
  return {
6318
6951
  id,
6319
6952
  query,
6320
- queryType: parseQueryType(queryType, `${path10}.queryType`),
6321
- expected: parseExpected(expected, `${path10}.expected`)
6953
+ queryType: parseQueryType(queryType, `${path11}.queryType`),
6954
+ expected: parseExpected(expected, `${path11}.expected`)
6322
6955
  };
6323
6956
  }
6324
6957
  function parseGoldenDataset(raw, sourceLabel) {
@@ -6387,6 +7020,10 @@ function parseBudget(raw, sourceLabel) {
6387
7020
  thresholds: {
6388
7021
  hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
6389
7022
  mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
7023
+ rawDistinctTop3RatioMaxDrop: thresholds.rawDistinctTop3RatioMaxDrop === void 0 ? void 0 : asPositiveNumber(
7024
+ thresholds.rawDistinctTop3RatioMaxDrop,
7025
+ `${sourceLabel}.thresholds.rawDistinctTop3RatioMaxDrop`
7026
+ ),
6390
7027
  p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
6391
7028
  thresholds.p95LatencyMaxMultiplier,
6392
7029
  `${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
@@ -6396,7 +7033,11 @@ function parseBudget(raw, sourceLabel) {
6396
7033
  `${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
6397
7034
  ),
6398
7035
  minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
6399
- minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
7036
+ minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`),
7037
+ minRawDistinctTop3Ratio: thresholds.minRawDistinctTop3Ratio === void 0 ? void 0 : asPositiveNumber(
7038
+ thresholds.minRawDistinctTop3Ratio,
7039
+ `${sourceLabel}.thresholds.minRawDistinctTop3Ratio`
7040
+ )
6400
7041
  }
6401
7042
  };
6402
7043
  }
@@ -6890,8 +7531,12 @@ async function handleEvalCommand(args, cwd) {
6890
7531
  if (!parsed.againstPath.endsWith(".json")) {
6891
7532
  throw new Error("eval diff --against must point to a summary JSON file");
6892
7533
  }
6893
- const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
6894
- const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
7534
+ const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath), {
7535
+ allowLegacyDiversityMetrics: true
7536
+ });
7537
+ const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath), {
7538
+ allowLegacyDiversityMetrics: true
7539
+ });
6895
7540
  const comparison = compareSummaries(
6896
7541
  currentSummary,
6897
7542
  baselineSummary,
@@ -6925,16 +7570,13 @@ function formatDefinitionLookup(results, query) {
6925
7570
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
6926
7571
  }
6927
7572
  const formatted = results.map((r, idx) => {
6928
- const header2 = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
6929
- return `${header2} (score: ${r.score.toFixed(2)})
7573
+ const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
7574
+ return `${header} (score: ${r.score.toFixed(2)})
6930
7575
  \`\`\`
6931
7576
  ${truncateContent(r.content)}
6932
7577
  \`\`\``;
6933
7578
  });
6934
- const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
6935
- return `${header}
6936
-
6937
- ${formatted.join("\n\n")}`;
7579
+ return formatted.join("\n\n");
6938
7580
  }
6939
7581
 
6940
7582
  // src/mcp-server.ts
@@ -7388,7 +8030,10 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
7388
8030
  return server;
7389
8031
  }
7390
8032
 
7391
- // src/cli.ts
8033
+ // src/config/merger.ts
8034
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
8035
+ import * as path9 from "path";
8036
+ import * as os4 from "os";
7392
8037
  function loadJsonFile(filePath) {
7393
8038
  try {
7394
8039
  if (existsSync6(filePath)) {
@@ -7399,25 +8044,101 @@ function loadJsonFile(filePath) {
7399
8044
  }
7400
8045
  return null;
7401
8046
  }
7402
- function loadPluginConfig(projectRoot, configPath) {
7403
- if (configPath) {
7404
- const config = loadJsonFile(configPath);
7405
- if (config) return config;
8047
+ function loadMergedConfig(projectRoot) {
8048
+ const globalConfigPath = path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8049
+ const globalConfig = loadJsonFile(globalConfigPath);
8050
+ const projectConfigPath = path9.join(projectRoot, ".opencode", "codebase-index.json");
8051
+ const projectConfig = loadJsonFile(projectConfigPath);
8052
+ if (!globalConfig && !projectConfig) {
8053
+ return {};
8054
+ }
8055
+ if (!projectConfig && globalConfig) {
8056
+ return globalConfig;
8057
+ }
8058
+ if (!globalConfig && projectConfig) {
8059
+ return projectConfig;
8060
+ }
8061
+ const merged = { ...globalConfig };
8062
+ if (projectConfig && "embeddingProvider" in projectConfig) {
8063
+ merged.embeddingProvider = projectConfig.embeddingProvider;
8064
+ } else if (globalConfig && globalConfig.embeddingProvider) {
8065
+ merged.embeddingProvider = globalConfig.embeddingProvider;
8066
+ }
8067
+ if (projectConfig && "customProvider" in projectConfig) {
8068
+ merged.customProvider = projectConfig.customProvider;
8069
+ } else if (globalConfig && globalConfig.customProvider) {
8070
+ merged.customProvider = globalConfig.customProvider;
8071
+ }
8072
+ if (projectConfig && "embeddingModel" in projectConfig) {
8073
+ merged.embeddingModel = projectConfig.embeddingModel;
8074
+ } else if (globalConfig && globalConfig.embeddingModel) {
8075
+ merged.embeddingModel = globalConfig.embeddingModel;
8076
+ }
8077
+ if (projectConfig && "reranker" in projectConfig) {
8078
+ merged.reranker = projectConfig.reranker;
8079
+ } else if (globalConfig && globalConfig.reranker) {
8080
+ merged.reranker = globalConfig.reranker;
8081
+ }
8082
+ if (projectConfig && "include" in projectConfig) {
8083
+ merged.include = projectConfig.include;
8084
+ } else if (globalConfig && globalConfig.include) {
8085
+ merged.include = globalConfig.include;
8086
+ }
8087
+ if (projectConfig && "exclude" in projectConfig) {
8088
+ merged.exclude = projectConfig.exclude;
8089
+ } else if (globalConfig && globalConfig.exclude) {
8090
+ merged.exclude = globalConfig.exclude;
8091
+ }
8092
+ if (projectConfig && "indexing" in projectConfig) {
8093
+ merged.indexing = projectConfig.indexing;
8094
+ } else if (globalConfig && globalConfig.indexing) {
8095
+ merged.indexing = globalConfig.indexing;
8096
+ }
8097
+ if (projectConfig && "search" in projectConfig) {
8098
+ merged.search = projectConfig.search;
8099
+ } else if (globalConfig && globalConfig.search) {
8100
+ merged.search = globalConfig.search;
8101
+ }
8102
+ if (projectConfig && "debug" in projectConfig) {
8103
+ merged.debug = projectConfig.debug;
8104
+ } else if (globalConfig && globalConfig.debug) {
8105
+ merged.debug = globalConfig.debug;
8106
+ }
8107
+ if (projectConfig && "scope" in projectConfig) {
8108
+ merged.scope = projectConfig.scope;
8109
+ } else if (globalConfig && "scope" in globalConfig) {
8110
+ merged.scope = globalConfig.scope;
8111
+ }
8112
+ if (projectConfig) {
8113
+ for (const key of Object.keys(projectConfig)) {
8114
+ if (key === "embeddingProvider" || key === "customProvider" || key === "embeddingModel" || key === "reranker" || key === "include" || key === "exclude" || key === "indexing" || key === "search" || key === "debug" || key === "scope" || key === "knowledgeBases" || key === "additionalInclude") {
8115
+ continue;
8116
+ }
8117
+ merged[key] = projectConfig[key];
8118
+ }
7406
8119
  }
7407
- const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
7408
- if (projectConfig) return projectConfig;
7409
- const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
7410
- if (globalConfig) return globalConfig;
7411
- return {};
8120
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
8121
+ const projectKbs = projectConfig && Array.isArray(projectConfig.knowledgeBases) ? projectConfig.knowledgeBases : [];
8122
+ const allKbs = [...globalKbs, ...projectKbs];
8123
+ const uniqueKbs = [...new Set(allKbs.map((p) => String(p).trim()))];
8124
+ merged.knowledgeBases = uniqueKbs;
8125
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
8126
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
8127
+ const allAdditional = [...globalAdditional, ...projectAdditional];
8128
+ const uniqueAdditional = [...new Set(allAdditional.map((p) => String(p).trim()))];
8129
+ merged.additionalInclude = uniqueAdditional;
8130
+ return merged;
7412
8131
  }
8132
+
8133
+ // src/cli.ts
7413
8134
  function parseArgs(argv) {
7414
8135
  let project = process.cwd();
7415
8136
  let config;
7416
8137
  for (let i = 2; i < argv.length; i++) {
7417
8138
  if (argv[i] === "--project" && argv[i + 1]) {
7418
- project = path9.resolve(argv[++i]);
8139
+ project = path10.resolve(argv[++i]);
7419
8140
  } else if (argv[i] === "--config" && argv[i + 1]) {
7420
- config = path9.resolve(argv[++i]);
8141
+ config = path10.resolve(argv[++i]);
7421
8142
  }
7422
8143
  }
7423
8144
  return { project, config };
@@ -7428,7 +8149,7 @@ async function main() {
7428
8149
  process.exit(exitCode);
7429
8150
  }
7430
8151
  const args = parseArgs(process.argv);
7431
- const rawConfig = loadPluginConfig(args.project, args.config);
8152
+ const rawConfig = loadMergedConfig(args.project);
7432
8153
  const config = parseConfig(rawConfig);
7433
8154
  const server = createMcpServer(args.project, config);
7434
8155
  const transport = new StdioServerTransport();