opencode-codebase-index 0.6.0 → 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": {
@@ -754,6 +756,27 @@ var DEFAULT_PROVIDER_MODELS = {
754
756
  "ollama": "nomic-embed-text"
755
757
  };
756
758
 
759
+ // src/config/env-substitution.ts
760
+ var ENV_REFERENCE_PATTERN = /^\{env:([A-Z_][A-Z0-9_]*)\}$/;
761
+ var ENV_REFERENCE_LIKE_PATTERN = /\{env:[^}]+\}/;
762
+ function substituteEnvString(value, keyPath) {
763
+ const match = value.match(ENV_REFERENCE_PATTERN);
764
+ if (!match) {
765
+ if (ENV_REFERENCE_LIKE_PATTERN.test(value)) {
766
+ throw new Error(
767
+ `Invalid environment variable reference at '${keyPath}'. Expected the entire string to match '{env:VAR_NAME}' with VAR_NAME matching [A-Z_][A-Z0-9_]*.`
768
+ );
769
+ }
770
+ return value;
771
+ }
772
+ const variableName = match[1];
773
+ const envValue = process.env[variableName];
774
+ if (envValue === void 0) {
775
+ throw new Error(`Missing environment variable '${variableName}' referenced by config at '${keyPath}'.`);
776
+ }
777
+ return envValue;
778
+ }
779
+
757
780
  // src/config/schema.ts
758
781
  function getDefaultIndexingConfig() {
759
782
  return {
@@ -767,7 +790,10 @@ function getDefaultIndexingConfig() {
767
790
  autoGc: true,
768
791
  gcIntervalDays: 7,
769
792
  gcOrphanThreshold: 100,
770
- requireProjectMarker: true
793
+ requireProjectMarker: true,
794
+ maxDepth: 5,
795
+ maxFilesPerDirectory: 100,
796
+ fallbackToTextOnMaxChunks: true
771
797
  };
772
798
  }
773
799
  function getDefaultSearchConfig() {
@@ -779,12 +805,26 @@ function getDefaultSearchConfig() {
779
805
  fusionStrategy: "rrf",
780
806
  rrfK: 60,
781
807
  rerankTopN: 20,
782
- contextLines: 0
808
+ contextLines: 0,
809
+ routingHints: true
783
810
  };
784
811
  }
785
812
  function isValidFusionStrategy(value) {
786
813
  return value === "weighted" || value === "rrf";
787
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
+ }
788
828
  function getDefaultDebugConfig() {
789
829
  return {
790
830
  enabled: false,
@@ -811,11 +851,27 @@ function isValidScope(value) {
811
851
  function isStringArray(value) {
812
852
  return Array.isArray(value) && value.every((item) => typeof item === "string");
813
853
  }
854
+ function getResolvedString(value, keyPath) {
855
+ if (typeof value !== "string") {
856
+ return void 0;
857
+ }
858
+ return substituteEnvString(value, keyPath);
859
+ }
860
+ function getResolvedStringArray(value, keyPath) {
861
+ if (!isStringArray(value)) {
862
+ return void 0;
863
+ }
864
+ return value.map((item, index) => substituteEnvString(item, `${keyPath}[${index}]`));
865
+ }
814
866
  function isValidLogLevel(value) {
815
867
  return typeof value === "string" && VALID_LOG_LEVELS.includes(value);
816
868
  }
817
869
  function parseConfig(raw) {
818
870
  const input = raw && typeof raw === "object" ? raw : {};
871
+ const embeddingProviderValue = getResolvedString(input.embeddingProvider, "$root.embeddingProvider");
872
+ const scopeValue = getResolvedString(input.scope, "$root.scope");
873
+ const includeValue = getResolvedStringArray(input.include, "$root.include");
874
+ const excludeValue = getResolvedStringArray(input.exclude, "$root.exclude");
819
875
  const defaultIndexing = getDefaultIndexingConfig();
820
876
  const defaultSearch = getDefaultSearchConfig();
821
877
  const defaultDebug = getDefaultDebugConfig();
@@ -831,7 +887,10 @@ function parseConfig(raw) {
831
887
  autoGc: typeof rawIndexing.autoGc === "boolean" ? rawIndexing.autoGc : defaultIndexing.autoGc,
832
888
  gcIntervalDays: typeof rawIndexing.gcIntervalDays === "number" ? Math.max(1, rawIndexing.gcIntervalDays) : defaultIndexing.gcIntervalDays,
833
889
  gcOrphanThreshold: typeof rawIndexing.gcOrphanThreshold === "number" ? Math.max(0, rawIndexing.gcOrphanThreshold) : defaultIndexing.gcOrphanThreshold,
834
- 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
835
894
  };
836
895
  const rawSearch = input.search && typeof input.search === "object" ? input.search : {};
837
896
  const search = {
@@ -842,7 +901,8 @@ function parseConfig(raw) {
842
901
  fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
843
902
  rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
844
903
  rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
845
- 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
846
906
  };
847
907
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
848
908
  const debug = {
@@ -855,22 +915,31 @@ function parseConfig(raw) {
855
915
  logBranch: typeof rawDebug.logBranch === "boolean" ? rawDebug.logBranch : defaultDebug.logBranch,
856
916
  metrics: typeof rawDebug.metrics === "boolean" ? rawDebug.metrics : defaultDebug.metrics
857
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()) : [];
858
922
  let embeddingProvider;
859
923
  let embeddingModel = void 0;
860
924
  let customProvider = void 0;
861
- if (input.embeddingProvider === "custom") {
925
+ let reranker = void 0;
926
+ if (embeddingProviderValue === "custom") {
862
927
  embeddingProvider = "custom";
863
928
  const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
864
- if (rawCustom && typeof rawCustom.baseUrl === "string" && rawCustom.baseUrl.trim().length > 0 && typeof rawCustom.model === "string" && rawCustom.model.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
929
+ const baseUrlValue = getResolvedString(rawCustom?.baseUrl, "$root.customProvider.baseUrl");
930
+ const modelValue = getResolvedString(rawCustom?.model, "$root.customProvider.model");
931
+ const apiKeyValue = getResolvedString(rawCustom?.apiKey, "$root.customProvider.apiKey");
932
+ if (rawCustom && typeof baseUrlValue === "string" && baseUrlValue.trim().length > 0 && typeof modelValue === "string" && modelValue.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
865
933
  customProvider = {
866
- baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
867
- model: rawCustom.model,
934
+ baseUrl: baseUrlValue.trim().replace(/\/+$/, ""),
935
+ model: modelValue,
868
936
  dimensions: rawCustom.dimensions,
869
- apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
937
+ apiKey: apiKeyValue,
870
938
  maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
871
939
  timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
872
940
  concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
873
- requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
941
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0,
942
+ maxBatchSize: typeof rawCustom.maxBatchSize === "number" ? Math.max(1, Math.floor(rawCustom.maxBatchSize)) : typeof rawCustom.max_batch_size === "number" ? Math.max(1, Math.floor(rawCustom.max_batch_size)) : void 0
874
943
  };
875
944
  if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
876
945
  console.warn(
@@ -882,24 +951,60 @@ function parseConfig(raw) {
882
951
  "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
883
952
  );
884
953
  }
885
- } else if (isValidProvider(input.embeddingProvider)) {
886
- embeddingProvider = input.embeddingProvider;
887
- if (input.embeddingModel) {
888
- embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
954
+ } else if (isValidProvider(embeddingProviderValue)) {
955
+ embeddingProvider = embeddingProviderValue;
956
+ const rawEmbeddingModel = input.embeddingModel;
957
+ if (typeof rawEmbeddingModel === "string") {
958
+ const embeddingModelValue = substituteEnvString(rawEmbeddingModel, "$root.embeddingModel");
959
+ if (embeddingModelValue) {
960
+ embeddingModel = isValidModel(embeddingModelValue, embeddingProvider) ? embeddingModelValue : DEFAULT_PROVIDER_MODELS[embeddingProvider];
961
+ }
962
+ } else if (rawEmbeddingModel) {
963
+ embeddingModel = DEFAULT_PROVIDER_MODELS[embeddingProvider];
889
964
  }
890
965
  } else {
891
966
  embeddingProvider = "auto";
892
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
+ }
893
995
  return {
894
996
  embeddingProvider,
895
997
  embeddingModel,
896
998
  customProvider,
897
- scope: isValidScope(input.scope) ? input.scope : "project",
898
- include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
899
- exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
999
+ scope: isValidScope(scopeValue) ? scopeValue : "project",
1000
+ include: includeValue ?? DEFAULT_INCLUDE,
1001
+ exclude: excludeValue ?? DEFAULT_EXCLUDE,
1002
+ additionalInclude,
900
1003
  indexing,
901
1004
  search,
902
- debug
1005
+ debug,
1006
+ reranker,
1007
+ knowledgeBases
903
1008
  };
904
1009
  }
905
1010
  function getDefaultModelForProvider(provider) {
@@ -933,6 +1038,8 @@ function compareSummaries(current, baseline, againstPath) {
933
1038
  hitAt10: metricDelta(current.metrics.hitAt10, baseline.metrics.hitAt10),
934
1039
  mrrAt10: metricDelta(current.metrics.mrrAt10, baseline.metrics.mrrAt10),
935
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),
936
1043
  latencyP50Ms: metricDelta(current.metrics.latencyMs.p50, baseline.metrics.latencyMs.p50),
937
1044
  latencyP95Ms: metricDelta(current.metrics.latencyMs.p95, baseline.metrics.latencyMs.p95),
938
1045
  latencyP99Ms: metricDelta(current.metrics.latencyMs.p99, baseline.metrics.latencyMs.p99),
@@ -951,6 +1058,35 @@ function compareSummaries(current, baseline, againstPath) {
951
1058
  // src/eval/reports.ts
952
1059
  var import_fs = require("fs");
953
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
+ }
954
1090
  function formatPct(value) {
955
1091
  return `${(value * 100).toFixed(2)}%`;
956
1092
  }
@@ -964,9 +1100,9 @@ function signed(value, digits = 4) {
964
1100
  const formatted = value.toFixed(digits);
965
1101
  return value > 0 ? `+${formatted}` : formatted;
966
1102
  }
967
- function loadSummary(summaryPath) {
1103
+ function loadSummary(summaryPath, options) {
968
1104
  const raw = (0, import_fs.readFileSync)(summaryPath, "utf-8");
969
- return JSON.parse(raw);
1105
+ return validateSummary(JSON.parse(raw), summaryPath, options);
970
1106
  }
971
1107
  function createRunDirectory(outputRoot, timestampOverride) {
972
1108
  const timestamp = (timestampOverride ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
@@ -1001,6 +1137,8 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1001
1137
  lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
1002
1138
  lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
1003
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)} |`);
1004
1142
  lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
1005
1143
  lines.push(`| Latency p95 | ${formatMs(summary.metrics.latencyMs.p95)} |`);
1006
1144
  lines.push(`| Latency p99 | ${formatMs(summary.metrics.latencyMs.p99)} |`);
@@ -1041,6 +1179,12 @@ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1041
1179
  lines.push(
1042
1180
  `| nDCG@10 | ${comparison.deltas.ndcgAt10.baseline.toFixed(4)} | ${comparison.deltas.ndcgAt10.current.toFixed(4)} | ${signed(comparison.deltas.ndcgAt10.absolute)} |`
1043
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
+ );
1044
1188
  lines.push(
1045
1189
  `| p95 latency (ms) | ${comparison.deltas.latencyP95Ms.baseline.toFixed(3)} | ${comparison.deltas.latencyP95Ms.current.toFixed(3)} | ${signed(comparison.deltas.latencyP95Ms.absolute, 3)} |`
1046
1190
  );
@@ -1124,7 +1268,7 @@ function pTimeout(promise, options) {
1124
1268
  } = options;
1125
1269
  let timer;
1126
1270
  let abortHandler;
1127
- const wrappedPromise = new Promise((resolve5, reject) => {
1271
+ const wrappedPromise = new Promise((resolve6, reject) => {
1128
1272
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
1129
1273
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
1130
1274
  }
@@ -1138,7 +1282,7 @@ function pTimeout(promise, options) {
1138
1282
  };
1139
1283
  signal.addEventListener("abort", abortHandler, { once: true });
1140
1284
  }
1141
- promise.then(resolve5, reject);
1285
+ promise.then(resolve6, reject);
1142
1286
  if (milliseconds === Number.POSITIVE_INFINITY) {
1143
1287
  return;
1144
1288
  }
@@ -1146,7 +1290,7 @@ function pTimeout(promise, options) {
1146
1290
  timer = customTimers.setTimeout.call(void 0, () => {
1147
1291
  if (fallback) {
1148
1292
  try {
1149
- resolve5(fallback());
1293
+ resolve6(fallback());
1150
1294
  } catch (error) {
1151
1295
  reject(error);
1152
1296
  }
@@ -1156,7 +1300,7 @@ function pTimeout(promise, options) {
1156
1300
  promise.cancel();
1157
1301
  }
1158
1302
  if (message === false) {
1159
- resolve5();
1303
+ resolve6();
1160
1304
  } else if (message instanceof Error) {
1161
1305
  reject(message);
1162
1306
  } else {
@@ -1220,6 +1364,17 @@ var PriorityQueue = class {
1220
1364
  const [item] = this.#queue.splice(index, 1);
1221
1365
  this.enqueue(item.run, { priority, id });
1222
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
+ }
1223
1378
  dequeue() {
1224
1379
  const item = this.#queue.shift();
1225
1380
  return item?.run;
@@ -1259,6 +1414,7 @@ var PQueue = class extends import_index.default {
1259
1414
  #idAssigner = 1n;
1260
1415
  // Track currently running tasks for debugging
1261
1416
  #runningTasks = /* @__PURE__ */ new Map();
1417
+ #queueAbortListenerCleanupFunctions = /* @__PURE__ */ new Set();
1262
1418
  /**
1263
1419
  Get or set the default timeout for all tasks. Can be changed at runtime.
1264
1420
 
@@ -1546,9 +1702,11 @@ var PQueue = class extends import_index.default {
1546
1702
  // Assign unique ID if not provided
1547
1703
  id: options.id ?? (this.#idAssigner++).toString()
1548
1704
  };
1549
- return new Promise((resolve5, reject) => {
1705
+ return new Promise((resolve6, reject) => {
1550
1706
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1551
- this.#queue.enqueue(async () => {
1707
+ let cleanupQueueAbortHandler = () => void 0;
1708
+ const run = async () => {
1709
+ cleanupQueueAbortHandler();
1552
1710
  this.#pending++;
1553
1711
  this.#runningTasks.set(taskSymbol, {
1554
1712
  id: options.id,
@@ -1584,7 +1742,7 @@ var PQueue = class extends import_index.default {
1584
1742
  })]);
1585
1743
  }
1586
1744
  const result = await operation;
1587
- resolve5(result);
1745
+ resolve6(result);
1588
1746
  this.emit("completed", result);
1589
1747
  } catch (error) {
1590
1748
  reject(error);
@@ -1598,7 +1756,35 @@ var PQueue = class extends import_index.default {
1598
1756
  this.#next();
1599
1757
  });
1600
1758
  }
1601
- }, 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
+ }
1602
1788
  this.emit("add");
1603
1789
  this.#tryToStartAnother();
1604
1790
  });
@@ -1627,6 +1813,9 @@ var PQueue = class extends import_index.default {
1627
1813
  Clear the queue.
1628
1814
  */
1629
1815
  clear() {
1816
+ for (const cleanupQueueAbortHandler of this.#queueAbortListenerCleanupFunctions) {
1817
+ cleanupQueueAbortHandler();
1818
+ }
1630
1819
  this.#queue = new this.#queueClass();
1631
1820
  this.#clearIntervalTimer();
1632
1821
  this.#updateRateLimitState();
@@ -1741,13 +1930,13 @@ var PQueue = class extends import_index.default {
1741
1930
  });
1742
1931
  }
1743
1932
  async #onEvent(event, filter) {
1744
- return new Promise((resolve5) => {
1933
+ return new Promise((resolve6) => {
1745
1934
  const listener = () => {
1746
1935
  if (filter && !filter()) {
1747
1936
  return;
1748
1937
  }
1749
1938
  this.off(event, listener);
1750
- resolve5();
1939
+ resolve6();
1751
1940
  };
1752
1941
  this.on(event, listener);
1753
1942
  });
@@ -1906,8 +2095,6 @@ var isError = (value) => objectToString.call(value) === "[object Error]";
1906
2095
  var errorMessages = /* @__PURE__ */ new Set([
1907
2096
  "network error",
1908
2097
  // Chrome
1909
- "Failed to fetch",
1910
- // Chrome
1911
2098
  "NetworkError when attempting to fetch resource.",
1912
2099
  // Firefox
1913
2100
  "The Internet connection appears to be offline.",
@@ -1935,6 +2122,9 @@ function isNetworkError(error) {
1935
2122
  if (message.startsWith("error sending request for url")) {
1936
2123
  return true;
1937
2124
  }
2125
+ if (message === "Failed to fetch" || message.startsWith("Failed to fetch (") && message.endsWith(")")) {
2126
+ return true;
2127
+ }
1938
2128
  return errorMessages.has(message);
1939
2129
  }
1940
2130
 
@@ -2032,7 +2222,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2032
2222
  const finalDelay = Math.min(delayTime, remainingTime);
2033
2223
  options.signal?.throwIfAborted();
2034
2224
  if (finalDelay > 0) {
2035
- await new Promise((resolve5, reject) => {
2225
+ await new Promise((resolve6, reject) => {
2036
2226
  const onAbort = () => {
2037
2227
  clearTimeout(timeoutToken);
2038
2228
  options.signal?.removeEventListener("abort", onAbort);
@@ -2040,7 +2230,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2040
2230
  };
2041
2231
  const timeoutToken = setTimeout(() => {
2042
2232
  options.signal?.removeEventListener("abort", onAbort);
2043
- resolve5();
2233
+ resolve6();
2044
2234
  }, finalDelay);
2045
2235
  if (options.unref) {
2046
2236
  timeoutToken.unref?.();
@@ -2268,7 +2458,8 @@ function createCustomProviderInfo(config) {
2268
2458
  dimensions: config.dimensions,
2269
2459
  maxTokens: config.maxTokens ?? 8192,
2270
2460
  costPer1MTokens: 0,
2271
- timeoutMs: config.timeoutMs ?? 3e4
2461
+ timeoutMs: config.timeoutMs ?? 3e4,
2462
+ maxBatchSize: config.maxBatchSize
2272
2463
  }
2273
2464
  };
2274
2465
  }
@@ -2531,21 +2722,24 @@ var CustomEmbeddingProvider = class {
2531
2722
  this.credentials = credentials;
2532
2723
  this.modelInfo = modelInfo;
2533
2724
  }
2534
- async embedQuery(query) {
2535
- const result = await this.embedBatch([query]);
2536
- return {
2537
- embedding: result.embeddings[0],
2538
- tokensUsed: result.totalTokensUsed
2539
- };
2540
- }
2541
- async embedDocument(document) {
2542
- const result = await this.embedBatch([document]);
2543
- return {
2544
- embedding: result.embeddings[0],
2545
- tokensUsed: result.totalTokensUsed
2546
- };
2725
+ splitIntoRequestBatches(texts) {
2726
+ const maxBatchSize = this.modelInfo.maxBatchSize;
2727
+ if (!maxBatchSize || texts.length <= maxBatchSize) {
2728
+ return [texts];
2729
+ }
2730
+ const batches = [];
2731
+ for (let i = 0; i < texts.length; i += maxBatchSize) {
2732
+ batches.push(texts.slice(i, i + maxBatchSize));
2733
+ }
2734
+ return batches;
2547
2735
  }
2548
- async embedBatch(texts) {
2736
+ async embedRequest(texts) {
2737
+ if (texts.length === 0) {
2738
+ return {
2739
+ embeddings: [],
2740
+ totalTokensUsed: 0
2741
+ };
2742
+ }
2549
2743
  const headers = {
2550
2744
  "Content-Type": "application/json"
2551
2745
  };
@@ -2606,11 +2800,115 @@ var CustomEmbeddingProvider = class {
2606
2800
  }
2607
2801
  throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2608
2802
  }
2803
+ async embedQuery(query) {
2804
+ const result = await this.embedBatch([query]);
2805
+ return {
2806
+ embedding: result.embeddings[0],
2807
+ tokensUsed: result.totalTokensUsed
2808
+ };
2809
+ }
2810
+ async embedDocument(document) {
2811
+ const result = await this.embedBatch([document]);
2812
+ return {
2813
+ embedding: result.embeddings[0],
2814
+ tokensUsed: result.totalTokensUsed
2815
+ };
2816
+ }
2817
+ async embedBatch(texts) {
2818
+ const requestBatches = this.splitIntoRequestBatches(texts);
2819
+ const embeddings = [];
2820
+ let totalTokensUsed = 0;
2821
+ for (const batch of requestBatches) {
2822
+ const result = await this.embedRequest(batch);
2823
+ embeddings.push(...result.embeddings);
2824
+ totalTokensUsed += result.totalTokensUsed;
2825
+ }
2826
+ return {
2827
+ embeddings,
2828
+ totalTokensUsed
2829
+ };
2830
+ }
2609
2831
  getModelInfo() {
2610
2832
  return this.modelInfo;
2611
2833
  }
2612
2834
  };
2613
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
+
2614
2912
  // src/utils/files.ts
2615
2913
  var import_ignore = __toESM(require_ignore(), 1);
2616
2914
  var import_fs3 = require("fs");
@@ -2628,7 +2926,11 @@ function createIgnoreFilter(projectRoot) {
2628
2926
  "__pycache__",
2629
2927
  "target",
2630
2928
  "vendor",
2631
- ".opencode"
2929
+ ".opencode",
2930
+ ".*",
2931
+ "**/.*",
2932
+ "**/.*/**",
2933
+ "**/*build*/**"
2632
2934
  ];
2633
2935
  ig.add(defaultIgnores);
2634
2936
  const gitignorePath = path3.join(projectRoot, ".gitignore");
@@ -2639,18 +2941,37 @@ function createIgnoreFilter(projectRoot) {
2639
2941
  return ig;
2640
2942
  }
2641
2943
  function matchGlob(filePath, pattern) {
2642
- 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("|")})`);
2643
2952
  if (regexPattern.startsWith(".*/")) {
2644
2953
  regexPattern = `(.*\\/)?${regexPattern.slice(3)}`;
2645
2954
  }
2646
2955
  const regex = new RegExp(`^${regexPattern}$`);
2647
2956
  return regex.test(filePath);
2648
2957
  }
2649
- async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
2958
+ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped, options, currentDepth = 0) {
2650
2959
  const entries = await import_fs3.promises.readdir(dir, { withFileTypes: true });
2960
+ const filesInDir = [];
2961
+ const subdirs = [];
2651
2962
  for (const entry of entries) {
2652
2963
  const fullPath = path3.join(dir, entry.name);
2653
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
+ }
2654
2975
  if (ignoreFilter.ignores(relativePath)) {
2655
2976
  if (entry.isFile()) {
2656
2977
  skipped.push({ path: relativePath, reason: "gitignore" });
@@ -2658,15 +2979,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2658
2979
  continue;
2659
2980
  }
2660
2981
  if (entry.isDirectory()) {
2661
- yield* walkDirectory(
2662
- fullPath,
2663
- projectRoot,
2664
- includePatterns,
2665
- excludePatterns,
2666
- ignoreFilter,
2667
- maxFileSize,
2668
- skipped
2669
- );
2982
+ subdirs.push({ fullPath, relativePath });
2670
2983
  } else if (entry.isFile()) {
2671
2984
  const stat = await import_fs3.promises.stat(fullPath);
2672
2985
  if (stat.size > maxFileSize) {
@@ -2687,12 +3000,37 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2687
3000
  }
2688
3001
  }
2689
3002
  if (matched) {
2690
- yield { path: fullPath, size: stat.size };
3003
+ filesInDir.push({ path: fullPath, size: stat.size });
2691
3004
  }
2692
3005
  }
2693
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
+ }
2694
3031
  }
2695
- 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 };
2696
3034
  const ignoreFilter = createIgnoreFilter(projectRoot);
2697
3035
  const files = [];
2698
3036
  const skipped = [];
@@ -2703,10 +3041,46 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
2703
3041
  excludePatterns,
2704
3042
  ignoreFilter,
2705
3043
  maxFileSize,
2706
- skipped
3044
+ skipped,
3045
+ opts,
3046
+ 0
2707
3047
  )) {
2708
3048
  files.push(file);
2709
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
+ }
2710
3084
  return { files, skipped };
2711
3085
  }
2712
3086
 
@@ -2995,7 +3369,6 @@ var Logger = class {
2995
3369
  formatMetrics() {
2996
3370
  const m = this.metrics;
2997
3371
  const lines = [];
2998
- lines.push("=== Metrics ===");
2999
3372
  if (m.indexingStartTime && m.indexingEndTime) {
3000
3373
  const duration = m.indexingEndTime - m.indexingStartTime;
3001
3374
  lines.push(`Indexing duration: ${(duration / 1e3).toFixed(2)}s`);
@@ -3108,7 +3481,52 @@ function getNativeBinding() {
3108
3481
  const require2 = module2.createRequire(requireTarget);
3109
3482
  return require2(nativePath);
3110
3483
  }
3111
- 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
+ }
3112
3530
  function parseFiles(files) {
3113
3531
  const result = native.parseFiles(files);
3114
3532
  return result.map((f) => ({
@@ -3934,6 +4352,32 @@ function isLikelyImplementationPath(filePath) {
3934
4352
  }
3935
4353
  return true;
3936
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
+ }
3937
4381
  function classifyQueryIntent(tokens) {
3938
4382
  const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
3939
4383
  const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
@@ -4308,8 +4752,74 @@ function rerankResults(query, candidates, rerankTopN, options) {
4308
4752
  return 0;
4309
4753
  });
4310
4754
  }
4755
+ const shouldDiversify = !(preferSourcePaths && identifierHints.length > 0);
4756
+ const diversifiedHead = diversifyEntriesByFileAndSymbol(head, (entry) => entry.candidate, shouldDiversify);
4311
4757
  const tail = candidates.slice(topN);
4312
- 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;
4313
4823
  }
4314
4824
  function rankHybridResults(query, semanticResults, keywordResults, options) {
4315
4825
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
@@ -4637,6 +5147,7 @@ var Indexer = class {
4637
5147
  database = null;
4638
5148
  provider = null;
4639
5149
  configuredProviderInfo = null;
5150
+ reranker = null;
4640
5151
  fileHashCache = /* @__PURE__ */ new Map();
4641
5152
  fileHashCachePath = "";
4642
5153
  failedBatchesPath = "";
@@ -4766,6 +5277,152 @@ var Indexer = class {
4766
5277
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
4767
5278
  }
4768
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
+ }
4769
5426
  async initialize() {
4770
5427
  if (this.config.embeddingProvider === "custom") {
4771
5428
  if (!this.config.customProvider) {
@@ -4785,9 +5442,19 @@ var Indexer = class {
4785
5442
  this.logger.info("Initializing indexer", {
4786
5443
  provider: this.configuredProviderInfo.provider,
4787
5444
  model: this.configuredProviderInfo.modelInfo.model,
4788
- scope: this.config.scope
5445
+ scope: this.config.scope,
5446
+ rerankerEnabled: this.config.reranker?.enabled ?? false
4789
5447
  });
4790
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
+ }
4791
5458
  await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
4792
5459
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
4793
5460
  const storePath = path6.join(this.indexPath, "vectors");
@@ -4977,11 +5644,14 @@ var Indexer = class {
4977
5644
  }
4978
5645
  async estimateCost() {
4979
5646
  const { configuredProviderInfo } = await this.ensureInitialized();
5647
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
4980
5648
  const { files } = await collectFiles(
4981
5649
  this.projectRoot,
4982
- this.config.include,
5650
+ includePatterns,
4983
5651
  this.config.exclude,
4984
- this.config.indexing.maxFileSize
5652
+ this.config.indexing.maxFileSize,
5653
+ this.config.knowledgeBases,
5654
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
4985
5655
  );
4986
5656
  return createCostEstimate(files, configuredProviderInfo);
4987
5657
  }
@@ -5016,11 +5686,14 @@ var Indexer = class {
5016
5686
  totalChunks: 0
5017
5687
  });
5018
5688
  this.loadFileHashCache();
5689
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5019
5690
  const { files, skipped } = await collectFiles(
5020
5691
  this.projectRoot,
5021
- this.config.include,
5692
+ includePatterns,
5022
5693
  this.config.exclude,
5023
- this.config.indexing.maxFileSize
5694
+ this.config.indexing.maxFileSize,
5695
+ this.config.knowledgeBases,
5696
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5024
5697
  );
5025
5698
  stats.totalFiles = files.length;
5026
5699
  stats.skippedFiles = skipped;
@@ -5089,7 +5762,15 @@ var Indexer = class {
5089
5762
  stats.parseFailures.push(relativePath);
5090
5763
  }
5091
5764
  let fileChunkCount = 0;
5092
- 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) {
5093
5774
  if (fileChunkCount >= this.config.indexing.maxChunksPerFile) {
5094
5775
  break;
5095
5776
  }
@@ -5296,7 +5977,7 @@ var Indexer = class {
5296
5977
  for (const batch of dynamicBatches) {
5297
5978
  queue.add(async () => {
5298
5979
  if (rateLimitBackoffMs > 0) {
5299
- await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
5980
+ await new Promise((resolve6) => setTimeout(resolve6, rateLimitBackoffMs));
5300
5981
  }
5301
5982
  try {
5302
5983
  const result = await pRetry(
@@ -5501,6 +6182,7 @@ var Indexer = class {
5501
6182
  const rerankTopN = this.config.search.rerankTopN;
5502
6183
  const filterByBranch = options?.filterByBranch ?? true;
5503
6184
  const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
6185
+ const identifierHints = extractIdentifierHints(query);
5504
6186
  this.logger.search("debug", "Starting search", {
5505
6187
  query,
5506
6188
  maxResults,
@@ -5555,10 +6237,14 @@ var Indexer = class {
5555
6237
  hybridWeight,
5556
6238
  prioritizeSourcePaths: sourceIntent
5557
6239
  });
6240
+ const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
6241
+ definitionIntent: options?.definitionIntent === true,
6242
+ hasIdentifierHints: identifierHints.length > 0
6243
+ });
5558
6244
  const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
5559
6245
  const rescued = promoteIdentifierMatches(
5560
6246
  query,
5561
- combined,
6247
+ rerankedCombined,
5562
6248
  semanticCandidates,
5563
6249
  keywordCandidates,
5564
6250
  database,
@@ -5589,7 +6275,7 @@ var Indexer = class {
5589
6275
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5590
6276
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5591
6277
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5592
- const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
6278
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
5593
6279
  const baseFiltered = tiered.filter((r) => {
5594
6280
  if (r.score < this.config.search.minScore) return false;
5595
6281
  if (options?.fileType) {
@@ -5609,6 +6295,7 @@ var Indexer = class {
5609
6295
  (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5610
6296
  );
5611
6297
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
6298
+ const finalResults = filtered;
5612
6299
  const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
5613
6300
  this.logger.recordSearch(totalSearchMs, {
5614
6301
  embeddingMs,
@@ -5618,7 +6305,7 @@ var Indexer = class {
5618
6305
  });
5619
6306
  this.logger.search("info", "Search complete", {
5620
6307
  query,
5621
- results: filtered.length,
6308
+ results: finalResults.length,
5622
6309
  totalMs: Math.round(totalSearchMs * 100) / 100,
5623
6310
  embeddingMs: Math.round(embeddingMs * 100) / 100,
5624
6311
  vectorMs: Math.round(vectorMs * 100) / 100,
@@ -5628,7 +6315,7 @@ var Indexer = class {
5628
6315
  });
5629
6316
  const metadataOnly = options?.metadataOnly ?? false;
5630
6317
  return Promise.all(
5631
- filtered.map(async (r) => {
6318
+ finalResults.map(async (r) => {
5632
6319
  let content = "";
5633
6320
  let contextStartLine = r.metadata.startLine;
5634
6321
  let contextEndLine = r.metadata.endLine;
@@ -5962,6 +6649,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
5962
6649
  message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
5963
6650
  });
5964
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
+ }
5965
6658
  if (comparison) {
5966
6659
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
5967
6660
  violations.push({
@@ -5975,6 +6668,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
5975
6668
  message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
5976
6669
  });
5977
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
+ }
5978
6677
  if (thresholds.p95LatencyMaxMultiplier !== void 0) {
5979
6678
  const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
5980
6679
  if (baselineP95 > BASELINE_P95_EPSILON_MS) {
@@ -6029,6 +6728,12 @@ function uniqueResultsByPath(results) {
6029
6728
  }
6030
6729
  return unique;
6031
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
+ }
6032
6737
  function pathMatchesExpected(actualPath, expectedPath) {
6033
6738
  const actual = normalizePath(actualPath);
6034
6739
  const expected = normalizePath(expectedPath);
@@ -6107,6 +6812,7 @@ function buildPerQueryResult(query, results, latencyMs, k) {
6107
6812
  reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
6108
6813
  ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
6109
6814
  failureBucket: classifyFailureBucket(query, results, k),
6815
+ rawTop3DistinctRatio: distinctTopKRatio(results, 3),
6110
6816
  results: deduped
6111
6817
  };
6112
6818
  return perQuery;
@@ -6120,7 +6826,9 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6120
6826
  hitAt5: 0,
6121
6827
  hitAt10: 0,
6122
6828
  mrrAt10: 0,
6123
- ndcgAt10: 0
6829
+ ndcgAt10: 0,
6830
+ distinctTop3Ratio: 0,
6831
+ rawDistinctTop3Ratio: 0
6124
6832
  };
6125
6833
  const failureBuckets = {
6126
6834
  "wrong-file": 0,
@@ -6136,6 +6844,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6136
6844
  if (query.hitAt10) sum.hitAt10 += 1;
6137
6845
  sum.mrrAt10 += query.reciprocalRankAt10;
6138
6846
  sum.ndcgAt10 += query.ndcgAt10;
6847
+ sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
6848
+ sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
6139
6849
  if (query.failureBucket) {
6140
6850
  failureBuckets[query.failureBucket] += 1;
6141
6851
  }
@@ -6148,6 +6858,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6148
6858
  hitAt10: safeDiv(sum.hitAt10),
6149
6859
  mrrAt10: safeDiv(sum.mrrAt10),
6150
6860
  ndcgAt10: safeDiv(sum.ndcgAt10),
6861
+ distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
6862
+ rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
6151
6863
  latencyMs: {
6152
6864
  p50: percentile(latencies, 0.5),
6153
6865
  p95: percentile(latencies, 0.95),
@@ -6178,23 +6890,23 @@ function isRecord(value) {
6178
6890
  function isStringArray2(value) {
6179
6891
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6180
6892
  }
6181
- function asPositiveNumber(value, path10) {
6893
+ function asPositiveNumber(value, path11) {
6182
6894
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6183
- throw new Error(`${path10} must be a non-negative number`);
6895
+ throw new Error(`${path11} must be a non-negative number`);
6184
6896
  }
6185
6897
  return value;
6186
6898
  }
6187
- function parseQueryType(value, path10) {
6899
+ function parseQueryType(value, path11) {
6188
6900
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6189
6901
  return value;
6190
6902
  }
6191
6903
  throw new Error(
6192
- `${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6904
+ `${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6193
6905
  );
6194
6906
  }
6195
- function parseExpected(input, path10) {
6907
+ function parseExpected(input, path11) {
6196
6908
  if (!isRecord(input)) {
6197
- throw new Error(`${path10} must be an object`);
6909
+ throw new Error(`${path11} must be an object`);
6198
6910
  }
6199
6911
  const filePathRaw = input.filePath;
6200
6912
  const acceptableFilesRaw = input.acceptableFiles;
@@ -6203,16 +6915,16 @@ function parseExpected(input, path10) {
6203
6915
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6204
6916
  const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6205
6917
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6206
- 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`);
6207
6919
  }
6208
6920
  if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6209
- throw new Error(`${path10}.acceptableFiles must be an array of strings`);
6921
+ throw new Error(`${path11}.acceptableFiles must be an array of strings`);
6210
6922
  }
6211
6923
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6212
- throw new Error(`${path10}.symbol must be a string when provided`);
6924
+ throw new Error(`${path11}.symbol must be a string when provided`);
6213
6925
  }
6214
6926
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6215
- throw new Error(`${path10}.branch must be a string when provided`);
6927
+ throw new Error(`${path11}.branch must be a string when provided`);
6216
6928
  }
6217
6929
  return {
6218
6930
  filePath,
@@ -6222,25 +6934,25 @@ function parseExpected(input, path10) {
6222
6934
  };
6223
6935
  }
6224
6936
  function parseQuery(input, index) {
6225
- const path10 = `queries[${index}]`;
6937
+ const path11 = `queries[${index}]`;
6226
6938
  if (!isRecord(input)) {
6227
- throw new Error(`${path10} must be an object`);
6939
+ throw new Error(`${path11} must be an object`);
6228
6940
  }
6229
6941
  const id = input.id;
6230
6942
  const query = input.query;
6231
6943
  const queryType = input.queryType;
6232
6944
  const expected = input.expected;
6233
6945
  if (typeof id !== "string" || id.trim().length === 0) {
6234
- throw new Error(`${path10}.id must be a non-empty string`);
6946
+ throw new Error(`${path11}.id must be a non-empty string`);
6235
6947
  }
6236
6948
  if (typeof query !== "string" || query.trim().length === 0) {
6237
- throw new Error(`${path10}.query must be a non-empty string`);
6949
+ throw new Error(`${path11}.query must be a non-empty string`);
6238
6950
  }
6239
6951
  return {
6240
6952
  id,
6241
6953
  query,
6242
- queryType: parseQueryType(queryType, `${path10}.queryType`),
6243
- expected: parseExpected(expected, `${path10}.expected`)
6954
+ queryType: parseQueryType(queryType, `${path11}.queryType`),
6955
+ expected: parseExpected(expected, `${path11}.expected`)
6244
6956
  };
6245
6957
  }
6246
6958
  function parseGoldenDataset(raw, sourceLabel) {
@@ -6309,6 +7021,10 @@ function parseBudget(raw, sourceLabel) {
6309
7021
  thresholds: {
6310
7022
  hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
6311
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
+ ),
6312
7028
  p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
6313
7029
  thresholds.p95LatencyMaxMultiplier,
6314
7030
  `${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
@@ -6318,7 +7034,11 @@ function parseBudget(raw, sourceLabel) {
6318
7034
  `${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
6319
7035
  ),
6320
7036
  minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
6321
- 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
+ )
6322
7042
  }
6323
7043
  };
6324
7044
  }
@@ -6812,8 +7532,12 @@ async function handleEvalCommand(args, cwd) {
6812
7532
  if (!parsed.againstPath.endsWith(".json")) {
6813
7533
  throw new Error("eval diff --against must point to a summary JSON file");
6814
7534
  }
6815
- const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
6816
- 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
+ });
6817
7541
  const comparison = compareSummaries(
6818
7542
  currentSummary,
6819
7543
  baselineSummary,
@@ -6847,16 +7571,13 @@ function formatDefinitionLookup(results, query) {
6847
7571
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
6848
7572
  }
6849
7573
  const formatted = results.map((r, idx) => {
6850
- 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}`;
6851
- 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)})
6852
7576
  \`\`\`
6853
7577
  ${truncateContent(r.content)}
6854
7578
  \`\`\``;
6855
7579
  });
6856
- const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
6857
- return `${header}
6858
-
6859
- ${formatted.join("\n\n")}`;
7580
+ return formatted.join("\n\n");
6860
7581
  }
6861
7582
 
6862
7583
  // src/mcp-server.ts
@@ -7310,7 +8031,10 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
7310
8031
  return server;
7311
8032
  }
7312
8033
 
7313
- // 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);
7314
8038
  function loadJsonFile(filePath) {
7315
8039
  try {
7316
8040
  if ((0, import_fs10.existsSync)(filePath)) {
@@ -7321,25 +8045,101 @@ function loadJsonFile(filePath) {
7321
8045
  }
7322
8046
  return null;
7323
8047
  }
7324
- function loadPluginConfig(projectRoot, configPath) {
7325
- if (configPath) {
7326
- const config = loadJsonFile(configPath);
7327
- 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
+ }
7328
8120
  }
7329
- const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
7330
- if (projectConfig) return projectConfig;
7331
- const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
7332
- if (globalConfig) return globalConfig;
7333
- 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;
7334
8132
  }
8133
+
8134
+ // src/cli.ts
7335
8135
  function parseArgs(argv) {
7336
8136
  let project = process.cwd();
7337
8137
  let config;
7338
8138
  for (let i = 2; i < argv.length; i++) {
7339
8139
  if (argv[i] === "--project" && argv[i + 1]) {
7340
- project = path9.resolve(argv[++i]);
8140
+ project = path10.resolve(argv[++i]);
7341
8141
  } else if (argv[i] === "--config" && argv[i + 1]) {
7342
- config = path9.resolve(argv[++i]);
8142
+ config = path10.resolve(argv[++i]);
7343
8143
  }
7344
8144
  }
7345
8145
  return { project, config };
@@ -7350,7 +8150,7 @@ async function main() {
7350
8150
  process.exit(exitCode);
7351
8151
  }
7352
8152
  const args = parseArgs(process.argv);
7353
- const rawConfig = loadPluginConfig(args.project, args.config);
8153
+ const rawConfig = loadMergedConfig(args.project);
7354
8154
  const config = parseConfig(rawConfig);
7355
8155
  const server = createMcpServer(args.project, config);
7356
8156
  const transport = new import_stdio.StdioServerTransport();