opencode-codebase-index 0.5.1 → 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 +226 -107
- package/commands/call-graph.md +24 -0
- package/commands/definition.md +24 -0
- package/dist/cli.cjs +2624 -269
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2626 -277
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1324 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1325 -81
- 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 +9 -3
- package/skill/SKILL.md +13 -1
package/dist/cli.cjs
CHANGED
|
@@ -29,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
|
|
30
30
|
// node_modules/eventemitter3/index.js
|
|
31
31
|
var require_eventemitter3 = __commonJS({
|
|
32
|
-
"node_modules/eventemitter3/index.js"(exports2,
|
|
32
|
+
"node_modules/eventemitter3/index.js"(exports2, module3) {
|
|
33
33
|
"use strict";
|
|
34
34
|
var has = Object.prototype.hasOwnProperty;
|
|
35
35
|
var prefix = "~";
|
|
@@ -183,15 +183,15 @@ var require_eventemitter3 = __commonJS({
|
|
|
183
183
|
EventEmitter2.prototype.addListener = EventEmitter2.prototype.on;
|
|
184
184
|
EventEmitter2.prefixed = prefix;
|
|
185
185
|
EventEmitter2.EventEmitter = EventEmitter2;
|
|
186
|
-
if ("undefined" !== typeof
|
|
187
|
-
|
|
186
|
+
if ("undefined" !== typeof module3) {
|
|
187
|
+
module3.exports = EventEmitter2;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// node_modules/ignore/index.js
|
|
193
193
|
var require_ignore = __commonJS({
|
|
194
|
-
"node_modules/ignore/index.js"(exports2,
|
|
194
|
+
"node_modules/ignore/index.js"(exports2, module3) {
|
|
195
195
|
"use strict";
|
|
196
196
|
function makeArray(subject) {
|
|
197
197
|
return Array.isArray(subject) ? subject : [subject];
|
|
@@ -487,7 +487,7 @@ var require_ignore = __commonJS({
|
|
|
487
487
|
// path matching.
|
|
488
488
|
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
489
489
|
// @returns {TestResult} true if a file is ignored
|
|
490
|
-
test(
|
|
490
|
+
test(path10, checkUnignored, mode) {
|
|
491
491
|
let ignored = false;
|
|
492
492
|
let unignored = false;
|
|
493
493
|
let matchedRule;
|
|
@@ -496,7 +496,7 @@ var require_ignore = __commonJS({
|
|
|
496
496
|
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
497
497
|
return;
|
|
498
498
|
}
|
|
499
|
-
const matched = rule[mode].test(
|
|
499
|
+
const matched = rule[mode].test(path10);
|
|
500
500
|
if (!matched) {
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
@@ -517,17 +517,17 @@ var require_ignore = __commonJS({
|
|
|
517
517
|
var throwError = (message, Ctor) => {
|
|
518
518
|
throw new Ctor(message);
|
|
519
519
|
};
|
|
520
|
-
var checkPath = (
|
|
521
|
-
if (!isString(
|
|
520
|
+
var checkPath = (path10, originalPath, doThrow) => {
|
|
521
|
+
if (!isString(path10)) {
|
|
522
522
|
return doThrow(
|
|
523
523
|
`path must be a string, but got \`${originalPath}\``,
|
|
524
524
|
TypeError
|
|
525
525
|
);
|
|
526
526
|
}
|
|
527
|
-
if (!
|
|
527
|
+
if (!path10) {
|
|
528
528
|
return doThrow(`path must not be empty`, TypeError);
|
|
529
529
|
}
|
|
530
|
-
if (checkPath.isNotRelative(
|
|
530
|
+
if (checkPath.isNotRelative(path10)) {
|
|
531
531
|
const r = "`path.relative()`d";
|
|
532
532
|
return doThrow(
|
|
533
533
|
`path should be a ${r} string, but got "${originalPath}"`,
|
|
@@ -536,7 +536,7 @@ var require_ignore = __commonJS({
|
|
|
536
536
|
}
|
|
537
537
|
return true;
|
|
538
538
|
};
|
|
539
|
-
var isNotRelative = (
|
|
539
|
+
var isNotRelative = (path10) => REGEX_TEST_INVALID_PATH.test(path10);
|
|
540
540
|
checkPath.isNotRelative = isNotRelative;
|
|
541
541
|
checkPath.convert = (p) => p;
|
|
542
542
|
var Ignore2 = class {
|
|
@@ -566,19 +566,19 @@ var require_ignore = __commonJS({
|
|
|
566
566
|
}
|
|
567
567
|
// @returns {TestResult}
|
|
568
568
|
_test(originalPath, cache, checkUnignored, slices) {
|
|
569
|
-
const
|
|
569
|
+
const path10 = originalPath && checkPath.convert(originalPath);
|
|
570
570
|
checkPath(
|
|
571
|
-
|
|
571
|
+
path10,
|
|
572
572
|
originalPath,
|
|
573
573
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
574
574
|
);
|
|
575
|
-
return this._t(
|
|
575
|
+
return this._t(path10, cache, checkUnignored, slices);
|
|
576
576
|
}
|
|
577
|
-
checkIgnore(
|
|
578
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
579
|
-
return this.test(
|
|
577
|
+
checkIgnore(path10) {
|
|
578
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
|
|
579
|
+
return this.test(path10);
|
|
580
580
|
}
|
|
581
|
-
const slices =
|
|
581
|
+
const slices = path10.split(SLASH).filter(Boolean);
|
|
582
582
|
slices.pop();
|
|
583
583
|
if (slices.length) {
|
|
584
584
|
const parent = this._t(
|
|
@@ -591,18 +591,18 @@ var require_ignore = __commonJS({
|
|
|
591
591
|
return parent;
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
|
-
return this._rules.test(
|
|
594
|
+
return this._rules.test(path10, false, MODE_CHECK_IGNORE);
|
|
595
595
|
}
|
|
596
|
-
_t(
|
|
597
|
-
if (
|
|
598
|
-
return cache[
|
|
596
|
+
_t(path10, cache, checkUnignored, slices) {
|
|
597
|
+
if (path10 in cache) {
|
|
598
|
+
return cache[path10];
|
|
599
599
|
}
|
|
600
600
|
if (!slices) {
|
|
601
|
-
slices =
|
|
601
|
+
slices = path10.split(SLASH).filter(Boolean);
|
|
602
602
|
}
|
|
603
603
|
slices.pop();
|
|
604
604
|
if (!slices.length) {
|
|
605
|
-
return cache[
|
|
605
|
+
return cache[path10] = this._rules.test(path10, checkUnignored, MODE_IGNORE);
|
|
606
606
|
}
|
|
607
607
|
const parent = this._t(
|
|
608
608
|
slices.join(SLASH) + SLASH,
|
|
@@ -610,29 +610,29 @@ var require_ignore = __commonJS({
|
|
|
610
610
|
checkUnignored,
|
|
611
611
|
slices
|
|
612
612
|
);
|
|
613
|
-
return cache[
|
|
613
|
+
return cache[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
|
|
614
614
|
}
|
|
615
|
-
ignores(
|
|
616
|
-
return this._test(
|
|
615
|
+
ignores(path10) {
|
|
616
|
+
return this._test(path10, this._ignoreCache, false).ignored;
|
|
617
617
|
}
|
|
618
618
|
createFilter() {
|
|
619
|
-
return (
|
|
619
|
+
return (path10) => !this.ignores(path10);
|
|
620
620
|
}
|
|
621
621
|
filter(paths) {
|
|
622
622
|
return makeArray(paths).filter(this.createFilter());
|
|
623
623
|
}
|
|
624
624
|
// @returns {TestResult}
|
|
625
|
-
test(
|
|
626
|
-
return this._test(
|
|
625
|
+
test(path10) {
|
|
626
|
+
return this._test(path10, this._testCache, true);
|
|
627
627
|
}
|
|
628
628
|
};
|
|
629
629
|
var factory = (options) => new Ignore2(options);
|
|
630
|
-
var isPathValid = (
|
|
630
|
+
var isPathValid = (path10) => checkPath(path10 && checkPath.convert(path10), path10, RETURN_FALSE);
|
|
631
631
|
var setupWindows = () => {
|
|
632
632
|
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
633
633
|
checkPath.convert = makePosix;
|
|
634
634
|
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
635
|
-
checkPath.isNotRelative = (
|
|
635
|
+
checkPath.isNotRelative = (path10) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path10) || isNotRelative(path10);
|
|
636
636
|
};
|
|
637
637
|
if (
|
|
638
638
|
// Detect `process` so that it can run in browsers.
|
|
@@ -640,18 +640,18 @@ var require_ignore = __commonJS({
|
|
|
640
640
|
) {
|
|
641
641
|
setupWindows();
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
module3.exports = factory;
|
|
644
644
|
factory.default = factory;
|
|
645
|
-
|
|
646
|
-
define(
|
|
645
|
+
module3.exports.isPathValid = isPathValid;
|
|
646
|
+
define(module3.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
|
|
647
647
|
}
|
|
648
648
|
});
|
|
649
649
|
|
|
650
650
|
// src/cli.ts
|
|
651
651
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
652
|
-
var
|
|
653
|
-
var
|
|
654
|
-
var
|
|
652
|
+
var import_fs10 = require("fs");
|
|
653
|
+
var path9 = __toESM(require("path"), 1);
|
|
654
|
+
var os4 = __toESM(require("os"), 1);
|
|
655
655
|
|
|
656
656
|
// src/config/constants.ts
|
|
657
657
|
var DEFAULT_INCLUDE = [
|
|
@@ -659,7 +659,7 @@ var DEFAULT_INCLUDE = [
|
|
|
659
659
|
"**/*.{py,pyi}",
|
|
660
660
|
"**/*.{go,rs,java,kt,scala}",
|
|
661
661
|
"**/*.{c,cpp,cc,h,hpp}",
|
|
662
|
-
"**/*.{rb,php,swift}",
|
|
662
|
+
"**/*.{rb,php,inc,swift}",
|
|
663
663
|
"**/*.{vue,svelte,astro}",
|
|
664
664
|
"**/*.{sql,graphql,proto}",
|
|
665
665
|
"**/*.{yaml,yml,toml}",
|
|
@@ -776,9 +776,15 @@ function getDefaultSearchConfig() {
|
|
|
776
776
|
minScore: 0.1,
|
|
777
777
|
includeContext: true,
|
|
778
778
|
hybridWeight: 0.5,
|
|
779
|
+
fusionStrategy: "rrf",
|
|
780
|
+
rrfK: 60,
|
|
781
|
+
rerankTopN: 20,
|
|
779
782
|
contextLines: 0
|
|
780
783
|
};
|
|
781
784
|
}
|
|
785
|
+
function isValidFusionStrategy(value) {
|
|
786
|
+
return value === "weighted" || value === "rrf";
|
|
787
|
+
}
|
|
782
788
|
function getDefaultDebugConfig() {
|
|
783
789
|
return {
|
|
784
790
|
enabled: false,
|
|
@@ -833,6 +839,9 @@ function parseConfig(raw) {
|
|
|
833
839
|
minScore: typeof rawSearch.minScore === "number" ? rawSearch.minScore : defaultSearch.minScore,
|
|
834
840
|
includeContext: typeof rawSearch.includeContext === "boolean" ? rawSearch.includeContext : defaultSearch.includeContext,
|
|
835
841
|
hybridWeight: typeof rawSearch.hybridWeight === "number" ? Math.min(1, Math.max(0, rawSearch.hybridWeight)) : defaultSearch.hybridWeight,
|
|
842
|
+
fusionStrategy: isValidFusionStrategy(rawSearch.fusionStrategy) ? rawSearch.fusionStrategy : defaultSearch.fusionStrategy,
|
|
843
|
+
rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
|
|
844
|
+
rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
|
|
836
845
|
contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines
|
|
837
846
|
};
|
|
838
847
|
const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
|
|
@@ -900,13 +909,197 @@ function getDefaultModelForProvider(provider) {
|
|
|
900
909
|
}
|
|
901
910
|
var availableProviders = Object.keys(EMBEDDING_MODELS);
|
|
902
911
|
|
|
903
|
-
// src/
|
|
904
|
-
var
|
|
905
|
-
|
|
912
|
+
// src/eval/cli.ts
|
|
913
|
+
var path8 = __toESM(require("path"), 1);
|
|
914
|
+
|
|
915
|
+
// src/eval/compare.ts
|
|
916
|
+
function metricDelta(current, baseline) {
|
|
917
|
+
const absolute = current - baseline;
|
|
918
|
+
const relativePct = baseline === 0 ? current === 0 ? 0 : 100 : absolute / baseline * 100;
|
|
919
|
+
return {
|
|
920
|
+
current,
|
|
921
|
+
baseline,
|
|
922
|
+
absolute,
|
|
923
|
+
relativePct
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function compareSummaries(current, baseline, againstPath) {
|
|
927
|
+
return {
|
|
928
|
+
againstPath,
|
|
929
|
+
deltas: {
|
|
930
|
+
hitAt1: metricDelta(current.metrics.hitAt1, baseline.metrics.hitAt1),
|
|
931
|
+
hitAt3: metricDelta(current.metrics.hitAt3, baseline.metrics.hitAt3),
|
|
932
|
+
hitAt5: metricDelta(current.metrics.hitAt5, baseline.metrics.hitAt5),
|
|
933
|
+
hitAt10: metricDelta(current.metrics.hitAt10, baseline.metrics.hitAt10),
|
|
934
|
+
mrrAt10: metricDelta(current.metrics.mrrAt10, baseline.metrics.mrrAt10),
|
|
935
|
+
ndcgAt10: metricDelta(current.metrics.ndcgAt10, baseline.metrics.ndcgAt10),
|
|
936
|
+
latencyP50Ms: metricDelta(current.metrics.latencyMs.p50, baseline.metrics.latencyMs.p50),
|
|
937
|
+
latencyP95Ms: metricDelta(current.metrics.latencyMs.p95, baseline.metrics.latencyMs.p95),
|
|
938
|
+
latencyP99Ms: metricDelta(current.metrics.latencyMs.p99, baseline.metrics.latencyMs.p99),
|
|
939
|
+
embeddingCallCount: metricDelta(
|
|
940
|
+
current.metrics.embedding.callCount,
|
|
941
|
+
baseline.metrics.embedding.callCount
|
|
942
|
+
),
|
|
943
|
+
estimatedCostUsd: metricDelta(
|
|
944
|
+
current.metrics.embedding.estimatedCostUsd,
|
|
945
|
+
baseline.metrics.embedding.estimatedCostUsd
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/eval/reports.ts
|
|
952
|
+
var import_fs = require("fs");
|
|
953
|
+
var path = __toESM(require("path"), 1);
|
|
954
|
+
function formatPct(value) {
|
|
955
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
956
|
+
}
|
|
957
|
+
function formatMs(value) {
|
|
958
|
+
return `${value.toFixed(3)}ms`;
|
|
959
|
+
}
|
|
960
|
+
function formatUsd(value) {
|
|
961
|
+
return `$${value.toFixed(6)}`;
|
|
962
|
+
}
|
|
963
|
+
function signed(value, digits = 4) {
|
|
964
|
+
const formatted = value.toFixed(digits);
|
|
965
|
+
return value > 0 ? `+${formatted}` : formatted;
|
|
966
|
+
}
|
|
967
|
+
function loadSummary(summaryPath) {
|
|
968
|
+
const raw = (0, import_fs.readFileSync)(summaryPath, "utf-8");
|
|
969
|
+
return JSON.parse(raw);
|
|
970
|
+
}
|
|
971
|
+
function createRunDirectory(outputRoot, timestampOverride) {
|
|
972
|
+
const timestamp = (timestampOverride ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
|
|
973
|
+
const dir = path.join(outputRoot, timestamp);
|
|
974
|
+
(0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
975
|
+
return dir;
|
|
976
|
+
}
|
|
977
|
+
function writeJson(filePath, value) {
|
|
978
|
+
(0, import_fs.writeFileSync)(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
979
|
+
}
|
|
980
|
+
function writeText(filePath, value) {
|
|
981
|
+
(0, import_fs.writeFileSync)(filePath, value, "utf-8");
|
|
982
|
+
}
|
|
983
|
+
function createSummaryMarkdown(summary, comparison, gate, sweep) {
|
|
984
|
+
const lines = [];
|
|
985
|
+
lines.push("# Evaluation Summary");
|
|
986
|
+
lines.push("");
|
|
987
|
+
lines.push(`- Generated: ${summary.generatedAt}`);
|
|
988
|
+
lines.push(`- Dataset: ${summary.datasetName} (v${summary.datasetVersion})`);
|
|
989
|
+
lines.push(`- Query count: ${summary.queryCount}`);
|
|
990
|
+
lines.push(
|
|
991
|
+
`- Search config: fusion=${summary.searchConfig.fusionStrategy}, hybridWeight=${summary.searchConfig.hybridWeight}, rrfK=${summary.searchConfig.rrfK}, rerankTopN=${summary.searchConfig.rerankTopN}`
|
|
992
|
+
);
|
|
993
|
+
lines.push("");
|
|
994
|
+
lines.push("## Metrics");
|
|
995
|
+
lines.push("");
|
|
996
|
+
lines.push("| Metric | Value |");
|
|
997
|
+
lines.push("|---|---:|");
|
|
998
|
+
lines.push(`| Hit@1 | ${formatPct(summary.metrics.hitAt1)} |`);
|
|
999
|
+
lines.push(`| Hit@3 | ${formatPct(summary.metrics.hitAt3)} |`);
|
|
1000
|
+
lines.push(`| Hit@5 | ${formatPct(summary.metrics.hitAt5)} |`);
|
|
1001
|
+
lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
|
|
1002
|
+
lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
|
|
1003
|
+
lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
|
|
1004
|
+
lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
|
|
1005
|
+
lines.push(`| Latency p95 | ${formatMs(summary.metrics.latencyMs.p95)} |`);
|
|
1006
|
+
lines.push(`| Latency p99 | ${formatMs(summary.metrics.latencyMs.p99)} |`);
|
|
1007
|
+
lines.push(`| Embedding calls | ${summary.metrics.embedding.callCount} |`);
|
|
1008
|
+
lines.push(`| Embedding tokens | ${summary.metrics.tokenEstimate.embeddingTokensUsed} |`);
|
|
1009
|
+
lines.push(`| Estimated embedding cost | ${formatUsd(summary.metrics.embedding.estimatedCostUsd)} |`);
|
|
1010
|
+
lines.push("");
|
|
1011
|
+
lines.push("## Failure Buckets");
|
|
1012
|
+
lines.push("");
|
|
1013
|
+
lines.push("| Bucket | Count |");
|
|
1014
|
+
lines.push("|---|---:|");
|
|
1015
|
+
lines.push(
|
|
1016
|
+
`| wrong-file | ${summary.metrics.failureBuckets["wrong-file"]} |`
|
|
1017
|
+
);
|
|
1018
|
+
lines.push(
|
|
1019
|
+
`| wrong-symbol | ${summary.metrics.failureBuckets["wrong-symbol"]} |`
|
|
1020
|
+
);
|
|
1021
|
+
lines.push(
|
|
1022
|
+
`| docs/tests outranking source | ${summary.metrics.failureBuckets["docs-tests-outranking-source"]} |`
|
|
1023
|
+
);
|
|
1024
|
+
lines.push(
|
|
1025
|
+
`| no relevant hit in top-k | ${summary.metrics.failureBuckets["no-relevant-hit-top-k"]} |`
|
|
1026
|
+
);
|
|
1027
|
+
lines.push("");
|
|
1028
|
+
if (comparison) {
|
|
1029
|
+
lines.push("## Comparison vs Baseline");
|
|
1030
|
+
lines.push("");
|
|
1031
|
+
lines.push(`- Against: ${comparison.againstPath}`);
|
|
1032
|
+
lines.push("");
|
|
1033
|
+
lines.push("| Metric | Baseline | Current | Delta |");
|
|
1034
|
+
lines.push("|---|---:|---:|---:|");
|
|
1035
|
+
lines.push(
|
|
1036
|
+
`| Hit@5 | ${formatPct(comparison.deltas.hitAt5.baseline)} | ${formatPct(comparison.deltas.hitAt5.current)} | ${signed(comparison.deltas.hitAt5.absolute)} |`
|
|
1037
|
+
);
|
|
1038
|
+
lines.push(
|
|
1039
|
+
`| MRR@10 | ${comparison.deltas.mrrAt10.baseline.toFixed(4)} | ${comparison.deltas.mrrAt10.current.toFixed(4)} | ${signed(comparison.deltas.mrrAt10.absolute)} |`
|
|
1040
|
+
);
|
|
1041
|
+
lines.push(
|
|
1042
|
+
`| nDCG@10 | ${comparison.deltas.ndcgAt10.baseline.toFixed(4)} | ${comparison.deltas.ndcgAt10.current.toFixed(4)} | ${signed(comparison.deltas.ndcgAt10.absolute)} |`
|
|
1043
|
+
);
|
|
1044
|
+
lines.push(
|
|
1045
|
+
`| p95 latency (ms) | ${comparison.deltas.latencyP95Ms.baseline.toFixed(3)} | ${comparison.deltas.latencyP95Ms.current.toFixed(3)} | ${signed(comparison.deltas.latencyP95Ms.absolute, 3)} |`
|
|
1046
|
+
);
|
|
1047
|
+
lines.push("");
|
|
1048
|
+
}
|
|
1049
|
+
if (gate) {
|
|
1050
|
+
lines.push("## CI Gate");
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
lines.push(`- Result: ${gate.passed ? "PASS \u2705" : "FAIL \u274C"}`);
|
|
1053
|
+
if (gate.violations.length > 0) {
|
|
1054
|
+
lines.push("- Violations:");
|
|
1055
|
+
for (const violation of gate.violations) {
|
|
1056
|
+
lines.push(` - ${violation.metric}: ${violation.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
lines.push("");
|
|
1060
|
+
}
|
|
1061
|
+
if (sweep) {
|
|
1062
|
+
lines.push("## Parameter Sweep");
|
|
1063
|
+
lines.push("");
|
|
1064
|
+
lines.push(`- Run count: ${sweep.runCount}`);
|
|
1065
|
+
if (sweep.bestByHitAt5) {
|
|
1066
|
+
lines.push(
|
|
1067
|
+
`- Best Hit@5: ${formatPct(sweep.bestByHitAt5.summary.metrics.hitAt5)} with fusion=${sweep.bestByHitAt5.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByHitAt5.searchConfig.hybridWeight}, rrfK=${sweep.bestByHitAt5.searchConfig.rrfK}, rerankTopN=${sweep.bestByHitAt5.searchConfig.rerankTopN}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (sweep.bestByMrrAt10) {
|
|
1071
|
+
lines.push(
|
|
1072
|
+
`- Best MRR@10: ${sweep.bestByMrrAt10.summary.metrics.mrrAt10.toFixed(4)} with fusion=${sweep.bestByMrrAt10.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByMrrAt10.searchConfig.hybridWeight}, rrfK=${sweep.bestByMrrAt10.searchConfig.rrfK}, rerankTopN=${sweep.bestByMrrAt10.searchConfig.rerankTopN}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (sweep.bestByP95Latency) {
|
|
1076
|
+
lines.push(
|
|
1077
|
+
`- Best p95 latency: ${formatMs(sweep.bestByP95Latency.summary.metrics.latencyMs.p95)} with fusion=${sweep.bestByP95Latency.searchConfig.fusionStrategy}, hybridWeight=${sweep.bestByP95Latency.searchConfig.hybridWeight}, rrfK=${sweep.bestByP95Latency.searchConfig.rrfK}, rerankTopN=${sweep.bestByP95Latency.searchConfig.rerankTopN}`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
lines.push("");
|
|
1081
|
+
}
|
|
1082
|
+
return `${lines.join("\n")}
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
function buildPerQueryArtifact(perQuery) {
|
|
1086
|
+
return {
|
|
1087
|
+
queryCount: perQuery.length,
|
|
1088
|
+
queries: [...perQuery].sort((a, b) => a.id.localeCompare(b.id))
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/eval/runner.ts
|
|
1093
|
+
var import_fs7 = require("fs");
|
|
1094
|
+
var import_fs8 = require("fs");
|
|
1095
|
+
var import_fs9 = require("fs");
|
|
1096
|
+
var os3 = __toESM(require("os"), 1);
|
|
1097
|
+
var path7 = __toESM(require("path"), 1);
|
|
1098
|
+
var import_perf_hooks2 = require("perf_hooks");
|
|
906
1099
|
|
|
907
1100
|
// src/indexer/index.ts
|
|
908
|
-
var
|
|
909
|
-
var
|
|
1101
|
+
var import_fs5 = require("fs");
|
|
1102
|
+
var path6 = __toESM(require("path"), 1);
|
|
910
1103
|
var import_perf_hooks = require("perf_hooks");
|
|
911
1104
|
|
|
912
1105
|
// node_modules/eventemitter3/index.mjs
|
|
@@ -931,7 +1124,7 @@ function pTimeout(promise, options) {
|
|
|
931
1124
|
} = options;
|
|
932
1125
|
let timer;
|
|
933
1126
|
let abortHandler;
|
|
934
|
-
const wrappedPromise = new Promise((
|
|
1127
|
+
const wrappedPromise = new Promise((resolve5, reject) => {
|
|
935
1128
|
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
|
|
936
1129
|
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
|
|
937
1130
|
}
|
|
@@ -945,7 +1138,7 @@ function pTimeout(promise, options) {
|
|
|
945
1138
|
};
|
|
946
1139
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
947
1140
|
}
|
|
948
|
-
promise.then(
|
|
1141
|
+
promise.then(resolve5, reject);
|
|
949
1142
|
if (milliseconds === Number.POSITIVE_INFINITY) {
|
|
950
1143
|
return;
|
|
951
1144
|
}
|
|
@@ -953,7 +1146,7 @@ function pTimeout(promise, options) {
|
|
|
953
1146
|
timer = customTimers.setTimeout.call(void 0, () => {
|
|
954
1147
|
if (fallback) {
|
|
955
1148
|
try {
|
|
956
|
-
|
|
1149
|
+
resolve5(fallback());
|
|
957
1150
|
} catch (error) {
|
|
958
1151
|
reject(error);
|
|
959
1152
|
}
|
|
@@ -963,7 +1156,7 @@ function pTimeout(promise, options) {
|
|
|
963
1156
|
promise.cancel();
|
|
964
1157
|
}
|
|
965
1158
|
if (message === false) {
|
|
966
|
-
|
|
1159
|
+
resolve5();
|
|
967
1160
|
} else if (message instanceof Error) {
|
|
968
1161
|
reject(message);
|
|
969
1162
|
} else {
|
|
@@ -1353,7 +1546,7 @@ var PQueue = class extends import_index.default {
|
|
|
1353
1546
|
// Assign unique ID if not provided
|
|
1354
1547
|
id: options.id ?? (this.#idAssigner++).toString()
|
|
1355
1548
|
};
|
|
1356
|
-
return new Promise((
|
|
1549
|
+
return new Promise((resolve5, reject) => {
|
|
1357
1550
|
const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
|
|
1358
1551
|
this.#queue.enqueue(async () => {
|
|
1359
1552
|
this.#pending++;
|
|
@@ -1391,7 +1584,7 @@ var PQueue = class extends import_index.default {
|
|
|
1391
1584
|
})]);
|
|
1392
1585
|
}
|
|
1393
1586
|
const result = await operation;
|
|
1394
|
-
|
|
1587
|
+
resolve5(result);
|
|
1395
1588
|
this.emit("completed", result);
|
|
1396
1589
|
} catch (error) {
|
|
1397
1590
|
reject(error);
|
|
@@ -1548,13 +1741,13 @@ var PQueue = class extends import_index.default {
|
|
|
1548
1741
|
});
|
|
1549
1742
|
}
|
|
1550
1743
|
async #onEvent(event, filter) {
|
|
1551
|
-
return new Promise((
|
|
1744
|
+
return new Promise((resolve5) => {
|
|
1552
1745
|
const listener = () => {
|
|
1553
1746
|
if (filter && !filter()) {
|
|
1554
1747
|
return;
|
|
1555
1748
|
}
|
|
1556
1749
|
this.off(event, listener);
|
|
1557
|
-
|
|
1750
|
+
resolve5();
|
|
1558
1751
|
};
|
|
1559
1752
|
this.on(event, listener);
|
|
1560
1753
|
});
|
|
@@ -1839,7 +2032,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1839
2032
|
const finalDelay = Math.min(delayTime, remainingTime);
|
|
1840
2033
|
options.signal?.throwIfAborted();
|
|
1841
2034
|
if (finalDelay > 0) {
|
|
1842
|
-
await new Promise((
|
|
2035
|
+
await new Promise((resolve5, reject) => {
|
|
1843
2036
|
const onAbort = () => {
|
|
1844
2037
|
clearTimeout(timeoutToken);
|
|
1845
2038
|
options.signal?.removeEventListener("abort", onAbort);
|
|
@@ -1847,7 +2040,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
|
|
|
1847
2040
|
};
|
|
1848
2041
|
const timeoutToken = setTimeout(() => {
|
|
1849
2042
|
options.signal?.removeEventListener("abort", onAbort);
|
|
1850
|
-
|
|
2043
|
+
resolve5();
|
|
1851
2044
|
}, finalDelay);
|
|
1852
2045
|
if (options.unref) {
|
|
1853
2046
|
timeoutToken.unref?.();
|
|
@@ -1908,17 +2101,17 @@ async function pRetry(input, options = {}) {
|
|
|
1908
2101
|
}
|
|
1909
2102
|
|
|
1910
2103
|
// src/embeddings/detector.ts
|
|
1911
|
-
var
|
|
1912
|
-
var
|
|
2104
|
+
var import_fs2 = require("fs");
|
|
2105
|
+
var path2 = __toESM(require("path"), 1);
|
|
1913
2106
|
var os = __toESM(require("os"), 1);
|
|
1914
2107
|
function getOpenCodeAuthPath() {
|
|
1915
|
-
return
|
|
2108
|
+
return path2.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
1916
2109
|
}
|
|
1917
2110
|
function loadOpenCodeAuth() {
|
|
1918
2111
|
const authPath = getOpenCodeAuthPath();
|
|
1919
2112
|
try {
|
|
1920
|
-
if ((0,
|
|
1921
|
-
return JSON.parse((0,
|
|
2113
|
+
if ((0, import_fs2.existsSync)(authPath)) {
|
|
2114
|
+
return JSON.parse((0, import_fs2.readFileSync)(authPath, "utf-8"));
|
|
1922
2115
|
}
|
|
1923
2116
|
} catch {
|
|
1924
2117
|
}
|
|
@@ -2420,8 +2613,8 @@ var CustomEmbeddingProvider = class {
|
|
|
2420
2613
|
|
|
2421
2614
|
// src/utils/files.ts
|
|
2422
2615
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
2423
|
-
var
|
|
2424
|
-
var
|
|
2616
|
+
var import_fs3 = require("fs");
|
|
2617
|
+
var path3 = __toESM(require("path"), 1);
|
|
2425
2618
|
function createIgnoreFilter(projectRoot) {
|
|
2426
2619
|
const ig = (0, import_ignore.default)();
|
|
2427
2620
|
const defaultIgnores = [
|
|
@@ -2438,9 +2631,9 @@ function createIgnoreFilter(projectRoot) {
|
|
|
2438
2631
|
".opencode"
|
|
2439
2632
|
];
|
|
2440
2633
|
ig.add(defaultIgnores);
|
|
2441
|
-
const gitignorePath =
|
|
2442
|
-
if ((0,
|
|
2443
|
-
const gitignoreContent = (0,
|
|
2634
|
+
const gitignorePath = path3.join(projectRoot, ".gitignore");
|
|
2635
|
+
if ((0, import_fs3.existsSync)(gitignorePath)) {
|
|
2636
|
+
const gitignoreContent = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
|
|
2444
2637
|
ig.add(gitignoreContent);
|
|
2445
2638
|
}
|
|
2446
2639
|
return ig;
|
|
@@ -2454,10 +2647,10 @@ function matchGlob(filePath, pattern) {
|
|
|
2454
2647
|
return regex.test(filePath);
|
|
2455
2648
|
}
|
|
2456
2649
|
async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
|
|
2457
|
-
const entries = await
|
|
2650
|
+
const entries = await import_fs3.promises.readdir(dir, { withFileTypes: true });
|
|
2458
2651
|
for (const entry of entries) {
|
|
2459
|
-
const fullPath =
|
|
2460
|
-
const relativePath =
|
|
2652
|
+
const fullPath = path3.join(dir, entry.name);
|
|
2653
|
+
const relativePath = path3.relative(projectRoot, fullPath);
|
|
2461
2654
|
if (ignoreFilter.ignores(relativePath)) {
|
|
2462
2655
|
if (entry.isFile()) {
|
|
2463
2656
|
skipped.push({ path: relativePath, reason: "gitignore" });
|
|
@@ -2475,7 +2668,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
|
|
|
2475
2668
|
skipped
|
|
2476
2669
|
);
|
|
2477
2670
|
} else if (entry.isFile()) {
|
|
2478
|
-
const stat = await
|
|
2671
|
+
const stat = await import_fs3.promises.stat(fullPath);
|
|
2479
2672
|
if (stat.size > maxFileSize) {
|
|
2480
2673
|
skipped.push({ path: relativePath, reason: "too_large" });
|
|
2481
2674
|
continue;
|
|
@@ -2518,6 +2711,9 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
|
|
|
2518
2711
|
}
|
|
2519
2712
|
|
|
2520
2713
|
// src/utils/cost.ts
|
|
2714
|
+
function estimateTokens(text) {
|
|
2715
|
+
return Math.ceil(text.length / 4);
|
|
2716
|
+
}
|
|
2521
2717
|
function estimateChunksFromFiles(files) {
|
|
2522
2718
|
let totalChunks = 0;
|
|
2523
2719
|
for (const file of files) {
|
|
@@ -2872,8 +3068,9 @@ function initializeLogger(config) {
|
|
|
2872
3068
|
}
|
|
2873
3069
|
|
|
2874
3070
|
// src/native/index.ts
|
|
2875
|
-
var
|
|
3071
|
+
var path4 = __toESM(require("path"), 1);
|
|
2876
3072
|
var os2 = __toESM(require("os"), 1);
|
|
3073
|
+
var module2 = __toESM(require("module"), 1);
|
|
2877
3074
|
var import_url = require("url");
|
|
2878
3075
|
var import_meta = {};
|
|
2879
3076
|
function getNativeBinding() {
|
|
@@ -2894,17 +3091,22 @@ function getNativeBinding() {
|
|
|
2894
3091
|
throw new Error(`Unsupported platform: ${platform2}-${arch2}`);
|
|
2895
3092
|
}
|
|
2896
3093
|
let currentDir;
|
|
3094
|
+
let requireTarget;
|
|
2897
3095
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
2898
|
-
currentDir =
|
|
3096
|
+
currentDir = path4.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
3097
|
+
requireTarget = import_meta.url;
|
|
2899
3098
|
} else if (typeof __dirname !== "undefined") {
|
|
2900
3099
|
currentDir = __dirname;
|
|
3100
|
+
requireTarget = __filename;
|
|
2901
3101
|
} else {
|
|
2902
3102
|
currentDir = process.cwd();
|
|
3103
|
+
requireTarget = path4.join(currentDir, "index.js");
|
|
2903
3104
|
}
|
|
2904
3105
|
const isDevMode = currentDir.includes("/src/native");
|
|
2905
|
-
const packageRoot = isDevMode ?
|
|
2906
|
-
const nativePath =
|
|
2907
|
-
|
|
3106
|
+
const packageRoot = isDevMode ? path4.resolve(currentDir, "../..") : path4.resolve(currentDir, "..");
|
|
3107
|
+
const nativePath = path4.join(packageRoot, "native", bindingName);
|
|
3108
|
+
const require2 = module2.createRequire(requireTarget);
|
|
3109
|
+
return require2(nativePath);
|
|
2908
3110
|
}
|
|
2909
3111
|
var native = getNativeBinding();
|
|
2910
3112
|
function parseFiles(files) {
|
|
@@ -2931,6 +3133,9 @@ function hashContent(content) {
|
|
|
2931
3133
|
function hashFile(filePath) {
|
|
2932
3134
|
return native.hashFile(filePath);
|
|
2933
3135
|
}
|
|
3136
|
+
function extractCalls(content, language) {
|
|
3137
|
+
return native.extractCalls(content, language);
|
|
3138
|
+
}
|
|
2934
3139
|
var VectorStore = class {
|
|
2935
3140
|
inner;
|
|
2936
3141
|
dimensions;
|
|
@@ -3019,7 +3224,7 @@ var VectorStore = class {
|
|
|
3019
3224
|
var CHARS_PER_TOKEN = 4;
|
|
3020
3225
|
var MAX_BATCH_TOKENS = 7500;
|
|
3021
3226
|
var MAX_SINGLE_CHUNK_TOKENS = 2e3;
|
|
3022
|
-
function
|
|
3227
|
+
function estimateTokens2(text) {
|
|
3023
3228
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3024
3229
|
}
|
|
3025
3230
|
function createEmbeddingText(chunk, filePath) {
|
|
@@ -3084,7 +3289,7 @@ function createDynamicBatches(chunks) {
|
|
|
3084
3289
|
let currentBatch = [];
|
|
3085
3290
|
let currentTokens = 0;
|
|
3086
3291
|
for (const chunk of chunks) {
|
|
3087
|
-
const chunkTokens =
|
|
3292
|
+
const chunkTokens = estimateTokens2(chunk.text);
|
|
3088
3293
|
if (currentBatch.length > 0 && currentTokens + chunkTokens > MAX_BATCH_TOKENS) {
|
|
3089
3294
|
batches.push(currentBatch);
|
|
3090
3295
|
currentBatch = [];
|
|
@@ -3262,6 +3467,12 @@ var Database = class {
|
|
|
3262
3467
|
getChunksByFile(filePath) {
|
|
3263
3468
|
return this.inner.getChunksByFile(filePath);
|
|
3264
3469
|
}
|
|
3470
|
+
getChunksByName(name) {
|
|
3471
|
+
return this.inner.getChunksByName(name);
|
|
3472
|
+
}
|
|
3473
|
+
getChunksByNameCi(name) {
|
|
3474
|
+
return this.inner.getChunksByNameCi(name);
|
|
3475
|
+
}
|
|
3265
3476
|
deleteChunksByFile(filePath) {
|
|
3266
3477
|
return this.inner.deleteChunksByFile(filePath);
|
|
3267
3478
|
}
|
|
@@ -3305,29 +3516,125 @@ var Database = class {
|
|
|
3305
3516
|
getStats() {
|
|
3306
3517
|
return this.inner.getStats();
|
|
3307
3518
|
}
|
|
3519
|
+
// ── Symbol methods ──────────────────────────────────────────────
|
|
3520
|
+
upsertSymbol(symbol) {
|
|
3521
|
+
this.inner.upsertSymbol(symbol);
|
|
3522
|
+
}
|
|
3523
|
+
upsertSymbolsBatch(symbols) {
|
|
3524
|
+
if (symbols.length === 0) return;
|
|
3525
|
+
this.inner.upsertSymbolsBatch(symbols);
|
|
3526
|
+
}
|
|
3527
|
+
getSymbolsByFile(filePath) {
|
|
3528
|
+
return this.inner.getSymbolsByFile(filePath);
|
|
3529
|
+
}
|
|
3530
|
+
getSymbolByName(name, filePath) {
|
|
3531
|
+
return this.inner.getSymbolByName(name, filePath) ?? null;
|
|
3532
|
+
}
|
|
3533
|
+
getSymbolsByName(name) {
|
|
3534
|
+
return this.inner.getSymbolsByName(name);
|
|
3535
|
+
}
|
|
3536
|
+
getSymbolsByNameCi(name) {
|
|
3537
|
+
return this.inner.getSymbolsByNameCi(name);
|
|
3538
|
+
}
|
|
3539
|
+
deleteSymbolsByFile(filePath) {
|
|
3540
|
+
return this.inner.deleteSymbolsByFile(filePath);
|
|
3541
|
+
}
|
|
3542
|
+
// ── Call Edge methods ────────────────────────────────────────────
|
|
3543
|
+
upsertCallEdge(edge) {
|
|
3544
|
+
this.inner.upsertCallEdge(edge);
|
|
3545
|
+
}
|
|
3546
|
+
upsertCallEdgesBatch(edges) {
|
|
3547
|
+
if (edges.length === 0) return;
|
|
3548
|
+
this.inner.upsertCallEdgesBatch(edges);
|
|
3549
|
+
}
|
|
3550
|
+
getCallers(targetName, branch) {
|
|
3551
|
+
return this.inner.getCallers(targetName, branch);
|
|
3552
|
+
}
|
|
3553
|
+
getCallersWithContext(targetName, branch) {
|
|
3554
|
+
return this.inner.getCallersWithContext(targetName, branch);
|
|
3555
|
+
}
|
|
3556
|
+
getCallees(symbolId, branch) {
|
|
3557
|
+
return this.inner.getCallees(symbolId, branch);
|
|
3558
|
+
}
|
|
3559
|
+
deleteCallEdgesByFile(filePath) {
|
|
3560
|
+
return this.inner.deleteCallEdgesByFile(filePath);
|
|
3561
|
+
}
|
|
3562
|
+
resolveCallEdge(edgeId, toSymbolId) {
|
|
3563
|
+
this.inner.resolveCallEdge(edgeId, toSymbolId);
|
|
3564
|
+
}
|
|
3565
|
+
// ── Branch Symbol methods ────────────────────────────────────────
|
|
3566
|
+
addSymbolsToBranch(branch, symbolIds) {
|
|
3567
|
+
this.inner.addSymbolsToBranch(branch, symbolIds);
|
|
3568
|
+
}
|
|
3569
|
+
addSymbolsToBranchBatch(branch, symbolIds) {
|
|
3570
|
+
if (symbolIds.length === 0) return;
|
|
3571
|
+
this.inner.addSymbolsToBranchBatch(branch, symbolIds);
|
|
3572
|
+
}
|
|
3573
|
+
getBranchSymbolIds(branch) {
|
|
3574
|
+
return this.inner.getBranchSymbolIds(branch);
|
|
3575
|
+
}
|
|
3576
|
+
clearBranchSymbols(branch) {
|
|
3577
|
+
return this.inner.clearBranchSymbols(branch);
|
|
3578
|
+
}
|
|
3579
|
+
// ── GC methods for symbols/edges ─────────────────────────────────
|
|
3580
|
+
gcOrphanSymbols() {
|
|
3581
|
+
return this.inner.gcOrphanSymbols();
|
|
3582
|
+
}
|
|
3583
|
+
gcOrphanCallEdges() {
|
|
3584
|
+
return this.inner.gcOrphanCallEdges();
|
|
3585
|
+
}
|
|
3308
3586
|
};
|
|
3309
3587
|
|
|
3310
3588
|
// src/git/index.ts
|
|
3311
|
-
var
|
|
3312
|
-
var
|
|
3313
|
-
|
|
3589
|
+
var import_fs4 = require("fs");
|
|
3590
|
+
var path5 = __toESM(require("path"), 1);
|
|
3591
|
+
function readPackedRefs(gitDir) {
|
|
3592
|
+
const packedRefsPath = path5.join(gitDir, "packed-refs");
|
|
3593
|
+
if (!(0, import_fs4.existsSync)(packedRefsPath)) {
|
|
3594
|
+
return [];
|
|
3595
|
+
}
|
|
3596
|
+
try {
|
|
3597
|
+
return (0, import_fs4.readFileSync)(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
|
|
3598
|
+
} catch {
|
|
3599
|
+
return [];
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
function resolveCommonGitDir(gitDir) {
|
|
3603
|
+
const commonDirPath = path5.join(gitDir, "commondir");
|
|
3604
|
+
if (!(0, import_fs4.existsSync)(commonDirPath)) {
|
|
3605
|
+
return gitDir;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const raw = (0, import_fs4.readFileSync)(commonDirPath, "utf-8").trim();
|
|
3609
|
+
if (!raw) {
|
|
3610
|
+
return gitDir;
|
|
3611
|
+
}
|
|
3612
|
+
const resolved = path5.isAbsolute(raw) ? raw : path5.resolve(gitDir, raw);
|
|
3613
|
+
if ((0, import_fs4.existsSync)(resolved)) {
|
|
3614
|
+
return resolved;
|
|
3615
|
+
}
|
|
3616
|
+
} catch {
|
|
3617
|
+
return gitDir;
|
|
3618
|
+
}
|
|
3619
|
+
return gitDir;
|
|
3620
|
+
}
|
|
3314
3621
|
function resolveGitDir(repoRoot) {
|
|
3315
|
-
const gitPath =
|
|
3316
|
-
if (!(0,
|
|
3622
|
+
const gitPath = path5.join(repoRoot, ".git");
|
|
3623
|
+
if (!(0, import_fs4.existsSync)(gitPath)) {
|
|
3317
3624
|
return null;
|
|
3318
3625
|
}
|
|
3319
3626
|
try {
|
|
3320
|
-
const stat = (0,
|
|
3627
|
+
const stat = (0, import_fs4.statSync)(gitPath);
|
|
3321
3628
|
if (stat.isDirectory()) {
|
|
3322
3629
|
return gitPath;
|
|
3323
3630
|
}
|
|
3324
3631
|
if (stat.isFile()) {
|
|
3325
|
-
const content = (0,
|
|
3632
|
+
const content = (0, import_fs4.readFileSync)(gitPath, "utf-8").trim();
|
|
3326
3633
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3327
3634
|
if (match) {
|
|
3328
3635
|
const gitdir = match[1];
|
|
3329
|
-
const resolvedPath =
|
|
3330
|
-
if ((0,
|
|
3636
|
+
const resolvedPath = path5.isAbsolute(gitdir) ? gitdir : path5.resolve(repoRoot, gitdir);
|
|
3637
|
+
if ((0, import_fs4.existsSync)(resolvedPath)) {
|
|
3331
3638
|
return resolvedPath;
|
|
3332
3639
|
}
|
|
3333
3640
|
}
|
|
@@ -3344,12 +3651,12 @@ function getCurrentBranch(repoRoot) {
|
|
|
3344
3651
|
if (!gitDir) {
|
|
3345
3652
|
return null;
|
|
3346
3653
|
}
|
|
3347
|
-
const headPath =
|
|
3348
|
-
if (!(0,
|
|
3654
|
+
const headPath = path5.join(gitDir, "HEAD");
|
|
3655
|
+
if (!(0, import_fs4.existsSync)(headPath)) {
|
|
3349
3656
|
return null;
|
|
3350
3657
|
}
|
|
3351
3658
|
try {
|
|
3352
|
-
const headContent = (0,
|
|
3659
|
+
const headContent = (0, import_fs4.readFileSync)(headPath, "utf-8").trim();
|
|
3353
3660
|
const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
|
|
3354
3661
|
if (match) {
|
|
3355
3662
|
return match[1];
|
|
@@ -3364,37 +3671,20 @@ function getCurrentBranch(repoRoot) {
|
|
|
3364
3671
|
}
|
|
3365
3672
|
function getBaseBranch(repoRoot) {
|
|
3366
3673
|
const gitDir = resolveGitDir(repoRoot);
|
|
3674
|
+
const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
|
|
3367
3675
|
const candidates = ["main", "master", "develop", "trunk"];
|
|
3368
|
-
if (
|
|
3676
|
+
if (refStoreDir) {
|
|
3369
3677
|
for (const candidate of candidates) {
|
|
3370
|
-
const refPath =
|
|
3371
|
-
if ((0,
|
|
3678
|
+
const refPath = path5.join(refStoreDir, "refs", "heads", candidate);
|
|
3679
|
+
if ((0, import_fs4.existsSync)(refPath)) {
|
|
3372
3680
|
return candidate;
|
|
3373
3681
|
}
|
|
3374
|
-
const
|
|
3375
|
-
if ((
|
|
3376
|
-
|
|
3377
|
-
const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
|
|
3378
|
-
if (content.includes(`refs/heads/${candidate}`)) {
|
|
3379
|
-
return candidate;
|
|
3380
|
-
}
|
|
3381
|
-
} catch {
|
|
3382
|
-
}
|
|
3682
|
+
const packedRefs = readPackedRefs(refStoreDir);
|
|
3683
|
+
if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
|
|
3684
|
+
return candidate;
|
|
3383
3685
|
}
|
|
3384
3686
|
}
|
|
3385
3687
|
}
|
|
3386
|
-
try {
|
|
3387
|
-
const result = (0, import_child_process.execSync)("git remote show origin", {
|
|
3388
|
-
cwd: repoRoot,
|
|
3389
|
-
encoding: "utf-8",
|
|
3390
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3391
|
-
});
|
|
3392
|
-
const match = result.match(/HEAD branch: (.+)/);
|
|
3393
|
-
if (match) {
|
|
3394
|
-
return match[1].trim();
|
|
3395
|
-
}
|
|
3396
|
-
} catch {
|
|
3397
|
-
}
|
|
3398
3688
|
return getCurrentBranch(repoRoot) ?? "main";
|
|
3399
3689
|
}
|
|
3400
3690
|
function getBranchOrDefault(repoRoot) {
|
|
@@ -3405,6 +3695,30 @@ function getBranchOrDefault(repoRoot) {
|
|
|
3405
3695
|
}
|
|
3406
3696
|
|
|
3407
3697
|
// src/indexer/index.ts
|
|
3698
|
+
var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php"]);
|
|
3699
|
+
var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
3700
|
+
"function_declaration",
|
|
3701
|
+
"function",
|
|
3702
|
+
"arrow_function",
|
|
3703
|
+
"method_definition",
|
|
3704
|
+
"class_declaration",
|
|
3705
|
+
"interface_declaration",
|
|
3706
|
+
"type_alias_declaration",
|
|
3707
|
+
"enum_declaration",
|
|
3708
|
+
"function_definition",
|
|
3709
|
+
"class_definition",
|
|
3710
|
+
"decorated_definition",
|
|
3711
|
+
"method_declaration",
|
|
3712
|
+
"type_declaration",
|
|
3713
|
+
"type_spec",
|
|
3714
|
+
"function_item",
|
|
3715
|
+
"impl_item",
|
|
3716
|
+
"struct_item",
|
|
3717
|
+
"enum_item",
|
|
3718
|
+
"trait_item",
|
|
3719
|
+
"mod_item",
|
|
3720
|
+
"trait_declaration"
|
|
3721
|
+
]);
|
|
3408
3722
|
function float32ArrayToBuffer(arr) {
|
|
3409
3723
|
const float32 = new Float32Array(arr);
|
|
3410
3724
|
return Buffer.from(float32.buffer);
|
|
@@ -3429,117 +3743,1002 @@ function isRateLimitError(error) {
|
|
|
3429
3743
|
return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
|
|
3430
3744
|
}
|
|
3431
3745
|
var INDEX_METADATA_VERSION = "1";
|
|
3432
|
-
var
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3746
|
+
var RANKING_TOKEN_CACHE_LIMIT = 4096;
|
|
3747
|
+
var rankingQueryTokenCache = /* @__PURE__ */ new Map();
|
|
3748
|
+
var rankingNameTokenCache = /* @__PURE__ */ new Map();
|
|
3749
|
+
var rankingPathTokenCache = /* @__PURE__ */ new Map();
|
|
3750
|
+
var rankingTextTokenCache = /* @__PURE__ */ new Map();
|
|
3751
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
3752
|
+
"the",
|
|
3753
|
+
"and",
|
|
3754
|
+
"for",
|
|
3755
|
+
"with",
|
|
3756
|
+
"from",
|
|
3757
|
+
"that",
|
|
3758
|
+
"this",
|
|
3759
|
+
"into",
|
|
3760
|
+
"using",
|
|
3761
|
+
"where",
|
|
3762
|
+
"what",
|
|
3763
|
+
"when",
|
|
3764
|
+
"why",
|
|
3765
|
+
"how",
|
|
3766
|
+
"are",
|
|
3767
|
+
"was",
|
|
3768
|
+
"were",
|
|
3769
|
+
"be",
|
|
3770
|
+
"been",
|
|
3771
|
+
"being",
|
|
3772
|
+
"find",
|
|
3773
|
+
"show",
|
|
3774
|
+
"get",
|
|
3775
|
+
"run",
|
|
3776
|
+
"use",
|
|
3777
|
+
"code",
|
|
3778
|
+
"function",
|
|
3779
|
+
"implementation",
|
|
3780
|
+
"retrieve",
|
|
3781
|
+
"results",
|
|
3782
|
+
"result",
|
|
3783
|
+
"search",
|
|
3784
|
+
"pipeline",
|
|
3785
|
+
"top",
|
|
3786
|
+
"in",
|
|
3787
|
+
"on",
|
|
3788
|
+
"of",
|
|
3789
|
+
"to",
|
|
3790
|
+
"by",
|
|
3791
|
+
"as",
|
|
3792
|
+
"or",
|
|
3793
|
+
"an",
|
|
3794
|
+
"a"
|
|
3795
|
+
]);
|
|
3796
|
+
var TEST_PATH_SEGMENTS = [
|
|
3797
|
+
"tests/",
|
|
3798
|
+
"__tests__/",
|
|
3799
|
+
"/test/",
|
|
3800
|
+
"fixtures/",
|
|
3801
|
+
"benchmark",
|
|
3802
|
+
"README",
|
|
3803
|
+
"ARCHITECTURE",
|
|
3804
|
+
"docs/"
|
|
3805
|
+
];
|
|
3806
|
+
var IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS = [
|
|
3807
|
+
"tests/",
|
|
3808
|
+
"__tests__/",
|
|
3809
|
+
"/test/",
|
|
3810
|
+
"fixtures/",
|
|
3811
|
+
"benchmark",
|
|
3812
|
+
"readme",
|
|
3813
|
+
"architecture",
|
|
3814
|
+
"docs/",
|
|
3815
|
+
"examples/",
|
|
3816
|
+
"example/",
|
|
3817
|
+
".github/",
|
|
3818
|
+
"/scripts/",
|
|
3819
|
+
"/migrations/",
|
|
3820
|
+
"/generated/"
|
|
3821
|
+
];
|
|
3822
|
+
var SOURCE_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3823
|
+
"implement",
|
|
3824
|
+
"implementation",
|
|
3825
|
+
"function",
|
|
3826
|
+
"method",
|
|
3827
|
+
"class",
|
|
3828
|
+
"logic",
|
|
3829
|
+
"algorithm",
|
|
3830
|
+
"pipeline",
|
|
3831
|
+
"indexer",
|
|
3832
|
+
"where"
|
|
3833
|
+
]);
|
|
3834
|
+
var DOC_TEST_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3835
|
+
"test",
|
|
3836
|
+
"tests",
|
|
3837
|
+
"fixture",
|
|
3838
|
+
"fixtures",
|
|
3839
|
+
"benchmark",
|
|
3840
|
+
"readme",
|
|
3841
|
+
"docs",
|
|
3842
|
+
"documentation"
|
|
3843
|
+
]);
|
|
3844
|
+
var DOC_INTENT_HINTS = /* @__PURE__ */ new Set([
|
|
3845
|
+
"readme",
|
|
3846
|
+
"docs",
|
|
3847
|
+
"documentation",
|
|
3848
|
+
"guide",
|
|
3849
|
+
"usage"
|
|
3850
|
+
]);
|
|
3851
|
+
function setBoundedCache(cache, key, value) {
|
|
3852
|
+
if (cache.size >= RANKING_TOKEN_CACHE_LIMIT) {
|
|
3853
|
+
const oldest = cache.keys().next().value;
|
|
3854
|
+
if (oldest !== void 0) {
|
|
3855
|
+
cache.delete(oldest);
|
|
3466
3856
|
}
|
|
3467
|
-
return path5.join(this.projectRoot, ".opencode", "index");
|
|
3468
3857
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
this.fileHashCache = new Map(Object.entries(parsed));
|
|
3475
|
-
}
|
|
3476
|
-
} catch {
|
|
3477
|
-
this.fileHashCache = /* @__PURE__ */ new Map();
|
|
3478
|
-
}
|
|
3858
|
+
cache.set(key, value);
|
|
3859
|
+
}
|
|
3860
|
+
function tokenizeTextForRanking(text) {
|
|
3861
|
+
if (!text) {
|
|
3862
|
+
return /* @__PURE__ */ new Set();
|
|
3479
3863
|
}
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
}
|
|
3485
|
-
this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
|
|
3864
|
+
const lowered = text.toLowerCase();
|
|
3865
|
+
const cache = rankingQueryTokenCache.get(lowered) ?? rankingTextTokenCache.get(lowered);
|
|
3866
|
+
if (cache) {
|
|
3867
|
+
return cache;
|
|
3486
3868
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3869
|
+
const tokens = new Set(
|
|
3870
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1 && !STOPWORDS.has(token))
|
|
3871
|
+
);
|
|
3872
|
+
setBoundedCache(rankingQueryTokenCache, lowered, tokens);
|
|
3873
|
+
setBoundedCache(rankingTextTokenCache, lowered, tokens);
|
|
3874
|
+
return tokens;
|
|
3875
|
+
}
|
|
3876
|
+
function splitPathTokens(filePath) {
|
|
3877
|
+
const lowered = filePath.toLowerCase();
|
|
3878
|
+
const cache = rankingPathTokenCache.get(lowered);
|
|
3879
|
+
if (cache) {
|
|
3880
|
+
return cache;
|
|
3881
|
+
}
|
|
3882
|
+
const normalized = lowered.replace(/[^a-z0-9/._-]/g, " ").split(/[/._-]+/).filter((token) => token.length > 1);
|
|
3883
|
+
const tokens = new Set(normalized);
|
|
3884
|
+
setBoundedCache(rankingPathTokenCache, lowered, tokens);
|
|
3885
|
+
return tokens;
|
|
3886
|
+
}
|
|
3887
|
+
function splitNameTokens(name) {
|
|
3888
|
+
if (!name) {
|
|
3889
|
+
return /* @__PURE__ */ new Set();
|
|
3491
3890
|
}
|
|
3492
|
-
|
|
3493
|
-
|
|
3891
|
+
const lowered = name.toLowerCase();
|
|
3892
|
+
const cache = rankingNameTokenCache.get(lowered);
|
|
3893
|
+
if (cache) {
|
|
3894
|
+
return cache;
|
|
3494
3895
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3896
|
+
const tokens = new Set(
|
|
3897
|
+
lowered.replace(/[^\w\s]/g, " ").split(/\s+/).filter((token) => token.length > 1)
|
|
3898
|
+
);
|
|
3899
|
+
setBoundedCache(rankingNameTokenCache, lowered, tokens);
|
|
3900
|
+
return tokens;
|
|
3901
|
+
}
|
|
3902
|
+
function chunkTypeBoost(chunkType) {
|
|
3903
|
+
switch (chunkType) {
|
|
3904
|
+
case "function":
|
|
3905
|
+
case "function_declaration":
|
|
3906
|
+
case "method":
|
|
3907
|
+
case "method_definition":
|
|
3908
|
+
case "class":
|
|
3909
|
+
case "class_declaration":
|
|
3910
|
+
return 0.2;
|
|
3911
|
+
case "interface":
|
|
3912
|
+
case "type":
|
|
3913
|
+
case "enum":
|
|
3914
|
+
case "struct":
|
|
3915
|
+
case "impl":
|
|
3916
|
+
case "trait":
|
|
3917
|
+
case "module":
|
|
3918
|
+
return 0.1;
|
|
3919
|
+
default:
|
|
3920
|
+
return 0;
|
|
3501
3921
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3922
|
+
}
|
|
3923
|
+
function isTestOrDocPath(filePath) {
|
|
3924
|
+
return TEST_PATH_SEGMENTS.some((segment) => filePath.includes(segment));
|
|
3925
|
+
}
|
|
3926
|
+
function isLikelyImplementationPath(filePath) {
|
|
3927
|
+
const lowered = filePath.toLowerCase();
|
|
3928
|
+
if (IMPLEMENTATION_EXCLUDE_PATH_SEGMENTS.some((segment) => lowered.includes(segment))) {
|
|
3929
|
+
return false;
|
|
3506
3930
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
(0, import_fs4.unlinkSync)(this.fileHashCachePath);
|
|
3511
|
-
}
|
|
3512
|
-
await this.healthCheck();
|
|
3513
|
-
this.releaseIndexingLock();
|
|
3514
|
-
this.logger.info("Recovery complete, next index will re-process all files");
|
|
3931
|
+
const ext = lowered.split(".").pop() ?? "";
|
|
3932
|
+
if (["md", "mdx", "txt", "rst", "adoc", "snap", "json", "yaml", "yml", "lock"].includes(ext)) {
|
|
3933
|
+
return false;
|
|
3515
3934
|
}
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3935
|
+
return true;
|
|
3936
|
+
}
|
|
3937
|
+
function classifyQueryIntent(tokens) {
|
|
3938
|
+
const sourceIntentHits = tokens.filter((t) => SOURCE_INTENT_HINTS.has(t)).length;
|
|
3939
|
+
const docTestIntentHits = tokens.filter((t) => DOC_TEST_INTENT_HINTS.has(t)).length;
|
|
3940
|
+
return sourceIntentHits >= docTestIntentHits ? "source" : "doc_test";
|
|
3941
|
+
}
|
|
3942
|
+
function classifyQueryIntentRaw(query) {
|
|
3943
|
+
const lowerQuery = query.toLowerCase();
|
|
3944
|
+
const docTestRawHits = Array.from(DOC_TEST_INTENT_HINTS).filter(
|
|
3945
|
+
(hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)
|
|
3946
|
+
).length;
|
|
3947
|
+
const sourceRawHits = [
|
|
3948
|
+
"implement",
|
|
3949
|
+
"implementation",
|
|
3950
|
+
"implements",
|
|
3951
|
+
"function",
|
|
3952
|
+
"method",
|
|
3953
|
+
"class",
|
|
3954
|
+
"logic",
|
|
3955
|
+
"algorithm",
|
|
3956
|
+
"pipeline",
|
|
3957
|
+
"indexer"
|
|
3958
|
+
].filter((hint) => new RegExp(`\\b${hint}\\b`).test(lowerQuery)).length;
|
|
3959
|
+
if (docTestRawHits > sourceRawHits) {
|
|
3960
|
+
return "doc_test";
|
|
3961
|
+
}
|
|
3962
|
+
if (sourceRawHits > docTestRawHits) {
|
|
3963
|
+
return "source";
|
|
3964
|
+
}
|
|
3965
|
+
const hasWhereIsPattern = /\bwhere\s+is\b/.test(lowerQuery);
|
|
3966
|
+
const hasIdentifierHints = extractIdentifierHints(query).length > 0;
|
|
3967
|
+
if (hasWhereIsPattern && hasIdentifierHints && docTestRawHits === 0) {
|
|
3968
|
+
return "source";
|
|
3969
|
+
}
|
|
3970
|
+
const queryTokens = Array.from(tokenizeTextForRanking(query));
|
|
3971
|
+
return classifyQueryIntent(queryTokens);
|
|
3972
|
+
}
|
|
3973
|
+
function classifyDocIntent(tokens) {
|
|
3974
|
+
const docHits = tokens.filter((t) => DOC_INTENT_HINTS.has(t)).length;
|
|
3975
|
+
const testHits = tokens.filter((t) => ["test", "tests", "fixture", "fixtures", "benchmark"].includes(t)).length;
|
|
3976
|
+
if (docHits > 0 && testHits === 0) return "docs";
|
|
3977
|
+
if (testHits > 0 && docHits === 0) return "test";
|
|
3978
|
+
if (testHits > 0 || docHits > 0) return "mixed";
|
|
3979
|
+
return "none";
|
|
3980
|
+
}
|
|
3981
|
+
function isImplementationChunkType(chunkType) {
|
|
3982
|
+
return [
|
|
3983
|
+
"export_statement",
|
|
3984
|
+
"function",
|
|
3985
|
+
"function_declaration",
|
|
3986
|
+
"method",
|
|
3987
|
+
"method_definition",
|
|
3988
|
+
"class",
|
|
3989
|
+
"class_declaration",
|
|
3990
|
+
"interface",
|
|
3991
|
+
"type",
|
|
3992
|
+
"enum",
|
|
3993
|
+
"module"
|
|
3994
|
+
].includes(chunkType);
|
|
3995
|
+
}
|
|
3996
|
+
function extractIdentifierHints(query) {
|
|
3997
|
+
const identifiers = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
3998
|
+
return identifiers.filter((id) => id.length >= 3).filter((id) => {
|
|
3999
|
+
const lower = id.toLowerCase();
|
|
4000
|
+
if (STOPWORDS.has(lower)) return false;
|
|
4001
|
+
return /[A-Z]/.test(id) || id.includes("_") || id.endsWith("Results") || id.endsWith("Result");
|
|
4002
|
+
}).map((id) => id.toLowerCase());
|
|
4003
|
+
}
|
|
4004
|
+
function extractCodeTermHints(query) {
|
|
4005
|
+
const terms = query.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
4006
|
+
return terms.map((term) => term.toLowerCase()).filter((term) => term.length >= 3).filter((term) => !STOPWORDS.has(term));
|
|
4007
|
+
}
|
|
4008
|
+
function normalizeIdentifierVariants(identifier) {
|
|
4009
|
+
const lower = identifier.toLowerCase();
|
|
4010
|
+
const compact = lower.replace(/[^a-z0-9]/g, "");
|
|
4011
|
+
const snake = compact.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
4012
|
+
const kebab = snake.replace(/_/g, "-");
|
|
4013
|
+
const variants = [lower, compact, snake, kebab].filter((value) => value.length > 0);
|
|
4014
|
+
return Array.from(new Set(variants));
|
|
4015
|
+
}
|
|
4016
|
+
function scoreIdentifierMatch(name, filePath, hints) {
|
|
4017
|
+
const nameLower = (name ?? "").toLowerCase();
|
|
4018
|
+
const pathLower = filePath.toLowerCase();
|
|
4019
|
+
let best = 0;
|
|
4020
|
+
for (const hint of hints) {
|
|
4021
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
4022
|
+
for (const variant of variants) {
|
|
4023
|
+
if (nameLower === variant) {
|
|
4024
|
+
best = Math.max(best, 1);
|
|
4025
|
+
} else if (nameLower.includes(variant)) {
|
|
4026
|
+
best = Math.max(best, 0.8);
|
|
4027
|
+
} else if (pathLower.includes(variant)) {
|
|
4028
|
+
best = Math.max(best, 0.6);
|
|
3521
4029
|
}
|
|
3522
|
-
} catch {
|
|
3523
|
-
return [];
|
|
3524
4030
|
}
|
|
4031
|
+
}
|
|
4032
|
+
return best;
|
|
4033
|
+
}
|
|
4034
|
+
function extractPrimaryIdentifierQueryHint(query) {
|
|
4035
|
+
const identifiers = extractIdentifierHints(query);
|
|
4036
|
+
if (identifiers.length > 0) {
|
|
4037
|
+
return identifiers[0] ?? null;
|
|
4038
|
+
}
|
|
4039
|
+
const codeTerms = extractCodeTermHints(query);
|
|
4040
|
+
const best = codeTerms.find((term) => term.length >= 6);
|
|
4041
|
+
return best ?? null;
|
|
4042
|
+
}
|
|
4043
|
+
var FILE_PATH_HINT_EXTENSIONS = [
|
|
4044
|
+
"ts",
|
|
4045
|
+
"tsx",
|
|
4046
|
+
"js",
|
|
4047
|
+
"jsx",
|
|
4048
|
+
"mjs",
|
|
4049
|
+
"cjs",
|
|
4050
|
+
"mts",
|
|
4051
|
+
"cts",
|
|
4052
|
+
"py",
|
|
4053
|
+
"rs",
|
|
4054
|
+
"go",
|
|
4055
|
+
"java",
|
|
4056
|
+
"kt",
|
|
4057
|
+
"kts",
|
|
4058
|
+
"swift",
|
|
4059
|
+
"rb",
|
|
4060
|
+
"php",
|
|
4061
|
+
"c",
|
|
4062
|
+
"h",
|
|
4063
|
+
"cc",
|
|
4064
|
+
"cpp",
|
|
4065
|
+
"cxx",
|
|
4066
|
+
"hpp",
|
|
4067
|
+
"cs",
|
|
4068
|
+
"scala",
|
|
4069
|
+
"lua",
|
|
4070
|
+
"sh",
|
|
4071
|
+
"bash",
|
|
4072
|
+
"zsh",
|
|
4073
|
+
"json",
|
|
4074
|
+
"yaml",
|
|
4075
|
+
"yml",
|
|
4076
|
+
"toml"
|
|
4077
|
+
];
|
|
4078
|
+
var FILE_PATH_HINT_SUFFIX_REGEX = new RegExp(
|
|
4079
|
+
"\\s+\\bin\\s+[\"'`]?((?:\\.\\/)?(?:[A-Za-z0-9._-]+\\/)+[A-Za-z0-9._-]+\\.(?:" + FILE_PATH_HINT_EXTENSIONS.join("|") + "))[\"'`]?[\\])}>.,;!?]*\\s*$",
|
|
4080
|
+
"i"
|
|
4081
|
+
);
|
|
4082
|
+
function normalizeFilePathForHintMatch(filePath) {
|
|
4083
|
+
return filePath.replace(/\\/g, "/").toLowerCase().replace(/^\.\//, "");
|
|
4084
|
+
}
|
|
4085
|
+
function pathMatchesHint(filePath, hint) {
|
|
4086
|
+
const normalizedPath = normalizeFilePathForHintMatch(filePath);
|
|
4087
|
+
const normalizedHint = normalizeFilePathForHintMatch(hint);
|
|
4088
|
+
return normalizedPath.endsWith(normalizedHint) || normalizedPath.includes(`/${normalizedHint}`) || normalizedPath.includes(normalizedHint);
|
|
4089
|
+
}
|
|
4090
|
+
function extractFilePathHint(query) {
|
|
4091
|
+
const match = query.match(FILE_PATH_HINT_SUFFIX_REGEX);
|
|
4092
|
+
const rawPath = match?.[1];
|
|
4093
|
+
if (!rawPath) {
|
|
4094
|
+
return null;
|
|
4095
|
+
}
|
|
4096
|
+
return rawPath.replace(/^\.\//, "");
|
|
4097
|
+
}
|
|
4098
|
+
function stripFilePathHint(query) {
|
|
4099
|
+
const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
|
|
4100
|
+
return stripped.length > 0 ? stripped : query;
|
|
4101
|
+
}
|
|
4102
|
+
function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4103
|
+
if (!prioritizeSourcePaths) {
|
|
3525
4104
|
return [];
|
|
3526
4105
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
4106
|
+
const primary = extractPrimaryIdentifierQueryHint(query);
|
|
4107
|
+
if (!primary) {
|
|
4108
|
+
return [];
|
|
4109
|
+
}
|
|
4110
|
+
const filePathHint = extractFilePathHint(query);
|
|
4111
|
+
const primaryVariants = normalizeIdentifierVariants(primary);
|
|
4112
|
+
const hints = [primary, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].map((value) => value.toLowerCase()).filter((value, idx, arr) => value.length >= 3 && arr.indexOf(value) === idx).slice(0, 8);
|
|
4113
|
+
const deterministic = candidates.filter(
|
|
4114
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
4115
|
+
).map((candidate) => {
|
|
4116
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4117
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
4118
|
+
let maxMatch = 0;
|
|
4119
|
+
const nameMatchesPrimary = primaryVariants.some(
|
|
4120
|
+
(variant) => nameLower === variant || nameLower.replace(/[^a-z0-9]/g, "") === variant.replace(/[^a-z0-9]/g, "")
|
|
4121
|
+
);
|
|
4122
|
+
const pathMatchesFileHint = filePathHint ? pathMatchesHint(candidate.metadata.filePath, filePathHint) : false;
|
|
4123
|
+
for (const hint of hints) {
|
|
4124
|
+
const variants = normalizeIdentifierVariants(hint);
|
|
4125
|
+
for (const variant of variants) {
|
|
4126
|
+
if (nameLower === variant) {
|
|
4127
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
4128
|
+
} else if (nameLower.includes(variant)) {
|
|
4129
|
+
maxMatch = Math.max(maxMatch, 0.85);
|
|
4130
|
+
} else if (pathLower.includes(variant)) {
|
|
4131
|
+
maxMatch = Math.max(maxMatch, 0.7);
|
|
4132
|
+
}
|
|
3532
4133
|
}
|
|
3533
|
-
return;
|
|
3534
4134
|
}
|
|
3535
|
-
|
|
4135
|
+
if (pathMatchesFileHint && nameMatchesPrimary) {
|
|
4136
|
+
maxMatch = Math.max(maxMatch, 1);
|
|
4137
|
+
}
|
|
4138
|
+
return {
|
|
4139
|
+
candidate,
|
|
4140
|
+
maxMatch,
|
|
4141
|
+
pathMatchesFileHint,
|
|
4142
|
+
nameMatchesPrimary
|
|
4143
|
+
};
|
|
4144
|
+
}).filter((entry) => entry.maxMatch >= 0.7).sort((a, b) => {
|
|
4145
|
+
const aAnchored = a.pathMatchesFileHint && a.nameMatchesPrimary ? 1 : 0;
|
|
4146
|
+
const bAnchored = b.pathMatchesFileHint && b.nameMatchesPrimary ? 1 : 0;
|
|
4147
|
+
if (aAnchored !== bAnchored) return bAnchored - aAnchored;
|
|
4148
|
+
if (b.maxMatch !== a.maxMatch) return b.maxMatch - a.maxMatch;
|
|
4149
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4150
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4151
|
+
}).slice(0, Math.max(limit * 2, 12));
|
|
4152
|
+
return deterministic.map((entry) => ({
|
|
4153
|
+
id: entry.candidate.id,
|
|
4154
|
+
score: entry.pathMatchesFileHint && entry.nameMatchesPrimary ? 0.995 : Math.min(1, 0.9 + entry.maxMatch * 0.09),
|
|
4155
|
+
metadata: entry.candidate.metadata
|
|
4156
|
+
}));
|
|
4157
|
+
}
|
|
4158
|
+
function fuseResultsWeighted(semanticResults, keywordResults, keywordWeight, limit) {
|
|
4159
|
+
const semanticWeight = 1 - keywordWeight;
|
|
4160
|
+
const fusedScores = /* @__PURE__ */ new Map();
|
|
4161
|
+
for (const r of semanticResults) {
|
|
4162
|
+
fusedScores.set(r.id, {
|
|
4163
|
+
score: r.score * semanticWeight,
|
|
4164
|
+
metadata: r.metadata
|
|
4165
|
+
});
|
|
3536
4166
|
}
|
|
3537
|
-
|
|
3538
|
-
const existing =
|
|
3539
|
-
existing
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
4167
|
+
for (const r of keywordResults) {
|
|
4168
|
+
const existing = fusedScores.get(r.id);
|
|
4169
|
+
if (existing) {
|
|
4170
|
+
existing.score += r.score * keywordWeight;
|
|
4171
|
+
} else {
|
|
4172
|
+
fusedScores.set(r.id, {
|
|
4173
|
+
score: r.score * keywordWeight,
|
|
4174
|
+
metadata: r.metadata
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
4179
|
+
id,
|
|
4180
|
+
score: data.score,
|
|
4181
|
+
metadata: data.metadata
|
|
4182
|
+
}));
|
|
4183
|
+
results.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4184
|
+
return results.slice(0, limit);
|
|
4185
|
+
}
|
|
4186
|
+
function fuseResultsRrf(semanticResults, keywordResults, rrfK, limit) {
|
|
4187
|
+
const maxPossibleRaw = 2 / (rrfK + 1);
|
|
4188
|
+
const rankByIdSemantic = /* @__PURE__ */ new Map();
|
|
4189
|
+
const rankByIdKeyword = /* @__PURE__ */ new Map();
|
|
4190
|
+
const metadataById = /* @__PURE__ */ new Map();
|
|
4191
|
+
semanticResults.forEach((result, index) => {
|
|
4192
|
+
rankByIdSemantic.set(result.id, index + 1);
|
|
4193
|
+
metadataById.set(result.id, result.metadata);
|
|
4194
|
+
});
|
|
4195
|
+
keywordResults.forEach((result, index) => {
|
|
4196
|
+
rankByIdKeyword.set(result.id, index + 1);
|
|
4197
|
+
if (!metadataById.has(result.id)) {
|
|
4198
|
+
metadataById.set(result.id, result.metadata);
|
|
4199
|
+
}
|
|
4200
|
+
});
|
|
4201
|
+
const allIds = /* @__PURE__ */ new Set([...rankByIdSemantic.keys(), ...rankByIdKeyword.keys()]);
|
|
4202
|
+
const fused = [];
|
|
4203
|
+
for (const id of allIds) {
|
|
4204
|
+
const semanticRank = rankByIdSemantic.get(id);
|
|
4205
|
+
const keywordRank = rankByIdKeyword.get(id);
|
|
4206
|
+
const semanticScore = semanticRank ? 1 / (rrfK + semanticRank) : 0;
|
|
4207
|
+
const keywordScore = keywordRank ? 1 / (rrfK + keywordRank) : 0;
|
|
4208
|
+
const metadata = metadataById.get(id);
|
|
4209
|
+
if (!metadata) continue;
|
|
4210
|
+
fused.push({
|
|
4211
|
+
id,
|
|
4212
|
+
score: maxPossibleRaw > 0 ? (semanticScore + keywordScore) / maxPossibleRaw : 0,
|
|
4213
|
+
metadata
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
fused.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4217
|
+
return fused.slice(0, limit);
|
|
4218
|
+
}
|
|
4219
|
+
function rerankResults(query, candidates, rerankTopN, options) {
|
|
4220
|
+
if (rerankTopN <= 0 || candidates.length <= 1) {
|
|
4221
|
+
return candidates;
|
|
4222
|
+
}
|
|
4223
|
+
const topN = Math.min(rerankTopN, candidates.length);
|
|
4224
|
+
const queryTokens = tokenizeTextForRanking(query);
|
|
4225
|
+
if (queryTokens.size === 0) {
|
|
4226
|
+
return candidates;
|
|
4227
|
+
}
|
|
4228
|
+
const queryTokenList = Array.from(queryTokens);
|
|
4229
|
+
const intent = classifyQueryIntentRaw(query);
|
|
4230
|
+
const docIntent = classifyDocIntent(queryTokenList);
|
|
4231
|
+
const preferSourcePaths = options?.prioritizeSourcePaths ?? intent === "source";
|
|
4232
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4233
|
+
const head = candidates.slice(0, topN).map((candidate, idx) => {
|
|
4234
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4235
|
+
const nameTokens = splitNameTokens(candidate.metadata.name ?? "");
|
|
4236
|
+
const chunkTypeTokens = tokenizeTextForRanking(candidate.metadata.chunkType);
|
|
4237
|
+
let exactOrPrefixNameHits = 0;
|
|
4238
|
+
let pathOverlap = 0;
|
|
4239
|
+
let chunkTypeHits = 0;
|
|
4240
|
+
for (const token of queryTokenList) {
|
|
4241
|
+
if (nameTokens.has(token)) {
|
|
4242
|
+
exactOrPrefixNameHits += 1;
|
|
4243
|
+
} else {
|
|
4244
|
+
for (const nameToken of nameTokens) {
|
|
4245
|
+
if (nameToken.startsWith(token) || token.startsWith(nameToken)) {
|
|
4246
|
+
exactOrPrefixNameHits += 1;
|
|
4247
|
+
break;
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
if (pathTokens.has(token)) {
|
|
4252
|
+
pathOverlap += 1;
|
|
4253
|
+
}
|
|
4254
|
+
if (chunkTypeTokens.has(token)) {
|
|
4255
|
+
chunkTypeHits += 1;
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
const likelyTestOrDoc = isTestOrDocPath(candidate.metadata.filePath);
|
|
4259
|
+
const lowerPath = candidate.metadata.filePath.toLowerCase();
|
|
4260
|
+
const lowerName = (candidate.metadata.name ?? "").toLowerCase();
|
|
4261
|
+
const hasIdentifierMatch = identifierHints.some((id) => lowerPath.includes(id) || lowerName.includes(id));
|
|
4262
|
+
const implementationPathBoost = preferSourcePaths && isLikelyImplementationPath(candidate.metadata.filePath) ? 0.08 : 0;
|
|
4263
|
+
const isReadmePath = candidate.metadata.filePath.toLowerCase().includes("readme");
|
|
4264
|
+
const testDocPenalty = preferSourcePaths && likelyTestOrDoc ? 0.12 : 0;
|
|
4265
|
+
const readmeDocBoost = !preferSourcePaths && isReadmePath ? 0.08 : 0;
|
|
4266
|
+
const identifierBoost = hasIdentifierMatch ? 0.12 : 0;
|
|
4267
|
+
const tokenCoverage = queryTokenList.length > 0 ? (exactOrPrefixNameHits + pathOverlap + chunkTypeHits) / queryTokenList.length : 0;
|
|
4268
|
+
const coverageBoost = Math.min(0.12, tokenCoverage * 0.06);
|
|
4269
|
+
const deterministicBoost = exactOrPrefixNameHits * 0.08 + pathOverlap * 0.03 + chunkTypeHits * 0.02 + coverageBoost + identifierBoost + implementationPathBoost - testDocPenalty + readmeDocBoost + chunkTypeBoost(candidate.metadata.chunkType);
|
|
4270
|
+
return {
|
|
4271
|
+
candidate,
|
|
4272
|
+
boostedScore: candidate.score + deterministicBoost,
|
|
4273
|
+
originalIndex: idx,
|
|
4274
|
+
hasIdentifierMatch,
|
|
4275
|
+
implementationChunk: isImplementationChunkType(candidate.metadata.chunkType),
|
|
4276
|
+
isLikelyImplementationPath: isLikelyImplementationPath(candidate.metadata.filePath),
|
|
4277
|
+
isTestOrDocPath: likelyTestOrDoc,
|
|
4278
|
+
isReadmePath
|
|
4279
|
+
};
|
|
4280
|
+
});
|
|
4281
|
+
head.sort((a, b) => {
|
|
4282
|
+
if (b.boostedScore !== a.boostedScore) return b.boostedScore - a.boostedScore;
|
|
4283
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4284
|
+
if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
|
|
4285
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4286
|
+
});
|
|
4287
|
+
if (preferSourcePaths) {
|
|
4288
|
+
head.sort((a, b) => {
|
|
4289
|
+
const aId = a.hasIdentifierMatch ? 1 : 0;
|
|
4290
|
+
const bId = b.hasIdentifierMatch ? 1 : 0;
|
|
4291
|
+
if (aId !== bId) return bId - aId;
|
|
4292
|
+
const aImpl = a.implementationChunk ? 1 : 0;
|
|
4293
|
+
const bImpl = b.implementationChunk ? 1 : 0;
|
|
4294
|
+
if (aImpl !== bImpl) return bImpl - aImpl;
|
|
4295
|
+
const aImplementationPath = a.isLikelyImplementationPath ? 1 : 0;
|
|
4296
|
+
const bImplementationPath = b.isLikelyImplementationPath ? 1 : 0;
|
|
4297
|
+
if (aImplementationPath !== bImplementationPath) return bImplementationPath - aImplementationPath;
|
|
4298
|
+
const aTestDoc = a.isTestOrDocPath ? 1 : 0;
|
|
4299
|
+
const bTestDoc = b.isTestOrDocPath ? 1 : 0;
|
|
4300
|
+
if (aTestDoc !== bTestDoc) return aTestDoc - bTestDoc;
|
|
4301
|
+
return 0;
|
|
4302
|
+
});
|
|
4303
|
+
} else if (docIntent === "docs") {
|
|
4304
|
+
head.sort((a, b) => {
|
|
4305
|
+
const aReadme = a.isReadmePath ? 1 : 0;
|
|
4306
|
+
const bReadme = b.isReadmePath ? 1 : 0;
|
|
4307
|
+
if (aReadme !== bReadme) return bReadme - aReadme;
|
|
4308
|
+
return 0;
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
const tail = candidates.slice(topN);
|
|
4312
|
+
return [...head.map((entry) => entry.candidate), ...tail];
|
|
4313
|
+
}
|
|
4314
|
+
function rankHybridResults(query, semanticResults, keywordResults, options) {
|
|
4315
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4316
|
+
const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
|
|
4317
|
+
const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
|
|
4318
|
+
const rerankPool = fused.slice(0, rerankPoolLimit);
|
|
4319
|
+
return rerankResults(query, rerankPool, options.rerankTopN, {
|
|
4320
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source"
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
function rankSemanticOnlyResults(query, semanticResults, options) {
|
|
4324
|
+
const overfetchLimit = Math.max(options.limit * 4, options.limit);
|
|
4325
|
+
const bounded = semanticResults.slice(0, overfetchLimit);
|
|
4326
|
+
return rerankResults(query, bounded, options.rerankTopN, {
|
|
4327
|
+
prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
|
|
4328
|
+
});
|
|
4329
|
+
}
|
|
4330
|
+
function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4331
|
+
if (combined.length === 0) {
|
|
4332
|
+
return combined;
|
|
4333
|
+
}
|
|
4334
|
+
if (!prioritizeSourcePaths) {
|
|
4335
|
+
return combined;
|
|
4336
|
+
}
|
|
4337
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4338
|
+
if (identifierHints.length === 0) {
|
|
4339
|
+
return combined;
|
|
4340
|
+
}
|
|
4341
|
+
const combinedById = new Map(combined.map((candidate) => [candidate.id, candidate]));
|
|
4342
|
+
const candidateUnion = /* @__PURE__ */ new Map();
|
|
4343
|
+
for (const candidate of semanticCandidates) {
|
|
4344
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4345
|
+
}
|
|
4346
|
+
for (const candidate of keywordCandidates) {
|
|
4347
|
+
if (!candidateUnion.has(candidate.id)) {
|
|
4348
|
+
candidateUnion.set(candidate.id, candidate);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (database) {
|
|
4352
|
+
for (const identifier of identifierHints) {
|
|
4353
|
+
const symbols = database.getSymbolsByName(identifier);
|
|
4354
|
+
for (const symbol of symbols) {
|
|
4355
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4356
|
+
for (const chunk of chunks) {
|
|
4357
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4361
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4370
|
+
const existing = combinedById.get(chunk.chunkId) ?? candidateUnion.get(chunk.chunkId);
|
|
4371
|
+
const metadata = existing?.metadata ?? {
|
|
4372
|
+
filePath: chunk.filePath,
|
|
4373
|
+
startLine: chunk.startLine,
|
|
4374
|
+
endLine: chunk.endLine,
|
|
4375
|
+
chunkType,
|
|
4376
|
+
name: chunk.name ?? void 0,
|
|
4377
|
+
language: chunk.language,
|
|
4378
|
+
hash: chunk.contentHash
|
|
4379
|
+
};
|
|
4380
|
+
const baselineScore = existing?.score ?? 0.5;
|
|
4381
|
+
candidateUnion.set(chunk.chunkId, {
|
|
4382
|
+
id: chunk.chunkId,
|
|
4383
|
+
score: Math.min(1, baselineScore + 0.5),
|
|
4384
|
+
metadata
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
const promoted = [];
|
|
4391
|
+
for (const candidate of candidateUnion.values()) {
|
|
4392
|
+
const filePathLower = candidate.metadata.filePath.toLowerCase();
|
|
4393
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4394
|
+
const exactIdentifierMatch = identifierHints.some((hint) => nameLower === hint);
|
|
4395
|
+
const hasIdentifierMatch = exactIdentifierMatch || identifierHints.some(
|
|
4396
|
+
(hint) => nameLower.includes(hint) || filePathLower.includes(hint)
|
|
4397
|
+
);
|
|
4398
|
+
if (!hasIdentifierMatch) {
|
|
4399
|
+
continue;
|
|
4400
|
+
}
|
|
4401
|
+
if (!isImplementationChunkType(candidate.metadata.chunkType)) {
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
if (!isLikelyImplementationPath(candidate.metadata.filePath)) {
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
const existing = combinedById.get(candidate.id) ?? candidate;
|
|
4408
|
+
const rescueBoost = exactIdentifierMatch ? 0.45 : 0.25;
|
|
4409
|
+
const boostedScore = Math.min(1, Math.max(existing.score, candidate.score) + rescueBoost);
|
|
4410
|
+
promoted.push({
|
|
4411
|
+
id: existing.id,
|
|
4412
|
+
score: boostedScore,
|
|
4413
|
+
metadata: existing.metadata
|
|
4414
|
+
});
|
|
4415
|
+
}
|
|
4416
|
+
if (promoted.length === 0) {
|
|
4417
|
+
return combined;
|
|
4418
|
+
}
|
|
4419
|
+
promoted.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4420
|
+
const promotedIds = new Set(promoted.map((candidate) => candidate.id));
|
|
4421
|
+
const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
|
|
4422
|
+
return [...promoted, ...remainder];
|
|
4423
|
+
}
|
|
4424
|
+
function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4425
|
+
if (!prioritizeSourcePaths) {
|
|
4426
|
+
return [];
|
|
4427
|
+
}
|
|
4428
|
+
const identifierHints = extractIdentifierHints(query);
|
|
4429
|
+
const codeTermHints = extractCodeTermHints(query);
|
|
4430
|
+
if (identifierHints.length === 0 && codeTermHints.length === 0) {
|
|
4431
|
+
return [];
|
|
4432
|
+
}
|
|
4433
|
+
const symbolCandidates = /* @__PURE__ */ new Map();
|
|
4434
|
+
const filePathHint = extractFilePathHint(query);
|
|
4435
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4436
|
+
const upsertChunkCandidate = (chunk, identifier, normalizedIdentifier, baseScore) => {
|
|
4437
|
+
if (branchChunkIds && !branchChunkIds.has(chunk.chunkId)) {
|
|
4438
|
+
return;
|
|
4439
|
+
}
|
|
4440
|
+
const chunkType = chunk.nodeType ?? "other";
|
|
4441
|
+
if (!isImplementationChunkType(chunkType)) {
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
if (!isLikelyImplementationPath(chunk.filePath)) {
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4447
|
+
const nameLower = (chunk.name ?? "").toLowerCase();
|
|
4448
|
+
const exactName = nameLower === identifier || nameLower.replace(/_/g, "") === normalizedIdentifier;
|
|
4449
|
+
const base = baseScore ?? (exactName ? 0.99 : 0.88);
|
|
4450
|
+
const existing = symbolCandidates.get(chunk.chunkId);
|
|
4451
|
+
if (!existing || base > existing.score) {
|
|
4452
|
+
symbolCandidates.set(chunk.chunkId, {
|
|
4453
|
+
id: chunk.chunkId,
|
|
4454
|
+
score: base,
|
|
4455
|
+
metadata: {
|
|
4456
|
+
filePath: chunk.filePath,
|
|
4457
|
+
startLine: chunk.startLine,
|
|
4458
|
+
endLine: chunk.endLine,
|
|
4459
|
+
chunkType,
|
|
4460
|
+
name: chunk.name ?? void 0,
|
|
4461
|
+
language: chunk.language,
|
|
4462
|
+
hash: chunk.contentHash
|
|
4463
|
+
}
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
const normalizedHints = identifierHints.flatMap((hint) => [
|
|
4468
|
+
hint,
|
|
4469
|
+
hint.replace(/_/g, ""),
|
|
4470
|
+
hint.replace(/_/g, "-")
|
|
4471
|
+
]).filter((hint, idx, arr) => hint.length >= 3 && arr.indexOf(hint) === idx).slice(0, 6);
|
|
4472
|
+
for (const identifier of normalizedHints) {
|
|
4473
|
+
const symbols = [
|
|
4474
|
+
...database.getSymbolsByName(identifier),
|
|
4475
|
+
...database.getSymbolsByNameCi(identifier)
|
|
4476
|
+
];
|
|
4477
|
+
const chunksByName = [
|
|
4478
|
+
...database.getChunksByName(identifier),
|
|
4479
|
+
...database.getChunksByNameCi(identifier)
|
|
4480
|
+
];
|
|
4481
|
+
const normalizedIdentifier = identifier.replace(/_/g, "");
|
|
4482
|
+
const dedupSymbols = /* @__PURE__ */ new Map();
|
|
4483
|
+
for (const symbol of symbols) {
|
|
4484
|
+
dedupSymbols.set(symbol.id, symbol);
|
|
4485
|
+
}
|
|
4486
|
+
for (const symbol of dedupSymbols.values()) {
|
|
4487
|
+
const chunks = database.getChunksByFile(symbol.filePath);
|
|
4488
|
+
for (const chunk of chunks) {
|
|
4489
|
+
if (chunk.startLine > symbol.startLine || chunk.endLine < symbol.endLine) {
|
|
4490
|
+
continue;
|
|
4491
|
+
}
|
|
4492
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
const dedupChunksByName = /* @__PURE__ */ new Map();
|
|
4496
|
+
for (const chunk of chunksByName) {
|
|
4497
|
+
dedupChunksByName.set(chunk.chunkId, chunk);
|
|
4498
|
+
}
|
|
4499
|
+
for (const chunk of dedupChunksByName.values()) {
|
|
4500
|
+
upsertChunkCandidate(chunk, identifier, normalizedIdentifier);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
if (filePathHint && primaryHint) {
|
|
4504
|
+
const primaryChunks = [
|
|
4505
|
+
...database.getChunksByName(primaryHint),
|
|
4506
|
+
...database.getChunksByNameCi(primaryHint)
|
|
4507
|
+
];
|
|
4508
|
+
const dedupPrimaryChunks = /* @__PURE__ */ new Map();
|
|
4509
|
+
for (const chunk of primaryChunks) {
|
|
4510
|
+
dedupPrimaryChunks.set(chunk.chunkId, chunk);
|
|
4511
|
+
}
|
|
4512
|
+
for (const chunk of dedupPrimaryChunks.values()) {
|
|
4513
|
+
if (!pathMatchesHint(chunk.filePath, filePathHint)) {
|
|
4514
|
+
continue;
|
|
4515
|
+
}
|
|
4516
|
+
const normalizedPrimary = primaryHint.replace(/_/g, "");
|
|
4517
|
+
upsertChunkCandidate(chunk, primaryHint, normalizedPrimary, 1);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const ranked = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4521
|
+
if (ranked.length === 0) {
|
|
4522
|
+
const implementationFallback = fallbackCandidates.filter(
|
|
4523
|
+
(candidate) => isImplementationChunkType(candidate.metadata.chunkType) && isLikelyImplementationPath(candidate.metadata.filePath)
|
|
4524
|
+
);
|
|
4525
|
+
for (const candidate of implementationFallback) {
|
|
4526
|
+
const nameLower = (candidate.metadata.name ?? "").toLowerCase();
|
|
4527
|
+
const pathLower = candidate.metadata.filePath.toLowerCase();
|
|
4528
|
+
const exactHintMatch = normalizedHints.some((hint) => nameLower === hint || nameLower.replace(/_/g, "") === hint.replace(/_/g, ""));
|
|
4529
|
+
const tokenizedName = tokenizeTextForRanking(nameLower);
|
|
4530
|
+
const tokenHits = codeTermHints.filter((term) => tokenizedName.has(term) || pathLower.includes(term)).length;
|
|
4531
|
+
if (!exactHintMatch && tokenHits === 0) {
|
|
4532
|
+
continue;
|
|
4533
|
+
}
|
|
4534
|
+
const laneScore = exactHintMatch ? Math.min(1, Math.max(candidate.score, 0.97)) : Math.min(0.95, Math.max(candidate.score, 0.82 + tokenHits * 0.03));
|
|
4535
|
+
symbolCandidates.set(candidate.id, {
|
|
4536
|
+
id: candidate.id,
|
|
4537
|
+
score: laneScore,
|
|
4538
|
+
metadata: candidate.metadata
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
if (symbolCandidates.size === 0) {
|
|
4542
|
+
const queryTokenSet = tokenizeTextForRanking(query);
|
|
4543
|
+
const rankedFallback = implementationFallback.map((candidate) => {
|
|
4544
|
+
const nameTokens = tokenizeTextForRanking(candidate.metadata.name ?? "");
|
|
4545
|
+
const pathTokens = splitPathTokens(candidate.metadata.filePath);
|
|
4546
|
+
let overlap = 0;
|
|
4547
|
+
for (const token of queryTokenSet) {
|
|
4548
|
+
if (nameTokens.has(token) || pathTokens.has(token)) {
|
|
4549
|
+
overlap += 1;
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
const overlapScore = queryTokenSet.size > 0 ? overlap / queryTokenSet.size : 0;
|
|
4553
|
+
return {
|
|
4554
|
+
candidate,
|
|
4555
|
+
overlapScore
|
|
4556
|
+
};
|
|
4557
|
+
}).filter((entry) => entry.overlapScore > 0).sort((a, b) => b.overlapScore - a.overlapScore || b.candidate.score - a.candidate.score).slice(0, Math.max(limit, 3));
|
|
4558
|
+
for (const entry of rankedFallback) {
|
|
4559
|
+
symbolCandidates.set(entry.candidate.id, {
|
|
4560
|
+
id: entry.candidate.id,
|
|
4561
|
+
score: Math.min(0.94, Math.max(entry.candidate.score, 0.8 + entry.overlapScore * 0.1)),
|
|
4562
|
+
metadata: entry.candidate.metadata
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4568
|
+
return withFallback.slice(0, Math.max(limit * 2, limit));
|
|
4569
|
+
}
|
|
4570
|
+
function buildIdentifierDefinitionLane(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
|
|
4571
|
+
if (!prioritizeSourcePaths) {
|
|
4572
|
+
return [];
|
|
4573
|
+
}
|
|
4574
|
+
const primaryHint = extractPrimaryIdentifierQueryHint(query);
|
|
4575
|
+
if (!primaryHint) {
|
|
4576
|
+
return [];
|
|
4577
|
+
}
|
|
4578
|
+
const hints = [primaryHint, ...extractIdentifierHints(query), ...extractCodeTermHints(query)].slice(0, 8);
|
|
4579
|
+
const scored = candidates.filter(
|
|
4580
|
+
(candidate) => isLikelyImplementationPath(candidate.metadata.filePath) && isImplementationChunkType(candidate.metadata.chunkType)
|
|
4581
|
+
).map((candidate) => {
|
|
4582
|
+
const matchScore = scoreIdentifierMatch(candidate.metadata.name, candidate.metadata.filePath, hints);
|
|
4583
|
+
return {
|
|
4584
|
+
candidate,
|
|
4585
|
+
matchScore
|
|
4586
|
+
};
|
|
4587
|
+
}).filter((entry) => entry.matchScore > 0).sort((a, b) => {
|
|
4588
|
+
if (b.matchScore !== a.matchScore) return b.matchScore - a.matchScore;
|
|
4589
|
+
if (b.candidate.score !== a.candidate.score) return b.candidate.score - a.candidate.score;
|
|
4590
|
+
return a.candidate.id.localeCompare(b.candidate.id);
|
|
4591
|
+
}).slice(0, Math.max(limit * 2, 10));
|
|
4592
|
+
return scored.map((entry) => ({
|
|
4593
|
+
id: entry.candidate.id,
|
|
4594
|
+
score: Math.min(1, 0.9 + entry.matchScore * 0.09),
|
|
4595
|
+
metadata: entry.candidate.metadata
|
|
4596
|
+
}));
|
|
4597
|
+
}
|
|
4598
|
+
function mergeTieredResults(symbolLane, hybridLane, limit) {
|
|
4599
|
+
if (symbolLane.length === 0) {
|
|
4600
|
+
return hybridLane.slice(0, limit);
|
|
4601
|
+
}
|
|
4602
|
+
const out = [];
|
|
4603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4604
|
+
for (const candidate of symbolLane) {
|
|
4605
|
+
if (seen.has(candidate.id)) continue;
|
|
4606
|
+
out.push(candidate);
|
|
4607
|
+
seen.add(candidate.id);
|
|
4608
|
+
if (out.length >= limit) return out;
|
|
4609
|
+
}
|
|
4610
|
+
for (const candidate of hybridLane) {
|
|
4611
|
+
if (seen.has(candidate.id)) continue;
|
|
4612
|
+
out.push(candidate);
|
|
4613
|
+
seen.add(candidate.id);
|
|
4614
|
+
if (out.length >= limit) return out;
|
|
4615
|
+
}
|
|
4616
|
+
return out;
|
|
4617
|
+
}
|
|
4618
|
+
function unionCandidates(semanticCandidates, keywordCandidates) {
|
|
4619
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4620
|
+
for (const candidate of semanticCandidates) {
|
|
4621
|
+
byId.set(candidate.id, candidate);
|
|
4622
|
+
}
|
|
4623
|
+
for (const candidate of keywordCandidates) {
|
|
4624
|
+
const existing = byId.get(candidate.id);
|
|
4625
|
+
if (!existing || candidate.score > existing.score) {
|
|
4626
|
+
byId.set(candidate.id, candidate);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return Array.from(byId.values());
|
|
4630
|
+
}
|
|
4631
|
+
var Indexer = class {
|
|
4632
|
+
config;
|
|
4633
|
+
projectRoot;
|
|
4634
|
+
indexPath;
|
|
4635
|
+
store = null;
|
|
4636
|
+
invertedIndex = null;
|
|
4637
|
+
database = null;
|
|
4638
|
+
provider = null;
|
|
4639
|
+
configuredProviderInfo = null;
|
|
4640
|
+
fileHashCache = /* @__PURE__ */ new Map();
|
|
4641
|
+
fileHashCachePath = "";
|
|
4642
|
+
failedBatchesPath = "";
|
|
4643
|
+
currentBranch = "default";
|
|
4644
|
+
baseBranch = "main";
|
|
4645
|
+
logger;
|
|
4646
|
+
queryEmbeddingCache = /* @__PURE__ */ new Map();
|
|
4647
|
+
maxQueryCacheSize = 100;
|
|
4648
|
+
queryCacheTtlMs = 5 * 60 * 1e3;
|
|
4649
|
+
querySimilarityThreshold = 0.85;
|
|
4650
|
+
indexCompatibility = null;
|
|
4651
|
+
indexingLockPath = "";
|
|
4652
|
+
constructor(projectRoot, config) {
|
|
4653
|
+
this.projectRoot = projectRoot;
|
|
4654
|
+
this.config = config;
|
|
4655
|
+
this.indexPath = this.getIndexPath();
|
|
4656
|
+
this.fileHashCachePath = path6.join(this.indexPath, "file-hashes.json");
|
|
4657
|
+
this.failedBatchesPath = path6.join(this.indexPath, "failed-batches.json");
|
|
4658
|
+
this.indexingLockPath = path6.join(this.indexPath, "indexing.lock");
|
|
4659
|
+
this.logger = initializeLogger(config.debug);
|
|
4660
|
+
}
|
|
4661
|
+
getIndexPath() {
|
|
4662
|
+
if (this.config.scope === "global") {
|
|
4663
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
4664
|
+
return path6.join(homeDir, ".opencode", "global-index");
|
|
4665
|
+
}
|
|
4666
|
+
return path6.join(this.projectRoot, ".opencode", "index");
|
|
4667
|
+
}
|
|
4668
|
+
loadFileHashCache() {
|
|
4669
|
+
try {
|
|
4670
|
+
if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
|
|
4671
|
+
const data = (0, import_fs5.readFileSync)(this.fileHashCachePath, "utf-8");
|
|
4672
|
+
const parsed = JSON.parse(data);
|
|
4673
|
+
this.fileHashCache = new Map(Object.entries(parsed));
|
|
4674
|
+
}
|
|
4675
|
+
} catch {
|
|
4676
|
+
this.fileHashCache = /* @__PURE__ */ new Map();
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
saveFileHashCache() {
|
|
4680
|
+
const obj = {};
|
|
4681
|
+
for (const [k, v] of this.fileHashCache) {
|
|
4682
|
+
obj[k] = v;
|
|
4683
|
+
}
|
|
4684
|
+
this.atomicWriteSync(this.fileHashCachePath, JSON.stringify(obj));
|
|
4685
|
+
}
|
|
4686
|
+
atomicWriteSync(targetPath, data) {
|
|
4687
|
+
const tempPath = `${targetPath}.tmp`;
|
|
4688
|
+
(0, import_fs5.writeFileSync)(tempPath, data);
|
|
4689
|
+
(0, import_fs5.renameSync)(tempPath, targetPath);
|
|
4690
|
+
}
|
|
4691
|
+
checkForInterruptedIndexing() {
|
|
4692
|
+
return (0, import_fs5.existsSync)(this.indexingLockPath);
|
|
4693
|
+
}
|
|
4694
|
+
acquireIndexingLock() {
|
|
4695
|
+
const lockData = {
|
|
4696
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4697
|
+
pid: process.pid
|
|
4698
|
+
};
|
|
4699
|
+
(0, import_fs5.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
|
|
4700
|
+
}
|
|
4701
|
+
releaseIndexingLock() {
|
|
4702
|
+
if ((0, import_fs5.existsSync)(this.indexingLockPath)) {
|
|
4703
|
+
(0, import_fs5.unlinkSync)(this.indexingLockPath);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
async recoverFromInterruptedIndexing() {
|
|
4707
|
+
this.logger.warn("Detected interrupted indexing session, recovering...");
|
|
4708
|
+
if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
|
|
4709
|
+
(0, import_fs5.unlinkSync)(this.fileHashCachePath);
|
|
4710
|
+
}
|
|
4711
|
+
await this.healthCheck();
|
|
4712
|
+
this.releaseIndexingLock();
|
|
4713
|
+
this.logger.info("Recovery complete, next index will re-process all files");
|
|
4714
|
+
}
|
|
4715
|
+
loadFailedBatches() {
|
|
4716
|
+
try {
|
|
4717
|
+
if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
|
|
4718
|
+
const data = (0, import_fs5.readFileSync)(this.failedBatchesPath, "utf-8");
|
|
4719
|
+
return JSON.parse(data);
|
|
4720
|
+
}
|
|
4721
|
+
} catch {
|
|
4722
|
+
return [];
|
|
4723
|
+
}
|
|
4724
|
+
return [];
|
|
4725
|
+
}
|
|
4726
|
+
saveFailedBatches(batches) {
|
|
4727
|
+
if (batches.length === 0) {
|
|
4728
|
+
if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
|
|
4729
|
+
import_fs5.promises.unlink(this.failedBatchesPath).catch(() => {
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
(0, import_fs5.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
|
|
4735
|
+
}
|
|
4736
|
+
addFailedBatch(batch, error) {
|
|
4737
|
+
const existing = this.loadFailedBatches();
|
|
4738
|
+
existing.push({
|
|
4739
|
+
chunks: batch,
|
|
4740
|
+
error,
|
|
4741
|
+
attemptCount: 1,
|
|
3543
4742
|
lastAttempt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3544
4743
|
});
|
|
3545
4744
|
this.saveFailedBatches(existing);
|
|
@@ -3589,26 +4788,26 @@ var Indexer = class {
|
|
|
3589
4788
|
scope: this.config.scope
|
|
3590
4789
|
});
|
|
3591
4790
|
this.provider = createEmbeddingProvider(this.configuredProviderInfo);
|
|
3592
|
-
await
|
|
4791
|
+
await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
|
|
3593
4792
|
const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
|
|
3594
|
-
const storePath =
|
|
4793
|
+
const storePath = path6.join(this.indexPath, "vectors");
|
|
3595
4794
|
this.store = new VectorStore(storePath, dimensions);
|
|
3596
|
-
const indexFilePath =
|
|
3597
|
-
if ((0,
|
|
4795
|
+
const indexFilePath = path6.join(this.indexPath, "vectors.usearch");
|
|
4796
|
+
if ((0, import_fs5.existsSync)(indexFilePath)) {
|
|
3598
4797
|
this.store.load();
|
|
3599
4798
|
}
|
|
3600
|
-
const invertedIndexPath =
|
|
4799
|
+
const invertedIndexPath = path6.join(this.indexPath, "inverted-index.json");
|
|
3601
4800
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
3602
4801
|
try {
|
|
3603
4802
|
this.invertedIndex.load();
|
|
3604
4803
|
} catch {
|
|
3605
|
-
if ((0,
|
|
3606
|
-
await
|
|
4804
|
+
if ((0, import_fs5.existsSync)(invertedIndexPath)) {
|
|
4805
|
+
await import_fs5.promises.unlink(invertedIndexPath);
|
|
3607
4806
|
}
|
|
3608
4807
|
this.invertedIndex = new InvertedIndex(invertedIndexPath);
|
|
3609
4808
|
}
|
|
3610
|
-
const dbPath =
|
|
3611
|
-
const dbIsNew = !(0,
|
|
4809
|
+
const dbPath = path6.join(this.indexPath, "codebase.db");
|
|
4810
|
+
const dbIsNew = !(0, import_fs5.existsSync)(dbPath);
|
|
3612
4811
|
this.database = new Database(dbPath);
|
|
3613
4812
|
if (this.checkForInterruptedIndexing()) {
|
|
3614
4813
|
await this.recoverFromInterruptedIndexing();
|
|
@@ -3840,7 +5039,7 @@ var Indexer = class {
|
|
|
3840
5039
|
unchangedFilePaths.add(f.path);
|
|
3841
5040
|
this.logger.recordCacheHit();
|
|
3842
5041
|
} else {
|
|
3843
|
-
const content = await
|
|
5042
|
+
const content = await import_fs5.promises.readFile(f.path, "utf-8");
|
|
3844
5043
|
changedFiles.push({ path: f.path, content, hash: currentHash });
|
|
3845
5044
|
this.logger.recordCacheMiss();
|
|
3846
5045
|
}
|
|
@@ -3886,7 +5085,7 @@ var Indexer = class {
|
|
|
3886
5085
|
for (const parsed of parsedFiles) {
|
|
3887
5086
|
currentFilePaths.add(parsed.path);
|
|
3888
5087
|
if (parsed.chunks.length === 0) {
|
|
3889
|
-
const relativePath =
|
|
5088
|
+
const relativePath = path6.relative(this.projectRoot, parsed.path);
|
|
3890
5089
|
stats.parseFailures.push(relativePath);
|
|
3891
5090
|
}
|
|
3892
5091
|
let fileChunkCount = 0;
|
|
@@ -3931,6 +5130,79 @@ var Indexer = class {
|
|
|
3931
5130
|
if (chunkDataBatch.length > 0) {
|
|
3932
5131
|
database.upsertChunksBatch(chunkDataBatch);
|
|
3933
5132
|
}
|
|
5133
|
+
const allSymbolIds = /* @__PURE__ */ new Set();
|
|
5134
|
+
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
5135
|
+
for (let i = 0; i < parsedFiles.length; i++) {
|
|
5136
|
+
const parsed = parsedFiles[i];
|
|
5137
|
+
const changedFile = changedFiles[i];
|
|
5138
|
+
database.deleteCallEdgesByFile(parsed.path);
|
|
5139
|
+
database.deleteSymbolsByFile(parsed.path);
|
|
5140
|
+
const fileSymbols = [];
|
|
5141
|
+
for (const chunk of parsed.chunks) {
|
|
5142
|
+
if (!chunk.name || !CALL_GRAPH_SYMBOL_CHUNK_TYPES.has(chunk.chunkType)) continue;
|
|
5143
|
+
const symbolId = `sym_${hashContent(parsed.path + ":" + chunk.name + ":" + chunk.chunkType + ":" + chunk.startLine).slice(0, 16)}`;
|
|
5144
|
+
const symbol = {
|
|
5145
|
+
id: symbolId,
|
|
5146
|
+
filePath: parsed.path,
|
|
5147
|
+
name: chunk.name,
|
|
5148
|
+
kind: chunk.chunkType,
|
|
5149
|
+
startLine: chunk.startLine,
|
|
5150
|
+
startCol: 0,
|
|
5151
|
+
endLine: chunk.endLine,
|
|
5152
|
+
endCol: 0,
|
|
5153
|
+
language: chunk.language
|
|
5154
|
+
};
|
|
5155
|
+
fileSymbols.push(symbol);
|
|
5156
|
+
allSymbolIds.add(symbolId);
|
|
5157
|
+
}
|
|
5158
|
+
const symbolsByName = /* @__PURE__ */ new Map();
|
|
5159
|
+
for (const symbol of fileSymbols) {
|
|
5160
|
+
const existing = symbolsByName.get(symbol.name) ?? [];
|
|
5161
|
+
existing.push(symbol);
|
|
5162
|
+
symbolsByName.set(symbol.name, existing);
|
|
5163
|
+
}
|
|
5164
|
+
if (fileSymbols.length > 0) {
|
|
5165
|
+
database.upsertSymbolsBatch(fileSymbols);
|
|
5166
|
+
symbolsByFile.set(parsed.path, fileSymbols);
|
|
5167
|
+
}
|
|
5168
|
+
const fileLanguage = parsed.chunks[0]?.language;
|
|
5169
|
+
if (!fileLanguage || !CALL_GRAPH_LANGUAGES.has(fileLanguage)) continue;
|
|
5170
|
+
const callSites = extractCalls(changedFile.content, fileLanguage);
|
|
5171
|
+
if (callSites.length === 0) continue;
|
|
5172
|
+
const edges = [];
|
|
5173
|
+
for (const site of callSites) {
|
|
5174
|
+
const enclosingSymbol = fileSymbols.find(
|
|
5175
|
+
(sym) => site.line >= sym.startLine && site.line <= sym.endLine
|
|
5176
|
+
);
|
|
5177
|
+
if (!enclosingSymbol) continue;
|
|
5178
|
+
const edgeId = `edge_${hashContent(enclosingSymbol.id + ":" + site.calleeName + ":" + site.line + ":" + site.column).slice(0, 16)}`;
|
|
5179
|
+
edges.push({
|
|
5180
|
+
id: edgeId,
|
|
5181
|
+
fromSymbolId: enclosingSymbol.id,
|
|
5182
|
+
targetName: site.calleeName,
|
|
5183
|
+
toSymbolId: void 0,
|
|
5184
|
+
callType: site.callType,
|
|
5185
|
+
line: site.line,
|
|
5186
|
+
col: site.column,
|
|
5187
|
+
isResolved: false
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
if (edges.length > 0) {
|
|
5191
|
+
database.upsertCallEdgesBatch(edges);
|
|
5192
|
+
for (const edge of edges) {
|
|
5193
|
+
const candidates = symbolsByName.get(edge.targetName);
|
|
5194
|
+
if (candidates && candidates.length === 1) {
|
|
5195
|
+
database.resolveCallEdge(edge.id, candidates[0].id);
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
for (const filePath of unchangedFilePaths) {
|
|
5201
|
+
const existingSymbols = database.getSymbolsByFile(filePath);
|
|
5202
|
+
for (const sym of existingSymbols) {
|
|
5203
|
+
allSymbolIds.add(sym.id);
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
3934
5206
|
let removedCount = 0;
|
|
3935
5207
|
for (const [chunkId] of existingChunks) {
|
|
3936
5208
|
if (!currentChunkIds.has(chunkId)) {
|
|
@@ -3952,6 +5224,8 @@ var Indexer = class {
|
|
|
3952
5224
|
if (pendingChunks.length === 0 && removedCount === 0) {
|
|
3953
5225
|
database.clearBranch(this.currentBranch);
|
|
3954
5226
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5227
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5228
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3955
5229
|
this.fileHashCache = currentFileHashes;
|
|
3956
5230
|
this.saveFileHashCache();
|
|
3957
5231
|
stats.durationMs = Date.now() - startTime;
|
|
@@ -3968,6 +5242,8 @@ var Indexer = class {
|
|
|
3968
5242
|
if (pendingChunks.length === 0) {
|
|
3969
5243
|
database.clearBranch(this.currentBranch);
|
|
3970
5244
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5245
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5246
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
3971
5247
|
store.save();
|
|
3972
5248
|
invertedIndex.save();
|
|
3973
5249
|
this.fileHashCache = currentFileHashes;
|
|
@@ -4020,7 +5296,7 @@ var Indexer = class {
|
|
|
4020
5296
|
for (const batch of dynamicBatches) {
|
|
4021
5297
|
queue.add(async () => {
|
|
4022
5298
|
if (rateLimitBackoffMs > 0) {
|
|
4023
|
-
await new Promise((
|
|
5299
|
+
await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
|
|
4024
5300
|
}
|
|
4025
5301
|
try {
|
|
4026
5302
|
const result = await pRetry(
|
|
@@ -4108,6 +5384,8 @@ var Indexer = class {
|
|
|
4108
5384
|
});
|
|
4109
5385
|
database.clearBranch(this.currentBranch);
|
|
4110
5386
|
database.addChunksToBranchBatch(this.currentBranch, Array.from(currentChunkIds));
|
|
5387
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
5388
|
+
database.addSymbolsToBranchBatch(this.currentBranch, Array.from(allSymbolIds));
|
|
4111
5389
|
store.save();
|
|
4112
5390
|
invertedIndex.save();
|
|
4113
5391
|
this.fileHashCache = currentFileHashes;
|
|
@@ -4218,15 +5496,23 @@ var Indexer = class {
|
|
|
4218
5496
|
}
|
|
4219
5497
|
const maxResults = limit ?? this.config.search.maxResults;
|
|
4220
5498
|
const hybridWeight = options?.hybridWeight ?? this.config.search.hybridWeight;
|
|
5499
|
+
const fusionStrategy = this.config.search.fusionStrategy;
|
|
5500
|
+
const rrfK = this.config.search.rrfK;
|
|
5501
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
4221
5502
|
const filterByBranch = options?.filterByBranch ?? true;
|
|
5503
|
+
const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
|
|
4222
5504
|
this.logger.search("debug", "Starting search", {
|
|
4223
5505
|
query,
|
|
4224
5506
|
maxResults,
|
|
4225
5507
|
hybridWeight,
|
|
5508
|
+
fusionStrategy,
|
|
5509
|
+
rrfK,
|
|
5510
|
+
rerankTopN,
|
|
4226
5511
|
filterByBranch
|
|
4227
5512
|
});
|
|
4228
5513
|
const embeddingStartTime = import_perf_hooks.performance.now();
|
|
4229
|
-
const
|
|
5514
|
+
const embeddingQuery = stripFilePathHint(query);
|
|
5515
|
+
const embedding = await this.getQueryEmbedding(embeddingQuery, provider);
|
|
4230
5516
|
const embeddingMs = import_perf_hooks.performance.now() - embeddingStartTime;
|
|
4231
5517
|
const vectorStartTime = import_perf_hooks.performance.now();
|
|
4232
5518
|
const semanticResults = store.search(embedding, maxResults * 4);
|
|
@@ -4234,16 +5520,78 @@ var Indexer = class {
|
|
|
4234
5520
|
const keywordStartTime = import_perf_hooks.performance.now();
|
|
4235
5521
|
const keywordResults = await this.keywordSearch(query, maxResults * 4);
|
|
4236
5522
|
const keywordMs = import_perf_hooks.performance.now() - keywordStartTime;
|
|
4237
|
-
const fusionStartTime = import_perf_hooks.performance.now();
|
|
4238
|
-
const combined = this.fuseResults(semanticResults, keywordResults, hybridWeight, maxResults * 4);
|
|
4239
|
-
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
4240
5523
|
let branchChunkIds = null;
|
|
4241
5524
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4242
5525
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4243
5526
|
}
|
|
4244
|
-
const
|
|
5527
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5528
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5529
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5530
|
+
const prefilteredKeyword = shouldPrefilterByBranch && branchChunkIds ? keywordResults.filter((r) => branchChunkIds.has(r.id)) : keywordResults;
|
|
5531
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5532
|
+
const keywordCandidates = shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0 ? keywordResults : prefilteredKeyword;
|
|
5533
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5534
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5535
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5536
|
+
branch: this.currentBranch
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5540
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5541
|
+
branch: this.currentBranch
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
if (shouldPrefilterByBranch && keywordResults.length > 0 && prefilteredKeyword.length === 0) {
|
|
5545
|
+
this.logger.search("warn", "Branch prefilter produced no keyword overlap, using unfiltered keyword candidates", {
|
|
5546
|
+
branch: this.currentBranch
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
const fusionStartTime = import_perf_hooks.performance.now();
|
|
5550
|
+
const combined = rankHybridResults(query, semanticCandidates, keywordCandidates, {
|
|
5551
|
+
fusionStrategy,
|
|
5552
|
+
rrfK,
|
|
5553
|
+
rerankTopN,
|
|
5554
|
+
limit: maxResults,
|
|
5555
|
+
hybridWeight,
|
|
5556
|
+
prioritizeSourcePaths: sourceIntent
|
|
5557
|
+
});
|
|
5558
|
+
const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
|
|
5559
|
+
const rescued = promoteIdentifierMatches(
|
|
5560
|
+
query,
|
|
5561
|
+
combined,
|
|
5562
|
+
semanticCandidates,
|
|
5563
|
+
keywordCandidates,
|
|
5564
|
+
database,
|
|
5565
|
+
branchChunkIds,
|
|
5566
|
+
sourceIntent
|
|
5567
|
+
);
|
|
5568
|
+
const union = unionCandidates(semanticCandidates, keywordCandidates);
|
|
5569
|
+
const deterministicIdentifierLane = buildDeterministicIdentifierPass(
|
|
5570
|
+
query,
|
|
5571
|
+
union,
|
|
5572
|
+
maxResults,
|
|
5573
|
+
sourceIntent
|
|
5574
|
+
);
|
|
5575
|
+
const identifierLane = buildIdentifierDefinitionLane(
|
|
5576
|
+
query,
|
|
5577
|
+
union,
|
|
5578
|
+
maxResults,
|
|
5579
|
+
sourceIntent
|
|
5580
|
+
);
|
|
5581
|
+
const symbolLane = buildSymbolDefinitionLane(
|
|
5582
|
+
query,
|
|
5583
|
+
database,
|
|
5584
|
+
branchChunkIds,
|
|
5585
|
+
maxResults,
|
|
5586
|
+
union,
|
|
5587
|
+
sourceIntent
|
|
5588
|
+
);
|
|
5589
|
+
const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
|
|
5590
|
+
const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
|
|
5591
|
+
const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
|
|
5592
|
+
const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
|
|
5593
|
+
const baseFiltered = tiered.filter((r) => {
|
|
4245
5594
|
if (r.score < this.config.search.minScore) return false;
|
|
4246
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4247
5595
|
if (options?.fileType) {
|
|
4248
5596
|
const ext = r.metadata.filePath.split(".").pop()?.toLowerCase();
|
|
4249
5597
|
if (ext !== options.fileType.toLowerCase().replace(/^\./, "")) return false;
|
|
@@ -4256,7 +5604,11 @@ var Indexer = class {
|
|
|
4256
5604
|
if (r.metadata.chunkType !== options.chunkType) return false;
|
|
4257
5605
|
}
|
|
4258
5606
|
return true;
|
|
4259
|
-
})
|
|
5607
|
+
});
|
|
5608
|
+
const implementationOnly = baseFiltered.filter(
|
|
5609
|
+
(r) => isLikelyImplementationPath(r.metadata.filePath) && isImplementationChunkType(r.metadata.chunkType)
|
|
5610
|
+
);
|
|
5611
|
+
const filtered = (sourceIntent && hasCodeHints && implementationOnly.length > 0 ? implementationOnly : baseFiltered).slice(0, maxResults);
|
|
4260
5612
|
const totalSearchMs = import_perf_hooks.performance.now() - searchStartTime;
|
|
4261
5613
|
this.logger.recordSearch(totalSearchMs, {
|
|
4262
5614
|
embeddingMs,
|
|
@@ -4271,6 +5623,7 @@ var Indexer = class {
|
|
|
4271
5623
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4272
5624
|
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
4273
5625
|
keywordMs: Math.round(keywordMs * 100) / 100,
|
|
5626
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100,
|
|
4274
5627
|
fusionMs: Math.round(fusionMs * 100) / 100
|
|
4275
5628
|
});
|
|
4276
5629
|
const metadataOnly = options?.metadataOnly ?? false;
|
|
@@ -4281,7 +5634,7 @@ var Indexer = class {
|
|
|
4281
5634
|
let contextEndLine = r.metadata.endLine;
|
|
4282
5635
|
if (!metadataOnly && this.config.search.includeContext) {
|
|
4283
5636
|
try {
|
|
4284
|
-
const fileContent = await
|
|
5637
|
+
const fileContent = await import_fs5.promises.readFile(
|
|
4285
5638
|
r.metadata.filePath,
|
|
4286
5639
|
"utf-8"
|
|
4287
5640
|
);
|
|
@@ -4324,34 +5677,6 @@ var Indexer = class {
|
|
|
4324
5677
|
results.sort((a, b) => b.score - a.score);
|
|
4325
5678
|
return results.slice(0, limit);
|
|
4326
5679
|
}
|
|
4327
|
-
fuseResults(semanticResults, keywordResults, keywordWeight, limit) {
|
|
4328
|
-
const semanticWeight = 1 - keywordWeight;
|
|
4329
|
-
const fusedScores = /* @__PURE__ */ new Map();
|
|
4330
|
-
for (const r of semanticResults) {
|
|
4331
|
-
fusedScores.set(r.id, {
|
|
4332
|
-
score: r.score * semanticWeight,
|
|
4333
|
-
metadata: r.metadata
|
|
4334
|
-
});
|
|
4335
|
-
}
|
|
4336
|
-
for (const r of keywordResults) {
|
|
4337
|
-
const existing = fusedScores.get(r.id);
|
|
4338
|
-
if (existing) {
|
|
4339
|
-
existing.score += r.score * keywordWeight;
|
|
4340
|
-
} else {
|
|
4341
|
-
fusedScores.set(r.id, {
|
|
4342
|
-
score: r.score * keywordWeight,
|
|
4343
|
-
metadata: r.metadata
|
|
4344
|
-
});
|
|
4345
|
-
}
|
|
4346
|
-
}
|
|
4347
|
-
const results = Array.from(fusedScores.entries()).map(([id, data]) => ({
|
|
4348
|
-
id,
|
|
4349
|
-
score: data.score,
|
|
4350
|
-
metadata: data.metadata
|
|
4351
|
-
}));
|
|
4352
|
-
results.sort((a, b) => b.score - a.score);
|
|
4353
|
-
return results.slice(0, limit);
|
|
4354
|
-
}
|
|
4355
5680
|
async getStatus() {
|
|
4356
5681
|
const { store, configuredProviderInfo } = await this.ensureInitialized();
|
|
4357
5682
|
return {
|
|
@@ -4374,6 +5699,7 @@ var Indexer = class {
|
|
|
4374
5699
|
this.fileHashCache.clear();
|
|
4375
5700
|
this.saveFileHashCache();
|
|
4376
5701
|
database.clearBranch(this.currentBranch);
|
|
5702
|
+
database.clearBranchSymbols(this.currentBranch);
|
|
4377
5703
|
database.deleteMetadata("index.version");
|
|
4378
5704
|
database.deleteMetadata("index.embeddingProvider");
|
|
4379
5705
|
database.deleteMetadata("index.embeddingModel");
|
|
@@ -4395,13 +5721,15 @@ var Indexer = class {
|
|
|
4395
5721
|
const removedFilePaths = [];
|
|
4396
5722
|
let removedCount = 0;
|
|
4397
5723
|
for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
|
|
4398
|
-
if (!(0,
|
|
5724
|
+
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
4399
5725
|
for (const key of chunkKeys) {
|
|
4400
5726
|
store.remove(key);
|
|
4401
5727
|
invertedIndex.removeChunk(key);
|
|
4402
5728
|
removedCount++;
|
|
4403
5729
|
}
|
|
4404
5730
|
database.deleteChunksByFile(filePath);
|
|
5731
|
+
database.deleteCallEdgesByFile(filePath);
|
|
5732
|
+
database.deleteSymbolsByFile(filePath);
|
|
4405
5733
|
removedFilePaths.push(filePath);
|
|
4406
5734
|
}
|
|
4407
5735
|
}
|
|
@@ -4411,6 +5739,8 @@ var Indexer = class {
|
|
|
4411
5739
|
}
|
|
4412
5740
|
const gcOrphanEmbeddings = database.gcOrphanEmbeddings();
|
|
4413
5741
|
const gcOrphanChunks = database.gcOrphanChunks();
|
|
5742
|
+
const gcOrphanSymbols = database.gcOrphanSymbols();
|
|
5743
|
+
const gcOrphanCallEdges = database.gcOrphanCallEdges();
|
|
4414
5744
|
this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
|
|
4415
5745
|
this.logger.gc("info", "Health check complete", {
|
|
4416
5746
|
removedStale: removedCount,
|
|
@@ -4418,7 +5748,7 @@ var Indexer = class {
|
|
|
4418
5748
|
orphanChunks: gcOrphanChunks,
|
|
4419
5749
|
removedFiles: removedFilePaths.length
|
|
4420
5750
|
});
|
|
4421
|
-
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks };
|
|
5751
|
+
return { removed: removedCount, filePaths: removedFilePaths, gcOrphanEmbeddings, gcOrphanChunks, gcOrphanSymbols, gcOrphanCallEdges };
|
|
4422
5752
|
}
|
|
4423
5753
|
async retryFailedBatches() {
|
|
4424
5754
|
const { store, provider, invertedIndex } = await this.ensureInitialized();
|
|
@@ -4524,9 +5854,29 @@ var Indexer = class {
|
|
|
4524
5854
|
if (filterByBranch && this.currentBranch !== "default") {
|
|
4525
5855
|
branchChunkIds = new Set(database.getBranchChunkIds(this.currentBranch));
|
|
4526
5856
|
}
|
|
4527
|
-
const
|
|
5857
|
+
const prefilterStartTime = import_perf_hooks.performance.now();
|
|
5858
|
+
const shouldPrefilterByBranch = branchChunkIds !== null && branchChunkIds.size > 0;
|
|
5859
|
+
const prefilteredSemantic = shouldPrefilterByBranch && branchChunkIds ? semanticResults.filter((r) => branchChunkIds.has(r.id)) : semanticResults;
|
|
5860
|
+
const semanticCandidates = shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0 ? semanticResults : prefilteredSemantic;
|
|
5861
|
+
const prefilterMs = import_perf_hooks.performance.now() - prefilterStartTime;
|
|
5862
|
+
if (branchChunkIds && branchChunkIds.size === 0) {
|
|
5863
|
+
this.logger.search("warn", "Branch prefilter skipped because branch catalog is empty", {
|
|
5864
|
+
branch: this.currentBranch
|
|
5865
|
+
});
|
|
5866
|
+
}
|
|
5867
|
+
if (shouldPrefilterByBranch && semanticResults.length > 0 && prefilteredSemantic.length === 0) {
|
|
5868
|
+
this.logger.search("warn", "Branch prefilter produced no semantic overlap, using unfiltered semantic candidates", {
|
|
5869
|
+
branch: this.currentBranch
|
|
5870
|
+
});
|
|
5871
|
+
}
|
|
5872
|
+
const rerankTopN = this.config.search.rerankTopN;
|
|
5873
|
+
const ranked = rankSemanticOnlyResults(code, semanticCandidates, {
|
|
5874
|
+
rerankTopN,
|
|
5875
|
+
limit,
|
|
5876
|
+
prioritizeSourcePaths: false
|
|
5877
|
+
});
|
|
5878
|
+
const filtered = ranked.filter((r) => {
|
|
4528
5879
|
if (r.score < this.config.search.minScore) return false;
|
|
4529
|
-
if (branchChunkIds && !branchChunkIds.has(r.id)) return false;
|
|
4530
5880
|
if (options?.excludeFile) {
|
|
4531
5881
|
if (r.metadata.filePath === options.excludeFile) return false;
|
|
4532
5882
|
}
|
|
@@ -4555,14 +5905,15 @@ var Indexer = class {
|
|
|
4555
5905
|
results: filtered.length,
|
|
4556
5906
|
totalMs: Math.round(totalSearchMs * 100) / 100,
|
|
4557
5907
|
embeddingMs: Math.round(embeddingMs * 100) / 100,
|
|
4558
|
-
vectorMs: Math.round(vectorMs * 100) / 100
|
|
5908
|
+
vectorMs: Math.round(vectorMs * 100) / 100,
|
|
5909
|
+
prefilterMs: Math.round(prefilterMs * 100) / 100
|
|
4559
5910
|
});
|
|
4560
5911
|
return Promise.all(
|
|
4561
5912
|
filtered.map(async (r) => {
|
|
4562
5913
|
let content = "";
|
|
4563
5914
|
if (this.config.search.includeContext) {
|
|
4564
5915
|
try {
|
|
4565
|
-
const fileContent = await
|
|
5916
|
+
const fileContent = await import_fs5.promises.readFile(
|
|
4566
5917
|
r.metadata.filePath,
|
|
4567
5918
|
"utf-8"
|
|
4568
5919
|
);
|
|
@@ -4584,9 +5935,906 @@ var Indexer = class {
|
|
|
4584
5935
|
})
|
|
4585
5936
|
);
|
|
4586
5937
|
}
|
|
5938
|
+
async getCallers(targetName) {
|
|
5939
|
+
const { database } = await this.ensureInitialized();
|
|
5940
|
+
return database.getCallersWithContext(targetName, this.currentBranch);
|
|
5941
|
+
}
|
|
5942
|
+
async getCallees(symbolId) {
|
|
5943
|
+
const { database } = await this.ensureInitialized();
|
|
5944
|
+
return database.getCallees(symbolId, this.currentBranch);
|
|
5945
|
+
}
|
|
4587
5946
|
};
|
|
4588
5947
|
|
|
5948
|
+
// src/eval/budget.ts
|
|
5949
|
+
function evaluateBudgetGate(budget, summary, comparison) {
|
|
5950
|
+
const BASELINE_P95_EPSILON_MS = 1e-3;
|
|
5951
|
+
const violations = [];
|
|
5952
|
+
const { thresholds } = budget;
|
|
5953
|
+
if (thresholds.minHitAt5 !== void 0 && summary.metrics.hitAt5 < thresholds.minHitAt5) {
|
|
5954
|
+
violations.push({
|
|
5955
|
+
metric: "minHitAt5",
|
|
5956
|
+
message: `Hit@5 ${summary.metrics.hitAt5.toFixed(4)} is below minimum ${thresholds.minHitAt5.toFixed(4)}`
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
if (thresholds.minMrrAt10 !== void 0 && summary.metrics.mrrAt10 < thresholds.minMrrAt10) {
|
|
5960
|
+
violations.push({
|
|
5961
|
+
metric: "minMrrAt10",
|
|
5962
|
+
message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
if (comparison) {
|
|
5966
|
+
if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
|
|
5967
|
+
violations.push({
|
|
5968
|
+
metric: "hitAt5MaxDrop",
|
|
5969
|
+
message: `Hit@5 drop ${comparison.deltas.hitAt5.absolute.toFixed(4)} exceeds allowed -${thresholds.hitAt5MaxDrop.toFixed(4)}`
|
|
5970
|
+
});
|
|
5971
|
+
}
|
|
5972
|
+
if (thresholds.mrrAt10MaxDrop !== void 0 && comparison.deltas.mrrAt10.absolute < -thresholds.mrrAt10MaxDrop) {
|
|
5973
|
+
violations.push({
|
|
5974
|
+
metric: "mrrAt10MaxDrop",
|
|
5975
|
+
message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
|
|
5976
|
+
});
|
|
5977
|
+
}
|
|
5978
|
+
if (thresholds.p95LatencyMaxMultiplier !== void 0) {
|
|
5979
|
+
const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
|
|
5980
|
+
if (baselineP95 > BASELINE_P95_EPSILON_MS) {
|
|
5981
|
+
const allowed = baselineP95 * thresholds.p95LatencyMaxMultiplier;
|
|
5982
|
+
if (summary.metrics.latencyMs.p95 > allowed) {
|
|
5983
|
+
violations.push({
|
|
5984
|
+
metric: "p95LatencyMaxMultiplier",
|
|
5985
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds allowed ${allowed.toFixed(3)}ms (${thresholds.p95LatencyMaxMultiplier.toFixed(2)}x baseline)`
|
|
5986
|
+
});
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
if (thresholds.p95LatencyMaxAbsoluteMs !== void 0 && summary.metrics.latencyMs.p95 > thresholds.p95LatencyMaxAbsoluteMs) {
|
|
5992
|
+
violations.push({
|
|
5993
|
+
metric: "p95LatencyMaxAbsoluteMs",
|
|
5994
|
+
message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds absolute maximum ${thresholds.p95LatencyMaxAbsoluteMs.toFixed(3)}ms`
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
return {
|
|
5998
|
+
passed: violations.length === 0,
|
|
5999
|
+
budgetName: budget.name,
|
|
6000
|
+
violations
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
// src/eval/metrics.ts
|
|
6005
|
+
function percentile(values, p) {
|
|
6006
|
+
if (values.length === 0) return 0;
|
|
6007
|
+
if (values.length === 1) return values[0];
|
|
6008
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6009
|
+
const x = p * (sorted.length - 1);
|
|
6010
|
+
const lowerIndex = Math.floor(x);
|
|
6011
|
+
const upperIndex = Math.ceil(x);
|
|
6012
|
+
if (lowerIndex === upperIndex) {
|
|
6013
|
+
return sorted[lowerIndex];
|
|
6014
|
+
}
|
|
6015
|
+
const fraction = x - lowerIndex;
|
|
6016
|
+
return sorted[lowerIndex] + fraction * (sorted[upperIndex] - sorted[lowerIndex]);
|
|
6017
|
+
}
|
|
6018
|
+
function normalizePath(input) {
|
|
6019
|
+
return input.replace(/\\/g, "/");
|
|
6020
|
+
}
|
|
6021
|
+
function uniqueResultsByPath(results) {
|
|
6022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6023
|
+
const unique = [];
|
|
6024
|
+
for (const result of results) {
|
|
6025
|
+
const normalized = normalizePath(result.filePath);
|
|
6026
|
+
if (seen.has(normalized)) continue;
|
|
6027
|
+
seen.add(normalized);
|
|
6028
|
+
unique.push(result);
|
|
6029
|
+
}
|
|
6030
|
+
return unique;
|
|
6031
|
+
}
|
|
6032
|
+
function pathMatchesExpected(actualPath, expectedPath) {
|
|
6033
|
+
const actual = normalizePath(actualPath);
|
|
6034
|
+
const expected = normalizePath(expectedPath);
|
|
6035
|
+
if (actual === expected) return true;
|
|
6036
|
+
return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
|
|
6037
|
+
}
|
|
6038
|
+
function getRelevantPaths(query) {
|
|
6039
|
+
const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
|
|
6040
|
+
const fromAcceptable = query.expected.acceptableFiles ?? [];
|
|
6041
|
+
return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
|
|
6042
|
+
}
|
|
6043
|
+
function isRelevantResult(filePath, relevantPaths) {
|
|
6044
|
+
return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
|
|
6045
|
+
}
|
|
6046
|
+
function reciprocalRankAtK(results, relevantPaths, k) {
|
|
6047
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6048
|
+
for (let i = 0; i < top.length; i += 1) {
|
|
6049
|
+
if (isRelevantResult(top[i].filePath, relevantPaths)) {
|
|
6050
|
+
return 1 / (i + 1);
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
return 0;
|
|
6054
|
+
}
|
|
6055
|
+
function ndcgAtK(results, relevantPaths, k) {
|
|
6056
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6057
|
+
const dcg = top.reduce((sum, result, i) => {
|
|
6058
|
+
const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
|
|
6059
|
+
return sum + rel / Math.log2(i + 2);
|
|
6060
|
+
}, 0);
|
|
6061
|
+
const idealLen = Math.min(k, relevantPaths.length);
|
|
6062
|
+
const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
|
|
6063
|
+
(sum, value) => sum + value,
|
|
6064
|
+
0
|
|
6065
|
+
);
|
|
6066
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
6067
|
+
}
|
|
6068
|
+
function isDocsOrTestsPath(filePath) {
|
|
6069
|
+
const lowered = normalizePath(filePath).toLowerCase();
|
|
6070
|
+
return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
|
|
6071
|
+
}
|
|
6072
|
+
function classifyFailureBucket(query, results, k) {
|
|
6073
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6074
|
+
const top = uniqueResultsByPath(results).slice(0, k);
|
|
6075
|
+
const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6076
|
+
if (!hasRelevantTopK) {
|
|
6077
|
+
return "no-relevant-hit-top-k";
|
|
6078
|
+
}
|
|
6079
|
+
if (query.expected.symbol) {
|
|
6080
|
+
const hasSymbol = top.some(
|
|
6081
|
+
(result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
|
|
6082
|
+
);
|
|
6083
|
+
if (!hasSymbol) return "wrong-symbol";
|
|
6084
|
+
}
|
|
6085
|
+
const top1 = top[0];
|
|
6086
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
|
|
6087
|
+
return "docs-tests-outranking-source";
|
|
6088
|
+
}
|
|
6089
|
+
if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
|
|
6090
|
+
return "wrong-file";
|
|
6091
|
+
}
|
|
6092
|
+
return void 0;
|
|
6093
|
+
}
|
|
6094
|
+
function buildPerQueryResult(query, results, latencyMs, k) {
|
|
6095
|
+
const relevantPaths = getRelevantPaths(query);
|
|
6096
|
+
const deduped = uniqueResultsByPath(results);
|
|
6097
|
+
const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
|
|
6098
|
+
const perQuery = {
|
|
6099
|
+
id: query.id,
|
|
6100
|
+
query: query.query,
|
|
6101
|
+
queryType: query.queryType,
|
|
6102
|
+
latencyMs,
|
|
6103
|
+
hitAt1: hitAt(1),
|
|
6104
|
+
hitAt3: hitAt(3),
|
|
6105
|
+
hitAt5: hitAt(5),
|
|
6106
|
+
hitAt10: hitAt(10),
|
|
6107
|
+
reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
|
|
6108
|
+
ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
|
|
6109
|
+
failureBucket: classifyFailureBucket(query, results, k),
|
|
6110
|
+
results: deduped
|
|
6111
|
+
};
|
|
6112
|
+
return perQuery;
|
|
6113
|
+
}
|
|
6114
|
+
function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
|
|
6115
|
+
const count = perQuery.length;
|
|
6116
|
+
const safeDiv = (value) => count === 0 ? 0 : value / count;
|
|
6117
|
+
const sum = {
|
|
6118
|
+
hitAt1: 0,
|
|
6119
|
+
hitAt3: 0,
|
|
6120
|
+
hitAt5: 0,
|
|
6121
|
+
hitAt10: 0,
|
|
6122
|
+
mrrAt10: 0,
|
|
6123
|
+
ndcgAt10: 0
|
|
6124
|
+
};
|
|
6125
|
+
const failureBuckets = {
|
|
6126
|
+
"wrong-file": 0,
|
|
6127
|
+
"wrong-symbol": 0,
|
|
6128
|
+
"docs-tests-outranking-source": 0,
|
|
6129
|
+
"no-relevant-hit-top-k": 0
|
|
6130
|
+
};
|
|
6131
|
+
const latencies = perQuery.map((item) => item.latencyMs);
|
|
6132
|
+
for (const query of perQuery) {
|
|
6133
|
+
if (query.hitAt1) sum.hitAt1 += 1;
|
|
6134
|
+
if (query.hitAt3) sum.hitAt3 += 1;
|
|
6135
|
+
if (query.hitAt5) sum.hitAt5 += 1;
|
|
6136
|
+
if (query.hitAt10) sum.hitAt10 += 1;
|
|
6137
|
+
sum.mrrAt10 += query.reciprocalRankAt10;
|
|
6138
|
+
sum.ndcgAt10 += query.ndcgAt10;
|
|
6139
|
+
if (query.failureBucket) {
|
|
6140
|
+
failureBuckets[query.failureBucket] += 1;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
|
|
6144
|
+
return {
|
|
6145
|
+
hitAt1: safeDiv(sum.hitAt1),
|
|
6146
|
+
hitAt3: safeDiv(sum.hitAt3),
|
|
6147
|
+
hitAt5: safeDiv(sum.hitAt5),
|
|
6148
|
+
hitAt10: safeDiv(sum.hitAt10),
|
|
6149
|
+
mrrAt10: safeDiv(sum.mrrAt10),
|
|
6150
|
+
ndcgAt10: safeDiv(sum.ndcgAt10),
|
|
6151
|
+
latencyMs: {
|
|
6152
|
+
p50: percentile(latencies, 0.5),
|
|
6153
|
+
p95: percentile(latencies, 0.95),
|
|
6154
|
+
p99: percentile(latencies, 0.99)
|
|
6155
|
+
},
|
|
6156
|
+
tokenEstimate: {
|
|
6157
|
+
queryTokens,
|
|
6158
|
+
embeddingTokensUsed
|
|
6159
|
+
},
|
|
6160
|
+
embedding: {
|
|
6161
|
+
callCount: embeddingCallCount,
|
|
6162
|
+
estimatedCostUsd: embeddingTokensUsed / 1e6 * costPer1MTokensUsd,
|
|
6163
|
+
costPer1MTokensUsd
|
|
6164
|
+
},
|
|
6165
|
+
failureBuckets
|
|
6166
|
+
};
|
|
6167
|
+
}
|
|
6168
|
+
|
|
6169
|
+
// src/eval/schema.ts
|
|
6170
|
+
var import_fs6 = require("fs");
|
|
6171
|
+
function parseJsonFile(filePath) {
|
|
6172
|
+
const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
6173
|
+
return JSON.parse(content);
|
|
6174
|
+
}
|
|
6175
|
+
function isRecord(value) {
|
|
6176
|
+
return typeof value === "object" && value !== null;
|
|
6177
|
+
}
|
|
6178
|
+
function isStringArray2(value) {
|
|
6179
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6180
|
+
}
|
|
6181
|
+
function asPositiveNumber(value, path10) {
|
|
6182
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
6183
|
+
throw new Error(`${path10} must be a non-negative number`);
|
|
6184
|
+
}
|
|
6185
|
+
return value;
|
|
6186
|
+
}
|
|
6187
|
+
function parseQueryType(value, path10) {
|
|
6188
|
+
if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
|
|
6189
|
+
return value;
|
|
6190
|
+
}
|
|
6191
|
+
throw new Error(
|
|
6192
|
+
`${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
function parseExpected(input, path10) {
|
|
6196
|
+
if (!isRecord(input)) {
|
|
6197
|
+
throw new Error(`${path10} must be an object`);
|
|
6198
|
+
}
|
|
6199
|
+
const filePathRaw = input.filePath;
|
|
6200
|
+
const acceptableFilesRaw = input.acceptableFiles;
|
|
6201
|
+
const symbolRaw = input.symbol;
|
|
6202
|
+
const branchRaw = input.branch;
|
|
6203
|
+
const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
|
|
6204
|
+
const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
|
|
6205
|
+
if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
|
|
6206
|
+
throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
|
|
6207
|
+
}
|
|
6208
|
+
if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
|
|
6209
|
+
throw new Error(`${path10}.acceptableFiles must be an array of strings`);
|
|
6210
|
+
}
|
|
6211
|
+
if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
|
|
6212
|
+
throw new Error(`${path10}.symbol must be a string when provided`);
|
|
6213
|
+
}
|
|
6214
|
+
if (branchRaw !== void 0 && typeof branchRaw !== "string") {
|
|
6215
|
+
throw new Error(`${path10}.branch must be a string when provided`);
|
|
6216
|
+
}
|
|
6217
|
+
return {
|
|
6218
|
+
filePath,
|
|
6219
|
+
acceptableFiles,
|
|
6220
|
+
symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
|
|
6221
|
+
branch: typeof branchRaw === "string" ? branchRaw : void 0
|
|
6222
|
+
};
|
|
6223
|
+
}
|
|
6224
|
+
function parseQuery(input, index) {
|
|
6225
|
+
const path10 = `queries[${index}]`;
|
|
6226
|
+
if (!isRecord(input)) {
|
|
6227
|
+
throw new Error(`${path10} must be an object`);
|
|
6228
|
+
}
|
|
6229
|
+
const id = input.id;
|
|
6230
|
+
const query = input.query;
|
|
6231
|
+
const queryType = input.queryType;
|
|
6232
|
+
const expected = input.expected;
|
|
6233
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
6234
|
+
throw new Error(`${path10}.id must be a non-empty string`);
|
|
6235
|
+
}
|
|
6236
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
6237
|
+
throw new Error(`${path10}.query must be a non-empty string`);
|
|
6238
|
+
}
|
|
6239
|
+
return {
|
|
6240
|
+
id,
|
|
6241
|
+
query,
|
|
6242
|
+
queryType: parseQueryType(queryType, `${path10}.queryType`),
|
|
6243
|
+
expected: parseExpected(expected, `${path10}.expected`)
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
function parseGoldenDataset(raw, sourceLabel) {
|
|
6247
|
+
if (!isRecord(raw)) {
|
|
6248
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6249
|
+
}
|
|
6250
|
+
const version = raw.version;
|
|
6251
|
+
const name = raw.name;
|
|
6252
|
+
const description = raw.description;
|
|
6253
|
+
const queriesRaw = raw.queries;
|
|
6254
|
+
if (typeof version !== "string" || version.trim().length === 0) {
|
|
6255
|
+
throw new Error(`${sourceLabel}.version must be a non-empty string`);
|
|
6256
|
+
}
|
|
6257
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6258
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6259
|
+
}
|
|
6260
|
+
if (description !== void 0 && typeof description !== "string") {
|
|
6261
|
+
throw new Error(`${sourceLabel}.description must be a string when provided`);
|
|
6262
|
+
}
|
|
6263
|
+
if (!Array.isArray(queriesRaw)) {
|
|
6264
|
+
throw new Error(`${sourceLabel}.queries must be an array`);
|
|
6265
|
+
}
|
|
6266
|
+
if (queriesRaw.length === 0) {
|
|
6267
|
+
throw new Error(`${sourceLabel}.queries must contain at least one query`);
|
|
6268
|
+
}
|
|
6269
|
+
const queries = queriesRaw.map((query, idx) => parseQuery(query, idx));
|
|
6270
|
+
const idSet = /* @__PURE__ */ new Set();
|
|
6271
|
+
for (const query of queries) {
|
|
6272
|
+
if (idSet.has(query.id)) {
|
|
6273
|
+
throw new Error(`${sourceLabel}.queries has duplicate id: ${query.id}`);
|
|
6274
|
+
}
|
|
6275
|
+
idSet.add(query.id);
|
|
6276
|
+
}
|
|
6277
|
+
return {
|
|
6278
|
+
version,
|
|
6279
|
+
name,
|
|
6280
|
+
description: typeof description === "string" ? description : void 0,
|
|
6281
|
+
queries
|
|
6282
|
+
};
|
|
6283
|
+
}
|
|
6284
|
+
function loadGoldenDataset(datasetPath) {
|
|
6285
|
+
const parsed = parseJsonFile(datasetPath);
|
|
6286
|
+
return parseGoldenDataset(parsed, datasetPath);
|
|
6287
|
+
}
|
|
6288
|
+
function parseBudget(raw, sourceLabel) {
|
|
6289
|
+
if (!isRecord(raw)) {
|
|
6290
|
+
throw new Error(`${sourceLabel} must be a JSON object`);
|
|
6291
|
+
}
|
|
6292
|
+
const name = raw.name;
|
|
6293
|
+
const baselinePath = raw.baselinePath;
|
|
6294
|
+
const failOnMissingBaseline = raw.failOnMissingBaseline;
|
|
6295
|
+
const thresholds = raw.thresholds;
|
|
6296
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
6297
|
+
throw new Error(`${sourceLabel}.name must be a non-empty string`);
|
|
6298
|
+
}
|
|
6299
|
+
if (baselinePath !== void 0 && typeof baselinePath !== "string") {
|
|
6300
|
+
throw new Error(`${sourceLabel}.baselinePath must be a string when provided`);
|
|
6301
|
+
}
|
|
6302
|
+
if (!isRecord(thresholds)) {
|
|
6303
|
+
throw new Error(`${sourceLabel}.thresholds must be an object`);
|
|
6304
|
+
}
|
|
6305
|
+
return {
|
|
6306
|
+
name,
|
|
6307
|
+
baselinePath: typeof baselinePath === "string" ? baselinePath : void 0,
|
|
6308
|
+
failOnMissingBaseline: typeof failOnMissingBaseline === "boolean" ? failOnMissingBaseline : true,
|
|
6309
|
+
thresholds: {
|
|
6310
|
+
hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
|
|
6311
|
+
mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
|
|
6312
|
+
p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
|
|
6313
|
+
thresholds.p95LatencyMaxMultiplier,
|
|
6314
|
+
`${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
|
|
6315
|
+
),
|
|
6316
|
+
p95LatencyMaxAbsoluteMs: thresholds.p95LatencyMaxAbsoluteMs === void 0 ? void 0 : asPositiveNumber(
|
|
6317
|
+
thresholds.p95LatencyMaxAbsoluteMs,
|
|
6318
|
+
`${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
|
|
6319
|
+
),
|
|
6320
|
+
minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
|
|
6321
|
+
minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
function loadBudget(budgetPath) {
|
|
6326
|
+
const parsed = parseJsonFile(budgetPath);
|
|
6327
|
+
return parseBudget(parsed, budgetPath);
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
// src/eval/runner.ts
|
|
6331
|
+
function toAbsolute(projectRoot, maybeRelative) {
|
|
6332
|
+
return path7.isAbsolute(maybeRelative) ? maybeRelative : path7.join(projectRoot, maybeRelative);
|
|
6333
|
+
}
|
|
6334
|
+
function loadRawConfig(projectRoot, configPath) {
|
|
6335
|
+
const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
|
|
6336
|
+
if (fromPath && (0, import_fs7.existsSync)(fromPath)) {
|
|
6337
|
+
return JSON.parse((0, import_fs8.readFileSync)(fromPath, "utf-8"));
|
|
6338
|
+
}
|
|
6339
|
+
const projectConfig = path7.join(projectRoot, ".opencode", "codebase-index.json");
|
|
6340
|
+
if ((0, import_fs7.existsSync)(projectConfig)) {
|
|
6341
|
+
return JSON.parse((0, import_fs8.readFileSync)(projectConfig, "utf-8"));
|
|
6342
|
+
}
|
|
6343
|
+
const globalConfig = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
|
|
6344
|
+
if ((0, import_fs7.existsSync)(globalConfig)) {
|
|
6345
|
+
return JSON.parse((0, import_fs8.readFileSync)(globalConfig, "utf-8"));
|
|
6346
|
+
}
|
|
6347
|
+
return {};
|
|
6348
|
+
}
|
|
6349
|
+
function getIndexRootPath(projectRoot, scope) {
|
|
6350
|
+
if (scope === "global") {
|
|
6351
|
+
return path7.join(os3.homedir(), ".opencode", "global-index");
|
|
6352
|
+
}
|
|
6353
|
+
return path7.join(projectRoot, ".opencode", "index");
|
|
6354
|
+
}
|
|
6355
|
+
function clearIndexRoot(projectRoot, scope) {
|
|
6356
|
+
const indexRoot = getIndexRootPath(projectRoot, scope);
|
|
6357
|
+
if ((0, import_fs7.existsSync)(indexRoot)) {
|
|
6358
|
+
(0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
function loadParsedConfig(projectRoot, configPath) {
|
|
6362
|
+
const raw = loadRawConfig(projectRoot, configPath);
|
|
6363
|
+
return parseConfig(raw);
|
|
6364
|
+
}
|
|
6365
|
+
function resolveSearchConfig(parsedConfig, overrides) {
|
|
6366
|
+
const nextSearch = {
|
|
6367
|
+
...parsedConfig.search
|
|
6368
|
+
};
|
|
6369
|
+
if (overrides?.fusionStrategy !== void 0) {
|
|
6370
|
+
nextSearch.fusionStrategy = overrides.fusionStrategy;
|
|
6371
|
+
}
|
|
6372
|
+
if (overrides?.hybridWeight !== void 0) {
|
|
6373
|
+
nextSearch.hybridWeight = overrides.hybridWeight;
|
|
6374
|
+
}
|
|
6375
|
+
if (overrides?.rrfK !== void 0) {
|
|
6376
|
+
nextSearch.rrfK = overrides.rrfK;
|
|
6377
|
+
}
|
|
6378
|
+
if (overrides?.rerankTopN !== void 0) {
|
|
6379
|
+
nextSearch.rerankTopN = overrides.rerankTopN;
|
|
6380
|
+
}
|
|
6381
|
+
return {
|
|
6382
|
+
...parsedConfig,
|
|
6383
|
+
search: nextSearch
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
async function runEvaluation(options) {
|
|
6387
|
+
const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
|
|
6388
|
+
const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
|
|
6389
|
+
const budgetPath = options.budgetPath ? toAbsolute(options.projectRoot, options.budgetPath) : void 0;
|
|
6390
|
+
const dataset = loadGoldenDataset(datasetPath);
|
|
6391
|
+
const parsedConfig = loadParsedConfig(options.projectRoot, options.configPath);
|
|
6392
|
+
const effectiveConfig = resolveSearchConfig(parsedConfig, options.searchOverrides);
|
|
6393
|
+
if (options.reindex) {
|
|
6394
|
+
clearIndexRoot(options.projectRoot, effectiveConfig.scope);
|
|
6395
|
+
}
|
|
6396
|
+
const indexer = new Indexer(options.projectRoot, effectiveConfig);
|
|
6397
|
+
await indexer.index();
|
|
6398
|
+
const perQuery = [];
|
|
6399
|
+
for (const query of dataset.queries) {
|
|
6400
|
+
if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
|
|
6401
|
+
throw new Error(
|
|
6402
|
+
`Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
|
|
6403
|
+
);
|
|
6404
|
+
}
|
|
6405
|
+
const start = import_perf_hooks2.performance.now();
|
|
6406
|
+
const result = await indexer.search(query.query, 10, {
|
|
6407
|
+
metadataOnly: true,
|
|
6408
|
+
filterByBranch: query.expected.branch ? true : false
|
|
6409
|
+
});
|
|
6410
|
+
const elapsed = import_perf_hooks2.performance.now() - start;
|
|
6411
|
+
const materialized = result.map((item) => ({
|
|
6412
|
+
filePath: item.filePath,
|
|
6413
|
+
startLine: item.startLine,
|
|
6414
|
+
endLine: item.endLine,
|
|
6415
|
+
score: item.score,
|
|
6416
|
+
chunkType: item.chunkType,
|
|
6417
|
+
name: item.name
|
|
6418
|
+
}));
|
|
6419
|
+
perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
|
|
6420
|
+
}
|
|
6421
|
+
const logger = indexer.getLogger();
|
|
6422
|
+
const metricSnapshot = logger.getMetrics();
|
|
6423
|
+
const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
|
|
6424
|
+
const summary = {
|
|
6425
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6426
|
+
projectRoot: options.projectRoot,
|
|
6427
|
+
datasetPath,
|
|
6428
|
+
datasetName: dataset.name,
|
|
6429
|
+
datasetVersion: dataset.version,
|
|
6430
|
+
queryCount: dataset.queries.length,
|
|
6431
|
+
topK: 10,
|
|
6432
|
+
searchConfig: {
|
|
6433
|
+
fusionStrategy: effectiveConfig.search.fusionStrategy,
|
|
6434
|
+
hybridWeight: effectiveConfig.search.hybridWeight,
|
|
6435
|
+
rrfK: effectiveConfig.search.rrfK,
|
|
6436
|
+
rerankTopN: effectiveConfig.search.rerankTopN
|
|
6437
|
+
},
|
|
6438
|
+
metrics: computeEvalMetrics(
|
|
6439
|
+
dataset.queries,
|
|
6440
|
+
perQuery,
|
|
6441
|
+
metricSnapshot.embeddingApiCalls,
|
|
6442
|
+
metricSnapshot.embeddingTokensUsed,
|
|
6443
|
+
costPer1MTokensUsd
|
|
6444
|
+
)
|
|
6445
|
+
};
|
|
6446
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6447
|
+
const perQueryArtifact = buildPerQueryArtifact(perQuery);
|
|
6448
|
+
writeJson(path7.join(outputDir, "summary.json"), summary);
|
|
6449
|
+
writeJson(path7.join(outputDir, "per-query.json"), perQueryArtifact);
|
|
6450
|
+
let comparison;
|
|
6451
|
+
if (againstPath) {
|
|
6452
|
+
const baseline = loadSummary(againstPath);
|
|
6453
|
+
comparison = compareSummaries(summary, baseline, againstPath);
|
|
6454
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6455
|
+
}
|
|
6456
|
+
let gate;
|
|
6457
|
+
if (options.ciMode) {
|
|
6458
|
+
if (!budgetPath) {
|
|
6459
|
+
throw new Error("CI mode requires --budget path");
|
|
6460
|
+
}
|
|
6461
|
+
const budget = loadBudget(budgetPath);
|
|
6462
|
+
if (!comparison && budget.baselinePath) {
|
|
6463
|
+
const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
|
|
6464
|
+
if ((0, import_fs7.existsSync)(resolvedBaseline)) {
|
|
6465
|
+
const baselineSummary = loadSummary(resolvedBaseline);
|
|
6466
|
+
comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
|
|
6467
|
+
writeJson(path7.join(outputDir, "compare.json"), comparison);
|
|
6468
|
+
} else if (budget.failOnMissingBaseline) {
|
|
6469
|
+
throw new Error(
|
|
6470
|
+
`Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
|
|
6471
|
+
);
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
gate = evaluateBudgetGate(budget, summary, comparison);
|
|
6475
|
+
}
|
|
6476
|
+
const markdown = createSummaryMarkdown(summary, comparison, gate);
|
|
6477
|
+
writeText(path7.join(outputDir, "summary.md"), markdown);
|
|
6478
|
+
return { outputDir, summary, perQuery, comparison, gate };
|
|
6479
|
+
}
|
|
6480
|
+
async function runSweep(options, sweep) {
|
|
6481
|
+
const fusionValues = sweep.fusionStrategy && sweep.fusionStrategy.length > 0 ? [...sweep.fusionStrategy] : [void 0];
|
|
6482
|
+
const weightValues = sweep.hybridWeight && sweep.hybridWeight.length > 0 ? [...sweep.hybridWeight] : [void 0];
|
|
6483
|
+
const rrfValues = sweep.rrfK && sweep.rrfK.length > 0 ? [...sweep.rrfK] : [void 0];
|
|
6484
|
+
const rerankValues = sweep.rerankTopN && sweep.rerankTopN.length > 0 ? [...sweep.rerankTopN] : [void 0];
|
|
6485
|
+
const runs = [];
|
|
6486
|
+
for (const fusion of fusionValues) {
|
|
6487
|
+
for (const hybridWeight of weightValues) {
|
|
6488
|
+
for (const rrfK of rrfValues) {
|
|
6489
|
+
for (const rerankTopN of rerankValues) {
|
|
6490
|
+
const run = await runEvaluation({
|
|
6491
|
+
...options,
|
|
6492
|
+
searchOverrides: {
|
|
6493
|
+
...fusion !== void 0 ? { fusionStrategy: fusion } : {},
|
|
6494
|
+
...hybridWeight !== void 0 ? { hybridWeight } : {},
|
|
6495
|
+
...rrfK !== void 0 ? { rrfK } : {},
|
|
6496
|
+
...rerankTopN !== void 0 ? { rerankTopN } : {}
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
runs.push({
|
|
6500
|
+
searchConfig: run.summary.searchConfig,
|
|
6501
|
+
summary: run.summary,
|
|
6502
|
+
comparison: run.comparison,
|
|
6503
|
+
gate: run.gate
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
const bestByHitAt5 = [...runs].sort(
|
|
6510
|
+
(a, b) => b.summary.metrics.hitAt5 - a.summary.metrics.hitAt5
|
|
6511
|
+
)[0];
|
|
6512
|
+
const bestByMrrAt10 = [...runs].sort(
|
|
6513
|
+
(a, b) => b.summary.metrics.mrrAt10 - a.summary.metrics.mrrAt10
|
|
6514
|
+
)[0];
|
|
6515
|
+
const bestByP95Latency = [...runs].sort(
|
|
6516
|
+
(a, b) => a.summary.metrics.latencyMs.p95 - b.summary.metrics.latencyMs.p95
|
|
6517
|
+
)[0];
|
|
6518
|
+
const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
|
|
6519
|
+
const failedGateRuns = runs.filter((run) => run.gate && !run.gate.passed).length;
|
|
6520
|
+
const gatePassed = failedGateRuns === 0;
|
|
6521
|
+
const aggregate = {
|
|
6522
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6523
|
+
againstPath: options.againstPath,
|
|
6524
|
+
runCount: runs.length,
|
|
6525
|
+
runs,
|
|
6526
|
+
gatePassed,
|
|
6527
|
+
failedGateRuns,
|
|
6528
|
+
bestByHitAt5,
|
|
6529
|
+
bestByMrrAt10,
|
|
6530
|
+
bestByP95Latency
|
|
6531
|
+
};
|
|
6532
|
+
writeJson(path7.join(outputDir, "compare.json"), aggregate);
|
|
6533
|
+
const md = createSummaryMarkdown(
|
|
6534
|
+
bestByHitAt5?.summary ?? runs[0].summary,
|
|
6535
|
+
bestByHitAt5?.comparison,
|
|
6536
|
+
void 0,
|
|
6537
|
+
aggregate
|
|
6538
|
+
);
|
|
6539
|
+
writeText(path7.join(outputDir, "summary.md"), md);
|
|
6540
|
+
writeJson(path7.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
|
|
6541
|
+
return { outputDir, aggregate };
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// src/eval/cli.ts
|
|
6545
|
+
function printUsage() {
|
|
6546
|
+
console.log(`
|
|
6547
|
+
Usage:
|
|
6548
|
+
opencode-codebase-index-mcp eval run [options]
|
|
6549
|
+
opencode-codebase-index-mcp eval compare --against <summary.json> [options]
|
|
6550
|
+
opencode-codebase-index-mcp eval diff --current <summary.json> --against <summary.json> [options]
|
|
6551
|
+
|
|
6552
|
+
Options:
|
|
6553
|
+
--project <path> Project root (default: cwd)
|
|
6554
|
+
--config <path> Config JSON path
|
|
6555
|
+
--dataset <path> Golden dataset path (default: benchmarks/golden/small.json)
|
|
6556
|
+
--current <path> Current summary.json path (required for eval diff)
|
|
6557
|
+
--output <path> Output root dir (default: benchmarks/results)
|
|
6558
|
+
--against <path> Baseline summary.json to compare against
|
|
6559
|
+
--budget <path> Budget file for CI mode (default: benchmarks/budgets/default.json)
|
|
6560
|
+
--ci Enable CI gate mode
|
|
6561
|
+
--reindex Force reindex before eval
|
|
6562
|
+
|
|
6563
|
+
Search overrides:
|
|
6564
|
+
--fusionStrategy <rrf|weighted>
|
|
6565
|
+
--hybridWeight <0-1>
|
|
6566
|
+
--rrfK <number>
|
|
6567
|
+
--rerankTopN <number>
|
|
6568
|
+
|
|
6569
|
+
Sweep options (comma-separated values):
|
|
6570
|
+
--sweepFusionStrategy <rrf,weighted>
|
|
6571
|
+
--sweepHybridWeight <0.3,0.5,0.7>
|
|
6572
|
+
--sweepRrfK <30,60,90>
|
|
6573
|
+
--sweepRerankTopN <10,20,40>
|
|
6574
|
+
`);
|
|
6575
|
+
}
|
|
6576
|
+
function parseNumber(value, flag) {
|
|
6577
|
+
const parsed = Number(value);
|
|
6578
|
+
if (Number.isNaN(parsed)) {
|
|
6579
|
+
throw new Error(`${flag} must be a number`);
|
|
6580
|
+
}
|
|
6581
|
+
return parsed;
|
|
6582
|
+
}
|
|
6583
|
+
function parseCsvNumbers(value, flag) {
|
|
6584
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0).map((item) => parseNumber(item, flag));
|
|
6585
|
+
}
|
|
6586
|
+
function parseCsvFusion(value) {
|
|
6587
|
+
const values = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
6588
|
+
const parsed = [];
|
|
6589
|
+
for (const candidate of values) {
|
|
6590
|
+
if (candidate !== "rrf" && candidate !== "weighted") {
|
|
6591
|
+
throw new Error("--sweepFusionStrategy accepts only rrf,weighted");
|
|
6592
|
+
}
|
|
6593
|
+
parsed.push(candidate);
|
|
6594
|
+
}
|
|
6595
|
+
return parsed;
|
|
6596
|
+
}
|
|
6597
|
+
function hasSweepOptions(sweep) {
|
|
6598
|
+
return Boolean(
|
|
6599
|
+
sweep.fusionStrategy && sweep.fusionStrategy.length > 0 || sweep.hybridWeight && sweep.hybridWeight.length > 0 || sweep.rrfK && sweep.rrfK.length > 0 || sweep.rerankTopN && sweep.rerankTopN.length > 0
|
|
6600
|
+
);
|
|
6601
|
+
}
|
|
6602
|
+
function parseEvalArgs(argv, cwd) {
|
|
6603
|
+
const parsed = {
|
|
6604
|
+
projectRoot: cwd,
|
|
6605
|
+
datasetPath: "benchmarks/golden/small.json",
|
|
6606
|
+
outputRoot: "benchmarks/results",
|
|
6607
|
+
budgetPath: "benchmarks/budgets/default.json",
|
|
6608
|
+
ciMode: false,
|
|
6609
|
+
reindex: false,
|
|
6610
|
+
sweep: {}
|
|
6611
|
+
};
|
|
6612
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6613
|
+
const arg = argv[i];
|
|
6614
|
+
const next = argv[i + 1];
|
|
6615
|
+
if (arg === "--project" && next) {
|
|
6616
|
+
parsed.projectRoot = path8.resolve(cwd, next);
|
|
6617
|
+
i += 1;
|
|
6618
|
+
continue;
|
|
6619
|
+
}
|
|
6620
|
+
if (arg === "--config" && next) {
|
|
6621
|
+
parsed.configPath = path8.resolve(cwd, next);
|
|
6622
|
+
i += 1;
|
|
6623
|
+
continue;
|
|
6624
|
+
}
|
|
6625
|
+
if (arg === "--dataset" && next) {
|
|
6626
|
+
parsed.datasetPath = next;
|
|
6627
|
+
i += 1;
|
|
6628
|
+
continue;
|
|
6629
|
+
}
|
|
6630
|
+
if (arg === "--current" && next) {
|
|
6631
|
+
parsed.currentPath = next;
|
|
6632
|
+
i += 1;
|
|
6633
|
+
continue;
|
|
6634
|
+
}
|
|
6635
|
+
if (arg === "--output" && next) {
|
|
6636
|
+
parsed.outputRoot = next;
|
|
6637
|
+
i += 1;
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
if (arg === "--against" && next) {
|
|
6641
|
+
parsed.againstPath = next;
|
|
6642
|
+
i += 1;
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
if (arg === "--budget" && next) {
|
|
6646
|
+
parsed.budgetPath = next;
|
|
6647
|
+
i += 1;
|
|
6648
|
+
continue;
|
|
6649
|
+
}
|
|
6650
|
+
if (arg === "--ci") {
|
|
6651
|
+
parsed.ciMode = true;
|
|
6652
|
+
continue;
|
|
6653
|
+
}
|
|
6654
|
+
if (arg === "--reindex") {
|
|
6655
|
+
parsed.reindex = true;
|
|
6656
|
+
continue;
|
|
6657
|
+
}
|
|
6658
|
+
if (arg === "--fusionStrategy" && next) {
|
|
6659
|
+
if (next !== "rrf" && next !== "weighted") {
|
|
6660
|
+
throw new Error("--fusionStrategy must be rrf or weighted");
|
|
6661
|
+
}
|
|
6662
|
+
parsed.fusionStrategy = next;
|
|
6663
|
+
i += 1;
|
|
6664
|
+
continue;
|
|
6665
|
+
}
|
|
6666
|
+
if (arg === "--hybridWeight" && next) {
|
|
6667
|
+
parsed.hybridWeight = parseNumber(next, "--hybridWeight");
|
|
6668
|
+
i += 1;
|
|
6669
|
+
continue;
|
|
6670
|
+
}
|
|
6671
|
+
if (arg === "--rrfK" && next) {
|
|
6672
|
+
parsed.rrfK = parseNumber(next, "--rrfK");
|
|
6673
|
+
i += 1;
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
if (arg === "--rerankTopN" && next) {
|
|
6677
|
+
parsed.rerankTopN = parseNumber(next, "--rerankTopN");
|
|
6678
|
+
i += 1;
|
|
6679
|
+
continue;
|
|
6680
|
+
}
|
|
6681
|
+
if (arg === "--sweepFusionStrategy" && next) {
|
|
6682
|
+
parsed.sweep.fusionStrategy = parseCsvFusion(next);
|
|
6683
|
+
i += 1;
|
|
6684
|
+
continue;
|
|
6685
|
+
}
|
|
6686
|
+
if (arg === "--sweepHybridWeight" && next) {
|
|
6687
|
+
parsed.sweep.hybridWeight = parseCsvNumbers(next, "--sweepHybridWeight");
|
|
6688
|
+
i += 1;
|
|
6689
|
+
continue;
|
|
6690
|
+
}
|
|
6691
|
+
if (arg === "--sweepRrfK" && next) {
|
|
6692
|
+
parsed.sweep.rrfK = parseCsvNumbers(next, "--sweepRrfK");
|
|
6693
|
+
i += 1;
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6696
|
+
if (arg === "--sweepRerankTopN" && next) {
|
|
6697
|
+
parsed.sweep.rerankTopN = parseCsvNumbers(next, "--sweepRerankTopN");
|
|
6698
|
+
i += 1;
|
|
6699
|
+
continue;
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
return parsed;
|
|
6703
|
+
}
|
|
6704
|
+
function parseEvalSubcommandOptions(argv, cwd) {
|
|
6705
|
+
let explicitAgainst;
|
|
6706
|
+
const filtered = [];
|
|
6707
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6708
|
+
const current = argv[i];
|
|
6709
|
+
const next = argv[i + 1];
|
|
6710
|
+
if (current === "--against" && next) {
|
|
6711
|
+
explicitAgainst = next;
|
|
6712
|
+
i += 1;
|
|
6713
|
+
continue;
|
|
6714
|
+
}
|
|
6715
|
+
filtered.push(current);
|
|
6716
|
+
}
|
|
6717
|
+
return {
|
|
6718
|
+
parsed: parseEvalArgs(filtered, cwd),
|
|
6719
|
+
explicitAgainst
|
|
6720
|
+
};
|
|
6721
|
+
}
|
|
6722
|
+
function toRunOptions(parsed) {
|
|
6723
|
+
return {
|
|
6724
|
+
projectRoot: parsed.projectRoot,
|
|
6725
|
+
configPath: parsed.configPath,
|
|
6726
|
+
datasetPath: parsed.datasetPath,
|
|
6727
|
+
outputRoot: parsed.outputRoot,
|
|
6728
|
+
againstPath: parsed.againstPath,
|
|
6729
|
+
budgetPath: parsed.budgetPath,
|
|
6730
|
+
ciMode: parsed.ciMode,
|
|
6731
|
+
reindex: parsed.reindex,
|
|
6732
|
+
searchOverrides: {
|
|
6733
|
+
...parsed.fusionStrategy !== void 0 ? { fusionStrategy: parsed.fusionStrategy } : {},
|
|
6734
|
+
...parsed.hybridWeight !== void 0 ? { hybridWeight: parsed.hybridWeight } : {},
|
|
6735
|
+
...parsed.rrfK !== void 0 ? { rrfK: parsed.rrfK } : {},
|
|
6736
|
+
...parsed.rerankTopN !== void 0 ? { rerankTopN: parsed.rerankTopN } : {}
|
|
6737
|
+
}
|
|
6738
|
+
};
|
|
6739
|
+
}
|
|
6740
|
+
async function handleEvalCommand(args, cwd) {
|
|
6741
|
+
const subcommand = args[0];
|
|
6742
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
6743
|
+
printUsage();
|
|
6744
|
+
return 0;
|
|
6745
|
+
}
|
|
6746
|
+
if (subcommand === "run") {
|
|
6747
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6748
|
+
if (explicitAgainst) {
|
|
6749
|
+
parsed.againstPath = explicitAgainst;
|
|
6750
|
+
}
|
|
6751
|
+
const runOptions = toRunOptions(parsed);
|
|
6752
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6753
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6754
|
+
console.log(`Eval sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6755
|
+
console.log(`Sweep runs: ${sweep.aggregate.runCount}`);
|
|
6756
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6757
|
+
console.error(
|
|
6758
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6759
|
+
);
|
|
6760
|
+
return 1;
|
|
6761
|
+
}
|
|
6762
|
+
return 0;
|
|
6763
|
+
}
|
|
6764
|
+
const result = await runEvaluation(runOptions);
|
|
6765
|
+
console.log(`Eval run complete. Artifacts: ${result.outputDir}`);
|
|
6766
|
+
console.log(
|
|
6767
|
+
`Hit@5=${(result.summary.metrics.hitAt5 * 100).toFixed(2)}% MRR@10=${result.summary.metrics.mrrAt10.toFixed(4)} p95=${result.summary.metrics.latencyMs.p95.toFixed(3)}ms`
|
|
6768
|
+
);
|
|
6769
|
+
if (result.gate && !result.gate.passed) {
|
|
6770
|
+
for (const violation of result.gate.violations) {
|
|
6771
|
+
console.error(`[CI-GATE] ${violation.metric}: ${violation.message}`);
|
|
6772
|
+
}
|
|
6773
|
+
return 1;
|
|
6774
|
+
}
|
|
6775
|
+
return 0;
|
|
6776
|
+
}
|
|
6777
|
+
if (subcommand === "compare") {
|
|
6778
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6779
|
+
if (!explicitAgainst) {
|
|
6780
|
+
throw new Error("eval compare requires --against <baseline summary.json>");
|
|
6781
|
+
}
|
|
6782
|
+
parsed.againstPath = explicitAgainst;
|
|
6783
|
+
const runOptions = toRunOptions(parsed);
|
|
6784
|
+
if (hasSweepOptions(parsed.sweep)) {
|
|
6785
|
+
const sweep = await runSweep(runOptions, parsed.sweep);
|
|
6786
|
+
console.log(`Eval compare sweep complete. Artifacts: ${sweep.outputDir}`);
|
|
6787
|
+
if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
|
|
6788
|
+
console.error(
|
|
6789
|
+
`[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
|
|
6790
|
+
);
|
|
6791
|
+
return 1;
|
|
6792
|
+
}
|
|
6793
|
+
return 0;
|
|
6794
|
+
}
|
|
6795
|
+
const result = await runEvaluation(runOptions);
|
|
6796
|
+
console.log(`Eval compare complete. Artifacts: ${result.outputDir}`);
|
|
6797
|
+
return 0;
|
|
6798
|
+
}
|
|
6799
|
+
if (subcommand === "diff") {
|
|
6800
|
+
const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
|
|
6801
|
+
if (!explicitAgainst) {
|
|
6802
|
+
throw new Error("eval diff requires --against <baseline summary.json>");
|
|
6803
|
+
}
|
|
6804
|
+
if (!parsed.currentPath) {
|
|
6805
|
+
throw new Error("eval diff requires --current <current summary.json>");
|
|
6806
|
+
}
|
|
6807
|
+
parsed.againstPath = explicitAgainst;
|
|
6808
|
+
const currentPath = parsed.currentPath;
|
|
6809
|
+
if (!currentPath.endsWith(".json")) {
|
|
6810
|
+
throw new Error("eval diff --current must point to a summary JSON file");
|
|
6811
|
+
}
|
|
6812
|
+
if (!parsed.againstPath.endsWith(".json")) {
|
|
6813
|
+
throw new Error("eval diff --against must point to a summary JSON file");
|
|
6814
|
+
}
|
|
6815
|
+
const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
|
|
6816
|
+
const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
|
|
6817
|
+
const comparison = compareSummaries(
|
|
6818
|
+
currentSummary,
|
|
6819
|
+
baselineSummary,
|
|
6820
|
+
path8.resolve(parsed.projectRoot, parsed.againstPath)
|
|
6821
|
+
);
|
|
6822
|
+
const outputDir = createRunDirectory(path8.resolve(parsed.projectRoot, parsed.outputRoot));
|
|
6823
|
+
const summaryMd = createSummaryMarkdown(currentSummary, comparison);
|
|
6824
|
+
writeJson(path8.join(outputDir, "compare.json"), comparison);
|
|
6825
|
+
writeText(path8.join(outputDir, "summary.md"), summaryMd);
|
|
6826
|
+
writeJson(path8.join(outputDir, "summary.json"), currentSummary);
|
|
6827
|
+
console.log(`Eval diff complete. Artifacts: ${outputDir}`);
|
|
6828
|
+
return 0;
|
|
6829
|
+
}
|
|
6830
|
+
throw new Error(`Unknown eval subcommand: ${subcommand}`);
|
|
6831
|
+
}
|
|
6832
|
+
|
|
4589
6833
|
// src/mcp-server.ts
|
|
6834
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6835
|
+
var import_zod = require("zod");
|
|
6836
|
+
|
|
6837
|
+
// src/tools/utils.ts
|
|
4590
6838
|
var MAX_CONTENT_LINES = 30;
|
|
4591
6839
|
function truncateContent(content) {
|
|
4592
6840
|
const lines = content.split("\n");
|
|
@@ -4594,6 +6842,31 @@ function truncateContent(content) {
|
|
|
4594
6842
|
return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
|
|
4595
6843
|
// ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
|
|
4596
6844
|
}
|
|
6845
|
+
function formatDefinitionLookup(results, query) {
|
|
6846
|
+
if (results.length === 0) {
|
|
6847
|
+
return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
|
|
6848
|
+
}
|
|
6849
|
+
const formatted = results.map((r, idx) => {
|
|
6850
|
+
const header2 = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
|
|
6851
|
+
return `${header2} (score: ${r.score.toFixed(2)})
|
|
6852
|
+
\`\`\`
|
|
6853
|
+
${truncateContent(r.content)}
|
|
6854
|
+
\`\`\``;
|
|
6855
|
+
});
|
|
6856
|
+
const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
|
|
6857
|
+
return `${header}
|
|
6858
|
+
|
|
6859
|
+
${formatted.join("\n\n")}`;
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
// src/mcp-server.ts
|
|
6863
|
+
var MAX_CONTENT_LINES2 = 30;
|
|
6864
|
+
function truncateContent2(content) {
|
|
6865
|
+
const lines = content.split("\n");
|
|
6866
|
+
if (lines.length <= MAX_CONTENT_LINES2) return content;
|
|
6867
|
+
return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
|
|
6868
|
+
// ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
|
|
6869
|
+
}
|
|
4597
6870
|
function formatIndexStats(stats, verbose = false) {
|
|
4598
6871
|
const lines = [];
|
|
4599
6872
|
if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
|
|
@@ -4707,7 +6980,7 @@ function createMcpServer(projectRoot, config) {
|
|
|
4707
6980
|
const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
|
|
4708
6981
|
return `${header} (score: ${r.score.toFixed(2)})
|
|
4709
6982
|
\`\`\`
|
|
4710
|
-
${
|
|
6983
|
+
${truncateContent2(r.content)}
|
|
4711
6984
|
\`\`\``;
|
|
4712
6985
|
});
|
|
4713
6986
|
return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
|
|
@@ -4786,7 +7059,7 @@ Use Read tool to examine specific files.` }] };
|
|
|
4786
7059
|
async () => {
|
|
4787
7060
|
await ensureInitialized();
|
|
4788
7061
|
const result = await indexer.healthCheck();
|
|
4789
|
-
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0) {
|
|
7062
|
+
if (result.removed === 0 && result.gcOrphanEmbeddings === 0 && result.gcOrphanChunks === 0 && result.gcOrphanSymbols === 0 && result.gcOrphanCallEdges === 0) {
|
|
4790
7063
|
return { content: [{ type: "text", text: "Index is healthy. No stale entries found." }] };
|
|
4791
7064
|
}
|
|
4792
7065
|
const lines = [`Health check complete:`];
|
|
@@ -4799,6 +7072,12 @@ Use Read tool to examine specific files.` }] };
|
|
|
4799
7072
|
if (result.gcOrphanChunks > 0) {
|
|
4800
7073
|
lines.push(` Garbage collected orphan chunks: ${result.gcOrphanChunks}`);
|
|
4801
7074
|
}
|
|
7075
|
+
if (result.gcOrphanSymbols > 0) {
|
|
7076
|
+
lines.push(` Garbage collected orphan symbols: ${result.gcOrphanSymbols}`);
|
|
7077
|
+
}
|
|
7078
|
+
if (result.gcOrphanCallEdges > 0) {
|
|
7079
|
+
lines.push(` Garbage collected orphan call edges: ${result.gcOrphanCallEdges}`);
|
|
7080
|
+
}
|
|
4802
7081
|
if (result.filePaths.length > 0) {
|
|
4803
7082
|
lines.push(` Cleaned paths: ${result.filePaths.join(", ")}`);
|
|
4804
7083
|
}
|
|
@@ -4879,7 +7158,7 @@ Use Read tool to examine specific files.` }] };
|
|
|
4879
7158
|
const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
|
|
4880
7159
|
return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
|
|
4881
7160
|
\`\`\`
|
|
4882
|
-
${
|
|
7161
|
+
${truncateContent2(r.content)}
|
|
4883
7162
|
\`\`\``;
|
|
4884
7163
|
});
|
|
4885
7164
|
return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
|
|
@@ -4887,6 +7166,62 @@ ${truncateContent(r.content)}
|
|
|
4887
7166
|
${formatted.join("\n\n")}` }] };
|
|
4888
7167
|
}
|
|
4889
7168
|
);
|
|
7169
|
+
server.tool(
|
|
7170
|
+
"implementation_lookup",
|
|
7171
|
+
"Jump to symbol definition. Find WHERE something is defined. Returns the authoritative source location(s). Prefers real implementation files over tests, docs, examples, and fixtures.",
|
|
7172
|
+
{
|
|
7173
|
+
query: import_zod.z.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
|
|
7174
|
+
limit: import_zod.z.number().optional().default(5).describe("Maximum number of results"),
|
|
7175
|
+
fileType: import_zod.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
|
|
7176
|
+
directory: import_zod.z.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
|
|
7177
|
+
},
|
|
7178
|
+
async (args) => {
|
|
7179
|
+
await ensureInitialized();
|
|
7180
|
+
const results = await indexer.search(args.query, args.limit ?? 5, {
|
|
7181
|
+
fileType: args.fileType,
|
|
7182
|
+
directory: args.directory,
|
|
7183
|
+
definitionIntent: true
|
|
7184
|
+
});
|
|
7185
|
+
return { content: [{ type: "text", text: formatDefinitionLookup(results, args.query) }] };
|
|
7186
|
+
}
|
|
7187
|
+
);
|
|
7188
|
+
server.tool(
|
|
7189
|
+
"call_graph",
|
|
7190
|
+
"Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
|
|
7191
|
+
{
|
|
7192
|
+
name: import_zod.z.string().describe("Function or method name to query"),
|
|
7193
|
+
direction: import_zod.z.enum(["callers", "callees"]).default("callers").describe("Direction: 'callers' finds who calls this function, 'callees' finds what this function calls"),
|
|
7194
|
+
symbolId: import_zod.z.string().optional().describe("Symbol ID (required for 'callees' direction)")
|
|
7195
|
+
},
|
|
7196
|
+
async (args) => {
|
|
7197
|
+
await ensureInitialized();
|
|
7198
|
+
if (args.direction === "callees") {
|
|
7199
|
+
if (!args.symbolId) {
|
|
7200
|
+
return { content: [{ type: "text", text: "Error: 'symbolId' is required when direction is 'callees'." }] };
|
|
7201
|
+
}
|
|
7202
|
+
const callees = await indexer.getCallees(args.symbolId);
|
|
7203
|
+
if (callees.length === 0) {
|
|
7204
|
+
return { content: [{ type: "text", text: `No callees found for symbol ${args.symbolId}.` }] };
|
|
7205
|
+
}
|
|
7206
|
+
const formatted2 = callees.map(
|
|
7207
|
+
(e, i) => `[${i + 1}] \u2192 ${e.targetName} (${e.callType}) at line ${e.line}${e.isResolved ? ` [resolved: ${e.toSymbolId}]` : " [unresolved]"}`
|
|
7208
|
+
);
|
|
7209
|
+
return { content: [{ type: "text", text: `Callees (${callees.length}):
|
|
7210
|
+
|
|
7211
|
+
${formatted2.join("\n")}` }] };
|
|
7212
|
+
}
|
|
7213
|
+
const callers = await indexer.getCallers(args.name);
|
|
7214
|
+
if (callers.length === 0) {
|
|
7215
|
+
return { content: [{ type: "text", text: `No callers found for "${args.name}".` }] };
|
|
7216
|
+
}
|
|
7217
|
+
const formatted = callers.map(
|
|
7218
|
+
(e, i) => `[${i + 1}] \u2190 from ${e.fromSymbolName ?? "<unknown>"} in ${e.fromSymbolFilePath ?? "<unknown file>"} [${e.fromSymbolId}] (${e.callType}) at line ${e.line}${e.isResolved ? " [resolved]" : " [unresolved]"}`
|
|
7219
|
+
);
|
|
7220
|
+
return { content: [{ type: "text", text: `"${args.name}" is called by ${callers.length} function(s):
|
|
7221
|
+
|
|
7222
|
+
${formatted.join("\n")}` }] };
|
|
7223
|
+
}
|
|
7224
|
+
);
|
|
4890
7225
|
server.prompt(
|
|
4891
7226
|
"search",
|
|
4892
7227
|
"Search codebase by meaning using semantic search",
|
|
@@ -4956,14 +7291,30 @@ Use a hybrid approach:
|
|
|
4956
7291
|
}]
|
|
4957
7292
|
})
|
|
4958
7293
|
);
|
|
7294
|
+
server.prompt(
|
|
7295
|
+
"definition",
|
|
7296
|
+
"Find where a symbol is defined in the codebase",
|
|
7297
|
+
{ query: import_zod.z.string().describe("Symbol name or description to find the definition of") },
|
|
7298
|
+
(args) => ({
|
|
7299
|
+
messages: [{
|
|
7300
|
+
role: "user",
|
|
7301
|
+
content: {
|
|
7302
|
+
type: "text",
|
|
7303
|
+
text: `Find the definition of: "${args.query}"
|
|
7304
|
+
|
|
7305
|
+
Use the implementation_lookup tool to find where this symbol is defined. This prioritizes real implementation files over tests, docs, and examples. If no definition is found, fall back to codebase_search for broader discovery.`
|
|
7306
|
+
}
|
|
7307
|
+
}]
|
|
7308
|
+
})
|
|
7309
|
+
);
|
|
4959
7310
|
return server;
|
|
4960
7311
|
}
|
|
4961
7312
|
|
|
4962
7313
|
// src/cli.ts
|
|
4963
7314
|
function loadJsonFile(filePath) {
|
|
4964
7315
|
try {
|
|
4965
|
-
if ((0,
|
|
4966
|
-
const content = (0,
|
|
7316
|
+
if ((0, import_fs10.existsSync)(filePath)) {
|
|
7317
|
+
const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
|
|
4967
7318
|
return JSON.parse(content);
|
|
4968
7319
|
}
|
|
4969
7320
|
} catch {
|
|
@@ -4975,9 +7326,9 @@ function loadPluginConfig(projectRoot, configPath) {
|
|
|
4975
7326
|
const config = loadJsonFile(configPath);
|
|
4976
7327
|
if (config) return config;
|
|
4977
7328
|
}
|
|
4978
|
-
const projectConfig = loadJsonFile(
|
|
7329
|
+
const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
|
|
4979
7330
|
if (projectConfig) return projectConfig;
|
|
4980
|
-
const globalConfig = loadJsonFile(
|
|
7331
|
+
const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
|
|
4981
7332
|
if (globalConfig) return globalConfig;
|
|
4982
7333
|
return {};
|
|
4983
7334
|
}
|
|
@@ -4986,14 +7337,18 @@ function parseArgs(argv) {
|
|
|
4986
7337
|
let config;
|
|
4987
7338
|
for (let i = 2; i < argv.length; i++) {
|
|
4988
7339
|
if (argv[i] === "--project" && argv[i + 1]) {
|
|
4989
|
-
project =
|
|
7340
|
+
project = path9.resolve(argv[++i]);
|
|
4990
7341
|
} else if (argv[i] === "--config" && argv[i + 1]) {
|
|
4991
|
-
config =
|
|
7342
|
+
config = path9.resolve(argv[++i]);
|
|
4992
7343
|
}
|
|
4993
7344
|
}
|
|
4994
7345
|
return { project, config };
|
|
4995
7346
|
}
|
|
4996
7347
|
async function main() {
|
|
7348
|
+
if (process.argv[2] === "eval") {
|
|
7349
|
+
const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
|
|
7350
|
+
process.exit(exitCode);
|
|
7351
|
+
}
|
|
4997
7352
|
const args = parseArgs(process.argv);
|
|
4998
7353
|
const rawConfig = loadPluginConfig(args.project, args.config);
|
|
4999
7354
|
const config = parseConfig(rawConfig);
|