opencode-codebase-index 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -491,7 +491,7 @@ var require_ignore = __commonJS({
491
491
  // path matching.
492
492
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
493
493
  // @returns {TestResult} true if a file is ignored
494
- test(path18, checkUnignored, mode) {
494
+ test(path19, checkUnignored, mode) {
495
495
  let ignored = false;
496
496
  let unignored = false;
497
497
  let matchedRule;
@@ -500,7 +500,7 @@ var require_ignore = __commonJS({
500
500
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
501
501
  return;
502
502
  }
503
- const matched = rule[mode].test(path18);
503
+ const matched = rule[mode].test(path19);
504
504
  if (!matched) {
505
505
  return;
506
506
  }
@@ -521,17 +521,17 @@ var require_ignore = __commonJS({
521
521
  var throwError = (message, Ctor) => {
522
522
  throw new Ctor(message);
523
523
  };
524
- var checkPath = (path18, originalPath, doThrow) => {
525
- if (!isString(path18)) {
524
+ var checkPath = (path19, originalPath, doThrow) => {
525
+ if (!isString(path19)) {
526
526
  return doThrow(
527
527
  `path must be a string, but got \`${originalPath}\``,
528
528
  TypeError
529
529
  );
530
530
  }
531
- if (!path18) {
531
+ if (!path19) {
532
532
  return doThrow(`path must not be empty`, TypeError);
533
533
  }
534
- if (checkPath.isNotRelative(path18)) {
534
+ if (checkPath.isNotRelative(path19)) {
535
535
  const r = "`path.relative()`d";
536
536
  return doThrow(
537
537
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -540,7 +540,7 @@ var require_ignore = __commonJS({
540
540
  }
541
541
  return true;
542
542
  };
543
- var isNotRelative = (path18) => REGEX_TEST_INVALID_PATH.test(path18);
543
+ var isNotRelative = (path19) => REGEX_TEST_INVALID_PATH.test(path19);
544
544
  checkPath.isNotRelative = isNotRelative;
545
545
  checkPath.convert = (p) => p;
546
546
  var Ignore2 = class {
@@ -570,19 +570,19 @@ var require_ignore = __commonJS({
570
570
  }
571
571
  // @returns {TestResult}
572
572
  _test(originalPath, cache, checkUnignored, slices) {
573
- const path18 = originalPath && checkPath.convert(originalPath);
573
+ const path19 = originalPath && checkPath.convert(originalPath);
574
574
  checkPath(
575
- path18,
575
+ path19,
576
576
  originalPath,
577
577
  this._strictPathCheck ? throwError : RETURN_FALSE
578
578
  );
579
- return this._t(path18, cache, checkUnignored, slices);
579
+ return this._t(path19, cache, checkUnignored, slices);
580
580
  }
581
- checkIgnore(path18) {
582
- if (!REGEX_TEST_TRAILING_SLASH.test(path18)) {
583
- return this.test(path18);
581
+ checkIgnore(path19) {
582
+ if (!REGEX_TEST_TRAILING_SLASH.test(path19)) {
583
+ return this.test(path19);
584
584
  }
585
- const slices = path18.split(SLASH).filter(Boolean);
585
+ const slices = path19.split(SLASH).filter(Boolean);
586
586
  slices.pop();
587
587
  if (slices.length) {
588
588
  const parent = this._t(
@@ -595,18 +595,18 @@ var require_ignore = __commonJS({
595
595
  return parent;
596
596
  }
597
597
  }
598
- return this._rules.test(path18, false, MODE_CHECK_IGNORE);
598
+ return this._rules.test(path19, false, MODE_CHECK_IGNORE);
599
599
  }
600
- _t(path18, cache, checkUnignored, slices) {
601
- if (path18 in cache) {
602
- return cache[path18];
600
+ _t(path19, cache, checkUnignored, slices) {
601
+ if (path19 in cache) {
602
+ return cache[path19];
603
603
  }
604
604
  if (!slices) {
605
- slices = path18.split(SLASH).filter(Boolean);
605
+ slices = path19.split(SLASH).filter(Boolean);
606
606
  }
607
607
  slices.pop();
608
608
  if (!slices.length) {
609
- return cache[path18] = this._rules.test(path18, checkUnignored, MODE_IGNORE);
609
+ return cache[path19] = this._rules.test(path19, checkUnignored, MODE_IGNORE);
610
610
  }
611
611
  const parent = this._t(
612
612
  slices.join(SLASH) + SLASH,
@@ -614,29 +614,29 @@ var require_ignore = __commonJS({
614
614
  checkUnignored,
615
615
  slices
616
616
  );
617
- return cache[path18] = parent.ignored ? parent : this._rules.test(path18, checkUnignored, MODE_IGNORE);
617
+ return cache[path19] = parent.ignored ? parent : this._rules.test(path19, checkUnignored, MODE_IGNORE);
618
618
  }
619
- ignores(path18) {
620
- return this._test(path18, this._ignoreCache, false).ignored;
619
+ ignores(path19) {
620
+ return this._test(path19, this._ignoreCache, false).ignored;
621
621
  }
622
622
  createFilter() {
623
- return (path18) => !this.ignores(path18);
623
+ return (path19) => !this.ignores(path19);
624
624
  }
625
625
  filter(paths) {
626
626
  return makeArray(paths).filter(this.createFilter());
627
627
  }
628
628
  // @returns {TestResult}
629
- test(path18) {
630
- return this._test(path18, this._testCache, true);
629
+ test(path19) {
630
+ return this._test(path19, this._testCache, true);
631
631
  }
632
632
  };
633
633
  var factory = (options) => new Ignore2(options);
634
- var isPathValid = (path18) => checkPath(path18 && checkPath.convert(path18), path18, RETURN_FALSE);
634
+ var isPathValid = (path19) => checkPath(path19 && checkPath.convert(path19), path19, RETURN_FALSE);
635
635
  var setupWindows = () => {
636
636
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
637
637
  checkPath.convert = makePosix;
638
638
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
639
- checkPath.isNotRelative = (path18) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path18) || isNotRelative(path18);
639
+ checkPath.isNotRelative = (path19) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path19) || isNotRelative(path19);
640
640
  };
641
641
  if (
642
642
  // Detect `process` so that it can run in browsers.
@@ -653,7 +653,7 @@ var require_ignore = __commonJS({
653
653
 
654
654
  // src/cli.ts
655
655
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
656
- var path17 = __toESM(require("path"), 1);
656
+ var path18 = __toESM(require("path"), 1);
657
657
 
658
658
  // src/config/constants.ts
659
659
  var DEFAULT_INCLUDE = [
@@ -799,6 +799,7 @@ function getDefaultSearchConfig() {
799
799
  rerankTopN: 20,
800
800
  contextLines: 0,
801
801
  routingHints: true,
802
+ routingGraphHandoffHints: false,
802
803
  routingHintRole: "system"
803
804
  };
804
805
  }
@@ -921,6 +922,7 @@ function parseConfig(raw) {
921
922
  rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
922
923
  contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines,
923
924
  routingHints: typeof rawSearch.routingHints === "boolean" ? rawSearch.routingHints : defaultSearch.routingHints,
925
+ routingGraphHandoffHints: typeof rawSearch.routingGraphHandoffHints === "boolean" ? rawSearch.routingGraphHandoffHints : defaultSearch.routingGraphHandoffHints,
924
926
  routingHintRole: rawSearch.routingHintRole === "developer" || rawSearch.routingHintRole === "system" ? rawSearch.routingHintRole : defaultSearch.routingHintRole
925
927
  };
926
928
  const rawDebug = input.debug && typeof input.debug === "object" ? input.debug : {};
@@ -1079,9 +1081,9 @@ var import_fs = require("fs");
1079
1081
  var path = __toESM(require("path"), 1);
1080
1082
 
1081
1083
  // src/eval/report-formatters.ts
1082
- function assertFiniteNumber(value, path18) {
1084
+ function assertFiniteNumber(value, path19) {
1083
1085
  if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
1084
- throw new Error(`${path18} must be a finite number`);
1086
+ throw new Error(`${path19} must be a finite number`);
1085
1087
  }
1086
1088
  return value;
1087
1089
  }
@@ -1270,13 +1272,15 @@ function buildPerQueryArtifact(perQuery) {
1270
1272
 
1271
1273
  // src/eval/runner.ts
1272
1274
  var import_fs10 = require("fs");
1273
- var path12 = __toESM(require("path"), 1);
1275
+ var path13 = __toESM(require("path"), 1);
1274
1276
  var import_perf_hooks2 = require("perf_hooks");
1275
1277
 
1276
1278
  // src/indexer/index.ts
1277
1279
  var import_fs7 = require("fs");
1278
- var path9 = __toESM(require("path"), 1);
1280
+ var path10 = __toESM(require("path"), 1);
1279
1281
  var import_perf_hooks = require("perf_hooks");
1282
+ var import_child_process2 = require("child_process");
1283
+ var import_util2 = require("util");
1280
1284
 
1281
1285
  // node_modules/eventemitter3/index.mjs
1282
1286
  var import_index = __toESM(require_eventemitter3(), 1);
@@ -1300,7 +1304,7 @@ function pTimeout(promise, options) {
1300
1304
  } = options;
1301
1305
  let timer;
1302
1306
  let abortHandler;
1303
- const wrappedPromise = new Promise((resolve10, reject) => {
1307
+ const wrappedPromise = new Promise((resolve11, reject) => {
1304
1308
  if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
1305
1309
  throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
1306
1310
  }
@@ -1314,7 +1318,7 @@ function pTimeout(promise, options) {
1314
1318
  };
1315
1319
  signal.addEventListener("abort", abortHandler, { once: true });
1316
1320
  }
1317
- promise.then(resolve10, reject);
1321
+ promise.then(resolve11, reject);
1318
1322
  if (milliseconds === Number.POSITIVE_INFINITY) {
1319
1323
  return;
1320
1324
  }
@@ -1322,7 +1326,7 @@ function pTimeout(promise, options) {
1322
1326
  timer = customTimers.setTimeout.call(void 0, () => {
1323
1327
  if (fallback) {
1324
1328
  try {
1325
- resolve10(fallback());
1329
+ resolve11(fallback());
1326
1330
  } catch (error) {
1327
1331
  reject(error);
1328
1332
  }
@@ -1332,7 +1336,7 @@ function pTimeout(promise, options) {
1332
1336
  promise.cancel();
1333
1337
  }
1334
1338
  if (message === false) {
1335
- resolve10();
1339
+ resolve11();
1336
1340
  } else if (message instanceof Error) {
1337
1341
  reject(message);
1338
1342
  } else {
@@ -1734,7 +1738,7 @@ var PQueue = class extends import_index.default {
1734
1738
  // Assign unique ID if not provided
1735
1739
  id: options.id ?? (this.#idAssigner++).toString()
1736
1740
  };
1737
- return new Promise((resolve10, reject) => {
1741
+ return new Promise((resolve11, reject) => {
1738
1742
  const taskSymbol = /* @__PURE__ */ Symbol(`task-${options.id}`);
1739
1743
  let cleanupQueueAbortHandler = () => void 0;
1740
1744
  const run = async () => {
@@ -1774,7 +1778,7 @@ var PQueue = class extends import_index.default {
1774
1778
  })]);
1775
1779
  }
1776
1780
  const result = await operation;
1777
- resolve10(result);
1781
+ resolve11(result);
1778
1782
  this.emit("completed", result);
1779
1783
  } catch (error) {
1780
1784
  reject(error);
@@ -1962,13 +1966,13 @@ var PQueue = class extends import_index.default {
1962
1966
  });
1963
1967
  }
1964
1968
  async #onEvent(event, filter) {
1965
- return new Promise((resolve10) => {
1969
+ return new Promise((resolve11) => {
1966
1970
  const listener = () => {
1967
1971
  if (filter && !filter()) {
1968
1972
  return;
1969
1973
  }
1970
1974
  this.off(event, listener);
1971
- resolve10();
1975
+ resolve11();
1972
1976
  };
1973
1977
  this.on(event, listener);
1974
1978
  });
@@ -2254,7 +2258,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2254
2258
  const finalDelay = Math.min(delayTime, remainingTime);
2255
2259
  options.signal?.throwIfAborted();
2256
2260
  if (finalDelay > 0) {
2257
- await new Promise((resolve10, reject) => {
2261
+ await new Promise((resolve11, reject) => {
2258
2262
  const onAbort = () => {
2259
2263
  clearTimeout(timeoutToken);
2260
2264
  options.signal?.removeEventListener("abort", onAbort);
@@ -2262,7 +2266,7 @@ async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTi
2262
2266
  };
2263
2267
  const timeoutToken = setTimeout(() => {
2264
2268
  options.signal?.removeEventListener("abort", onAbort);
2265
- resolve10();
2269
+ resolve11();
2266
2270
  }, finalDelay);
2267
2271
  if (options.unref) {
2268
2272
  timeoutToken.unref?.();
@@ -3682,6 +3686,15 @@ function createMockNativeBinding() {
3682
3686
  close() {
3683
3687
  throw error;
3684
3688
  }
3689
+ getTransitiveReachability() {
3690
+ throw error;
3691
+ }
3692
+ detectCommunities() {
3693
+ throw error;
3694
+ }
3695
+ computeCentrality() {
3696
+ throw error;
3697
+ }
3685
3698
  }
3686
3699
  };
3687
3700
  }
@@ -4232,6 +4245,14 @@ var Database = class {
4232
4245
  this.throwIfClosed();
4233
4246
  return this.inner.getSymbolsByNameCi(name);
4234
4247
  }
4248
+ getSymbolsForBranch(branch) {
4249
+ this.throwIfClosed();
4250
+ return this.inner.getSymbolsForBranch(branch);
4251
+ }
4252
+ getSymbolsForFiles(filePaths, branch) {
4253
+ this.throwIfClosed();
4254
+ return this.inner.getSymbolsForFiles(filePaths, branch);
4255
+ }
4235
4256
  deleteSymbolsByFile(filePath) {
4236
4257
  this.throwIfClosed();
4237
4258
  return this.inner.deleteSymbolsByFile(filePath);
@@ -4309,6 +4330,23 @@ var Database = class {
4309
4330
  this.throwIfClosed();
4310
4331
  return this.inner.gcOrphanCallEdges();
4311
4332
  }
4333
+ getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth) {
4334
+ this.throwIfClosed();
4335
+ return this.inner.getTransitiveReachability(
4336
+ rootSymbolIds,
4337
+ branch,
4338
+ direction,
4339
+ maxDepth ?? null
4340
+ );
4341
+ }
4342
+ detectCommunities(branch, symbolIds) {
4343
+ this.throwIfClosed();
4344
+ return this.inner.detectCommunities(branch, symbolIds ?? null);
4345
+ }
4346
+ computeCentrality(branch) {
4347
+ this.throwIfClosed();
4348
+ return this.inner.computeCentrality(branch);
4349
+ }
4312
4350
  };
4313
4351
 
4314
4352
  // src/git/index.ts
@@ -4482,6 +4520,106 @@ function resolveProjectIndexPath(projectRoot, scope) {
4482
4520
  return resolveWorktreeFallbackPath(projectRoot, PROJECT_INDEX_RELATIVE_PATH) ?? localIndexPath;
4483
4521
  }
4484
4522
 
4523
+ // src/tools/changed-files.ts
4524
+ var path9 = __toESM(require("path"), 1);
4525
+ var import_child_process = require("child_process");
4526
+ var import_util = require("util");
4527
+ var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
4528
+ function getErrorMessage(error) {
4529
+ if (error instanceof Error) return error.message;
4530
+ return String(error);
4531
+ }
4532
+ async function getChangedFiles(opts) {
4533
+ const { pr, branch, projectRoot, baseBranch = "main" } = opts;
4534
+ if (pr !== void 0) {
4535
+ return getChangedFilesForPr(pr, projectRoot, baseBranch);
4536
+ }
4537
+ return getChangedFilesForBranch(branch, projectRoot, baseBranch);
4538
+ }
4539
+ async function getChangedFilesForPr(pr, projectRoot, baseBranch) {
4540
+ let headRefName;
4541
+ let actualBaseBranch = baseBranch;
4542
+ try {
4543
+ const { stdout } = await execFileAsync(
4544
+ "gh",
4545
+ ["pr", "view", String(pr), "--json", "headRefName,baseRefName,files"],
4546
+ { cwd: projectRoot, timeout: 3e4 }
4547
+ );
4548
+ const data = JSON.parse(stdout);
4549
+ headRefName = data.headRefName;
4550
+ actualBaseBranch = data.baseRefName || baseBranch;
4551
+ if (data.files && data.files.length > 0) {
4552
+ return {
4553
+ files: normalizeFiles(
4554
+ data.files.map((f) => f.path),
4555
+ projectRoot
4556
+ ),
4557
+ baseBranch: actualBaseBranch,
4558
+ source: "gh",
4559
+ headRefName
4560
+ };
4561
+ }
4562
+ } catch (error) {
4563
+ throw new Error(
4564
+ `Failed to retrieve PR #${pr} via gh CLI: ${getErrorMessage(error)}`
4565
+ );
4566
+ }
4567
+ if (headRefName === void 0) {
4568
+ throw new Error(
4569
+ `PR #${pr} returned no usable branch or file information.`
4570
+ );
4571
+ }
4572
+ const result = await getChangedFilesForBranch(headRefName, projectRoot, actualBaseBranch);
4573
+ return { ...result, headRefName };
4574
+ }
4575
+ async function getChangedFilesForBranch(branch, projectRoot, baseBranch) {
4576
+ const targetBranch = branch || await getCurrentBranch2(projectRoot);
4577
+ const mergeBase = await getMergeBase(projectRoot, baseBranch, targetBranch);
4578
+ const { stdout } = await execFileAsync(
4579
+ "git",
4580
+ ["diff", "--name-only", `${mergeBase}...${targetBranch}`],
4581
+ { cwd: projectRoot, timeout: 3e4 }
4582
+ );
4583
+ return {
4584
+ files: normalizeFiles(stdout.split("\n"), projectRoot),
4585
+ baseBranch,
4586
+ source: "git",
4587
+ headRefName: targetBranch
4588
+ };
4589
+ }
4590
+ async function getCurrentBranch2(projectRoot) {
4591
+ const { stdout } = await execFileAsync(
4592
+ "git",
4593
+ ["branch", "--show-current"],
4594
+ { cwd: projectRoot, timeout: 3e4 }
4595
+ );
4596
+ return stdout.trim() || "HEAD";
4597
+ }
4598
+ async function getMergeBase(projectRoot, baseBranch, branch) {
4599
+ const { stdout } = await execFileAsync(
4600
+ "git",
4601
+ ["merge-base", baseBranch, branch],
4602
+ { cwd: projectRoot, timeout: 3e4 }
4603
+ );
4604
+ return stdout.trim();
4605
+ }
4606
+ function normalizeFiles(rawFiles, projectRoot) {
4607
+ const seen = /* @__PURE__ */ new Set();
4608
+ const result = [];
4609
+ for (const raw of rawFiles) {
4610
+ const trimmed = raw.trim();
4611
+ if (!trimmed) continue;
4612
+ const absolute = path9.resolve(projectRoot, trimmed);
4613
+ const relative5 = path9.relative(projectRoot, absolute);
4614
+ const cleaned = relative5.startsWith("./") ? relative5.slice(2) : relative5;
4615
+ if (!seen.has(cleaned)) {
4616
+ seen.add(cleaned);
4617
+ result.push(cleaned);
4618
+ }
4619
+ }
4620
+ return result;
4621
+ }
4622
+
4485
4623
  // src/indexer/index.ts
4486
4624
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab"]);
4487
4625
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
@@ -4489,6 +4627,7 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4489
4627
  "function_declaration",
4490
4628
  "function",
4491
4629
  "arrow_function",
4630
+ "export_statement",
4492
4631
  "method_definition",
4493
4632
  "class_declaration",
4494
4633
  "interface_declaration",
@@ -4527,7 +4666,7 @@ function float32ArrayToBuffer(arr) {
4527
4666
  function bufferToFloat32Array(buf) {
4528
4667
  return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
4529
4668
  }
4530
- function getErrorMessage(error) {
4669
+ function getErrorMessage2(error) {
4531
4670
  if (error instanceof Error) {
4532
4671
  return error.message;
4533
4672
  }
@@ -4540,7 +4679,7 @@ function getErrorMessage(error) {
4540
4679
  return String(error);
4541
4680
  }
4542
4681
  function isRateLimitError(error) {
4543
- const message = getErrorMessage(error);
4682
+ const message = getErrorMessage2(error);
4544
4683
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
4545
4684
  }
4546
4685
  function getSafeEmbeddingChunkTokenLimit(provider) {
@@ -4558,7 +4697,7 @@ function getDynamicBatchOptions(provider) {
4558
4697
  return {};
4559
4698
  }
4560
4699
  function isSqliteCorruptionError(error) {
4561
- const message = getErrorMessage(error).toLowerCase();
4700
+ const message = getErrorMessage2(error).toLowerCase();
4562
4701
  return message.includes("database disk image is malformed") || message.includes("file is not a database") || message.includes("database schema is corrupt") || message.includes("sqlite_corrupt");
4563
4702
  }
4564
4703
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
@@ -4720,9 +4859,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
4720
4859
  return true;
4721
4860
  }
4722
4861
  function isPathWithinRoot(filePath, rootPath) {
4723
- const normalizedFilePath = path9.resolve(filePath);
4724
- const normalizedRoot = path9.resolve(rootPath);
4725
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path9.sep}`);
4862
+ const normalizedFilePath = path10.resolve(filePath);
4863
+ const normalizedRoot = path10.resolve(rootPath);
4864
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path10.sep}`);
4726
4865
  }
4727
4866
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
4728
4867
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -5775,9 +5914,9 @@ var Indexer = class {
5775
5914
  this.projectRoot = projectRoot;
5776
5915
  this.config = config;
5777
5916
  this.indexPath = this.getIndexPath();
5778
- this.fileHashCachePath = path9.join(this.indexPath, "file-hashes.json");
5779
- this.failedBatchesPath = path9.join(this.indexPath, "failed-batches.json");
5780
- this.indexingLockPath = path9.join(this.indexPath, "indexing.lock");
5917
+ this.fileHashCachePath = path10.join(this.indexPath, "file-hashes.json");
5918
+ this.failedBatchesPath = path10.join(this.indexPath, "failed-batches.json");
5919
+ this.indexingLockPath = path10.join(this.indexPath, "indexing.lock");
5781
5920
  this.logger = initializeLogger(config.debug);
5782
5921
  }
5783
5922
  getIndexPath() {
@@ -5809,14 +5948,14 @@ var Indexer = class {
5809
5948
  }
5810
5949
  atomicWriteSync(targetPath, data) {
5811
5950
  const tempPath = `${targetPath}.tmp`;
5812
- (0, import_fs7.mkdirSync)(path9.dirname(targetPath), { recursive: true });
5951
+ (0, import_fs7.mkdirSync)(path10.dirname(targetPath), { recursive: true });
5813
5952
  (0, import_fs7.writeFileSync)(tempPath, data);
5814
5953
  (0, import_fs7.renameSync)(tempPath, targetPath);
5815
5954
  }
5816
5955
  getScopedRoots() {
5817
- const roots = /* @__PURE__ */ new Set([path9.resolve(this.projectRoot)]);
5956
+ const roots = /* @__PURE__ */ new Set([path10.resolve(this.projectRoot)]);
5818
5957
  for (const kbRoot of this.config.knowledgeBases) {
5819
- roots.add(path9.resolve(this.projectRoot, kbRoot));
5958
+ roots.add(path10.resolve(this.projectRoot, kbRoot));
5820
5959
  }
5821
5960
  return Array.from(roots);
5822
5961
  }
@@ -5825,22 +5964,29 @@ var Indexer = class {
5825
5964
  if (this.config.scope !== "global") {
5826
5965
  return branchName;
5827
5966
  }
5828
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5967
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5968
+ return `${projectHash}:${branchName}`;
5969
+ }
5970
+ getBranchCatalogKeyFor(branchName) {
5971
+ if (this.config.scope !== "global") {
5972
+ return branchName;
5973
+ }
5974
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5829
5975
  return `${projectHash}:${branchName}`;
5830
5976
  }
5831
5977
  getLegacyBranchCatalogKey() {
5832
5978
  return this.currentBranch || "default";
5833
5979
  }
5834
5980
  getLegacyMigrationMetadataKey() {
5835
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5981
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5836
5982
  return `index.globalBranchMigration.${projectHash}`;
5837
5983
  }
5838
5984
  getProjectEmbeddingStrategyMetadataKey() {
5839
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5985
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5840
5986
  return `index.embeddingStrategyVersion.${projectHash}`;
5841
5987
  }
5842
5988
  getProjectForceReembedMetadataKey() {
5843
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5989
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5844
5990
  return `index.forceReembed.${projectHash}`;
5845
5991
  }
5846
5992
  hasProjectForceReembedPending() {
@@ -5934,7 +6080,7 @@ var Indexer = class {
5934
6080
  if (!this.database) {
5935
6081
  return { chunkIds, symbolIds };
5936
6082
  }
5937
- const projectRootPath = path9.resolve(this.projectRoot);
6083
+ const projectRootPath = path10.resolve(this.projectRoot);
5938
6084
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
5939
6085
  ...Array.from(this.fileHashCache.keys()).filter(
5940
6086
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -5957,7 +6103,7 @@ var Indexer = class {
5957
6103
  if (this.config.scope !== "global") {
5958
6104
  return this.getBranchCatalogCleanupKeys();
5959
6105
  }
5960
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
6106
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5961
6107
  const keys = /* @__PURE__ */ new Set();
5962
6108
  const projectChunkIdSet = new Set(projectChunkIds);
5963
6109
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6041,7 +6187,7 @@ var Indexer = class {
6041
6187
  if (!this.database || this.config.scope !== "global") {
6042
6188
  return false;
6043
6189
  }
6044
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
6190
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6045
6191
  const roots = this.getScopedRoots();
6046
6192
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6047
6193
  return this.database.getAllBranches().some(
@@ -6075,7 +6221,7 @@ var Indexer = class {
6075
6221
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6076
6222
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6077
6223
  ]);
6078
- const projectRootPath = path9.resolve(this.projectRoot);
6224
+ const projectRootPath = path10.resolve(this.projectRoot);
6079
6225
  const projectLocalFilePaths = new Set(
6080
6226
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6081
6227
  );
@@ -6328,7 +6474,7 @@ var Indexer = class {
6328
6474
  this.logger.search("warn", "External reranker failed; using deterministic order", {
6329
6475
  provider: reranker.provider,
6330
6476
  model: reranker.model,
6331
- error: getErrorMessage(error)
6477
+ error: getErrorMessage2(error)
6332
6478
  });
6333
6479
  return candidates;
6334
6480
  }
@@ -6435,13 +6581,13 @@ var Indexer = class {
6435
6581
  }
6436
6582
  await import_fs7.promises.mkdir(this.indexPath, { recursive: true });
6437
6583
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6438
- const storePath = path9.join(this.indexPath, "vectors");
6584
+ const storePath = path10.join(this.indexPath, "vectors");
6439
6585
  this.store = new VectorStore(storePath, dimensions);
6440
- const indexFilePath = path9.join(this.indexPath, "vectors.usearch");
6586
+ const indexFilePath = path10.join(this.indexPath, "vectors.usearch");
6441
6587
  if ((0, import_fs7.existsSync)(indexFilePath)) {
6442
6588
  this.store.load();
6443
6589
  }
6444
- const invertedIndexPath = path9.join(this.indexPath, "inverted-index.json");
6590
+ const invertedIndexPath = path10.join(this.indexPath, "inverted-index.json");
6445
6591
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6446
6592
  try {
6447
6593
  this.invertedIndex.load();
@@ -6451,7 +6597,7 @@ var Indexer = class {
6451
6597
  }
6452
6598
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6453
6599
  }
6454
- const dbPath = path9.join(this.indexPath, "codebase.db");
6600
+ const dbPath = path10.join(this.indexPath, "codebase.db");
6455
6601
  let dbIsNew = !(0, import_fs7.existsSync)(dbPath);
6456
6602
  try {
6457
6603
  this.database = new Database(dbPath);
@@ -6532,7 +6678,7 @@ var Indexer = class {
6532
6678
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6533
6679
  return {
6534
6680
  resetCorruptedIndex: true,
6535
- warning: this.getCorruptedIndexWarning(path9.join(this.indexPath, "codebase.db"))
6681
+ warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
6536
6682
  };
6537
6683
  }
6538
6684
  throw error;
@@ -6547,7 +6693,7 @@ var Indexer = class {
6547
6693
  return;
6548
6694
  }
6549
6695
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6550
- const storeBasePath = path9.join(this.indexPath, "vectors");
6696
+ const storeBasePath = path10.join(this.indexPath, "vectors");
6551
6697
  const storeIndexPath = `${storeBasePath}.usearch`;
6552
6698
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6553
6699
  const backupIndexPath = `${storeIndexPath}.bak`;
@@ -6632,9 +6778,9 @@ var Indexer = class {
6632
6778
  if (!isSqliteCorruptionError(error)) {
6633
6779
  return false;
6634
6780
  }
6635
- const dbPath = path9.join(this.indexPath, "codebase.db");
6781
+ const dbPath = path10.join(this.indexPath, "codebase.db");
6636
6782
  const warning = this.getCorruptedIndexWarning(dbPath);
6637
- const errorMessage = getErrorMessage(error);
6783
+ const errorMessage = getErrorMessage2(error);
6638
6784
  if (this.config.scope === "global") {
6639
6785
  this.logger.error("Detected corrupted shared global index database", {
6640
6786
  stage,
@@ -6655,15 +6801,15 @@ var Indexer = class {
6655
6801
  this.indexCompatibility = null;
6656
6802
  this.fileHashCache.clear();
6657
6803
  const resetPaths = [
6658
- path9.join(this.indexPath, "codebase.db"),
6659
- path9.join(this.indexPath, "codebase.db-shm"),
6660
- path9.join(this.indexPath, "codebase.db-wal"),
6661
- path9.join(this.indexPath, "vectors.usearch"),
6662
- path9.join(this.indexPath, "inverted-index.json"),
6663
- path9.join(this.indexPath, "file-hashes.json"),
6664
- path9.join(this.indexPath, "failed-batches.json"),
6665
- path9.join(this.indexPath, "indexing.lock"),
6666
- path9.join(this.indexPath, "vectors")
6804
+ path10.join(this.indexPath, "codebase.db"),
6805
+ path10.join(this.indexPath, "codebase.db-shm"),
6806
+ path10.join(this.indexPath, "codebase.db-wal"),
6807
+ path10.join(this.indexPath, "vectors.usearch"),
6808
+ path10.join(this.indexPath, "inverted-index.json"),
6809
+ path10.join(this.indexPath, "file-hashes.json"),
6810
+ path10.join(this.indexPath, "failed-batches.json"),
6811
+ path10.join(this.indexPath, "indexing.lock"),
6812
+ path10.join(this.indexPath, "vectors")
6667
6813
  ];
6668
6814
  await Promise.all(resetPaths.map(async (targetPath) => {
6669
6815
  try {
@@ -6928,7 +7074,7 @@ var Indexer = class {
6928
7074
  for (const parsed of parsedFiles) {
6929
7075
  currentFilePaths.add(parsed.path);
6930
7076
  if (parsed.chunks.length === 0) {
6931
- const relativePath = path9.relative(this.projectRoot, parsed.path);
7077
+ const relativePath = path10.relative(this.projectRoot, parsed.path);
6932
7078
  stats.parseFailures.push(relativePath);
6933
7079
  }
6934
7080
  let fileChunkCount = 0;
@@ -7212,7 +7358,7 @@ var Indexer = class {
7212
7358
  for (const requestBatch of requestBatches) {
7213
7359
  queue.add(async () => {
7214
7360
  if (rateLimitBackoffMs > 0) {
7215
- await new Promise((resolve10) => setTimeout(resolve10, rateLimitBackoffMs));
7361
+ await new Promise((resolve11) => setTimeout(resolve11, rateLimitBackoffMs));
7216
7362
  }
7217
7363
  try {
7218
7364
  const result = await pRetry(
@@ -7227,7 +7373,7 @@ var Indexer = class {
7227
7373
  factor: 2,
7228
7374
  shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
7229
7375
  onFailedAttempt: (error) => {
7230
- const message = getErrorMessage(error);
7376
+ const message = getErrorMessage2(error);
7231
7377
  if (isRateLimitError(error)) {
7232
7378
  rateLimitBackoffMs = Math.min(providerRateLimits.maxRetryMs, (rateLimitBackoffMs || providerRateLimits.minRetryMs) * 2);
7233
7379
  this.logger.embedding("warn", `Rate limited, backing off`, {
@@ -7334,7 +7480,7 @@ var Indexer = class {
7334
7480
  });
7335
7481
  } catch (error) {
7336
7482
  const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id));
7337
- const failureMessage = getErrorMessage(error);
7483
+ const failureMessage = getErrorMessage2(error);
7338
7484
  const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
7339
7485
  for (const chunk of failedChunks) {
7340
7486
  if (!failedChunkIds.has(chunk.id)) {
@@ -7773,8 +7919,8 @@ var Indexer = class {
7773
7919
  this.indexCompatibility = compatibility;
7774
7920
  return;
7775
7921
  }
7776
- const localProjectIndexPath = path9.join(this.projectRoot, ".opencode", "index");
7777
- if (path9.resolve(this.indexPath) !== path9.resolve(localProjectIndexPath)) {
7922
+ const localProjectIndexPath = path10.join(this.projectRoot, ".opencode", "index");
7923
+ if (path10.resolve(this.indexPath) !== path10.resolve(localProjectIndexPath)) {
7778
7924
  throw new Error(
7779
7925
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
7780
7926
  );
@@ -7862,7 +8008,7 @@ var Indexer = class {
7862
8008
  gcOrphanSymbols: 0,
7863
8009
  gcOrphanCallEdges: 0,
7864
8010
  resetCorruptedIndex: true,
7865
- warning: this.getCorruptedIndexWarning(path9.join(this.indexPath, "codebase.db"))
8011
+ warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
7866
8012
  };
7867
8013
  }
7868
8014
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8013,7 +8159,7 @@ var Indexer = class {
8013
8159
  succeeded += successfulResults.length;
8014
8160
  stillFailing.push(...failedChunksForBatch.values());
8015
8161
  } catch (error) {
8016
- const failureMessage = getErrorMessage(error);
8162
+ const failureMessage = getErrorMessage2(error);
8017
8163
  const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8018
8164
  const unaccountedChunks = batch.chunks.filter(
8019
8165
  (chunk) => !failedChunksForBatch.has(chunk.id) && !completedChunkIds.has(chunk.id)
@@ -8219,13 +8365,194 @@ var Indexer = class {
8219
8365
  const { database } = await this.ensureInitialized();
8220
8366
  let shortest = [];
8221
8367
  for (const branchKey of this.getBranchCatalogKeys()) {
8222
- const path18 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8223
- if (path18.length > 0 && (shortest.length === 0 || path18.length < shortest.length)) {
8224
- shortest = path18;
8368
+ const path19 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8369
+ if (path19.length > 0 && (shortest.length === 0 || path19.length < shortest.length)) {
8370
+ shortest = path19;
8225
8371
  }
8226
8372
  }
8227
8373
  return shortest;
8228
8374
  }
8375
+ async getSymbolsForBranch(branch) {
8376
+ const { database } = await this.ensureInitialized();
8377
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8378
+ return database.getSymbolsForBranch(resolvedBranch);
8379
+ }
8380
+ async getSymbolsForFiles(filePaths, branch) {
8381
+ const { database } = await this.ensureInitialized();
8382
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8383
+ return database.getSymbolsForFiles(filePaths, resolvedBranch);
8384
+ }
8385
+ async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8386
+ const { database } = await this.ensureInitialized();
8387
+ const branch = this.getBranchCatalogKey();
8388
+ return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8389
+ }
8390
+ async detectCommunities(branch, symbolIds) {
8391
+ const { database } = await this.ensureInitialized();
8392
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8393
+ return database.detectCommunities(resolvedBranch, symbolIds);
8394
+ }
8395
+ async computeCentrality(branch) {
8396
+ const { database } = await this.ensureInitialized();
8397
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8398
+ return database.computeCentrality(resolvedBranch);
8399
+ }
8400
+ async getPrImpact(opts) {
8401
+ const { database } = await this.ensureInitialized();
8402
+ const execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
8403
+ const changedFilesResult = await getChangedFiles({
8404
+ pr: opts.pr,
8405
+ branch: opts.branch,
8406
+ projectRoot: this.projectRoot,
8407
+ baseBranch: this.baseBranch
8408
+ });
8409
+ const changedFiles = changedFilesResult.files;
8410
+ const headRefName = changedFilesResult.headRefName;
8411
+ if (opts.pr !== void 0 && headRefName === void 0) {
8412
+ throw new Error(
8413
+ `Could not resolve head branch for PR #${opts.pr}. Run index_codebase on the PR branch first.`
8414
+ );
8415
+ }
8416
+ const resolvedBranch = opts.pr !== void 0 ? headRefName : opts.branch || this.currentBranch;
8417
+ const branchKey = this.getBranchCatalogKeyFor(resolvedBranch || "default");
8418
+ const branchSymbols = database.getSymbolsForBranch(branchKey);
8419
+ if (branchSymbols.length === 0) {
8420
+ throw new Error(
8421
+ "Run index_codebase first to build the call graph and symbol index for this branch."
8422
+ );
8423
+ }
8424
+ const absoluteChangedFiles = changedFiles.map((f) => path10.resolve(this.projectRoot, f));
8425
+ const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8426
+ const directIds = directSymbols.map((s) => s.id);
8427
+ const direction = opts.direction ?? "both";
8428
+ const maxDepth = opts.maxDepth ?? 5;
8429
+ const transitiveCallers = database.getTransitiveReachability(
8430
+ directIds,
8431
+ branchKey,
8432
+ direction,
8433
+ maxDepth
8434
+ );
8435
+ const affectedIdsSet = new Set(directIds);
8436
+ for (const caller of transitiveCallers) {
8437
+ affectedIdsSet.add(caller.symbolId);
8438
+ }
8439
+ const allAffectedIds = Array.from(affectedIdsSet);
8440
+ const communitiesData = database.detectCommunities(branchKey, allAffectedIds);
8441
+ const communityMap = /* @__PURE__ */ new Map();
8442
+ for (const c of communitiesData) {
8443
+ if (!communityMap.has(c.communityLabel)) {
8444
+ communityMap.set(c.communityLabel, {
8445
+ label: c.communityLabel,
8446
+ symbolCount: 0,
8447
+ directSymbols: /* @__PURE__ */ new Set()
8448
+ });
8449
+ }
8450
+ const entry = communityMap.get(c.communityLabel);
8451
+ entry.symbolCount++;
8452
+ if (directIds.includes(c.symbolId)) {
8453
+ entry.directSymbols.add(c.symbolId);
8454
+ }
8455
+ }
8456
+ const communities = Array.from(communityMap.values()).map((c) => ({
8457
+ label: c.label,
8458
+ symbolCount: c.symbolCount,
8459
+ directSymbols: Array.from(c.directSymbols)
8460
+ }));
8461
+ const centralityData = database.computeCentrality(branchKey);
8462
+ const hubThreshold = opts.hubThreshold ?? 10;
8463
+ const hubNodes = centralityData.filter((c) => directIds.includes(c.symbolId) && c.callerCount >= hubThreshold).map((c) => ({
8464
+ id: c.symbolId,
8465
+ name: c.symbolName,
8466
+ callerCount: c.callerCount,
8467
+ filePath: c.filePath
8468
+ }));
8469
+ const totalAffected = allAffectedIds.length;
8470
+ let riskLevel;
8471
+ let riskReason;
8472
+ if (totalAffected < 5 && hubNodes.length === 0) {
8473
+ riskLevel = "LOW";
8474
+ riskReason = `Small impact: ${totalAffected} affected symbols, no hub nodes touched.`;
8475
+ } else if (totalAffected > 20 || hubNodes.length > 1) {
8476
+ riskLevel = "HIGH";
8477
+ riskReason = `Large impact: ${totalAffected} affected symbols${hubNodes.length > 0 ? `, ${hubNodes.length} hub nodes touched` : ""}.`;
8478
+ } else {
8479
+ riskLevel = "MEDIUM";
8480
+ riskReason = `Moderate impact: ${totalAffected} affected symbols${hubNodes.length === 1 ? ", 1 hub node touched" : ""}.`;
8481
+ }
8482
+ let conflictingPRs;
8483
+ if (opts.checkConflicts) {
8484
+ conflictingPRs = [];
8485
+ try {
8486
+ const { stdout } = await execFileAsync2(
8487
+ "gh",
8488
+ ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "10000"],
8489
+ { cwd: this.projectRoot, timeout: 3e4 }
8490
+ );
8491
+ const openPRs = JSON.parse(stdout);
8492
+ const currentCommunityLabels = new Set(communities.map((c) => c.label));
8493
+ const allCommunitiesData = database.detectCommunities(branchKey);
8494
+ const symbolToCommunity = /* @__PURE__ */ new Map();
8495
+ const structuralKey = (filePath, name) => `${filePath.toLowerCase()}:${name.toLowerCase()}`;
8496
+ for (const c of allCommunitiesData) {
8497
+ symbolToCommunity.set(structuralKey(c.filePath, c.symbolName), c.communityLabel);
8498
+ }
8499
+ for (const openPr of openPRs) {
8500
+ if (openPr.number === opts.pr) continue;
8501
+ try {
8502
+ const otherChanged = await getChangedFiles({
8503
+ pr: openPr.number,
8504
+ projectRoot: this.projectRoot,
8505
+ baseBranch: this.baseBranch
8506
+ });
8507
+ const otherAbsolute = otherChanged.files.map((f) => path10.resolve(this.projectRoot, f));
8508
+ const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8509
+ const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8510
+ const otherLabels = /* @__PURE__ */ new Set();
8511
+ for (const sym of otherSymbols) {
8512
+ const label = symbolToCommunity.get(structuralKey(sym.filePath, sym.name));
8513
+ if (label) {
8514
+ otherLabels.add(label);
8515
+ }
8516
+ }
8517
+ const overlapping = Array.from(otherLabels).filter(
8518
+ (l) => currentCommunityLabels.has(l)
8519
+ );
8520
+ if (overlapping.length > 0) {
8521
+ conflictingPRs.push({
8522
+ pr: openPr.number,
8523
+ branch: openPr.headRefName,
8524
+ overlappingCommunities: overlapping
8525
+ });
8526
+ }
8527
+ } catch {
8528
+ }
8529
+ }
8530
+ } catch {
8531
+ }
8532
+ }
8533
+ return {
8534
+ changedFiles,
8535
+ directSymbols: directSymbols.map((s) => ({
8536
+ id: s.id,
8537
+ name: s.name,
8538
+ kind: s.kind,
8539
+ filePath: s.filePath
8540
+ })),
8541
+ transitiveCallers: transitiveCallers.map((c) => ({
8542
+ id: c.symbolId,
8543
+ name: c.symbolName,
8544
+ filePath: c.filePath,
8545
+ depth: c.depth
8546
+ })),
8547
+ totalAffected,
8548
+ communities,
8549
+ hubNodes,
8550
+ riskLevel,
8551
+ riskReason,
8552
+ direction,
8553
+ conflictingPRs
8554
+ };
8555
+ }
8229
8556
  async close() {
8230
8557
  await this.database?.close();
8231
8558
  this.database = null;
@@ -8485,13 +8812,13 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
8485
8812
  // src/eval/runner-config.ts
8486
8813
  var import_fs8 = require("fs");
8487
8814
  var os4 = __toESM(require("os"), 1);
8488
- var path11 = __toESM(require("path"), 1);
8815
+ var path12 = __toESM(require("path"), 1);
8489
8816
 
8490
8817
  // src/config/rebase.ts
8491
- var path10 = __toESM(require("path"), 1);
8818
+ var path11 = __toESM(require("path"), 1);
8492
8819
  function isWithinRoot(rootDir, targetPath) {
8493
- const relativePath = path10.relative(rootDir, targetPath);
8494
- return relativePath === "" || !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
8820
+ const relativePath = path11.relative(rootDir, targetPath);
8821
+ return relativePath === "" || !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
8495
8822
  }
8496
8823
  function rebasePathEntries(values, fromDir, toDir) {
8497
8824
  if (!Array.isArray(values)) {
@@ -8499,10 +8826,10 @@ function rebasePathEntries(values, fromDir, toDir) {
8499
8826
  }
8500
8827
  return values.filter((value) => typeof value === "string").map((value) => {
8501
8828
  const trimmed = value.trim();
8502
- if (!trimmed || path10.isAbsolute(trimmed)) {
8829
+ if (!trimmed || path11.isAbsolute(trimmed)) {
8503
8830
  return trimmed;
8504
8831
  }
8505
- return normalizePathSeparators(path10.normalize(path10.relative(toDir, path10.resolve(fromDir, trimmed))));
8832
+ return normalizePathSeparators(path11.normalize(path11.relative(toDir, path11.resolve(fromDir, trimmed))));
8506
8833
  }).filter(Boolean);
8507
8834
  }
8508
8835
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -8514,17 +8841,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
8514
8841
  if (!trimmed) {
8515
8842
  return trimmed;
8516
8843
  }
8517
- if (path10.isAbsolute(trimmed)) {
8844
+ if (path11.isAbsolute(trimmed)) {
8518
8845
  if (isWithinRoot(sourceRoot, trimmed)) {
8519
- return normalizePathSeparators(path10.normalize(path10.relative(sourceRoot, trimmed) || "."));
8846
+ return normalizePathSeparators(path11.normalize(path11.relative(sourceRoot, trimmed) || "."));
8520
8847
  }
8521
- return path10.normalize(trimmed);
8848
+ return path11.normalize(trimmed);
8522
8849
  }
8523
- const resolvedFromSource = path10.resolve(sourceRoot, trimmed);
8850
+ const resolvedFromSource = path11.resolve(sourceRoot, trimmed);
8524
8851
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
8525
- return normalizePathSeparators(path10.normalize(trimmed));
8852
+ return normalizePathSeparators(path11.normalize(trimmed));
8526
8853
  }
8527
- return normalizePathSeparators(path10.normalize(path10.relative(targetRoot, resolvedFromSource)));
8854
+ return normalizePathSeparators(path11.normalize(path11.relative(targetRoot, resolvedFromSource)));
8528
8855
  }).filter(Boolean);
8529
8856
  }
8530
8857
 
@@ -8572,20 +8899,20 @@ function parseJsonConfigFile(filePath) {
8572
8899
  }
8573
8900
  }
8574
8901
  function toAbsolute(projectRoot, maybeRelative) {
8575
- return path11.isAbsolute(maybeRelative) ? maybeRelative : path11.join(projectRoot, maybeRelative);
8902
+ return path12.isAbsolute(maybeRelative) ? maybeRelative : path12.join(projectRoot, maybeRelative);
8576
8903
  }
8577
8904
  function isProjectScopedConfigPath(configPath) {
8578
- return path11.basename(configPath) === "codebase-index.json" && path11.basename(path11.dirname(configPath)) === ".opencode";
8905
+ return path12.basename(configPath) === "codebase-index.json" && path12.basename(path12.dirname(configPath)) === ".opencode";
8579
8906
  }
8580
8907
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
8581
8908
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
8582
8909
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
8583
8910
  values,
8584
- path11.dirname(path11.dirname(resolvedConfigPath)),
8911
+ path12.dirname(path12.dirname(resolvedConfigPath)),
8585
8912
  projectRoot
8586
8913
  ) : rebasePathEntries(
8587
8914
  values,
8588
- path11.dirname(resolvedConfigPath),
8915
+ path12.dirname(resolvedConfigPath),
8589
8916
  projectRoot
8590
8917
  );
8591
8918
  if (Array.isArray(config.knowledgeBases)) {
@@ -8613,7 +8940,7 @@ function loadRawConfig(projectRoot, configPath) {
8613
8940
  projectConfig
8614
8941
  );
8615
8942
  }
8616
- const globalConfig = path11.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8943
+ const globalConfig = path12.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8617
8944
  if ((0, import_fs8.existsSync)(globalConfig)) {
8618
8945
  return parseJsonConfigFile(globalConfig);
8619
8946
  }
@@ -8623,10 +8950,10 @@ function getIndexRootPath(projectRoot, scope) {
8623
8950
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
8624
8951
  }
8625
8952
  function getLocalProjectIndexRoot(projectRoot) {
8626
- return path11.join(projectRoot, ".opencode", "index");
8953
+ return path12.join(projectRoot, ".opencode", "index");
8627
8954
  }
8628
8955
  function getLocalProjectConfigPath(projectRoot) {
8629
- return path11.join(projectRoot, ".opencode", "codebase-index.json");
8956
+ return path12.join(projectRoot, ".opencode", "codebase-index.json");
8630
8957
  }
8631
8958
  function clearIndexRoot(projectRoot, scope) {
8632
8959
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
@@ -8648,7 +8975,7 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
8648
8975
  projectRoot,
8649
8976
  resolvedConfigPath
8650
8977
  );
8651
- (0, import_fs8.mkdirSync)(path11.dirname(localConfigPath), { recursive: true });
8978
+ (0, import_fs8.mkdirSync)(path12.dirname(localConfigPath), { recursive: true });
8652
8979
  (0, import_fs8.writeFileSync)(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
8653
8980
  return localConfigPath;
8654
8981
  }
@@ -8698,23 +9025,23 @@ function isRecord2(value) {
8698
9025
  function isStringArray3(value) {
8699
9026
  return Array.isArray(value) && value.every((item) => typeof item === "string");
8700
9027
  }
8701
- function asPositiveNumber(value, path18) {
9028
+ function asPositiveNumber(value, path19) {
8702
9029
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
8703
- throw new Error(`${path18} must be a non-negative number`);
9030
+ throw new Error(`${path19} must be a non-negative number`);
8704
9031
  }
8705
9032
  return value;
8706
9033
  }
8707
- function parseQueryType(value, path18) {
9034
+ function parseQueryType(value, path19) {
8708
9035
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
8709
9036
  return value;
8710
9037
  }
8711
9038
  throw new Error(
8712
- `${path18} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9039
+ `${path19} must be one of: definition, implementation-intent, similarity, keyword-heavy`
8713
9040
  );
8714
9041
  }
8715
- function parseExpected(input, path18) {
9042
+ function parseExpected(input, path19) {
8716
9043
  if (!isRecord2(input)) {
8717
- throw new Error(`${path18} must be an object`);
9044
+ throw new Error(`${path19} must be an object`);
8718
9045
  }
8719
9046
  const filePathRaw = input.filePath;
8720
9047
  const acceptableFilesRaw = input.acceptableFiles;
@@ -8723,16 +9050,16 @@ function parseExpected(input, path18) {
8723
9050
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
8724
9051
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
8725
9052
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
8726
- throw new Error(`${path18} must include either expected.filePath or expected.acceptableFiles`);
9053
+ throw new Error(`${path19} must include either expected.filePath or expected.acceptableFiles`);
8727
9054
  }
8728
9055
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
8729
- throw new Error(`${path18}.acceptableFiles must be an array of strings`);
9056
+ throw new Error(`${path19}.acceptableFiles must be an array of strings`);
8730
9057
  }
8731
9058
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
8732
- throw new Error(`${path18}.symbol must be a string when provided`);
9059
+ throw new Error(`${path19}.symbol must be a string when provided`);
8733
9060
  }
8734
9061
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
8735
- throw new Error(`${path18}.branch must be a string when provided`);
9062
+ throw new Error(`${path19}.branch must be a string when provided`);
8736
9063
  }
8737
9064
  return {
8738
9065
  filePath,
@@ -8742,25 +9069,25 @@ function parseExpected(input, path18) {
8742
9069
  };
8743
9070
  }
8744
9071
  function parseQuery(input, index) {
8745
- const path18 = `queries[${index}]`;
9072
+ const path19 = `queries[${index}]`;
8746
9073
  if (!isRecord2(input)) {
8747
- throw new Error(`${path18} must be an object`);
9074
+ throw new Error(`${path19} must be an object`);
8748
9075
  }
8749
9076
  const id = input.id;
8750
9077
  const query = input.query;
8751
9078
  const queryType = input.queryType;
8752
9079
  const expected = input.expected;
8753
9080
  if (typeof id !== "string" || id.trim().length === 0) {
8754
- throw new Error(`${path18}.id must be a non-empty string`);
9081
+ throw new Error(`${path19}.id must be a non-empty string`);
8755
9082
  }
8756
9083
  if (typeof query !== "string" || query.trim().length === 0) {
8757
- throw new Error(`${path18}.query must be a non-empty string`);
9084
+ throw new Error(`${path19}.query must be a non-empty string`);
8758
9085
  }
8759
9086
  return {
8760
9087
  id,
8761
9088
  query,
8762
- queryType: parseQueryType(queryType, `${path18}.queryType`),
8763
- expected: parseExpected(expected, `${path18}.expected`)
9089
+ queryType: parseQueryType(queryType, `${path19}.queryType`),
9090
+ expected: parseExpected(expected, `${path19}.expected`)
8764
9091
  };
8765
9092
  }
8766
9093
  function parseGoldenDataset(raw, sourceLabel) {
@@ -8943,13 +9270,13 @@ async function runEvaluation(options) {
8943
9270
  };
8944
9271
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
8945
9272
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
8946
- writeJson(path12.join(outputDir, "summary.json"), summary);
8947
- writeJson(path12.join(outputDir, "per-query.json"), perQueryArtifact);
9273
+ writeJson(path13.join(outputDir, "summary.json"), summary);
9274
+ writeJson(path13.join(outputDir, "per-query.json"), perQueryArtifact);
8948
9275
  let comparison;
8949
9276
  if (againstPath) {
8950
9277
  const baseline = loadSummary(againstPath);
8951
9278
  comparison = compareSummaries(summary, baseline, againstPath);
8952
- writeJson(path12.join(outputDir, "compare.json"), comparison);
9279
+ writeJson(path13.join(outputDir, "compare.json"), comparison);
8953
9280
  }
8954
9281
  let gate;
8955
9282
  if (options.ciMode) {
@@ -8962,7 +9289,7 @@ async function runEvaluation(options) {
8962
9289
  if ((0, import_fs10.existsSync)(resolvedBaseline)) {
8963
9290
  const baselineSummary = loadSummary(resolvedBaseline);
8964
9291
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
8965
- writeJson(path12.join(outputDir, "compare.json"), comparison);
9292
+ writeJson(path13.join(outputDir, "compare.json"), comparison);
8966
9293
  } else if (budget.failOnMissingBaseline) {
8967
9294
  throw new Error(
8968
9295
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -8972,7 +9299,7 @@ async function runEvaluation(options) {
8972
9299
  gate = evaluateBudgetGate(budget, summary, comparison);
8973
9300
  }
8974
9301
  const markdown = createSummaryMarkdown(summary, comparison, gate);
8975
- writeText(path12.join(outputDir, "summary.md"), markdown);
9302
+ writeText(path13.join(outputDir, "summary.md"), markdown);
8976
9303
  return { outputDir, summary, perQuery, comparison, gate };
8977
9304
  } finally {
8978
9305
  await indexer.close();
@@ -9030,23 +9357,23 @@ async function runSweep(options, sweep) {
9030
9357
  bestByMrrAt10,
9031
9358
  bestByP95Latency
9032
9359
  };
9033
- writeJson(path12.join(outputDir, "compare.json"), aggregate);
9360
+ writeJson(path13.join(outputDir, "compare.json"), aggregate);
9034
9361
  const md = createSummaryMarkdown(
9035
9362
  bestByHitAt5?.summary ?? runs[0].summary,
9036
9363
  bestByHitAt5?.comparison,
9037
9364
  void 0,
9038
9365
  aggregate
9039
9366
  );
9040
- writeText(path12.join(outputDir, "summary.md"), md);
9041
- writeJson(path12.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9367
+ writeText(path13.join(outputDir, "summary.md"), md);
9368
+ writeJson(path13.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9042
9369
  return { outputDir, aggregate };
9043
9370
  }
9044
9371
 
9045
9372
  // src/eval/cli.ts
9046
- var path14 = __toESM(require("path"), 1);
9373
+ var path15 = __toESM(require("path"), 1);
9047
9374
 
9048
9375
  // src/eval/cli-parser.ts
9049
- var path13 = __toESM(require("path"), 1);
9376
+ var path14 = __toESM(require("path"), 1);
9050
9377
  function printUsage() {
9051
9378
  console.log(`
9052
9379
  Usage:
@@ -9118,12 +9445,12 @@ function parseEvalArgs(argv, cwd) {
9118
9445
  const arg = argv[i];
9119
9446
  const next = argv[i + 1];
9120
9447
  if (arg === "--project" && next) {
9121
- parsed.projectRoot = path13.resolve(cwd, next);
9448
+ parsed.projectRoot = path14.resolve(cwd, next);
9122
9449
  i += 1;
9123
9450
  continue;
9124
9451
  }
9125
9452
  if (arg === "--config" && next) {
9126
- parsed.configPath = path13.resolve(cwd, next);
9453
+ parsed.configPath = path14.resolve(cwd, next);
9127
9454
  i += 1;
9128
9455
  continue;
9129
9456
  }
@@ -9319,22 +9646,22 @@ async function handleEvalCommand(args, cwd) {
9319
9646
  if (!parsed.againstPath.endsWith(".json")) {
9320
9647
  throw new Error("eval diff --against must point to a summary JSON file");
9321
9648
  }
9322
- const currentSummary = loadSummary(path14.resolve(parsed.projectRoot, currentPath), {
9649
+ const currentSummary = loadSummary(path15.resolve(parsed.projectRoot, currentPath), {
9323
9650
  allowLegacyDiversityMetrics: true
9324
9651
  });
9325
- const baselineSummary = loadSummary(path14.resolve(parsed.projectRoot, parsed.againstPath), {
9652
+ const baselineSummary = loadSummary(path15.resolve(parsed.projectRoot, parsed.againstPath), {
9326
9653
  allowLegacyDiversityMetrics: true
9327
9654
  });
9328
9655
  const comparison = compareSummaries(
9329
9656
  currentSummary,
9330
9657
  baselineSummary,
9331
- path14.resolve(parsed.projectRoot, parsed.againstPath)
9658
+ path15.resolve(parsed.projectRoot, parsed.againstPath)
9332
9659
  );
9333
- const outputDir = createRunDirectory(path14.resolve(parsed.projectRoot, parsed.outputRoot));
9660
+ const outputDir = createRunDirectory(path15.resolve(parsed.projectRoot, parsed.outputRoot));
9334
9661
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
9335
- writeJson(path14.join(outputDir, "compare.json"), comparison);
9336
- writeText(path14.join(outputDir, "summary.md"), summaryMd);
9337
- writeJson(path14.join(outputDir, "summary.json"), currentSummary);
9662
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9663
+ writeText(path15.join(outputDir, "summary.md"), summaryMd);
9664
+ writeJson(path15.join(outputDir, "summary.json"), currentSummary);
9338
9665
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
9339
9666
  return 0;
9340
9667
  }
@@ -9343,7 +9670,7 @@ async function handleEvalCommand(args, cwd) {
9343
9670
 
9344
9671
  // src/mcp-server.ts
9345
9672
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
9346
- var path16 = __toESM(require("path"), 1);
9673
+ var path17 = __toESM(require("path"), 1);
9347
9674
  var import_fs12 = require("fs");
9348
9675
 
9349
9676
  // src/mcp-server/register-prompts.ts
@@ -9442,7 +9769,7 @@ var import_zod2 = require("zod");
9442
9769
  // src/config/merger.ts
9443
9770
  var import_fs11 = require("fs");
9444
9771
  var os5 = __toESM(require("os"), 1);
9445
- var path15 = __toESM(require("path"), 1);
9772
+ var path16 = __toESM(require("path"), 1);
9446
9773
  var PROJECT_OVERRIDE_KEYS = [
9447
9774
  "embeddingProvider",
9448
9775
  "customProvider",
@@ -9475,8 +9802,8 @@ function mergeUniqueStringArray(values) {
9475
9802
  return [...new Set(values.map((value) => String(value).trim()))];
9476
9803
  }
9477
9804
  function normalizeKnowledgeBasePath(value) {
9478
- let normalized = path15.normalize(String(value).trim());
9479
- const root = path15.parse(normalized).root;
9805
+ let normalized = path16.normalize(String(value).trim());
9806
+ const root = path16.parse(normalized).root;
9480
9807
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
9481
9808
  normalized = normalized.slice(0, -1);
9482
9809
  }
@@ -9526,8 +9853,8 @@ function loadJsonFile(filePath) {
9526
9853
  return null;
9527
9854
  }
9528
9855
  function materializeLocalProjectConfig(projectRoot, config) {
9529
- const localConfigPath = path15.join(projectRoot, ".opencode", "codebase-index.json");
9530
- (0, import_fs11.mkdirSync)(path15.dirname(localConfigPath), { recursive: true });
9856
+ const localConfigPath = path16.join(projectRoot, ".opencode", "codebase-index.json");
9857
+ (0, import_fs11.mkdirSync)(path16.dirname(localConfigPath), { recursive: true });
9531
9858
  (0, import_fs11.writeFileSync)(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
9532
9859
  return localConfigPath;
9533
9860
  }
@@ -9538,7 +9865,7 @@ function loadProjectConfigLayer(projectRoot) {
9538
9865
  return {};
9539
9866
  }
9540
9867
  const normalizedConfig = { ...projectConfig };
9541
- const projectConfigBaseDir = path15.dirname(path15.dirname(projectConfigPath));
9868
+ const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
9542
9869
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
9543
9870
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
9544
9871
  normalizedConfig.knowledgeBases,
@@ -9757,6 +10084,57 @@ ${truncateContent(r.content)}
9757
10084
  return formatted.join("\n\n");
9758
10085
  }
9759
10086
 
10087
+ // src/tools/format-pr-impact.ts
10088
+ function formatPrImpact(result) {
10089
+ const lines = [];
10090
+ lines.push(`\u2192 Files changed: ${result.changedFiles.length}`);
10091
+ for (const file of result.changedFiles) {
10092
+ lines.push(` - ${file}`);
10093
+ }
10094
+ const directCount = result.directSymbols.length;
10095
+ const transitiveCount = result.transitiveCallers.length;
10096
+ const directionLabel = result.direction === "callees" ? "callees" : result.direction === "both" ? "reachable (callers + callees)" : "callers";
10097
+ lines.push(
10098
+ `\u2192 Symbols affected: ${result.totalAffected} (${directCount} direct, ${transitiveCount} transitive ${directionLabel})`
10099
+ );
10100
+ if (directCount > 0) {
10101
+ lines.push(
10102
+ ` Direct: ${result.directSymbols.map((s) => `${s.name} (${s.kind})`).join(", ")}`
10103
+ );
10104
+ }
10105
+ if (transitiveCount > 0) {
10106
+ lines.push(
10107
+ ` Transitive ${directionLabel}: ${result.transitiveCallers.map((s) => s.name).join(", ")}`
10108
+ );
10109
+ }
10110
+ if (result.communities.length > 0) {
10111
+ lines.push(
10112
+ `\u2192 Communities touched: ${result.communities.map((c) => c.label).join(", ")}`
10113
+ );
10114
+ for (const community of result.communities) {
10115
+ lines.push(
10116
+ ` - ${community.label}: ${community.symbolCount} symbols`
10117
+ );
10118
+ }
10119
+ } else {
10120
+ lines.push("\u2192 Communities touched: none");
10121
+ }
10122
+ lines.push(`\u2192 Risk: ${result.riskLevel} \u2014 ${result.riskReason}`);
10123
+ if (result.hubNodes.length > 0) {
10124
+ lines.push(" Hub nodes in change scope:");
10125
+ for (const hub of result.hubNodes) {
10126
+ lines.push(` - ${hub.name} (${hub.callerCount} callers) at ${hub.filePath}`);
10127
+ }
10128
+ }
10129
+ if (result.conflictingPRs && result.conflictingPRs.length > 0) {
10130
+ const conflictList = result.conflictingPRs.map(
10131
+ (c) => `PR #${c.pr} (also touches ${c.overlappingCommunities.join(", ")} community)`
10132
+ );
10133
+ lines.push(`\u2192 Potential conflicts with: ${conflictList.join(", ")}`);
10134
+ }
10135
+ return lines.join("\n");
10136
+ }
10137
+
9760
10138
  // src/mcp-server/shared.ts
9761
10139
  var MAX_CONTENT_LINES2 = 30;
9762
10140
  function truncateContent2(content) {
@@ -10051,19 +10429,49 @@ ${formatted.join("\n")}` }] };
10051
10429
  async (args) => {
10052
10430
  await runtime.ensureInitialized();
10053
10431
  const indexer = runtime.getIndexer();
10054
- const path18 = await indexer.findCallPath(args.from, args.to, args.maxDepth);
10055
- if (path18.length === 0) {
10432
+ const path19 = await indexer.findCallPath(args.from, args.to, args.maxDepth);
10433
+ if (path19.length === 0) {
10056
10434
  return { content: [{ type: "text", text: `No path found between "${args.from}" and "${args.to}". They may be in disconnected components, or the call graph index needs updating.` }] };
10057
10435
  }
10058
- const formatted = path18.map((hop, i) => {
10436
+ const formatted = path19.map((hop, i) => {
10059
10437
  const prefix = i === 0 ? "[start]" : `--${hop.callType}-->`;
10060
10438
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10061
10439
  return `${prefix} ${hop.symbolName}${location}`;
10062
10440
  });
10063
- return { content: [{ type: "text", text: `Path (${path18.length} hops):
10441
+ return { content: [{ type: "text", text: `Path (${path19.length} hops):
10064
10442
  ${formatted.join("\n")}` }] };
10065
10443
  }
10066
10444
  );
10445
+ server.tool(
10446
+ "pr_impact",
10447
+ "Analyze the impact of a pull request or branch by examining changed files, affected symbols, transitive callers, communities touched, hub nodes, and risk level. Use to understand blast radius before merging.",
10448
+ {
10449
+ pr: import_zod2.z.number().optional().describe("Pull request number to analyze"),
10450
+ branch: import_zod2.z.string().optional().describe("Branch name to analyze (defaults to current branch)"),
10451
+ maxDepth: import_zod2.z.number().optional().default(5).describe("Maximum traversal depth for transitive callers (default: 5)"),
10452
+ hubThreshold: import_zod2.z.number().optional().default(10).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
10453
+ checkConflicts: import_zod2.z.boolean().optional().default(false).describe("Check for conflicting open PRs touching the same communities (default: false)"),
10454
+ direction: import_zod2.z.enum(["callers", "callees", "both"]).optional().default("both").describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
10455
+ },
10456
+ async (args) => {
10457
+ await runtime.ensureInitialized();
10458
+ const indexer = runtime.getIndexer();
10459
+ try {
10460
+ const result = await indexer.getPrImpact({
10461
+ pr: args.pr,
10462
+ branch: args.branch,
10463
+ maxDepth: args.maxDepth,
10464
+ hubThreshold: args.hubThreshold,
10465
+ checkConflicts: args.checkConflicts,
10466
+ direction: args.direction
10467
+ });
10468
+ return { content: [{ type: "text", text: formatPrImpact(result) }] };
10469
+ } catch (error) {
10470
+ const message = error instanceof Error ? error.message : String(error);
10471
+ return { content: [{ type: "text", text: `Error analyzing PR impact: ${message}` }] };
10472
+ }
10473
+ }
10474
+ );
10067
10475
  }
10068
10476
 
10069
10477
  // src/mcp-server.ts
@@ -10082,12 +10490,12 @@ function createMcpServer(projectRoot, config) {
10082
10490
  if (config.scope !== "project") {
10083
10491
  return false;
10084
10492
  }
10085
- const localIndexPath = path16.join(projectRoot, ".opencode", "index");
10493
+ const localIndexPath = path17.join(projectRoot, ".opencode", "index");
10086
10494
  const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
10087
10495
  if (!mainRepoRoot) {
10088
10496
  return false;
10089
10497
  }
10090
- const inheritedIndexPath = path16.join(mainRepoRoot, ".opencode", "index");
10498
+ const inheritedIndexPath = path17.join(mainRepoRoot, ".opencode", "index");
10091
10499
  return !(0, import_fs12.existsSync)(localIndexPath) && (0, import_fs12.existsSync)(inheritedIndexPath);
10092
10500
  }
10093
10501
  async function ensureInitialized() {
@@ -10113,9 +10521,9 @@ function parseArgs(argv) {
10113
10521
  let config;
10114
10522
  for (let i = 2; i < argv.length; i++) {
10115
10523
  if (argv[i] === "--project" && argv[i + 1]) {
10116
- project = path17.resolve(argv[++i]);
10524
+ project = path18.resolve(argv[++i]);
10117
10525
  } else if (argv[i] === "--config" && argv[i + 1]) {
10118
- config = path17.resolve(argv[++i]);
10526
+ config = path18.resolve(argv[++i]);
10119
10527
  }
10120
10528
  }
10121
10529
  return { project, config };