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.cjs 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
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
652
- var import_fs10 = require("fs");
653
- var path9 = __toESM(require("path"), 1);
654
- var os4 = __toESM(require("os"), 1);
652
+ var path10 = __toESM(require("path"), 1);
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
  var import_fs = require("fs");
1000
1060
  var path = __toESM(require("path"), 1);
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 = (0, import_fs.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
  var import_fs3 = require("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 import_fs3.promises.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 import_fs3.promises.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 import_fs3.promises.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`);
@@ -3187,7 +3481,52 @@ function getNativeBinding() {
3187
3481
  const require2 = module2.createRequire(requireTarget);
3188
3482
  return require2(nativePath);
3189
3483
  }
3190
- var native = getNativeBinding();
3484
+ function createMockNativeBinding() {
3485
+ const error = new Error("Native module not available. Please rebuild with 'npm run build:native'.");
3486
+ return {
3487
+ parseFile: () => {
3488
+ throw error;
3489
+ },
3490
+ parseFiles: () => {
3491
+ throw error;
3492
+ },
3493
+ hashContent: () => {
3494
+ throw error;
3495
+ },
3496
+ hashFile: () => {
3497
+ throw error;
3498
+ },
3499
+ extractCalls: () => {
3500
+ throw error;
3501
+ },
3502
+ VectorStore: class {
3503
+ constructor() {
3504
+ throw error;
3505
+ }
3506
+ },
3507
+ InvertedIndex: class {
3508
+ constructor() {
3509
+ throw error;
3510
+ }
3511
+ },
3512
+ Database: class {
3513
+ constructor() {
3514
+ throw error;
3515
+ }
3516
+ }
3517
+ };
3518
+ }
3519
+ var native;
3520
+ try {
3521
+ native = getNativeBinding();
3522
+ } catch (e) {
3523
+ console.error("[codebase-index] Failed to load native module:", e);
3524
+ native = createMockNativeBinding();
3525
+ }
3526
+ function parseFileAsText(filePath, content) {
3527
+ const result = native.parseFileAsText(filePath, content);
3528
+ return result.map(mapChunk);
3529
+ }
3191
3530
  function parseFiles(files) {
3192
3531
  const result = native.parseFiles(files);
3193
3532
  return result.map((f) => ({
@@ -4013,6 +4352,32 @@ function isLikelyImplementationPath(filePath) {
4013
4352
  }
4014
4353
  return true;
4015
4354
  }
4355
+ function isDocumentationPath(filePath) {
4356
+ const lowered = filePath.toLowerCase();
4357
+ const ext = lowered.split(".").pop() ?? "";
4358
+ return lowered.includes("readme") || ["md", "mdx", "rst", "adoc", "txt"].includes(ext);
4359
+ }
4360
+ function classifyExternalRerankBand(candidate, preferSourcePaths, docIntent) {
4361
+ const isDocOrTest = isTestOrDocPath(candidate.metadata.filePath);
4362
+ const isDocumentation = isDocumentationPath(candidate.metadata.filePath);
4363
+ const isImplementation = isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType);
4364
+ if (preferSourcePaths) {
4365
+ if (isImplementation) return "implementation";
4366
+ if (isDocumentation) return "documentation";
4367
+ if (isDocOrTest) return "test";
4368
+ return "other";
4369
+ }
4370
+ if (docIntent) {
4371
+ if (isDocumentation) return "documentation";
4372
+ if (isImplementation) return "implementation";
4373
+ if (isDocOrTest) return "test";
4374
+ return "other";
4375
+ }
4376
+ if (isImplementation) return "implementation";
4377
+ if (isDocumentation) return "documentation";
4378
+ if (isDocOrTest) return "test";
4379
+ return "other";
4380
+ }
4016
4381
  function classifyQueryIntent(tokens) {
4017
4382
  const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
4018
4383
  const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
@@ -4387,8 +4752,74 @@ function rerankResults(query, candidates, rerankTopN, options) {
4387
4752
  return 0;
4388
4753
  });
4389
4754
  }
4755
+ const shouldDiversify = !(preferSourcePaths && identifierHints.length > 0);
4756
+ const diversifiedHead = diversifyEntriesByFileAndSymbol(head, (entry) => entry.candidate, shouldDiversify);
4390
4757
  const tail = candidates.slice(topN);
4391
- return [...head.map((entry) => entry.candidate), ...tail];
4758
+ return [...diversifiedHead.map((entry) => entry.candidate), ...tail];
4759
+ }
4760
+ function diversifyEntriesByFileAndSymbol(entries, getCandidate, enabled) {
4761
+ if (!enabled || entries.length <= 2) {
4762
+ return entries;
4763
+ }
4764
+ const groups = /* @__PURE__ */ new Map();
4765
+ const groupOrder = [];
4766
+ for (const entry of entries) {
4767
+ const candidate = getCandidate(entry);
4768
+ const filePath = candidate.metadata.filePath;
4769
+ if (!groups.has(filePath)) {
4770
+ groups.set(filePath, []);
4771
+ groupOrder.push(filePath);
4772
+ }
4773
+ groups.get(filePath)?.push(entry);
4774
+ }
4775
+ const diversifiedGroups = groupOrder.map((filePath) => {
4776
+ const group = groups.get(filePath) ?? [];
4777
+ return diversifyGroupBySymbol(group, getCandidate);
4778
+ });
4779
+ const result = [];
4780
+ let added = true;
4781
+ let round = 0;
4782
+ while (added) {
4783
+ added = false;
4784
+ for (const group of diversifiedGroups) {
4785
+ const entry = group[round];
4786
+ if (entry !== void 0) {
4787
+ result.push(entry);
4788
+ added = true;
4789
+ }
4790
+ }
4791
+ round += 1;
4792
+ }
4793
+ return result;
4794
+ }
4795
+ function diversifyCandidatesByFile(candidates, enabled) {
4796
+ return diversifyEntriesByFileAndSymbol(candidates, (candidate) => candidate, enabled);
4797
+ }
4798
+ function diversifyGroupBySymbol(entries, getCandidate) {
4799
+ if (entries.length <= 2) {
4800
+ return entries;
4801
+ }
4802
+ const seenKeys = /* @__PURE__ */ new Set();
4803
+ const primary = [];
4804
+ const remainder = [];
4805
+ for (const entry of entries) {
4806
+ const key = buildDiversityKey(getCandidate(entry).metadata);
4807
+ if (!seenKeys.has(key)) {
4808
+ seenKeys.add(key);
4809
+ primary.push(entry);
4810
+ } else {
4811
+ remainder.push(entry);
4812
+ }
4813
+ }
4814
+ return [...primary, ...remainder];
4815
+ }
4816
+ function buildDiversityKey(metadata) {
4817
+ const normalizedPath = metadata.filePath.toLowerCase();
4818
+ const normalizedName = (metadata.name ?? "").trim().toLowerCase();
4819
+ if (normalizedName.length > 0) {
4820
+ return `${normalizedPath}#${normalizedName}`;
4821
+ }
4822
+ return normalizedPath;
4392
4823
  }
4393
4824
  function rankHybridResults(query, semanticResults, keywordResults, options) {
4394
4825
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
@@ -4716,6 +5147,7 @@ var Indexer = class {
4716
5147
  database = null;
4717
5148
  provider = null;
4718
5149
  configuredProviderInfo = null;
5150
+ reranker = null;
4719
5151
  fileHashCache = /* @__PURE__ */ new Map();
4720
5152
  fileHashCachePath = "";
4721
5153
  failedBatchesPath = "";
@@ -4845,6 +5277,152 @@ var Indexer = class {
4845
5277
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
4846
5278
  }
4847
5279
  }
5280
+ async rerankCandidatesWithApi(query, candidates, options) {
5281
+ const reranker = this.config.reranker;
5282
+ if (!reranker || !reranker.enabled || candidates.length <= 1) {
5283
+ return candidates;
5284
+ }
5285
+ const queryTokens = Array.from(tokenizeTextForRanking(query));
5286
+ const preferSourcePaths = classifyQueryIntentRaw(query) === "source";
5287
+ const docIntent = classifyDocIntent(queryTokens) === "docs";
5288
+ if (options?.definitionIntent === true) {
5289
+ return candidates;
5290
+ }
5291
+ if (options?.hasIdentifierHints === true && preferSourcePaths && !docIntent) {
5292
+ return candidates;
5293
+ }
5294
+ const topN = Math.min(reranker.topN, candidates.length);
5295
+ const head = candidates.slice(0, topN);
5296
+ const tail = candidates.slice(topN);
5297
+ const grouped = /* @__PURE__ */ new Map([
5298
+ ["implementation", []],
5299
+ ["documentation", []],
5300
+ ["test", []],
5301
+ ["other", []]
5302
+ ]);
5303
+ for (const candidate of head) {
5304
+ const band = classifyExternalRerankBand(candidate, preferSourcePaths, docIntent);
5305
+ grouped.get(band)?.push(candidate);
5306
+ }
5307
+ const orderedBands = preferSourcePaths ? ["implementation", "other", "documentation", "test"] : docIntent ? ["documentation", "implementation", "other", "test"] : ["implementation", "other", "documentation", "test"];
5308
+ try {
5309
+ const rerankedHead = [];
5310
+ for (const band of orderedBands) {
5311
+ const bandCandidates = grouped.get(band) ?? [];
5312
+ if (bandCandidates.length <= 1) {
5313
+ rerankedHead.push(...bandCandidates);
5314
+ continue;
5315
+ }
5316
+ const documents = await Promise.all(
5317
+ bandCandidates.map(async (candidate) => ({
5318
+ id: candidate.id,
5319
+ text: await this.createRerankerDocumentText(candidate)
5320
+ }))
5321
+ );
5322
+ const rankedIds = await this.callExternalReranker(query, documents, reranker);
5323
+ if (rankedIds.length === 0) {
5324
+ rerankedHead.push(...bandCandidates);
5325
+ continue;
5326
+ }
5327
+ const order = new Map(rankedIds.map((id, index) => [id, index]));
5328
+ const bandReranked = [...bandCandidates].sort((a, b) => {
5329
+ const aRank = order.get(a.id) ?? Number.MAX_SAFE_INTEGER;
5330
+ const bRank = order.get(b.id) ?? Number.MAX_SAFE_INTEGER;
5331
+ if (aRank !== bRank) {
5332
+ return aRank - bRank;
5333
+ }
5334
+ if (b.score !== a.score) {
5335
+ return b.score - a.score;
5336
+ }
5337
+ return a.id.localeCompare(b.id);
5338
+ });
5339
+ const shouldDiversifyBand = !options?.hasIdentifierHints;
5340
+ rerankedHead.push(...diversifyCandidatesByFile(bandReranked, shouldDiversifyBand));
5341
+ }
5342
+ this.logger.search("debug", "Applied external reranker", {
5343
+ provider: reranker.provider,
5344
+ model: reranker.model,
5345
+ candidateCount: head.length,
5346
+ bands: orderedBands
5347
+ });
5348
+ return [...rerankedHead, ...tail];
5349
+ } catch (error) {
5350
+ this.logger.search("warn", "External reranker failed; using deterministic order", {
5351
+ provider: reranker.provider,
5352
+ model: reranker.model,
5353
+ error: getErrorMessage(error)
5354
+ });
5355
+ return candidates;
5356
+ }
5357
+ }
5358
+ async callExternalReranker(query, documents, reranker) {
5359
+ const headers = {
5360
+ "Content-Type": "application/json"
5361
+ };
5362
+ if (reranker.apiKey) {
5363
+ headers.Authorization = `Bearer ${reranker.apiKey}`;
5364
+ }
5365
+ const controller = new AbortController();
5366
+ const timeout = setTimeout(() => controller.abort(), reranker.timeoutMs);
5367
+ try {
5368
+ const response = await fetch(`${reranker.baseUrl}/rerank`, {
5369
+ method: "POST",
5370
+ headers,
5371
+ body: JSON.stringify({
5372
+ model: reranker.model,
5373
+ query,
5374
+ documents: documents.map((document) => document.text),
5375
+ top_n: documents.length,
5376
+ return_documents: false
5377
+ }),
5378
+ signal: controller.signal
5379
+ });
5380
+ if (!response.ok) {
5381
+ throw new Error(`Reranker API error: ${response.status} - ${await response.text()}`);
5382
+ }
5383
+ const body = await response.json();
5384
+ if (!Array.isArray(body.results)) {
5385
+ throw new Error("Reranker API returned unexpected response format.");
5386
+ }
5387
+ return body.results.map((result) => {
5388
+ const index = typeof result.index === "number" ? result.index : -1;
5389
+ return documents[index]?.id;
5390
+ }).filter((id) => typeof id === "string");
5391
+ } catch (error) {
5392
+ if (error instanceof Error && error.name === "AbortError") {
5393
+ throw new Error(`Reranker request timed out after ${reranker.timeoutMs}ms`);
5394
+ }
5395
+ throw error;
5396
+ } finally {
5397
+ clearTimeout(timeout);
5398
+ }
5399
+ }
5400
+ async createRerankerDocumentText(candidate) {
5401
+ const parts = [
5402
+ `path: ${candidate.metadata.filePath}`,
5403
+ `chunk_type: ${candidate.metadata.chunkType}`,
5404
+ `language: ${candidate.metadata.language}`,
5405
+ `lines: ${candidate.metadata.startLine}-${candidate.metadata.endLine}`
5406
+ ];
5407
+ if (candidate.metadata.name) {
5408
+ parts.push(`name: ${candidate.metadata.name}`);
5409
+ }
5410
+ const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
5411
+ parts.push(`intent_hint: ${intent}`);
5412
+ try {
5413
+ const fileContent = await import_fs5.promises.readFile(candidate.metadata.filePath, "utf-8");
5414
+ const lines = fileContent.split("\n");
5415
+ const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
5416
+ const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
5417
+ const snippet = lines.slice(snippetStartLine - 1, snippetEndLine).join("\n").trim();
5418
+ parts.push("snippet:");
5419
+ parts.push(snippet.length > 0 ? snippet : "[empty]");
5420
+ } catch {
5421
+ parts.push("snippet:");
5422
+ parts.push("[unavailable]");
5423
+ }
5424
+ return parts.join("\n");
5425
+ }
4848
5426
  async initialize() {
4849
5427
  if (this.config.embeddingProvider === "custom") {
4850
5428
  if (!this.config.customProvider) {
@@ -4864,9 +5442,19 @@ var Indexer = class {
4864
5442
  this.logger.info("Initializing indexer", {
4865
5443
  provider: this.configuredProviderInfo.provider,
4866
5444
  model: this.configuredProviderInfo.modelInfo.model,
4867
- scope: this.config.scope
5445
+ scope: this.config.scope,
5446
+ rerankerEnabled: this.config.reranker?.enabled ?? false
4868
5447
  });
4869
5448
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
5449
+ if (this.config.reranker?.enabled) {
5450
+ this.reranker = createReranker(this.config.reranker);
5451
+ if (this.reranker.isAvailable()) {
5452
+ this.logger.info("Reranker initialized", {
5453
+ model: this.config.reranker.model,
5454
+ baseUrl: this.config.reranker.baseUrl
5455
+ });
5456
+ }
5457
+ }
4870
5458
  await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
4871
5459
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
4872
5460
  const storePath = path6.join(this.indexPath, "vectors");
@@ -5056,11 +5644,14 @@ var Indexer = class {
5056
5644
  }
5057
5645
  async estimateCost() {
5058
5646
  const { configuredProviderInfo } = await this.ensureInitialized();
5647
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5059
5648
  const { files } = await collectFiles(
5060
5649
  this.projectRoot,
5061
- this.config.include,
5650
+ includePatterns,
5062
5651
  this.config.exclude,
5063
- this.config.indexing.maxFileSize
5652
+ this.config.indexing.maxFileSize,
5653
+ this.config.knowledgeBases,
5654
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5064
5655
  );
5065
5656
  return createCostEstimate(files, configuredProviderInfo);
5066
5657
  }
@@ -5095,11 +5686,14 @@ var Indexer = class {
5095
5686
  totalChunks: 0
5096
5687
  });
5097
5688
  this.loadFileHashCache();
5689
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5098
5690
  const { files, skipped } = await collectFiles(
5099
5691
  this.projectRoot,
5100
- this.config.include,
5692
+ includePatterns,
5101
5693
  this.config.exclude,
5102
- this.config.indexing.maxFileSize
5694
+ this.config.indexing.maxFileSize,
5695
+ this.config.knowledgeBases,
5696
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5103
5697
  );
5104
5698
  stats.totalFiles = files.length;
5105
5699
  stats.skippedFiles = skipped;
@@ -5168,7 +5762,15 @@ var Indexer = class {
5168
5762
  stats.parseFailures.push(relativePath);
5169
5763
  }
5170
5764
  let fileChunkCount = 0;
5171
- for (const chunk of parsed.chunks) {
5765
+ let chunksToProcess = parsed.chunks;
5766
+ if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
5767
+ const changedFile = changedFiles.find((f) => f.path === parsed.path);
5768
+ if (changedFile) {
5769
+ const textChunks = parseFileAsText(parsed.path, changedFile.content);
5770
+ chunksToProcess = textChunks;
5771
+ }
5772
+ }
5773
+ for (const chunk of chunksToProcess) {
5172
5774
  if (fileChunkCount >= this.config.indexing.maxChunksPerFile) {
5173
5775
  break;
5174
5776
  }
@@ -5375,7 +5977,7 @@ var Indexer = class {
5375
5977
  for (const batch of dynamicBatches) {
5376
5978
  queue.add(async () => {
5377
5979
  if (rateLimitBackoffMs > 0) {
5378
- await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
5980
+ await new Promise((resolve6) => setTimeout(resolve6, rateLimitBackoffMs));
5379
5981
  }
5380
5982
  try {
5381
5983
  const result = await pRetry(
@@ -5580,6 +6182,7 @@ var Indexer = class {
5580
6182
  const rerankTopN = this.config.search.rerankTopN;
5581
6183
  const filterByBranch = options?.filterByBranch ?? true;
5582
6184
  const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
6185
+ const identifierHints = extractIdentifierHints(query);
5583
6186
  this.logger.search("debug", "Starting search", {
5584
6187
  query,
5585
6188
  maxResults,
@@ -5634,10 +6237,14 @@ var Indexer = class {
5634
6237
  hybridWeight,
5635
6238
  prioritizeSourcePaths: sourceIntent
5636
6239
  });
6240
+ const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
6241
+ definitionIntent: options?.definitionIntent === true,
6242
+ hasIdentifierHints: identifierHints.length > 0
6243
+ });
5637
6244
  const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
5638
6245
  const rescued = promoteIdentifierMatches(
5639
6246
  query,
5640
- combined,
6247
+ rerankedCombined,
5641
6248
  semanticCandidates,
5642
6249
  keywordCandidates,
5643
6250
  database,
@@ -5668,7 +6275,7 @@ var Indexer = class {
5668
6275
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5669
6276
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5670
6277
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5671
- const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
6278
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
5672
6279
  const baseFiltered = tiered.filter((r) => {
5673
6280
  if (r.score < this.config.search.minScore) return false;
5674
6281
  if (options?.fileType) {
@@ -5688,6 +6295,7 @@ var Indexer = class {
5688
6295
  (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5689
6296
  );
5690
6297
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
6298
+ const finalResults = filtered;
5691
6299
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
5692
6300
  this.logger.recordSearch(totalSearchMs, {
5693
6301
  embeddingMs,
@@ -5697,7 +6305,7 @@ var Indexer = class {
5697
6305
  });
5698
6306
  this.logger.search("info", "Search complete", {
5699
6307
  query,
5700
- results: filtered.length,
6308
+ results: finalResults.length,
5701
6309
  totalMs: Math.round(totalSearchMs * 100) / 100,
5702
6310
  embeddingMs: Math.round(embeddingMs * 100) / 100,
5703
6311
  vectorMs: Math.round(vectorMs * 100) / 100,
@@ -5707,7 +6315,7 @@ var Indexer = class {
5707
6315
  });
5708
6316
  const metadataOnly = options?.metadataOnly ?? false;
5709
6317
  return Promise.all(
5710
- filtered.map(async (r) => {
6318
+ finalResults.map(async (r) => {
5711
6319
  let content = "";
5712
6320
  let contextStartLine = r.metadata.startLine;
5713
6321
  let contextEndLine = r.metadata.endLine;
@@ -6041,6 +6649,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
6041
6649
  message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
6042
6650
  });
6043
6651
  }
6652
+ if (thresholds.minRawDistinctTop3Ratio !== void 0 && summary.metrics.rawDistinctTop3Ratio < thresholds.minRawDistinctTop3Ratio) {
6653
+ violations.push({
6654
+ metric: "minRawDistinctTop3Ratio",
6655
+ message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
6656
+ });
6657
+ }
6044
6658
  if (comparison) {
6045
6659
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
6046
6660
  violations.push({
@@ -6054,6 +6668,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
6054
6668
  message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
6055
6669
  });
6056
6670
  }
6671
+ if (thresholds.rawDistinctTop3RatioMaxDrop !== void 0 && comparison.deltas.rawDistinctTop3Ratio.absolute < -thresholds.rawDistinctTop3RatioMaxDrop) {
6672
+ violations.push({
6673
+ metric: "rawDistinctTop3RatioMaxDrop",
6674
+ message: `Raw Distinct Top@3 drop ${comparison.deltas.rawDistinctTop3Ratio.absolute.toFixed(4)} exceeds allowed -${thresholds.rawDistinctTop3RatioMaxDrop.toFixed(4)}`
6675
+ });
6676
+ }
6057
6677
  if (thresholds.p95LatencyMaxMultiplier !== void 0) {
6058
6678
  const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
6059
6679
  if (baselineP95 > BASELINE_P95_EPSILON_MS) {
@@ -6108,6 +6728,12 @@ function uniqueResultsByPath(results) {
6108
6728
  }
6109
6729
  return unique;
6110
6730
  }
6731
+ function distinctTopKRatio(results, k) {
6732
+ const top = results.slice(0, k);
6733
+ if (top.length === 0) return 0;
6734
+ const distinct = new Set(top.map((result) => normalizePath(result.filePath))).size;
6735
+ return distinct / top.length;
6736
+ }
6111
6737
  function pathMatchesExpected(actualPath, expectedPath) {
6112
6738
  const actual = normalizePath(actualPath);
6113
6739
  const expected = normalizePath(expectedPath);
@@ -6186,6 +6812,7 @@ function buildPerQueryResult(query, results, latencyMs, k) {
6186
6812
  reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
6187
6813
  ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
6188
6814
  failureBucket: classifyFailureBucket(query, results, k),
6815
+ rawTop3DistinctRatio: distinctTopKRatio(results, 3),
6189
6816
  results: deduped
6190
6817
  };
6191
6818
  return perQuery;
@@ -6199,7 +6826,9 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6199
6826
  hitAt5: 0,
6200
6827
  hitAt10: 0,
6201
6828
  mrrAt10: 0,
6202
- ndcgAt10: 0
6829
+ ndcgAt10: 0,
6830
+ distinctTop3Ratio: 0,
6831
+ rawDistinctTop3Ratio: 0
6203
6832
  };
6204
6833
  const failureBuckets = {
6205
6834
  "wrong-file": 0,
@@ -6215,6 +6844,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6215
6844
  if (query.hitAt10) sum.hitAt10 += 1;
6216
6845
  sum.mrrAt10 += query.reciprocalRankAt10;
6217
6846
  sum.ndcgAt10 += query.ndcgAt10;
6847
+ sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
6848
+ sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
6218
6849
  if (query.failureBucket) {
6219
6850
  failureBuckets[query.failureBucket] += 1;
6220
6851
  }
@@ -6227,6 +6858,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6227
6858
  hitAt10: safeDiv(sum.hitAt10),
6228
6859
  mrrAt10: safeDiv(sum.mrrAt10),
6229
6860
  ndcgAt10: safeDiv(sum.ndcgAt10),
6861
+ distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
6862
+ rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
6230
6863
  latencyMs: {
6231
6864
  p50: percentile(latencies, 0.5),
6232
6865
  p95: percentile(latencies, 0.95),
@@ -6257,23 +6890,23 @@ function isRecord(value) {
6257
6890
  function isStringArray2(value) {
6258
6891
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6259
6892
  }
6260
- function asPositiveNumber(value, path10) {
6893
+ function asPositiveNumber(value, path11) {
6261
6894
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6262
- throw new Error(`${path10} must be a non-negative number`);
6895
+ throw new Error(`${path11} must be a non-negative number`);
6263
6896
  }
6264
6897
  return value;
6265
6898
  }
6266
- function parseQueryType(value, path10) {
6899
+ function parseQueryType(value, path11) {
6267
6900
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6268
6901
  return value;
6269
6902
  }
6270
6903
  throw new Error(
6271
- `${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6904
+ `${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6272
6905
  );
6273
6906
  }
6274
- function parseExpected(input, path10) {
6907
+ function parseExpected(input, path11) {
6275
6908
  if (!isRecord(input)) {
6276
- throw new Error(`${path10} must be an object`);
6909
+ throw new Error(`${path11} must be an object`);
6277
6910
  }
6278
6911
  const filePathRaw = input.filePath;
6279
6912
  const acceptableFilesRaw = input.acceptableFiles;
@@ -6282,16 +6915,16 @@ function parseExpected(input, path10) {
6282
6915
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6283
6916
  const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6284
6917
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6285
- throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
6918
+ throw new Error(`${path11} must include either expected.filePath or expected.acceptableFiles`);
6286
6919
  }
6287
6920
  if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6288
- throw new Error(`${path10}.acceptableFiles must be an array of strings`);
6921
+ throw new Error(`${path11}.acceptableFiles must be an array of strings`);
6289
6922
  }
6290
6923
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6291
- throw new Error(`${path10}.symbol must be a string when provided`);
6924
+ throw new Error(`${path11}.symbol must be a string when provided`);
6292
6925
  }
6293
6926
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6294
- throw new Error(`${path10}.branch must be a string when provided`);
6927
+ throw new Error(`${path11}.branch must be a string when provided`);
6295
6928
  }
6296
6929
  return {
6297
6930
  filePath,
@@ -6301,25 +6934,25 @@ function parseExpected(input, path10) {
6301
6934
  };
6302
6935
  }
6303
6936
  function parseQuery(input, index) {
6304
- const path10 = `queries[${index}]`;
6937
+ const path11 = `queries[${index}]`;
6305
6938
  if (!isRecord(input)) {
6306
- throw new Error(`${path10} must be an object`);
6939
+ throw new Error(`${path11} must be an object`);
6307
6940
  }
6308
6941
  const id = input.id;
6309
6942
  const query = input.query;
6310
6943
  const queryType = input.queryType;
6311
6944
  const expected = input.expected;
6312
6945
  if (typeof id !== "string" || id.trim().length === 0) {
6313
- throw new Error(`${path10}.id must be a non-empty string`);
6946
+ throw new Error(`${path11}.id must be a non-empty string`);
6314
6947
  }
6315
6948
  if (typeof query !== "string" || query.trim().length === 0) {
6316
- throw new Error(`${path10}.query must be a non-empty string`);
6949
+ throw new Error(`${path11}.query must be a non-empty string`);
6317
6950
  }
6318
6951
  return {
6319
6952
  id,
6320
6953
  query,
6321
- queryType: parseQueryType(queryType, `${path10}.queryType`),
6322
- expected: parseExpected(expected, `${path10}.expected`)
6954
+ queryType: parseQueryType(queryType, `${path11}.queryType`),
6955
+ expected: parseExpected(expected, `${path11}.expected`)
6323
6956
  };
6324
6957
  }
6325
6958
  function parseGoldenDataset(raw, sourceLabel) {
@@ -6388,6 +7021,10 @@ function parseBudget(raw, sourceLabel) {
6388
7021
  thresholds: {
6389
7022
  hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
6390
7023
  mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
7024
+ rawDistinctTop3RatioMaxDrop: thresholds.rawDistinctTop3RatioMaxDrop === void 0 ? void 0 : asPositiveNumber(
7025
+ thresholds.rawDistinctTop3RatioMaxDrop,
7026
+ `${sourceLabel}.thresholds.rawDistinctTop3RatioMaxDrop`
7027
+ ),
6391
7028
  p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
6392
7029
  thresholds.p95LatencyMaxMultiplier,
6393
7030
  `${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
@@ -6397,7 +7034,11 @@ function parseBudget(raw, sourceLabel) {
6397
7034
  `${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
6398
7035
  ),
6399
7036
  minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
6400
- minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
7037
+ minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`),
7038
+ minRawDistinctTop3Ratio: thresholds.minRawDistinctTop3Ratio === void 0 ? void 0 : asPositiveNumber(
7039
+ thresholds.minRawDistinctTop3Ratio,
7040
+ `${sourceLabel}.thresholds.minRawDistinctTop3Ratio`
7041
+ )
6401
7042
  }
6402
7043
  };
6403
7044
  }
@@ -6891,8 +7532,12 @@ async function handleEvalCommand(args, cwd) {
6891
7532
  if (!parsed.againstPath.endsWith(".json")) {
6892
7533
  throw new Error("eval diff --against must point to a summary JSON file");
6893
7534
  }
6894
- const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
6895
- const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
7535
+ const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath), {
7536
+ allowLegacyDiversityMetrics: true
7537
+ });
7538
+ const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath), {
7539
+ allowLegacyDiversityMetrics: true
7540
+ });
6896
7541
  const comparison = compareSummaries(
6897
7542
  currentSummary,
6898
7543
  baselineSummary,
@@ -6926,16 +7571,13 @@ function formatDefinitionLookup(results, query) {
6926
7571
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
6927
7572
  }
6928
7573
  const formatted = results.map((r, idx) => {
6929
- 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}`;
6930
- return `${header2} (score: ${r.score.toFixed(2)})
7574
+ 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}`;
7575
+ return `${header} (score: ${r.score.toFixed(2)})
6931
7576
  \`\`\`
6932
7577
  ${truncateContent(r.content)}
6933
7578
  \`\`\``;
6934
7579
  });
6935
- const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
6936
- return `${header}
6937
-
6938
- ${formatted.join("\n\n")}`;
7580
+ return formatted.join("\n\n");
6939
7581
  }
6940
7582
 
6941
7583
  // src/mcp-server.ts
@@ -7389,7 +8031,10 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
7389
8031
  return server;
7390
8032
  }
7391
8033
 
7392
- // src/cli.ts
8034
+ // src/config/merger.ts
8035
+ var import_fs10 = require("fs");
8036
+ var path9 = __toESM(require("path"), 1);
8037
+ var os4 = __toESM(require("os"), 1);
7393
8038
  function loadJsonFile(filePath) {
7394
8039
  try {
7395
8040
  if ((0, import_fs10.existsSync)(filePath)) {
@@ -7400,25 +8045,101 @@ function loadJsonFile(filePath) {
7400
8045
  }
7401
8046
  return null;
7402
8047
  }
7403
- function loadPluginConfig(projectRoot, configPath) {
7404
- if (configPath) {
7405
- const config = loadJsonFile(configPath);
7406
- if (config) return config;
8048
+ function loadMergedConfig(projectRoot) {
8049
+ const globalConfigPath = path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8050
+ const globalConfig = loadJsonFile(globalConfigPath);
8051
+ const projectConfigPath = path9.join(projectRoot, ".opencode", "codebase-index.json");
8052
+ const projectConfig = loadJsonFile(projectConfigPath);
8053
+ if (!globalConfig && !projectConfig) {
8054
+ return {};
8055
+ }
8056
+ if (!projectConfig && globalConfig) {
8057
+ return globalConfig;
8058
+ }
8059
+ if (!globalConfig && projectConfig) {
8060
+ return projectConfig;
8061
+ }
8062
+ const merged = { ...globalConfig };
8063
+ if (projectConfig && "embeddingProvider" in projectConfig) {
8064
+ merged.embeddingProvider = projectConfig.embeddingProvider;
8065
+ } else if (globalConfig && globalConfig.embeddingProvider) {
8066
+ merged.embeddingProvider = globalConfig.embeddingProvider;
8067
+ }
8068
+ if (projectConfig && "customProvider" in projectConfig) {
8069
+ merged.customProvider = projectConfig.customProvider;
8070
+ } else if (globalConfig && globalConfig.customProvider) {
8071
+ merged.customProvider = globalConfig.customProvider;
8072
+ }
8073
+ if (projectConfig && "embeddingModel" in projectConfig) {
8074
+ merged.embeddingModel = projectConfig.embeddingModel;
8075
+ } else if (globalConfig && globalConfig.embeddingModel) {
8076
+ merged.embeddingModel = globalConfig.embeddingModel;
8077
+ }
8078
+ if (projectConfig && "reranker" in projectConfig) {
8079
+ merged.reranker = projectConfig.reranker;
8080
+ } else if (globalConfig && globalConfig.reranker) {
8081
+ merged.reranker = globalConfig.reranker;
8082
+ }
8083
+ if (projectConfig && "include" in projectConfig) {
8084
+ merged.include = projectConfig.include;
8085
+ } else if (globalConfig && globalConfig.include) {
8086
+ merged.include = globalConfig.include;
8087
+ }
8088
+ if (projectConfig && "exclude" in projectConfig) {
8089
+ merged.exclude = projectConfig.exclude;
8090
+ } else if (globalConfig && globalConfig.exclude) {
8091
+ merged.exclude = globalConfig.exclude;
8092
+ }
8093
+ if (projectConfig && "indexing" in projectConfig) {
8094
+ merged.indexing = projectConfig.indexing;
8095
+ } else if (globalConfig && globalConfig.indexing) {
8096
+ merged.indexing = globalConfig.indexing;
8097
+ }
8098
+ if (projectConfig && "search" in projectConfig) {
8099
+ merged.search = projectConfig.search;
8100
+ } else if (globalConfig && globalConfig.search) {
8101
+ merged.search = globalConfig.search;
8102
+ }
8103
+ if (projectConfig && "debug" in projectConfig) {
8104
+ merged.debug = projectConfig.debug;
8105
+ } else if (globalConfig && globalConfig.debug) {
8106
+ merged.debug = globalConfig.debug;
8107
+ }
8108
+ if (projectConfig && "scope" in projectConfig) {
8109
+ merged.scope = projectConfig.scope;
8110
+ } else if (globalConfig && "scope" in globalConfig) {
8111
+ merged.scope = globalConfig.scope;
8112
+ }
8113
+ if (projectConfig) {
8114
+ for (const key of Object.keys(projectConfig)) {
8115
+ 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") {
8116
+ continue;
8117
+ }
8118
+ merged[key] = projectConfig[key];
8119
+ }
7407
8120
  }
7408
- const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
7409
- if (projectConfig) return projectConfig;
7410
- const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
7411
- if (globalConfig) return globalConfig;
7412
- return {};
8121
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
8122
+ const projectKbs = projectConfig && Array.isArray(projectConfig.knowledgeBases) ? projectConfig.knowledgeBases : [];
8123
+ const allKbs = [...globalKbs, ...projectKbs];
8124
+ const uniqueKbs = [...new Set(allKbs.map((p) => String(p).trim()))];
8125
+ merged.knowledgeBases = uniqueKbs;
8126
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
8127
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
8128
+ const allAdditional = [...globalAdditional, ...projectAdditional];
8129
+ const uniqueAdditional = [...new Set(allAdditional.map((p) => String(p).trim()))];
8130
+ merged.additionalInclude = uniqueAdditional;
8131
+ return merged;
7413
8132
  }
8133
+
8134
+ // src/cli.ts
7414
8135
  function parseArgs(argv) {
7415
8136
  let project = process.cwd();
7416
8137
  let config;
7417
8138
  for (let i = 2; i < argv.length; i++) {
7418
8139
  if (argv[i] === "--project" && argv[i + 1]) {
7419
- project = path9.resolve(argv[++i]);
8140
+ project = path10.resolve(argv[++i]);
7420
8141
  } else if (argv[i] === "--config" && argv[i + 1]) {
7421
- config = path9.resolve(argv[++i]);
8142
+ config = path10.resolve(argv[++i]);
7422
8143
  }
7423
8144
  }
7424
8145
  return { project, config };
@@ -7429,7 +8150,7 @@ async function main() {
7429
8150
  process.exit(exitCode);
7430
8151
  }
7431
8152
  const args = parseArgs(process.argv);
7432
- const rawConfig = loadPluginConfig(args.project, args.config);
8153
+ const rawConfig = loadMergedConfig(args.project);
7433
8154
  const config = parseConfig(rawConfig);
7434
8155
  const server = createMcpServer(args.project, config);
7435
8156
  const transport = new import_stdio.StdioServerTransport();