opencode-codebase-index 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.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, module2) {
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 module2) {
187
- module2.exports = EventEmitter2;
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, module2) {
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(path7, checkUnignored, mode) {
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(path7);
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 = (path7, originalPath, doThrow) => {
521
- if (!isString(path7)) {
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 (!path7) {
527
+ if (!path10) {
528
528
  return doThrow(`path must not be empty`, TypeError);
529
529
  }
530
- if (checkPath.isNotRelative(path7)) {
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 = (path7) => REGEX_TEST_INVALID_PATH.test(path7);
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 path7 = originalPath && checkPath.convert(originalPath);
569
+ const path10 = originalPath && checkPath.convert(originalPath);
570
570
  checkPath(
571
- path7,
571
+ path10,
572
572
  originalPath,
573
573
  this._strictPathCheck ? throwError : RETURN_FALSE
574
574
  );
575
- return this._t(path7, cache, checkUnignored, slices);
575
+ return this._t(path10, cache, checkUnignored, slices);
576
576
  }
577
- checkIgnore(path7) {
578
- if (!REGEX_TEST_TRAILING_SLASH.test(path7)) {
579
- return this.test(path7);
577
+ checkIgnore(path10) {
578
+ if (!REGEX_TEST_TRAILING_SLASH.test(path10)) {
579
+ return this.test(path10);
580
580
  }
581
- const slices = path7.split(SLASH).filter(Boolean);
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(path7, false, MODE_CHECK_IGNORE);
594
+ return this._rules.test(path10, false, MODE_CHECK_IGNORE);
595
595
  }
596
- _t(path7, cache, checkUnignored, slices) {
597
- if (path7 in cache) {
598
- return cache[path7];
596
+ _t(path10, cache, checkUnignored, slices) {
597
+ if (path10 in cache) {
598
+ return cache[path10];
599
599
  }
600
600
  if (!slices) {
601
- slices = path7.split(SLASH).filter(Boolean);
601
+ slices = path10.split(SLASH).filter(Boolean);
602
602
  }
603
603
  slices.pop();
604
604
  if (!slices.length) {
605
- return cache[path7] = this._rules.test(path7, checkUnignored, MODE_IGNORE);
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[path7] = parent.ignored ? parent : this._rules.test(path7, checkUnignored, MODE_IGNORE);
613
+ return cache[path10] = parent.ignored ? parent : this._rules.test(path10, checkUnignored, MODE_IGNORE);
614
614
  }
615
- ignores(path7) {
616
- return this._test(path7, this._ignoreCache, false).ignored;
615
+ ignores(path10) {
616
+ return this._test(path10, this._ignoreCache, false).ignored;
617
617
  }
618
618
  createFilter() {
619
- return (path7) => !this.ignores(path7);
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(path7) {
626
- return this._test(path7, this._testCache, true);
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 = (path7) => checkPath(path7 && checkPath.convert(path7), path7, RETURN_FALSE);
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 = (path7) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path7) || isNotRelative(path7);
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
- module2.exports = factory;
643
+ module3.exports = factory;
644
644
  factory.default = factory;
645
- module2.exports.isPathValid = isPathValid;
646
- define(module2.exports, /* @__PURE__ */ Symbol.for("setupWindows"), setupWindows);
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 import_fs5 = require("fs");
653
- var path6 = __toESM(require("path"), 1);
654
- var os3 = __toESM(require("os"), 1);
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}",
@@ -754,6 +754,27 @@ var DEFAULT_PROVIDER_MODELS = {
754
754
  "ollama": "nomic-embed-text"
755
755
  };
756
756
 
757
+ // src/config/env-substitution.ts
758
+ var ENV_REFERENCE_PATTERN = /^\{env:([A-Z_][A-Z0-9_]*)\}$/;
759
+ var ENV_REFERENCE_LIKE_PATTERN = /\{env:[^}]+\}/;
760
+ function substituteEnvString(value, keyPath) {
761
+ const match = value.match(ENV_REFERENCE_PATTERN);
762
+ if (!match) {
763
+ if (ENV_REFERENCE_LIKE_PATTERN.test(value)) {
764
+ throw new Error(
765
+ `Invalid environment variable reference at '${keyPath}'. Expected the entire string to match '{env:VAR_NAME}' with VAR_NAME matching [A-Z_][A-Z0-9_]*.`
766
+ );
767
+ }
768
+ return value;
769
+ }
770
+ const variableName = match[1];
771
+ const envValue = process.env[variableName];
772
+ if (envValue === void 0) {
773
+ throw new Error(`Missing environment variable '${variableName}' referenced by config at '${keyPath}'.`);
774
+ }
775
+ return envValue;
776
+ }
777
+
757
778
  // src/config/schema.ts
758
779
  function getDefaultIndexingConfig() {
759
780
  return {
@@ -811,11 +832,27 @@ function isValidScope(value) {
811
832
  function isStringArray(value) {
812
833
  return Array.isArray(value) && value.every((item) => typeof item === "string");
813
834
  }
835
+ function getResolvedString(value, keyPath) {
836
+ if (typeof value !== "string") {
837
+ return void 0;
838
+ }
839
+ return substituteEnvString(value, keyPath);
840
+ }
841
+ function getResolvedStringArray(value, keyPath) {
842
+ if (!isStringArray(value)) {
843
+ return void 0;
844
+ }
845
+ return value.map((item, index) => substituteEnvString(item, `${keyPath}[${index}]`));
846
+ }
814
847
  function isValidLogLevel(value) {
815
848
  return typeof value === "string" && VALID_LOG_LEVELS.includes(value);
816
849
  }
817
850
  function parseConfig(raw) {
818
851
  const input = raw && typeof raw === "object" ? raw : {};
852
+ const embeddingProviderValue = getResolvedString(input.embeddingProvider, "$root.embeddingProvider");
853
+ const scopeValue = getResolvedString(input.scope, "$root.scope");
854
+ const includeValue = getResolvedStringArray(input.include, "$root.include");
855
+ const excludeValue = getResolvedStringArray(input.exclude, "$root.exclude");
819
856
  const defaultIndexing = getDefaultIndexingConfig();
820
857
  const defaultSearch = getDefaultSearchConfig();
821
858
  const defaultDebug = getDefaultDebugConfig();
@@ -858,19 +895,23 @@ function parseConfig(raw) {
858
895
  let embeddingProvider;
859
896
  let embeddingModel = void 0;
860
897
  let customProvider = void 0;
861
- if (input.embeddingProvider === "custom") {
898
+ if (embeddingProviderValue === "custom") {
862
899
  embeddingProvider = "custom";
863
900
  const rawCustom = input.customProvider && typeof input.customProvider === "object" ? input.customProvider : null;
864
- if (rawCustom && typeof rawCustom.baseUrl === "string" && rawCustom.baseUrl.trim().length > 0 && typeof rawCustom.model === "string" && rawCustom.model.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
901
+ const baseUrlValue = getResolvedString(rawCustom?.baseUrl, "$root.customProvider.baseUrl");
902
+ const modelValue = getResolvedString(rawCustom?.model, "$root.customProvider.model");
903
+ const apiKeyValue = getResolvedString(rawCustom?.apiKey, "$root.customProvider.apiKey");
904
+ if (rawCustom && typeof baseUrlValue === "string" && baseUrlValue.trim().length > 0 && typeof modelValue === "string" && modelValue.trim().length > 0 && typeof rawCustom.dimensions === "number" && Number.isInteger(rawCustom.dimensions) && rawCustom.dimensions > 0) {
865
905
  customProvider = {
866
- baseUrl: rawCustom.baseUrl.trim().replace(/\/+$/, ""),
867
- model: rawCustom.model,
906
+ baseUrl: baseUrlValue.trim().replace(/\/+$/, ""),
907
+ model: modelValue,
868
908
  dimensions: rawCustom.dimensions,
869
- apiKey: typeof rawCustom.apiKey === "string" ? rawCustom.apiKey : void 0,
909
+ apiKey: apiKeyValue,
870
910
  maxTokens: typeof rawCustom.maxTokens === "number" ? rawCustom.maxTokens : void 0,
871
911
  timeoutMs: typeof rawCustom.timeoutMs === "number" ? Math.max(1e3, rawCustom.timeoutMs) : void 0,
872
912
  concurrency: typeof rawCustom.concurrency === "number" ? Math.max(1, Math.floor(rawCustom.concurrency)) : void 0,
873
- requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0
913
+ requestIntervalMs: typeof rawCustom.requestIntervalMs === "number" ? Math.max(0, Math.floor(rawCustom.requestIntervalMs)) : void 0,
914
+ maxBatchSize: typeof rawCustom.maxBatchSize === "number" ? Math.max(1, Math.floor(rawCustom.maxBatchSize)) : typeof rawCustom.max_batch_size === "number" ? Math.max(1, Math.floor(rawCustom.max_batch_size)) : void 0
874
915
  };
875
916
  if (!/\/v\d+\/?$/.test(customProvider.baseUrl)) {
876
917
  console.warn(
@@ -882,10 +923,16 @@ function parseConfig(raw) {
882
923
  "embeddingProvider is 'custom' but customProvider config is missing or invalid. Required fields: baseUrl (string), model (string), dimensions (positive integer)."
883
924
  );
884
925
  }
885
- } else if (isValidProvider(input.embeddingProvider)) {
886
- embeddingProvider = input.embeddingProvider;
887
- if (input.embeddingModel) {
888
- embeddingModel = isValidModel(input.embeddingModel, embeddingProvider) ? input.embeddingModel : DEFAULT_PROVIDER_MODELS[embeddingProvider];
926
+ } else if (isValidProvider(embeddingProviderValue)) {
927
+ embeddingProvider = embeddingProviderValue;
928
+ const rawEmbeddingModel = input.embeddingModel;
929
+ if (typeof rawEmbeddingModel === "string") {
930
+ const embeddingModelValue = substituteEnvString(rawEmbeddingModel, "$root.embeddingModel");
931
+ if (embeddingModelValue) {
932
+ embeddingModel = isValidModel(embeddingModelValue, embeddingProvider) ? embeddingModelValue : DEFAULT_PROVIDER_MODELS[embeddingProvider];
933
+ }
934
+ } else if (rawEmbeddingModel) {
935
+ embeddingModel = DEFAULT_PROVIDER_MODELS[embeddingProvider];
889
936
  }
890
937
  } else {
891
938
  embeddingProvider = "auto";
@@ -894,9 +941,9 @@ function parseConfig(raw) {
894
941
  embeddingProvider,
895
942
  embeddingModel,
896
943
  customProvider,
897
- scope: isValidScope(input.scope) ? input.scope : "project",
898
- include: isStringArray(input.include) ? input.include : DEFAULT_INCLUDE,
899
- exclude: isStringArray(input.exclude) ? input.exclude : DEFAULT_EXCLUDE,
944
+ scope: isValidScope(scopeValue) ? scopeValue : "project",
945
+ include: includeValue ?? DEFAULT_INCLUDE,
946
+ exclude: excludeValue ?? DEFAULT_EXCLUDE,
900
947
  indexing,
901
948
  search,
902
949
  debug
@@ -909,13 +956,197 @@ function getDefaultModelForProvider(provider) {
909
956
  }
910
957
  var availableProviders = Object.keys(EMBEDDING_MODELS);
911
958
 
912
- // src/mcp-server.ts
913
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
914
- var import_zod = require("zod");
959
+ // src/eval/cli.ts
960
+ var path8 = __toESM(require("path"), 1);
961
+
962
+ // src/eval/compare.ts
963
+ function metricDelta(current, baseline) {
964
+ const absolute = current - baseline;
965
+ const relativePct = baseline === 0 ? current === 0 ? 0 : 100 : absolute / baseline * 100;
966
+ return {
967
+ current,
968
+ baseline,
969
+ absolute,
970
+ relativePct
971
+ };
972
+ }
973
+ function compareSummaries(current, baseline, againstPath) {
974
+ return {
975
+ againstPath,
976
+ deltas: {
977
+ hitAt1: metricDelta(current.metrics.hitAt1, baseline.metrics.hitAt1),
978
+ hitAt3: metricDelta(current.metrics.hitAt3, baseline.metrics.hitAt3),
979
+ hitAt5: metricDelta(current.metrics.hitAt5, baseline.metrics.hitAt5),
980
+ hitAt10: metricDelta(current.metrics.hitAt10, baseline.metrics.hitAt10),
981
+ mrrAt10: metricDelta(current.metrics.mrrAt10, baseline.metrics.mrrAt10),
982
+ ndcgAt10: metricDelta(current.metrics.ndcgAt10, baseline.metrics.ndcgAt10),
983
+ latencyP50Ms: metricDelta(current.metrics.latencyMs.p50, baseline.metrics.latencyMs.p50),
984
+ latencyP95Ms: metricDelta(current.metrics.latencyMs.p95, baseline.metrics.latencyMs.p95),
985
+ latencyP99Ms: metricDelta(current.metrics.latencyMs.p99, baseline.metrics.latencyMs.p99),
986
+ embeddingCallCount: metricDelta(
987
+ current.metrics.embedding.callCount,
988
+ baseline.metrics.embedding.callCount
989
+ ),
990
+ estimatedCostUsd: metricDelta(
991
+ current.metrics.embedding.estimatedCostUsd,
992
+ baseline.metrics.embedding.estimatedCostUsd
993
+ )
994
+ }
995
+ };
996
+ }
997
+
998
+ // src/eval/reports.ts
999
+ var import_fs = require("fs");
1000
+ var path = __toESM(require("path"), 1);
1001
+ function formatPct(value) {
1002
+ return `${(value * 100).toFixed(2)}%`;
1003
+ }
1004
+ function formatMs(value) {
1005
+ return `${value.toFixed(3)}ms`;
1006
+ }
1007
+ function formatUsd(value) {
1008
+ return `$${value.toFixed(6)}`;
1009
+ }
1010
+ function signed(value, digits = 4) {
1011
+ const formatted = value.toFixed(digits);
1012
+ return value > 0 ? `+${formatted}` : formatted;
1013
+ }
1014
+ function loadSummary(summaryPath) {
1015
+ const raw = (0, import_fs.readFileSync)(summaryPath, "utf-8");
1016
+ return JSON.parse(raw);
1017
+ }
1018
+ function createRunDirectory(outputRoot, timestampOverride) {
1019
+ const timestamp = (timestampOverride ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/[:.]/g, "-");
1020
+ const dir = path.join(outputRoot, timestamp);
1021
+ (0, import_fs.mkdirSync)(dir, { recursive: true });
1022
+ return dir;
1023
+ }
1024
+ function writeJson(filePath, value) {
1025
+ (0, import_fs.writeFileSync)(filePath, JSON.stringify(value, null, 2), "utf-8");
1026
+ }
1027
+ function writeText(filePath, value) {
1028
+ (0, import_fs.writeFileSync)(filePath, value, "utf-8");
1029
+ }
1030
+ function createSummaryMarkdown(summary, comparison, gate, sweep) {
1031
+ const lines = [];
1032
+ lines.push("# Evaluation Summary");
1033
+ lines.push("");
1034
+ lines.push(`- Generated: ${summary.generatedAt}`);
1035
+ lines.push(`- Dataset: ${summary.datasetName} (v${summary.datasetVersion})`);
1036
+ lines.push(`- Query count: ${summary.queryCount}`);
1037
+ lines.push(
1038
+ `- Search config: fusion=${summary.searchConfig.fusionStrategy}, hybridWeight=${summary.searchConfig.hybridWeight}, rrfK=${summary.searchConfig.rrfK}, rerankTopN=${summary.searchConfig.rerankTopN}`
1039
+ );
1040
+ lines.push("");
1041
+ lines.push("## Metrics");
1042
+ lines.push("");
1043
+ lines.push("| Metric | Value |");
1044
+ lines.push("|---|---:|");
1045
+ lines.push(`| Hit@1 | ${formatPct(summary.metrics.hitAt1)} |`);
1046
+ lines.push(`| Hit@3 | ${formatPct(summary.metrics.hitAt3)} |`);
1047
+ lines.push(`| Hit@5 | ${formatPct(summary.metrics.hitAt5)} |`);
1048
+ lines.push(`| Hit@10 | ${formatPct(summary.metrics.hitAt10)} |`);
1049
+ lines.push(`| MRR@10 | ${summary.metrics.mrrAt10.toFixed(4)} |`);
1050
+ lines.push(`| nDCG@10 | ${summary.metrics.ndcgAt10.toFixed(4)} |`);
1051
+ lines.push(`| Latency p50 | ${formatMs(summary.metrics.latencyMs.p50)} |`);
1052
+ lines.push(`| Latency p95 | ${formatMs(summary.metrics.latencyMs.p95)} |`);
1053
+ lines.push(`| Latency p99 | ${formatMs(summary.metrics.latencyMs.p99)} |`);
1054
+ lines.push(`| Embedding calls | ${summary.metrics.embedding.callCount} |`);
1055
+ lines.push(`| Embedding tokens | ${summary.metrics.tokenEstimate.embeddingTokensUsed} |`);
1056
+ lines.push(`| Estimated embedding cost | ${formatUsd(summary.metrics.embedding.estimatedCostUsd)} |`);
1057
+ lines.push("");
1058
+ lines.push("## Failure Buckets");
1059
+ lines.push("");
1060
+ lines.push("| Bucket | Count |");
1061
+ lines.push("|---|---:|");
1062
+ lines.push(
1063
+ `| wrong-file | ${summary.metrics.failureBuckets["wrong-file"]} |`
1064
+ );
1065
+ lines.push(
1066
+ `| wrong-symbol | ${summary.metrics.failureBuckets["wrong-symbol"]} |`
1067
+ );
1068
+ lines.push(
1069
+ `| docs/tests outranking source | ${summary.metrics.failureBuckets["docs-tests-outranking-source"]} |`
1070
+ );
1071
+ lines.push(
1072
+ `| no relevant hit in top-k | ${summary.metrics.failureBuckets["no-relevant-hit-top-k"]} |`
1073
+ );
1074
+ lines.push("");
1075
+ if (comparison) {
1076
+ lines.push("## Comparison vs Baseline");
1077
+ lines.push("");
1078
+ lines.push(`- Against: ${comparison.againstPath}`);
1079
+ lines.push("");
1080
+ lines.push("| Metric | Baseline | Current | Delta |");
1081
+ lines.push("|---|---:|---:|---:|");
1082
+ lines.push(
1083
+ `| Hit@5 | ${formatPct(comparison.deltas.hitAt5.baseline)} | ${formatPct(comparison.deltas.hitAt5.current)} | ${signed(comparison.deltas.hitAt5.absolute)} |`
1084
+ );
1085
+ lines.push(
1086
+ `| MRR@10 | ${comparison.deltas.mrrAt10.baseline.toFixed(4)} | ${comparison.deltas.mrrAt10.current.toFixed(4)} | ${signed(comparison.deltas.mrrAt10.absolute)} |`
1087
+ );
1088
+ lines.push(
1089
+ `| nDCG@10 | ${comparison.deltas.ndcgAt10.baseline.toFixed(4)} | ${comparison.deltas.ndcgAt10.current.toFixed(4)} | ${signed(comparison.deltas.ndcgAt10.absolute)} |`
1090
+ );
1091
+ lines.push(
1092
+ `| p95 latency (ms) | ${comparison.deltas.latencyP95Ms.baseline.toFixed(3)} | ${comparison.deltas.latencyP95Ms.current.toFixed(3)} | ${signed(comparison.deltas.latencyP95Ms.absolute, 3)} |`
1093
+ );
1094
+ lines.push("");
1095
+ }
1096
+ if (gate) {
1097
+ lines.push("## CI Gate");
1098
+ lines.push("");
1099
+ lines.push(`- Result: ${gate.passed ? "PASS \u2705" : "FAIL \u274C"}`);
1100
+ if (gate.violations.length > 0) {
1101
+ lines.push("- Violations:");
1102
+ for (const violation of gate.violations) {
1103
+ lines.push(` - ${violation.metric}: ${violation.message}`);
1104
+ }
1105
+ }
1106
+ lines.push("");
1107
+ }
1108
+ if (sweep) {
1109
+ lines.push("## Parameter Sweep");
1110
+ lines.push("");
1111
+ lines.push(`- Run count: ${sweep.runCount}`);
1112
+ if (sweep.bestByHitAt5) {
1113
+ lines.push(
1114
+ `- 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}`
1115
+ );
1116
+ }
1117
+ if (sweep.bestByMrrAt10) {
1118
+ lines.push(
1119
+ `- 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}`
1120
+ );
1121
+ }
1122
+ if (sweep.bestByP95Latency) {
1123
+ lines.push(
1124
+ `- 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}`
1125
+ );
1126
+ }
1127
+ lines.push("");
1128
+ }
1129
+ return `${lines.join("\n")}
1130
+ `;
1131
+ }
1132
+ function buildPerQueryArtifact(perQuery) {
1133
+ return {
1134
+ queryCount: perQuery.length,
1135
+ queries: [...perQuery].sort((a, b) => a.id.localeCompare(b.id))
1136
+ };
1137
+ }
1138
+
1139
+ // src/eval/runner.ts
1140
+ var import_fs7 = require("fs");
1141
+ var import_fs8 = require("fs");
1142
+ var import_fs9 = require("fs");
1143
+ var os3 = __toESM(require("os"), 1);
1144
+ var path7 = __toESM(require("path"), 1);
1145
+ var import_perf_hooks2 = require("perf_hooks");
915
1146
 
916
1147
  // src/indexer/index.ts
917
- var import_fs4 = require("fs");
918
- var path5 = __toESM(require("path"), 1);
1148
+ var import_fs5 = require("fs");
1149
+ var path6 = __toESM(require("path"), 1);
919
1150
  var import_perf_hooks = require("perf_hooks");
920
1151
 
921
1152
  // node_modules/eventemitter3/index.mjs
@@ -940,7 +1171,7 @@ function pTimeout(promise, options) {
940
1171
  } = options;
941
1172
  let timer;
942
1173
  let abortHandler;
943
- const wrappedPromise = new Promise((resolve4, reject) => {
1174
+ const wrappedPromise = new Promise((resolve5, reject) => {
944
1175
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
945
1176
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
946
1177
  }
@@ -954,7 +1185,7 @@ function pTimeout(promise, options) {
954
1185
  };
955
1186
  signal.addEventListener("abort", abortHandler, { once: true });
956
1187
  }
957
- promise.then(resolve4, reject);
1188
+ promise.then(resolve5, reject);
958
1189
  if (milliseconds === Number.POSITIVE_INFINITY) {
959
1190
  return;
960
1191
  }
@@ -962,7 +1193,7 @@ function pTimeout(promise, options) {
962
1193
  timer = customTimers.setTimeout.call(void 0, () => {
963
1194
  if (fallback) {
964
1195
  try {
965
- resolve4(fallback());
1196
+ resolve5(fallback());
966
1197
  } catch (error) {
967
1198
  reject(error);
968
1199
  }
@@ -972,7 +1203,7 @@ function pTimeout(promise, options) {
972
1203
  promise.cancel();
973
1204
  }
974
1205
  if (message === false) {
975
- resolve4();
1206
+ resolve5();
976
1207
  } else if (message instanceof Error) {
977
1208
  reject(message);
978
1209
  } else {
@@ -1362,7 +1593,7 @@ var PQueue = class extends import_index.default {
1362
1593
  // Assign unique ID if not provided
1363
1594
  id: options.id ?? (this.#idAssigner++).toString()
1364
1595
  };
1365
- return new Promise((resolve4, reject) => {
1596
+ return new Promise((resolve5, reject) => {
1366
1597
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1367
1598
  this.#queue.enqueue(async () => {
1368
1599
  this.#pending++;
@@ -1400,7 +1631,7 @@ var PQueue = class extends import_index.default {
1400
1631
  })]);
1401
1632
  }
1402
1633
  const result = await operation;
1403
- resolve4(result);
1634
+ resolve5(result);
1404
1635
  this.emit("completed", result);
1405
1636
  } catch (error) {
1406
1637
  reject(error);
@@ -1557,13 +1788,13 @@ var PQueue = class extends import_index.default {
1557
1788
  });
1558
1789
  }
1559
1790
  async #onEvent(event, filter) {
1560
- return new Promise((resolve4) => {
1791
+ return new Promise((resolve5) => {
1561
1792
  const listener = () => {
1562
1793
  if (filter && !filter()) {
1563
1794
  return;
1564
1795
  }
1565
1796
  this.off(event, listener);
1566
- resolve4();
1797
+ resolve5();
1567
1798
  };
1568
1799
  this.on(event, listener);
1569
1800
  });
@@ -1848,7 +2079,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1848
2079
  const finalDelay = Math.min(delayTime, remainingTime);
1849
2080
  options.signal?.throwIfAborted();
1850
2081
  if (finalDelay > 0) {
1851
- await new Promise((resolve4, reject) => {
2082
+ await new Promise((resolve5, reject) => {
1852
2083
  const onAbort = () => {
1853
2084
  clearTimeout(timeoutToken);
1854
2085
  options.signal?.removeEventListener("abort", onAbort);
@@ -1856,7 +2087,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
1856
2087
  };
1857
2088
  const timeoutToken = setTimeout(() => {
1858
2089
  options.signal?.removeEventListener("abort", onAbort);
1859
- resolve4();
2090
+ resolve5();
1860
2091
  }, finalDelay);
1861
2092
  if (options.unref) {
1862
2093
  timeoutToken.unref?.();
@@ -1917,17 +2148,17 @@ async function pRetry(input, options = {}) {
1917
2148
  }
1918
2149
 
1919
2150
  // src/embeddings/detector.ts
1920
- var import_fs = require("fs");
1921
- var path = __toESM(require("path"), 1);
2151
+ var import_fs2 = require("fs");
2152
+ var path2 = __toESM(require("path"), 1);
1922
2153
  var os = __toESM(require("os"), 1);
1923
2154
  function getOpenCodeAuthPath() {
1924
- return path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
2155
+ return path2.join(os.homedir(), ".local", "share", "opencode", "auth.json");
1925
2156
  }
1926
2157
  function loadOpenCodeAuth() {
1927
2158
  const authPath = getOpenCodeAuthPath();
1928
2159
  try {
1929
- if ((0, import_fs.existsSync)(authPath)) {
1930
- return JSON.parse((0, import_fs.readFileSync)(authPath, "utf-8"));
2160
+ if ((0, import_fs2.existsSync)(authPath)) {
2161
+ return JSON.parse((0, import_fs2.readFileSync)(authPath, "utf-8"));
1931
2162
  }
1932
2163
  } catch {
1933
2164
  }
@@ -2084,7 +2315,8 @@ function createCustomProviderInfo(config) {
2084
2315
  dimensions: config.dimensions,
2085
2316
  maxTokens: config.maxTokens ?? 8192,
2086
2317
  costPer1MTokens: 0,
2087
- timeoutMs: config.timeoutMs ?? 3e4
2318
+ timeoutMs: config.timeoutMs ?? 3e4,
2319
+ maxBatchSize: config.maxBatchSize
2088
2320
  }
2089
2321
  };
2090
2322
  }
@@ -2347,21 +2579,24 @@ var CustomEmbeddingProvider = class {
2347
2579
  this.credentials = credentials;
2348
2580
  this.modelInfo = modelInfo;
2349
2581
  }
2350
- async embedQuery(query) {
2351
- const result = await this.embedBatch([query]);
2352
- return {
2353
- embedding: result.embeddings[0],
2354
- tokensUsed: result.totalTokensUsed
2355
- };
2356
- }
2357
- async embedDocument(document) {
2358
- const result = await this.embedBatch([document]);
2359
- return {
2360
- embedding: result.embeddings[0],
2361
- tokensUsed: result.totalTokensUsed
2362
- };
2582
+ splitIntoRequestBatches(texts) {
2583
+ const maxBatchSize = this.modelInfo.maxBatchSize;
2584
+ if (!maxBatchSize || texts.length <= maxBatchSize) {
2585
+ return [texts];
2586
+ }
2587
+ const batches = [];
2588
+ for (let i = 0; i < texts.length; i += maxBatchSize) {
2589
+ batches.push(texts.slice(i, i + maxBatchSize));
2590
+ }
2591
+ return batches;
2363
2592
  }
2364
- async embedBatch(texts) {
2593
+ async embedRequest(texts) {
2594
+ if (texts.length === 0) {
2595
+ return {
2596
+ embeddings: [],
2597
+ totalTokensUsed: 0
2598
+ };
2599
+ }
2365
2600
  const headers = {
2366
2601
  "Content-Type": "application/json"
2367
2602
  };
@@ -2422,6 +2657,34 @@ var CustomEmbeddingProvider = class {
2422
2657
  }
2423
2658
  throw new Error("Custom embedding API returned unexpected response format. Expected OpenAI-compatible format with data[].embedding.");
2424
2659
  }
2660
+ async embedQuery(query) {
2661
+ const result = await this.embedBatch([query]);
2662
+ return {
2663
+ embedding: result.embeddings[0],
2664
+ tokensUsed: result.totalTokensUsed
2665
+ };
2666
+ }
2667
+ async embedDocument(document) {
2668
+ const result = await this.embedBatch([document]);
2669
+ return {
2670
+ embedding: result.embeddings[0],
2671
+ tokensUsed: result.totalTokensUsed
2672
+ };
2673
+ }
2674
+ async embedBatch(texts) {
2675
+ const requestBatches = this.splitIntoRequestBatches(texts);
2676
+ const embeddings = [];
2677
+ let totalTokensUsed = 0;
2678
+ for (const batch of requestBatches) {
2679
+ const result = await this.embedRequest(batch);
2680
+ embeddings.push(...result.embeddings);
2681
+ totalTokensUsed += result.totalTokensUsed;
2682
+ }
2683
+ return {
2684
+ embeddings,
2685
+ totalTokensUsed
2686
+ };
2687
+ }
2425
2688
  getModelInfo() {
2426
2689
  return this.modelInfo;
2427
2690
  }
@@ -2429,8 +2692,8 @@ var CustomEmbeddingProvider = class {
2429
2692
 
2430
2693
  // src/utils/files.ts
2431
2694
  var import_ignore = __toESM(require_ignore(), 1);
2432
- var import_fs2 = require("fs");
2433
- var path2 = __toESM(require("path"), 1);
2695
+ var import_fs3 = require("fs");
2696
+ var path3 = __toESM(require("path"), 1);
2434
2697
  function createIgnoreFilter(projectRoot) {
2435
2698
  const ig = (0, import_ignore.default)();
2436
2699
  const defaultIgnores = [
@@ -2447,9 +2710,9 @@ function createIgnoreFilter(projectRoot) {
2447
2710
  ".opencode"
2448
2711
  ];
2449
2712
  ig.add(defaultIgnores);
2450
- const gitignorePath = path2.join(projectRoot, ".gitignore");
2451
- if ((0, import_fs2.existsSync)(gitignorePath)) {
2452
- const gitignoreContent = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
2713
+ const gitignorePath = path3.join(projectRoot, ".gitignore");
2714
+ if ((0, import_fs3.existsSync)(gitignorePath)) {
2715
+ const gitignoreContent = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
2453
2716
  ig.add(gitignoreContent);
2454
2717
  }
2455
2718
  return ig;
@@ -2463,10 +2726,10 @@ function matchGlob(filePath, pattern) {
2463
2726
  return regex.test(filePath);
2464
2727
  }
2465
2728
  async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns, ignoreFilter, maxFileSize, skipped) {
2466
- const entries = await import_fs2.promises.readdir(dir, { withFileTypes: true });
2729
+ const entries = await import_fs3.promises.readdir(dir, { withFileTypes: true });
2467
2730
  for (const entry of entries) {
2468
- const fullPath = path2.join(dir, entry.name);
2469
- const relativePath = path2.relative(projectRoot, fullPath);
2731
+ const fullPath = path3.join(dir, entry.name);
2732
+ const relativePath = path3.relative(projectRoot, fullPath);
2470
2733
  if (ignoreFilter.ignores(relativePath)) {
2471
2734
  if (entry.isFile()) {
2472
2735
  skipped.push({ path: relativePath, reason: "gitignore" });
@@ -2484,7 +2747,7 @@ async function* walkDirectory(dir, projectRoot, includePatterns, excludePatterns
2484
2747
  skipped
2485
2748
  );
2486
2749
  } else if (entry.isFile()) {
2487
- const stat = await import_fs2.promises.stat(fullPath);
2750
+ const stat = await import_fs3.promises.stat(fullPath);
2488
2751
  if (stat.size > maxFileSize) {
2489
2752
  skipped.push({ path: relativePath, reason: "too_large" });
2490
2753
  continue;
@@ -2527,6 +2790,9 @@ async function collectFiles(projectRoot, includePatterns, excludePatterns, maxFi
2527
2790
  }
2528
2791
 
2529
2792
  // src/utils/cost.ts
2793
+ function estimateTokens(text) {
2794
+ return Math.ceil(text.length / 4);
2795
+ }
2530
2796
  function estimateChunksFromFiles(files) {
2531
2797
  let totalChunks = 0;
2532
2798
  for (const file of files) {
@@ -2881,8 +3147,9 @@ function initializeLogger(config) {
2881
3147
  }
2882
3148
 
2883
3149
  // src/native/index.ts
2884
- var path3 = __toESM(require("path"), 1);
3150
+ var path4 = __toESM(require("path"), 1);
2885
3151
  var os2 = __toESM(require("os"), 1);
3152
+ var module2 = __toESM(require("module"), 1);
2886
3153
  var import_url = require("url");
2887
3154
  var import_meta = {};
2888
3155
  function getNativeBinding() {
@@ -2903,17 +3170,22 @@ function getNativeBinding() {
2903
3170
  throw new Error(`Unsupported platform: ${platform2}-${arch2}`);
2904
3171
  }
2905
3172
  let currentDir;
3173
+ let requireTarget;
2906
3174
  if (typeof import_meta !== "undefined" && import_meta.url) {
2907
- currentDir = path3.dirname((0, import_url.fileURLToPath)(import_meta.url));
3175
+ currentDir = path4.dirname((0, import_url.fileURLToPath)(import_meta.url));
3176
+ requireTarget = import_meta.url;
2908
3177
  } else if (typeof __dirname !== "undefined") {
2909
3178
  currentDir = __dirname;
3179
+ requireTarget = __filename;
2910
3180
  } else {
2911
3181
  currentDir = process.cwd();
3182
+ requireTarget = path4.join(currentDir, "index.js");
2912
3183
  }
2913
3184
  const isDevMode = currentDir.includes("/src/native");
2914
- const packageRoot = isDevMode ? path3.resolve(currentDir, "../..") : path3.resolve(currentDir, "..");
2915
- const nativePath = path3.join(packageRoot, "native", bindingName);
2916
- return require(nativePath);
3185
+ const packageRoot = isDevMode ? path4.resolve(currentDir, "../..") : path4.resolve(currentDir, "..");
3186
+ const nativePath = path4.join(packageRoot, "native", bindingName);
3187
+ const require2 = module2.createRequire(requireTarget);
3188
+ return require2(nativePath);
2917
3189
  }
2918
3190
  var native = getNativeBinding();
2919
3191
  function parseFiles(files) {
@@ -3031,7 +3303,7 @@ var VectorStore = class {
3031
3303
  var CHARS_PER_TOKEN = 4;
3032
3304
  var MAX_BATCH_TOKENS = 7500;
3033
3305
  var MAX_SINGLE_CHUNK_TOKENS = 2e3;
3034
- function estimateTokens(text) {
3306
+ function estimateTokens2(text) {
3035
3307
  return Math.ceil(text.length / CHARS_PER_TOKEN);
3036
3308
  }
3037
3309
  function createEmbeddingText(chunk, filePath) {
@@ -3096,7 +3368,7 @@ function createDynamicBatches(chunks) {
3096
3368
  let currentBatch = [];
3097
3369
  let currentTokens = 0;
3098
3370
  for (const chunk of chunks) {
3099
- const chunkTokens = estimateTokens(chunk.text);
3371
+ const chunkTokens = estimateTokens2(chunk.text);
3100
3372
  if (currentBatch.length > 0 && currentTokens + chunkTokens > MAX_BATCH_TOKENS) {
3101
3373
  batches.push(currentBatch);
3102
3374
  currentBatch = [];
@@ -3393,26 +3665,55 @@ var Database = class {
3393
3665
  };
3394
3666
 
3395
3667
  // src/git/index.ts
3396
- var import_fs3 = require("fs");
3397
- var path4 = __toESM(require("path"), 1);
3398
- var import_child_process = require("child_process");
3668
+ var import_fs4 = require("fs");
3669
+ var path5 = __toESM(require("path"), 1);
3670
+ function readPackedRefs(gitDir) {
3671
+ const packedRefsPath = path5.join(gitDir, "packed-refs");
3672
+ if (!(0, import_fs4.existsSync)(packedRefsPath)) {
3673
+ return [];
3674
+ }
3675
+ try {
3676
+ return (0, import_fs4.readFileSync)(packedRefsPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("^"));
3677
+ } catch {
3678
+ return [];
3679
+ }
3680
+ }
3681
+ function resolveCommonGitDir(gitDir) {
3682
+ const commonDirPath = path5.join(gitDir, "commondir");
3683
+ if (!(0, import_fs4.existsSync)(commonDirPath)) {
3684
+ return gitDir;
3685
+ }
3686
+ try {
3687
+ const raw = (0, import_fs4.readFileSync)(commonDirPath, "utf-8").trim();
3688
+ if (!raw) {
3689
+ return gitDir;
3690
+ }
3691
+ const resolved = path5.isAbsolute(raw) ? raw : path5.resolve(gitDir, raw);
3692
+ if ((0, import_fs4.existsSync)(resolved)) {
3693
+ return resolved;
3694
+ }
3695
+ } catch {
3696
+ return gitDir;
3697
+ }
3698
+ return gitDir;
3699
+ }
3399
3700
  function resolveGitDir(repoRoot) {
3400
- const gitPath = path4.join(repoRoot, ".git");
3401
- if (!(0, import_fs3.existsSync)(gitPath)) {
3701
+ const gitPath = path5.join(repoRoot, ".git");
3702
+ if (!(0, import_fs4.existsSync)(gitPath)) {
3402
3703
  return null;
3403
3704
  }
3404
3705
  try {
3405
- const stat = (0, import_fs3.statSync)(gitPath);
3706
+ const stat = (0, import_fs4.statSync)(gitPath);
3406
3707
  if (stat.isDirectory()) {
3407
3708
  return gitPath;
3408
3709
  }
3409
3710
  if (stat.isFile()) {
3410
- const content = (0, import_fs3.readFileSync)(gitPath, "utf-8").trim();
3711
+ const content = (0, import_fs4.readFileSync)(gitPath, "utf-8").trim();
3411
3712
  const match = content.match(/^gitdir:\s*(.+)$/);
3412
3713
  if (match) {
3413
3714
  const gitdir = match[1];
3414
- const resolvedPath = path4.isAbsolute(gitdir) ? gitdir : path4.resolve(repoRoot, gitdir);
3415
- if ((0, import_fs3.existsSync)(resolvedPath)) {
3715
+ const resolvedPath = path5.isAbsolute(gitdir) ? gitdir : path5.resolve(repoRoot, gitdir);
3716
+ if ((0, import_fs4.existsSync)(resolvedPath)) {
3416
3717
  return resolvedPath;
3417
3718
  }
3418
3719
  }
@@ -3429,12 +3730,12 @@ function getCurrentBranch(repoRoot) {
3429
3730
  if (!gitDir) {
3430
3731
  return null;
3431
3732
  }
3432
- const headPath = path4.join(gitDir, "HEAD");
3433
- if (!(0, import_fs3.existsSync)(headPath)) {
3733
+ const headPath = path5.join(gitDir, "HEAD");
3734
+ if (!(0, import_fs4.existsSync)(headPath)) {
3434
3735
  return null;
3435
3736
  }
3436
3737
  try {
3437
- const headContent = (0, import_fs3.readFileSync)(headPath, "utf-8").trim();
3738
+ const headContent = (0, import_fs4.readFileSync)(headPath, "utf-8").trim();
3438
3739
  const match = headContent.match(/^ref: refs\/heads\/(.+)$/);
3439
3740
  if (match) {
3440
3741
  return match[1];
@@ -3449,37 +3750,20 @@ function getCurrentBranch(repoRoot) {
3449
3750
  }
3450
3751
  function getBaseBranch(repoRoot) {
3451
3752
  const gitDir = resolveGitDir(repoRoot);
3753
+ const refStoreDir = gitDir ? resolveCommonGitDir(gitDir) : null;
3452
3754
  const candidates = ["main", "master", "develop", "trunk"];
3453
- if (gitDir) {
3755
+ if (refStoreDir) {
3454
3756
  for (const candidate of candidates) {
3455
- const refPath = path4.join(gitDir, "refs", "heads", candidate);
3456
- if ((0, import_fs3.existsSync)(refPath)) {
3757
+ const refPath = path5.join(refStoreDir, "refs", "heads", candidate);
3758
+ if ((0, import_fs4.existsSync)(refPath)) {
3457
3759
  return candidate;
3458
3760
  }
3459
- const packedRefsPath = path4.join(gitDir, "packed-refs");
3460
- if ((0, import_fs3.existsSync)(packedRefsPath)) {
3461
- try {
3462
- const content = (0, import_fs3.readFileSync)(packedRefsPath, "utf-8");
3463
- if (content.includes(`refs/heads/${candidate}`)) {
3464
- return candidate;
3465
- }
3466
- } catch {
3467
- }
3761
+ const packedRefs = readPackedRefs(refStoreDir);
3762
+ if (packedRefs.some((line) => line.endsWith(` refs/heads/${candidate}`))) {
3763
+ return candidate;
3468
3764
  }
3469
3765
  }
3470
3766
  }
3471
- try {
3472
- const result = (0, import_child_process.execSync)("git remote show origin", {
3473
- cwd: repoRoot,
3474
- encoding: "utf-8",
3475
- stdio: ["pipe", "pipe", "pipe"]
3476
- });
3477
- const match = result.match(/HEAD branch: (.+)/);
3478
- if (match) {
3479
- return match[1].trim();
3480
- }
3481
- } catch {
3482
- }
3483
3767
  return getCurrentBranch(repoRoot) ?? "main";
3484
3768
  }
3485
3769
  function getBranchOrDefault(repoRoot) {
@@ -3490,7 +3774,7 @@ function getBranchOrDefault(repoRoot) {
3490
3774
  }
3491
3775
 
3492
3776
  // src/indexer/index.ts
3493
- var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust"]);
3777
+ var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php"]);
3494
3778
  var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
3495
3779
  "function_declaration",
3496
3780
  "function",
@@ -3511,7 +3795,8 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
3511
3795
  "struct_item",
3512
3796
  "enum_item",
3513
3797
  "trait_item",
3514
- "mod_item"
3798
+ "mod_item",
3799
+ "trait_declaration"
3515
3800
  ]);
3516
3801
  function float32ArrayToBuffer(arr) {
3517
3802
  const float32 = new Float32Array(arr);
@@ -3893,8 +4178,8 @@ function stripFilePathHint(query) {
3893
4178
  const stripped = query.replace(FILE_PATH_HINT_SUFFIX_REGEX, "").trim();
3894
4179
  return stripped.length > 0 ? stripped : query;
3895
4180
  }
3896
- function buildDeterministicIdentifierPass(query, candidates, limit) {
3897
- if (classifyQueryIntentRaw(query) !== "source") {
4181
+ function buildDeterministicIdentifierPass(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
4182
+ if (!prioritizeSourcePaths) {
3898
4183
  return [];
3899
4184
  }
3900
4185
  const primary = extractPrimaryIdentifierQueryHint(query);
@@ -4110,9 +4395,8 @@ function rankHybridResults(query, semanticResults, keywordResults, options) {
4110
4395
  const fused = options.fusionStrategy === "rrf" ? fuseResultsRrf(semanticResults, keywordResults, options.rrfK, overfetchLimit) : fuseResultsWeighted(semanticResults, keywordResults, options.hybridWeight, overfetchLimit);
4111
4396
  const rerankPoolLimit = Math.max(overfetchLimit, options.rerankTopN * 3, options.limit * 6);
4112
4397
  const rerankPool = fused.slice(0, rerankPoolLimit);
4113
- const intent = classifyQueryIntentRaw(query);
4114
4398
  return rerankResults(query, rerankPool, options.rerankTopN, {
4115
- prioritizeSourcePaths: intent === "source"
4399
+ prioritizeSourcePaths: options.prioritizeSourcePaths ?? classifyQueryIntentRaw(query) === "source"
4116
4400
  });
4117
4401
  }
4118
4402
  function rankSemanticOnlyResults(query, semanticResults, options) {
@@ -4122,11 +4406,11 @@ function rankSemanticOnlyResults(query, semanticResults, options) {
4122
4406
  prioritizeSourcePaths: options.prioritizeSourcePaths ?? false
4123
4407
  });
4124
4408
  }
4125
- function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds) {
4409
+ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCandidates, database, branchChunkIds, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
4126
4410
  if (combined.length === 0) {
4127
4411
  return combined;
4128
4412
  }
4129
- if (classifyQueryIntentRaw(query) !== "source") {
4413
+ if (!prioritizeSourcePaths) {
4130
4414
  return combined;
4131
4415
  }
4132
4416
  const identifierHints = extractIdentifierHints(query);
@@ -4216,8 +4500,8 @@ function promoteIdentifierMatches(query, combined, semanticCandidates, keywordCa
4216
4500
  const remainder = combined.filter((candidate) => !promotedIds.has(candidate.id));
4217
4501
  return [...promoted, ...remainder];
4218
4502
  }
4219
- function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates) {
4220
- if (classifyQueryIntentRaw(query) !== "source") {
4503
+ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallbackCandidates, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
4504
+ if (!prioritizeSourcePaths) {
4221
4505
  return [];
4222
4506
  }
4223
4507
  const identifierHints = extractIdentifierHints(query);
@@ -4362,8 +4646,8 @@ function buildSymbolDefinitionLane(query, database, branchChunkIds, limit, fallb
4362
4646
  const withFallback = Array.from(symbolCandidates.values()).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
4363
4647
  return withFallback.slice(0, Math.max(limit * 2, limit));
4364
4648
  }
4365
- function buildIdentifierDefinitionLane(query, candidates, limit) {
4366
- if (classifyQueryIntentRaw(query) !== "source") {
4649
+ function buildIdentifierDefinitionLane(query, candidates, limit, prioritizeSourcePaths = classifyQueryIntentRaw(query) === "source") {
4650
+ if (!prioritizeSourcePaths) {
4367
4651
  return [];
4368
4652
  }
4369
4653
  const primaryHint = extractPrimaryIdentifierQueryHint(query);
@@ -4448,22 +4732,22 @@ var Indexer = class {
4448
4732
  this.projectRoot = projectRoot;
4449
4733
  this.config = config;
4450
4734
  this.indexPath = this.getIndexPath();
4451
- this.fileHashCachePath = path5.join(this.indexPath, "file-hashes.json");
4452
- this.failedBatchesPath = path5.join(this.indexPath, "failed-batches.json");
4453
- this.indexingLockPath = path5.join(this.indexPath, "indexing.lock");
4735
+ this.fileHashCachePath = path6.join(this.indexPath, "file-hashes.json");
4736
+ this.failedBatchesPath = path6.join(this.indexPath, "failed-batches.json");
4737
+ this.indexingLockPath = path6.join(this.indexPath, "indexing.lock");
4454
4738
  this.logger = initializeLogger(config.debug);
4455
4739
  }
4456
4740
  getIndexPath() {
4457
4741
  if (this.config.scope === "global") {
4458
4742
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
4459
- return path5.join(homeDir, ".opencode", "global-index");
4743
+ return path6.join(homeDir, ".opencode", "global-index");
4460
4744
  }
4461
- return path5.join(this.projectRoot, ".opencode", "index");
4745
+ return path6.join(this.projectRoot, ".opencode", "index");
4462
4746
  }
4463
4747
  loadFileHashCache() {
4464
4748
  try {
4465
- if ((0, import_fs4.existsSync)(this.fileHashCachePath)) {
4466
- const data = (0, import_fs4.readFileSync)(this.fileHashCachePath, "utf-8");
4749
+ if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
4750
+ const data = (0, import_fs5.readFileSync)(this.fileHashCachePath, "utf-8");
4467
4751
  const parsed = JSON.parse(data);
4468
4752
  this.fileHashCache = new Map(Object.entries(parsed));
4469
4753
  }
@@ -4480,28 +4764,28 @@ var Indexer = class {
4480
4764
  }
4481
4765
  atomicWriteSync(targetPath, data) {
4482
4766
  const tempPath = `${targetPath}.tmp`;
4483
- (0, import_fs4.writeFileSync)(tempPath, data);
4484
- (0, import_fs4.renameSync)(tempPath, targetPath);
4767
+ (0, import_fs5.writeFileSync)(tempPath, data);
4768
+ (0, import_fs5.renameSync)(tempPath, targetPath);
4485
4769
  }
4486
4770
  checkForInterruptedIndexing() {
4487
- return (0, import_fs4.existsSync)(this.indexingLockPath);
4771
+ return (0, import_fs5.existsSync)(this.indexingLockPath);
4488
4772
  }
4489
4773
  acquireIndexingLock() {
4490
4774
  const lockData = {
4491
4775
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4492
4776
  pid: process.pid
4493
4777
  };
4494
- (0, import_fs4.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
4778
+ (0, import_fs5.writeFileSync)(this.indexingLockPath, JSON.stringify(lockData));
4495
4779
  }
4496
4780
  releaseIndexingLock() {
4497
- if ((0, import_fs4.existsSync)(this.indexingLockPath)) {
4498
- (0, import_fs4.unlinkSync)(this.indexingLockPath);
4781
+ if ((0, import_fs5.existsSync)(this.indexingLockPath)) {
4782
+ (0, import_fs5.unlinkSync)(this.indexingLockPath);
4499
4783
  }
4500
4784
  }
4501
4785
  async recoverFromInterruptedIndexing() {
4502
4786
  this.logger.warn("Detected interrupted indexing session, recovering...");
4503
- if ((0, import_fs4.existsSync)(this.fileHashCachePath)) {
4504
- (0, import_fs4.unlinkSync)(this.fileHashCachePath);
4787
+ if ((0, import_fs5.existsSync)(this.fileHashCachePath)) {
4788
+ (0, import_fs5.unlinkSync)(this.fileHashCachePath);
4505
4789
  }
4506
4790
  await this.healthCheck();
4507
4791
  this.releaseIndexingLock();
@@ -4509,8 +4793,8 @@ var Indexer = class {
4509
4793
  }
4510
4794
  loadFailedBatches() {
4511
4795
  try {
4512
- if ((0, import_fs4.existsSync)(this.failedBatchesPath)) {
4513
- const data = (0, import_fs4.readFileSync)(this.failedBatchesPath, "utf-8");
4796
+ if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
4797
+ const data = (0, import_fs5.readFileSync)(this.failedBatchesPath, "utf-8");
4514
4798
  return JSON.parse(data);
4515
4799
  }
4516
4800
  } catch {
@@ -4520,13 +4804,13 @@ var Indexer = class {
4520
4804
  }
4521
4805
  saveFailedBatches(batches) {
4522
4806
  if (batches.length === 0) {
4523
- if ((0, import_fs4.existsSync)(this.failedBatchesPath)) {
4524
- import_fs4.promises.unlink(this.failedBatchesPath).catch(() => {
4807
+ if ((0, import_fs5.existsSync)(this.failedBatchesPath)) {
4808
+ import_fs5.promises.unlink(this.failedBatchesPath).catch(() => {
4525
4809
  });
4526
4810
  }
4527
4811
  return;
4528
4812
  }
4529
- (0, import_fs4.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
4813
+ (0, import_fs5.writeFileSync)(this.failedBatchesPath, JSON.stringify(batches, null, 2));
4530
4814
  }
4531
4815
  addFailedBatch(batch, error) {
4532
4816
  const existing = this.loadFailedBatches();
@@ -4583,26 +4867,26 @@ var Indexer = class {
4583
4867
  scope: this.config.scope
4584
4868
  });
4585
4869
  this.provider = createEmbeddingProvider(this.configuredProviderInfo);
4586
- await import_fs4.promises.mkdir(this.indexPath, { recursive: true });
4870
+ await import_fs5.promises.mkdir(this.indexPath, { recursive: true });
4587
4871
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
4588
- const storePath = path5.join(this.indexPath, "vectors");
4872
+ const storePath = path6.join(this.indexPath, "vectors");
4589
4873
  this.store = new VectorStore(storePath, dimensions);
4590
- const indexFilePath = path5.join(this.indexPath, "vectors.usearch");
4591
- if ((0, import_fs4.existsSync)(indexFilePath)) {
4874
+ const indexFilePath = path6.join(this.indexPath, "vectors.usearch");
4875
+ if ((0, import_fs5.existsSync)(indexFilePath)) {
4592
4876
  this.store.load();
4593
4877
  }
4594
- const invertedIndexPath = path5.join(this.indexPath, "inverted-index.json");
4878
+ const invertedIndexPath = path6.join(this.indexPath, "inverted-index.json");
4595
4879
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
4596
4880
  try {
4597
4881
  this.invertedIndex.load();
4598
4882
  } catch {
4599
- if ((0, import_fs4.existsSync)(invertedIndexPath)) {
4600
- await import_fs4.promises.unlink(invertedIndexPath);
4883
+ if ((0, import_fs5.existsSync)(invertedIndexPath)) {
4884
+ await import_fs5.promises.unlink(invertedIndexPath);
4601
4885
  }
4602
4886
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
4603
4887
  }
4604
- const dbPath = path5.join(this.indexPath, "codebase.db");
4605
- const dbIsNew = !(0, import_fs4.existsSync)(dbPath);
4888
+ const dbPath = path6.join(this.indexPath, "codebase.db");
4889
+ const dbIsNew = !(0, import_fs5.existsSync)(dbPath);
4606
4890
  this.database = new Database(dbPath);
4607
4891
  if (this.checkForInterruptedIndexing()) {
4608
4892
  await this.recoverFromInterruptedIndexing();
@@ -4834,7 +5118,7 @@ var Indexer = class {
4834
5118
  unchangedFilePaths.add(f.path);
4835
5119
  this.logger.recordCacheHit();
4836
5120
  } else {
4837
- const content = await import_fs4.promises.readFile(f.path, "utf-8");
5121
+ const content = await import_fs5.promises.readFile(f.path, "utf-8");
4838
5122
  changedFiles.push({ path: f.path, content, hash: currentHash });
4839
5123
  this.logger.recordCacheMiss();
4840
5124
  }
@@ -4880,7 +5164,7 @@ var Indexer = class {
4880
5164
  for (const parsed of parsedFiles) {
4881
5165
  currentFilePaths.add(parsed.path);
4882
5166
  if (parsed.chunks.length === 0) {
4883
- const relativePath = path5.relative(this.projectRoot, parsed.path);
5167
+ const relativePath = path6.relative(this.projectRoot, parsed.path);
4884
5168
  stats.parseFailures.push(relativePath);
4885
5169
  }
4886
5170
  let fileChunkCount = 0;
@@ -5091,7 +5375,7 @@ var Indexer = class {
5091
5375
  for (const batch of dynamicBatches) {
5092
5376
  queue.add(async () => {
5093
5377
  if (rateLimitBackoffMs > 0) {
5094
- await new Promise((resolve4) => setTimeout(resolve4, rateLimitBackoffMs));
5378
+ await new Promise((resolve5) => setTimeout(resolve5, rateLimitBackoffMs));
5095
5379
  }
5096
5380
  try {
5097
5381
  const result = await pRetry(
@@ -5295,6 +5579,7 @@ var Indexer = class {
5295
5579
  const rrfK = this.config.search.rrfK;
5296
5580
  const rerankTopN = this.config.search.rerankTopN;
5297
5581
  const filterByBranch = options?.filterByBranch ?? true;
5582
+ const sourceIntent = options?.definitionIntent === true || classifyQueryIntentRaw(query) === "source";
5298
5583
  this.logger.search("debug", "Starting search", {
5299
5584
  query,
5300
5585
  maxResults,
@@ -5346,7 +5631,8 @@ var Indexer = class {
5346
5631
  rrfK,
5347
5632
  rerankTopN,
5348
5633
  limit: maxResults,
5349
- hybridWeight
5634
+ hybridWeight,
5635
+ prioritizeSourcePaths: sourceIntent
5350
5636
  });
5351
5637
  const fusionMs = import_perf_hooks.performance.now() - fusionStartTime;
5352
5638
  const rescued = promoteIdentifierMatches(
@@ -5355,30 +5641,33 @@ var Indexer = class {
5355
5641
  semanticCandidates,
5356
5642
  keywordCandidates,
5357
5643
  database,
5358
- branchChunkIds
5644
+ branchChunkIds,
5645
+ sourceIntent
5359
5646
  );
5360
5647
  const union = unionCandidates(semanticCandidates, keywordCandidates);
5361
5648
  const deterministicIdentifierLane = buildDeterministicIdentifierPass(
5362
5649
  query,
5363
5650
  union,
5364
- maxResults
5651
+ maxResults,
5652
+ sourceIntent
5365
5653
  );
5366
5654
  const identifierLane = buildIdentifierDefinitionLane(
5367
5655
  query,
5368
5656
  union,
5369
- maxResults
5657
+ maxResults,
5658
+ sourceIntent
5370
5659
  );
5371
5660
  const symbolLane = buildSymbolDefinitionLane(
5372
5661
  query,
5373
5662
  database,
5374
5663
  branchChunkIds,
5375
5664
  maxResults,
5376
- union
5665
+ union,
5666
+ sourceIntent
5377
5667
  );
5378
5668
  const prePrimaryLane = mergeTieredResults(deterministicIdentifierLane, identifierLane, maxResults * 4);
5379
5669
  const primaryLane = mergeTieredResults(prePrimaryLane, symbolLane, maxResults * 4);
5380
5670
  const tiered = mergeTieredResults(primaryLane, rescued, maxResults * 4);
5381
- const sourceIntent = classifyQueryIntentRaw(query) === "source";
5382
5671
  const hasCodeHints = extractCodeTermHints(query).length > 0 || extractIdentifierHints(query).length > 0;
5383
5672
  const baseFiltered = tiered.filter((r) => {
5384
5673
  if (r.score < this.config.search.minScore) return false;
@@ -5424,7 +5713,7 @@ var Indexer = class {
5424
5713
  let contextEndLine = r.metadata.endLine;
5425
5714
  if (!metadataOnly && this.config.search.includeContext) {
5426
5715
  try {
5427
- const fileContent = await import_fs4.promises.readFile(
5716
+ const fileContent = await import_fs5.promises.readFile(
5428
5717
  r.metadata.filePath,
5429
5718
  "utf-8"
5430
5719
  );
@@ -5511,7 +5800,7 @@ var Indexer = class {
5511
5800
  const removedFilePaths = [];
5512
5801
  let removedCount = 0;
5513
5802
  for (const [filePath, chunkKeys] of filePathsToChunkKeys) {
5514
- if (!(0, import_fs4.existsSync)(filePath)) {
5803
+ if (!(0, import_fs5.existsSync)(filePath)) {
5515
5804
  for (const key of chunkKeys) {
5516
5805
  store.remove(key);
5517
5806
  invertedIndex.removeChunk(key);
@@ -5703,7 +5992,7 @@ var Indexer = class {
5703
5992
  let content = "";
5704
5993
  if (this.config.search.includeContext) {
5705
5994
  try {
5706
- const fileContent = await import_fs4.promises.readFile(
5995
+ const fileContent = await import_fs5.promises.readFile(
5707
5996
  r.metadata.filePath,
5708
5997
  "utf-8"
5709
5998
  );
@@ -5735,7 +6024,896 @@ var Indexer = class {
5735
6024
  }
5736
6025
  };
5737
6026
 
6027
+ // src/eval/budget.ts
6028
+ function evaluateBudgetGate(budget, summary, comparison) {
6029
+ const BASELINE_P95_EPSILON_MS = 1e-3;
6030
+ const violations = [];
6031
+ const { thresholds } = budget;
6032
+ if (thresholds.minHitAt5 !== void 0 && summary.metrics.hitAt5 < thresholds.minHitAt5) {
6033
+ violations.push({
6034
+ metric: "minHitAt5",
6035
+ message: `Hit@5 ${summary.metrics.hitAt5.toFixed(4)} is below minimum ${thresholds.minHitAt5.toFixed(4)}`
6036
+ });
6037
+ }
6038
+ if (thresholds.minMrrAt10 !== void 0 && summary.metrics.mrrAt10 < thresholds.minMrrAt10) {
6039
+ violations.push({
6040
+ metric: "minMrrAt10",
6041
+ message: `MRR@10 ${summary.metrics.mrrAt10.toFixed(4)} is below minimum ${thresholds.minMrrAt10.toFixed(4)}`
6042
+ });
6043
+ }
6044
+ if (comparison) {
6045
+ if (thresholds.hitAt5MaxDrop !== void 0 && comparison.deltas.hitAt5.absolute < -thresholds.hitAt5MaxDrop) {
6046
+ violations.push({
6047
+ metric: "hitAt5MaxDrop",
6048
+ message: `Hit@5 drop ${comparison.deltas.hitAt5.absolute.toFixed(4)} exceeds allowed -${thresholds.hitAt5MaxDrop.toFixed(4)}`
6049
+ });
6050
+ }
6051
+ if (thresholds.mrrAt10MaxDrop !== void 0 && comparison.deltas.mrrAt10.absolute < -thresholds.mrrAt10MaxDrop) {
6052
+ violations.push({
6053
+ metric: "mrrAt10MaxDrop",
6054
+ message: `MRR@10 drop ${comparison.deltas.mrrAt10.absolute.toFixed(4)} exceeds allowed -${thresholds.mrrAt10MaxDrop.toFixed(4)}`
6055
+ });
6056
+ }
6057
+ if (thresholds.p95LatencyMaxMultiplier !== void 0) {
6058
+ const baselineP95 = comparison.deltas.latencyP95Ms.baseline;
6059
+ if (baselineP95 > BASELINE_P95_EPSILON_MS) {
6060
+ const allowed = baselineP95 * thresholds.p95LatencyMaxMultiplier;
6061
+ if (summary.metrics.latencyMs.p95 > allowed) {
6062
+ violations.push({
6063
+ metric: "p95LatencyMaxMultiplier",
6064
+ message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds allowed ${allowed.toFixed(3)}ms (${thresholds.p95LatencyMaxMultiplier.toFixed(2)}x baseline)`
6065
+ });
6066
+ }
6067
+ }
6068
+ }
6069
+ }
6070
+ if (thresholds.p95LatencyMaxAbsoluteMs !== void 0 && summary.metrics.latencyMs.p95 > thresholds.p95LatencyMaxAbsoluteMs) {
6071
+ violations.push({
6072
+ metric: "p95LatencyMaxAbsoluteMs",
6073
+ message: `p95 latency ${summary.metrics.latencyMs.p95.toFixed(3)}ms exceeds absolute maximum ${thresholds.p95LatencyMaxAbsoluteMs.toFixed(3)}ms`
6074
+ });
6075
+ }
6076
+ return {
6077
+ passed: violations.length === 0,
6078
+ budgetName: budget.name,
6079
+ violations
6080
+ };
6081
+ }
6082
+
6083
+ // src/eval/metrics.ts
6084
+ function percentile(values, p) {
6085
+ if (values.length === 0) return 0;
6086
+ if (values.length === 1) return values[0];
6087
+ const sorted = [...values].sort((a, b) => a - b);
6088
+ const x = p * (sorted.length - 1);
6089
+ const lowerIndex = Math.floor(x);
6090
+ const upperIndex = Math.ceil(x);
6091
+ if (lowerIndex === upperIndex) {
6092
+ return sorted[lowerIndex];
6093
+ }
6094
+ const fraction = x - lowerIndex;
6095
+ return sorted[lowerIndex] + fraction * (sorted[upperIndex] - sorted[lowerIndex]);
6096
+ }
6097
+ function normalizePath(input) {
6098
+ return input.replace(/\\/g, "/");
6099
+ }
6100
+ function uniqueResultsByPath(results) {
6101
+ const seen = /* @__PURE__ */ new Set();
6102
+ const unique = [];
6103
+ for (const result of results) {
6104
+ const normalized = normalizePath(result.filePath);
6105
+ if (seen.has(normalized)) continue;
6106
+ seen.add(normalized);
6107
+ unique.push(result);
6108
+ }
6109
+ return unique;
6110
+ }
6111
+ function pathMatchesExpected(actualPath, expectedPath) {
6112
+ const actual = normalizePath(actualPath);
6113
+ const expected = normalizePath(expectedPath);
6114
+ if (actual === expected) return true;
6115
+ return actual.endsWith(`/${expected}`) || expected.endsWith(`/${actual}`);
6116
+ }
6117
+ function getRelevantPaths(query) {
6118
+ const fromExact = query.expected.filePath ? [query.expected.filePath] : [];
6119
+ const fromAcceptable = query.expected.acceptableFiles ?? [];
6120
+ return Array.from(/* @__PURE__ */ new Set([...fromExact, ...fromAcceptable]));
6121
+ }
6122
+ function isRelevantResult(filePath, relevantPaths) {
6123
+ return relevantPaths.some((expected) => pathMatchesExpected(filePath, expected));
6124
+ }
6125
+ function reciprocalRankAtK(results, relevantPaths, k) {
6126
+ const top = uniqueResultsByPath(results).slice(0, k);
6127
+ for (let i = 0; i < top.length; i += 1) {
6128
+ if (isRelevantResult(top[i].filePath, relevantPaths)) {
6129
+ return 1 / (i + 1);
6130
+ }
6131
+ }
6132
+ return 0;
6133
+ }
6134
+ function ndcgAtK(results, relevantPaths, k) {
6135
+ const top = uniqueResultsByPath(results).slice(0, k);
6136
+ const dcg = top.reduce((sum, result, i) => {
6137
+ const rel = isRelevantResult(result.filePath, relevantPaths) ? 1 : 0;
6138
+ return sum + rel / Math.log2(i + 2);
6139
+ }, 0);
6140
+ const idealLen = Math.min(k, relevantPaths.length);
6141
+ const idcg = Array.from({ length: idealLen }, (_, i) => 1 / Math.log2(i + 2)).reduce(
6142
+ (sum, value) => sum + value,
6143
+ 0
6144
+ );
6145
+ return idcg === 0 ? 0 : dcg / idcg;
6146
+ }
6147
+ function isDocsOrTestsPath(filePath) {
6148
+ const lowered = normalizePath(filePath).toLowerCase();
6149
+ return lowered.includes("/docs/") || lowered.includes("/test/") || lowered.includes("/tests/") || lowered.includes("readme") || lowered.includes("/benchmarks/");
6150
+ }
6151
+ function classifyFailureBucket(query, results, k) {
6152
+ const relevantPaths = getRelevantPaths(query);
6153
+ const top = uniqueResultsByPath(results).slice(0, k);
6154
+ const hasRelevantTopK = top.some((result) => isRelevantResult(result.filePath, relevantPaths));
6155
+ if (!hasRelevantTopK) {
6156
+ return "no-relevant-hit-top-k";
6157
+ }
6158
+ if (query.expected.symbol) {
6159
+ const hasSymbol = top.some(
6160
+ (result) => isRelevantResult(result.filePath, relevantPaths) && result.name === query.expected.symbol
6161
+ );
6162
+ if (!hasSymbol) return "wrong-symbol";
6163
+ }
6164
+ const top1 = top[0];
6165
+ if (top1 && !isRelevantResult(top1.filePath, relevantPaths) && isDocsOrTestsPath(top1.filePath)) {
6166
+ return "docs-tests-outranking-source";
6167
+ }
6168
+ if (top1 && !isRelevantResult(top1.filePath, relevantPaths)) {
6169
+ return "wrong-file";
6170
+ }
6171
+ return void 0;
6172
+ }
6173
+ function buildPerQueryResult(query, results, latencyMs, k) {
6174
+ const relevantPaths = getRelevantPaths(query);
6175
+ const deduped = uniqueResultsByPath(results);
6176
+ const hitAt = (cutoff) => deduped.slice(0, cutoff).some((result) => isRelevantResult(result.filePath, relevantPaths));
6177
+ const perQuery = {
6178
+ id: query.id,
6179
+ query: query.query,
6180
+ queryType: query.queryType,
6181
+ latencyMs,
6182
+ hitAt1: hitAt(1),
6183
+ hitAt3: hitAt(3),
6184
+ hitAt5: hitAt(5),
6185
+ hitAt10: hitAt(10),
6186
+ reciprocalRankAt10: reciprocalRankAtK(deduped, relevantPaths, 10),
6187
+ ndcgAt10: ndcgAtK(deduped, relevantPaths, 10),
6188
+ failureBucket: classifyFailureBucket(query, results, k),
6189
+ results: deduped
6190
+ };
6191
+ return perQuery;
6192
+ }
6193
+ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingTokensUsed, costPer1MTokensUsd) {
6194
+ const count = perQuery.length;
6195
+ const safeDiv = (value) => count === 0 ? 0 : value / count;
6196
+ const sum = {
6197
+ hitAt1: 0,
6198
+ hitAt3: 0,
6199
+ hitAt5: 0,
6200
+ hitAt10: 0,
6201
+ mrrAt10: 0,
6202
+ ndcgAt10: 0
6203
+ };
6204
+ const failureBuckets = {
6205
+ "wrong-file": 0,
6206
+ "wrong-symbol": 0,
6207
+ "docs-tests-outranking-source": 0,
6208
+ "no-relevant-hit-top-k": 0
6209
+ };
6210
+ const latencies = perQuery.map((item) => item.latencyMs);
6211
+ for (const query of perQuery) {
6212
+ if (query.hitAt1) sum.hitAt1 += 1;
6213
+ if (query.hitAt3) sum.hitAt3 += 1;
6214
+ if (query.hitAt5) sum.hitAt5 += 1;
6215
+ if (query.hitAt10) sum.hitAt10 += 1;
6216
+ sum.mrrAt10 += query.reciprocalRankAt10;
6217
+ sum.ndcgAt10 += query.ndcgAt10;
6218
+ if (query.failureBucket) {
6219
+ failureBuckets[query.failureBucket] += 1;
6220
+ }
6221
+ }
6222
+ const queryTokens = queries.reduce((acc, q) => acc + estimateTokens(q.query), 0);
6223
+ return {
6224
+ hitAt1: safeDiv(sum.hitAt1),
6225
+ hitAt3: safeDiv(sum.hitAt3),
6226
+ hitAt5: safeDiv(sum.hitAt5),
6227
+ hitAt10: safeDiv(sum.hitAt10),
6228
+ mrrAt10: safeDiv(sum.mrrAt10),
6229
+ ndcgAt10: safeDiv(sum.ndcgAt10),
6230
+ latencyMs: {
6231
+ p50: percentile(latencies, 0.5),
6232
+ p95: percentile(latencies, 0.95),
6233
+ p99: percentile(latencies, 0.99)
6234
+ },
6235
+ tokenEstimate: {
6236
+ queryTokens,
6237
+ embeddingTokensUsed
6238
+ },
6239
+ embedding: {
6240
+ callCount: embeddingCallCount,
6241
+ estimatedCostUsd: embeddingTokensUsed / 1e6 * costPer1MTokensUsd,
6242
+ costPer1MTokensUsd
6243
+ },
6244
+ failureBuckets
6245
+ };
6246
+ }
6247
+
6248
+ // src/eval/schema.ts
6249
+ var import_fs6 = require("fs");
6250
+ function parseJsonFile(filePath) {
6251
+ const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
6252
+ return JSON.parse(content);
6253
+ }
6254
+ function isRecord(value) {
6255
+ return typeof value === "object" && value !== null;
6256
+ }
6257
+ function isStringArray2(value) {
6258
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
6259
+ }
6260
+ function asPositiveNumber(value, path10) {
6261
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
6262
+ throw new Error(`${path10} must be a non-negative number`);
6263
+ }
6264
+ return value;
6265
+ }
6266
+ function parseQueryType(value, path10) {
6267
+ if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
6268
+ return value;
6269
+ }
6270
+ throw new Error(
6271
+ `${path10} must be one of: definition, implementation-intent, similarity, keyword-heavy`
6272
+ );
6273
+ }
6274
+ function parseExpected(input, path10) {
6275
+ if (!isRecord(input)) {
6276
+ throw new Error(`${path10} must be an object`);
6277
+ }
6278
+ const filePathRaw = input.filePath;
6279
+ const acceptableFilesRaw = input.acceptableFiles;
6280
+ const symbolRaw = input.symbol;
6281
+ const branchRaw = input.branch;
6282
+ const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
6283
+ const acceptableFiles = isStringArray2(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
6284
+ if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
6285
+ throw new Error(`${path10} must include either expected.filePath or expected.acceptableFiles`);
6286
+ }
6287
+ if (acceptableFilesRaw !== void 0 && !isStringArray2(acceptableFilesRaw)) {
6288
+ throw new Error(`${path10}.acceptableFiles must be an array of strings`);
6289
+ }
6290
+ if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
6291
+ throw new Error(`${path10}.symbol must be a string when provided`);
6292
+ }
6293
+ if (branchRaw !== void 0 && typeof branchRaw !== "string") {
6294
+ throw new Error(`${path10}.branch must be a string when provided`);
6295
+ }
6296
+ return {
6297
+ filePath,
6298
+ acceptableFiles,
6299
+ symbol: typeof symbolRaw === "string" ? symbolRaw : void 0,
6300
+ branch: typeof branchRaw === "string" ? branchRaw : void 0
6301
+ };
6302
+ }
6303
+ function parseQuery(input, index) {
6304
+ const path10 = `queries[${index}]`;
6305
+ if (!isRecord(input)) {
6306
+ throw new Error(`${path10} must be an object`);
6307
+ }
6308
+ const id = input.id;
6309
+ const query = input.query;
6310
+ const queryType = input.queryType;
6311
+ const expected = input.expected;
6312
+ if (typeof id !== "string" || id.trim().length === 0) {
6313
+ throw new Error(`${path10}.id must be a non-empty string`);
6314
+ }
6315
+ if (typeof query !== "string" || query.trim().length === 0) {
6316
+ throw new Error(`${path10}.query must be a non-empty string`);
6317
+ }
6318
+ return {
6319
+ id,
6320
+ query,
6321
+ queryType: parseQueryType(queryType, `${path10}.queryType`),
6322
+ expected: parseExpected(expected, `${path10}.expected`)
6323
+ };
6324
+ }
6325
+ function parseGoldenDataset(raw, sourceLabel) {
6326
+ if (!isRecord(raw)) {
6327
+ throw new Error(`${sourceLabel} must be a JSON object`);
6328
+ }
6329
+ const version = raw.version;
6330
+ const name = raw.name;
6331
+ const description = raw.description;
6332
+ const queriesRaw = raw.queries;
6333
+ if (typeof version !== "string" || version.trim().length === 0) {
6334
+ throw new Error(`${sourceLabel}.version must be a non-empty string`);
6335
+ }
6336
+ if (typeof name !== "string" || name.trim().length === 0) {
6337
+ throw new Error(`${sourceLabel}.name must be a non-empty string`);
6338
+ }
6339
+ if (description !== void 0 && typeof description !== "string") {
6340
+ throw new Error(`${sourceLabel}.description must be a string when provided`);
6341
+ }
6342
+ if (!Array.isArray(queriesRaw)) {
6343
+ throw new Error(`${sourceLabel}.queries must be an array`);
6344
+ }
6345
+ if (queriesRaw.length === 0) {
6346
+ throw new Error(`${sourceLabel}.queries must contain at least one query`);
6347
+ }
6348
+ const queries = queriesRaw.map((query, idx) => parseQuery(query, idx));
6349
+ const idSet = /* @__PURE__ */ new Set();
6350
+ for (const query of queries) {
6351
+ if (idSet.has(query.id)) {
6352
+ throw new Error(`${sourceLabel}.queries has duplicate id: ${query.id}`);
6353
+ }
6354
+ idSet.add(query.id);
6355
+ }
6356
+ return {
6357
+ version,
6358
+ name,
6359
+ description: typeof description === "string" ? description : void 0,
6360
+ queries
6361
+ };
6362
+ }
6363
+ function loadGoldenDataset(datasetPath) {
6364
+ const parsed = parseJsonFile(datasetPath);
6365
+ return parseGoldenDataset(parsed, datasetPath);
6366
+ }
6367
+ function parseBudget(raw, sourceLabel) {
6368
+ if (!isRecord(raw)) {
6369
+ throw new Error(`${sourceLabel} must be a JSON object`);
6370
+ }
6371
+ const name = raw.name;
6372
+ const baselinePath = raw.baselinePath;
6373
+ const failOnMissingBaseline = raw.failOnMissingBaseline;
6374
+ const thresholds = raw.thresholds;
6375
+ if (typeof name !== "string" || name.trim().length === 0) {
6376
+ throw new Error(`${sourceLabel}.name must be a non-empty string`);
6377
+ }
6378
+ if (baselinePath !== void 0 && typeof baselinePath !== "string") {
6379
+ throw new Error(`${sourceLabel}.baselinePath must be a string when provided`);
6380
+ }
6381
+ if (!isRecord(thresholds)) {
6382
+ throw new Error(`${sourceLabel}.thresholds must be an object`);
6383
+ }
6384
+ return {
6385
+ name,
6386
+ baselinePath: typeof baselinePath === "string" ? baselinePath : void 0,
6387
+ failOnMissingBaseline: typeof failOnMissingBaseline === "boolean" ? failOnMissingBaseline : true,
6388
+ thresholds: {
6389
+ hitAt5MaxDrop: thresholds.hitAt5MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.hitAt5MaxDrop, `${sourceLabel}.thresholds.hitAt5MaxDrop`),
6390
+ mrrAt10MaxDrop: thresholds.mrrAt10MaxDrop === void 0 ? void 0 : asPositiveNumber(thresholds.mrrAt10MaxDrop, `${sourceLabel}.thresholds.mrrAt10MaxDrop`),
6391
+ p95LatencyMaxMultiplier: thresholds.p95LatencyMaxMultiplier === void 0 ? void 0 : asPositiveNumber(
6392
+ thresholds.p95LatencyMaxMultiplier,
6393
+ `${sourceLabel}.thresholds.p95LatencyMaxMultiplier`
6394
+ ),
6395
+ p95LatencyMaxAbsoluteMs: thresholds.p95LatencyMaxAbsoluteMs === void 0 ? void 0 : asPositiveNumber(
6396
+ thresholds.p95LatencyMaxAbsoluteMs,
6397
+ `${sourceLabel}.thresholds.p95LatencyMaxAbsoluteMs`
6398
+ ),
6399
+ minHitAt5: thresholds.minHitAt5 === void 0 ? void 0 : asPositiveNumber(thresholds.minHitAt5, `${sourceLabel}.thresholds.minHitAt5`),
6400
+ minMrrAt10: thresholds.minMrrAt10 === void 0 ? void 0 : asPositiveNumber(thresholds.minMrrAt10, `${sourceLabel}.thresholds.minMrrAt10`)
6401
+ }
6402
+ };
6403
+ }
6404
+ function loadBudget(budgetPath) {
6405
+ const parsed = parseJsonFile(budgetPath);
6406
+ return parseBudget(parsed, budgetPath);
6407
+ }
6408
+
6409
+ // src/eval/runner.ts
6410
+ function toAbsolute(projectRoot, maybeRelative) {
6411
+ return path7.isAbsolute(maybeRelative) ? maybeRelative : path7.join(projectRoot, maybeRelative);
6412
+ }
6413
+ function loadRawConfig(projectRoot, configPath) {
6414
+ const fromPath = configPath ? toAbsolute(projectRoot, configPath) : null;
6415
+ if (fromPath && (0, import_fs7.existsSync)(fromPath)) {
6416
+ return JSON.parse((0, import_fs8.readFileSync)(fromPath, "utf-8"));
6417
+ }
6418
+ const projectConfig = path7.join(projectRoot, ".opencode", "codebase-index.json");
6419
+ if ((0, import_fs7.existsSync)(projectConfig)) {
6420
+ return JSON.parse((0, import_fs8.readFileSync)(projectConfig, "utf-8"));
6421
+ }
6422
+ const globalConfig = path7.join(os3.homedir(), ".config", "opencode", "codebase-index.json");
6423
+ if ((0, import_fs7.existsSync)(globalConfig)) {
6424
+ return JSON.parse((0, import_fs8.readFileSync)(globalConfig, "utf-8"));
6425
+ }
6426
+ return {};
6427
+ }
6428
+ function getIndexRootPath(projectRoot, scope) {
6429
+ if (scope === "global") {
6430
+ return path7.join(os3.homedir(), ".opencode", "global-index");
6431
+ }
6432
+ return path7.join(projectRoot, ".opencode", "index");
6433
+ }
6434
+ function clearIndexRoot(projectRoot, scope) {
6435
+ const indexRoot = getIndexRootPath(projectRoot, scope);
6436
+ if ((0, import_fs7.existsSync)(indexRoot)) {
6437
+ (0, import_fs9.rmSync)(indexRoot, { recursive: true, force: true });
6438
+ }
6439
+ }
6440
+ function loadParsedConfig(projectRoot, configPath) {
6441
+ const raw = loadRawConfig(projectRoot, configPath);
6442
+ return parseConfig(raw);
6443
+ }
6444
+ function resolveSearchConfig(parsedConfig, overrides) {
6445
+ const nextSearch = {
6446
+ ...parsedConfig.search
6447
+ };
6448
+ if (overrides?.fusionStrategy !== void 0) {
6449
+ nextSearch.fusionStrategy = overrides.fusionStrategy;
6450
+ }
6451
+ if (overrides?.hybridWeight !== void 0) {
6452
+ nextSearch.hybridWeight = overrides.hybridWeight;
6453
+ }
6454
+ if (overrides?.rrfK !== void 0) {
6455
+ nextSearch.rrfK = overrides.rrfK;
6456
+ }
6457
+ if (overrides?.rerankTopN !== void 0) {
6458
+ nextSearch.rerankTopN = overrides.rerankTopN;
6459
+ }
6460
+ return {
6461
+ ...parsedConfig,
6462
+ search: nextSearch
6463
+ };
6464
+ }
6465
+ async function runEvaluation(options) {
6466
+ const datasetPath = toAbsolute(options.projectRoot, options.datasetPath);
6467
+ const againstPath = options.againstPath ? toAbsolute(options.projectRoot, options.againstPath) : void 0;
6468
+ const budgetPath = options.budgetPath ? toAbsolute(options.projectRoot, options.budgetPath) : void 0;
6469
+ const dataset = loadGoldenDataset(datasetPath);
6470
+ const parsedConfig = loadParsedConfig(options.projectRoot, options.configPath);
6471
+ const effectiveConfig = resolveSearchConfig(parsedConfig, options.searchOverrides);
6472
+ if (options.reindex) {
6473
+ clearIndexRoot(options.projectRoot, effectiveConfig.scope);
6474
+ }
6475
+ const indexer = new Indexer(options.projectRoot, effectiveConfig);
6476
+ await indexer.index();
6477
+ const perQuery = [];
6478
+ for (const query of dataset.queries) {
6479
+ if (query.expected.branch && query.expected.branch !== indexer.getCurrentBranch()) {
6480
+ throw new Error(
6481
+ `Query '${query.id}' expects branch '${query.expected.branch}', but current branch is '${indexer.getCurrentBranch()}'. Switch branch before running this dataset.`
6482
+ );
6483
+ }
6484
+ const start = import_perf_hooks2.performance.now();
6485
+ const result = await indexer.search(query.query, 10, {
6486
+ metadataOnly: true,
6487
+ filterByBranch: query.expected.branch ? true : false
6488
+ });
6489
+ const elapsed = import_perf_hooks2.performance.now() - start;
6490
+ const materialized = result.map((item) => ({
6491
+ filePath: item.filePath,
6492
+ startLine: item.startLine,
6493
+ endLine: item.endLine,
6494
+ score: item.score,
6495
+ chunkType: item.chunkType,
6496
+ name: item.name
6497
+ }));
6498
+ perQuery.push(buildPerQueryResult(query, materialized, elapsed, 10));
6499
+ }
6500
+ const logger = indexer.getLogger();
6501
+ const metricSnapshot = logger.getMetrics();
6502
+ const costPer1MTokensUsd = effectiveConfig.embeddingProvider === "custom" || effectiveConfig.embeddingProvider === "auto" ? 0 : getDefaultModelForProvider(effectiveConfig.embeddingProvider).costPer1MTokens;
6503
+ const summary = {
6504
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6505
+ projectRoot: options.projectRoot,
6506
+ datasetPath,
6507
+ datasetName: dataset.name,
6508
+ datasetVersion: dataset.version,
6509
+ queryCount: dataset.queries.length,
6510
+ topK: 10,
6511
+ searchConfig: {
6512
+ fusionStrategy: effectiveConfig.search.fusionStrategy,
6513
+ hybridWeight: effectiveConfig.search.hybridWeight,
6514
+ rrfK: effectiveConfig.search.rrfK,
6515
+ rerankTopN: effectiveConfig.search.rerankTopN
6516
+ },
6517
+ metrics: computeEvalMetrics(
6518
+ dataset.queries,
6519
+ perQuery,
6520
+ metricSnapshot.embeddingApiCalls,
6521
+ metricSnapshot.embeddingTokensUsed,
6522
+ costPer1MTokensUsd
6523
+ )
6524
+ };
6525
+ const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
6526
+ const perQueryArtifact = buildPerQueryArtifact(perQuery);
6527
+ writeJson(path7.join(outputDir, "summary.json"), summary);
6528
+ writeJson(path7.join(outputDir, "per-query.json"), perQueryArtifact);
6529
+ let comparison;
6530
+ if (againstPath) {
6531
+ const baseline = loadSummary(againstPath);
6532
+ comparison = compareSummaries(summary, baseline, againstPath);
6533
+ writeJson(path7.join(outputDir, "compare.json"), comparison);
6534
+ }
6535
+ let gate;
6536
+ if (options.ciMode) {
6537
+ if (!budgetPath) {
6538
+ throw new Error("CI mode requires --budget path");
6539
+ }
6540
+ const budget = loadBudget(budgetPath);
6541
+ if (!comparison && budget.baselinePath) {
6542
+ const resolvedBaseline = toAbsolute(options.projectRoot, budget.baselinePath);
6543
+ if ((0, import_fs7.existsSync)(resolvedBaseline)) {
6544
+ const baselineSummary = loadSummary(resolvedBaseline);
6545
+ comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
6546
+ writeJson(path7.join(outputDir, "compare.json"), comparison);
6547
+ } else if (budget.failOnMissingBaseline) {
6548
+ throw new Error(
6549
+ `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
6550
+ );
6551
+ }
6552
+ }
6553
+ gate = evaluateBudgetGate(budget, summary, comparison);
6554
+ }
6555
+ const markdown = createSummaryMarkdown(summary, comparison, gate);
6556
+ writeText(path7.join(outputDir, "summary.md"), markdown);
6557
+ return { outputDir, summary, perQuery, comparison, gate };
6558
+ }
6559
+ async function runSweep(options, sweep) {
6560
+ const fusionValues = sweep.fusionStrategy && sweep.fusionStrategy.length > 0 ? [...sweep.fusionStrategy] : [void 0];
6561
+ const weightValues = sweep.hybridWeight && sweep.hybridWeight.length > 0 ? [...sweep.hybridWeight] : [void 0];
6562
+ const rrfValues = sweep.rrfK && sweep.rrfK.length > 0 ? [...sweep.rrfK] : [void 0];
6563
+ const rerankValues = sweep.rerankTopN && sweep.rerankTopN.length > 0 ? [...sweep.rerankTopN] : [void 0];
6564
+ const runs = [];
6565
+ for (const fusion of fusionValues) {
6566
+ for (const hybridWeight of weightValues) {
6567
+ for (const rrfK of rrfValues) {
6568
+ for (const rerankTopN of rerankValues) {
6569
+ const run = await runEvaluation({
6570
+ ...options,
6571
+ searchOverrides: {
6572
+ ...fusion !== void 0 ? { fusionStrategy: fusion } : {},
6573
+ ...hybridWeight !== void 0 ? { hybridWeight } : {},
6574
+ ...rrfK !== void 0 ? { rrfK } : {},
6575
+ ...rerankTopN !== void 0 ? { rerankTopN } : {}
6576
+ }
6577
+ });
6578
+ runs.push({
6579
+ searchConfig: run.summary.searchConfig,
6580
+ summary: run.summary,
6581
+ comparison: run.comparison,
6582
+ gate: run.gate
6583
+ });
6584
+ }
6585
+ }
6586
+ }
6587
+ }
6588
+ const bestByHitAt5 = [...runs].sort(
6589
+ (a, b) => b.summary.metrics.hitAt5 - a.summary.metrics.hitAt5
6590
+ )[0];
6591
+ const bestByMrrAt10 = [...runs].sort(
6592
+ (a, b) => b.summary.metrics.mrrAt10 - a.summary.metrics.mrrAt10
6593
+ )[0];
6594
+ const bestByP95Latency = [...runs].sort(
6595
+ (a, b) => a.summary.metrics.latencyMs.p95 - b.summary.metrics.latencyMs.p95
6596
+ )[0];
6597
+ const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
6598
+ const failedGateRuns = runs.filter((run) => run.gate && !run.gate.passed).length;
6599
+ const gatePassed = failedGateRuns === 0;
6600
+ const aggregate = {
6601
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6602
+ againstPath: options.againstPath,
6603
+ runCount: runs.length,
6604
+ runs,
6605
+ gatePassed,
6606
+ failedGateRuns,
6607
+ bestByHitAt5,
6608
+ bestByMrrAt10,
6609
+ bestByP95Latency
6610
+ };
6611
+ writeJson(path7.join(outputDir, "compare.json"), aggregate);
6612
+ const md = createSummaryMarkdown(
6613
+ bestByHitAt5?.summary ?? runs[0].summary,
6614
+ bestByHitAt5?.comparison,
6615
+ void 0,
6616
+ aggregate
6617
+ );
6618
+ writeText(path7.join(outputDir, "summary.md"), md);
6619
+ writeJson(path7.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
6620
+ return { outputDir, aggregate };
6621
+ }
6622
+
6623
+ // src/eval/cli.ts
6624
+ function printUsage() {
6625
+ console.log(`
6626
+ Usage:
6627
+ opencode-codebase-index-mcp eval run [options]
6628
+ opencode-codebase-index-mcp eval compare --against <summary.json> [options]
6629
+ opencode-codebase-index-mcp eval diff --current <summary.json> --against <summary.json> [options]
6630
+
6631
+ Options:
6632
+ --project <path> Project root (default: cwd)
6633
+ --config <path> Config JSON path
6634
+ --dataset <path> Golden dataset path (default: benchmarks/golden/small.json)
6635
+ --current <path> Current summary.json path (required for eval diff)
6636
+ --output <path> Output root dir (default: benchmarks/results)
6637
+ --against <path> Baseline summary.json to compare against
6638
+ --budget <path> Budget file for CI mode (default: benchmarks/budgets/default.json)
6639
+ --ci Enable CI gate mode
6640
+ --reindex Force reindex before eval
6641
+
6642
+ Search overrides:
6643
+ --fusionStrategy <rrf|weighted>
6644
+ --hybridWeight <0-1>
6645
+ --rrfK <number>
6646
+ --rerankTopN <number>
6647
+
6648
+ Sweep options (comma-separated values):
6649
+ --sweepFusionStrategy <rrf,weighted>
6650
+ --sweepHybridWeight <0.3,0.5,0.7>
6651
+ --sweepRrfK <30,60,90>
6652
+ --sweepRerankTopN <10,20,40>
6653
+ `);
6654
+ }
6655
+ function parseNumber(value, flag) {
6656
+ const parsed = Number(value);
6657
+ if (Number.isNaN(parsed)) {
6658
+ throw new Error(`${flag} must be a number`);
6659
+ }
6660
+ return parsed;
6661
+ }
6662
+ function parseCsvNumbers(value, flag) {
6663
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0).map((item) => parseNumber(item, flag));
6664
+ }
6665
+ function parseCsvFusion(value) {
6666
+ const values = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
6667
+ const parsed = [];
6668
+ for (const candidate of values) {
6669
+ if (candidate !== "rrf" && candidate !== "weighted") {
6670
+ throw new Error("--sweepFusionStrategy accepts only rrf,weighted");
6671
+ }
6672
+ parsed.push(candidate);
6673
+ }
6674
+ return parsed;
6675
+ }
6676
+ function hasSweepOptions(sweep) {
6677
+ return Boolean(
6678
+ sweep.fusionStrategy && sweep.fusionStrategy.length > 0 || sweep.hybridWeight && sweep.hybridWeight.length > 0 || sweep.rrfK && sweep.rrfK.length > 0 || sweep.rerankTopN && sweep.rerankTopN.length > 0
6679
+ );
6680
+ }
6681
+ function parseEvalArgs(argv, cwd) {
6682
+ const parsed = {
6683
+ projectRoot: cwd,
6684
+ datasetPath: "benchmarks/golden/small.json",
6685
+ outputRoot: "benchmarks/results",
6686
+ budgetPath: "benchmarks/budgets/default.json",
6687
+ ciMode: false,
6688
+ reindex: false,
6689
+ sweep: {}
6690
+ };
6691
+ for (let i = 0; i < argv.length; i += 1) {
6692
+ const arg = argv[i];
6693
+ const next = argv[i + 1];
6694
+ if (arg === "--project" && next) {
6695
+ parsed.projectRoot = path8.resolve(cwd, next);
6696
+ i += 1;
6697
+ continue;
6698
+ }
6699
+ if (arg === "--config" && next) {
6700
+ parsed.configPath = path8.resolve(cwd, next);
6701
+ i += 1;
6702
+ continue;
6703
+ }
6704
+ if (arg === "--dataset" && next) {
6705
+ parsed.datasetPath = next;
6706
+ i += 1;
6707
+ continue;
6708
+ }
6709
+ if (arg === "--current" && next) {
6710
+ parsed.currentPath = next;
6711
+ i += 1;
6712
+ continue;
6713
+ }
6714
+ if (arg === "--output" && next) {
6715
+ parsed.outputRoot = next;
6716
+ i += 1;
6717
+ continue;
6718
+ }
6719
+ if (arg === "--against" && next) {
6720
+ parsed.againstPath = next;
6721
+ i += 1;
6722
+ continue;
6723
+ }
6724
+ if (arg === "--budget" && next) {
6725
+ parsed.budgetPath = next;
6726
+ i += 1;
6727
+ continue;
6728
+ }
6729
+ if (arg === "--ci") {
6730
+ parsed.ciMode = true;
6731
+ continue;
6732
+ }
6733
+ if (arg === "--reindex") {
6734
+ parsed.reindex = true;
6735
+ continue;
6736
+ }
6737
+ if (arg === "--fusionStrategy" && next) {
6738
+ if (next !== "rrf" && next !== "weighted") {
6739
+ throw new Error("--fusionStrategy must be rrf or weighted");
6740
+ }
6741
+ parsed.fusionStrategy = next;
6742
+ i += 1;
6743
+ continue;
6744
+ }
6745
+ if (arg === "--hybridWeight" && next) {
6746
+ parsed.hybridWeight = parseNumber(next, "--hybridWeight");
6747
+ i += 1;
6748
+ continue;
6749
+ }
6750
+ if (arg === "--rrfK" && next) {
6751
+ parsed.rrfK = parseNumber(next, "--rrfK");
6752
+ i += 1;
6753
+ continue;
6754
+ }
6755
+ if (arg === "--rerankTopN" && next) {
6756
+ parsed.rerankTopN = parseNumber(next, "--rerankTopN");
6757
+ i += 1;
6758
+ continue;
6759
+ }
6760
+ if (arg === "--sweepFusionStrategy" && next) {
6761
+ parsed.sweep.fusionStrategy = parseCsvFusion(next);
6762
+ i += 1;
6763
+ continue;
6764
+ }
6765
+ if (arg === "--sweepHybridWeight" && next) {
6766
+ parsed.sweep.hybridWeight = parseCsvNumbers(next, "--sweepHybridWeight");
6767
+ i += 1;
6768
+ continue;
6769
+ }
6770
+ if (arg === "--sweepRrfK" && next) {
6771
+ parsed.sweep.rrfK = parseCsvNumbers(next, "--sweepRrfK");
6772
+ i += 1;
6773
+ continue;
6774
+ }
6775
+ if (arg === "--sweepRerankTopN" && next) {
6776
+ parsed.sweep.rerankTopN = parseCsvNumbers(next, "--sweepRerankTopN");
6777
+ i += 1;
6778
+ continue;
6779
+ }
6780
+ }
6781
+ return parsed;
6782
+ }
6783
+ function parseEvalSubcommandOptions(argv, cwd) {
6784
+ let explicitAgainst;
6785
+ const filtered = [];
6786
+ for (let i = 0; i < argv.length; i += 1) {
6787
+ const current = argv[i];
6788
+ const next = argv[i + 1];
6789
+ if (current === "--against" && next) {
6790
+ explicitAgainst = next;
6791
+ i += 1;
6792
+ continue;
6793
+ }
6794
+ filtered.push(current);
6795
+ }
6796
+ return {
6797
+ parsed: parseEvalArgs(filtered, cwd),
6798
+ explicitAgainst
6799
+ };
6800
+ }
6801
+ function toRunOptions(parsed) {
6802
+ return {
6803
+ projectRoot: parsed.projectRoot,
6804
+ configPath: parsed.configPath,
6805
+ datasetPath: parsed.datasetPath,
6806
+ outputRoot: parsed.outputRoot,
6807
+ againstPath: parsed.againstPath,
6808
+ budgetPath: parsed.budgetPath,
6809
+ ciMode: parsed.ciMode,
6810
+ reindex: parsed.reindex,
6811
+ searchOverrides: {
6812
+ ...parsed.fusionStrategy !== void 0 ? { fusionStrategy: parsed.fusionStrategy } : {},
6813
+ ...parsed.hybridWeight !== void 0 ? { hybridWeight: parsed.hybridWeight } : {},
6814
+ ...parsed.rrfK !== void 0 ? { rrfK: parsed.rrfK } : {},
6815
+ ...parsed.rerankTopN !== void 0 ? { rerankTopN: parsed.rerankTopN } : {}
6816
+ }
6817
+ };
6818
+ }
6819
+ async function handleEvalCommand(args, cwd) {
6820
+ const subcommand = args[0];
6821
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
6822
+ printUsage();
6823
+ return 0;
6824
+ }
6825
+ if (subcommand === "run") {
6826
+ const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
6827
+ if (explicitAgainst) {
6828
+ parsed.againstPath = explicitAgainst;
6829
+ }
6830
+ const runOptions = toRunOptions(parsed);
6831
+ if (hasSweepOptions(parsed.sweep)) {
6832
+ const sweep = await runSweep(runOptions, parsed.sweep);
6833
+ console.log(`Eval sweep complete. Artifacts: ${sweep.outputDir}`);
6834
+ console.log(`Sweep runs: ${sweep.aggregate.runCount}`);
6835
+ if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
6836
+ console.error(
6837
+ `[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
6838
+ );
6839
+ return 1;
6840
+ }
6841
+ return 0;
6842
+ }
6843
+ const result = await runEvaluation(runOptions);
6844
+ console.log(`Eval run complete. Artifacts: ${result.outputDir}`);
6845
+ console.log(
6846
+ `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`
6847
+ );
6848
+ if (result.gate && !result.gate.passed) {
6849
+ for (const violation of result.gate.violations) {
6850
+ console.error(`[CI-GATE] ${violation.metric}: ${violation.message}`);
6851
+ }
6852
+ return 1;
6853
+ }
6854
+ return 0;
6855
+ }
6856
+ if (subcommand === "compare") {
6857
+ const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
6858
+ if (!explicitAgainst) {
6859
+ throw new Error("eval compare requires --against <baseline summary.json>");
6860
+ }
6861
+ parsed.againstPath = explicitAgainst;
6862
+ const runOptions = toRunOptions(parsed);
6863
+ if (hasSweepOptions(parsed.sweep)) {
6864
+ const sweep = await runSweep(runOptions, parsed.sweep);
6865
+ console.log(`Eval compare sweep complete. Artifacts: ${sweep.outputDir}`);
6866
+ if (parsed.ciMode && sweep.aggregate.gatePassed === false) {
6867
+ console.error(
6868
+ `[CI-GATE] Sweep failed: ${sweep.aggregate.failedGateRuns ?? 0} run(s) violated budget/baseline gates`
6869
+ );
6870
+ return 1;
6871
+ }
6872
+ return 0;
6873
+ }
6874
+ const result = await runEvaluation(runOptions);
6875
+ console.log(`Eval compare complete. Artifacts: ${result.outputDir}`);
6876
+ return 0;
6877
+ }
6878
+ if (subcommand === "diff") {
6879
+ const { parsed, explicitAgainst } = parseEvalSubcommandOptions(args.slice(1), cwd);
6880
+ if (!explicitAgainst) {
6881
+ throw new Error("eval diff requires --against <baseline summary.json>");
6882
+ }
6883
+ if (!parsed.currentPath) {
6884
+ throw new Error("eval diff requires --current <current summary.json>");
6885
+ }
6886
+ parsed.againstPath = explicitAgainst;
6887
+ const currentPath = parsed.currentPath;
6888
+ if (!currentPath.endsWith(".json")) {
6889
+ throw new Error("eval diff --current must point to a summary JSON file");
6890
+ }
6891
+ if (!parsed.againstPath.endsWith(".json")) {
6892
+ throw new Error("eval diff --against must point to a summary JSON file");
6893
+ }
6894
+ const currentSummary = loadSummary(path8.resolve(parsed.projectRoot, currentPath));
6895
+ const baselineSummary = loadSummary(path8.resolve(parsed.projectRoot, parsed.againstPath));
6896
+ const comparison = compareSummaries(
6897
+ currentSummary,
6898
+ baselineSummary,
6899
+ path8.resolve(parsed.projectRoot, parsed.againstPath)
6900
+ );
6901
+ const outputDir = createRunDirectory(path8.resolve(parsed.projectRoot, parsed.outputRoot));
6902
+ const summaryMd = createSummaryMarkdown(currentSummary, comparison);
6903
+ writeJson(path8.join(outputDir, "compare.json"), comparison);
6904
+ writeText(path8.join(outputDir, "summary.md"), summaryMd);
6905
+ writeJson(path8.join(outputDir, "summary.json"), currentSummary);
6906
+ console.log(`Eval diff complete. Artifacts: ${outputDir}`);
6907
+ return 0;
6908
+ }
6909
+ throw new Error(`Unknown eval subcommand: ${subcommand}`);
6910
+ }
6911
+
5738
6912
  // src/mcp-server.ts
6913
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6914
+ var import_zod = require("zod");
6915
+
6916
+ // src/tools/utils.ts
5739
6917
  var MAX_CONTENT_LINES = 30;
5740
6918
  function truncateContent(content) {
5741
6919
  const lines = content.split("\n");
@@ -5743,6 +6921,31 @@ function truncateContent(content) {
5743
6921
  return lines.slice(0, MAX_CONTENT_LINES).join("\n") + `
5744
6922
  // ... (${lines.length - MAX_CONTENT_LINES} more lines)`;
5745
6923
  }
6924
+ function formatDefinitionLookup(results, query) {
6925
+ if (results.length === 0) {
6926
+ return `No definition found for "${query}". Try codebase_search for broader discovery, or verify the symbol name.`;
6927
+ }
6928
+ const formatted = results.map((r, idx) => {
6929
+ 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}`;
6930
+ return `${header2} (score: ${r.score.toFixed(2)})
6931
+ \`\`\`
6932
+ ${truncateContent(r.content)}
6933
+ \`\`\``;
6934
+ });
6935
+ const header = results.length === 1 ? `Definition found for "${query}":` : `Found ${results.length} definition candidates for "${query}":`;
6936
+ return `${header}
6937
+
6938
+ ${formatted.join("\n\n")}`;
6939
+ }
6940
+
6941
+ // src/mcp-server.ts
6942
+ var MAX_CONTENT_LINES2 = 30;
6943
+ function truncateContent2(content) {
6944
+ const lines = content.split("\n");
6945
+ if (lines.length <= MAX_CONTENT_LINES2) return content;
6946
+ return lines.slice(0, MAX_CONTENT_LINES2).join("\n") + `
6947
+ // ... (${lines.length - MAX_CONTENT_LINES2} more lines)`;
6948
+ }
5746
6949
  function formatIndexStats(stats, verbose = false) {
5747
6950
  const lines = [];
5748
6951
  if (stats.indexedChunks === 0 && stats.removedChunks === 0) {
@@ -5856,7 +7059,7 @@ function createMcpServer(projectRoot, config) {
5856
7059
  const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
5857
7060
  return `${header} (score: ${r.score.toFixed(2)})
5858
7061
  \`\`\`
5859
- ${truncateContent(r.content)}
7062
+ ${truncateContent2(r.content)}
5860
7063
  \`\`\``;
5861
7064
  });
5862
7065
  return { content: [{ type: "text", text: `Found ${results.length} results for "${args.query}":
@@ -6034,7 +7237,7 @@ Use Read tool to examine specific files.` }] };
6034
7237
  const header = r.name ? `[${idx + 1}] ${r.chunkType} "${r.name}" in ${r.filePath}:${r.startLine}-${r.endLine}` : `[${idx + 1}] ${r.chunkType} in ${r.filePath}:${r.startLine}-${r.endLine}`;
6035
7238
  return `${header} (similarity: ${(r.score * 100).toFixed(1)}%)
6036
7239
  \`\`\`
6037
- ${truncateContent(r.content)}
7240
+ ${truncateContent2(r.content)}
6038
7241
  \`\`\``;
6039
7242
  });
6040
7243
  return { content: [{ type: "text", text: `Found ${results.length} similar code blocks:
@@ -6042,6 +7245,25 @@ ${truncateContent(r.content)}
6042
7245
  ${formatted.join("\n\n")}` }] };
6043
7246
  }
6044
7247
  );
7248
+ server.tool(
7249
+ "implementation_lookup",
7250
+ "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.",
7251
+ {
7252
+ query: import_zod.z.string().describe("Symbol name or natural language description (e.g., 'validateToken', 'where is the payment handler defined')"),
7253
+ limit: import_zod.z.number().optional().default(5).describe("Maximum number of results"),
7254
+ fileType: import_zod.z.string().optional().describe("Filter by file extension (e.g., 'ts', 'py')"),
7255
+ directory: import_zod.z.string().optional().describe("Filter by directory path (e.g., 'src/utils')")
7256
+ },
7257
+ async (args) => {
7258
+ await ensureInitialized();
7259
+ const results = await indexer.search(args.query, args.limit ?? 5, {
7260
+ fileType: args.fileType,
7261
+ directory: args.directory,
7262
+ definitionIntent: true
7263
+ });
7264
+ return { content: [{ type: "text", text: formatDefinitionLookup(results, args.query) }] };
7265
+ }
7266
+ );
6045
7267
  server.tool(
6046
7268
  "call_graph",
6047
7269
  "Query the call graph to find callers or callees of a function/method. Use to understand code flow and dependencies.",
@@ -6148,14 +7370,30 @@ Use a hybrid approach:
6148
7370
  }]
6149
7371
  })
6150
7372
  );
7373
+ server.prompt(
7374
+ "definition",
7375
+ "Find where a symbol is defined in the codebase",
7376
+ { query: import_zod.z.string().describe("Symbol name or description to find the definition of") },
7377
+ (args) => ({
7378
+ messages: [{
7379
+ role: "user",
7380
+ content: {
7381
+ type: "text",
7382
+ text: `Find the definition of: "${args.query}"
7383
+
7384
+ 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.`
7385
+ }
7386
+ }]
7387
+ })
7388
+ );
6151
7389
  return server;
6152
7390
  }
6153
7391
 
6154
7392
  // src/cli.ts
6155
7393
  function loadJsonFile(filePath) {
6156
7394
  try {
6157
- if ((0, import_fs5.existsSync)(filePath)) {
6158
- const content = (0, import_fs5.readFileSync)(filePath, "utf-8");
7395
+ if ((0, import_fs10.existsSync)(filePath)) {
7396
+ const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
6159
7397
  return JSON.parse(content);
6160
7398
  }
6161
7399
  } catch {
@@ -6167,9 +7405,9 @@ function loadPluginConfig(projectRoot, configPath) {
6167
7405
  const config = loadJsonFile(configPath);
6168
7406
  if (config) return config;
6169
7407
  }
6170
- const projectConfig = loadJsonFile(path6.join(projectRoot, ".opencode", "codebase-index.json"));
7408
+ const projectConfig = loadJsonFile(path9.join(projectRoot, ".opencode", "codebase-index.json"));
6171
7409
  if (projectConfig) return projectConfig;
6172
- const globalConfig = loadJsonFile(path6.join(os3.homedir(), ".config", "opencode", "codebase-index.json"));
7410
+ const globalConfig = loadJsonFile(path9.join(os4.homedir(), ".config", "opencode", "codebase-index.json"));
6173
7411
  if (globalConfig) return globalConfig;
6174
7412
  return {};
6175
7413
  }
@@ -6178,14 +7416,18 @@ function parseArgs(argv) {
6178
7416
  let config;
6179
7417
  for (let i = 2; i < argv.length; i++) {
6180
7418
  if (argv[i] === "--project" && argv[i + 1]) {
6181
- project = path6.resolve(argv[++i]);
7419
+ project = path9.resolve(argv[++i]);
6182
7420
  } else if (argv[i] === "--config" && argv[i + 1]) {
6183
- config = path6.resolve(argv[++i]);
7421
+ config = path9.resolve(argv[++i]);
6184
7422
  }
6185
7423
  }
6186
7424
  return { project, config };
6187
7425
  }
6188
7426
  async function main() {
7427
+ if (process.argv[2] === "eval") {
7428
+ const exitCode = await handleEvalCommand(process.argv.slice(3), process.cwd());
7429
+ process.exit(exitCode);
7430
+ }
6189
7431
  const args = parseArgs(process.argv);
6190
7432
  const rawConfig = loadPluginConfig(args.project, args.config);
6191
7433
  const config = parseConfig(rawConfig);