opencode-codebase-index 0.5.2 → 0.6.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 +189 -101
- package/commands/definition.md +24 -0
- package/dist/cli.cjs +1335 -172
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1308 -151
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +110 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -59
- 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 +8 -1
package/dist/cli.cjs
CHANGED
|
@@ -29,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
|
|
30
30
|
// node_modules/eventemitter3/index.js
|
|
31
31
|
var require_eventemitter3 = __commonJS({
|
|
32
|
-
"node_modules/eventemitter3/index.js"(exports2,
|
|
32
|
+
"node_modules/eventemitter3/index.js"(exports2, module3) {
|
|
33
33
|
"use strict";
|
|
34
34
|
var has = Object.prototype.hasOwnProperty;
|
|
35
35
|
var prefix = "~";
|
|
@@ -183,15 +183,15 @@ var require_eventemitter3 = __commonJS({
|
|
|
183
183
|
EventEmitter2.prototype.addListener = EventEmitter2.prototype.on;
|
|
184
184
|
EventEmitter2.prefixed = prefix;
|
|
185
185
|
EventEmitter2.EventEmitter = EventEmitter2;
|
|
186
|
-
if ("undefined" !== typeof
|
|
187
|
-
|
|
186
|
+
if ("undefined" !== typeof module3) {
|
|
187
|
+
module3.exports = EventEmitter2;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// node_modules/ignore/index.js
|
|
193
193
|
var require_ignore = __commonJS({
|
|
194
|
-
"node_modules/ignore/index.js"(exports2,
|
|
194
|
+
"node_modules/ignore/index.js"(exports2, module3) {
|
|
195
195
|
"use strict";
|
|
196
196
|
function makeArray(subject) {
|
|
197
197
|
return Array.isArray(subject) ? subject : [subject];
|
|
@@ -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(path10, 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(path10);
|
|
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 = (path10, originalPath, doThrow) => {
|
|
521
|
+
if (!isString(path10)) {
|
|
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 (!path10) {
|
|
528
528
|
return doThrow(`path must not be empty`, TypeError);
|
|
529
529
|
}
|
|
530
|
-
if (checkPath.isNotRelative(
|
|
530
|
+
if (checkPath.isNotRelative(path10)) {
|
|
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 = (path10) => REGEX_TEST_INVALID_PATH.test(path10);
|
|
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 path10 = originalPath && checkPath.convert(originalPath);
|
|
570
570
|
checkPath(
|
|
571
|
-
|
|
571
|
+
path10,
|
|
572
572
|
originalPath,
|
|
573
573
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
574
574
|
);
|
|
575
|
-
return this._t(
|
|
575
|
+
return this._t(path10, cache, checkUnignored, slices);
|
|
576
576
|
}
|
|
577
|
-
checkIgnore(
|
|
578
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
579
|
-
return this.test(
|
|
577
|
+
checkIgnore(path10) {
|
|
578
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
|
|
579
|
+
return this.test(path10);
|
|
580
580
|
}
|
|
581
|
-
const slices =
|
|
581
|
+
const slices = path10.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(path10, false, MODE_CHECK_IGNORE);
|
|
595
595
|
}
|
|
596
|
-
_t(
|
|
597
|
-
if (
|
|
598
|
-
return cache[
|
|
596
|
+
_t(path10, cache, checkUnignored, slices) {
|
|
597
|
+
if (path10 in cache) {
|
|
598
|
+
return cache[path10];
|
|
599
599
|
}
|
|
600
600
|
if (!slices) {
|
|
601
|
-
slices =
|
|
601
|
+
slices = path10.split(SLASH).filter(Boolean);
|
|
602
602
|
}
|
|
603
603
|
slices.pop();
|
|
604
604
|
if (!slices.length) {
|
|
605
|
-
return cache[
|
|
605
|
+
return cache[path10] = this._rules.test(path10, 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[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
|
|
614
614
|
}
|
|
615
|
-
ignores(
|
|
616
|
-
return this._test(
|
|
615
|
+
ignores(path10) {
|
|
616
|
+
return this._test(path10, this._ignoreCache, false).ignored;
|
|
617
617
|
}
|
|
618
618
|
createFilter() {
|
|
619
|
-
return (
|
|
619
|
+
return (path10) => !this.ignores(path10);
|
|
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(path10) {
|
|
626
|
+
return this._test(path10, this._testCache, true);
|
|
627
627
|
}
|
|
628
628
|
};
|
|
629
629
|
var factory = (options) => new Ignore2(options);
|
|
630
|
-
var isPathValid = (
|
|
630
|
+
var isPathValid = (path10) => checkPath(path10 && checkPath.convert(path10), path10, 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 = (path10) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path10) || isNotRelative(path10);
|
|
636
636
|
};
|
|
637
637
|
if (
|
|
638
638
|
// Detect `process` so that it can run in browsers.
|
|
@@ -640,18 +640,18 @@ var require_ignore = __commonJS({
|
|
|
640
640
|
) {
|
|
641
641
|
setupWindows();
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
module3.exports = factory;
|
|
644
644
|
factory.default = factory;
|
|
645
|
-
|
|
646
|
-
define(
|
|
645
|
+
module3.exports.isPathValid = isPathValid;
|
|
646
|
+
define(module3.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
647
647
|
}
|
|
648
648
|
});
|
|
649
649
|
|
|
650
650
|
// src/cli.ts
|
|
651
651
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
652
|
-
var
|
|
653
|
-
var
|
|
654
|
-
var
|
|
652
|
+
var import_fs10 = require("fs");
|
|
653
|
+
var path9 = __toESM(require("path"), 1);
|
|
654
|
+
var os4 = __toESM(require("os"), 1);
|
|
655
655
|
|
|
656
656
|
// src/config/constants.ts
|
|
657
657
|
var DEFAULT_INCLUDE = [
|
|
@@ -659,7 +659,7 @@ var DEFAULT_INCLUDE = [
|
|
|
659
659
|
"**/*.{py,pyi}",
|
|
660
660
|
"**/*.{go,rs,java,kt,scala}",
|
|
661
661
|
"**/*.{c,cpp,cc,h,hpp}",
|
|
662
|
-
"**/*.{rb,php,swift}",
|
|
662
|
+
"**/*.{rb,php,inc,swift}",
|
|
663
663
|
"**/*.{vue,svelte,astro}",
|
|
664
664
|
"**/*.{sql,graphql,proto}",
|
|
665
665
|
"**/*.{yaml,yml,toml}",
|
|
@@ -909,13 +909,197 @@ function getDefaultModelForProvider(provider) {
|
|
|
909
909
|
}
|
|
910
910
|
var availableProviders = Object.keys(EMBEDDING_MODELS);
|
|
911
911
|
|
|
912
|
-
// src/
|
|
913
|
-
var
|
|
914
|
-
|
|
912
|
+
// src/eval/cli.ts
|
|
913
|
+
var path8 = __toESM(require("path"), 1);
|
|
914
|
+
|
|
915
|
+
// src/eval/compare.ts
|
|
916
|
+
function metricDelta(current, baseline) {
|
|
917
|
+
const absolute = current - baseline;
|
|
918
|
+
const relativePct = baseline === 0 ? current === 0 ? 0 : 100 : absolute / baseline * 100;
|
|
919
|
+
return {
|
|
920
|
+
current,
|
|
921
|
+
baseline,
|
|
922
|
+
absolute,
|
|
923
|
+
relativePct
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function compareSummaries(current, baseline, againstPath) {
|
|
927
|
+
return {
|
|
928
|
+
againstPath,
|
|
929
|
+
deltas: {
|
|
930
|
+
hitAt1: metricDelta(current.metrics.hitAt1, baseline.metrics.hitAt1),
|
|
931
|
+
hitAt3: metricDelta(current.metrics.hitAt3, baseline.metrics.hitAt3),
|
|
932
|
+
hitAt5: metricDelta(current.metrics.hitAt5, baseline.metrics.hitAt5),
|
|
933
|
+
hitAt10: metricDelta(current.metrics.hitAt10, baseline.metrics.hitAt10),
|
|
934
|
+
mrrAt10: metricDelta(current.metrics.mrrAt10, baseline.metrics.mrrAt10),
|
|
935
|
+
ndcgAt10: metricDelta(current.metrics.ndcgAt10, baseline.metrics.ndcgAt10),
|
|
936
|
+
latencyP50Ms: metricDelta(current.metrics.latencyMs.p50, baseline.metrics.latencyMs.p50),
|
|
937
|
+
latencyP95Ms: metricDelta(current.metrics.latencyMs.p95, baseline.metrics.latencyMs.p95),
|
|
938
|
+
latencyP99Ms: metricDelta(current.metrics.latencyMs.p99, baseline.metrics.latencyMs.p99),
|
|
939
|
+
embeddingCallCount: metricDelta(
|
|
940
|
+
current.metrics.embedding.callCount,
|
|
941
|
+
baseline.metrics.embedding.callCount
|
|
942
|
+
),
|
|
943
|
+
estimatedCostUsd: metricDelta(
|
|
944
|
+
current.metrics.embedding.estimatedCostUsd,
|
|
945
|
+
baseline.metrics.embedding.estimatedCostUsd
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/eval/reports.ts
|
|
952
|
+
var import_fs = require("fs");
|
|
953
|
+
var path = __toESM(require("path"), 1);
|
|
954
|
+
function formatPct(value) {
|
|
955
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
956
|
+
}
|
|
957
|
+
function formatMs(value) {
|
|
958
|
+
return `${value.toFixed(3)}ms`;
|
|
959
|
+
}
|
|
960
|
+
function formatUsd(value) {
|
|
961
|
+
return `$${value.toFixed(6)}`;
|
|
962
|
+
}
|
|
963
|
+
function signed(value, digits = 4) {
|
|
964
|
+
const formatted = value.toFixed(digits);
|
|
965
|
+
return value > 0 ? `+${formatted}` : formatted;
|
|
966
|
+
}
|
|
967
|
+
function loadSummary(summaryPath) {
|
|
968
|
+
const raw = (0, import_fs.readFileSync)(summaryPath, "utf-8");
|
|
969
|
+
return JSON.parse(raw);
|
|
970
|
+
}
|
|
971
|
+
function createRunDirectory(outputRoot, timestampOverride) {
|
|
972
|
+
const timestamp = (timestampOverride ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
|
|
973
|
+
const dir = path.join(outputRoot, timestamp);
|
|
974
|
+
(0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
975
|
+
return dir;
|
|
976
|
+
}
|
|
977
|
+
function writeJson(filePath, value) {
|
|
978
|
+
(0, import_fs.writeFileSync)(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
979
|
+
}
|
|
980
|
+
function writeText(filePath, value) {
|
|
981
|
+
(0, import_fs.writeFileSync)(filePath, value, "utf-8");
|
|
982
|
+
}
|
|
983
|
+
function createSummaryMarkdown(summary, comparison, gate, sweep) {
|
|
984
|
+
const lines = [];
|
|
985
|
+
lines.push("# Evaluation Summary");
|
|
986
|
+
lines.push("");
|
|
987
|
+
lines.push(`- Generated: ${summary.generatedAt}`);
|
|
988
|
+
lines.push(`- Dataset: ${summary.datasetName} (v${summary.datasetVersion})`);
|
|
989
|
+
lines.push(`- Query count: ${summary.queryCount}`);
|
|
990
|
+
lines.push(
|
|
991
|
+
`- Search config: fusion=${summary.searchConfig.fusionStrategy}, hybridWeight=${summary.searchConfig.hybridWeight}, rrfK=${summary.searchConfig.rrfK}, rerankTopN=${summary.searchConfig.rerankTopN}`
|
|
992
|
+
);
|
|
993
|
+
lines.push("");
|
|
994
|
+
lines.push("## Metrics");
|
|
995
|
+
lines.push("");
|
|
996
|
+
lines.push("| Metric | Value |");
|
|
997
|
+
lines.push("|---|---:|");
|
|
998
|
+
lines.push(`| Hit@1 | ${formatPct(summary.metrics.hitAt1)} |`);
|
|
999
|
+
lines.push(`| Hit@3 | ${formatPct(summary.metrics.hitAt3)} |`);
|
|
1000
|
+
lines.push(`| Hit@5 | ${formatPct(summary.metrics.hitAt5)} |`);
|
|
1001
|
+
lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
|
|
1002
|
+
lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
|
|
1003
|
+
lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
|
|
1004
|
+
lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
|
|
1005
|
+
lines.push(`| Latency p95 | ${formatMs(summary.metrics.latencyMs.p95)} |`);
|
|
1006
|
+
lines.push(`| Latency p99 | ${formatMs(summary.metrics.latencyMs.p99)} |`);
|
|
1007
|
+
lines.push(`| Embedding calls | ${summary.metrics.embedding.callCount} |`);
|
|
1008
|
+
lines.push(`| Embedding tokens | ${summary.metrics.tokenEstimate.embeddingTokensUsed} |`);
|
|
1009
|
+
lines.push(`| Estimated embedding cost | ${formatUsd(summary.metrics.embedding.estimatedCostUsd)} |`);
|
|
1010
|
+
lines.push("");
|
|
1011
|
+
lines.push("## Failure Buckets");
|
|
1012
|
+
lines.push("");
|
|
1013
|
+
lines.push("| Bucket | Count |");
|
|
1014
|
+
lines.push("|---|---:|");
|
|
1015
|
+
lines.push(
|
|
1016
|
+
`| wrong-file | ${summary.metrics.failureBuckets["wrong-file"]} |`
|
|
1017
|
+
);
|
|
1018
|
+
lines.push(
|
|
1019
|
+
`| wrong-symbol | ${summary.metrics.failureBuckets["wrong-symbol"]} |`
|
|
1020
|
+
);
|
|
1021
|
+
lines.push(
|
|
1022
|
+
`| docs/tests outranking source | ${summary.metrics.failureBuckets["docs-tests-outranking-source"]} |`
|
|
1023
|
+
);
|
|
1024
|
+
lines.push(
|
|
1025
|
+
`| no relevant hit in top-k | ${summary.metrics.failureBuckets["no-relevant-hit-top-k"]} |`
|
|
1026
|
+
);
|
|
1027
|
+
lines.push("");
|
|
1028
|
+
if (comparison) {
|
|
1029
|
+
lines.push("## Comparison vs Baseline");
|
|
1030
|
+
lines.push("");
|
|
1031
|
+
lines.push(`- Against: ${comparison.againstPath}`);
|
|
1032
|
+
lines.push("");
|
|
1033
|
+
lines.push("| Metric | Baseline | Current | Delta |");
|
|
1034
|
+
lines.push("|---|---:|---:|---:|");
|
|
1035
|
+
lines.push(
|
|
1036
|
+
`| Hit@5 | ${formatPct(comparison.deltas.hitAt5.baseline)} | ${formatPct(comparison.deltas.hitAt5.current)} | ${signed(comparison.deltas.hitAt5.absolute)} |`
|
|
1037
|
+
);
|
|
1038
|
+
lines.push(
|
|
1039
|
+
`| MRR@10 | ${comparison.deltas.mrrAt10.baseline.toFixed(4)} | ${comparison.deltas.mrrAt10.current.toFixed(4)} | ${signed(comparison.deltas.mrrAt10.absolute)} |`
|
|
1040
|
+
);
|
|
1041
|
+
lines.push(
|
|
1042
|
+
`| nDCG@10 | ${comparison.deltas.ndcgAt10.baseline.toFixed(4)} | ${comparison.deltas.ndcgAt10.current.toFixed(4)} | ${signed(comparison.deltas.ndcgAt10.absolute)} |`
|
|
1043
|
+
);
|
|
1044
|
+
lines.push(
|
|
1045
|
+
`| p95 latency (ms) | ${comparison.deltas.latencyP95Ms.baseline.toFixed(3)} | ${comparison.deltas.latencyP95Ms.current.toFixed(3)} | ${signed(comparison.deltas.latencyP95Ms.absolute, 3)} |`
|
|
1046
|
+
);
|
|
1047
|
+
lines.push("");
|
|
1048
|
+
}
|
|
1049
|
+
if (gate) {
|
|
1050
|
+
lines.push("## CI Gate");
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
lines.push(`- Result: ${gate.passed ? "PASS \u2705" : "FAIL \u274C"}`);
|
|
1053
|
+
if (gate.violations.length > 0) {
|
|
1054
|
+
lines.push("- Violations:");
|
|
1055
|
+
for (const violation of gate.violations) {
|
|
1056
|
+
lines.push(` - ${violation.metric}: ${violation.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
lines.push("");
|
|
1060
|
+
}
|
|
1061
|
+
if (sweep) {
|
|
1062
|
+
lines.push("## Parameter Sweep");
|
|
1063
|
+
lines.push("");
|
|
1064
|
+
lines.push(`- Run count: ${sweep.runCount}`);
|
|
1065
|
+
if (sweep.bestByHitAt5) {
|
|
1066
|
+
lines.push(
|
|
1067
|
+
`- Best Hit@5: ${formatPct(sweep.bestByHitAt5.summary.metrics.hitAt5)} with fusion=${sweep.bestByHitAt5.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByHitAt5.searchConfig.hybridWeight}, rrfK=${sweep.bestByHitAt5.searchConfig.rrfK}, rerankTopN=${sweep.bestByHitAt5.searchConfig.rerankTopN}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (sweep.bestByMrrAt10) {
|
|
1071
|
+
lines.push(
|
|
1072
|
+
`- Best MRR@10: ${sweep.bestByMrrAt10.summary.metrics.mrrAt10.toFixed(4)} with fusion=${sweep.bestByMrrAt10.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByMrrAt10.searchConfig.hybridWeight}, rrfK=${sweep.bestByMrrAt10.searchConfig.rrfK}, rerankTopN=${sweep.bestByMrrAt10.searchConfig.rerankTopN}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (sweep.bestByP95Latency) {
|
|
1076
|
+
lines.push(
|
|
1077
|
+
`- Best p95 latency: ${formatMs(sweep.bestByP95Latency.summary.metrics.latencyMs.p95)} with fusion=${sweep.bestByP95Latency.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByP95Latency.searchConfig.hybridWeight}, rrfK=${sweep.bestByP95Latency.searchConfig.rrfK}, rerankTopN=${sweep.bestByP95Latency.searchConfig.rerankTopN}`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
lines.push("");
|
|
1081
|
+
}
|
|
1082
|
+
return `${lines.join("\n")}
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
function buildPerQueryArtifact(perQuery) {
|
|
1086
|
+
return {
|
|
1087
|
+
queryCount: perQuery.length,
|
|
1088
|
+
queries: [...perQuery].sort((a, b) => a.id.localeCompare(b.id))
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/eval/runner.ts
|
|
1093
|
+
var import_fs7 = require("fs");
|
|
1094
|
+
var import_fs8 = require("fs");
|
|
1095
|
+
var import_fs9 = require("fs");
|
|
1096
|
+
var os3 = __toESM(require("os"), 1);
|
|
1097
|
+
var path7 = __toESM(require("path"), 1);
|
|
1098
|
+
var import_perf_hooks2 = require("perf_hooks");
|
|
915
1099
|
|
|
916
1100
|
// src/indexer/index.ts
|
|
917
|
-
var
|
|
918
|
-
var
|
|
1101
|
+
var import_fs5 = require("fs");
|
|
1102
|
+
var path6 = __toESM(require("path"), 1);
|
|
919
1103
|
var import_perf_hooks = require("perf_hooks");
|
|
920
1104
|
|
|
921
1105
|
// node_modules/eventemitter3/index.mjs
|
|
@@ -940,7 +1124,7 @@ function pTimeout(promise, options) {
|
|
|
940
1124
|
} = options;
|
|
941
1125
|
let timer;
|
|
942
1126
|
let abortHandler;
|
|
943
|
-
const wrappedPromise = new Promise((
|
|
1127
|
+
const wrappedPromise = new Promise((resolve5, reject) => {
|
|
944
1128
|
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
|
|
945
1129
|
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
|
|
946
1130
|
}
|
|
@@ -954,7 +1138,7 @@ function pTimeout(promise, options) {
|
|
|
954
1138
|
};
|
|
955
1139
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
956
1140
|
}
|
|
957
|
-
promise.then(
|
|
1141
|
+
promise.then(resolve5, reject);
|
|
958
1142
|
if (milliseconds === Number.POSITIVE_INFINITY) {
|
|
959
1143
|
return;
|
|
960
1144
|
}
|
|
@@ -962,7 +1146,7 @@ function pTimeout(promise, options) {
|
|
|
962
1146
|
timer = customTimers.setTimeout.call(void 0, () => {
|
|
963
1147
|
if (fallback) {
|
|
964
1148
|
try {
|
|
965
|
-
|
|
1149
|
+
resolve5(fallback());
|
|
966
1150
|
} catch (error) {
|
|
967
1151
|
reject(error);
|
|
968
1152
|
}
|
|
@@ -972,7 +1156,7 @@ function pTimeout(promise, options) {
|
|
|
972
1156
|
promise.cancel();
|
|
973
1157
|
}
|
|
974
1158
|
if (message === false) {
|
|
975
|
-
|
|
1159
|
+
resolve5();
|
|
976
1160
|
} else if (message instanceof Error) {
|
|
977
1161
|
reject(message);
|
|
978
1162
|
} else {
|
|
@@ -1362,7 +1546,7 @@ var PQueue = class extends import_index.default {
|
|
|
1362
1546
|
// Assign unique ID if not provided
|
|
1363
1547
|
id: options.id ?? (this.#idAssigner++).toString()
|
|
1364
1548
|
};
|
|
1365
|
-
return new Promise((
|
|
1549
|
+
return new Promise((resolve5, reject) => {
|
|
1366
1550
|
const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
|
|
1367
1551
|
this.#queue.enqueue(async () => {
|
|
1368
1552
|
this.#pending++;
|
|
@@ -1400,7 +1584,7 @@ var PQueue = class extends import_index.default {
|
|
|
1400
1584
|
})]);
|
|
1401
1585
|
}
|
|
1402
1586
|
const result = await operation;
|
|
1403
|
-
|
|
1587
|
+
resolve5(result);
|
|
1404
1588
|
this.emit("completed", result);
|
|
1405
1589
|
} catch (error) {
|
|
1406
1590
|
reject(error);
|
|
@@ -1557,13 +1741,13 @@ var PQueue = class extends import_index.default {
|
|
|
1557
1741
|
});
|
|
1558
1742
|
}
|
|
1559
1743
|
async #onEvent(event, filter) {
|
|
1560
|
-
return new Promise((
|
|
1744
|
+
return new Promise((resolve5) => {
|
|
1561
1745
|
const listener = () => {
|
|
1562
1746
|
if (filter && !filter()) {
|
|
1563
1747
|
return;
|
|
1564
1748
|
}
|
|
1565
1749
|
this.off(event, listener);
|
|
1566
|
-
|
|
1750
|
+
resolve5();
|
|
1567
1751
|
};
|
|
1568
1752
|
this.on(event, listener);
|
|
1569
1753
|
});
|
|
@@ -1848,7 +2032,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1848
2032
|
const finalDelay = Math.min(delayTime, remainingTime);
|
|
1849
2033
|
options.signal?.throwIfAborted();
|
|
1850
2034
|
if (finalDelay > 0) {
|
|
1851
|
-
await new Promise((
|
|
2035
|
+
await new Promise((resolve5, reject) => {
|
|
1852
2036
|
const onAbort = () => {
|
|
1853
2037
|
clearTimeout(timeoutToken);
|
|
1854
2038
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -1856,7 +2040,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1856
2040
|
};
|
|
1857
2041
|
const timeoutToken = setTimeout(() => {
|
|
1858
2042
|
options.signal?.removeEventListener("abort", onAbort);
|
|
1859
|
-
|
|
2043
|
+
resolve5();
|
|
1860
2044
|
}, finalDelay);
|
|
1861
2045
|
if (options.unref) {
|
|
1862
2046
|
timeoutToken.unref?.();
|
|
@@ -1917,17 +2101,17 @@ async function pRetry(input, options = {}) {
|
|
|
1917
2101
|
}
|
|
1918
2102
|
|
|
1919
2103
|
// src/embeddings/detector.ts
|
|
1920
|
-
var
|
|
1921
|
-
var
|
|
2104
|
+
var import_fs2 = require("fs");
|
|
2105
|
+
var path2 = __toESM(require("path"), 1);
|
|
1922
2106
|
var os = __toESM(require("os"), 1);
|
|
1923
2107
|
function getOpenCodeAuthPath() {
|
|
1924
|
-
return
|
|
2108
|
+
return path2.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
1925
2109
|
}
|
|
1926
2110
|
function loadOpenCodeAuth() {
|
|
1927
2111
|
const authPath = getOpenCodeAuthPath();
|
|
1928
2112
|
try {
|
|
1929
|
-
if ((0,
|
|
1930
|
-
return JSON.parse((0,
|
|
2113
|
+
if ((0, import_fs2.existsSync)(authPath)) {
|
|
2114
|
+
return JSON.parse((0, import_fs2.readFileSync)(authPath, "utf-8"));
|
|
1931
2115
|
}
|
|
1932
2116
|
} catch {
|
|
1933
2117
|
}
|
|
@@ -2429,8 +2613,8 @@ var CustomEmbeddingProvider = class {
|
|
|
2429
2613
|
|
|
2430
2614
|
// src/utils/files.ts
|
|
2431
2615
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
2432
|
-
var
|
|
2433
|
-
var
|
|
2616
|
+
var import_fs3 = require("fs");
|
|
2617
|
+
var path3 = __toESM(require("path"), 1);
|
|
2434
2618
|
function createIgnoreFilter(projectRoot) {
|
|
2435
2619
|
const ig = (0, import_ignore.default)();
|
|
2436
2620
|
const defaultIgnores = [
|
|
@@ -2447,9 +2631,9 @@ function createIgnoreFilter(projectRoot) {
|
|
|
2447
2631
|
".opencode"
|
|
2448
2632
|
];
|
|
2449
2633
|
ig.add(defaultIgnores);
|
|
2450
|
-
const gitignorePath =
|
|
2451
|
-
if ((0,
|
|
2452
|
-
const gitignoreContent = (0,
|
|
2634
|
+
const gitignorePath = path3.join(projectRoot, ".gitignore");
|
|
2635
|
+
if ((0, import_fs3.existsSync)(gitignorePath)) {
|
|
2636
|
+
const gitignoreContent = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
|
|
2453
2637
|
ig.add(gitignoreContent);
|
|
2454
2638
|
}
|
|
2455
2639
|
return ig;
|
|
@@ -2463,10 +2647,10 @@ function matchGlob(filePath, pattern) {
|
|
|
2463
2647
|
return regex.test(filePath);
|
|
2464
2648
|
}
|
|
2465
2649
|
async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
|
|
2466
|
-
const entries = await
|
|
2650
|
+
const entries = await import_fs3.promises.readdir(dir, { withFileTypes: true });
|
|
2467
2651
|
for (const entry of entries) {
|
|
2468
|
-
const fullPath =
|
|
2469
|
-
const relativePath =
|
|
2652
|
+
const fullPath = path3.join(dir, entry.name);
|
|
2653
|
+
const relativePath = path3.relative(projectRoot, fullPath);
|
|
2470
2654
|
if (ignoreFilter.ignores(relativePath)) {
|
|
2471
2655
|
if (entry.isFile()) {
|
|
2472
2656
|
skipped.push({ path: relativePath, reason: "gitignore" });
|
|
@@ -2484,7 +2668,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
|
|
|
2484
2668
|
skipped
|
|
2485
2669
|
);
|
|
2486
2670
|
} else if (entry.isFile()) {
|
|
2487
|
-
const stat = await
|
|
2671
|
+
const stat = await import_fs3.promises.stat(fullPath);
|
|
2488
2672
|
if (stat.size > maxFileSize) {
|
|
2489
2673
|
skipped.push({ path: relativePath, reason: "too_large" });
|
|
2490
2674
|
continue;
|
|
@@ -2527,6 +2711,9 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
|
|
|
2527
2711
|
}
|
|
2528
2712
|
|
|
2529
2713
|
// src/utils/cost.ts
|
|
2714
|
+
function estimateTokens(text) {
|
|
2715
|
+
return Math.ceil(text.length / 4);
|
|
2716
|
+
}
|
|
2530
2717
|
function estimateChunksFromFiles(files) {
|
|
2531
2718
|
let totalChunks = 0;
|
|
2532
2719
|
for (const file of files) {
|
|
@@ -2881,8 +3068,9 @@ function initializeLogger(config) {
|
|
|
2881
3068
|
}
|
|
2882
3069
|
|
|
2883
3070
|
// src/native/index.ts
|
|
2884
|
-
var
|
|
3071
|
+
var path4 = __toESM(require("path"), 1);
|
|
2885
3072
|
var os2 = __toESM(require("os"), 1);
|
|
3073
|
+
var module2 = __toESM(require("module"), 1);
|
|
2886
3074
|
var import_url = require("url");
|
|
2887
3075
|
var import_meta = {};
|
|
2888
3076
|
function getNativeBinding() {
|
|
@@ -2903,17 +3091,22 @@ function getNativeBinding() {
|
|
|
2903
3091
|
throw new Error(`Unsupported platform: ${platform2}-${arch2}`);
|
|
2904
3092
|
}
|
|
2905
3093
|
let currentDir;
|
|
3094
|
+
let requireTarget;
|
|
2906
3095
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
2907
|
-
currentDir =
|
|
3096
|
+
currentDir = path4.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
3097
|
+
requireTarget = import_meta.url;
|
|
2908
3098
|
} else if (typeof __dirname !== "undefined") {
|
|
2909
3099
|
currentDir = __dirname;
|
|
3100
|
+
requireTarget = __filename;
|
|
2910
3101
|
} else {
|
|
2911
3102
|
currentDir = process.cwd();
|
|
3103
|
+
requireTarget = path4.join(currentDir, "index.js");
|
|
2912
3104
|
}
|
|
2913
3105
|
const isDevMode = currentDir.includes("/src/native");
|
|
2914
|
-
const packageRoot = isDevMode ?
|
|
2915
|
-
const nativePath =
|
|
2916
|
-
|
|
3106
|
+
const packageRoot = isDevMode ? path4.resolve(currentDir, "../..") : path4.resolve(currentDir, "..");
|
|
3107
|
+
const nativePath = path4.join(packageRoot, "native", bindingName);
|
|
3108
|
+
const require2 = module2.createRequire(requireTarget);
|
|
3109
|
+
return require2(nativePath);
|
|
2917
3110
|
}
|
|
2918
3111
|
var native = getNativeBinding();
|
|
2919
3112
|
function parseFiles(files) {
|
|
@@ -3031,7 +3224,7 @@ var VectorStore = class {
|
|
|
3031
3224
|
var CHARS_PER_TOKEN = 4;
|
|
3032
3225
|
var MAX_BATCH_TOKENS = 7500;
|
|
3033
3226
|
var MAX_SINGLE_CHUNK_TOKENS = 2e3;
|
|
3034
|
-
function
|
|
3227
|
+
function estimateTokens2(text) {
|
|
3035
3228
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3036
3229
|
}
|
|
3037
3230
|
function createEmbeddingText(chunk, filePath) {
|
|
@@ -3096,7 +3289,7 @@ function createDynamicBatches(chunks) {
|
|
|
3096
3289
|
let currentBatch = [];
|
|
3097
3290
|
let currentTokens = 0;
|
|
3098
3291
|
for (const chunk of chunks) {
|
|
3099
|
-
const chunkTokens =
|
|
3292
|
+
const chunkTokens = estimateTokens2(chunk.text);
|
|
3100
3293
|
if (currentBatch.length > 0 && currentTokens + chunkTokens > MAX_BATCH_TOKENS) {
|
|
3101
3294
|
batches.push(currentBatch);
|
|
3102
3295
|
currentBatch = [];
|
|
@@ -3393,26 +3586,55 @@ var Database = class {
|
|
|
3393
3586
|
};
|
|
3394
3587
|
|
|
3395
3588
|
// src/git/index.ts
|
|
3396
|
-
var
|
|
3397
|
-
var
|
|
3398
|
-
|
|
3589
|
+
var import_fs4 = require("fs");
|
|
3590
|
+
var path5 = __toESM(require("path"), 1);
|
|
3591
|
+
function readPackedRefs(gitDir) {
|
|
3592
|
+
const packedRefsPath = path5.join(gitDir, "packed-refs");
|
|
3593
|
+
if (!(0, import_fs4.existsSync)(packedRefsPath)) {
|
|
3594
|
+
return [];
|
|
3595
|
+
}
|
|
3596
|
+
try {
|
|
3597
|
+
return (0, import_fs4.readFileSync)(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
|
|
3598
|
+
} catch {
|
|
3599
|
+
return [];
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
function resolveCommonGitDir(gitDir) {
|
|
3603
|
+
const commonDirPath = path5.join(gitDir, "commondir");
|
|
3604
|
+
if (!(0, import_fs4.existsSync)(commonDirPath)) {
|
|
3605
|
+
return gitDir;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const raw = (0, import_fs4.readFileSync)(commonDirPath, "utf-8").trim();
|
|
3609
|
+
if (!raw) {
|
|
3610
|
+
return gitDir;
|
|
3611
|
+
}
|
|
3612
|
+
const resolved = path5.isAbsolute(raw) ? raw : path5.resolve(gitDir, raw);
|
|
3613
|
+
if ((0, import_fs4.existsSync)(resolved)) {
|
|
3614
|
+
return resolved;
|
|
3615
|
+
}
|
|
3616
|
+
} catch {
|
|
3617
|
+
return gitDir;
|
|
3618
|
+
}
|
|
3619
|
+
return gitDir;
|
|
3620
|
+
}
|
|
3399
3621
|
function resolveGitDir(repoRoot) {
|
|
3400
|
-
const gitPath =
|
|
3401
|
-
if (!(0,
|
|
3622
|
+
const gitPath = path5.join(repoRoot, ".git");
|
|
3623
|
+
if (!(0, import_fs4.existsSync)(gitPath)) {
|
|
3402
3624
|
return null;
|
|
3403
3625
|
}
|
|
3404
3626
|
try {
|
|
3405
|
-
const stat = (0,
|
|
3627
|
+
const stat = (0, import_fs4.statSync)(gitPath);
|
|
3406
3628
|
if (stat.isDirectory()) {
|
|
3407
3629
|
return gitPath;
|
|
3408
3630
|
}
|
|
3409
3631
|
if (stat.isFile()) {
|
|
3410
|
-
const content = (0,
|
|
3632
|
+
const content = (0, import_fs4.readFileSync)(gitPath, "utf-8").trim();
|
|
3411
3633
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3412
3634
|
if (match) {
|
|
3413
3635
|
const gitdir = match[1];
|
|
3414
|
-
const resolvedPath =
|
|
3415
|
-
if ((0,
|
|
3636
|
+
const resolvedPath = path5.isAbsolute(gitdir) ? gitdir : path5.resolve(repoRoot, gitdir);
|
|
3637
|
+
if ((0, import_fs4.existsSync)(resolvedPath)) {
|
|
3416
3638
|
return resolvedPath;
|
|
3417
3639
|
}
|
|
3418
3640
|
}
|
|
@@ -3429,12 +3651,12 @@ function getCurrentBranch(repoRoot) {
|
|
|
3429
3651
|
if (!gitDir) {
|
|
3430
3652
|
return null;
|
|
3431
3653
|
}
|
|
3432
|
-
const headPath =
|
|
3433
|
-
if (!(0,
|
|
3654
|
+
const headPath = path5.join(gitDir, "HEAD");
|
|
3655
|
+
if (!(0, import_fs4.existsSync)(headPath)) {
|
|
3434
3656
|
return null;
|
|
3435
3657
|
}
|
|
3436
3658
|
try {
|
|
3437
|
-
const headContent = (0,
|
|
3659
|
+
const headContent = (0, import_fs4.readFileSync)(headPath, "utf-8").trim();
|
|
3438
3660
|
const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
|
|
3439
3661
|
if (match) {
|
|
3440
3662
|
return match[1];
|
|
@@ -3449,37 +3671,20 @@ function getCurrentBranch(repoRoot) {
|
|
|
3449
3671
|
}
|
|
3450
3672
|
function getBaseBranch(repoRoot) {
|
|
3451
3673
|
const gitDir = resolveGitDir(repoRoot);
|
|
3674
|
+
const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
|
|
3452
3675
|
const candidates = ["main", "master", "develop", "trunk"];
|
|
3453
|
-
if (
|
|
3676
|
+
if (refStoreDir) {
|
|
3454
3677
|
for (const candidate of candidates) {
|
|
3455
|
-
const refPath =
|
|
3456
|
-
if ((0,
|
|
3678
|
+
const refPath = path5.join(refStoreDir, "refs", "heads", candidate);
|
|
3679
|
+
if ((0, import_fs4.existsSync)(refPath)) {
|
|
3457
3680
|
return candidate;
|
|
3458
3681
|
}
|
|
3459
|
-
const
|
|
3460
|
-
if ((
|
|
3461
|
-
|
|
3462
|
-
const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
|
|
3463
|
-
if (content.includes(`refs/heads/${candidate}`)) {
|
|
3464
|
-
return candidate;
|
|
3465
|
-
}
|
|
3466
|
-
} catch {
|
|
3467
|
-
}
|
|
3682
|
+
const packedRefs = readPackedRefs(refStoreDir);
|
|
3683
|
+
if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
|
|
3684
|
+
return candidate;
|
|
3468
3685
|
}
|
|
3469
3686
|
}
|
|
3470
3687
|
}
|
|
3471
|
-
try {
|
|
3472
|
-
const result = (0, import_child_process.execSync)("git remote show origin", {
|
|
3473
|
-
cwd: repoRoot,
|
|
3474
|
-
encoding: "utf-8",
|
|
3475
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3476
|
-
});
|
|
3477
|
-
const match = result.match(/HEAD branch: (.+)/);
|
|
3478
|
-
if (match) {
|
|
3479
|
-
return match[1].trim();
|
|
3480
|
-
}
|
|
3481
|
-
} catch {
|
|
3482
|
-
}
|
|
3483
3688
|
return getCurrentBranch(repoRoot) ?? "main";
|
|
3484
3689
|
}
|
|
3485
3690
|
function getBranchOrDefault(repoRoot) {
|
|
@@ -3490,7 +3695,7 @@ function getBranchOrDefault(repoRoot) {
|
|
|
3490
3695
|
}
|
|
3491
3696
|
|
|
3492
3697
|
// src/indexer/index.ts
|
|
3493
|
-
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
|
|
3698
|
+
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php"]);
|
|
3494
3699
|
var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
3495
3700
|
"function_declaration",
|
|
3496
3701
|
"function",
|
|
@@ -3511,7 +3716,8 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
|
3511
3716
|
"struct_item",
|
|
3512
3717
|
"enum_item",
|
|
3513
3718
|
"trait_item",
|
|
3514
|
-
"mod_item"
|
|
3719
|
+
"mod_item",
|
|
3720
|
+
"trait_declaration"
|
|
3515
3721
|
]);
|
|
3516
3722
|
function float32ArrayToBuffer(arr) {
|
|
3517
3723
|
const float32 = new Float32Array(arr);
|
|
@@ -3893,8 +4099,8 @@ function stripFilePathHint(query) {
|
|
|
3893
4099
|
const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
|
|
3894
4100
|
return stripped.length > 0 ? stripped : query;
|
|
3895
4101
|
}
|
|
3896
|
-
function buildDeterministicIdentifierPass(query, candidates, limit) {
|
|
3897
|
-
if (
|
|
4102
|
+
function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4103
|
+
if (!prioritizeSourcePaths) {
|
|
3898
4104
|
return [];
|
|
3899
4105
|
}
|
|
3900
4106
|
const primary = extractPrimaryIdentifierQueryHint(query);
|
|
@@ -4110,9 +4316,8 @@ function rankHybridResults(query, semanticResults, keywordResults, options) {
|
|
|
4110
4316
|
const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
|
|
4111
4317
|
const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
|
|
4112
4318
|
const rerankPool = fused.slice(0, rerankPoolLimit);
|
|
4113
|
-
const intent = classifyQueryIntentRaw(query);
|
|
4114
4319
|
return rerankResults(query, rerankPool, options.rerankTopN, {
|
|
4115
|
-
prioritizeSourcePaths:
|
|
4320
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source"
|
|
4116
4321
|
});
|
|
4117
4322
|
}
|
|
4118
4323
|
function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
@@ -4122,11 +4327,11 @@ function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
|
4122
4327
|
prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
|
|
4123
4328
|
});
|
|
4124
4329
|
}
|
|
4125
|
-
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
|
|
4330
|
+
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4126
4331
|
if (combined.length === 0) {
|
|
4127
4332
|
return combined;
|
|
4128
4333
|
}
|
|
4129
|
-
if (
|
|
4334
|
+
if (!prioritizeSourcePaths) {
|
|
4130
4335
|
return combined;
|
|
4131
4336
|
}
|
|
4132
4337
|
const identifierHints = extractIdentifierHints(query);
|
|
@@ -4216,8 +4421,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
|
|
|
4216
4421
|
const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
|
|
4217
4422
|
return [...promoted, ...remainder];
|
|
4218
4423
|
}
|
|
4219
|
-
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
|
|
4220
|
-
if (
|
|
4424
|
+
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4425
|
+
if (!prioritizeSourcePaths) {
|
|
4221
4426
|
return [];
|
|
4222
4427
|
}
|
|
4223
4428
|
const identifierHints = extractIdentifierHints(query);
|
|
@@ -4362,8 +4567,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
|
|
|
4362
4567
|
const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4363
4568
|
return withFallback.slice(0, Math.max(limit * 2, limit));
|
|
4364
4569
|
}
|
|
4365
|
-
function buildIdentifierDefinitionLane(query, candidates, limit) {
|
|
4366
|
-
if (
|
|
4570
|
+
function buildIdentifierDefinitionLane(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4571
|
+
if (!prioritizeSourcePaths) {
|
|
4367
4572
|
return [];
|
|
4368
4573
|
}
|
|
4369
4574
|
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
@@ -4448,22 +4653,22 @@ var Indexer = class {
|
|
|
4448
4653
|
this.projectRoot = projectRoot;
|
|
4449
4654
|
this.config = config;
|
|
4450
4655
|
this.indexPath = this.getIndexPath();
|
|
4451
|
-
this.fileHashCachePath =
|
|
4452
|
-
this.failedBatchesPath =
|
|
4453
|
-
this.indexingLockPath =
|
|
4656
|
+
this.fileHashCachePath = path6.join(this.indexPath, "file-hashes.json");
|
|
4657
|
+
this.failedBatchesPath = path6.join(this.indexPath, "failed-batches.json");
|
|
4658
|
+
this.indexingLockPath = path6.join(this.indexPath, "indexing.lock");
|
|
4454
4659
|
this.logger = initializeLogger(config.debug);
|
|
4455
4660
|
}
|
|
4456
4661
|
getIndexPath() {
|
|
4457
4662
|
if (this.config.scope === "global") {
|
|
4458
4663
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
4459
|
-
return
|
|
4664
|
+
return path6.join(homeDir, ".opencode", "global-index");
|
|
4460
4665
|
}
|
|
4461
|
-
return
|
|
4666
|
+
return path6.join(this.projectRoot, ".opencode", "index");
|
|
4462
4667
|
}
|
|
4463
4668
|
loadFileHashCache() {
|
|
4464
4669
|
try {
|
|
4465
|
-
if ((0,
|
|
4466
|
-
const data = (0,
|
|
4670
|
+
if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
|
|
4671
|
+
const data = (0, import_fs5.readFileSync)(this.fileHashCachePath, "utf-8");
|
|
4467
4672
|
const parsed = JSON.parse(data);
|
|
4468
4673
|
this.fileHashCache = new Map(Object.entries(parsed));
|
|
4469
4674
|
}
|
|
@@ -4480,28 +4685,28 @@ var Indexer = class {
|
|
|
4480
4685
|
}
|
|
4481
4686
|
atomicWriteSync(targetPath, data) {
|
|
4482
4687
|
const tempPath = `${targetPath}.tmp`;
|
|
4483
|
-
(0,
|
|
4484
|
-
(0,
|
|
4688
|
+
(0, import_fs5.writeFileSync)(tempPath, data);
|
|
4689
|
+
(0, import_fs5.renameSync)(tempPath, targetPath);
|
|
4485
4690
|
}
|
|
4486
4691
|
checkForInterruptedIndexing() {
|
|
4487
|
-
return (0,
|
|
4692
|
+
return (0, import_fs5.existsSync)(this.indexingLockPath);
|
|
4488
4693
|
}
|
|
4489
4694
|
acquireIndexingLock() {
|
|
4490
4695
|
const lockData = {
|
|
4491
4696
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4492
4697
|
pid: process.pid
|
|
4493
4698
|
};
|
|
4494
|
-
(0,
|
|
4699
|
+
(0, import_fs5.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
|
|
4495
4700
|
}
|
|
4496
4701
|
releaseIndexingLock() {
|
|
4497
|
-
if ((0,
|
|
4498
|
-
(0,
|
|
4702
|
+
if ((0, import_fs5.existsSync)(this.indexingLockPath)) {
|
|
4703
|
+
(0, import_fs5.unlinkSync)(this.indexingLockPath);
|
|
4499
4704
|
}
|
|
4500
4705
|
}
|
|
4501
4706
|
async recoverFromInterruptedIndexing() {
|
|
4502
4707
|
this.logger.warn("Detected interrupted indexing session, recovering...");
|
|
4503
|
-
if ((0,
|
|
4504
|
-
(0,
|
|
4708
|
+
if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
|
|
4709
|
+
(0, import_fs5.unlinkSync)(this.fileHashCachePath);
|
|
4505
4710
|
}
|
|
4506
4711
|
await this.healthCheck();
|
|
4507
4712
|
this.releaseIndexingLock();
|
|
@@ -4509,8 +4714,8 @@ var Indexer = class {
|
|
|
4509
4714
|
}
|
|
4510
4715
|
loadFailedBatches() {
|
|
4511
4716
|
try {
|
|
4512
|
-
if ((0,
|
|
4513
|
-
const data = (0,
|
|
4717
|
+
if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
|
|
4718
|
+
const data = (0, import_fs5.readFileSync)(this.failedBatchesPath, "utf-8");
|
|
4514
4719
|
return JSON.parse(data);
|
|
4515
4720
|
}
|
|
4516
4721
|
} catch {
|
|
@@ -4520,13 +4725,13 @@ var Indexer = class {
|
|
|
4520
4725
|
}
|
|
4521
4726
|
saveFailedBatches(batches) {
|
|
4522
4727
|
if (batches.length === 0) {
|
|
4523
|
-
if ((0,
|
|
4524
|
-
|
|
4728
|
+
if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
|
|
4729
|
+
import_fs5.promises.unlink(this.failedBatchesPath).catch(() => {
|
|
4525
4730
|
});
|
|
4526
4731
|
}
|
|
4527
4732
|
return;
|
|
4528
4733
|
}
|
|
4529
|
-
(0,
|
|
4734
|
+
(0, import_fs5.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
|
|
4530
4735
|
}
|
|
4531
4736
|
addFailedBatch(batch, error) {
|
|
4532
4737
|
const existing = this.loadFailedBatches();
|
|
@@ -4583,26 +4788,26 @@ var Indexer = class {
|
|
|
4583
4788
|
scope: this.config.scope
|
|
4584
4789
|
});
|
|
4585
4790
|
this.provider = createEmbeddingProvider(this.configuredProviderInfo);
|
|
4586
|
-
await
|
|
4791
|
+
await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
|
|
4587
4792
|
const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
|
|
4588
|
-
const storePath =
|
|
4793
|
+
const storePath = path6.join(this.indexPath, "vectors");
|
|
4589
4794
|
this.store = new VectorStore(storePath, dimensions);
|
|
4590
|
-
const indexFilePath =
|
|
4591
|
-
if ((0,
|
|
4795
|
+
const indexFilePath = path6.join(this.indexPath, "vectors.usearch");
|
|
4796
|
+
if ((0, import_fs5.existsSync)(indexFilePath)) {
|
|
4592
4797
|
this.store.load();
|
|
4593
4798
|
}
|
|
4594
|
-
const invertedIndexPath =
|
|
4799
|
+
const invertedIndexPath = path6.join(this.indexPath, "inverted-index.json");
|
|
4595
4800
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
4596
4801
|
try {
|
|
4597
4802
|
this.invertedIndex.load();
|
|
4598
4803
|
} catch {
|
|
4599
|
-
if ((0,
|
|
4600
|
-
await
|
|
4804
|
+
if ((0, import_fs5.existsSync)(invertedIndexPath)) {
|
|
4805
|
+
await import_fs5.promises.unlink(invertedIndexPath);
|
|
4601
4806
|
}
|
|
4602
4807
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
4603
4808
|
}
|
|
4604
|
-
const dbPath =
|
|
4605
|
-
const dbIsNew = !(0,
|
|
4809
|
+
const dbPath = path6.join(this.indexPath, "codebase.db");
|
|
4810
|
+
const dbIsNew = !(0, import_fs5.existsSync)(dbPath);
|
|
4606
4811
|
this.database = new Database(dbPath);
|
|
4607
4812
|
if (this.checkForInterruptedIndexing()) {
|
|
4608
4813
|
await this.recoverFromInterruptedIndexing();
|
|
@@ -4834,7 +5039,7 @@ var Indexer = class {
|
|
|
4834
5039
|
unchangedFilePaths.add(f.path);
|
|
4835
5040
|
this.logger.recordCacheHit();
|
|
4836
5041
|
} else {
|
|
4837
|
-
const content = await
|
|
5042
|
+
const content = await import_fs5.promises.readFile(f.path, "utf-8");
|
|
4838
5043
|
changedFiles.push({ path: f.path, content, hash: currentHash });
|
|
4839
5044
|
this.logger.recordCacheMiss();
|
|
4840
5045
|
}
|
|
@@ -4880,7 +5085,7 @@ var Indexer = class {
|
|
|
4880
5085
|
for (const parsed of parsedFiles) {
|
|
4881
5086
|
currentFilePaths.add(parsed.path);
|
|
4882
5087
|
if (parsed.chunks.length === 0) {
|
|
4883
|
-
const relativePath =
|
|
5088
|
+
const relativePath = path6.relative(this.projectRoot, parsed.path);
|
|
4884
5089
|
stats.parseFailures.push(relativePath);
|
|
4885
5090
|
}
|
|
4886
5091
|
let fileChunkCount = 0;
|
|
@@ -5091,7 +5296,7 @@ var Indexer = class {
|
|
|
5091
5296
|
for (const batch of dynamicBatches) {
|
|
5092
5297
|
queue.add(async () => {
|
|
5093
5298
|
if (rateLimitBackoffMs > 0) {
|
|
5094
|
-
await new Promise((
|
|
5299
|
+
await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
|
|
5095
5300
|
}
|
|
5096
5301
|
try {
|
|
5097
5302
|
const result = await pRetry(
|
|
@@ -5295,6 +5500,7 @@ var Indexer = class {
|
|
|
5295
5500
|
const rrfK = this.config.search.rrfK;
|
|
5296
5501
|
const rerankTopN = this.config.search.rerankTopN;
|
|
5297
5502
|
const filterByBranch = options?.filterByBranch ?? true;
|
|
5503
|
+
const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
|
|
5298
5504
|
this.logger.search("debug", "Starting search", {
|
|
5299
5505
|
query,
|
|
5300
5506
|
maxResults,
|
|
@@ -5346,7 +5552,8 @@ var Indexer = class {
|
|
|
5346
5552
|
rrfK,
|
|
5347
5553
|
rerankTopN,
|
|
5348
5554
|
limit: maxResults,
|
|
5349
|
-
hybridWeight
|
|
5555
|
+
hybridWeight,
|
|
5556
|
+
prioritizeSourcePaths: sourceIntent
|
|
5350
5557
|
});
|
|
5351
5558
|
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
5352
5559
|
const rescued = promoteIdentifierMatches(
|
|
@@ -5355,30 +5562,33 @@ var Indexer = class {
|
|
|
5355
5562
|
semanticCandidates,
|
|
5356
5563
|
keywordCandidates,
|
|
5357
5564
|
database,
|
|
5358
|
-
branchChunkIds
|
|
5565
|
+
branchChunkIds,
|
|
5566
|
+
sourceIntent
|
|
5359
5567
|
);
|
|
5360
5568
|
const union = unionCandidates(semanticCandidates, keywordCandidates);
|
|
5361
5569
|
const deterministicIdentifierLane = buildDeterministicIdentifierPass(
|
|
5362
5570
|
query,
|
|
5363
5571
|
union,
|
|
5364
|
-
maxResults
|
|
5572
|
+
maxResults,
|
|
5573
|
+
sourceIntent
|
|
5365
5574
|
);
|
|
5366
5575
|
const identifierLane = buildIdentifierDefinitionLane(
|
|
5367
5576
|
query,
|
|
5368
5577
|
union,
|
|
5369
|
-
maxResults
|
|
5578
|
+
maxResults,
|
|
5579
|
+
sourceIntent
|
|
5370
5580
|
);
|
|
5371
5581
|
const symbolLane = buildSymbolDefinitionLane(
|
|
5372
5582
|
query,
|
|
5373
5583
|
database,
|
|
5374
5584
|
branchChunkIds,
|
|
5375
5585
|
maxResults,
|
|
5376
|
-
union
|
|
5586
|
+
union,
|
|
5587
|
+
sourceIntent
|
|
5377
5588
|
);
|
|
5378
5589
|
const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
|
|
5379
5590
|
const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
|
|
5380
5591
|
const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
|
|
5381
|
-
const sourceIntent = classifyQueryIntentRaw(query) === "source";
|
|
5382
5592
|
const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
|
|
5383
5593
|
const baseFiltered = tiered.filter((r) => {
|
|
5384
5594
|
if (r.score < this.config.search.minScore) return false;
|
|
@@ -5424,7 +5634,7 @@ var Indexer = class {
|
|
|
5424
5634
|
let contextEndLine = r.metadata.endLine;
|
|
5425
5635
|
if (!metadataOnly && this.config.search.includeContext) {
|
|
5426
5636
|
try {
|
|
5427
|
-
const fileContent = await
|
|
5637
|
+
const fileContent = await import_fs5.promises.readFile(
|
|
5428
5638
|
r.metadata.filePath,
|
|
5429
5639
|
"utf-8"
|
|
5430
5640
|
);
|
|
@@ -5511,7 +5721,7 @@ var Indexer = class {
|
|
|
5511
5721
|
const removedFilePaths = [];
|
|
5512
5722
|
let removedCount = 0;
|
|
5513
5723
|
for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
|
|
5514
|
-
if (!(0,
|
|
5724
|
+
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
5515
5725
|
for (const key of chunkKeys) {
|
|
5516
5726
|
store.remove(key);
|
|
5517
5727
|
invertedIndex.removeChunk(key);
|
|
@@ -5703,7 +5913,7 @@ var Indexer = class {
|
|
|
5703
5913
|
let content = "";
|
|
5704
5914
|
if (this.config.search.includeContext) {
|
|
5705
5915
|
try {
|
|
5706
|
-
const fileContent = await
|
|
5916
|
+
const fileContent = await import_fs5.promises.readFile(
|
|
5707
5917
|
r.metadata.filePath,
|
|
5708
5918
|
"utf-8"
|
|
5709
5919
|
);
|
|
@@ -5735,7 +5945,896 @@ var Indexer = class {
|
|
|
5735
5945
|
}
|
|
5736
5946
|
};
|
|
5737
5947
|
|
|
5948
|
+
// src/eval/budget.ts
|
|
5949
|
+
function evaluateBudgetGate(budget, summary, comparison) {
|
|
5950
|
+
const BASELINE_P95_EPSILON_MS = 1e-3;
|
|
5951
|
+
const violations = [];
|
|
5952
|
+
const { thresholds } = budget;
|
|
5953
|
+
if (thresholds.minHitAt5 !== void 0 && summary.metrics.hitAt5 < thresholds.minHitAt5) {
|
|
5954
|
+
violations.push({
|
|
5955
|
+
metric: "minHitAt5",
|
|
5956
|
+
message: `Hit@5 ${summary.metrics.hitAt5.toFixed(4)} is below minimum ${thresholds.minHitAt5.toFixed(4)}`
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
if (thresholds.minMrrAt10 !== void 0 && summary.metrics.mrrAt10 < thresholds.minMrrAt10) {
|
|
5960
|
+
violations.push({
|
|
5961
|
+
metric: "minMrrAt10",
|
|
5962
|
+
message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
if (comparison) {
|
|
5966
|
+
if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
|
|
5967
|
+
violations.push({
|
|
5968
|
+
metric: "hitAt5MaxDrop",
|
|
5969
|
+
message: `Hit@5 drop ${comparison.deltas.hitAt5.absolute.toFixed(4)} exceeds allowed -${thresholds.hitAt5MaxDrop.toFixed(4)}`
|
|
5970
|
+
});
|
|
5971
|
+
}
|
|
5972
|
+
if (thresholds.mrrAt10MaxDrop !== void 0 && comparison.deltas.mrrAt10.absolute < -thresholds.mrrAt10MaxDrop) {
|
|
5973
|
+
violations.push({
|
|
5974
|
+
metric: "mrrAt10MaxDrop",
|
|
5975
|
+
message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
|
|
5976
|
+
});
|
|
5977
|
+
}
|
|
5978
|
+
if (thresholds.p95LatencyMaxMultiplier !== void 0) {
|
|
5979
|
+
const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
|
|
5980
|
+
if (baselineP95 > BASELINE_P95_EPSILON_MS) {
|
|
5981
|
+
const allowed = baselineP95 * thresholds.p95LatencyMaxMultiplier;
|
|
5982
|
+
if (summary.metrics.latencyMs.p95 > allowed) {
|
|
5983
|
+
violations.push({
|
|
5984
|
+
metric: "p95LatencyMaxMultiplier",
|
|
5985
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds allowed ${allowed.toFixed(3)}ms (${thresholds.p95LatencyMaxMultiplier.toFixed(2)}x baseline)`
|
|
5986
|
+
});
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
if (thresholds.p95LatencyMaxAbsoluteMs !== void 0 && summary.metrics.latencyMs.p95 > thresholds.p95LatencyMaxAbsoluteMs) {
|
|
5992
|
+
violations.push({
|
|
5993
|
+
metric: "p95LatencyMaxAbsoluteMs",
|
|
5994
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds absolute maximum ${thresholds.p95LatencyMaxAbsoluteMs.toFixed(3)}ms`
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
return {
|
|
5998
|
+
passed: violations.length === 0,
|
|
5999
|
+
budgetName: budget.name,
|
|
6000
|
+
violations
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
// src/eval/metrics.ts
|
|
6005
|
+
function percentile(values, p) {
|
|
6006
|
+
if (values.length === 0) return 0;
|
|
6007
|
+
if (values.length === 1) return values[0];
|
|
6008
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6009
|
+
const x = p * (sorted.length - 1);
|
|
6010
|
+
const lowerIndex = Math.floor(x);
|
|
6011
|
+
const upperIndex = Math.ceil(x);
|
|
6012
|
+
if (lowerIndex === upperIndex) {
|
|
6013
|
+
return sorted[lowerIndex];
|
|
6014
|
+
}
|
|
6015
|
+
const fraction = x - lowerIndex;
|
|
6016
|
+
return sorted[lowerIndex] + fraction * (sorted[upperIndex] - sorted[lowerIndex]);
|
|
6017
|
+
}
|
|
6018
|
+
function normalizePath(input) {
|
|
6019
|
+
return input.replace(/\\/g, "/");
|
|
6020
|
+
}
|
|
6021
|
+
function uniqueResultsByPath(results) {
|
|
6022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6023
|
+
const unique = [];
|
|
6024
|
+
for (const result of results) {
|
|
6025
|
+
const normalized = normalizePath(result.filePath);
|
|
6026
|
+
if (seen.has(normalized)) continue;
|
|
6027
|
+
seen.add(normalized);
|
|
6028
|
+
unique.push(result);
|
|
6029
|
+
}
|
|
6030
|
+
return unique;
|
|
6031
|
+
}
|
|
6032
|
+
function pathMatchesExpected(actualPath, expectedPath) {
|
|
6033
|
+
const actual = normalizePath(actualPath);
|
|
6034
|
+
const expected = normalizePath(expectedPath);
|
|
6035
|
+
if (actual === expected) return true;
|
|
6036
|
+
return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
|
|
6037
|
+
}
|
|
6038
|
+
function getRelevantPaths(query) {
|
|
6039
|
+
const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
|
|
6040
|
+
const fromAcceptable = query.expected.acceptableFiles ?? [];
|
|
6041
|
+
return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
|
|
6042
|
+
}
|
|
6043
|
+
function isRelevantResult(filePath, relevantPaths) {
|
|
6044
|
+
return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
|
|
6045
|
+
}
|
|
6046
|
+
function reciprocalRankAtK(results, relevantPaths, k) {
|
|
6047
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6048
|
+
for (let i = 0; i < top.length; i += 1) {
|
|
6049
|
+
if (isRelevantResult(top[i].filePath, relevantPaths)) {
|
|
6050
|
+
return 1 / (i + 1);
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
return 0;
|
|
6054
|
+
}
|
|
6055
|
+
function ndcgAtK(results, relevantPaths, k) {
|
|
6056
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6057
|
+
const dcg = top.reduce((sum, result, i) => {
|
|
6058
|
+
const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
|
|
6059
|
+
return sum + rel / Math.log2(i + 2);
|
|
6060
|
+
}, 0);
|
|
6061
|
+
const idealLen = Math.min(k, relevantPaths.length);
|
|
6062
|
+
const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
|
|
6063
|
+
(sum, value) => sum + value,
|
|
6064
|
+
0
|
|
6065
|
+
);
|
|
6066
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
6067
|
+
}
|
|
6068
|
+
function isDocsOrTestsPath(filePath) {
|
|
6069
|
+
const lowered = normalizePath(filePath).toLowerCase();
|
|
6070
|
+
return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
|
|
6071
|
+
}
|
|
6072
|
+
function classifyFailureBucket(query, results, k) {
|
|
6073
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6074
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6075
|
+
const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6076
|
+
if (!hasRelevantTopK) {
|
|
6077
|
+
return "no-relevant-hit-top-k";
|
|
6078
|
+
}
|
|
6079
|
+
if (query.expected.symbol) {
|
|
6080
|
+
const hasSymbol = top.some(
|
|
6081
|
+
(result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
|
|
6082
|
+
);
|
|
6083
|
+
if (!hasSymbol) return "wrong-symbol";
|
|
6084
|
+
}
|
|
6085
|
+
const top1 = top[0];
|
|
6086
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
|
|
6087
|
+
return "docs-tests-outranking-source";
|
|
6088
|
+
}
|
|
6089
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
|
|
6090
|
+
return "wrong-file";
|
|
6091
|
+
}
|
|
6092
|
+
return void 0;
|
|
6093
|
+
}
|
|
6094
|
+
function buildPerQueryResult(query, results, latencyMs, k) {
|
|
6095
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6096
|
+
const deduped = uniqueResultsByPath(results);
|
|
6097
|
+
const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6098
|
+
const perQuery = {
|
|
6099
|
+
id: query.id,
|
|
6100
|
+
query: query.query,
|
|
6101
|
+
queryType: query.queryType,
|
|
6102
|
+
latencyMs,
|
|
6103
|
+
hitAt1: hitAt(1),
|
|
6104
|
+
hitAt3: hitAt(3),
|
|
6105
|
+
hitAt5: hitAt(5),
|
|
6106
|
+
hitAt10: hitAt(10),
|
|
6107
|
+
reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
|
|
6108
|
+
ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
|
|
6109
|
+
failureBucket: classifyFailureBucket(query, results, k),
|
|
6110
|
+
results: deduped
|
|
6111
|
+
};
|
|
6112
|
+
return perQuery;
|
|
6113
|
+
}
|
|
6114
|
+
function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
|
|
6115
|
+
const count = perQuery.length;
|
|
6116
|
+
const safeDiv = (value) => count === 0 ? 0 : value / count;
|
|
6117
|
+
const sum = {
|
|
6118
|
+
hitAt1: 0,
|
|
6119
|
+
hitAt3: 0,
|
|
6120
|
+
hitAt5: 0,
|
|
6121
|
+
hitAt10: 0,
|
|
6122
|
+
mrrAt10: 0,
|
|
6123
|
+
ndcgAt10: 0
|
|
6124
|
+
};
|
|
6125
|
+
const failureBuckets = {
|
|
6126
|
+
"wrong-file": 0,
|
|
6127
|
+
"wrong-symbol": 0,
|
|
6128
|
+
"docs-tests-outranking-source": 0,
|
|
6129
|
+
"no-relevant-hit-top-k": 0
|
|
6130
|
+
};
|
|
6131
|
+
const latencies = perQuery.map((item) => item.latencyMs);
|
|
6132
|
+
for (const query of perQuery) {
|
|
6133
|
+
if (query.hitAt1) sum.hitAt1 += 1;
|
|
6134
|
+
if (query.hitAt3) sum.hitAt3 += 1;
|
|
6135
|
+
if (query.hitAt5) sum.hitAt5 += 1;
|
|
6136
|
+
if (query.hitAt10) sum.hitAt10 += 1;
|
|
6137
|
+
sum.mrrAt10 += query.reciprocalRankAt10;
|
|
6138
|
+
sum.ndcgAt10 += query.ndcgAt10;
|
|
6139
|
+
if (query.failureBucket) {
|
|
6140
|
+
failureBuckets[query.failureBucket] += 1;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
|
|
6144
|
+
return {
|
|
6145
|
+
hitAt1: safeDiv(sum.hitAt1),
|
|
6146
|
+
hitAt3: safeDiv(sum.hitAt3),
|
|
6147
|
+
hitAt5: safeDiv(sum.hitAt5),
|
|
6148
|
+
hitAt10: safeDiv(sum.hitAt10),
|
|
6149
|
+
mrrAt10: safeDiv(sum.mrrAt10),
|
|
6150
|
+
ndcgAt10: safeDiv(sum.ndcgAt10),
|
|
6151
|
+
latencyMs: {
|
|
6152
|
+
p50: percentile(latencies, 0.5),
|
|
6153
|
+
p95: percentile(latencies, 0.95),
|
|
6154
|
+
p99: percentile(latencies, 0.99)
|
|
6155
|
+
},
|
|
6156
|
+
tokenEstimate: {
|
|
6157
|
+
queryTokens,
|
|
6158
|
+
embeddingTokensUsed
|
|
6159
|
+
},
|
|
6160
|
+
embedding: {
|
|
6161
|
+
callCount: embeddingCallCount,
|
|
6162
|
+
estimatedCostUsd: embeddingTokensUsed / 1e6 * costPer1MTokensUsd,
|
|
6163
|
+
costPer1MTokensUsd
|
|
6164
|
+
},
|
|
6165
|
+
failureBuckets
|
|
6166
|
+
};
|
|
6167
|
+
}
|
|
6168
|
+
|
|
6169
|
+
// src/eval/schema.ts
|
|
6170
|
+
var import_fs6 = require("fs");
|
|
6171
|
+
function parseJsonFile(filePath) {
|
|
6172
|
+
const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
6173
|
+
return JSON.parse(content);
|
|
6174
|
+
}
|
|
6175
|
+
function isRecord(value) {
|
|
6176
|
+
return typeof value === "object" && value !== null;
|
|
6177
|
+
}
|
|
6178
|
+
function isStringArray2(value) {
|
|
6179
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6180
|
+
}
|
|
6181
|
+
function asPositiveNumber(value, path10) {
|
|
6182
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
6183
|
+
throw new Error(`${path10} must be a non-negative number`);
|
|
6184
|
+
}
|
|
6185
|
+
return value;
|
|
6186
|
+
}
|
|
6187
|
+
function parseQueryType(value, path10) {
|
|
6188
|
+
if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
|
|
6189
|
+
return value;
|
|
6190
|
+
}
|
|
6191
|
+
throw new Error(
|
|
6192
|
+
`${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
function parseExpected(input, path10) {
|
|
6196
|
+
if (!isRecord(input)) {
|
|
6197
|
+
throw new Error(`${path10} must be an object`);
|
|
6198
|
+
}
|
|
6199
|
+
const filePathRaw = input.filePath;
|
|
6200
|
+
const acceptableFilesRaw = input.acceptableFiles;
|
|
6201
|
+
const symbolRaw = input.symbol;
|
|
6202
|
+
const branchRaw = input.branch;
|
|
6203
|
+
const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
|
|
6204
|
+
const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
|
|
6205
|
+
if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
|
|
6206
|
+
throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
|
|
6207
|
+
}
|
|
6208
|
+
if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
|
|
6209
|
+
throw new Error(`${path10}.acceptableFiles must be an array of strings`);
|
|
6210
|
+
}
|
|
6211
|
+
if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
|
|
6212
|
+
throw new Error(`${path10}.symbol must be a string when provided`);
|
|
6213
|
+
}
|
|
6214
|
+
if (branchRaw !== void 0 && typeof branchRaw !== "string") {
|
|
6215
|
+
throw new Error(`${path10}.branch must be a string when provided`);
|
|
6216
|
+
}
|
|
6217
|
+
return {
|
|
6218
|
+
filePath,
|
|
6219
|
+
acceptableFiles,
|
|
6220
|
+
symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
|
|
6221
|
+
branch: typeof branchRaw === "string" ? branchRaw : void 0
|
|
6222
|
+
};
|
|
6223
|
+
}
|
|
6224
|
+
function parseQuery(input, index) {
|
|
6225
|
+
const path10 = `queries[${index}]`;
|
|
6226
|
+
if (!isRecord(input)) {
|
|
6227
|
+
throw new Error(`${path10} must be an object`);
|
|
6228
|
+
}
|
|
6229
|
+
const id = input.id;
|
|
6230
|
+
const query = input.query;
|
|
6231
|
+
const queryType = input.queryType;
|
|
6232
|
+
const expected = input.expected;
|
|
6233
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
6234
|
+
throw new Error(`${path10}.id must be a non-empty string`);
|
|
6235
|
+
}
|
|
6236
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
6237
|
+
throw new Error(`${path10}.query must be a non-empty string`);
|
|
6238
|
+
}
|
|
6239
|
+
return {
|
|
6240
|
+
id,
|
|
6241
|
+
query,
|
|
6242
|
+
queryType: parseQueryType(queryType, `${path10}.queryType`),
|
|
6243
|
+
expected: parseExpected(expected, `${path10}.expected`)
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
function parseGoldenDataset(raw, sourceLabel) {
|
|
6247
|
+
if (!isRecord(raw)) {
|
|
6248
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6249
|
+
}
|
|
6250
|
+
const version = raw.version;
|
|
6251
|
+
const name = raw.name;
|
|
6252
|
+
const description = raw.description;
|
|
6253
|
+
const queriesRaw = raw.queries;
|
|
6254
|
+
if (typeof version !== "string" || version.trim().length === 0) {
|
|
6255
|
+
throw new Error(`${sourceLabel}.version must be a non-empty string`);
|
|
6256
|
+
}
|
|
6257
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6258
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6259
|
+
}
|
|
6260
|
+
if (description !== void 0 && typeof description !== "string") {
|
|
6261
|
+
throw new Error(`${sourceLabel}.description must be a string when provided`);
|
|
6262
|
+
}
|
|
6263
|
+
if (!Array.isArray(queriesRaw)) {
|
|
6264
|
+
throw new Error(`${sourceLabel}.queries must be an array`);
|
|
6265
|
+
}
|
|
6266
|
+
if (queriesRaw.length === 0) {
|
|
6267
|
+
throw new Error(`${sourceLabel}.queries must contain at least one query`);
|
|
6268
|
+
}
|
|
6269
|
+
const queries = queriesRaw.map((query, idx) => parseQuery(query, idx));
|
|
6270
|
+
const idSet = /* @__PURE__ */ new Set();
|
|
6271
|
+
for (const query of queries) {
|
|
6272
|
+
if (idSet.has(query.id)) {
|
|
6273
|
+
throw new Error(`${sourceLabel}.queries has duplicate id: ${query.id}`);
|
|
6274
|
+
}
|
|
6275
|
+
idSet.add(query.id);
|
|
6276
|
+
}
|
|
6277
|
+
return {
|
|
6278
|
+
version,
|
|
6279
|
+
name,
|
|
6280
|
+
description: typeof description === "string" ? description : void 0,
|
|
6281
|
+
queries
|
|
6282
|
+
};
|
|
6283
|
+
}
|
|
6284
|
+
function loadGoldenDataset(datasetPath) {
|
|
6285
|
+
const parsed = parseJsonFile(datasetPath);
|
|
6286
|
+
return parseGoldenDataset(parsed, datasetPath);
|
|
6287
|
+
}
|
|
6288
|
+
function parseBudget(raw, sourceLabel) {
|
|
6289
|
+
if (!isRecord(raw)) {
|
|
6290
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6291
|
+
}
|
|
6292
|
+
const name = raw.name;
|
|
6293
|
+
const baselinePath = raw.baselinePath;
|
|
6294
|
+
const failOnMissingBaseline = raw.failOnMissingBaseline;
|
|
6295
|
+
const thresholds = raw.thresholds;
|
|
6296
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6297
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6298
|
+
}
|
|
6299
|
+
if (baselinePath !== void 0 && typeof baselinePath !== "string") {
|
|
6300
|
+
throw new Error(`${sourceLabel}.baselinePath must be a string when provided`);
|
|
6301
|
+
}
|
|
6302
|
+
if (!isRecord(thresholds)) {
|
|
6303
|
+
throw new Error(`${sourceLabel}.thresholds must be an object`);
|
|
6304
|
+
}
|
|
6305
|
+
return {
|
|
6306
|
+
name,
|
|
6307
|
+
baselinePath: typeof baselinePath === "string" ? baselinePath : void 0,
|
|
6308
|
+
failOnMissingBaseline: typeof failOnMissingBaseline === "boolean" ? failOnMissingBaseline : true,
|
|
6309
|
+
thresholds: {
|
|
6310
|
+
hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
|
|
6311
|
+
mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
|
|
6312
|
+
p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
|
|
6313
|
+
thresholds.p95LatencyMaxMultiplier,
|
|
6314
|
+
`${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
|
|
6315
|
+
),
|
|
6316
|
+
p95LatencyMaxAbsoluteMs: thresholds.p95LatencyMaxAbsoluteMs === void 0 ? void 0 : asPositiveNumber(
|
|
6317
|
+
thresholds.p95LatencyMaxAbsoluteMs,
|
|
6318
|
+
`${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
|
|
6319
|
+
),
|
|
6320
|
+
minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
|
|
6321
|
+
minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
function loadBudget(budgetPath) {
|
|
6326
|
+
const parsed = parseJsonFile(budgetPath);
|
|
6327
|
+
return parseBudget(parsed, budgetPath);
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
// src/eval/runner.ts
|
|
6331
|
+
function toAbsolute(projectRoot, maybeRelative) {
|
|
6332
|
+
return path7.isAbsolute(maybeRelative) ? maybeRelative : path7.join(projectRoot, maybeRelative);
|
|
6333
|
+
}
|
|
6334
|
+
function loadRawConfig(projectRoot, configPath) {
|
|
6335
|
+
const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
|
|
6336
|
+
if (fromPath && (0, import_fs7.existsSync)(fromPath)) {
|
|
6337
|
+
return JSON.parse((0, import_fs8.readFileSync)(fromPath, "utf-8"));
|
|
6338
|
+
}
|
|
6339
|
+
const projectConfig = path7.join(projectRoot, ".opencode", "codebase-index.json");
|
|
6340
|
+
if ((0, import_fs7.existsSync)(projectConfig)) {
|
|
6341
|
+
return JSON.parse((0, import_fs8.readFileSync)(projectConfig, "utf-8"));
|
|
6342
|
+
}
|
|
6343
|
+
const globalConfig = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
|
|
6344
|
+
if ((0, import_fs7.existsSync)(globalConfig)) {
|
|
6345
|
+
return JSON.parse((0, import_fs8.readFileSync)(globalConfig, "utf-8"));
|
|
6346
|
+
}
|
|
6347
|
+
return {};
|
|
6348
|
+
}
|
|
6349
|
+
function getIndexRootPath(projectRoot, scope) {
|
|
6350
|
+
if (scope === "global") {
|
|
6351
|
+
return path7.join(os3.homedir(), ".opencode", "global-index");
|
|
6352
|
+
}
|
|
6353
|
+
return path7.join(projectRoot, ".opencode", "index");
|
|
6354
|
+
}
|
|
6355
|
+
function clearIndexRoot(projectRoot, scope) {
|
|
6356
|
+
const indexRoot = getIndexRootPath(projectRoot, scope);
|
|
6357
|
+
if ((0, import_fs7.existsSync)(indexRoot)) {
|
|
6358
|
+
(0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
function loadParsedConfig(projectRoot, configPath) {
|
|
6362
|
+
const raw = loadRawConfig(projectRoot, configPath);
|
|
6363
|
+
return parseConfig(raw);
|
|
6364
|
+
}
|
|
6365
|
+
function resolveSearchConfig(parsedConfig, overrides) {
|
|
6366
|
+
const nextSearch = {
|
|
6367
|
+
...parsedConfig.search
|
|
6368
|
+
};
|
|
6369
|
+
if (overrides?.fusionStrategy !== void 0) {
|
|
6370
|
+
nextSearch.fusionStrategy = overrides.fusionStrategy;
|
|
6371
|
+
}
|
|
6372
|
+
if (overrides?.hybridWeight !== void 0) {
|
|
6373
|
+
nextSearch.hybridWeight = overrides.hybridWeight;
|
|
6374
|
+
}
|
|
6375
|
+
if (overrides?.rrfK !== void 0) {
|
|
6376
|
+
nextSearch.rrfK = overrides.rrfK;
|
|
6377
|
+
}
|
|
6378
|
+
if (overrides?.rerankTopN !== void 0) {
|
|
6379
|
+
nextSearch.rerankTopN = overrides.rerankTopN;
|
|
6380
|
+
}
|
|
6381
|
+
return {
|
|
6382
|
+
...parsedConfig,
|
|
6383
|
+
search: nextSearch
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
async function runEvaluation(options) {
|
|
6387
|
+
const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
|
|
6388
|
+
const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
|
|
6389
|
+
const budgetPath = options.budgetPath ? toAbsolute(options.projectRoot, options.budgetPath) : void 0;
|
|
6390
|
+
const dataset = loadGoldenDataset(datasetPath);
|
|
6391
|
+
const parsedConfig = loadParsedConfig(options.projectRoot, options.configPath);
|
|
6392
|
+
const effectiveConfig = resolveSearchConfig(parsedConfig, options.searchOverrides);
|
|
6393
|
+
if (options.reindex) {
|
|
6394
|
+
clearIndexRoot(options.projectRoot, effectiveConfig.scope);
|
|
6395
|
+
}
|
|
6396
|
+
const indexer = new Indexer(options.projectRoot, effectiveConfig);
|
|
6397
|
+
await indexer.index();
|
|
6398
|
+
const perQuery = [];
|
|
6399
|
+
for (const query of dataset.queries) {
|
|
6400
|
+
if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
|
|
6401
|
+
throw new Error(
|
|
6402
|
+
`Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
|
|
6403
|
+
);
|
|
6404
|
+
}
|
|
6405
|
+
const start = import_perf_hooks2.performance.now();
|
|
6406
|
+
const result = await indexer.search(query.query, 10, {
|
|
6407
|
+
metadataOnly: true,
|
|
6408
|
+
filterByBranch: query.expected.branch ? true : false
|
|
6409
|
+
});
|
|
6410
|
+
const elapsed = import_perf_hooks2.performance.now() - start;
|
|
6411
|
+
const materialized = result.map((item) => ({
|
|
6412
|
+
filePath: item.filePath,
|
|
6413
|
+
startLine: item.startLine,
|
|
6414
|
+
endLine: item.endLine,
|
|
6415
|
+
score: item.score,
|
|
6416
|
+
chunkType: item.chunkType,
|
|
6417
|
+
name: item.name
|
|
6418
|
+
}));
|
|
6419
|
+
perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
|
|
6420
|
+
}
|
|
6421
|
+
const logger = indexer.getLogger();
|
|
6422
|
+
const metricSnapshot = logger.getMetrics();
|
|
6423
|
+
const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
|
|
6424
|
+
const summary = {
|
|
6425
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6426
|
+
projectRoot: options.projectRoot,
|
|
6427
|
+
datasetPath,
|
|
6428
|
+
datasetName: dataset.name,
|
|
6429
|
+
datasetVersion: dataset.version,
|
|
6430
|
+
queryCount: dataset.queries.length,
|
|
6431
|
+
topK: 10,
|
|
6432
|
+
searchConfig: {
|
|
6433
|
+
fusionStrategy: effectiveConfig.search.fusionStrategy,
|
|
6434
|
+
hybridWeight: effectiveConfig.search.hybridWeight,
|
|
6435
|
+
rrfK: effectiveConfig.search.rrfK,
|
|
6436
|
+
rerankTopN: effectiveConfig.search.rerankTopN
|
|
6437
|
+
},
|
|
6438
|
+
metrics: computeEvalMetrics(
|
|
6439
|
+
dataset.queries,
|
|
6440
|
+
perQuery,
|
|
6441
|
+
metricSnapshot.embeddingApiCalls,
|
|
6442
|
+
metricSnapshot.embeddingTokensUsed,
|
|
6443
|
+
costPer1MTokensUsd
|
|
6444
|
+
)
|
|
6445
|
+
};
|
|
6446
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6447
|
+
const perQueryArtifact = buildPerQueryArtifact(perQuery);
|
|
6448
|
+
writeJson(path7.join(outputDir, "summary.json"), summary);
|
|
6449
|
+
writeJson(path7.join(outputDir, "per-query.json"), perQueryArtifact);
|
|
6450
|
+
let comparison;
|
|
6451
|
+
if (againstPath) {
|
|
6452
|
+
const baseline = loadSummary(againstPath);
|
|
6453
|
+
comparison = compareSummaries(summary, baseline, againstPath);
|
|
6454
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6455
|
+
}
|
|
6456
|
+
let gate;
|
|
6457
|
+
if (options.ciMode) {
|
|
6458
|
+
if (!budgetPath) {
|
|
6459
|
+
throw new Error("CI mode requires --budget path");
|
|
6460
|
+
}
|
|
6461
|
+
const budget = loadBudget(budgetPath);
|
|
6462
|
+
if (!comparison && budget.baselinePath) {
|
|
6463
|
+
const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
|
|
6464
|
+
if ((0, import_fs7.existsSync)(resolvedBaseline)) {
|
|
6465
|
+
const baselineSummary = loadSummary(resolvedBaseline);
|
|
6466
|
+
comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
|
|
6467
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6468
|
+
} else if (budget.failOnMissingBaseline) {
|
|
6469
|
+
throw new Error(
|
|
6470
|
+
`Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
|
|
6471
|
+
);
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
gate = evaluateBudgetGate(budget, summary, comparison);
|
|
6475
|
+
}
|
|
6476
|
+
const markdown = createSummaryMarkdown(summary, comparison, gate);
|
|
6477
|
+
writeText(path7.join(outputDir, "summary.md"), markdown);
|
|
6478
|
+
return { outputDir, summary, perQuery, comparison, gate };
|
|
6479
|
+
}
|
|
6480
|
+
async function runSweep(options, sweep) {
|
|
6481
|
+
const fusionValues = sweep.fusionStrategy && sweep.fusionStrategy.length > 0 ? [...sweep.fusionStrategy] : [void 0];
|
|
6482
|
+
const weightValues = sweep.hybridWeight && sweep.hybridWeight.length > 0 ? [...sweep.hybridWeight] : [void 0];
|
|
6483
|
+
const rrfValues = sweep.rrfK && sweep.rrfK.length > 0 ? [...sweep.rrfK] : [void 0];
|
|
6484
|
+
const rerankValues = sweep.rerankTopN && sweep.rerankTopN.length > 0 ? [...sweep.rerankTopN] : [void 0];
|
|
6485
|
+
const runs = [];
|
|
6486
|
+
for (const fusion of fusionValues) {
|
|
6487
|
+
for (const hybridWeight of weightValues) {
|
|
6488
|
+
for (const rrfK of rrfValues) {
|
|
6489
|
+
for (const rerankTopN of rerankValues) {
|
|
6490
|
+
const run = await runEvaluation({
|
|
6491
|
+
...options,
|
|
6492
|
+
searchOverrides: {
|
|
6493
|
+
...fusion !== void 0 ? { fusionStrategy: fusion } : {},
|
|
6494
|
+
...hybridWeight !== void 0 ? { hybridWeight } : {},
|
|
6495
|
+
...rrfK !== void 0 ? { rrfK } : {},
|
|
6496
|
+
...rerankTopN !== void 0 ? { rerankTopN } : {}
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
runs.push({
|
|
6500
|
+
searchConfig: run.summary.searchConfig,
|
|
6501
|
+
summary: run.summary,
|
|
6502
|
+
comparison: run.comparison,
|
|
6503
|
+
gate: run.gate
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
const bestByHitAt5 = [...runs].sort(
|
|
6510
|
+
(a, b) => b.summary.metrics.hitAt5 - a.summary.metrics.hitAt5
|
|
6511
|
+
)[0];
|
|
6512
|
+
const bestByMrrAt10 = [...runs].sort(
|
|
6513
|
+
(a, b) => b.summary.metrics.mrrAt10 - a.summary.metrics.mrrAt10
|
|
6514
|
+
)[0];
|
|
6515
|
+
const bestByP95Latency = [...runs].sort(
|
|
6516
|
+
(a, b) => a.summary.metrics.latencyMs.p95 - b.summary.metrics.latencyMs.p95
|
|
6517
|
+
)[0];
|
|
6518
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6519
|
+
const failedGateRuns = runs.filter((run) => run.gate && !run.gate.passed).length;
|
|
6520
|
+
const gatePassed = failedGateRuns === 0;
|
|
6521
|
+
const aggregate = {
|
|
6522
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6523
|
+
againstPath: options.againstPath,
|
|
6524
|
+
runCount: runs.length,
|
|
6525
|
+
runs,
|
|
6526
|
+
gatePassed,
|
|
6527
|
+
failedGateRuns,
|
|
6528
|
+
bestByHitAt5,
|
|
6529
|
+
bestByMrrAt10,
|
|
6530
|
+
bestByP95Latency
|
|
6531
|
+
};
|
|
6532
|
+
writeJson(path7.join(outputDir, "compare.json"), aggregate);
|
|
6533
|
+
const md = createSummaryMarkdown(
|
|
6534
|
+
bestByHitAt5?.summary ?? runs[0].summary,
|
|
6535
|
+
bestByHitAt5?.comparison,
|
|
6536
|
+
void 0,
|
|
6537
|
+
aggregate
|
|
6538
|
+
);
|
|
6539
|
+
writeText(path7.join(outputDir, "summary.md"), md);
|
|
6540
|
+
writeJson(path7.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
|
|
6541
|
+
return { outputDir, aggregate };
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// src/eval/cli.ts
|
|
6545
|
+
function printUsage() {
|
|
6546
|
+
console.log(`
|
|
6547
|
+
Usage:
|
|
6548
|
+
opencode-codebase-index-mcp eval run [options]
|
|
6549
|
+
opencode-codebase-index-mcp eval compare --against <summary.json> [options]
|
|
6550
|
+
opencode-codebase-index-mcp eval diff --current <summary.json> --against <summary.json> [options]
|
|
6551
|
+
|
|
6552
|
+
Options:
|
|
6553
|
+
--project <path> Project root (default: cwd)
|
|
6554
|
+
--config <path> Config JSON path
|
|
6555
|
+
--dataset <path> Golden dataset path (default: benchmarks/golden/small.json)
|
|
6556
|
+
--current <path> Current summary.json path (required for eval diff)
|
|
6557
|
+
--output <path> Output root dir (default: benchmarks/results)
|
|
6558
|
+
--against <path> Baseline summary.json to compare against
|
|
6559
|
+
--budget <path> Budget file for CI mode (default: benchmarks/budgets/default.json)
|
|
6560
|
+
--ci Enable CI gate mode
|
|
6561
|
+
--reindex Force reindex before eval
|
|
6562
|
+
|
|
6563
|
+
Search overrides:
|
|
6564
|
+
--fusionStrategy <rrf|weighted>
|
|
6565
|
+
--hybridWeight <0-1>
|
|
6566
|
+
--rrfK <number>
|
|
6567
|
+
--rerankTopN <number>
|
|
6568
|
+
|
|
6569
|
+
Sweep options (comma-separated values):
|
|
6570
|
+
--sweepFusionStrategy <rrf,weighted>
|
|
6571
|
+
--sweepHybridWeight <0.3,0.5,0.7>
|
|
6572
|
+
--sweepRrfK <30,60,90>
|
|
6573
|
+
--sweepRerankTopN <10,20,40>
|
|
6574
|
+
`);
|
|
6575
|
+
}
|
|
6576
|
+
function parseNumber(value, flag) {
|
|
6577
|
+
const parsed = Number(value);
|
|
6578
|
+
if (Number.isNaN(parsed)) {
|
|
6579
|
+
throw new Error(`${flag} must be a number`);
|
|
6580
|
+
}
|
|
6581
|
+
return parsed;
|
|
6582
|
+
}
|
|
6583
|
+
function parseCsvNumbers(value, flag) {
|
|
6584
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0).map((item) => parseNumber(item, flag));
|
|
6585
|
+
}
|
|
6586
|
+
function parseCsvFusion(value) {
|
|
6587
|
+
const values = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
6588
|
+
const parsed = [];
|
|
6589
|
+
for (const candidate of values) {
|
|
6590
|
+
if (candidate !== "rrf" && candidate !== "weighted") {
|
|
6591
|
+
throw new Error("--sweepFusionStrategy accepts only rrf,weighted");
|
|
6592
|
+
}
|
|
6593
|
+
parsed.push(candidate);
|
|
6594
|
+
}
|
|
6595
|
+
return parsed;
|
|
6596
|
+
}
|
|
6597
|
+
function hasSweepOptions(sweep) {
|
|
6598
|
+
return Boolean(
|
|
6599
|
+
sweep.fusionStrategy && sweep.fusionStrategy.length > 0 || sweep.hybridWeight && sweep.hybridWeight.length > 0 || sweep.rrfK && sweep.rrfK.length > 0 || sweep.rerankTopN && sweep.rerankTopN.length > 0
|
|
6600
|
+
);
|
|
6601
|
+
}
|
|
6602
|
+
function parseEvalArgs(argv, cwd) {
|
|
6603
|
+
const parsed = {
|
|
6604
|
+
projectRoot: cwd,
|
|
6605
|
+
datasetPath: "benchmarks/golden/small.json",
|
|
6606
|
+
outputRoot: "benchmarks/results",
|
|
6607
|
+
budgetPath: "benchmarks/budgets/default.json",
|
|
6608
|
+
ciMode: false,
|
|
6609
|
+
reindex: false,
|
|
6610
|
+
sweep: {}
|
|
6611
|
+
};
|
|
6612
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6613
|
+
const arg = argv[i];
|
|
6614
|
+
const next = argv[i + 1];
|
|
6615
|
+
if (arg === "--project" && next) {
|
|
6616
|
+
parsed.projectRoot = path8.resolve(cwd, next);
|
|
6617
|
+
i += 1;
|
|
6618
|
+
continue;
|
|
6619
|
+
}
|
|
6620
|
+
if (arg === "--config" && next) {
|
|
6621
|
+
parsed.configPath = path8.resolve(cwd, next);
|
|
6622
|
+
i += 1;
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6625
|
+
if (arg === "--dataset" && next) {
|
|
6626
|
+
parsed.datasetPath = next;
|
|
6627
|
+
i += 1;
|
|
6628
|
+
continue;
|
|
6629
|
+
}
|
|
6630
|
+
if (arg === "--current" && next) {
|
|
6631
|
+
parsed.currentPath = next;
|
|
6632
|
+
i += 1;
|
|
6633
|
+
continue;
|
|
6634
|
+
}
|
|
6635
|
+
if (arg === "--output" && next) {
|
|
6636
|
+
parsed.outputRoot = next;
|
|
6637
|
+
i += 1;
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
if (arg === "--against" && next) {
|
|
6641
|
+
parsed.againstPath = next;
|
|
6642
|
+
i += 1;
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
if (arg === "--budget" && next) {
|
|
6646
|
+
parsed.budgetPath = next;
|
|
6647
|
+
i += 1;
|
|
6648
|
+
continue;
|
|
6649
|
+
}
|
|
6650
|
+
if (arg === "--ci") {
|
|
6651
|
+
parsed.ciMode = true;
|
|
6652
|
+
continue;
|
|
6653
|
+
}
|
|
6654
|
+
if (arg === "--reindex") {
|
|
6655
|
+
parsed.reindex = true;
|
|
6656
|
+
continue;
|
|
6657
|
+
}
|
|
6658
|
+
if (arg === "--fusionStrategy" && next) {
|
|
6659
|
+
if (next !== "rrf" && next !== "weighted") {
|
|
6660
|
+
throw new Error("--fusionStrategy must be rrf or weighted");
|
|
6661
|
+
}
|
|
6662
|
+
parsed.fusionStrategy = next;
|
|
6663
|
+
i += 1;
|
|
6664
|
+
continue;
|
|
6665
|
+
}
|
|
6666
|
+
if (arg === "--hybridWeight" && next) {
|
|
6667
|
+
parsed.hybridWeight = parseNumber(next, "--hybridWeight");
|
|
6668
|
+
i += 1;
|
|
6669
|
+
continue;
|
|
6670
|
+
}
|
|
6671
|
+
if (arg === "--rrfK" && next) {
|
|
6672
|
+
parsed.rrfK = parseNumber(next, "--rrfK");
|
|
6673
|
+
i += 1;
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
if (arg === "--rerankTopN" && next) {
|
|
6677
|
+
parsed.rerankTopN = parseNumber(next, "--rerankTopN");
|
|
6678
|
+
i += 1;
|
|
6679
|
+
continue;
|
|
6680
|
+
}
|
|
6681
|
+
if (arg === "--sweepFusionStrategy" && next) {
|
|
6682
|
+
parsed.sweep.fusionStrategy = parseCsvFusion(next);
|
|
6683
|
+
i += 1;
|
|
6684
|
+
continue;
|
|
6685
|
+
}
|
|
6686
|
+
if (arg === "--sweepHybridWeight" && next) {
|
|
6687
|
+
parsed.sweep.hybridWeight = parseCsvNumbers(next, "--sweepHybridWeight");
|
|
6688
|
+
i += 1;
|
|
6689
|
+
continue;
|
|
6690
|
+
}
|
|
6691
|
+
if (arg === "--sweepRrfK" && next) {
|
|
6692
|
+
parsed.sweep.rrfK = parseCsvNumbers(next, "--sweepRrfK");
|
|
6693
|
+
i += 1;
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6696
|
+
if (arg === "--sweepRerankTopN" && next) {
|
|
6697
|
+
parsed.sweep.rerankTopN = parseCsvNumbers(next, "--sweepRerankTopN");
|
|
6698
|
+
i += 1;
|
|
6699
|
+
continue;
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
return parsed;
|
|
6703
|
+
}
|
|
6704
|
+
function parseEvalSubcommandOptions(argv, cwd) {
|
|
6705
|
+
let explicitAgainst;
|
|
6706
|
+
const filtered = [];
|
|
6707
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6708
|
+
const current = argv[i];
|
|
6709
|
+
const next = argv[i + 1];
|
|
6710
|
+
if (current === "--against" && next) {
|
|
6711
|
+
explicitAgainst = next;
|
|
6712
|
+
i += 1;
|
|
6713
|
+
continue;
|
|
6714
|
+
}
|
|
6715
|
+
filtered.push(current);
|
|
6716
|
+
}
|
|
6717
|
+
return {
|
|
6718
|
+
parsed: parseEvalArgs(filtered, cwd),
|
|
6719
|
+
explicitAgainst
|
|
6720
|
+
};
|
|
6721
|
+
}
|
|
6722
|
+
function toRunOptions(parsed) {
|
|
6723
|
+
return {
|
|
6724
|
+
projectRoot: parsed.projectRoot,
|
|
6725
|
+
configPath: parsed.configPath,
|
|
6726
|
+
datasetPath: parsed.datasetPath,
|
|
6727
|
+
outputRoot: parsed.outputRoot,
|
|
6728
|
+
againstPath: parsed.againstPath,
|
|
6729
|
+
budgetPath: parsed.budgetPath,
|
|
6730
|
+
ciMode: parsed.ciMode,
|
|
6731
|
+
reindex: parsed.reindex,
|
|
6732
|
+
searchOverrides: {
|
|
6733
|
+
...parsed.fusionStrategy !== void 0 ? { fusionStrategy: parsed.fusionStrategy } : {},
|
|
6734
|
+
...parsed.hybridWeight !== void 0 ? { hybridWeight: parsed.hybridWeight } : {},
|
|
6735
|
+
...parsed.rrfK !== void 0 ? { rrfK: parsed.rrfK } : {},
|
|
6736
|
+
...parsed.rerankTopN !== void 0 ? { rerankTopN: parsed.rerankTopN } : {}
|
|
6737
|
+
}
|
|
6738
|
+
};
|
|
6739
|
+
}
|
|
6740
|
+
async function handleEvalCommand(args, cwd) {
|
|
6741
|
+
const subcommand = args[0];
|
|
6742
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
6743
|
+
printUsage();
|
|
6744
|
+
return 0;
|
|
6745
|
+
}
|
|
6746
|
+
if (subcommand === "run") {
|
|
6747
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6748
|
+
if (explicitAgainst) {
|
|
6749
|
+
parsed.againstPath = explicitAgainst;
|
|
6750
|
+
}
|
|
6751
|
+
const runOptions = toRunOptions(parsed);
|
|
6752
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6753
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6754
|
+
console.log(`Eval sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6755
|
+
console.log(`Sweep runs: ${sweep.aggregate.runCount}`);
|
|
6756
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6757
|
+
console.error(
|
|
6758
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6759
|
+
);
|
|
6760
|
+
return 1;
|
|
6761
|
+
}
|
|
6762
|
+
return 0;
|
|
6763
|
+
}
|
|
6764
|
+
const result = await runEvaluation(runOptions);
|
|
6765
|
+
console.log(`Eval run complete. Artifacts: ${result.outputDir}`);
|
|
6766
|
+
console.log(
|
|
6767
|
+
`Hit@5=${(result.summary.metrics.hitAt5 * 100).toFixed(2)}% MRR@10=${result.summary.metrics.mrrAt10.toFixed(4)} p95=${result.summary.metrics.latencyMs.p95.toFixed(3)}ms`
|
|
6768
|
+
);
|
|
6769
|
+
if (result.gate && !result.gate.passed) {
|
|
6770
|
+
for (const violation of result.gate.violations) {
|
|
6771
|
+
console.error(`[CI-GATE] ${violation.metric}: ${violation.message}`);
|
|
6772
|
+
}
|
|
6773
|
+
return 1;
|
|
6774
|
+
}
|
|
6775
|
+
return 0;
|
|
6776
|
+
}
|
|
6777
|
+
if (subcommand === "compare") {
|
|
6778
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6779
|
+
if (!explicitAgainst) {
|
|
6780
|
+
throw new Error("eval compare requires --against <baseline summary.json>");
|
|
6781
|
+
}
|
|
6782
|
+
parsed.againstPath = explicitAgainst;
|
|
6783
|
+
const runOptions = toRunOptions(parsed);
|
|
6784
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6785
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6786
|
+
console.log(`Eval compare sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6787
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6788
|
+
console.error(
|
|
6789
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6790
|
+
);
|
|
6791
|
+
return 1;
|
|
6792
|
+
}
|
|
6793
|
+
return 0;
|
|
6794
|
+
}
|
|
6795
|
+
const result = await runEvaluation(runOptions);
|
|
6796
|
+
console.log(`Eval compare complete. Artifacts: ${result.outputDir}`);
|
|
6797
|
+
return 0;
|
|
6798
|
+
}
|
|
6799
|
+
if (subcommand === "diff") {
|
|
6800
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6801
|
+
if (!explicitAgainst) {
|
|
6802
|
+
throw new Error("eval diff requires --against <baseline summary.json>");
|
|
6803
|
+
}
|
|
6804
|
+
if (!parsed.currentPath) {
|
|
6805
|
+
throw new Error("eval diff requires --current <current summary.json>");
|
|
6806
|
+
}
|
|
6807
|
+
parsed.againstPath = explicitAgainst;
|
|
6808
|
+
const currentPath = parsed.currentPath;
|
|
6809
|
+
if (!currentPath.endsWith(".json")) {
|
|
6810
|
+
throw new Error("eval diff --current must point to a summary JSON file");
|
|
6811
|
+
}
|
|
6812
|
+
if (!parsed.againstPath.endsWith(".json")) {
|
|
6813
|
+
throw new Error("eval diff --against must point to a summary JSON file");
|
|
6814
|
+
}
|
|
6815
|
+
const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
|
|
6816
|
+
const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
|
|
6817
|
+
const comparison = compareSummaries(
|
|
6818
|
+
currentSummary,
|
|
6819
|
+
baselineSummary,
|
|
6820
|
+
path8.resolve(parsed.projectRoot, parsed.againstPath)
|
|
6821
|
+
);
|
|
6822
|
+
const outputDir = createRunDirectory(path8.resolve(parsed.projectRoot, parsed.outputRoot));
|
|
6823
|
+
const summaryMd = createSummaryMarkdown(currentSummary, comparison);
|
|
6824
|
+
writeJson(path8.join(outputDir, "compare.json"), comparison);
|
|
6825
|
+
writeText(path8.join(outputDir, "summary.md"), summaryMd);
|
|
6826
|
+
writeJson(path8.join(outputDir, "summary.json"), currentSummary);
|
|
6827
|
+
console.log(`Eval diff complete. Artifacts: ${outputDir}`);
|
|
6828
|
+
return 0;
|
|
6829
|
+
}
|
|
6830
|
+
throw new Error(`Unknown eval subcommand: ${subcommand}`);
|
|
6831
|
+
}
|
|
6832
|
+
|
|
5738
6833
|
// src/mcp-server.ts
|
|
6834
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6835
|
+
var import_zod = require("zod");
|
|
6836
|
+
|
|
6837
|
+
// src/tools/utils.ts
|
|
5739
6838
|
var MAX_CONTENT_LINES = 30;
|
|
5740
6839
|
function truncateContent(content) {
|
|
5741
6840
|
const lines = content.split("\n");
|
|
@@ -5743,6 +6842,31 @@ function truncateContent(content) {
|
|
|
5743
6842
|
return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
|
|
5744
6843
|
// ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
|
|
5745
6844
|
}
|
|
6845
|
+
function formatDefinitionLookup(results, query) {
|
|
6846
|
+
if (results.length === 0) {
|
|
6847
|
+
return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
|
|
6848
|
+
}
|
|
6849
|
+
const formatted = results.map((r, idx) => {
|
|
6850
|
+
const header2 = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
|
|
6851
|
+
return `${header2} (score: ${r.score.toFixed(2)})
|
|
6852
|
+
\`\`\`
|
|
6853
|
+
${truncateContent(r.content)}
|
|
6854
|
+
\`\`\``;
|
|
6855
|
+
});
|
|
6856
|
+
const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
|
|
6857
|
+
return `${header}
|
|
6858
|
+
|
|
6859
|
+
${formatted.join("\n\n")}`;
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
// src/mcp-server.ts
|
|
6863
|
+
var MAX_CONTENT_LINES2 = 30;
|
|
6864
|
+
function truncateContent2(content) {
|
|
6865
|
+
const lines = content.split("\n");
|
|
6866
|
+
if (lines.length <= MAX_CONTENT_LINES2) return content;
|
|
6867
|
+
return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
|
|
6868
|
+
// ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
|
|
6869
|
+
}
|
|
5746
6870
|
function formatIndexStats(stats, verbose = false) {
|
|
5747
6871
|
const lines = [];
|
|
5748
6872
|
if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
|
|
@@ -5856,7 +6980,7 @@ function createMcpServer(projectRoot, config) {
|
|
|
5856
6980
|
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}`;
|
|
5857
6981
|
return `${header} (score: ${r.score.toFixed(2)})
|
|
5858
6982
|
\`\`\`
|
|
5859
|
-
${
|
|
6983
|
+
${truncateContent2(r.content)}
|
|
5860
6984
|
\`\`\``;
|
|
5861
6985
|
});
|
|
5862
6986
|
return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
|
|
@@ -6034,7 +7158,7 @@ Use Read tool to examine specific files.` }] };
|
|
|
6034
7158
|
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}`;
|
|
6035
7159
|
return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
|
|
6036
7160
|
\`\`\`
|
|
6037
|
-
${
|
|
7161
|
+
${truncateContent2(r.content)}
|
|
6038
7162
|
\`\`\``;
|
|
6039
7163
|
});
|
|
6040
7164
|
return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
|
|
@@ -6042,6 +7166,25 @@ ${truncateContent(r.content)}
|
|
|
6042
7166
|
${formatted.join("\n\n")}` }] };
|
|
6043
7167
|
}
|
|
6044
7168
|
);
|
|
7169
|
+
server.tool(
|
|
7170
|
+
"implementation_lookup",
|
|
7171
|
+
"Jump to symbol definition. Find WHERE something is defined. Returns the authoritative source location(s). Prefers real implementation files over tests, docs, examples, and fixtures.",
|
|
7172
|
+
{
|
|
7173
|
+
query: import_zod.z.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
|
|
7174
|
+
limit: import_zod.z.number().optional().default(5).describe("Maximum number of results"),
|
|
7175
|
+
fileType: import_zod.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
|
|
7176
|
+
directory: import_zod.z.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
|
|
7177
|
+
},
|
|
7178
|
+
async (args) => {
|
|
7179
|
+
await ensureInitialized();
|
|
7180
|
+
const results = await indexer.search(args.query, args.limit ?? 5, {
|
|
7181
|
+
fileType: args.fileType,
|
|
7182
|
+
directory: args.directory,
|
|
7183
|
+
definitionIntent: true
|
|
7184
|
+
});
|
|
7185
|
+
return { content: [{ type: "text", text: formatDefinitionLookup(results, args.query) }] };
|
|
7186
|
+
}
|
|
7187
|
+
);
|
|
6045
7188
|
server.tool(
|
|
6046
7189
|
"call_graph",
|
|
6047
7190
|
"Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
|
|
@@ -6148,14 +7291,30 @@ Use a hybrid approach:
|
|
|
6148
7291
|
}]
|
|
6149
7292
|
})
|
|
6150
7293
|
);
|
|
7294
|
+
server.prompt(
|
|
7295
|
+
"definition",
|
|
7296
|
+
"Find where a symbol is defined in the codebase",
|
|
7297
|
+
{ query: import_zod.z.string().describe("Symbol name or description to find the definition of") },
|
|
7298
|
+
(args) => ({
|
|
7299
|
+
messages: [{
|
|
7300
|
+
role: "user",
|
|
7301
|
+
content: {
|
|
7302
|
+
type: "text",
|
|
7303
|
+
text: `Find the definition of: "${args.query}"
|
|
7304
|
+
|
|
7305
|
+
Use the implementation_lookup tool to find where this symbol is defined. This prioritizes real implementation files over tests, docs, and examples. If no definition is found, fall back to codebase_search for broader discovery.`
|
|
7306
|
+
}
|
|
7307
|
+
}]
|
|
7308
|
+
})
|
|
7309
|
+
);
|
|
6151
7310
|
return server;
|
|
6152
7311
|
}
|
|
6153
7312
|
|
|
6154
7313
|
// src/cli.ts
|
|
6155
7314
|
function loadJsonFile(filePath) {
|
|
6156
7315
|
try {
|
|
6157
|
-
if ((0,
|
|
6158
|
-
const content = (0,
|
|
7316
|
+
if ((0, import_fs10.existsSync)(filePath)) {
|
|
7317
|
+
const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
|
|
6159
7318
|
return JSON.parse(content);
|
|
6160
7319
|
}
|
|
6161
7320
|
} catch {
|
|
@@ -6167,9 +7326,9 @@ function loadPluginConfig(projectRoot, configPath) {
|
|
|
6167
7326
|
const config = loadJsonFile(configPath);
|
|
6168
7327
|
if (config) return config;
|
|
6169
7328
|
}
|
|
6170
|
-
const projectConfig = loadJsonFile(
|
|
7329
|
+
const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
|
|
6171
7330
|
if (projectConfig) return projectConfig;
|
|
6172
|
-
const globalConfig = loadJsonFile(
|
|
7331
|
+
const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
|
|
6173
7332
|
if (globalConfig) return globalConfig;
|
|
6174
7333
|
return {};
|
|
6175
7334
|
}
|
|
@@ -6178,14 +7337,18 @@ function parseArgs(argv) {
|
|
|
6178
7337
|
let config;
|
|
6179
7338
|
for (let i = 2; i < argv.length; i++) {
|
|
6180
7339
|
if (argv[i] === "--project" && argv[i + 1]) {
|
|
6181
|
-
project =
|
|
7340
|
+
project = path9.resolve(argv[++i]);
|
|
6182
7341
|
} else if (argv[i] === "--config" && argv[i + 1]) {
|
|
6183
|
-
config =
|
|
7342
|
+
config = path9.resolve(argv[++i]);
|
|
6184
7343
|
}
|
|
6185
7344
|
}
|
|
6186
7345
|
return { project, config };
|
|
6187
7346
|
}
|
|
6188
7347
|
async function main() {
|
|
7348
|
+
if (process.argv[2] === "eval") {
|
|
7349
|
+
const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
|
|
7350
|
+
process.exit(exitCode);
|
|
7351
|
+
}
|
|
6189
7352
|
const args = parseArgs(process.argv);
|
|
6190
7353
|
const rawConfig = loadPluginConfig(args.project, args.config);
|
|
6191
7354
|
const config = parseConfig(rawConfig);
|