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/README.md +298 -28
- package/commands/index.md +6 -4
- package/dist/cli.cjs +950 -150
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +950 -150
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +8024 -6826
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7942 -6744
- package/dist/index.js.map +1 -1
- package/native/codebase-index-native.darwin-arm64.node +0 -0
- package/native/codebase-index-native.darwin-x64.node +0 -0
- package/native/codebase-index-native.linux-arm64-gnu.node +0 -0
- package/native/codebase-index-native.linux-x64-gnu.node +0 -0
- package/native/codebase-index-native.win32-x64-msvc.node +0 -0
- package/package.json +21 -16
- package/skill/SKILL.md +63 -10
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(
|
|
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(
|
|
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 = (
|
|
521
|
-
if (!isString(
|
|
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 (!
|
|
527
|
+
if (!path11) {
|
|
528
528
|
return doThrow(`path must not be empty`, TypeError);
|
|
529
529
|
}
|
|
530
|
-
if (checkPath.isNotRelative(
|
|
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 = (
|
|
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
|
|
569
|
+
const path11 = originalPath && checkPath.convert(originalPath);
|
|
570
570
|
checkPath(
|
|
571
|
-
|
|
571
|
+
path11,
|
|
572
572
|
originalPath,
|
|
573
573
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
574
574
|
);
|
|
575
|
-
return this._t(
|
|
575
|
+
return this._t(path11, cache, checkUnignored, slices);
|
|
576
576
|
}
|
|
577
|
-
checkIgnore(
|
|
578
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
579
|
-
return this.test(
|
|
577
|
+
checkIgnore(path11) {
|
|
578
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path11)) {
|
|
579
|
+
return this.test(path11);
|
|
580
580
|
}
|
|
581
|
-
const slices =
|
|
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(
|
|
594
|
+
return this._rules.test(path11, false, MODE_CHECK_IGNORE);
|
|
595
595
|
}
|
|
596
|
-
_t(
|
|
597
|
-
if (
|
|
598
|
-
return cache[
|
|
596
|
+
_t(path11, cache, checkUnignored, slices) {
|
|
597
|
+
if (path11 in cache) {
|
|
598
|
+
return cache[path11];
|
|
599
599
|
}
|
|
600
600
|
if (!slices) {
|
|
601
|
-
slices =
|
|
601
|
+
slices = path11.split(SLASH).filter(Boolean);
|
|
602
602
|
}
|
|
603
603
|
slices.pop();
|
|
604
604
|
if (!slices.length) {
|
|
605
|
-
return cache[
|
|
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[
|
|
613
|
+
return cache[path11] = parent.ignored ? parent : this._rules.test(path11, checkUnignored, MODE_IGNORE);
|
|
614
614
|
}
|
|
615
|
-
ignores(
|
|
616
|
-
return this._test(
|
|
615
|
+
ignores(path11) {
|
|
616
|
+
return this._test(path11, this._ignoreCache, false).ignored;
|
|
617
617
|
}
|
|
618
618
|
createFilter() {
|
|
619
|
-
return (
|
|
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(
|
|
626
|
-
return this._test(
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
867
|
-
model:
|
|
934
|
+
baseUrl: baseUrlValue.trim().replace(/\/+$/, ""),
|
|
935
|
+
model: modelValue,
|
|
868
936
|
dimensions: rawCustom.dimensions,
|
|
869
|
-
apiKey:
|
|
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(
|
|
886
|
-
embeddingProvider =
|
|
887
|
-
|
|
888
|
-
|
|
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(
|
|
898
|
-
include:
|
|
899
|
-
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((
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
1705
|
+
return new Promise((resolve6, reject) => {
|
|
1550
1706
|
const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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 ||
|
|
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:
|
|
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
|
-
|
|
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,
|
|
6892
|
+
function asPositiveNumber(value, path11) {
|
|
6181
6893
|
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
6182
|
-
throw new Error(`${
|
|
6894
|
+
throw new Error(`${path11} must be a non-negative number`);
|
|
6183
6895
|
}
|
|
6184
6896
|
return value;
|
|
6185
6897
|
}
|
|
6186
|
-
function parseQueryType(value,
|
|
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
|
-
`${
|
|
6903
|
+
`${path11} must be one of: definition, implementation-intent, similarity, keyword-heavy`
|
|
6192
6904
|
);
|
|
6193
6905
|
}
|
|
6194
|
-
function parseExpected(input,
|
|
6906
|
+
function parseExpected(input, path11) {
|
|
6195
6907
|
if (!isRecord(input)) {
|
|
6196
|
-
throw new Error(`${
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
|
6936
|
+
const path11 = `queries[${index}]`;
|
|
6225
6937
|
if (!isRecord(input)) {
|
|
6226
|
-
throw new Error(`${
|
|
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(`${
|
|
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(`${
|
|
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, `${
|
|
6242
|
-
expected: parseExpected(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
|
-
|
|
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
|
|
6850
|
-
return `${
|
|
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
|
-
|
|
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/
|
|
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
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
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
|
|
7329
|
-
|
|
7330
|
-
const
|
|
7331
|
-
|
|
7332
|
-
|
|
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 =
|
|
8139
|
+
project = path10.resolve(argv[++i]);
|
|
7340
8140
|
} else if (argv[i] === "--config" && argv[i + 1]) {
|
|
7341
|
-
config =
|
|
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 =
|
|
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();
|