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.js CHANGED
@@ -487,7 +487,7 @@ var require_ignore = __commonJS({
487
487
  // path matching.
488
488
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
489
489
  // @returns {TestResult} true if a file is ignored
490
- test(path10, checkUnignored, mode) {
490
+ test(path11, checkUnignored, mode) {
491
491
  let ignored = false;
492
492
  let unignored = false;
493
493
  let matchedRule;
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
496
496
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
497
497
  return;
498
498
  }
499
- const matched = rule[mode].test(path10);
499
+ const matched = rule[mode].test(path11);
500
500
  if (!matched) {
501
501
  return;
502
502
  }
@@ -517,17 +517,17 @@ var require_ignore = __commonJS({
517
517
  var throwError = (message, Ctor) => {
518
518
  throw new Ctor(message);
519
519
  };
520
- var checkPath = (path10, originalPath, doThrow) => {
521
- if (!isString(path10)) {
520
+ var checkPath = (path11, originalPath, doThrow) => {
521
+ if (!isString(path11)) {
522
522
  return doThrow(
523
523
  `path must be a string, but got \`${originalPath}\``,
524
524
  TypeError
525
525
  );
526
526
  }
527
- if (!path10) {
527
+ if (!path11) {
528
528
  return doThrow(`path must not be empty`, TypeError);
529
529
  }
530
- if (checkPath.isNotRelative(path10)) {
530
+ if (checkPath.isNotRelative(path11)) {
531
531
  const r = "`path.relative()`d";
532
532
  return doThrow(
533
533
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -536,7 +536,7 @@ var require_ignore = __commonJS({
536
536
  }
537
537
  return true;
538
538
  };
539
- var isNotRelative = (path10) => REGEX_TEST_INVALID_PATH.test(path10);
539
+ var isNotRelative = (path11) => REGEX_TEST_INVALID_PATH.test(path11);
540
540
  checkPath.isNotRelative = isNotRelative;
541
541
  checkPath.convert = (p) => p;
542
542
  var Ignore2 = class {
@@ -566,19 +566,19 @@ var require_ignore = __commonJS({
566
566
  }
567
567
  // @returns {TestResult}
568
568
  _test(originalPath, cache, checkUnignored, slices) {
569
- const path10 = originalPath && checkPath.convert(originalPath);
569
+ const path11 = originalPath && checkPath.convert(originalPath);
570
570
  checkPath(
571
- path10,
571
+ path11,
572
572
  originalPath,
573
573
  this._strictPathCheck ? throwError : RETURN_FALSE
574
574
  );
575
- return this._t(path10, cache, checkUnignored, slices);
575
+ return this._t(path11, cache, checkUnignored, slices);
576
576
  }
577
- checkIgnore(path10) {
578
- if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
579
- return this.test(path10);
577
+ checkIgnore(path11) {
578
+ if (!REGEX_TEST_TRAILING_SLASH.test(path11)) {
579
+ return this.test(path11);
580
580
  }
581
- const slices = path10.split(SLASH).filter(Boolean);
581
+ const slices = path11.split(SLASH).filter(Boolean);
582
582
  slices.pop();
583
583
  if (slices.length) {
584
584
  const parent = this._t(
@@ -591,18 +591,18 @@ var require_ignore = __commonJS({
591
591
  return parent;
592
592
  }
593
593
  }
594
- return this._rules.test(path10, false, MODE_CHECK_IGNORE);
594
+ return this._rules.test(path11, false, MODE_CHECK_IGNORE);
595
595
  }
596
- _t(path10, cache, checkUnignored, slices) {
597
- if (path10 in cache) {
598
- return cache[path10];
596
+ _t(path11, cache, checkUnignored, slices) {
597
+ if (path11 in cache) {
598
+ return cache[path11];
599
599
  }
600
600
  if (!slices) {
601
- slices = path10.split(SLASH).filter(Boolean);
601
+ slices = path11.split(SLASH).filter(Boolean);
602
602
  }
603
603
  slices.pop();
604
604
  if (!slices.length) {
605
- return cache[path10] = this._rules.test(path10, checkUnignored, MODE_IGNORE);
605
+ return cache[path11] = this._rules.test(path11, checkUnignored, MODE_IGNORE);
606
606
  }
607
607
  const parent = this._t(
608
608
  slices.join(SLASH) + SLASH,
@@ -610,29 +610,29 @@ var require_ignore = __commonJS({
610
610
  checkUnignored,
611
611
  slices
612
612
  );
613
- return cache[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
613
+ return cache[path11] = parent.ignored ? parent : this._rules.test(path11, checkUnignored, MODE_IGNORE);
614
614
  }
615
- ignores(path10) {
616
- return this._test(path10, this._ignoreCache, false).ignored;
615
+ ignores(path11) {
616
+ return this._test(path11, this._ignoreCache, false).ignored;
617
617
  }
618
618
  createFilter() {
619
- return (path10) => !this.ignores(path10);
619
+ return (path11) => !this.ignores(path11);
620
620
  }
621
621
  filter(paths) {
622
622
  return makeArray(paths).filter(this.createFilter());
623
623
  }
624
624
  // @returns {TestResult}
625
- test(path10) {
626
- return this._test(path10, this._testCache, true);
625
+ test(path11) {
626
+ return this._test(path11, this._testCache, true);
627
627
  }
628
628
  };
629
629
  var factory = (options) => new Ignore2(options);
630
- var isPathValid = (path10) => checkPath(path10 && checkPath.convert(path10), path10, RETURN_FALSE);
630
+ var isPathValid = (path11) => checkPath(path11 && checkPath.convert(path11), path11, RETURN_FALSE);
631
631
  var setupWindows = () => {
632
632
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
633
633
  checkPath.convert = makePosix;
634
634
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
635
- checkPath.isNotRelative = (path10) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path10) || isNotRelative(path10);
635
+ checkPath.isNotRelative = (path11) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path11) || isNotRelative(path11);
636
636
  };
637
637
  if (
638
638
  // Detect `process` so that it can run in browsers.
@@ -649,9 +649,7 @@ var require_ignore = __commonJS({
649
649
 
650
650
  // src/cli.ts
651
651
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
652
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
653
- import * as path9 from "path";
654
- import * as os4 from "os";
652
+ import * as path10 from "path";
655
653
 
656
654
  // src/config/constants.ts
657
655
  var DEFAULT_INCLUDE = [
@@ -664,13 +662,15 @@ var DEFAULT_INCLUDE = [
664
662
  "**/*.{sql,graphql,proto}",
665
663
  "**/*.{yaml,yml,toml}",
666
664
  "**/*.{md,mdx}",
667
- "**/*.{sh,bash,zsh}"
665
+ "**/*.{sh,bash,zsh}",
666
+ "**/*.{txt,html,htm}"
668
667
  ];
669
668
  var DEFAULT_EXCLUDE = [
670
669
  "**/node_modules/**",
671
670
  "**/.git/**",
672
671
  "**/dist/**",
673
672
  "**/build/**",
673
+ "**/*build*/**",
674
674
  "**/*.min.js",
675
675
  "**/*.bundle.js",
676
676
  "**/vendor/**",
@@ -679,7 +679,9 @@ var DEFAULT_EXCLUDE = [
679
679
  "**/coverage/**",
680
680
  "**/.next/**",
681
681
  "**/.nuxt/**",
682
- "**/.opencode/**"
682
+ "**/.opencode/**",
683
+ "**/.*",
684
+ "**/.*/**"
683
685
  ];
684
686
  var EMBEDDING_MODELS = {
685
687
  "google": {
@@ -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
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
953
1060
  import * as path from "path";
1061
+ function assertFiniteNumber(value, path11) {
1062
+ if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1063
+ throw new Error(`${path11} must be a finite number`);
1064
+ }
1065
+ return value;
1066
+ }
1067
+ function validateSummary(summary, summaryPath, options) {
1068
+ assertFiniteNumber(summary.metrics.hitAt1, `${summaryPath}.metrics.hitAt1`);
1069
+ assertFiniteNumber(summary.metrics.hitAt3, `${summaryPath}.metrics.hitAt3`);
1070
+ assertFiniteNumber(summary.metrics.hitAt5, `${summaryPath}.metrics.hitAt5`);
1071
+ assertFiniteNumber(summary.metrics.hitAt10, `${summaryPath}.metrics.hitAt10`);
1072
+ assertFiniteNumber(summary.metrics.mrrAt10, `${summaryPath}.metrics.mrrAt10`);
1073
+ assertFiniteNumber(summary.metrics.ndcgAt10, `${summaryPath}.metrics.ndcgAt10`);
1074
+ const metrics = summary.metrics;
1075
+ if (metrics.distinctTop3Ratio === void 0 && options?.allowLegacyDiversityMetrics) {
1076
+ metrics.distinctTop3Ratio = 0;
1077
+ }
1078
+ if (metrics.rawDistinctTop3Ratio === void 0 && options?.allowLegacyDiversityMetrics) {
1079
+ metrics.rawDistinctTop3Ratio = 0;
1080
+ }
1081
+ assertFiniteNumber(metrics.distinctTop3Ratio, `${summaryPath}.metrics.distinctTop3Ratio`);
1082
+ assertFiniteNumber(metrics.rawDistinctTop3Ratio, `${summaryPath}.metrics.rawDistinctTop3Ratio`);
1083
+ assertFiniteNumber(summary.metrics.latencyMs.p50, `${summaryPath}.metrics.latencyMs.p50`);
1084
+ assertFiniteNumber(summary.metrics.latencyMs.p95, `${summaryPath}.metrics.latencyMs.p95`);
1085
+ assertFiniteNumber(summary.metrics.latencyMs.p99, `${summaryPath}.metrics.latencyMs.p99`);
1086
+ assertFiniteNumber(summary.metrics.embedding.callCount, `${summaryPath}.metrics.embedding.callCount`);
1087
+ assertFiniteNumber(summary.metrics.embedding.estimatedCostUsd, `${summaryPath}.metrics.embedding.estimatedCostUsd`);
1088
+ return summary;
1089
+ }
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 = 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
  import { existsSync as existsSync2, readFileSync as readFileSync3, promises as fsPromises } from "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 fsPromises.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 fsPromises.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 fsPromises.stat(resolvedKbRoot);
3061
+ if (!stat.isDirectory()) {
3062
+ skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3063
+ continue;
3064
+ }
3065
+ const kbIgnoreFilter = createIgnoreFilter(resolvedKbRoot);
3066
+ for await (const file of walkDirectory(
3067
+ resolvedKbRoot,
3068
+ resolvedKbRoot,
3069
+ includePatterns,
3070
+ excludePatterns,
3071
+ kbIgnoreFilter,
3072
+ maxFileSize,
3073
+ skipped,
3074
+ opts,
3075
+ 0
3076
+ )) {
3077
+ files.push(file);
3078
+ }
3079
+ } catch {
3080
+ skipped.push({ path: resolvedKbRoot, reason: "excluded" });
3081
+ }
3082
+ }
3083
+ }
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`);
@@ -3107,7 +3480,52 @@ function getNativeBinding() {
3107
3480
  const require2 = module.createRequire(requireTarget);
3108
3481
  return require2(nativePath);
3109
3482
  }
3110
- var native = getNativeBinding();
3483
+ function createMockNativeBinding() {
3484
+ const error = new Error("Native module not available. Please rebuild with 'npm run build:native'.");
3485
+ return {
3486
+ parseFile: () => {
3487
+ throw error;
3488
+ },
3489
+ parseFiles: () => {
3490
+ throw error;
3491
+ },
3492
+ hashContent: () => {
3493
+ throw error;
3494
+ },
3495
+ hashFile: () => {
3496
+ throw error;
3497
+ },
3498
+ extractCalls: () => {
3499
+ throw error;
3500
+ },
3501
+ VectorStore: class {
3502
+ constructor() {
3503
+ throw error;
3504
+ }
3505
+ },
3506
+ InvertedIndex: class {
3507
+ constructor() {
3508
+ throw error;
3509
+ }
3510
+ },
3511
+ Database: class {
3512
+ constructor() {
3513
+ throw error;
3514
+ }
3515
+ }
3516
+ };
3517
+ }
3518
+ var native;
3519
+ try {
3520
+ native = getNativeBinding();
3521
+ } catch (e) {
3522
+ console.error("[codebase-index] Failed to load native module:", e);
3523
+ native = createMockNativeBinding();
3524
+ }
3525
+ function parseFileAsText(filePath, content) {
3526
+ const result = native.parseFileAsText(filePath, content);
3527
+ return result.map(mapChunk);
3528
+ }
3111
3529
  function parseFiles(files) {
3112
3530
  const result = native.parseFiles(files);
3113
3531
  return result.map((f) => ({
@@ -3933,6 +4351,32 @@ function isLikelyImplementationPath(filePath) {
3933
4351
  }
3934
4352
  return true;
3935
4353
  }
4354
+ function isDocumentationPath(filePath) {
4355
+ const lowered = filePath.toLowerCase();
4356
+ const ext = lowered.split(".").pop() ?? "";
4357
+ return lowered.includes("readme") || ["md", "mdx", "rst", "adoc", "txt"].includes(ext);
4358
+ }
4359
+ function classifyExternalRerankBand(candidate, preferSourcePaths, docIntent) {
4360
+ const isDocOrTest = isTestOrDocPath(candidate.metadata.filePath);
4361
+ const isDocumentation = isDocumentationPath(candidate.metadata.filePath);
4362
+ const isImplementation = isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType);
4363
+ if (preferSourcePaths) {
4364
+ if (isImplementation) return "implementation";
4365
+ if (isDocumentation) return "documentation";
4366
+ if (isDocOrTest) return "test";
4367
+ return "other";
4368
+ }
4369
+ if (docIntent) {
4370
+ if (isDocumentation) return "documentation";
4371
+ if (isImplementation) return "implementation";
4372
+ if (isDocOrTest) return "test";
4373
+ return "other";
4374
+ }
4375
+ if (isImplementation) return "implementation";
4376
+ if (isDocumentation) return "documentation";
4377
+ if (isDocOrTest) return "test";
4378
+ return "other";
4379
+ }
3936
4380
  function classifyQueryIntent(tokens) {
3937
4381
  const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
3938
4382
  const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
@@ -4307,8 +4751,74 @@ function rerankResults(query, candidates, rerankTopN, options) {
4307
4751
  return 0;
4308
4752
  });
4309
4753
  }
4754
+ const shouldDiversify = !(preferSourcePaths && identifierHints.length > 0);
4755
+ const diversifiedHead = diversifyEntriesByFileAndSymbol(head, (entry) => entry.candidate, shouldDiversify);
4310
4756
  const tail = candidates.slice(topN);
4311
- return [...head.map((entry) => entry.candidate), ...tail];
4757
+ return [...diversifiedHead.map((entry) => entry.candidate), ...tail];
4758
+ }
4759
+ function diversifyEntriesByFileAndSymbol(entries, getCandidate, enabled) {
4760
+ if (!enabled || entries.length <= 2) {
4761
+ return entries;
4762
+ }
4763
+ const groups = /* @__PURE__ */ new Map();
4764
+ const groupOrder = [];
4765
+ for (const entry of entries) {
4766
+ const candidate = getCandidate(entry);
4767
+ const filePath = candidate.metadata.filePath;
4768
+ if (!groups.has(filePath)) {
4769
+ groups.set(filePath, []);
4770
+ groupOrder.push(filePath);
4771
+ }
4772
+ groups.get(filePath)?.push(entry);
4773
+ }
4774
+ const diversifiedGroups = groupOrder.map((filePath) => {
4775
+ const group = groups.get(filePath) ?? [];
4776
+ return diversifyGroupBySymbol(group, getCandidate);
4777
+ });
4778
+ const result = [];
4779
+ let added = true;
4780
+ let round = 0;
4781
+ while (added) {
4782
+ added = false;
4783
+ for (const group of diversifiedGroups) {
4784
+ const entry = group[round];
4785
+ if (entry !== void 0) {
4786
+ result.push(entry);
4787
+ added = true;
4788
+ }
4789
+ }
4790
+ round += 1;
4791
+ }
4792
+ return result;
4793
+ }
4794
+ function diversifyCandidatesByFile(candidates, enabled) {
4795
+ return diversifyEntriesByFileAndSymbol(candidates, (candidate) => candidate, enabled);
4796
+ }
4797
+ function diversifyGroupBySymbol(entries, getCandidate) {
4798
+ if (entries.length <= 2) {
4799
+ return entries;
4800
+ }
4801
+ const seenKeys = /* @__PURE__ */ new Set();
4802
+ const primary = [];
4803
+ const remainder = [];
4804
+ for (const entry of entries) {
4805
+ const key = buildDiversityKey(getCandidate(entry).metadata);
4806
+ if (!seenKeys.has(key)) {
4807
+ seenKeys.add(key);
4808
+ primary.push(entry);
4809
+ } else {
4810
+ remainder.push(entry);
4811
+ }
4812
+ }
4813
+ return [...primary, ...remainder];
4814
+ }
4815
+ function buildDiversityKey(metadata) {
4816
+ const normalizedPath = metadata.filePath.toLowerCase();
4817
+ const normalizedName = (metadata.name ?? "").trim().toLowerCase();
4818
+ if (normalizedName.length > 0) {
4819
+ return `${normalizedPath}#${normalizedName}`;
4820
+ }
4821
+ return normalizedPath;
4312
4822
  }
4313
4823
  function rankHybridResults(query, semanticResults, keywordResults, options) {
4314
4824
  const overfetchLimit = Math.max(options.limit * 4, options.limit);
@@ -4636,6 +5146,7 @@ var Indexer = class {
4636
5146
  database = null;
4637
5147
  provider = null;
4638
5148
  configuredProviderInfo = null;
5149
+ reranker = null;
4639
5150
  fileHashCache = /* @__PURE__ */ new Map();
4640
5151
  fileHashCachePath = "";
4641
5152
  failedBatchesPath = "";
@@ -4765,6 +5276,152 @@ var Indexer = class {
4765
5276
  return { concurrency: 3, intervalMs: 1e3, minRetryMs: 1e3, maxRetryMs: 3e4 };
4766
5277
  }
4767
5278
  }
5279
+ async rerankCandidatesWithApi(query, candidates, options) {
5280
+ const reranker = this.config.reranker;
5281
+ if (!reranker || !reranker.enabled || candidates.length <= 1) {
5282
+ return candidates;
5283
+ }
5284
+ const queryTokens = Array.from(tokenizeTextForRanking(query));
5285
+ const preferSourcePaths = classifyQueryIntentRaw(query) === "source";
5286
+ const docIntent = classifyDocIntent(queryTokens) === "docs";
5287
+ if (options?.definitionIntent === true) {
5288
+ return candidates;
5289
+ }
5290
+ if (options?.hasIdentifierHints === true && preferSourcePaths && !docIntent) {
5291
+ return candidates;
5292
+ }
5293
+ const topN = Math.min(reranker.topN, candidates.length);
5294
+ const head = candidates.slice(0, topN);
5295
+ const tail = candidates.slice(topN);
5296
+ const grouped = /* @__PURE__ */ new Map([
5297
+ ["implementation", []],
5298
+ ["documentation", []],
5299
+ ["test", []],
5300
+ ["other", []]
5301
+ ]);
5302
+ for (const candidate of head) {
5303
+ const band = classifyExternalRerankBand(candidate, preferSourcePaths, docIntent);
5304
+ grouped.get(band)?.push(candidate);
5305
+ }
5306
+ const orderedBands = preferSourcePaths ? ["implementation", "other", "documentation", "test"] : docIntent ? ["documentation", "implementation", "other", "test"] : ["implementation", "other", "documentation", "test"];
5307
+ try {
5308
+ const rerankedHead = [];
5309
+ for (const band of orderedBands) {
5310
+ const bandCandidates = grouped.get(band) ?? [];
5311
+ if (bandCandidates.length <= 1) {
5312
+ rerankedHead.push(...bandCandidates);
5313
+ continue;
5314
+ }
5315
+ const documents = await Promise.all(
5316
+ bandCandidates.map(async (candidate) => ({
5317
+ id: candidate.id,
5318
+ text: await this.createRerankerDocumentText(candidate)
5319
+ }))
5320
+ );
5321
+ const rankedIds = await this.callExternalReranker(query, documents, reranker);
5322
+ if (rankedIds.length === 0) {
5323
+ rerankedHead.push(...bandCandidates);
5324
+ continue;
5325
+ }
5326
+ const order = new Map(rankedIds.map((id, index) => [id, index]));
5327
+ const bandReranked = [...bandCandidates].sort((a, b) => {
5328
+ const aRank = order.get(a.id) ?? Number.MAX_SAFE_INTEGER;
5329
+ const bRank = order.get(b.id) ?? Number.MAX_SAFE_INTEGER;
5330
+ if (aRank !== bRank) {
5331
+ return aRank - bRank;
5332
+ }
5333
+ if (b.score !== a.score) {
5334
+ return b.score - a.score;
5335
+ }
5336
+ return a.id.localeCompare(b.id);
5337
+ });
5338
+ const shouldDiversifyBand = !options?.hasIdentifierHints;
5339
+ rerankedHead.push(...diversifyCandidatesByFile(bandReranked, shouldDiversifyBand));
5340
+ }
5341
+ this.logger.search("debug", "Applied external reranker", {
5342
+ provider: reranker.provider,
5343
+ model: reranker.model,
5344
+ candidateCount: head.length,
5345
+ bands: orderedBands
5346
+ });
5347
+ return [...rerankedHead, ...tail];
5348
+ } catch (error) {
5349
+ this.logger.search("warn", "External reranker failed; using deterministic order", {
5350
+ provider: reranker.provider,
5351
+ model: reranker.model,
5352
+ error: getErrorMessage(error)
5353
+ });
5354
+ return candidates;
5355
+ }
5356
+ }
5357
+ async callExternalReranker(query, documents, reranker) {
5358
+ const headers = {
5359
+ "Content-Type": "application/json"
5360
+ };
5361
+ if (reranker.apiKey) {
5362
+ headers.Authorization = `Bearer ${reranker.apiKey}`;
5363
+ }
5364
+ const controller = new AbortController();
5365
+ const timeout = setTimeout(() => controller.abort(), reranker.timeoutMs);
5366
+ try {
5367
+ const response = await fetch(`${reranker.baseUrl}/rerank`, {
5368
+ method: "POST",
5369
+ headers,
5370
+ body: JSON.stringify({
5371
+ model: reranker.model,
5372
+ query,
5373
+ documents: documents.map((document) => document.text),
5374
+ top_n: documents.length,
5375
+ return_documents: false
5376
+ }),
5377
+ signal: controller.signal
5378
+ });
5379
+ if (!response.ok) {
5380
+ throw new Error(`Reranker API error: ${response.status} - ${await response.text()}`);
5381
+ }
5382
+ const body = await response.json();
5383
+ if (!Array.isArray(body.results)) {
5384
+ throw new Error("Reranker API returned unexpected response format.");
5385
+ }
5386
+ return body.results.map((result) => {
5387
+ const index = typeof result.index === "number" ? result.index : -1;
5388
+ return documents[index]?.id;
5389
+ }).filter((id) => typeof id === "string");
5390
+ } catch (error) {
5391
+ if (error instanceof Error && error.name === "AbortError") {
5392
+ throw new Error(`Reranker request timed out after ${reranker.timeoutMs}ms`);
5393
+ }
5394
+ throw error;
5395
+ } finally {
5396
+ clearTimeout(timeout);
5397
+ }
5398
+ }
5399
+ async createRerankerDocumentText(candidate) {
5400
+ const parts = [
5401
+ `path: ${candidate.metadata.filePath}`,
5402
+ `chunk_type: ${candidate.metadata.chunkType}`,
5403
+ `language: ${candidate.metadata.language}`,
5404
+ `lines: ${candidate.metadata.startLine}-${candidate.metadata.endLine}`
5405
+ ];
5406
+ if (candidate.metadata.name) {
5407
+ parts.push(`name: ${candidate.metadata.name}`);
5408
+ }
5409
+ const intent = isLikelyImplementationPath(candidate.metadata.filePath) ? "implementation" : "doc_or_test";
5410
+ parts.push(`intent_hint: ${intent}`);
5411
+ try {
5412
+ const fileContent = await fsPromises2.readFile(candidate.metadata.filePath, "utf-8");
5413
+ const lines = fileContent.split("\n");
5414
+ const snippetStartLine = Math.max(1, candidate.metadata.startLine - 2);
5415
+ const snippetEndLine = Math.min(lines.length, candidate.metadata.endLine + 2);
5416
+ const snippet = lines.slice(snippetStartLine - 1, snippetEndLine).join("\n").trim();
5417
+ parts.push("snippet:");
5418
+ parts.push(snippet.length > 0 ? snippet : "[empty]");
5419
+ } catch {
5420
+ parts.push("snippet:");
5421
+ parts.push("[unavailable]");
5422
+ }
5423
+ return parts.join("\n");
5424
+ }
4768
5425
  async initialize() {
4769
5426
  if (this.config.embeddingProvider === "custom") {
4770
5427
  if (!this.config.customProvider) {
@@ -4784,9 +5441,19 @@ var Indexer = class {
4784
5441
  this.logger.info("Initializing indexer", {
4785
5442
  provider: this.configuredProviderInfo.provider,
4786
5443
  model: this.configuredProviderInfo.modelInfo.model,
4787
- scope: this.config.scope
5444
+ scope: this.config.scope,
5445
+ rerankerEnabled: this.config.reranker?.enabled ?? false
4788
5446
  });
4789
5447
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
5448
+ if (this.config.reranker?.enabled) {
5449
+ this.reranker = createReranker(this.config.reranker);
5450
+ if (this.reranker.isAvailable()) {
5451
+ this.logger.info("Reranker initialized", {
5452
+ model: this.config.reranker.model,
5453
+ baseUrl: this.config.reranker.baseUrl
5454
+ });
5455
+ }
5456
+ }
4790
5457
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
4791
5458
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
4792
5459
  const storePath = path6.join(this.indexPath, "vectors");
@@ -4976,11 +5643,14 @@ var Indexer = class {
4976
5643
  }
4977
5644
  async estimateCost() {
4978
5645
  const { configuredProviderInfo } = await this.ensureInitialized();
5646
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
4979
5647
  const { files } = await collectFiles(
4980
5648
  this.projectRoot,
4981
- this.config.include,
5649
+ includePatterns,
4982
5650
  this.config.exclude,
4983
- this.config.indexing.maxFileSize
5651
+ this.config.indexing.maxFileSize,
5652
+ this.config.knowledgeBases,
5653
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
4984
5654
  );
4985
5655
  return createCostEstimate(files, configuredProviderInfo);
4986
5656
  }
@@ -5015,11 +5685,14 @@ var Indexer = class {
5015
5685
  totalChunks: 0
5016
5686
  });
5017
5687
  this.loadFileHashCache();
5688
+ const includePatterns = [...this.config.include, ...this.config.additionalInclude];
5018
5689
  const { files, skipped } = await collectFiles(
5019
5690
  this.projectRoot,
5020
- this.config.include,
5691
+ includePatterns,
5021
5692
  this.config.exclude,
5022
- this.config.indexing.maxFileSize
5693
+ this.config.indexing.maxFileSize,
5694
+ this.config.knowledgeBases,
5695
+ { maxDepth: this.config.indexing.maxDepth, maxFilesPerDirectory: this.config.indexing.maxFilesPerDirectory }
5023
5696
  );
5024
5697
  stats.totalFiles = files.length;
5025
5698
  stats.skippedFiles = skipped;
@@ -5088,7 +5761,15 @@ var Indexer = class {
5088
5761
  stats.parseFailures.push(relativePath);
5089
5762
  }
5090
5763
  let fileChunkCount = 0;
5091
- for (const chunk of parsed.chunks) {
5764
+ let chunksToProcess = parsed.chunks;
5765
+ if (this.config.indexing.fallbackToTextOnMaxChunks && chunksToProcess.length > this.config.indexing.maxChunksPerFile) {
5766
+ const changedFile = changedFiles.find((f) => f.path === parsed.path);
5767
+ if (changedFile) {
5768
+ const textChunks = parseFileAsText(parsed.path, changedFile.content);
5769
+ chunksToProcess = textChunks;
5770
+ }
5771
+ }
5772
+ for (const chunk of chunksToProcess) {
5092
5773
  if (fileChunkCount >= this.config.indexing.maxChunksPerFile) {
5093
5774
  break;
5094
5775
  }
@@ -5295,7 +5976,7 @@ var Indexer = class {
5295
5976
  for (const batch of dynamicBatches) {
5296
5977
  queue.add(async () => {
5297
5978
  if (rateLimitBackoffMs > 0) {
5298
- await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
5979
+ await new Promise((resolve6) => setTimeout(resolve6, rateLimitBackoffMs));
5299
5980
  }
5300
5981
  try {
5301
5982
  const result = await pRetry(
@@ -5500,6 +6181,7 @@ var Indexer = class {
5500
6181
  const rerankTopN = this.config.search.rerankTopN;
5501
6182
  const filterByBranch = options?.filterByBranch ?? true;
5502
6183
  const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
6184
+ const identifierHints = extractIdentifierHints(query);
5503
6185
  this.logger.search("debug", "Starting search", {
5504
6186
  query,
5505
6187
  maxResults,
@@ -5554,10 +6236,14 @@ var Indexer = class {
5554
6236
  hybridWeight,
5555
6237
  prioritizeSourcePaths: sourceIntent
5556
6238
  });
6239
+ const rerankedCombined = await this.rerankCandidatesWithApi(query, combined, {
6240
+ definitionIntent: options?.definitionIntent === true,
6241
+ hasIdentifierHints: identifierHints.length > 0
6242
+ });
5557
6243
  const fusionMs = performance2.now() - fusionStartTime;
5558
6244
  const rescued = promoteIdentifierMatches(
5559
6245
  query,
5560
- combined,
6246
+ rerankedCombined,
5561
6247
  semanticCandidates,
5562
6248
  keywordCandidates,
5563
6249
  database,
@@ -5588,7 +6274,7 @@ var Indexer = class {
5588
6274
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5589
6275
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5590
6276
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5591
- const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
6277
+ const hasCodeHints = extractCodeTermHints(query).length > 0 || identifierHints.length > 0;
5592
6278
  const baseFiltered = tiered.filter((r) => {
5593
6279
  if (r.score < this.config.search.minScore) return false;
5594
6280
  if (options?.fileType) {
@@ -5608,6 +6294,7 @@ var Indexer = class {
5608
6294
  (r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
5609
6295
  );
5610
6296
  const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
6297
+ const finalResults = filtered;
5611
6298
  const totalSearchMs = performance2.now() - searchStartTime;
5612
6299
  this.logger.recordSearch(totalSearchMs, {
5613
6300
  embeddingMs,
@@ -5617,7 +6304,7 @@ var Indexer = class {
5617
6304
  });
5618
6305
  this.logger.search("info", "Search complete", {
5619
6306
  query,
5620
- results: filtered.length,
6307
+ results: finalResults.length,
5621
6308
  totalMs: Math.round(totalSearchMs * 100) / 100,
5622
6309
  embeddingMs: Math.round(embeddingMs * 100) / 100,
5623
6310
  vectorMs: Math.round(vectorMs * 100) / 100,
@@ -5627,7 +6314,7 @@ var Indexer = class {
5627
6314
  });
5628
6315
  const metadataOnly = options?.metadataOnly ?? false;
5629
6316
  return Promise.all(
5630
- filtered.map(async (r) => {
6317
+ finalResults.map(async (r) => {
5631
6318
  let content = "";
5632
6319
  let contextStartLine = r.metadata.startLine;
5633
6320
  let contextEndLine = r.metadata.endLine;
@@ -5961,6 +6648,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
5961
6648
  message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
5962
6649
  });
5963
6650
  }
6651
+ if (thresholds.minRawDistinctTop3Ratio !== void 0 && summary.metrics.rawDistinctTop3Ratio < thresholds.minRawDistinctTop3Ratio) {
6652
+ violations.push({
6653
+ metric: "minRawDistinctTop3Ratio",
6654
+ message: `Raw Distinct Top@3 ${summary.metrics.rawDistinctTop3Ratio.toFixed(4)} is below minimum ${thresholds.minRawDistinctTop3Ratio.toFixed(4)}`
6655
+ });
6656
+ }
5964
6657
  if (comparison) {
5965
6658
  if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
5966
6659
  violations.push({
@@ -5974,6 +6667,12 @@ function evaluateBudgetGate(budget, summary, comparison) {
5974
6667
  message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
5975
6668
  });
5976
6669
  }
6670
+ if (thresholds.rawDistinctTop3RatioMaxDrop !== void 0 && comparison.deltas.rawDistinctTop3Ratio.absolute < -thresholds.rawDistinctTop3RatioMaxDrop) {
6671
+ violations.push({
6672
+ metric: "rawDistinctTop3RatioMaxDrop",
6673
+ message: `Raw Distinct Top@3 drop ${comparison.deltas.rawDistinctTop3Ratio.absolute.toFixed(4)} exceeds allowed -${thresholds.rawDistinctTop3RatioMaxDrop.toFixed(4)}`
6674
+ });
6675
+ }
5977
6676
  if (thresholds.p95LatencyMaxMultiplier !== void 0) {
5978
6677
  const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
5979
6678
  if (baselineP95 > BASELINE_P95_EPSILON_MS) {
@@ -6028,6 +6727,12 @@ function uniqueResultsByPath(results) {
6028
6727
  }
6029
6728
  return unique;
6030
6729
  }
6730
+ function distinctTopKRatio(results, k) {
6731
+ const top = results.slice(0, k);
6732
+ if (top.length === 0) return 0;
6733
+ const distinct = new Set(top.map((result) => normalizePath(result.filePath))).size;
6734
+ return distinct / top.length;
6735
+ }
6031
6736
  function pathMatchesExpected(actualPath, expectedPath) {
6032
6737
  const actual = normalizePath(actualPath);
6033
6738
  const expected = normalizePath(expectedPath);
@@ -6106,6 +6811,7 @@ function buildPerQueryResult(query, results, latencyMs, k) {
6106
6811
  reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
6107
6812
  ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
6108
6813
  failureBucket: classifyFailureBucket(query, results, k),
6814
+ rawTop3DistinctRatio: distinctTopKRatio(results, 3),
6109
6815
  results: deduped
6110
6816
  };
6111
6817
  return perQuery;
@@ -6119,7 +6825,9 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6119
6825
  hitAt5: 0,
6120
6826
  hitAt10: 0,
6121
6827
  mrrAt10: 0,
6122
- ndcgAt10: 0
6828
+ ndcgAt10: 0,
6829
+ distinctTop3Ratio: 0,
6830
+ rawDistinctTop3Ratio: 0
6123
6831
  };
6124
6832
  const failureBuckets = {
6125
6833
  "wrong-file": 0,
@@ -6135,6 +6843,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6135
6843
  if (query.hitAt10) sum.hitAt10 += 1;
6136
6844
  sum.mrrAt10 += query.reciprocalRankAt10;
6137
6845
  sum.ndcgAt10 += query.ndcgAt10;
6846
+ sum.distinctTop3Ratio += distinctTopKRatio(query.results, 3);
6847
+ sum.rawDistinctTop3Ratio += query.rawTop3DistinctRatio;
6138
6848
  if (query.failureBucket) {
6139
6849
  failureBuckets[query.failureBucket] += 1;
6140
6850
  }
@@ -6147,6 +6857,8 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
6147
6857
  hitAt10: safeDiv(sum.hitAt10),
6148
6858
  mrrAt10: safeDiv(sum.mrrAt10),
6149
6859
  ndcgAt10: safeDiv(sum.ndcgAt10),
6860
+ distinctTop3Ratio: safeDiv(sum.distinctTop3Ratio),
6861
+ rawDistinctTop3Ratio: safeDiv(sum.rawDistinctTop3Ratio),
6150
6862
  latencyMs: {
6151
6863
  p50: percentile(latencies, 0.5),
6152
6864
  p95: percentile(latencies, 0.95),
@@ -6177,23 +6889,23 @@ function isRecord(value) {
6177
6889
  function isStringArray2(value) {
6178
6890
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6179
6891
  }
6180
- function asPositiveNumber(value, path10) {
6892
+ function asPositiveNumber(value, path11) {
6181
6893
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6182
- throw new Error(`${path10} must be a non-negative number`);
6894
+ throw new Error(`${path11} must be a non-negative number`);
6183
6895
  }
6184
6896
  return value;
6185
6897
  }
6186
- function parseQueryType(value, path10) {
6898
+ function parseQueryType(value, path11) {
6187
6899
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6188
6900
  return value;
6189
6901
  }
6190
6902
  throw new Error(
6191
- `${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6903
+ `${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6192
6904
  );
6193
6905
  }
6194
- function parseExpected(input, path10) {
6906
+ function parseExpected(input, path11) {
6195
6907
  if (!isRecord(input)) {
6196
- throw new Error(`${path10} must be an object`);
6908
+ throw new Error(`${path11} must be an object`);
6197
6909
  }
6198
6910
  const filePathRaw = input.filePath;
6199
6911
  const acceptableFilesRaw = input.acceptableFiles;
@@ -6202,16 +6914,16 @@ function parseExpected(input, path10) {
6202
6914
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6203
6915
  const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6204
6916
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6205
- throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
6917
+ throw new Error(`${path11} must include either expected.filePath or expected.acceptableFiles`);
6206
6918
  }
6207
6919
  if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6208
- throw new Error(`${path10}.acceptableFiles must be an array of strings`);
6920
+ throw new Error(`${path11}.acceptableFiles must be an array of strings`);
6209
6921
  }
6210
6922
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6211
- throw new Error(`${path10}.symbol must be a string when provided`);
6923
+ throw new Error(`${path11}.symbol must be a string when provided`);
6212
6924
  }
6213
6925
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6214
- throw new Error(`${path10}.branch must be a string when provided`);
6926
+ throw new Error(`${path11}.branch must be a string when provided`);
6215
6927
  }
6216
6928
  return {
6217
6929
  filePath,
@@ -6221,25 +6933,25 @@ function parseExpected(input, path10) {
6221
6933
  };
6222
6934
  }
6223
6935
  function parseQuery(input, index) {
6224
- const path10 = `queries[${index}]`;
6936
+ const path11 = `queries[${index}]`;
6225
6937
  if (!isRecord(input)) {
6226
- throw new Error(`${path10} must be an object`);
6938
+ throw new Error(`${path11} must be an object`);
6227
6939
  }
6228
6940
  const id = input.id;
6229
6941
  const query = input.query;
6230
6942
  const queryType = input.queryType;
6231
6943
  const expected = input.expected;
6232
6944
  if (typeof id !== "string" || id.trim().length === 0) {
6233
- throw new Error(`${path10}.id must be a non-empty string`);
6945
+ throw new Error(`${path11}.id must be a non-empty string`);
6234
6946
  }
6235
6947
  if (typeof query !== "string" || query.trim().length === 0) {
6236
- throw new Error(`${path10}.query must be a non-empty string`);
6948
+ throw new Error(`${path11}.query must be a non-empty string`);
6237
6949
  }
6238
6950
  return {
6239
6951
  id,
6240
6952
  query,
6241
- queryType: parseQueryType(queryType, `${path10}.queryType`),
6242
- expected: parseExpected(expected, `${path10}.expected`)
6953
+ queryType: parseQueryType(queryType, `${path11}.queryType`),
6954
+ expected: parseExpected(expected, `${path11}.expected`)
6243
6955
  };
6244
6956
  }
6245
6957
  function parseGoldenDataset(raw, sourceLabel) {
@@ -6308,6 +7020,10 @@ function parseBudget(raw, sourceLabel) {
6308
7020
  thresholds: {
6309
7021
  hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
6310
7022
  mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
7023
+ rawDistinctTop3RatioMaxDrop: thresholds.rawDistinctTop3RatioMaxDrop === void 0 ? void 0 : asPositiveNumber(
7024
+ thresholds.rawDistinctTop3RatioMaxDrop,
7025
+ `${sourceLabel}.thresholds.rawDistinctTop3RatioMaxDrop`
7026
+ ),
6311
7027
  p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
6312
7028
  thresholds.p95LatencyMaxMultiplier,
6313
7029
  `${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
@@ -6317,7 +7033,11 @@ function parseBudget(raw, sourceLabel) {
6317
7033
  `${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
6318
7034
  ),
6319
7035
  minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
6320
- minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
7036
+ minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`),
7037
+ minRawDistinctTop3Ratio: thresholds.minRawDistinctTop3Ratio === void 0 ? void 0 : asPositiveNumber(
7038
+ thresholds.minRawDistinctTop3Ratio,
7039
+ `${sourceLabel}.thresholds.minRawDistinctTop3Ratio`
7040
+ )
6321
7041
  }
6322
7042
  };
6323
7043
  }
@@ -6811,8 +7531,12 @@ async function handleEvalCommand(args, cwd) {
6811
7531
  if (!parsed.againstPath.endsWith(".json")) {
6812
7532
  throw new Error("eval diff --against must point to a summary JSON file");
6813
7533
  }
6814
- const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
6815
- const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
7534
+ const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath), {
7535
+ allowLegacyDiversityMetrics: true
7536
+ });
7537
+ const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath), {
7538
+ allowLegacyDiversityMetrics: true
7539
+ });
6816
7540
  const comparison = compareSummaries(
6817
7541
  currentSummary,
6818
7542
  baselineSummary,
@@ -6846,16 +7570,13 @@ function formatDefinitionLookup(results, query) {
6846
7570
  return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
6847
7571
  }
6848
7572
  const formatted = results.map((r, idx) => {
6849
- 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}`;
6850
- return `${header2} (score: ${r.score.toFixed(2)})
7573
+ const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
7574
+ return `${header} (score: ${r.score.toFixed(2)})
6851
7575
  \`\`\`
6852
7576
  ${truncateContent(r.content)}
6853
7577
  \`\`\``;
6854
7578
  });
6855
- const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
6856
- return `${header}
6857
-
6858
- ${formatted.join("\n\n")}`;
7579
+ return formatted.join("\n\n");
6859
7580
  }
6860
7581
 
6861
7582
  // src/mcp-server.ts
@@ -7309,7 +8030,10 @@ Use the implementation_lookup tool to find where this symbol is defined. This pr
7309
8030
  return server;
7310
8031
  }
7311
8032
 
7312
- // src/cli.ts
8033
+ // src/config/merger.ts
8034
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
8035
+ import * as path9 from "path";
8036
+ import * as os4 from "os";
7313
8037
  function loadJsonFile(filePath) {
7314
8038
  try {
7315
8039
  if (existsSync6(filePath)) {
@@ -7320,25 +8044,101 @@ function loadJsonFile(filePath) {
7320
8044
  }
7321
8045
  return null;
7322
8046
  }
7323
- function loadPluginConfig(projectRoot, configPath) {
7324
- if (configPath) {
7325
- const config = loadJsonFile(configPath);
7326
- if (config) return config;
8047
+ function loadMergedConfig(projectRoot) {
8048
+ const globalConfigPath = path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8049
+ const globalConfig = loadJsonFile(globalConfigPath);
8050
+ const projectConfigPath = path9.join(projectRoot, ".opencode", "codebase-index.json");
8051
+ const projectConfig = loadJsonFile(projectConfigPath);
8052
+ if (!globalConfig && !projectConfig) {
8053
+ return {};
8054
+ }
8055
+ if (!projectConfig && globalConfig) {
8056
+ return globalConfig;
8057
+ }
8058
+ if (!globalConfig && projectConfig) {
8059
+ return projectConfig;
8060
+ }
8061
+ const merged = { ...globalConfig };
8062
+ if (projectConfig && "embeddingProvider" in projectConfig) {
8063
+ merged.embeddingProvider = projectConfig.embeddingProvider;
8064
+ } else if (globalConfig && globalConfig.embeddingProvider) {
8065
+ merged.embeddingProvider = globalConfig.embeddingProvider;
8066
+ }
8067
+ if (projectConfig && "customProvider" in projectConfig) {
8068
+ merged.customProvider = projectConfig.customProvider;
8069
+ } else if (globalConfig && globalConfig.customProvider) {
8070
+ merged.customProvider = globalConfig.customProvider;
8071
+ }
8072
+ if (projectConfig && "embeddingModel" in projectConfig) {
8073
+ merged.embeddingModel = projectConfig.embeddingModel;
8074
+ } else if (globalConfig && globalConfig.embeddingModel) {
8075
+ merged.embeddingModel = globalConfig.embeddingModel;
8076
+ }
8077
+ if (projectConfig && "reranker" in projectConfig) {
8078
+ merged.reranker = projectConfig.reranker;
8079
+ } else if (globalConfig && globalConfig.reranker) {
8080
+ merged.reranker = globalConfig.reranker;
8081
+ }
8082
+ if (projectConfig && "include" in projectConfig) {
8083
+ merged.include = projectConfig.include;
8084
+ } else if (globalConfig && globalConfig.include) {
8085
+ merged.include = globalConfig.include;
8086
+ }
8087
+ if (projectConfig && "exclude" in projectConfig) {
8088
+ merged.exclude = projectConfig.exclude;
8089
+ } else if (globalConfig && globalConfig.exclude) {
8090
+ merged.exclude = globalConfig.exclude;
8091
+ }
8092
+ if (projectConfig && "indexing" in projectConfig) {
8093
+ merged.indexing = projectConfig.indexing;
8094
+ } else if (globalConfig && globalConfig.indexing) {
8095
+ merged.indexing = globalConfig.indexing;
8096
+ }
8097
+ if (projectConfig && "search" in projectConfig) {
8098
+ merged.search = projectConfig.search;
8099
+ } else if (globalConfig && globalConfig.search) {
8100
+ merged.search = globalConfig.search;
8101
+ }
8102
+ if (projectConfig && "debug" in projectConfig) {
8103
+ merged.debug = projectConfig.debug;
8104
+ } else if (globalConfig && globalConfig.debug) {
8105
+ merged.debug = globalConfig.debug;
8106
+ }
8107
+ if (projectConfig && "scope" in projectConfig) {
8108
+ merged.scope = projectConfig.scope;
8109
+ } else if (globalConfig && "scope" in globalConfig) {
8110
+ merged.scope = globalConfig.scope;
8111
+ }
8112
+ if (projectConfig) {
8113
+ for (const key of Object.keys(projectConfig)) {
8114
+ if (key === "embeddingProvider" || key === "customProvider" || key === "embeddingModel" || key === "reranker" || key === "include" || key === "exclude" || key === "indexing" || key === "search" || key === "debug" || key === "scope" || key === "knowledgeBases" || key === "additionalInclude") {
8115
+ continue;
8116
+ }
8117
+ merged[key] = projectConfig[key];
8118
+ }
7327
8119
  }
7328
- const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
7329
- if (projectConfig) return projectConfig;
7330
- const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
7331
- if (globalConfig) return globalConfig;
7332
- return {};
8120
+ const globalKbs = globalConfig && Array.isArray(globalConfig.knowledgeBases) ? globalConfig.knowledgeBases : [];
8121
+ const projectKbs = projectConfig && Array.isArray(projectConfig.knowledgeBases) ? projectConfig.knowledgeBases : [];
8122
+ const allKbs = [...globalKbs, ...projectKbs];
8123
+ const uniqueKbs = [...new Set(allKbs.map((p) => String(p).trim()))];
8124
+ merged.knowledgeBases = uniqueKbs;
8125
+ const globalAdditional = globalConfig && Array.isArray(globalConfig.additionalInclude) ? globalConfig.additionalInclude : [];
8126
+ const projectAdditional = projectConfig && Array.isArray(projectConfig.additionalInclude) ? projectConfig.additionalInclude : [];
8127
+ const allAdditional = [...globalAdditional, ...projectAdditional];
8128
+ const uniqueAdditional = [...new Set(allAdditional.map((p) => String(p).trim()))];
8129
+ merged.additionalInclude = uniqueAdditional;
8130
+ return merged;
7333
8131
  }
8132
+
8133
+ // src/cli.ts
7334
8134
  function parseArgs(argv) {
7335
8135
  let project = process.cwd();
7336
8136
  let config;
7337
8137
  for (let i = 2; i < argv.length; i++) {
7338
8138
  if (argv[i] === "--project" && argv[i + 1]) {
7339
- project = path9.resolve(argv[++i]);
8139
+ project = path10.resolve(argv[++i]);
7340
8140
  } else if (argv[i] === "--config" && argv[i + 1]) {
7341
- config = path9.resolve(argv[++i]);
8141
+ config = path10.resolve(argv[++i]);
7342
8142
  }
7343
8143
  }
7344
8144
  return { project, config };
@@ -7349,7 +8149,7 @@ async function main() {
7349
8149
  process.exit(exitCode);
7350
8150
  }
7351
8151
  const args = parseArgs(process.argv);
7352
- const rawConfig = loadPluginConfig(args.project, args.config);
8152
+ const rawConfig = loadMergedConfig(args.project);
7353
8153
  const config = parseConfig(rawConfig);
7354
8154
  const server = createMcpServer(args.project, config);
7355
8155
  const transport = new StdioServerTransport();