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.js
CHANGED
|
@@ -7,13 +7,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
7
7
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
8
|
var __getProtoOf = Object.getPrototypeOf;
|
|
9
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
-
var
|
|
11
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
12
|
-
}) : x)(function(x) {
|
|
13
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
14
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
|
-
});
|
|
16
|
-
var __commonJS = (cb, mod) => function __require2() {
|
|
10
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
17
11
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
18
12
|
};
|
|
19
13
|
var __copyProps = (to, from, except, desc) => {
|
|
@@ -35,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
29
|
|
|
36
30
|
// node_modules/eventemitter3/index.js
|
|
37
31
|
var require_eventemitter3 = __commonJS({
|
|
38
|
-
"node_modules/eventemitter3/index.js"(exports,
|
|
32
|
+
"node_modules/eventemitter3/index.js"(exports, module2) {
|
|
39
33
|
"use strict";
|
|
40
34
|
var has = Object.prototype.hasOwnProperty;
|
|
41
35
|
var prefix = "~";
|
|
@@ -189,15 +183,15 @@ var require_eventemitter3 = __commonJS({
|
|
|
189
183
|
EventEmitter2.prototype.addListener = EventEmitter2.prototype.on;
|
|
190
184
|
EventEmitter2.prefixed = prefix;
|
|
191
185
|
EventEmitter2.EventEmitter = EventEmitter2;
|
|
192
|
-
if ("undefined" !== typeof
|
|
193
|
-
|
|
186
|
+
if ("undefined" !== typeof module2) {
|
|
187
|
+
module2.exports = EventEmitter2;
|
|
194
188
|
}
|
|
195
189
|
}
|
|
196
190
|
});
|
|
197
191
|
|
|
198
192
|
// node_modules/ignore/index.js
|
|
199
193
|
var require_ignore = __commonJS({
|
|
200
|
-
"node_modules/ignore/index.js"(exports,
|
|
194
|
+
"node_modules/ignore/index.js"(exports, module2) {
|
|
201
195
|
"use strict";
|
|
202
196
|
function makeArray(subject) {
|
|
203
197
|
return Array.isArray(subject) ? subject : [subject];
|
|
@@ -493,7 +487,7 @@ var require_ignore = __commonJS({
|
|
|
493
487
|
// path matching.
|
|
494
488
|
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
495
489
|
// @returns {TestResult} true if a file is ignored
|
|
496
|
-
test(
|
|
490
|
+
test(path10, checkUnignored, mode) {
|
|
497
491
|
let ignored = false;
|
|
498
492
|
let unignored = false;
|
|
499
493
|
let matchedRule;
|
|
@@ -502,7 +496,7 @@ var require_ignore = __commonJS({
|
|
|
502
496
|
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
503
497
|
return;
|
|
504
498
|
}
|
|
505
|
-
const matched = rule[mode].test(
|
|
499
|
+
const matched = rule[mode].test(path10);
|
|
506
500
|
if (!matched) {
|
|
507
501
|
return;
|
|
508
502
|
}
|
|
@@ -523,17 +517,17 @@ var require_ignore = __commonJS({
|
|
|
523
517
|
var throwError = (message, Ctor) => {
|
|
524
518
|
throw new Ctor(message);
|
|
525
519
|
};
|
|
526
|
-
var checkPath = (
|
|
527
|
-
if (!isString(
|
|
520
|
+
var checkPath = (path10, originalPath, doThrow) => {
|
|
521
|
+
if (!isString(path10)) {
|
|
528
522
|
return doThrow(
|
|
529
523
|
`path must be a string, but got \`${originalPath}\``,
|
|
530
524
|
TypeError
|
|
531
525
|
);
|
|
532
526
|
}
|
|
533
|
-
if (!
|
|
527
|
+
if (!path10) {
|
|
534
528
|
return doThrow(`path must not be empty`, TypeError);
|
|
535
529
|
}
|
|
536
|
-
if (checkPath.isNotRelative(
|
|
530
|
+
if (checkPath.isNotRelative(path10)) {
|
|
537
531
|
const r = "`path.relative()`d";
|
|
538
532
|
return doThrow(
|
|
539
533
|
`path should be a ${r} string, but got "${originalPath}"`,
|
|
@@ -542,7 +536,7 @@ var require_ignore = __commonJS({
|
|
|
542
536
|
}
|
|
543
537
|
return true;
|
|
544
538
|
};
|
|
545
|
-
var isNotRelative = (
|
|
539
|
+
var isNotRelative = (path10) => REGEX_TEST_INVALID_PATH.test(path10);
|
|
546
540
|
checkPath.isNotRelative = isNotRelative;
|
|
547
541
|
checkPath.convert = (p) => p;
|
|
548
542
|
var Ignore2 = class {
|
|
@@ -572,19 +566,19 @@ var require_ignore = __commonJS({
|
|
|
572
566
|
}
|
|
573
567
|
// @returns {TestResult}
|
|
574
568
|
_test(originalPath, cache, checkUnignored, slices) {
|
|
575
|
-
const
|
|
569
|
+
const path10 = originalPath && checkPath.convert(originalPath);
|
|
576
570
|
checkPath(
|
|
577
|
-
|
|
571
|
+
path10,
|
|
578
572
|
originalPath,
|
|
579
573
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
580
574
|
);
|
|
581
|
-
return this._t(
|
|
575
|
+
return this._t(path10, cache, checkUnignored, slices);
|
|
582
576
|
}
|
|
583
|
-
checkIgnore(
|
|
584
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
585
|
-
return this.test(
|
|
577
|
+
checkIgnore(path10) {
|
|
578
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
|
|
579
|
+
return this.test(path10);
|
|
586
580
|
}
|
|
587
|
-
const slices =
|
|
581
|
+
const slices = path10.split(SLASH).filter(Boolean);
|
|
588
582
|
slices.pop();
|
|
589
583
|
if (slices.length) {
|
|
590
584
|
const parent = this._t(
|
|
@@ -597,18 +591,18 @@ var require_ignore = __commonJS({
|
|
|
597
591
|
return parent;
|
|
598
592
|
}
|
|
599
593
|
}
|
|
600
|
-
return this._rules.test(
|
|
594
|
+
return this._rules.test(path10, false, MODE_CHECK_IGNORE);
|
|
601
595
|
}
|
|
602
|
-
_t(
|
|
603
|
-
if (
|
|
604
|
-
return cache[
|
|
596
|
+
_t(path10, cache, checkUnignored, slices) {
|
|
597
|
+
if (path10 in cache) {
|
|
598
|
+
return cache[path10];
|
|
605
599
|
}
|
|
606
600
|
if (!slices) {
|
|
607
|
-
slices =
|
|
601
|
+
slices = path10.split(SLASH).filter(Boolean);
|
|
608
602
|
}
|
|
609
603
|
slices.pop();
|
|
610
604
|
if (!slices.length) {
|
|
611
|
-
return cache[
|
|
605
|
+
return cache[path10] = this._rules.test(path10, checkUnignored, MODE_IGNORE);
|
|
612
606
|
}
|
|
613
607
|
const parent = this._t(
|
|
614
608
|
slices.join(SLASH) + SLASH,
|
|
@@ -616,29 +610,29 @@ var require_ignore = __commonJS({
|
|
|
616
610
|
checkUnignored,
|
|
617
611
|
slices
|
|
618
612
|
);
|
|
619
|
-
return cache[
|
|
613
|
+
return cache[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
|
|
620
614
|
}
|
|
621
|
-
ignores(
|
|
622
|
-
return this._test(
|
|
615
|
+
ignores(path10) {
|
|
616
|
+
return this._test(path10, this._ignoreCache, false).ignored;
|
|
623
617
|
}
|
|
624
618
|
createFilter() {
|
|
625
|
-
return (
|
|
619
|
+
return (path10) => !this.ignores(path10);
|
|
626
620
|
}
|
|
627
621
|
filter(paths) {
|
|
628
622
|
return makeArray(paths).filter(this.createFilter());
|
|
629
623
|
}
|
|
630
624
|
// @returns {TestResult}
|
|
631
|
-
test(
|
|
632
|
-
return this._test(
|
|
625
|
+
test(path10) {
|
|
626
|
+
return this._test(path10, this._testCache, true);
|
|
633
627
|
}
|
|
634
628
|
};
|
|
635
629
|
var factory = (options) => new Ignore2(options);
|
|
636
|
-
var isPathValid = (
|
|
630
|
+
var isPathValid = (path10) => checkPath(path10 && checkPath.convert(path10), path10, RETURN_FALSE);
|
|
637
631
|
var setupWindows = () => {
|
|
638
632
|
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
639
633
|
checkPath.convert = makePosix;
|
|
640
634
|
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
641
|
-
checkPath.isNotRelative = (
|
|
635
|
+
checkPath.isNotRelative = (path10) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path10) || isNotRelative(path10);
|
|
642
636
|
};
|
|
643
637
|
if (
|
|
644
638
|
// Detect `process` so that it can run in browsers.
|
|
@@ -646,18 +640,18 @@ var require_ignore = __commonJS({
|
|
|
646
640
|
) {
|
|
647
641
|
setupWindows();
|
|
648
642
|
}
|
|
649
|
-
|
|
643
|
+
module2.exports = factory;
|
|
650
644
|
factory.default = factory;
|
|
651
|
-
|
|
652
|
-
define(
|
|
645
|
+
module2.exports.isPathValid = isPathValid;
|
|
646
|
+
define(module2.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
653
647
|
}
|
|
654
648
|
});
|
|
655
649
|
|
|
656
650
|
// src/cli.ts
|
|
657
651
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
658
|
-
import { existsSync as
|
|
659
|
-
import * as
|
|
660
|
-
import * as
|
|
652
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
|
|
653
|
+
import * as path9 from "path";
|
|
654
|
+
import * as os4 from "os";
|
|
661
655
|
|
|
662
656
|
// src/config/constants.ts
|
|
663
657
|
var DEFAULT_INCLUDE = [
|
|
@@ -665,7 +659,7 @@ var DEFAULT_INCLUDE = [
|
|
|
665
659
|
"**/*.{py,pyi}",
|
|
666
660
|
"**/*.{go,rs,java,kt,scala}",
|
|
667
661
|
"**/*.{c,cpp,cc,h,hpp}",
|
|
668
|
-
"**/*.{rb,php,swift}",
|
|
662
|
+
"**/*.{rb,php,inc,swift}",
|
|
669
663
|
"**/*.{vue,svelte,astro}",
|
|
670
664
|
"**/*.{sql,graphql,proto}",
|
|
671
665
|
"**/*.{yaml,yml,toml}",
|
|
@@ -915,13 +909,197 @@ function getDefaultModelForProvider(provider) {
|
|
|
915
909
|
}
|
|
916
910
|
var availableProviders = Object.keys(EMBEDDING_MODELS);
|
|
917
911
|
|
|
918
|
-
// src/
|
|
919
|
-
import
|
|
920
|
-
|
|
912
|
+
// src/eval/cli.ts
|
|
913
|
+
import * as path8 from "path";
|
|
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
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
953
|
+
import * as path from "path";
|
|
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 = 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
|
+
mkdirSync(dir, { recursive: true });
|
|
975
|
+
return dir;
|
|
976
|
+
}
|
|
977
|
+
function writeJson(filePath, value) {
|
|
978
|
+
writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
979
|
+
}
|
|
980
|
+
function writeText(filePath, value) {
|
|
981
|
+
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
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1094
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1095
|
+
import { rmSync } from "fs";
|
|
1096
|
+
import * as os3 from "os";
|
|
1097
|
+
import * as path7 from "path";
|
|
1098
|
+
import { performance as performance3 } from "perf_hooks";
|
|
921
1099
|
|
|
922
1100
|
// src/indexer/index.ts
|
|
923
|
-
import { existsSync as existsSync4, readFileSync as
|
|
924
|
-
import * as
|
|
1101
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, renameSync, unlinkSync, promises as fsPromises2 } from "fs";
|
|
1102
|
+
import * as path6 from "path";
|
|
925
1103
|
import { performance as performance2 } from "perf_hooks";
|
|
926
1104
|
|
|
927
1105
|
// node_modules/eventemitter3/index.mjs
|
|
@@ -946,7 +1124,7 @@ function pTimeout(promise, options) {
|
|
|
946
1124
|
} = options;
|
|
947
1125
|
let timer;
|
|
948
1126
|
let abortHandler;
|
|
949
|
-
const wrappedPromise = new Promise((
|
|
1127
|
+
const wrappedPromise = new Promise((resolve5, reject) => {
|
|
950
1128
|
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
|
|
951
1129
|
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
|
|
952
1130
|
}
|
|
@@ -960,7 +1138,7 @@ function pTimeout(promise, options) {
|
|
|
960
1138
|
};
|
|
961
1139
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
962
1140
|
}
|
|
963
|
-
promise.then(
|
|
1141
|
+
promise.then(resolve5, reject);
|
|
964
1142
|
if (milliseconds === Number.POSITIVE_INFINITY) {
|
|
965
1143
|
return;
|
|
966
1144
|
}
|
|
@@ -968,7 +1146,7 @@ function pTimeout(promise, options) {
|
|
|
968
1146
|
timer = customTimers.setTimeout.call(void 0, () => {
|
|
969
1147
|
if (fallback) {
|
|
970
1148
|
try {
|
|
971
|
-
|
|
1149
|
+
resolve5(fallback());
|
|
972
1150
|
} catch (error) {
|
|
973
1151
|
reject(error);
|
|
974
1152
|
}
|
|
@@ -978,7 +1156,7 @@ function pTimeout(promise, options) {
|
|
|
978
1156
|
promise.cancel();
|
|
979
1157
|
}
|
|
980
1158
|
if (message === false) {
|
|
981
|
-
|
|
1159
|
+
resolve5();
|
|
982
1160
|
} else if (message instanceof Error) {
|
|
983
1161
|
reject(message);
|
|
984
1162
|
} else {
|
|
@@ -1368,7 +1546,7 @@ var PQueue = class extends import_index.default {
|
|
|
1368
1546
|
// Assign unique ID if not provided
|
|
1369
1547
|
id: options.id ?? (this.#idAssigner++).toString()
|
|
1370
1548
|
};
|
|
1371
|
-
return new Promise((
|
|
1549
|
+
return new Promise((resolve5, reject) => {
|
|
1372
1550
|
const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
|
|
1373
1551
|
this.#queue.enqueue(async () => {
|
|
1374
1552
|
this.#pending++;
|
|
@@ -1406,7 +1584,7 @@ var PQueue = class extends import_index.default {
|
|
|
1406
1584
|
})]);
|
|
1407
1585
|
}
|
|
1408
1586
|
const result = await operation;
|
|
1409
|
-
|
|
1587
|
+
resolve5(result);
|
|
1410
1588
|
this.emit("completed", result);
|
|
1411
1589
|
} catch (error) {
|
|
1412
1590
|
reject(error);
|
|
@@ -1563,13 +1741,13 @@ var PQueue = class extends import_index.default {
|
|
|
1563
1741
|
});
|
|
1564
1742
|
}
|
|
1565
1743
|
async #onEvent(event, filter) {
|
|
1566
|
-
return new Promise((
|
|
1744
|
+
return new Promise((resolve5) => {
|
|
1567
1745
|
const listener = () => {
|
|
1568
1746
|
if (filter && !filter()) {
|
|
1569
1747
|
return;
|
|
1570
1748
|
}
|
|
1571
1749
|
this.off(event, listener);
|
|
1572
|
-
|
|
1750
|
+
resolve5();
|
|
1573
1751
|
};
|
|
1574
1752
|
this.on(event, listener);
|
|
1575
1753
|
});
|
|
@@ -1854,7 +2032,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1854
2032
|
const finalDelay = Math.min(delayTime, remainingTime);
|
|
1855
2033
|
options.signal?.throwIfAborted();
|
|
1856
2034
|
if (finalDelay > 0) {
|
|
1857
|
-
await new Promise((
|
|
2035
|
+
await new Promise((resolve5, reject) => {
|
|
1858
2036
|
const onAbort = () => {
|
|
1859
2037
|
clearTimeout(timeoutToken);
|
|
1860
2038
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -1862,7 +2040,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1862
2040
|
};
|
|
1863
2041
|
const timeoutToken = setTimeout(() => {
|
|
1864
2042
|
options.signal?.removeEventListener("abort", onAbort);
|
|
1865
|
-
|
|
2043
|
+
resolve5();
|
|
1866
2044
|
}, finalDelay);
|
|
1867
2045
|
if (options.unref) {
|
|
1868
2046
|
timeoutToken.unref?.();
|
|
@@ -1923,17 +2101,17 @@ async function pRetry(input, options = {}) {
|
|
|
1923
2101
|
}
|
|
1924
2102
|
|
|
1925
2103
|
// src/embeddings/detector.ts
|
|
1926
|
-
import { existsSync, readFileSync } from "fs";
|
|
1927
|
-
import * as
|
|
2104
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2105
|
+
import * as path2 from "path";
|
|
1928
2106
|
import * as os from "os";
|
|
1929
2107
|
function getOpenCodeAuthPath() {
|
|
1930
|
-
return
|
|
2108
|
+
return path2.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
1931
2109
|
}
|
|
1932
2110
|
function loadOpenCodeAuth() {
|
|
1933
2111
|
const authPath = getOpenCodeAuthPath();
|
|
1934
2112
|
try {
|
|
1935
2113
|
if (existsSync(authPath)) {
|
|
1936
|
-
return JSON.parse(
|
|
2114
|
+
return JSON.parse(readFileSync2(authPath, "utf-8"));
|
|
1937
2115
|
}
|
|
1938
2116
|
} catch {
|
|
1939
2117
|
}
|
|
@@ -2435,8 +2613,8 @@ var CustomEmbeddingProvider = class {
|
|
|
2435
2613
|
|
|
2436
2614
|
// src/utils/files.ts
|
|
2437
2615
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
2438
|
-
import { existsSync as existsSync2, readFileSync as
|
|
2439
|
-
import * as
|
|
2616
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, promises as fsPromises } from "fs";
|
|
2617
|
+
import * as path3 from "path";
|
|
2440
2618
|
function createIgnoreFilter(projectRoot) {
|
|
2441
2619
|
const ig = (0, import_ignore.default)();
|
|
2442
2620
|
const defaultIgnores = [
|
|
@@ -2453,9 +2631,9 @@ function createIgnoreFilter(projectRoot) {
|
|
|
2453
2631
|
".opencode"
|
|
2454
2632
|
];
|
|
2455
2633
|
ig.add(defaultIgnores);
|
|
2456
|
-
const gitignorePath =
|
|
2634
|
+
const gitignorePath = path3.join(projectRoot, ".gitignore");
|
|
2457
2635
|
if (existsSync2(gitignorePath)) {
|
|
2458
|
-
const gitignoreContent =
|
|
2636
|
+
const gitignoreContent = readFileSync3(gitignorePath, "utf-8");
|
|
2459
2637
|
ig.add(gitignoreContent);
|
|
2460
2638
|
}
|
|
2461
2639
|
return ig;
|
|
@@ -2471,8 +2649,8 @@ function matchGlob(filePath, pattern) {
|
|
|
2471
2649
|
async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
|
|
2472
2650
|
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
2473
2651
|
for (const entry of entries) {
|
|
2474
|
-
const fullPath =
|
|
2475
|
-
const relativePath =
|
|
2652
|
+
const fullPath = path3.join(dir, entry.name);
|
|
2653
|
+
const relativePath = path3.relative(projectRoot, fullPath);
|
|
2476
2654
|
if (ignoreFilter.ignores(relativePath)) {
|
|
2477
2655
|
if (entry.isFile()) {
|
|
2478
2656
|
skipped.push({ path: relativePath, reason: "gitignore" });
|
|
@@ -2533,6 +2711,9 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
|
|
|
2533
2711
|
}
|
|
2534
2712
|
|
|
2535
2713
|
// src/utils/cost.ts
|
|
2714
|
+
function estimateTokens(text) {
|
|
2715
|
+
return Math.ceil(text.length / 4);
|
|
2716
|
+
}
|
|
2536
2717
|
function estimateChunksFromFiles(files) {
|
|
2537
2718
|
let totalChunks = 0;
|
|
2538
2719
|
for (const file of files) {
|
|
@@ -2887,8 +3068,9 @@ function initializeLogger(config) {
|
|
|
2887
3068
|
}
|
|
2888
3069
|
|
|
2889
3070
|
// src/native/index.ts
|
|
2890
|
-
import * as
|
|
3071
|
+
import * as path4 from "path";
|
|
2891
3072
|
import * as os2 from "os";
|
|
3073
|
+
import * as module from "module";
|
|
2892
3074
|
import { fileURLToPath } from "url";
|
|
2893
3075
|
function getNativeBinding() {
|
|
2894
3076
|
const platform2 = os2.platform();
|
|
@@ -2908,17 +3090,22 @@ function getNativeBinding() {
|
|
|
2908
3090
|
throw new Error(`Unsupported platform: ${platform2}-${arch2}`);
|
|
2909
3091
|
}
|
|
2910
3092
|
let currentDir;
|
|
3093
|
+
let requireTarget;
|
|
2911
3094
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
2912
|
-
currentDir =
|
|
3095
|
+
currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
3096
|
+
requireTarget = import.meta.url;
|
|
2913
3097
|
} else if (typeof __dirname !== "undefined") {
|
|
2914
3098
|
currentDir = __dirname;
|
|
3099
|
+
requireTarget = __filename;
|
|
2915
3100
|
} else {
|
|
2916
3101
|
currentDir = process.cwd();
|
|
3102
|
+
requireTarget = path4.join(currentDir, "index.js");
|
|
2917
3103
|
}
|
|
2918
3104
|
const isDevMode = currentDir.includes("/src/native");
|
|
2919
|
-
const packageRoot = isDevMode ?
|
|
2920
|
-
const nativePath =
|
|
2921
|
-
|
|
3105
|
+
const packageRoot = isDevMode ? path4.resolve(currentDir, "../..") : path4.resolve(currentDir, "..");
|
|
3106
|
+
const nativePath = path4.join(packageRoot, "native", bindingName);
|
|
3107
|
+
const require2 = module.createRequire(requireTarget);
|
|
3108
|
+
return require2(nativePath);
|
|
2922
3109
|
}
|
|
2923
3110
|
var native = getNativeBinding();
|
|
2924
3111
|
function parseFiles(files) {
|
|
@@ -3036,7 +3223,7 @@ var VectorStore = class {
|
|
|
3036
3223
|
var CHARS_PER_TOKEN = 4;
|
|
3037
3224
|
var MAX_BATCH_TOKENS = 7500;
|
|
3038
3225
|
var MAX_SINGLE_CHUNK_TOKENS = 2e3;
|
|
3039
|
-
function
|
|
3226
|
+
function estimateTokens2(text) {
|
|
3040
3227
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3041
3228
|
}
|
|
3042
3229
|
function createEmbeddingText(chunk, filePath) {
|
|
@@ -3101,7 +3288,7 @@ function createDynamicBatches(chunks) {
|
|
|
3101
3288
|
let currentBatch = [];
|
|
3102
3289
|
let currentTokens = 0;
|
|
3103
3290
|
for (const chunk of chunks) {
|
|
3104
|
-
const chunkTokens =
|
|
3291
|
+
const chunkTokens = estimateTokens2(chunk.text);
|
|
3105
3292
|
if (currentBatch.length > 0 && currentTokens + chunkTokens > MAX_BATCH_TOKENS) {
|
|
3106
3293
|
batches.push(currentBatch);
|
|
3107
3294
|
currentBatch = [];
|
|
@@ -3398,11 +3585,40 @@ var Database = class {
|
|
|
3398
3585
|
};
|
|
3399
3586
|
|
|
3400
3587
|
// src/git/index.ts
|
|
3401
|
-
import { existsSync as existsSync3, readFileSync as
|
|
3402
|
-
import * as
|
|
3403
|
-
|
|
3588
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
3589
|
+
import * as path5 from "path";
|
|
3590
|
+
function readPackedRefs(gitDir) {
|
|
3591
|
+
const packedRefsPath = path5.join(gitDir, "packed-refs");
|
|
3592
|
+
if (!existsSync3(packedRefsPath)) {
|
|
3593
|
+
return [];
|
|
3594
|
+
}
|
|
3595
|
+
try {
|
|
3596
|
+
return readFileSync4(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
|
|
3597
|
+
} catch {
|
|
3598
|
+
return [];
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
function resolveCommonGitDir(gitDir) {
|
|
3602
|
+
const commonDirPath = path5.join(gitDir, "commondir");
|
|
3603
|
+
if (!existsSync3(commonDirPath)) {
|
|
3604
|
+
return gitDir;
|
|
3605
|
+
}
|
|
3606
|
+
try {
|
|
3607
|
+
const raw = readFileSync4(commonDirPath, "utf-8").trim();
|
|
3608
|
+
if (!raw) {
|
|
3609
|
+
return gitDir;
|
|
3610
|
+
}
|
|
3611
|
+
const resolved = path5.isAbsolute(raw) ? raw : path5.resolve(gitDir, raw);
|
|
3612
|
+
if (existsSync3(resolved)) {
|
|
3613
|
+
return resolved;
|
|
3614
|
+
}
|
|
3615
|
+
} catch {
|
|
3616
|
+
return gitDir;
|
|
3617
|
+
}
|
|
3618
|
+
return gitDir;
|
|
3619
|
+
}
|
|
3404
3620
|
function resolveGitDir(repoRoot) {
|
|
3405
|
-
const gitPath =
|
|
3621
|
+
const gitPath = path5.join(repoRoot, ".git");
|
|
3406
3622
|
if (!existsSync3(gitPath)) {
|
|
3407
3623
|
return null;
|
|
3408
3624
|
}
|
|
@@ -3412,11 +3628,11 @@ function resolveGitDir(repoRoot) {
|
|
|
3412
3628
|
return gitPath;
|
|
3413
3629
|
}
|
|
3414
3630
|
if (stat.isFile()) {
|
|
3415
|
-
const content =
|
|
3631
|
+
const content = readFileSync4(gitPath, "utf-8").trim();
|
|
3416
3632
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3417
3633
|
if (match) {
|
|
3418
3634
|
const gitdir = match[1];
|
|
3419
|
-
const resolvedPath =
|
|
3635
|
+
const resolvedPath = path5.isAbsolute(gitdir) ? gitdir : path5.resolve(repoRoot, gitdir);
|
|
3420
3636
|
if (existsSync3(resolvedPath)) {
|
|
3421
3637
|
return resolvedPath;
|
|
3422
3638
|
}
|
|
@@ -3434,12 +3650,12 @@ function getCurrentBranch(repoRoot) {
|
|
|
3434
3650
|
if (!gitDir) {
|
|
3435
3651
|
return null;
|
|
3436
3652
|
}
|
|
3437
|
-
const headPath =
|
|
3653
|
+
const headPath = path5.join(gitDir, "HEAD");
|
|
3438
3654
|
if (!existsSync3(headPath)) {
|
|
3439
3655
|
return null;
|
|
3440
3656
|
}
|
|
3441
3657
|
try {
|
|
3442
|
-
const headContent =
|
|
3658
|
+
const headContent = readFileSync4(headPath, "utf-8").trim();
|
|
3443
3659
|
const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
|
|
3444
3660
|
if (match) {
|
|
3445
3661
|
return match[1];
|
|
@@ -3454,37 +3670,20 @@ function getCurrentBranch(repoRoot) {
|
|
|
3454
3670
|
}
|
|
3455
3671
|
function getBaseBranch(repoRoot) {
|
|
3456
3672
|
const gitDir = resolveGitDir(repoRoot);
|
|
3673
|
+
const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
|
|
3457
3674
|
const candidates = ["main", "master", "develop", "trunk"];
|
|
3458
|
-
if (
|
|
3675
|
+
if (refStoreDir) {
|
|
3459
3676
|
for (const candidate of candidates) {
|
|
3460
|
-
const refPath =
|
|
3677
|
+
const refPath = path5.join(refStoreDir, "refs", "heads", candidate);
|
|
3461
3678
|
if (existsSync3(refPath)) {
|
|
3462
3679
|
return candidate;
|
|
3463
3680
|
}
|
|
3464
|
-
const
|
|
3465
|
-
if (
|
|
3466
|
-
|
|
3467
|
-
const content = readFileSync3(packedRefsPath, "utf-8");
|
|
3468
|
-
if (content.includes(`refs/heads/${candidate}`)) {
|
|
3469
|
-
return candidate;
|
|
3470
|
-
}
|
|
3471
|
-
} catch {
|
|
3472
|
-
}
|
|
3681
|
+
const packedRefs = readPackedRefs(refStoreDir);
|
|
3682
|
+
if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
|
|
3683
|
+
return candidate;
|
|
3473
3684
|
}
|
|
3474
3685
|
}
|
|
3475
3686
|
}
|
|
3476
|
-
try {
|
|
3477
|
-
const result = execSync("git remote show origin", {
|
|
3478
|
-
cwd: repoRoot,
|
|
3479
|
-
encoding: "utf-8",
|
|
3480
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3481
|
-
});
|
|
3482
|
-
const match = result.match(/HEAD branch: (.+)/);
|
|
3483
|
-
if (match) {
|
|
3484
|
-
return match[1].trim();
|
|
3485
|
-
}
|
|
3486
|
-
} catch {
|
|
3487
|
-
}
|
|
3488
3687
|
return getCurrentBranch(repoRoot) ?? "main";
|
|
3489
3688
|
}
|
|
3490
3689
|
function getBranchOrDefault(repoRoot) {
|
|
@@ -3495,7 +3694,7 @@ function getBranchOrDefault(repoRoot) {
|
|
|
3495
3694
|
}
|
|
3496
3695
|
|
|
3497
3696
|
// src/indexer/index.ts
|
|
3498
|
-
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
|
|
3697
|
+
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php"]);
|
|
3499
3698
|
var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
3500
3699
|
"function_declaration",
|
|
3501
3700
|
"function",
|
|
@@ -3516,7 +3715,8 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
|
3516
3715
|
"struct_item",
|
|
3517
3716
|
"enum_item",
|
|
3518
3717
|
"trait_item",
|
|
3519
|
-
"mod_item"
|
|
3718
|
+
"mod_item",
|
|
3719
|
+
"trait_declaration"
|
|
3520
3720
|
]);
|
|
3521
3721
|
function float32ArrayToBuffer(arr) {
|
|
3522
3722
|
const float32 = new Float32Array(arr);
|
|
@@ -3898,8 +4098,8 @@ function stripFilePathHint(query) {
|
|
|
3898
4098
|
const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
|
|
3899
4099
|
return stripped.length > 0 ? stripped : query;
|
|
3900
4100
|
}
|
|
3901
|
-
function buildDeterministicIdentifierPass(query, candidates, limit) {
|
|
3902
|
-
if (
|
|
4101
|
+
function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4102
|
+
if (!prioritizeSourcePaths) {
|
|
3903
4103
|
return [];
|
|
3904
4104
|
}
|
|
3905
4105
|
const primary = extractPrimaryIdentifierQueryHint(query);
|
|
@@ -4115,9 +4315,8 @@ function rankHybridResults(query, semanticResults, keywordResults, options) {
|
|
|
4115
4315
|
const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
|
|
4116
4316
|
const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
|
|
4117
4317
|
const rerankPool = fused.slice(0, rerankPoolLimit);
|
|
4118
|
-
const intent = classifyQueryIntentRaw(query);
|
|
4119
4318
|
return rerankResults(query, rerankPool, options.rerankTopN, {
|
|
4120
|
-
prioritizeSourcePaths:
|
|
4319
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source"
|
|
4121
4320
|
});
|
|
4122
4321
|
}
|
|
4123
4322
|
function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
@@ -4127,11 +4326,11 @@ function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
|
4127
4326
|
prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
|
|
4128
4327
|
});
|
|
4129
4328
|
}
|
|
4130
|
-
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
|
|
4329
|
+
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4131
4330
|
if (combined.length === 0) {
|
|
4132
4331
|
return combined;
|
|
4133
4332
|
}
|
|
4134
|
-
if (
|
|
4333
|
+
if (!prioritizeSourcePaths) {
|
|
4135
4334
|
return combined;
|
|
4136
4335
|
}
|
|
4137
4336
|
const identifierHints = extractIdentifierHints(query);
|
|
@@ -4221,8 +4420,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
|
|
|
4221
4420
|
const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
|
|
4222
4421
|
return [...promoted, ...remainder];
|
|
4223
4422
|
}
|
|
4224
|
-
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
|
|
4225
|
-
if (
|
|
4423
|
+
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4424
|
+
if (!prioritizeSourcePaths) {
|
|
4226
4425
|
return [];
|
|
4227
4426
|
}
|
|
4228
4427
|
const identifierHints = extractIdentifierHints(query);
|
|
@@ -4367,8 +4566,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
|
|
|
4367
4566
|
const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4368
4567
|
return withFallback.slice(0, Math.max(limit * 2, limit));
|
|
4369
4568
|
}
|
|
4370
|
-
function buildIdentifierDefinitionLane(query, candidates, limit) {
|
|
4371
|
-
if (
|
|
4569
|
+
function buildIdentifierDefinitionLane(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4570
|
+
if (!prioritizeSourcePaths) {
|
|
4372
4571
|
return [];
|
|
4373
4572
|
}
|
|
4374
4573
|
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
@@ -4453,22 +4652,22 @@ var Indexer = class {
|
|
|
4453
4652
|
this.projectRoot = projectRoot;
|
|
4454
4653
|
this.config = config;
|
|
4455
4654
|
this.indexPath = this.getIndexPath();
|
|
4456
|
-
this.fileHashCachePath =
|
|
4457
|
-
this.failedBatchesPath =
|
|
4458
|
-
this.indexingLockPath =
|
|
4655
|
+
this.fileHashCachePath = path6.join(this.indexPath, "file-hashes.json");
|
|
4656
|
+
this.failedBatchesPath = path6.join(this.indexPath, "failed-batches.json");
|
|
4657
|
+
this.indexingLockPath = path6.join(this.indexPath, "indexing.lock");
|
|
4459
4658
|
this.logger = initializeLogger(config.debug);
|
|
4460
4659
|
}
|
|
4461
4660
|
getIndexPath() {
|
|
4462
4661
|
if (this.config.scope === "global") {
|
|
4463
4662
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
4464
|
-
return
|
|
4663
|
+
return path6.join(homeDir, ".opencode", "global-index");
|
|
4465
4664
|
}
|
|
4466
|
-
return
|
|
4665
|
+
return path6.join(this.projectRoot, ".opencode", "index");
|
|
4467
4666
|
}
|
|
4468
4667
|
loadFileHashCache() {
|
|
4469
4668
|
try {
|
|
4470
4669
|
if (existsSync4(this.fileHashCachePath)) {
|
|
4471
|
-
const data =
|
|
4670
|
+
const data = readFileSync5(this.fileHashCachePath, "utf-8");
|
|
4472
4671
|
const parsed = JSON.parse(data);
|
|
4473
4672
|
this.fileHashCache = new Map(Object.entries(parsed));
|
|
4474
4673
|
}
|
|
@@ -4485,7 +4684,7 @@ var Indexer = class {
|
|
|
4485
4684
|
}
|
|
4486
4685
|
atomicWriteSync(targetPath, data) {
|
|
4487
4686
|
const tempPath = `${targetPath}.tmp`;
|
|
4488
|
-
|
|
4687
|
+
writeFileSync2(tempPath, data);
|
|
4489
4688
|
renameSync(tempPath, targetPath);
|
|
4490
4689
|
}
|
|
4491
4690
|
checkForInterruptedIndexing() {
|
|
@@ -4496,7 +4695,7 @@ var Indexer = class {
|
|
|
4496
4695
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4497
4696
|
pid: process.pid
|
|
4498
4697
|
};
|
|
4499
|
-
|
|
4698
|
+
writeFileSync2(this.indexingLockPath, JSON.stringify(lockData));
|
|
4500
4699
|
}
|
|
4501
4700
|
releaseIndexingLock() {
|
|
4502
4701
|
if (existsSync4(this.indexingLockPath)) {
|
|
@@ -4515,7 +4714,7 @@ var Indexer = class {
|
|
|
4515
4714
|
loadFailedBatches() {
|
|
4516
4715
|
try {
|
|
4517
4716
|
if (existsSync4(this.failedBatchesPath)) {
|
|
4518
|
-
const data =
|
|
4717
|
+
const data = readFileSync5(this.failedBatchesPath, "utf-8");
|
|
4519
4718
|
return JSON.parse(data);
|
|
4520
4719
|
}
|
|
4521
4720
|
} catch {
|
|
@@ -4531,7 +4730,7 @@ var Indexer = class {
|
|
|
4531
4730
|
}
|
|
4532
4731
|
return;
|
|
4533
4732
|
}
|
|
4534
|
-
|
|
4733
|
+
writeFileSync2(this.failedBatchesPath, JSON.stringify(batches, null, 2));
|
|
4535
4734
|
}
|
|
4536
4735
|
addFailedBatch(batch, error) {
|
|
4537
4736
|
const existing = this.loadFailedBatches();
|
|
@@ -4590,13 +4789,13 @@ var Indexer = class {
|
|
|
4590
4789
|
this.provider = createEmbeddingProvider(this.configuredProviderInfo);
|
|
4591
4790
|
await fsPromises2.mkdir(this.indexPath, { recursive: true });
|
|
4592
4791
|
const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
|
|
4593
|
-
const storePath =
|
|
4792
|
+
const storePath = path6.join(this.indexPath, "vectors");
|
|
4594
4793
|
this.store = new VectorStore(storePath, dimensions);
|
|
4595
|
-
const indexFilePath =
|
|
4794
|
+
const indexFilePath = path6.join(this.indexPath, "vectors.usearch");
|
|
4596
4795
|
if (existsSync4(indexFilePath)) {
|
|
4597
4796
|
this.store.load();
|
|
4598
4797
|
}
|
|
4599
|
-
const invertedIndexPath =
|
|
4798
|
+
const invertedIndexPath = path6.join(this.indexPath, "inverted-index.json");
|
|
4600
4799
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
4601
4800
|
try {
|
|
4602
4801
|
this.invertedIndex.load();
|
|
@@ -4606,7 +4805,7 @@ var Indexer = class {
|
|
|
4606
4805
|
}
|
|
4607
4806
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
4608
4807
|
}
|
|
4609
|
-
const dbPath =
|
|
4808
|
+
const dbPath = path6.join(this.indexPath, "codebase.db");
|
|
4610
4809
|
const dbIsNew = !existsSync4(dbPath);
|
|
4611
4810
|
this.database = new Database(dbPath);
|
|
4612
4811
|
if (this.checkForInterruptedIndexing()) {
|
|
@@ -4885,7 +5084,7 @@ var Indexer = class {
|
|
|
4885
5084
|
for (const parsed of parsedFiles) {
|
|
4886
5085
|
currentFilePaths.add(parsed.path);
|
|
4887
5086
|
if (parsed.chunks.length === 0) {
|
|
4888
|
-
const relativePath =
|
|
5087
|
+
const relativePath = path6.relative(this.projectRoot, parsed.path);
|
|
4889
5088
|
stats.parseFailures.push(relativePath);
|
|
4890
5089
|
}
|
|
4891
5090
|
let fileChunkCount = 0;
|
|
@@ -5096,7 +5295,7 @@ var Indexer = class {
|
|
|
5096
5295
|
for (const batch of dynamicBatches) {
|
|
5097
5296
|
queue.add(async () => {
|
|
5098
5297
|
if (rateLimitBackoffMs > 0) {
|
|
5099
|
-
await new Promise((
|
|
5298
|
+
await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
|
|
5100
5299
|
}
|
|
5101
5300
|
try {
|
|
5102
5301
|
const result = await pRetry(
|
|
@@ -5300,6 +5499,7 @@ var Indexer = class {
|
|
|
5300
5499
|
const rrfK = this.config.search.rrfK;
|
|
5301
5500
|
const rerankTopN = this.config.search.rerankTopN;
|
|
5302
5501
|
const filterByBranch = options?.filterByBranch ?? true;
|
|
5502
|
+
const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
|
|
5303
5503
|
this.logger.search("debug", "Starting search", {
|
|
5304
5504
|
query,
|
|
5305
5505
|
maxResults,
|
|
@@ -5351,7 +5551,8 @@ var Indexer = class {
|
|
|
5351
5551
|
rrfK,
|
|
5352
5552
|
rerankTopN,
|
|
5353
5553
|
limit: maxResults,
|
|
5354
|
-
hybridWeight
|
|
5554
|
+
hybridWeight,
|
|
5555
|
+
prioritizeSourcePaths: sourceIntent
|
|
5355
5556
|
});
|
|
5356
5557
|
const fusionMs = performance2.now() - fusionStartTime;
|
|
5357
5558
|
const rescued = promoteIdentifierMatches(
|
|
@@ -5360,30 +5561,33 @@ var Indexer = class {
|
|
|
5360
5561
|
semanticCandidates,
|
|
5361
5562
|
keywordCandidates,
|
|
5362
5563
|
database,
|
|
5363
|
-
branchChunkIds
|
|
5564
|
+
branchChunkIds,
|
|
5565
|
+
sourceIntent
|
|
5364
5566
|
);
|
|
5365
5567
|
const union = unionCandidates(semanticCandidates, keywordCandidates);
|
|
5366
5568
|
const deterministicIdentifierLane = buildDeterministicIdentifierPass(
|
|
5367
5569
|
query,
|
|
5368
5570
|
union,
|
|
5369
|
-
maxResults
|
|
5571
|
+
maxResults,
|
|
5572
|
+
sourceIntent
|
|
5370
5573
|
);
|
|
5371
5574
|
const identifierLane = buildIdentifierDefinitionLane(
|
|
5372
5575
|
query,
|
|
5373
5576
|
union,
|
|
5374
|
-
maxResults
|
|
5577
|
+
maxResults,
|
|
5578
|
+
sourceIntent
|
|
5375
5579
|
);
|
|
5376
5580
|
const symbolLane = buildSymbolDefinitionLane(
|
|
5377
5581
|
query,
|
|
5378
5582
|
database,
|
|
5379
5583
|
branchChunkIds,
|
|
5380
5584
|
maxResults,
|
|
5381
|
-
union
|
|
5585
|
+
union,
|
|
5586
|
+
sourceIntent
|
|
5382
5587
|
);
|
|
5383
5588
|
const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
|
|
5384
5589
|
const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
|
|
5385
5590
|
const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
|
|
5386
|
-
const sourceIntent = classifyQueryIntentRaw(query) === "source";
|
|
5387
5591
|
const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
|
|
5388
5592
|
const baseFiltered = tiered.filter((r) => {
|
|
5389
5593
|
if (r.score < this.config.search.minScore) return false;
|
|
@@ -5740,7 +5944,896 @@ var Indexer = class {
|
|
|
5740
5944
|
}
|
|
5741
5945
|
};
|
|
5742
5946
|
|
|
5947
|
+
// src/eval/budget.ts
|
|
5948
|
+
function evaluateBudgetGate(budget, summary, comparison) {
|
|
5949
|
+
const BASELINE_P95_EPSILON_MS = 1e-3;
|
|
5950
|
+
const violations = [];
|
|
5951
|
+
const { thresholds } = budget;
|
|
5952
|
+
if (thresholds.minHitAt5 !== void 0 && summary.metrics.hitAt5 < thresholds.minHitAt5) {
|
|
5953
|
+
violations.push({
|
|
5954
|
+
metric: "minHitAt5",
|
|
5955
|
+
message: `Hit@5 ${summary.metrics.hitAt5.toFixed(4)} is below minimum ${thresholds.minHitAt5.toFixed(4)}`
|
|
5956
|
+
});
|
|
5957
|
+
}
|
|
5958
|
+
if (thresholds.minMrrAt10 !== void 0 && summary.metrics.mrrAt10 < thresholds.minMrrAt10) {
|
|
5959
|
+
violations.push({
|
|
5960
|
+
metric: "minMrrAt10",
|
|
5961
|
+
message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5964
|
+
if (comparison) {
|
|
5965
|
+
if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
|
|
5966
|
+
violations.push({
|
|
5967
|
+
metric: "hitAt5MaxDrop",
|
|
5968
|
+
message: `Hit@5 drop ${comparison.deltas.hitAt5.absolute.toFixed(4)} exceeds allowed -${thresholds.hitAt5MaxDrop.toFixed(4)}`
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
if (thresholds.mrrAt10MaxDrop !== void 0 && comparison.deltas.mrrAt10.absolute < -thresholds.mrrAt10MaxDrop) {
|
|
5972
|
+
violations.push({
|
|
5973
|
+
metric: "mrrAt10MaxDrop",
|
|
5974
|
+
message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
if (thresholds.p95LatencyMaxMultiplier !== void 0) {
|
|
5978
|
+
const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
|
|
5979
|
+
if (baselineP95 > BASELINE_P95_EPSILON_MS) {
|
|
5980
|
+
const allowed = baselineP95 * thresholds.p95LatencyMaxMultiplier;
|
|
5981
|
+
if (summary.metrics.latencyMs.p95 > allowed) {
|
|
5982
|
+
violations.push({
|
|
5983
|
+
metric: "p95LatencyMaxMultiplier",
|
|
5984
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds allowed ${allowed.toFixed(3)}ms (${thresholds.p95LatencyMaxMultiplier.toFixed(2)}x baseline)`
|
|
5985
|
+
});
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
if (thresholds.p95LatencyMaxAbsoluteMs !== void 0 && summary.metrics.latencyMs.p95 > thresholds.p95LatencyMaxAbsoluteMs) {
|
|
5991
|
+
violations.push({
|
|
5992
|
+
metric: "p95LatencyMaxAbsoluteMs",
|
|
5993
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds absolute maximum ${thresholds.p95LatencyMaxAbsoluteMs.toFixed(3)}ms`
|
|
5994
|
+
});
|
|
5995
|
+
}
|
|
5996
|
+
return {
|
|
5997
|
+
passed: violations.length === 0,
|
|
5998
|
+
budgetName: budget.name,
|
|
5999
|
+
violations
|
|
6000
|
+
};
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
// src/eval/metrics.ts
|
|
6004
|
+
function percentile(values, p) {
|
|
6005
|
+
if (values.length === 0) return 0;
|
|
6006
|
+
if (values.length === 1) return values[0];
|
|
6007
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6008
|
+
const x = p * (sorted.length - 1);
|
|
6009
|
+
const lowerIndex = Math.floor(x);
|
|
6010
|
+
const upperIndex = Math.ceil(x);
|
|
6011
|
+
if (lowerIndex === upperIndex) {
|
|
6012
|
+
return sorted[lowerIndex];
|
|
6013
|
+
}
|
|
6014
|
+
const fraction = x - lowerIndex;
|
|
6015
|
+
return sorted[lowerIndex] + fraction * (sorted[upperIndex] - sorted[lowerIndex]);
|
|
6016
|
+
}
|
|
6017
|
+
function normalizePath(input) {
|
|
6018
|
+
return input.replace(/\\/g, "/");
|
|
6019
|
+
}
|
|
6020
|
+
function uniqueResultsByPath(results) {
|
|
6021
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6022
|
+
const unique = [];
|
|
6023
|
+
for (const result of results) {
|
|
6024
|
+
const normalized = normalizePath(result.filePath);
|
|
6025
|
+
if (seen.has(normalized)) continue;
|
|
6026
|
+
seen.add(normalized);
|
|
6027
|
+
unique.push(result);
|
|
6028
|
+
}
|
|
6029
|
+
return unique;
|
|
6030
|
+
}
|
|
6031
|
+
function pathMatchesExpected(actualPath, expectedPath) {
|
|
6032
|
+
const actual = normalizePath(actualPath);
|
|
6033
|
+
const expected = normalizePath(expectedPath);
|
|
6034
|
+
if (actual === expected) return true;
|
|
6035
|
+
return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
|
|
6036
|
+
}
|
|
6037
|
+
function getRelevantPaths(query) {
|
|
6038
|
+
const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
|
|
6039
|
+
const fromAcceptable = query.expected.acceptableFiles ?? [];
|
|
6040
|
+
return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
|
|
6041
|
+
}
|
|
6042
|
+
function isRelevantResult(filePath, relevantPaths) {
|
|
6043
|
+
return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
|
|
6044
|
+
}
|
|
6045
|
+
function reciprocalRankAtK(results, relevantPaths, k) {
|
|
6046
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6047
|
+
for (let i = 0; i < top.length; i += 1) {
|
|
6048
|
+
if (isRelevantResult(top[i].filePath, relevantPaths)) {
|
|
6049
|
+
return 1 / (i + 1);
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
return 0;
|
|
6053
|
+
}
|
|
6054
|
+
function ndcgAtK(results, relevantPaths, k) {
|
|
6055
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6056
|
+
const dcg = top.reduce((sum, result, i) => {
|
|
6057
|
+
const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
|
|
6058
|
+
return sum + rel / Math.log2(i + 2);
|
|
6059
|
+
}, 0);
|
|
6060
|
+
const idealLen = Math.min(k, relevantPaths.length);
|
|
6061
|
+
const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
|
|
6062
|
+
(sum, value) => sum + value,
|
|
6063
|
+
0
|
|
6064
|
+
);
|
|
6065
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
6066
|
+
}
|
|
6067
|
+
function isDocsOrTestsPath(filePath) {
|
|
6068
|
+
const lowered = normalizePath(filePath).toLowerCase();
|
|
6069
|
+
return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
|
|
6070
|
+
}
|
|
6071
|
+
function classifyFailureBucket(query, results, k) {
|
|
6072
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6073
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6074
|
+
const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6075
|
+
if (!hasRelevantTopK) {
|
|
6076
|
+
return "no-relevant-hit-top-k";
|
|
6077
|
+
}
|
|
6078
|
+
if (query.expected.symbol) {
|
|
6079
|
+
const hasSymbol = top.some(
|
|
6080
|
+
(result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
|
|
6081
|
+
);
|
|
6082
|
+
if (!hasSymbol) return "wrong-symbol";
|
|
6083
|
+
}
|
|
6084
|
+
const top1 = top[0];
|
|
6085
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
|
|
6086
|
+
return "docs-tests-outranking-source";
|
|
6087
|
+
}
|
|
6088
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
|
|
6089
|
+
return "wrong-file";
|
|
6090
|
+
}
|
|
6091
|
+
return void 0;
|
|
6092
|
+
}
|
|
6093
|
+
function buildPerQueryResult(query, results, latencyMs, k) {
|
|
6094
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6095
|
+
const deduped = uniqueResultsByPath(results);
|
|
6096
|
+
const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6097
|
+
const perQuery = {
|
|
6098
|
+
id: query.id,
|
|
6099
|
+
query: query.query,
|
|
6100
|
+
queryType: query.queryType,
|
|
6101
|
+
latencyMs,
|
|
6102
|
+
hitAt1: hitAt(1),
|
|
6103
|
+
hitAt3: hitAt(3),
|
|
6104
|
+
hitAt5: hitAt(5),
|
|
6105
|
+
hitAt10: hitAt(10),
|
|
6106
|
+
reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
|
|
6107
|
+
ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
|
|
6108
|
+
failureBucket: classifyFailureBucket(query, results, k),
|
|
6109
|
+
results: deduped
|
|
6110
|
+
};
|
|
6111
|
+
return perQuery;
|
|
6112
|
+
}
|
|
6113
|
+
function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
|
|
6114
|
+
const count = perQuery.length;
|
|
6115
|
+
const safeDiv = (value) => count === 0 ? 0 : value / count;
|
|
6116
|
+
const sum = {
|
|
6117
|
+
hitAt1: 0,
|
|
6118
|
+
hitAt3: 0,
|
|
6119
|
+
hitAt5: 0,
|
|
6120
|
+
hitAt10: 0,
|
|
6121
|
+
mrrAt10: 0,
|
|
6122
|
+
ndcgAt10: 0
|
|
6123
|
+
};
|
|
6124
|
+
const failureBuckets = {
|
|
6125
|
+
"wrong-file": 0,
|
|
6126
|
+
"wrong-symbol": 0,
|
|
6127
|
+
"docs-tests-outranking-source": 0,
|
|
6128
|
+
"no-relevant-hit-top-k": 0
|
|
6129
|
+
};
|
|
6130
|
+
const latencies = perQuery.map((item) => item.latencyMs);
|
|
6131
|
+
for (const query of perQuery) {
|
|
6132
|
+
if (query.hitAt1) sum.hitAt1 += 1;
|
|
6133
|
+
if (query.hitAt3) sum.hitAt3 += 1;
|
|
6134
|
+
if (query.hitAt5) sum.hitAt5 += 1;
|
|
6135
|
+
if (query.hitAt10) sum.hitAt10 += 1;
|
|
6136
|
+
sum.mrrAt10 += query.reciprocalRankAt10;
|
|
6137
|
+
sum.ndcgAt10 += query.ndcgAt10;
|
|
6138
|
+
if (query.failureBucket) {
|
|
6139
|
+
failureBuckets[query.failureBucket] += 1;
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
|
|
6143
|
+
return {
|
|
6144
|
+
hitAt1: safeDiv(sum.hitAt1),
|
|
6145
|
+
hitAt3: safeDiv(sum.hitAt3),
|
|
6146
|
+
hitAt5: safeDiv(sum.hitAt5),
|
|
6147
|
+
hitAt10: safeDiv(sum.hitAt10),
|
|
6148
|
+
mrrAt10: safeDiv(sum.mrrAt10),
|
|
6149
|
+
ndcgAt10: safeDiv(sum.ndcgAt10),
|
|
6150
|
+
latencyMs: {
|
|
6151
|
+
p50: percentile(latencies, 0.5),
|
|
6152
|
+
p95: percentile(latencies, 0.95),
|
|
6153
|
+
p99: percentile(latencies, 0.99)
|
|
6154
|
+
},
|
|
6155
|
+
tokenEstimate: {
|
|
6156
|
+
queryTokens,
|
|
6157
|
+
embeddingTokensUsed
|
|
6158
|
+
},
|
|
6159
|
+
embedding: {
|
|
6160
|
+
callCount: embeddingCallCount,
|
|
6161
|
+
estimatedCostUsd: embeddingTokensUsed / 1e6 * costPer1MTokensUsd,
|
|
6162
|
+
costPer1MTokensUsd
|
|
6163
|
+
},
|
|
6164
|
+
failureBuckets
|
|
6165
|
+
};
|
|
6166
|
+
}
|
|
6167
|
+
|
|
6168
|
+
// src/eval/schema.ts
|
|
6169
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
6170
|
+
function parseJsonFile(filePath) {
|
|
6171
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
6172
|
+
return JSON.parse(content);
|
|
6173
|
+
}
|
|
6174
|
+
function isRecord(value) {
|
|
6175
|
+
return typeof value === "object" && value !== null;
|
|
6176
|
+
}
|
|
6177
|
+
function isStringArray2(value) {
|
|
6178
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6179
|
+
}
|
|
6180
|
+
function asPositiveNumber(value, path10) {
|
|
6181
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
6182
|
+
throw new Error(`${path10} must be a non-negative number`);
|
|
6183
|
+
}
|
|
6184
|
+
return value;
|
|
6185
|
+
}
|
|
6186
|
+
function parseQueryType(value, path10) {
|
|
6187
|
+
if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
|
|
6188
|
+
return value;
|
|
6189
|
+
}
|
|
6190
|
+
throw new Error(
|
|
6191
|
+
`${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
|
|
6192
|
+
);
|
|
6193
|
+
}
|
|
6194
|
+
function parseExpected(input, path10) {
|
|
6195
|
+
if (!isRecord(input)) {
|
|
6196
|
+
throw new Error(`${path10} must be an object`);
|
|
6197
|
+
}
|
|
6198
|
+
const filePathRaw = input.filePath;
|
|
6199
|
+
const acceptableFilesRaw = input.acceptableFiles;
|
|
6200
|
+
const symbolRaw = input.symbol;
|
|
6201
|
+
const branchRaw = input.branch;
|
|
6202
|
+
const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
|
|
6203
|
+
const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
|
|
6204
|
+
if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
|
|
6205
|
+
throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
|
|
6206
|
+
}
|
|
6207
|
+
if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
|
|
6208
|
+
throw new Error(`${path10}.acceptableFiles must be an array of strings`);
|
|
6209
|
+
}
|
|
6210
|
+
if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
|
|
6211
|
+
throw new Error(`${path10}.symbol must be a string when provided`);
|
|
6212
|
+
}
|
|
6213
|
+
if (branchRaw !== void 0 && typeof branchRaw !== "string") {
|
|
6214
|
+
throw new Error(`${path10}.branch must be a string when provided`);
|
|
6215
|
+
}
|
|
6216
|
+
return {
|
|
6217
|
+
filePath,
|
|
6218
|
+
acceptableFiles,
|
|
6219
|
+
symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
|
|
6220
|
+
branch: typeof branchRaw === "string" ? branchRaw : void 0
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
function parseQuery(input, index) {
|
|
6224
|
+
const path10 = `queries[${index}]`;
|
|
6225
|
+
if (!isRecord(input)) {
|
|
6226
|
+
throw new Error(`${path10} must be an object`);
|
|
6227
|
+
}
|
|
6228
|
+
const id = input.id;
|
|
6229
|
+
const query = input.query;
|
|
6230
|
+
const queryType = input.queryType;
|
|
6231
|
+
const expected = input.expected;
|
|
6232
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
6233
|
+
throw new Error(`${path10}.id must be a non-empty string`);
|
|
6234
|
+
}
|
|
6235
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
6236
|
+
throw new Error(`${path10}.query must be a non-empty string`);
|
|
6237
|
+
}
|
|
6238
|
+
return {
|
|
6239
|
+
id,
|
|
6240
|
+
query,
|
|
6241
|
+
queryType: parseQueryType(queryType, `${path10}.queryType`),
|
|
6242
|
+
expected: parseExpected(expected, `${path10}.expected`)
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
function parseGoldenDataset(raw, sourceLabel) {
|
|
6246
|
+
if (!isRecord(raw)) {
|
|
6247
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6248
|
+
}
|
|
6249
|
+
const version = raw.version;
|
|
6250
|
+
const name = raw.name;
|
|
6251
|
+
const description = raw.description;
|
|
6252
|
+
const queriesRaw = raw.queries;
|
|
6253
|
+
if (typeof version !== "string" || version.trim().length === 0) {
|
|
6254
|
+
throw new Error(`${sourceLabel}.version must be a non-empty string`);
|
|
6255
|
+
}
|
|
6256
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6257
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6258
|
+
}
|
|
6259
|
+
if (description !== void 0 && typeof description !== "string") {
|
|
6260
|
+
throw new Error(`${sourceLabel}.description must be a string when provided`);
|
|
6261
|
+
}
|
|
6262
|
+
if (!Array.isArray(queriesRaw)) {
|
|
6263
|
+
throw new Error(`${sourceLabel}.queries must be an array`);
|
|
6264
|
+
}
|
|
6265
|
+
if (queriesRaw.length === 0) {
|
|
6266
|
+
throw new Error(`${sourceLabel}.queries must contain at least one query`);
|
|
6267
|
+
}
|
|
6268
|
+
const queries = queriesRaw.map((query, idx) => parseQuery(query, idx));
|
|
6269
|
+
const idSet = /* @__PURE__ */ new Set();
|
|
6270
|
+
for (const query of queries) {
|
|
6271
|
+
if (idSet.has(query.id)) {
|
|
6272
|
+
throw new Error(`${sourceLabel}.queries has duplicate id: ${query.id}`);
|
|
6273
|
+
}
|
|
6274
|
+
idSet.add(query.id);
|
|
6275
|
+
}
|
|
6276
|
+
return {
|
|
6277
|
+
version,
|
|
6278
|
+
name,
|
|
6279
|
+
description: typeof description === "string" ? description : void 0,
|
|
6280
|
+
queries
|
|
6281
|
+
};
|
|
6282
|
+
}
|
|
6283
|
+
function loadGoldenDataset(datasetPath) {
|
|
6284
|
+
const parsed = parseJsonFile(datasetPath);
|
|
6285
|
+
return parseGoldenDataset(parsed, datasetPath);
|
|
6286
|
+
}
|
|
6287
|
+
function parseBudget(raw, sourceLabel) {
|
|
6288
|
+
if (!isRecord(raw)) {
|
|
6289
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6290
|
+
}
|
|
6291
|
+
const name = raw.name;
|
|
6292
|
+
const baselinePath = raw.baselinePath;
|
|
6293
|
+
const failOnMissingBaseline = raw.failOnMissingBaseline;
|
|
6294
|
+
const thresholds = raw.thresholds;
|
|
6295
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6296
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6297
|
+
}
|
|
6298
|
+
if (baselinePath !== void 0 && typeof baselinePath !== "string") {
|
|
6299
|
+
throw new Error(`${sourceLabel}.baselinePath must be a string when provided`);
|
|
6300
|
+
}
|
|
6301
|
+
if (!isRecord(thresholds)) {
|
|
6302
|
+
throw new Error(`${sourceLabel}.thresholds must be an object`);
|
|
6303
|
+
}
|
|
6304
|
+
return {
|
|
6305
|
+
name,
|
|
6306
|
+
baselinePath: typeof baselinePath === "string" ? baselinePath : void 0,
|
|
6307
|
+
failOnMissingBaseline: typeof failOnMissingBaseline === "boolean" ? failOnMissingBaseline : true,
|
|
6308
|
+
thresholds: {
|
|
6309
|
+
hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
|
|
6310
|
+
mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
|
|
6311
|
+
p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
|
|
6312
|
+
thresholds.p95LatencyMaxMultiplier,
|
|
6313
|
+
`${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
|
|
6314
|
+
),
|
|
6315
|
+
p95LatencyMaxAbsoluteMs: thresholds.p95LatencyMaxAbsoluteMs === void 0 ? void 0 : asPositiveNumber(
|
|
6316
|
+
thresholds.p95LatencyMaxAbsoluteMs,
|
|
6317
|
+
`${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
|
|
6318
|
+
),
|
|
6319
|
+
minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
|
|
6320
|
+
minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
|
|
6321
|
+
}
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
function loadBudget(budgetPath) {
|
|
6325
|
+
const parsed = parseJsonFile(budgetPath);
|
|
6326
|
+
return parseBudget(parsed, budgetPath);
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
// src/eval/runner.ts
|
|
6330
|
+
function toAbsolute(projectRoot, maybeRelative) {
|
|
6331
|
+
return path7.isAbsolute(maybeRelative) ? maybeRelative : path7.join(projectRoot, maybeRelative);
|
|
6332
|
+
}
|
|
6333
|
+
function loadRawConfig(projectRoot, configPath) {
|
|
6334
|
+
const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
|
|
6335
|
+
if (fromPath && existsSync5(fromPath)) {
|
|
6336
|
+
return JSON.parse(readFileSync7(fromPath, "utf-8"));
|
|
6337
|
+
}
|
|
6338
|
+
const projectConfig = path7.join(projectRoot, ".opencode", "codebase-index.json");
|
|
6339
|
+
if (existsSync5(projectConfig)) {
|
|
6340
|
+
return JSON.parse(readFileSync7(projectConfig, "utf-8"));
|
|
6341
|
+
}
|
|
6342
|
+
const globalConfig = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
|
|
6343
|
+
if (existsSync5(globalConfig)) {
|
|
6344
|
+
return JSON.parse(readFileSync7(globalConfig, "utf-8"));
|
|
6345
|
+
}
|
|
6346
|
+
return {};
|
|
6347
|
+
}
|
|
6348
|
+
function getIndexRootPath(projectRoot, scope) {
|
|
6349
|
+
if (scope === "global") {
|
|
6350
|
+
return path7.join(os3.homedir(), ".opencode", "global-index");
|
|
6351
|
+
}
|
|
6352
|
+
return path7.join(projectRoot, ".opencode", "index");
|
|
6353
|
+
}
|
|
6354
|
+
function clearIndexRoot(projectRoot, scope) {
|
|
6355
|
+
const indexRoot = getIndexRootPath(projectRoot, scope);
|
|
6356
|
+
if (existsSync5(indexRoot)) {
|
|
6357
|
+
rmSync(indexRoot, { recursive: true, force: true });
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
function loadParsedConfig(projectRoot, configPath) {
|
|
6361
|
+
const raw = loadRawConfig(projectRoot, configPath);
|
|
6362
|
+
return parseConfig(raw);
|
|
6363
|
+
}
|
|
6364
|
+
function resolveSearchConfig(parsedConfig, overrides) {
|
|
6365
|
+
const nextSearch = {
|
|
6366
|
+
...parsedConfig.search
|
|
6367
|
+
};
|
|
6368
|
+
if (overrides?.fusionStrategy !== void 0) {
|
|
6369
|
+
nextSearch.fusionStrategy = overrides.fusionStrategy;
|
|
6370
|
+
}
|
|
6371
|
+
if (overrides?.hybridWeight !== void 0) {
|
|
6372
|
+
nextSearch.hybridWeight = overrides.hybridWeight;
|
|
6373
|
+
}
|
|
6374
|
+
if (overrides?.rrfK !== void 0) {
|
|
6375
|
+
nextSearch.rrfK = overrides.rrfK;
|
|
6376
|
+
}
|
|
6377
|
+
if (overrides?.rerankTopN !== void 0) {
|
|
6378
|
+
nextSearch.rerankTopN = overrides.rerankTopN;
|
|
6379
|
+
}
|
|
6380
|
+
return {
|
|
6381
|
+
...parsedConfig,
|
|
6382
|
+
search: nextSearch
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
async function runEvaluation(options) {
|
|
6386
|
+
const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
|
|
6387
|
+
const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
|
|
6388
|
+
const budgetPath = options.budgetPath ? toAbsolute(options.projectRoot, options.budgetPath) : void 0;
|
|
6389
|
+
const dataset = loadGoldenDataset(datasetPath);
|
|
6390
|
+
const parsedConfig = loadParsedConfig(options.projectRoot, options.configPath);
|
|
6391
|
+
const effectiveConfig = resolveSearchConfig(parsedConfig, options.searchOverrides);
|
|
6392
|
+
if (options.reindex) {
|
|
6393
|
+
clearIndexRoot(options.projectRoot, effectiveConfig.scope);
|
|
6394
|
+
}
|
|
6395
|
+
const indexer = new Indexer(options.projectRoot, effectiveConfig);
|
|
6396
|
+
await indexer.index();
|
|
6397
|
+
const perQuery = [];
|
|
6398
|
+
for (const query of dataset.queries) {
|
|
6399
|
+
if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
|
|
6400
|
+
throw new Error(
|
|
6401
|
+
`Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
|
|
6402
|
+
);
|
|
6403
|
+
}
|
|
6404
|
+
const start = performance3.now();
|
|
6405
|
+
const result = await indexer.search(query.query, 10, {
|
|
6406
|
+
metadataOnly: true,
|
|
6407
|
+
filterByBranch: query.expected.branch ? true : false
|
|
6408
|
+
});
|
|
6409
|
+
const elapsed = performance3.now() - start;
|
|
6410
|
+
const materialized = result.map((item) => ({
|
|
6411
|
+
filePath: item.filePath,
|
|
6412
|
+
startLine: item.startLine,
|
|
6413
|
+
endLine: item.endLine,
|
|
6414
|
+
score: item.score,
|
|
6415
|
+
chunkType: item.chunkType,
|
|
6416
|
+
name: item.name
|
|
6417
|
+
}));
|
|
6418
|
+
perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
|
|
6419
|
+
}
|
|
6420
|
+
const logger = indexer.getLogger();
|
|
6421
|
+
const metricSnapshot = logger.getMetrics();
|
|
6422
|
+
const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
|
|
6423
|
+
const summary = {
|
|
6424
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6425
|
+
projectRoot: options.projectRoot,
|
|
6426
|
+
datasetPath,
|
|
6427
|
+
datasetName: dataset.name,
|
|
6428
|
+
datasetVersion: dataset.version,
|
|
6429
|
+
queryCount: dataset.queries.length,
|
|
6430
|
+
topK: 10,
|
|
6431
|
+
searchConfig: {
|
|
6432
|
+
fusionStrategy: effectiveConfig.search.fusionStrategy,
|
|
6433
|
+
hybridWeight: effectiveConfig.search.hybridWeight,
|
|
6434
|
+
rrfK: effectiveConfig.search.rrfK,
|
|
6435
|
+
rerankTopN: effectiveConfig.search.rerankTopN
|
|
6436
|
+
},
|
|
6437
|
+
metrics: computeEvalMetrics(
|
|
6438
|
+
dataset.queries,
|
|
6439
|
+
perQuery,
|
|
6440
|
+
metricSnapshot.embeddingApiCalls,
|
|
6441
|
+
metricSnapshot.embeddingTokensUsed,
|
|
6442
|
+
costPer1MTokensUsd
|
|
6443
|
+
)
|
|
6444
|
+
};
|
|
6445
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6446
|
+
const perQueryArtifact = buildPerQueryArtifact(perQuery);
|
|
6447
|
+
writeJson(path7.join(outputDir, "summary.json"), summary);
|
|
6448
|
+
writeJson(path7.join(outputDir, "per-query.json"), perQueryArtifact);
|
|
6449
|
+
let comparison;
|
|
6450
|
+
if (againstPath) {
|
|
6451
|
+
const baseline = loadSummary(againstPath);
|
|
6452
|
+
comparison = compareSummaries(summary, baseline, againstPath);
|
|
6453
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6454
|
+
}
|
|
6455
|
+
let gate;
|
|
6456
|
+
if (options.ciMode) {
|
|
6457
|
+
if (!budgetPath) {
|
|
6458
|
+
throw new Error("CI mode requires --budget path");
|
|
6459
|
+
}
|
|
6460
|
+
const budget = loadBudget(budgetPath);
|
|
6461
|
+
if (!comparison && budget.baselinePath) {
|
|
6462
|
+
const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
|
|
6463
|
+
if (existsSync5(resolvedBaseline)) {
|
|
6464
|
+
const baselineSummary = loadSummary(resolvedBaseline);
|
|
6465
|
+
comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
|
|
6466
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6467
|
+
} else if (budget.failOnMissingBaseline) {
|
|
6468
|
+
throw new Error(
|
|
6469
|
+
`Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
|
|
6470
|
+
);
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
gate = evaluateBudgetGate(budget, summary, comparison);
|
|
6474
|
+
}
|
|
6475
|
+
const markdown = createSummaryMarkdown(summary, comparison, gate);
|
|
6476
|
+
writeText(path7.join(outputDir, "summary.md"), markdown);
|
|
6477
|
+
return { outputDir, summary, perQuery, comparison, gate };
|
|
6478
|
+
}
|
|
6479
|
+
async function runSweep(options, sweep) {
|
|
6480
|
+
const fusionValues = sweep.fusionStrategy && sweep.fusionStrategy.length > 0 ? [...sweep.fusionStrategy] : [void 0];
|
|
6481
|
+
const weightValues = sweep.hybridWeight && sweep.hybridWeight.length > 0 ? [...sweep.hybridWeight] : [void 0];
|
|
6482
|
+
const rrfValues = sweep.rrfK && sweep.rrfK.length > 0 ? [...sweep.rrfK] : [void 0];
|
|
6483
|
+
const rerankValues = sweep.rerankTopN && sweep.rerankTopN.length > 0 ? [...sweep.rerankTopN] : [void 0];
|
|
6484
|
+
const runs = [];
|
|
6485
|
+
for (const fusion of fusionValues) {
|
|
6486
|
+
for (const hybridWeight of weightValues) {
|
|
6487
|
+
for (const rrfK of rrfValues) {
|
|
6488
|
+
for (const rerankTopN of rerankValues) {
|
|
6489
|
+
const run = await runEvaluation({
|
|
6490
|
+
...options,
|
|
6491
|
+
searchOverrides: {
|
|
6492
|
+
...fusion !== void 0 ? { fusionStrategy: fusion } : {},
|
|
6493
|
+
...hybridWeight !== void 0 ? { hybridWeight } : {},
|
|
6494
|
+
...rrfK !== void 0 ? { rrfK } : {},
|
|
6495
|
+
...rerankTopN !== void 0 ? { rerankTopN } : {}
|
|
6496
|
+
}
|
|
6497
|
+
});
|
|
6498
|
+
runs.push({
|
|
6499
|
+
searchConfig: run.summary.searchConfig,
|
|
6500
|
+
summary: run.summary,
|
|
6501
|
+
comparison: run.comparison,
|
|
6502
|
+
gate: run.gate
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
const bestByHitAt5 = [...runs].sort(
|
|
6509
|
+
(a, b) => b.summary.metrics.hitAt5 - a.summary.metrics.hitAt5
|
|
6510
|
+
)[0];
|
|
6511
|
+
const bestByMrrAt10 = [...runs].sort(
|
|
6512
|
+
(a, b) => b.summary.metrics.mrrAt10 - a.summary.metrics.mrrAt10
|
|
6513
|
+
)[0];
|
|
6514
|
+
const bestByP95Latency = [...runs].sort(
|
|
6515
|
+
(a, b) => a.summary.metrics.latencyMs.p95 - b.summary.metrics.latencyMs.p95
|
|
6516
|
+
)[0];
|
|
6517
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6518
|
+
const failedGateRuns = runs.filter((run) => run.gate && !run.gate.passed).length;
|
|
6519
|
+
const gatePassed = failedGateRuns === 0;
|
|
6520
|
+
const aggregate = {
|
|
6521
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6522
|
+
againstPath: options.againstPath,
|
|
6523
|
+
runCount: runs.length,
|
|
6524
|
+
runs,
|
|
6525
|
+
gatePassed,
|
|
6526
|
+
failedGateRuns,
|
|
6527
|
+
bestByHitAt5,
|
|
6528
|
+
bestByMrrAt10,
|
|
6529
|
+
bestByP95Latency
|
|
6530
|
+
};
|
|
6531
|
+
writeJson(path7.join(outputDir, "compare.json"), aggregate);
|
|
6532
|
+
const md = createSummaryMarkdown(
|
|
6533
|
+
bestByHitAt5?.summary ?? runs[0].summary,
|
|
6534
|
+
bestByHitAt5?.comparison,
|
|
6535
|
+
void 0,
|
|
6536
|
+
aggregate
|
|
6537
|
+
);
|
|
6538
|
+
writeText(path7.join(outputDir, "summary.md"), md);
|
|
6539
|
+
writeJson(path7.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
|
|
6540
|
+
return { outputDir, aggregate };
|
|
6541
|
+
}
|
|
6542
|
+
|
|
6543
|
+
// src/eval/cli.ts
|
|
6544
|
+
function printUsage() {
|
|
6545
|
+
console.log(`
|
|
6546
|
+
Usage:
|
|
6547
|
+
opencode-codebase-index-mcp eval run [options]
|
|
6548
|
+
opencode-codebase-index-mcp eval compare --against <summary.json> [options]
|
|
6549
|
+
opencode-codebase-index-mcp eval diff --current <summary.json> --against <summary.json> [options]
|
|
6550
|
+
|
|
6551
|
+
Options:
|
|
6552
|
+
--project <path> Project root (default: cwd)
|
|
6553
|
+
--config <path> Config JSON path
|
|
6554
|
+
--dataset <path> Golden dataset path (default: benchmarks/golden/small.json)
|
|
6555
|
+
--current <path> Current summary.json path (required for eval diff)
|
|
6556
|
+
--output <path> Output root dir (default: benchmarks/results)
|
|
6557
|
+
--against <path> Baseline summary.json to compare against
|
|
6558
|
+
--budget <path> Budget file for CI mode (default: benchmarks/budgets/default.json)
|
|
6559
|
+
--ci Enable CI gate mode
|
|
6560
|
+
--reindex Force reindex before eval
|
|
6561
|
+
|
|
6562
|
+
Search overrides:
|
|
6563
|
+
--fusionStrategy <rrf|weighted>
|
|
6564
|
+
--hybridWeight <0-1>
|
|
6565
|
+
--rrfK <number>
|
|
6566
|
+
--rerankTopN <number>
|
|
6567
|
+
|
|
6568
|
+
Sweep options (comma-separated values):
|
|
6569
|
+
--sweepFusionStrategy <rrf,weighted>
|
|
6570
|
+
--sweepHybridWeight <0.3,0.5,0.7>
|
|
6571
|
+
--sweepRrfK <30,60,90>
|
|
6572
|
+
--sweepRerankTopN <10,20,40>
|
|
6573
|
+
`);
|
|
6574
|
+
}
|
|
6575
|
+
function parseNumber(value, flag) {
|
|
6576
|
+
const parsed = Number(value);
|
|
6577
|
+
if (Number.isNaN(parsed)) {
|
|
6578
|
+
throw new Error(`${flag} must be a number`);
|
|
6579
|
+
}
|
|
6580
|
+
return parsed;
|
|
6581
|
+
}
|
|
6582
|
+
function parseCsvNumbers(value, flag) {
|
|
6583
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0).map((item) => parseNumber(item, flag));
|
|
6584
|
+
}
|
|
6585
|
+
function parseCsvFusion(value) {
|
|
6586
|
+
const values = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
6587
|
+
const parsed = [];
|
|
6588
|
+
for (const candidate of values) {
|
|
6589
|
+
if (candidate !== "rrf" && candidate !== "weighted") {
|
|
6590
|
+
throw new Error("--sweepFusionStrategy accepts only rrf,weighted");
|
|
6591
|
+
}
|
|
6592
|
+
parsed.push(candidate);
|
|
6593
|
+
}
|
|
6594
|
+
return parsed;
|
|
6595
|
+
}
|
|
6596
|
+
function hasSweepOptions(sweep) {
|
|
6597
|
+
return Boolean(
|
|
6598
|
+
sweep.fusionStrategy && sweep.fusionStrategy.length > 0 || sweep.hybridWeight && sweep.hybridWeight.length > 0 || sweep.rrfK && sweep.rrfK.length > 0 || sweep.rerankTopN && sweep.rerankTopN.length > 0
|
|
6599
|
+
);
|
|
6600
|
+
}
|
|
6601
|
+
function parseEvalArgs(argv, cwd) {
|
|
6602
|
+
const parsed = {
|
|
6603
|
+
projectRoot: cwd,
|
|
6604
|
+
datasetPath: "benchmarks/golden/small.json",
|
|
6605
|
+
outputRoot: "benchmarks/results",
|
|
6606
|
+
budgetPath: "benchmarks/budgets/default.json",
|
|
6607
|
+
ciMode: false,
|
|
6608
|
+
reindex: false,
|
|
6609
|
+
sweep: {}
|
|
6610
|
+
};
|
|
6611
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6612
|
+
const arg = argv[i];
|
|
6613
|
+
const next = argv[i + 1];
|
|
6614
|
+
if (arg === "--project" && next) {
|
|
6615
|
+
parsed.projectRoot = path8.resolve(cwd, next);
|
|
6616
|
+
i += 1;
|
|
6617
|
+
continue;
|
|
6618
|
+
}
|
|
6619
|
+
if (arg === "--config" && next) {
|
|
6620
|
+
parsed.configPath = path8.resolve(cwd, next);
|
|
6621
|
+
i += 1;
|
|
6622
|
+
continue;
|
|
6623
|
+
}
|
|
6624
|
+
if (arg === "--dataset" && next) {
|
|
6625
|
+
parsed.datasetPath = next;
|
|
6626
|
+
i += 1;
|
|
6627
|
+
continue;
|
|
6628
|
+
}
|
|
6629
|
+
if (arg === "--current" && next) {
|
|
6630
|
+
parsed.currentPath = next;
|
|
6631
|
+
i += 1;
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6634
|
+
if (arg === "--output" && next) {
|
|
6635
|
+
parsed.outputRoot = next;
|
|
6636
|
+
i += 1;
|
|
6637
|
+
continue;
|
|
6638
|
+
}
|
|
6639
|
+
if (arg === "--against" && next) {
|
|
6640
|
+
parsed.againstPath = next;
|
|
6641
|
+
i += 1;
|
|
6642
|
+
continue;
|
|
6643
|
+
}
|
|
6644
|
+
if (arg === "--budget" && next) {
|
|
6645
|
+
parsed.budgetPath = next;
|
|
6646
|
+
i += 1;
|
|
6647
|
+
continue;
|
|
6648
|
+
}
|
|
6649
|
+
if (arg === "--ci") {
|
|
6650
|
+
parsed.ciMode = true;
|
|
6651
|
+
continue;
|
|
6652
|
+
}
|
|
6653
|
+
if (arg === "--reindex") {
|
|
6654
|
+
parsed.reindex = true;
|
|
6655
|
+
continue;
|
|
6656
|
+
}
|
|
6657
|
+
if (arg === "--fusionStrategy" && next) {
|
|
6658
|
+
if (next !== "rrf" && next !== "weighted") {
|
|
6659
|
+
throw new Error("--fusionStrategy must be rrf or weighted");
|
|
6660
|
+
}
|
|
6661
|
+
parsed.fusionStrategy = next;
|
|
6662
|
+
i += 1;
|
|
6663
|
+
continue;
|
|
6664
|
+
}
|
|
6665
|
+
if (arg === "--hybridWeight" && next) {
|
|
6666
|
+
parsed.hybridWeight = parseNumber(next, "--hybridWeight");
|
|
6667
|
+
i += 1;
|
|
6668
|
+
continue;
|
|
6669
|
+
}
|
|
6670
|
+
if (arg === "--rrfK" && next) {
|
|
6671
|
+
parsed.rrfK = parseNumber(next, "--rrfK");
|
|
6672
|
+
i += 1;
|
|
6673
|
+
continue;
|
|
6674
|
+
}
|
|
6675
|
+
if (arg === "--rerankTopN" && next) {
|
|
6676
|
+
parsed.rerankTopN = parseNumber(next, "--rerankTopN");
|
|
6677
|
+
i += 1;
|
|
6678
|
+
continue;
|
|
6679
|
+
}
|
|
6680
|
+
if (arg === "--sweepFusionStrategy" && next) {
|
|
6681
|
+
parsed.sweep.fusionStrategy = parseCsvFusion(next);
|
|
6682
|
+
i += 1;
|
|
6683
|
+
continue;
|
|
6684
|
+
}
|
|
6685
|
+
if (arg === "--sweepHybridWeight" && next) {
|
|
6686
|
+
parsed.sweep.hybridWeight = parseCsvNumbers(next, "--sweepHybridWeight");
|
|
6687
|
+
i += 1;
|
|
6688
|
+
continue;
|
|
6689
|
+
}
|
|
6690
|
+
if (arg === "--sweepRrfK" && next) {
|
|
6691
|
+
parsed.sweep.rrfK = parseCsvNumbers(next, "--sweepRrfK");
|
|
6692
|
+
i += 1;
|
|
6693
|
+
continue;
|
|
6694
|
+
}
|
|
6695
|
+
if (arg === "--sweepRerankTopN" && next) {
|
|
6696
|
+
parsed.sweep.rerankTopN = parseCsvNumbers(next, "--sweepRerankTopN");
|
|
6697
|
+
i += 1;
|
|
6698
|
+
continue;
|
|
6699
|
+
}
|
|
6700
|
+
}
|
|
6701
|
+
return parsed;
|
|
6702
|
+
}
|
|
6703
|
+
function parseEvalSubcommandOptions(argv, cwd) {
|
|
6704
|
+
let explicitAgainst;
|
|
6705
|
+
const filtered = [];
|
|
6706
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6707
|
+
const current = argv[i];
|
|
6708
|
+
const next = argv[i + 1];
|
|
6709
|
+
if (current === "--against" && next) {
|
|
6710
|
+
explicitAgainst = next;
|
|
6711
|
+
i += 1;
|
|
6712
|
+
continue;
|
|
6713
|
+
}
|
|
6714
|
+
filtered.push(current);
|
|
6715
|
+
}
|
|
6716
|
+
return {
|
|
6717
|
+
parsed: parseEvalArgs(filtered, cwd),
|
|
6718
|
+
explicitAgainst
|
|
6719
|
+
};
|
|
6720
|
+
}
|
|
6721
|
+
function toRunOptions(parsed) {
|
|
6722
|
+
return {
|
|
6723
|
+
projectRoot: parsed.projectRoot,
|
|
6724
|
+
configPath: parsed.configPath,
|
|
6725
|
+
datasetPath: parsed.datasetPath,
|
|
6726
|
+
outputRoot: parsed.outputRoot,
|
|
6727
|
+
againstPath: parsed.againstPath,
|
|
6728
|
+
budgetPath: parsed.budgetPath,
|
|
6729
|
+
ciMode: parsed.ciMode,
|
|
6730
|
+
reindex: parsed.reindex,
|
|
6731
|
+
searchOverrides: {
|
|
6732
|
+
...parsed.fusionStrategy !== void 0 ? { fusionStrategy: parsed.fusionStrategy } : {},
|
|
6733
|
+
...parsed.hybridWeight !== void 0 ? { hybridWeight: parsed.hybridWeight } : {},
|
|
6734
|
+
...parsed.rrfK !== void 0 ? { rrfK: parsed.rrfK } : {},
|
|
6735
|
+
...parsed.rerankTopN !== void 0 ? { rerankTopN: parsed.rerankTopN } : {}
|
|
6736
|
+
}
|
|
6737
|
+
};
|
|
6738
|
+
}
|
|
6739
|
+
async function handleEvalCommand(args, cwd) {
|
|
6740
|
+
const subcommand = args[0];
|
|
6741
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
6742
|
+
printUsage();
|
|
6743
|
+
return 0;
|
|
6744
|
+
}
|
|
6745
|
+
if (subcommand === "run") {
|
|
6746
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6747
|
+
if (explicitAgainst) {
|
|
6748
|
+
parsed.againstPath = explicitAgainst;
|
|
6749
|
+
}
|
|
6750
|
+
const runOptions = toRunOptions(parsed);
|
|
6751
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6752
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6753
|
+
console.log(`Eval sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6754
|
+
console.log(`Sweep runs: ${sweep.aggregate.runCount}`);
|
|
6755
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6756
|
+
console.error(
|
|
6757
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6758
|
+
);
|
|
6759
|
+
return 1;
|
|
6760
|
+
}
|
|
6761
|
+
return 0;
|
|
6762
|
+
}
|
|
6763
|
+
const result = await runEvaluation(runOptions);
|
|
6764
|
+
console.log(`Eval run complete. Artifacts: ${result.outputDir}`);
|
|
6765
|
+
console.log(
|
|
6766
|
+
`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`
|
|
6767
|
+
);
|
|
6768
|
+
if (result.gate && !result.gate.passed) {
|
|
6769
|
+
for (const violation of result.gate.violations) {
|
|
6770
|
+
console.error(`[CI-GATE] ${violation.metric}: ${violation.message}`);
|
|
6771
|
+
}
|
|
6772
|
+
return 1;
|
|
6773
|
+
}
|
|
6774
|
+
return 0;
|
|
6775
|
+
}
|
|
6776
|
+
if (subcommand === "compare") {
|
|
6777
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6778
|
+
if (!explicitAgainst) {
|
|
6779
|
+
throw new Error("eval compare requires --against <baseline summary.json>");
|
|
6780
|
+
}
|
|
6781
|
+
parsed.againstPath = explicitAgainst;
|
|
6782
|
+
const runOptions = toRunOptions(parsed);
|
|
6783
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6784
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6785
|
+
console.log(`Eval compare sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6786
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6787
|
+
console.error(
|
|
6788
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6789
|
+
);
|
|
6790
|
+
return 1;
|
|
6791
|
+
}
|
|
6792
|
+
return 0;
|
|
6793
|
+
}
|
|
6794
|
+
const result = await runEvaluation(runOptions);
|
|
6795
|
+
console.log(`Eval compare complete. Artifacts: ${result.outputDir}`);
|
|
6796
|
+
return 0;
|
|
6797
|
+
}
|
|
6798
|
+
if (subcommand === "diff") {
|
|
6799
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6800
|
+
if (!explicitAgainst) {
|
|
6801
|
+
throw new Error("eval diff requires --against <baseline summary.json>");
|
|
6802
|
+
}
|
|
6803
|
+
if (!parsed.currentPath) {
|
|
6804
|
+
throw new Error("eval diff requires --current <current summary.json>");
|
|
6805
|
+
}
|
|
6806
|
+
parsed.againstPath = explicitAgainst;
|
|
6807
|
+
const currentPath = parsed.currentPath;
|
|
6808
|
+
if (!currentPath.endsWith(".json")) {
|
|
6809
|
+
throw new Error("eval diff --current must point to a summary JSON file");
|
|
6810
|
+
}
|
|
6811
|
+
if (!parsed.againstPath.endsWith(".json")) {
|
|
6812
|
+
throw new Error("eval diff --against must point to a summary JSON file");
|
|
6813
|
+
}
|
|
6814
|
+
const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
|
|
6815
|
+
const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
|
|
6816
|
+
const comparison = compareSummaries(
|
|
6817
|
+
currentSummary,
|
|
6818
|
+
baselineSummary,
|
|
6819
|
+
path8.resolve(parsed.projectRoot, parsed.againstPath)
|
|
6820
|
+
);
|
|
6821
|
+
const outputDir = createRunDirectory(path8.resolve(parsed.projectRoot, parsed.outputRoot));
|
|
6822
|
+
const summaryMd = createSummaryMarkdown(currentSummary, comparison);
|
|
6823
|
+
writeJson(path8.join(outputDir, "compare.json"), comparison);
|
|
6824
|
+
writeText(path8.join(outputDir, "summary.md"), summaryMd);
|
|
6825
|
+
writeJson(path8.join(outputDir, "summary.json"), currentSummary);
|
|
6826
|
+
console.log(`Eval diff complete. Artifacts: ${outputDir}`);
|
|
6827
|
+
return 0;
|
|
6828
|
+
}
|
|
6829
|
+
throw new Error(`Unknown eval subcommand: ${subcommand}`);
|
|
6830
|
+
}
|
|
6831
|
+
|
|
5743
6832
|
// src/mcp-server.ts
|
|
6833
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6834
|
+
import { z } from "zod";
|
|
6835
|
+
|
|
6836
|
+
// src/tools/utils.ts
|
|
5744
6837
|
var MAX_CONTENT_LINES = 30;
|
|
5745
6838
|
function truncateContent(content) {
|
|
5746
6839
|
const lines = content.split("\n");
|
|
@@ -5748,6 +6841,31 @@ function truncateContent(content) {
|
|
|
5748
6841
|
return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
|
|
5749
6842
|
// ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
|
|
5750
6843
|
}
|
|
6844
|
+
function formatDefinitionLookup(results, query) {
|
|
6845
|
+
if (results.length === 0) {
|
|
6846
|
+
return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
|
|
6847
|
+
}
|
|
6848
|
+
const formatted = results.map((r, idx) => {
|
|
6849
|
+
const header2 = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
|
|
6850
|
+
return `${header2} (score: ${r.score.toFixed(2)})
|
|
6851
|
+
\`\`\`
|
|
6852
|
+
${truncateContent(r.content)}
|
|
6853
|
+
\`\`\``;
|
|
6854
|
+
});
|
|
6855
|
+
const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
|
|
6856
|
+
return `${header}
|
|
6857
|
+
|
|
6858
|
+
${formatted.join("\n\n")}`;
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6861
|
+
// src/mcp-server.ts
|
|
6862
|
+
var MAX_CONTENT_LINES2 = 30;
|
|
6863
|
+
function truncateContent2(content) {
|
|
6864
|
+
const lines = content.split("\n");
|
|
6865
|
+
if (lines.length <= MAX_CONTENT_LINES2) return content;
|
|
6866
|
+
return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
|
|
6867
|
+
// ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
|
|
6868
|
+
}
|
|
5751
6869
|
function formatIndexStats(stats, verbose = false) {
|
|
5752
6870
|
const lines = [];
|
|
5753
6871
|
if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
|
|
@@ -5861,7 +6979,7 @@ function createMcpServer(projectRoot, config) {
|
|
|
5861
6979
|
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}`;
|
|
5862
6980
|
return `${header} (score: ${r.score.toFixed(2)})
|
|
5863
6981
|
\`\`\`
|
|
5864
|
-
${
|
|
6982
|
+
${truncateContent2(r.content)}
|
|
5865
6983
|
\`\`\``;
|
|
5866
6984
|
});
|
|
5867
6985
|
return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
|
|
@@ -6039,7 +7157,7 @@ Use Read tool to examine specific files.` }] };
|
|
|
6039
7157
|
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}`;
|
|
6040
7158
|
return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
|
|
6041
7159
|
\`\`\`
|
|
6042
|
-
${
|
|
7160
|
+
${truncateContent2(r.content)}
|
|
6043
7161
|
\`\`\``;
|
|
6044
7162
|
});
|
|
6045
7163
|
return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
|
|
@@ -6047,6 +7165,25 @@ ${truncateContent(r.content)}
|
|
|
6047
7165
|
${formatted.join("\n\n")}` }] };
|
|
6048
7166
|
}
|
|
6049
7167
|
);
|
|
7168
|
+
server.tool(
|
|
7169
|
+
"implementation_lookup",
|
|
7170
|
+
"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.",
|
|
7171
|
+
{
|
|
7172
|
+
query: z.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
|
|
7173
|
+
limit: z.number().optional().default(5).describe("Maximum number of results"),
|
|
7174
|
+
fileType: z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
|
|
7175
|
+
directory: z.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
|
|
7176
|
+
},
|
|
7177
|
+
async (args) => {
|
|
7178
|
+
await ensureInitialized();
|
|
7179
|
+
const results = await indexer.search(args.query, args.limit ?? 5, {
|
|
7180
|
+
fileType: args.fileType,
|
|
7181
|
+
directory: args.directory,
|
|
7182
|
+
definitionIntent: true
|
|
7183
|
+
});
|
|
7184
|
+
return { content: [{ type: "text", text: formatDefinitionLookup(results, args.query) }] };
|
|
7185
|
+
}
|
|
7186
|
+
);
|
|
6050
7187
|
server.tool(
|
|
6051
7188
|
"call_graph",
|
|
6052
7189
|
"Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
|
|
@@ -6153,14 +7290,30 @@ Use a hybrid approach:
|
|
|
6153
7290
|
}]
|
|
6154
7291
|
})
|
|
6155
7292
|
);
|
|
7293
|
+
server.prompt(
|
|
7294
|
+
"definition",
|
|
7295
|
+
"Find where a symbol is defined in the codebase",
|
|
7296
|
+
{ query: z.string().describe("Symbol name or description to find the definition of") },
|
|
7297
|
+
(args) => ({
|
|
7298
|
+
messages: [{
|
|
7299
|
+
role: "user",
|
|
7300
|
+
content: {
|
|
7301
|
+
type: "text",
|
|
7302
|
+
text: `Find the definition of: "${args.query}"
|
|
7303
|
+
|
|
7304
|
+
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.`
|
|
7305
|
+
}
|
|
7306
|
+
}]
|
|
7307
|
+
})
|
|
7308
|
+
);
|
|
6156
7309
|
return server;
|
|
6157
7310
|
}
|
|
6158
7311
|
|
|
6159
7312
|
// src/cli.ts
|
|
6160
7313
|
function loadJsonFile(filePath) {
|
|
6161
7314
|
try {
|
|
6162
|
-
if (
|
|
6163
|
-
const content =
|
|
7315
|
+
if (existsSync6(filePath)) {
|
|
7316
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
6164
7317
|
return JSON.parse(content);
|
|
6165
7318
|
}
|
|
6166
7319
|
} catch {
|
|
@@ -6172,9 +7325,9 @@ function loadPluginConfig(projectRoot, configPath) {
|
|
|
6172
7325
|
const config = loadJsonFile(configPath);
|
|
6173
7326
|
if (config) return config;
|
|
6174
7327
|
}
|
|
6175
|
-
const projectConfig = loadJsonFile(
|
|
7328
|
+
const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
|
|
6176
7329
|
if (projectConfig) return projectConfig;
|
|
6177
|
-
const globalConfig = loadJsonFile(
|
|
7330
|
+
const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
|
|
6178
7331
|
if (globalConfig) return globalConfig;
|
|
6179
7332
|
return {};
|
|
6180
7333
|
}
|
|
@@ -6183,14 +7336,18 @@ function parseArgs(argv) {
|
|
|
6183
7336
|
let config;
|
|
6184
7337
|
for (let i = 2; i < argv.length; i++) {
|
|
6185
7338
|
if (argv[i] === "--project" && argv[i + 1]) {
|
|
6186
|
-
project =
|
|
7339
|
+
project = path9.resolve(argv[++i]);
|
|
6187
7340
|
} else if (argv[i] === "--config" && argv[i + 1]) {
|
|
6188
|
-
config =
|
|
7341
|
+
config = path9.resolve(argv[++i]);
|
|
6189
7342
|
}
|
|
6190
7343
|
}
|
|
6191
7344
|
return { project, config };
|
|
6192
7345
|
}
|
|
6193
7346
|
async function main() {
|
|
7347
|
+
if (process.argv[2] === "eval") {
|
|
7348
|
+
const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
|
|
7349
|
+
process.exit(exitCode);
|
|
7350
|
+
}
|
|
6194
7351
|
const args = parseArgs(process.argv);
|
|
6195
7352
|
const rawConfig = loadPluginConfig(args.project, args.config);
|
|
6196
7353
|
const config = parseConfig(rawConfig);
|