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.js 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
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
- import * as path17 from "path";
656
+ import * as path18 from "path";
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 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
1079
1081
  import * as path from "path";
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
  import { existsSync as existsSync8 } from "fs";
1273
- import * as path12 from "path";
1275
+ import * as path13 from "path";
1274
1276
  import { performance as performance3 } from "perf_hooks";
1275
1277
 
1276
1278
  // src/indexer/index.ts
1277
1279
  import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync, unlinkSync, mkdirSync as mkdirSync2, promises as fsPromises2 } from "fs";
1278
- import * as path9 from "path";
1280
+ import * as path10 from "path";
1279
1281
  import { performance as performance2 } from "perf_hooks";
1282
+ import { execFile as execFile2 } from "child_process";
1283
+ import { promisify as promisify2 } from "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?.();
@@ -3681,6 +3685,15 @@ function createMockNativeBinding() {
3681
3685
  close() {
3682
3686
  throw error;
3683
3687
  }
3688
+ getTransitiveReachability() {
3689
+ throw error;
3690
+ }
3691
+ detectCommunities() {
3692
+ throw error;
3693
+ }
3694
+ computeCentrality() {
3695
+ throw error;
3696
+ }
3684
3697
  }
3685
3698
  };
3686
3699
  }
@@ -4231,6 +4244,14 @@ var Database = class {
4231
4244
  this.throwIfClosed();
4232
4245
  return this.inner.getSymbolsByNameCi(name);
4233
4246
  }
4247
+ getSymbolsForBranch(branch) {
4248
+ this.throwIfClosed();
4249
+ return this.inner.getSymbolsForBranch(branch);
4250
+ }
4251
+ getSymbolsForFiles(filePaths, branch) {
4252
+ this.throwIfClosed();
4253
+ return this.inner.getSymbolsForFiles(filePaths, branch);
4254
+ }
4234
4255
  deleteSymbolsByFile(filePath) {
4235
4256
  this.throwIfClosed();
4236
4257
  return this.inner.deleteSymbolsByFile(filePath);
@@ -4308,6 +4329,23 @@ var Database = class {
4308
4329
  this.throwIfClosed();
4309
4330
  return this.inner.gcOrphanCallEdges();
4310
4331
  }
4332
+ getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth) {
4333
+ this.throwIfClosed();
4334
+ return this.inner.getTransitiveReachability(
4335
+ rootSymbolIds,
4336
+ branch,
4337
+ direction,
4338
+ maxDepth ?? null
4339
+ );
4340
+ }
4341
+ detectCommunities(branch, symbolIds) {
4342
+ this.throwIfClosed();
4343
+ return this.inner.detectCommunities(branch, symbolIds ?? null);
4344
+ }
4345
+ computeCentrality(branch) {
4346
+ this.throwIfClosed();
4347
+ return this.inner.computeCentrality(branch);
4348
+ }
4311
4349
  };
4312
4350
 
4313
4351
  // src/git/index.ts
@@ -4481,6 +4519,106 @@ function resolveProjectIndexPath(projectRoot, scope) {
4481
4519
  return resolveWorktreeFallbackPath(projectRoot, PROJECT_INDEX_RELATIVE_PATH) ?? localIndexPath;
4482
4520
  }
4483
4521
 
4522
+ // src/tools/changed-files.ts
4523
+ import * as path9 from "path";
4524
+ import { execFile } from "child_process";
4525
+ import { promisify } from "util";
4526
+ var execFileAsync = promisify(execFile);
4527
+ function getErrorMessage(error) {
4528
+ if (error instanceof Error) return error.message;
4529
+ return String(error);
4530
+ }
4531
+ async function getChangedFiles(opts) {
4532
+ const { pr, branch, projectRoot, baseBranch = "main" } = opts;
4533
+ if (pr !== void 0) {
4534
+ return getChangedFilesForPr(pr, projectRoot, baseBranch);
4535
+ }
4536
+ return getChangedFilesForBranch(branch, projectRoot, baseBranch);
4537
+ }
4538
+ async function getChangedFilesForPr(pr, projectRoot, baseBranch) {
4539
+ let headRefName;
4540
+ let actualBaseBranch = baseBranch;
4541
+ try {
4542
+ const { stdout } = await execFileAsync(
4543
+ "gh",
4544
+ ["pr", "view", String(pr), "--json", "headRefName,baseRefName,files"],
4545
+ { cwd: projectRoot, timeout: 3e4 }
4546
+ );
4547
+ const data = JSON.parse(stdout);
4548
+ headRefName = data.headRefName;
4549
+ actualBaseBranch = data.baseRefName || baseBranch;
4550
+ if (data.files && data.files.length > 0) {
4551
+ return {
4552
+ files: normalizeFiles(
4553
+ data.files.map((f) => f.path),
4554
+ projectRoot
4555
+ ),
4556
+ baseBranch: actualBaseBranch,
4557
+ source: "gh",
4558
+ headRefName
4559
+ };
4560
+ }
4561
+ } catch (error) {
4562
+ throw new Error(
4563
+ `Failed to retrieve PR #${pr} via gh CLI: ${getErrorMessage(error)}`
4564
+ );
4565
+ }
4566
+ if (headRefName === void 0) {
4567
+ throw new Error(
4568
+ `PR #${pr} returned no usable branch or file information.`
4569
+ );
4570
+ }
4571
+ const result = await getChangedFilesForBranch(headRefName, projectRoot, actualBaseBranch);
4572
+ return { ...result, headRefName };
4573
+ }
4574
+ async function getChangedFilesForBranch(branch, projectRoot, baseBranch) {
4575
+ const targetBranch = branch || await getCurrentBranch2(projectRoot);
4576
+ const mergeBase = await getMergeBase(projectRoot, baseBranch, targetBranch);
4577
+ const { stdout } = await execFileAsync(
4578
+ "git",
4579
+ ["diff", "--name-only", `${mergeBase}...${targetBranch}`],
4580
+ { cwd: projectRoot, timeout: 3e4 }
4581
+ );
4582
+ return {
4583
+ files: normalizeFiles(stdout.split("\n"), projectRoot),
4584
+ baseBranch,
4585
+ source: "git",
4586
+ headRefName: targetBranch
4587
+ };
4588
+ }
4589
+ async function getCurrentBranch2(projectRoot) {
4590
+ const { stdout } = await execFileAsync(
4591
+ "git",
4592
+ ["branch", "--show-current"],
4593
+ { cwd: projectRoot, timeout: 3e4 }
4594
+ );
4595
+ return stdout.trim() || "HEAD";
4596
+ }
4597
+ async function getMergeBase(projectRoot, baseBranch, branch) {
4598
+ const { stdout } = await execFileAsync(
4599
+ "git",
4600
+ ["merge-base", baseBranch, branch],
4601
+ { cwd: projectRoot, timeout: 3e4 }
4602
+ );
4603
+ return stdout.trim();
4604
+ }
4605
+ function normalizeFiles(rawFiles, projectRoot) {
4606
+ const seen = /* @__PURE__ */ new Set();
4607
+ const result = [];
4608
+ for (const raw of rawFiles) {
4609
+ const trimmed = raw.trim();
4610
+ if (!trimmed) continue;
4611
+ const absolute = path9.resolve(projectRoot, trimmed);
4612
+ const relative5 = path9.relative(projectRoot, absolute);
4613
+ const cleaned = relative5.startsWith("./") ? relative5.slice(2) : relative5;
4614
+ if (!seen.has(cleaned)) {
4615
+ seen.add(cleaned);
4616
+ result.push(cleaned);
4617
+ }
4618
+ }
4619
+ return result;
4620
+ }
4621
+
4484
4622
  // src/indexer/index.ts
4485
4623
  var CALL_GRAPH_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "tsx", "javascript", "jsx", "python", "go", "rust", "php", "apex", "zig", "gdscript", "matlab"]);
4486
4624
  var CASE_INSENSITIVE_LANGUAGES = /* @__PURE__ */ new Set(["apex"]);
@@ -4488,6 +4626,7 @@ var CALL_GRAPH_SYMBOL_CHUNK_TYPES = /* @__PURE__ */ new Set([
4488
4626
  "function_declaration",
4489
4627
  "function",
4490
4628
  "arrow_function",
4629
+ "export_statement",
4491
4630
  "method_definition",
4492
4631
  "class_declaration",
4493
4632
  "interface_declaration",
@@ -4526,7 +4665,7 @@ function float32ArrayToBuffer(arr) {
4526
4665
  function bufferToFloat32Array(buf) {
4527
4666
  return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
4528
4667
  }
4529
- function getErrorMessage(error) {
4668
+ function getErrorMessage2(error) {
4530
4669
  if (error instanceof Error) {
4531
4670
  return error.message;
4532
4671
  }
@@ -4539,7 +4678,7 @@ function getErrorMessage(error) {
4539
4678
  return String(error);
4540
4679
  }
4541
4680
  function isRateLimitError(error) {
4542
- const message = getErrorMessage(error);
4681
+ const message = getErrorMessage2(error);
4543
4682
  return message.includes("429") || message.toLowerCase().includes("rate limit") || message.toLowerCase().includes("too many requests");
4544
4683
  }
4545
4684
  function getSafeEmbeddingChunkTokenLimit(provider) {
@@ -4557,7 +4696,7 @@ function getDynamicBatchOptions(provider) {
4557
4696
  return {};
4558
4697
  }
4559
4698
  function isSqliteCorruptionError(error) {
4560
- const message = getErrorMessage(error).toLowerCase();
4699
+ const message = getErrorMessage2(error).toLowerCase();
4561
4700
  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");
4562
4701
  }
4563
4702
  var STARTUP_WARNING_METADATA_KEY = "index.startupWarning";
@@ -4719,9 +4858,9 @@ function hasAllEmbeddingParts(parts, expectedPartCount) {
4719
4858
  return true;
4720
4859
  }
4721
4860
  function isPathWithinRoot(filePath, rootPath) {
4722
- const normalizedFilePath = path9.resolve(filePath);
4723
- const normalizedRoot = path9.resolve(rootPath);
4724
- return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path9.sep}`);
4861
+ const normalizedFilePath = path10.resolve(filePath);
4862
+ const normalizedRoot = path10.resolve(rootPath);
4863
+ return normalizedFilePath === normalizedRoot || normalizedFilePath.startsWith(`${normalizedRoot}${path10.sep}`);
4725
4864
  }
4726
4865
  var rankingQueryTokenCache = /* @__PURE__ */ new Map();
4727
4866
  var rankingNameTokenCache = /* @__PURE__ */ new Map();
@@ -5774,9 +5913,9 @@ var Indexer = class {
5774
5913
  this.projectRoot = projectRoot;
5775
5914
  this.config = config;
5776
5915
  this.indexPath = this.getIndexPath();
5777
- this.fileHashCachePath = path9.join(this.indexPath, "file-hashes.json");
5778
- this.failedBatchesPath = path9.join(this.indexPath, "failed-batches.json");
5779
- this.indexingLockPath = path9.join(this.indexPath, "indexing.lock");
5916
+ this.fileHashCachePath = path10.join(this.indexPath, "file-hashes.json");
5917
+ this.failedBatchesPath = path10.join(this.indexPath, "failed-batches.json");
5918
+ this.indexingLockPath = path10.join(this.indexPath, "indexing.lock");
5780
5919
  this.logger = initializeLogger(config.debug);
5781
5920
  }
5782
5921
  getIndexPath() {
@@ -5808,14 +5947,14 @@ var Indexer = class {
5808
5947
  }
5809
5948
  atomicWriteSync(targetPath, data) {
5810
5949
  const tempPath = `${targetPath}.tmp`;
5811
- mkdirSync2(path9.dirname(targetPath), { recursive: true });
5950
+ mkdirSync2(path10.dirname(targetPath), { recursive: true });
5812
5951
  writeFileSync2(tempPath, data);
5813
5952
  renameSync(tempPath, targetPath);
5814
5953
  }
5815
5954
  getScopedRoots() {
5816
- const roots = /* @__PURE__ */ new Set([path9.resolve(this.projectRoot)]);
5955
+ const roots = /* @__PURE__ */ new Set([path10.resolve(this.projectRoot)]);
5817
5956
  for (const kbRoot of this.config.knowledgeBases) {
5818
- roots.add(path9.resolve(this.projectRoot, kbRoot));
5957
+ roots.add(path10.resolve(this.projectRoot, kbRoot));
5819
5958
  }
5820
5959
  return Array.from(roots);
5821
5960
  }
@@ -5824,22 +5963,29 @@ var Indexer = class {
5824
5963
  if (this.config.scope !== "global") {
5825
5964
  return branchName;
5826
5965
  }
5827
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5966
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5967
+ return `${projectHash}:${branchName}`;
5968
+ }
5969
+ getBranchCatalogKeyFor(branchName) {
5970
+ if (this.config.scope !== "global") {
5971
+ return branchName;
5972
+ }
5973
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5828
5974
  return `${projectHash}:${branchName}`;
5829
5975
  }
5830
5976
  getLegacyBranchCatalogKey() {
5831
5977
  return this.currentBranch || "default";
5832
5978
  }
5833
5979
  getLegacyMigrationMetadataKey() {
5834
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5980
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5835
5981
  return `index.globalBranchMigration.${projectHash}`;
5836
5982
  }
5837
5983
  getProjectEmbeddingStrategyMetadataKey() {
5838
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5984
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5839
5985
  return `index.embeddingStrategyVersion.${projectHash}`;
5840
5986
  }
5841
5987
  getProjectForceReembedMetadataKey() {
5842
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
5988
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5843
5989
  return `index.forceReembed.${projectHash}`;
5844
5990
  }
5845
5991
  hasProjectForceReembedPending() {
@@ -5933,7 +6079,7 @@ var Indexer = class {
5933
6079
  if (!this.database) {
5934
6080
  return { chunkIds, symbolIds };
5935
6081
  }
5936
- const projectRootPath = path9.resolve(this.projectRoot);
6082
+ const projectRootPath = path10.resolve(this.projectRoot);
5937
6083
  const projectLocalFilePaths = /* @__PURE__ */ new Set([
5938
6084
  ...Array.from(this.fileHashCache.keys()).filter(
5939
6085
  (filePath) => this.isFileInCurrentScope(filePath, roots) && isPathWithinRoot(filePath, projectRootPath)
@@ -5956,7 +6102,7 @@ var Indexer = class {
5956
6102
  if (this.config.scope !== "global") {
5957
6103
  return this.getBranchCatalogCleanupKeys();
5958
6104
  }
5959
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
6105
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
5960
6106
  const keys = /* @__PURE__ */ new Set();
5961
6107
  const projectChunkIdSet = new Set(projectChunkIds);
5962
6108
  const projectSymbolIdSet = new Set(projectSymbolIds);
@@ -6040,7 +6186,7 @@ var Indexer = class {
6040
6186
  if (!this.database || this.config.scope !== "global") {
6041
6187
  return false;
6042
6188
  }
6043
- const projectHash = hashContent(path9.resolve(this.projectRoot)).slice(0, 16);
6189
+ const projectHash = hashContent(path10.resolve(this.projectRoot)).slice(0, 16);
6044
6190
  const roots = this.getScopedRoots();
6045
6191
  const { chunkIds: projectLocalChunkIds, symbolIds: projectLocalSymbolIds } = this.getProjectLocalScopedOwnershipIds(roots);
6046
6192
  return this.database.getAllBranches().some(
@@ -6074,7 +6220,7 @@ var Indexer = class {
6074
6220
  ...Array.from(this.fileHashCache.keys()).filter((filePath) => this.isFileInCurrentScope(filePath, roots)),
6075
6221
  ...scopedEntries.map(({ metadata }) => metadata.filePath)
6076
6222
  ]);
6077
- const projectRootPath = path9.resolve(this.projectRoot);
6223
+ const projectRootPath = path10.resolve(this.projectRoot);
6078
6224
  const projectLocalFilePaths = new Set(
6079
6225
  Array.from(filePaths).filter((filePath) => isPathWithinRoot(filePath, projectRootPath))
6080
6226
  );
@@ -6327,7 +6473,7 @@ var Indexer = class {
6327
6473
  this.logger.search("warn", "External reranker failed; using deterministic order", {
6328
6474
  provider: reranker.provider,
6329
6475
  model: reranker.model,
6330
- error: getErrorMessage(error)
6476
+ error: getErrorMessage2(error)
6331
6477
  });
6332
6478
  return candidates;
6333
6479
  }
@@ -6434,13 +6580,13 @@ var Indexer = class {
6434
6580
  }
6435
6581
  await fsPromises2.mkdir(this.indexPath, { recursive: true });
6436
6582
  const dimensions = this.configuredProviderInfo.modelInfo.dimensions;
6437
- const storePath = path9.join(this.indexPath, "vectors");
6583
+ const storePath = path10.join(this.indexPath, "vectors");
6438
6584
  this.store = new VectorStore(storePath, dimensions);
6439
- const indexFilePath = path9.join(this.indexPath, "vectors.usearch");
6585
+ const indexFilePath = path10.join(this.indexPath, "vectors.usearch");
6440
6586
  if (existsSync6(indexFilePath)) {
6441
6587
  this.store.load();
6442
6588
  }
6443
- const invertedIndexPath = path9.join(this.indexPath, "inverted-index.json");
6589
+ const invertedIndexPath = path10.join(this.indexPath, "inverted-index.json");
6444
6590
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6445
6591
  try {
6446
6592
  this.invertedIndex.load();
@@ -6450,7 +6596,7 @@ var Indexer = class {
6450
6596
  }
6451
6597
  this.invertedIndex = new InvertedIndex(invertedIndexPath);
6452
6598
  }
6453
- const dbPath = path9.join(this.indexPath, "codebase.db");
6599
+ const dbPath = path10.join(this.indexPath, "codebase.db");
6454
6600
  let dbIsNew = !existsSync6(dbPath);
6455
6601
  try {
6456
6602
  this.database = new Database(dbPath);
@@ -6531,7 +6677,7 @@ var Indexer = class {
6531
6677
  if (await this.tryResetCorruptedIndex("running automatic orphan garbage collection", error)) {
6532
6678
  return {
6533
6679
  resetCorruptedIndex: true,
6534
- warning: this.getCorruptedIndexWarning(path9.join(this.indexPath, "codebase.db"))
6680
+ warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
6535
6681
  };
6536
6682
  }
6537
6683
  throw error;
@@ -6546,7 +6692,7 @@ var Indexer = class {
6546
6692
  return;
6547
6693
  }
6548
6694
  const retainedEntries = store.getAllMetadata().filter(({ key }) => !excludedSet.has(key));
6549
- const storeBasePath = path9.join(this.indexPath, "vectors");
6695
+ const storeBasePath = path10.join(this.indexPath, "vectors");
6550
6696
  const storeIndexPath = `${storeBasePath}.usearch`;
6551
6697
  const storeMetadataPath = `${storeBasePath}.meta.json`;
6552
6698
  const backupIndexPath = `${storeIndexPath}.bak`;
@@ -6631,9 +6777,9 @@ var Indexer = class {
6631
6777
  if (!isSqliteCorruptionError(error)) {
6632
6778
  return false;
6633
6779
  }
6634
- const dbPath = path9.join(this.indexPath, "codebase.db");
6780
+ const dbPath = path10.join(this.indexPath, "codebase.db");
6635
6781
  const warning = this.getCorruptedIndexWarning(dbPath);
6636
- const errorMessage = getErrorMessage(error);
6782
+ const errorMessage = getErrorMessage2(error);
6637
6783
  if (this.config.scope === "global") {
6638
6784
  this.logger.error("Detected corrupted shared global index database", {
6639
6785
  stage,
@@ -6654,15 +6800,15 @@ var Indexer = class {
6654
6800
  this.indexCompatibility = null;
6655
6801
  this.fileHashCache.clear();
6656
6802
  const resetPaths = [
6657
- path9.join(this.indexPath, "codebase.db"),
6658
- path9.join(this.indexPath, "codebase.db-shm"),
6659
- path9.join(this.indexPath, "codebase.db-wal"),
6660
- path9.join(this.indexPath, "vectors.usearch"),
6661
- path9.join(this.indexPath, "inverted-index.json"),
6662
- path9.join(this.indexPath, "file-hashes.json"),
6663
- path9.join(this.indexPath, "failed-batches.json"),
6664
- path9.join(this.indexPath, "indexing.lock"),
6665
- path9.join(this.indexPath, "vectors")
6803
+ path10.join(this.indexPath, "codebase.db"),
6804
+ path10.join(this.indexPath, "codebase.db-shm"),
6805
+ path10.join(this.indexPath, "codebase.db-wal"),
6806
+ path10.join(this.indexPath, "vectors.usearch"),
6807
+ path10.join(this.indexPath, "inverted-index.json"),
6808
+ path10.join(this.indexPath, "file-hashes.json"),
6809
+ path10.join(this.indexPath, "failed-batches.json"),
6810
+ path10.join(this.indexPath, "indexing.lock"),
6811
+ path10.join(this.indexPath, "vectors")
6666
6812
  ];
6667
6813
  await Promise.all(resetPaths.map(async (targetPath) => {
6668
6814
  try {
@@ -6927,7 +7073,7 @@ var Indexer = class {
6927
7073
  for (const parsed of parsedFiles) {
6928
7074
  currentFilePaths.add(parsed.path);
6929
7075
  if (parsed.chunks.length === 0) {
6930
- const relativePath = path9.relative(this.projectRoot, parsed.path);
7076
+ const relativePath = path10.relative(this.projectRoot, parsed.path);
6931
7077
  stats.parseFailures.push(relativePath);
6932
7078
  }
6933
7079
  let fileChunkCount = 0;
@@ -7211,7 +7357,7 @@ var Indexer = class {
7211
7357
  for (const requestBatch of requestBatches) {
7212
7358
  queue.add(async () => {
7213
7359
  if (rateLimitBackoffMs > 0) {
7214
- await new Promise((resolve10) => setTimeout(resolve10, rateLimitBackoffMs));
7360
+ await new Promise((resolve11) => setTimeout(resolve11, rateLimitBackoffMs));
7215
7361
  }
7216
7362
  try {
7217
7363
  const result = await pRetry(
@@ -7226,7 +7372,7 @@ var Indexer = class {
7226
7372
  factor: 2,
7227
7373
  shouldRetry: (error) => !(error.error instanceof CustomProviderNonRetryableError),
7228
7374
  onFailedAttempt: (error) => {
7229
- const message = getErrorMessage(error);
7375
+ const message = getErrorMessage2(error);
7230
7376
  if (isRateLimitError(error)) {
7231
7377
  rateLimitBackoffMs = Math.min(providerRateLimits.maxRetryMs, (rateLimitBackoffMs || providerRateLimits.minRetryMs) * 2);
7232
7378
  this.logger.embedding("warn", `Rate limited, backing off`, {
@@ -7333,7 +7479,7 @@ var Indexer = class {
7333
7479
  });
7334
7480
  } catch (error) {
7335
7481
  const failedChunks = getUniquePendingChunksFromRequests(requestBatch).filter((chunk) => !completedChunkIds.has(chunk.id));
7336
- const failureMessage = getErrorMessage(error);
7482
+ const failureMessage = getErrorMessage2(error);
7337
7483
  const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
7338
7484
  for (const chunk of failedChunks) {
7339
7485
  if (!failedChunkIds.has(chunk.id)) {
@@ -7772,8 +7918,8 @@ var Indexer = class {
7772
7918
  this.indexCompatibility = compatibility;
7773
7919
  return;
7774
7920
  }
7775
- const localProjectIndexPath = path9.join(this.projectRoot, ".opencode", "index");
7776
- if (path9.resolve(this.indexPath) !== path9.resolve(localProjectIndexPath)) {
7921
+ const localProjectIndexPath = path10.join(this.projectRoot, ".opencode", "index");
7922
+ if (path10.resolve(this.indexPath) !== path10.resolve(localProjectIndexPath)) {
7777
7923
  throw new Error(
7778
7924
  "Project-scoped force rebuild is unsafe while using an inherited worktree index. Create a local project config boundary before clearing the index."
7779
7925
  );
@@ -7861,7 +8007,7 @@ var Indexer = class {
7861
8007
  gcOrphanSymbols: 0,
7862
8008
  gcOrphanCallEdges: 0,
7863
8009
  resetCorruptedIndex: true,
7864
- warning: this.getCorruptedIndexWarning(path9.join(this.indexPath, "codebase.db"))
8010
+ warning: this.getCorruptedIndexWarning(path10.join(this.indexPath, "codebase.db"))
7865
8011
  };
7866
8012
  }
7867
8013
  this.logger.recordGc(removedCount, gcOrphanChunks, gcOrphanEmbeddings);
@@ -8012,7 +8158,7 @@ var Indexer = class {
8012
8158
  succeeded += successfulResults.length;
8013
8159
  stillFailing.push(...failedChunksForBatch.values());
8014
8160
  } catch (error) {
8015
- const failureMessage = getErrorMessage(error);
8161
+ const failureMessage = getErrorMessage2(error);
8016
8162
  const failureTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8017
8163
  const unaccountedChunks = batch.chunks.filter(
8018
8164
  (chunk) => !failedChunksForBatch.has(chunk.id) && !completedChunkIds.has(chunk.id)
@@ -8218,13 +8364,194 @@ var Indexer = class {
8218
8364
  const { database } = await this.ensureInitialized();
8219
8365
  let shortest = [];
8220
8366
  for (const branchKey of this.getBranchCatalogKeys()) {
8221
- const path18 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8222
- if (path18.length > 0 && (shortest.length === 0 || path18.length < shortest.length)) {
8223
- shortest = path18;
8367
+ const path19 = database.findShortestPath(fromName, toName, branchKey, maxDepth);
8368
+ if (path19.length > 0 && (shortest.length === 0 || path19.length < shortest.length)) {
8369
+ shortest = path19;
8224
8370
  }
8225
8371
  }
8226
8372
  return shortest;
8227
8373
  }
8374
+ async getSymbolsForBranch(branch) {
8375
+ const { database } = await this.ensureInitialized();
8376
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8377
+ return database.getSymbolsForBranch(resolvedBranch);
8378
+ }
8379
+ async getSymbolsForFiles(filePaths, branch) {
8380
+ const { database } = await this.ensureInitialized();
8381
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8382
+ return database.getSymbolsForFiles(filePaths, resolvedBranch);
8383
+ }
8384
+ async getTransitiveReachability(rootSymbolIds, direction, maxDepth) {
8385
+ const { database } = await this.ensureInitialized();
8386
+ const branch = this.getBranchCatalogKey();
8387
+ return database.getTransitiveReachability(rootSymbolIds, branch, direction, maxDepth);
8388
+ }
8389
+ async detectCommunities(branch, symbolIds) {
8390
+ const { database } = await this.ensureInitialized();
8391
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8392
+ return database.detectCommunities(resolvedBranch, symbolIds);
8393
+ }
8394
+ async computeCentrality(branch) {
8395
+ const { database } = await this.ensureInitialized();
8396
+ const resolvedBranch = branch ?? this.getBranchCatalogKey();
8397
+ return database.computeCentrality(resolvedBranch);
8398
+ }
8399
+ async getPrImpact(opts) {
8400
+ const { database } = await this.ensureInitialized();
8401
+ const execFileAsync2 = promisify2(execFile2);
8402
+ const changedFilesResult = await getChangedFiles({
8403
+ pr: opts.pr,
8404
+ branch: opts.branch,
8405
+ projectRoot: this.projectRoot,
8406
+ baseBranch: this.baseBranch
8407
+ });
8408
+ const changedFiles = changedFilesResult.files;
8409
+ const headRefName = changedFilesResult.headRefName;
8410
+ if (opts.pr !== void 0 && headRefName === void 0) {
8411
+ throw new Error(
8412
+ `Could not resolve head branch for PR #${opts.pr}. Run index_codebase on the PR branch first.`
8413
+ );
8414
+ }
8415
+ const resolvedBranch = opts.pr !== void 0 ? headRefName : opts.branch || this.currentBranch;
8416
+ const branchKey = this.getBranchCatalogKeyFor(resolvedBranch || "default");
8417
+ const branchSymbols = database.getSymbolsForBranch(branchKey);
8418
+ if (branchSymbols.length === 0) {
8419
+ throw new Error(
8420
+ "Run index_codebase first to build the call graph and symbol index for this branch."
8421
+ );
8422
+ }
8423
+ const absoluteChangedFiles = changedFiles.map((f) => path10.resolve(this.projectRoot, f));
8424
+ const directSymbols = database.getSymbolsForFiles(absoluteChangedFiles, branchKey);
8425
+ const directIds = directSymbols.map((s) => s.id);
8426
+ const direction = opts.direction ?? "both";
8427
+ const maxDepth = opts.maxDepth ?? 5;
8428
+ const transitiveCallers = database.getTransitiveReachability(
8429
+ directIds,
8430
+ branchKey,
8431
+ direction,
8432
+ maxDepth
8433
+ );
8434
+ const affectedIdsSet = new Set(directIds);
8435
+ for (const caller of transitiveCallers) {
8436
+ affectedIdsSet.add(caller.symbolId);
8437
+ }
8438
+ const allAffectedIds = Array.from(affectedIdsSet);
8439
+ const communitiesData = database.detectCommunities(branchKey, allAffectedIds);
8440
+ const communityMap = /* @__PURE__ */ new Map();
8441
+ for (const c of communitiesData) {
8442
+ if (!communityMap.has(c.communityLabel)) {
8443
+ communityMap.set(c.communityLabel, {
8444
+ label: c.communityLabel,
8445
+ symbolCount: 0,
8446
+ directSymbols: /* @__PURE__ */ new Set()
8447
+ });
8448
+ }
8449
+ const entry = communityMap.get(c.communityLabel);
8450
+ entry.symbolCount++;
8451
+ if (directIds.includes(c.symbolId)) {
8452
+ entry.directSymbols.add(c.symbolId);
8453
+ }
8454
+ }
8455
+ const communities = Array.from(communityMap.values()).map((c) => ({
8456
+ label: c.label,
8457
+ symbolCount: c.symbolCount,
8458
+ directSymbols: Array.from(c.directSymbols)
8459
+ }));
8460
+ const centralityData = database.computeCentrality(branchKey);
8461
+ const hubThreshold = opts.hubThreshold ?? 10;
8462
+ const hubNodes = centralityData.filter((c) => directIds.includes(c.symbolId) && c.callerCount >= hubThreshold).map((c) => ({
8463
+ id: c.symbolId,
8464
+ name: c.symbolName,
8465
+ callerCount: c.callerCount,
8466
+ filePath: c.filePath
8467
+ }));
8468
+ const totalAffected = allAffectedIds.length;
8469
+ let riskLevel;
8470
+ let riskReason;
8471
+ if (totalAffected < 5 && hubNodes.length === 0) {
8472
+ riskLevel = "LOW";
8473
+ riskReason = `Small impact: ${totalAffected} affected symbols, no hub nodes touched.`;
8474
+ } else if (totalAffected > 20 || hubNodes.length > 1) {
8475
+ riskLevel = "HIGH";
8476
+ riskReason = `Large impact: ${totalAffected} affected symbols${hubNodes.length > 0 ? `, ${hubNodes.length} hub nodes touched` : ""}.`;
8477
+ } else {
8478
+ riskLevel = "MEDIUM";
8479
+ riskReason = `Moderate impact: ${totalAffected} affected symbols${hubNodes.length === 1 ? ", 1 hub node touched" : ""}.`;
8480
+ }
8481
+ let conflictingPRs;
8482
+ if (opts.checkConflicts) {
8483
+ conflictingPRs = [];
8484
+ try {
8485
+ const { stdout } = await execFileAsync2(
8486
+ "gh",
8487
+ ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "10000"],
8488
+ { cwd: this.projectRoot, timeout: 3e4 }
8489
+ );
8490
+ const openPRs = JSON.parse(stdout);
8491
+ const currentCommunityLabels = new Set(communities.map((c) => c.label));
8492
+ const allCommunitiesData = database.detectCommunities(branchKey);
8493
+ const symbolToCommunity = /* @__PURE__ */ new Map();
8494
+ const structuralKey = (filePath, name) => `${filePath.toLowerCase()}:${name.toLowerCase()}`;
8495
+ for (const c of allCommunitiesData) {
8496
+ symbolToCommunity.set(structuralKey(c.filePath, c.symbolName), c.communityLabel);
8497
+ }
8498
+ for (const openPr of openPRs) {
8499
+ if (openPr.number === opts.pr) continue;
8500
+ try {
8501
+ const otherChanged = await getChangedFiles({
8502
+ pr: openPr.number,
8503
+ projectRoot: this.projectRoot,
8504
+ baseBranch: this.baseBranch
8505
+ });
8506
+ const otherAbsolute = otherChanged.files.map((f) => path10.resolve(this.projectRoot, f));
8507
+ const otherBranchKey = this.getBranchCatalogKeyFor(openPr.headRefName);
8508
+ const otherSymbols = database.getSymbolsForFiles(otherAbsolute, otherBranchKey);
8509
+ const otherLabels = /* @__PURE__ */ new Set();
8510
+ for (const sym of otherSymbols) {
8511
+ const label = symbolToCommunity.get(structuralKey(sym.filePath, sym.name));
8512
+ if (label) {
8513
+ otherLabels.add(label);
8514
+ }
8515
+ }
8516
+ const overlapping = Array.from(otherLabels).filter(
8517
+ (l) => currentCommunityLabels.has(l)
8518
+ );
8519
+ if (overlapping.length > 0) {
8520
+ conflictingPRs.push({
8521
+ pr: openPr.number,
8522
+ branch: openPr.headRefName,
8523
+ overlappingCommunities: overlapping
8524
+ });
8525
+ }
8526
+ } catch {
8527
+ }
8528
+ }
8529
+ } catch {
8530
+ }
8531
+ }
8532
+ return {
8533
+ changedFiles,
8534
+ directSymbols: directSymbols.map((s) => ({
8535
+ id: s.id,
8536
+ name: s.name,
8537
+ kind: s.kind,
8538
+ filePath: s.filePath
8539
+ })),
8540
+ transitiveCallers: transitiveCallers.map((c) => ({
8541
+ id: c.symbolId,
8542
+ name: c.symbolName,
8543
+ filePath: c.filePath,
8544
+ depth: c.depth
8545
+ })),
8546
+ totalAffected,
8547
+ communities,
8548
+ hubNodes,
8549
+ riskLevel,
8550
+ riskReason,
8551
+ direction,
8552
+ conflictingPRs
8553
+ };
8554
+ }
8228
8555
  async close() {
8229
8556
  await this.database?.close();
8230
8557
  this.database = null;
@@ -8484,13 +8811,13 @@ function computeEvalMetrics(queries, perQuery, embeddingCallCount, embeddingToke
8484
8811
  // src/eval/runner-config.ts
8485
8812
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync3 } from "fs";
8486
8813
  import * as os4 from "os";
8487
- import * as path11 from "path";
8814
+ import * as path12 from "path";
8488
8815
 
8489
8816
  // src/config/rebase.ts
8490
- import * as path10 from "path";
8817
+ import * as path11 from "path";
8491
8818
  function isWithinRoot(rootDir, targetPath) {
8492
- const relativePath = path10.relative(rootDir, targetPath);
8493
- return relativePath === "" || !relativePath.startsWith("..") && !path10.isAbsolute(relativePath);
8819
+ const relativePath = path11.relative(rootDir, targetPath);
8820
+ return relativePath === "" || !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
8494
8821
  }
8495
8822
  function rebasePathEntries(values, fromDir, toDir) {
8496
8823
  if (!Array.isArray(values)) {
@@ -8498,10 +8825,10 @@ function rebasePathEntries(values, fromDir, toDir) {
8498
8825
  }
8499
8826
  return values.filter((value) => typeof value === "string").map((value) => {
8500
8827
  const trimmed = value.trim();
8501
- if (!trimmed || path10.isAbsolute(trimmed)) {
8828
+ if (!trimmed || path11.isAbsolute(trimmed)) {
8502
8829
  return trimmed;
8503
8830
  }
8504
- return normalizePathSeparators(path10.normalize(path10.relative(toDir, path10.resolve(fromDir, trimmed))));
8831
+ return normalizePathSeparators(path11.normalize(path11.relative(toDir, path11.resolve(fromDir, trimmed))));
8505
8832
  }).filter(Boolean);
8506
8833
  }
8507
8834
  function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
@@ -8513,17 +8840,17 @@ function resolveInheritedKnowledgeBaseEntries(values, sourceRoot, targetRoot) {
8513
8840
  if (!trimmed) {
8514
8841
  return trimmed;
8515
8842
  }
8516
- if (path10.isAbsolute(trimmed)) {
8843
+ if (path11.isAbsolute(trimmed)) {
8517
8844
  if (isWithinRoot(sourceRoot, trimmed)) {
8518
- return normalizePathSeparators(path10.normalize(path10.relative(sourceRoot, trimmed) || "."));
8845
+ return normalizePathSeparators(path11.normalize(path11.relative(sourceRoot, trimmed) || "."));
8519
8846
  }
8520
- return path10.normalize(trimmed);
8847
+ return path11.normalize(trimmed);
8521
8848
  }
8522
- const resolvedFromSource = path10.resolve(sourceRoot, trimmed);
8849
+ const resolvedFromSource = path11.resolve(sourceRoot, trimmed);
8523
8850
  if (isWithinRoot(sourceRoot, resolvedFromSource)) {
8524
- return normalizePathSeparators(path10.normalize(trimmed));
8851
+ return normalizePathSeparators(path11.normalize(trimmed));
8525
8852
  }
8526
- return normalizePathSeparators(path10.normalize(path10.relative(targetRoot, resolvedFromSource)));
8853
+ return normalizePathSeparators(path11.normalize(path11.relative(targetRoot, resolvedFromSource)));
8527
8854
  }).filter(Boolean);
8528
8855
  }
8529
8856
 
@@ -8571,20 +8898,20 @@ function parseJsonConfigFile(filePath) {
8571
8898
  }
8572
8899
  }
8573
8900
  function toAbsolute(projectRoot, maybeRelative) {
8574
- return path11.isAbsolute(maybeRelative) ? maybeRelative : path11.join(projectRoot, maybeRelative);
8901
+ return path12.isAbsolute(maybeRelative) ? maybeRelative : path12.join(projectRoot, maybeRelative);
8575
8902
  }
8576
8903
  function isProjectScopedConfigPath(configPath) {
8577
- return path11.basename(configPath) === "codebase-index.json" && path11.basename(path11.dirname(configPath)) === ".opencode";
8904
+ return path12.basename(configPath) === "codebase-index.json" && path12.basename(path12.dirname(configPath)) === ".opencode";
8578
8905
  }
8579
8906
  function normalizeEvalConfigKnowledgeBases(rawConfig, projectRoot, resolvedConfigPath) {
8580
8907
  const config = rawConfig && typeof rawConfig === "object" ? { ...rawConfig } : {};
8581
8908
  const rebaseEntries = (values) => isProjectScopedConfigPath(resolvedConfigPath) ? resolveInheritedKnowledgeBaseEntries(
8582
8909
  values,
8583
- path11.dirname(path11.dirname(resolvedConfigPath)),
8910
+ path12.dirname(path12.dirname(resolvedConfigPath)),
8584
8911
  projectRoot
8585
8912
  ) : rebasePathEntries(
8586
8913
  values,
8587
- path11.dirname(resolvedConfigPath),
8914
+ path12.dirname(resolvedConfigPath),
8588
8915
  projectRoot
8589
8916
  );
8590
8917
  if (Array.isArray(config.knowledgeBases)) {
@@ -8612,7 +8939,7 @@ function loadRawConfig(projectRoot, configPath) {
8612
8939
  projectConfig
8613
8940
  );
8614
8941
  }
8615
- const globalConfig = path11.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8942
+ const globalConfig = path12.join(os4.homedir(), ".config", "opencode", "codebase-index.json");
8616
8943
  if (existsSync7(globalConfig)) {
8617
8944
  return parseJsonConfigFile(globalConfig);
8618
8945
  }
@@ -8622,10 +8949,10 @@ function getIndexRootPath(projectRoot, scope) {
8622
8949
  return scope === "global" ? getGlobalIndexPath() : resolveProjectIndexPath(projectRoot, scope);
8623
8950
  }
8624
8951
  function getLocalProjectIndexRoot(projectRoot) {
8625
- return path11.join(projectRoot, ".opencode", "index");
8952
+ return path12.join(projectRoot, ".opencode", "index");
8626
8953
  }
8627
8954
  function getLocalProjectConfigPath(projectRoot) {
8628
- return path11.join(projectRoot, ".opencode", "codebase-index.json");
8955
+ return path12.join(projectRoot, ".opencode", "codebase-index.json");
8629
8956
  }
8630
8957
  function clearIndexRoot(projectRoot, scope) {
8631
8958
  const indexRoot = scope === "global" ? getIndexRootPath(projectRoot, scope) : getLocalProjectIndexRoot(projectRoot);
@@ -8647,7 +8974,7 @@ function ensureLocalEvalProjectConfig(projectRoot, configPath) {
8647
8974
  projectRoot,
8648
8975
  resolvedConfigPath
8649
8976
  );
8650
- mkdirSync3(path11.dirname(localConfigPath), { recursive: true });
8977
+ mkdirSync3(path12.dirname(localConfigPath), { recursive: true });
8651
8978
  writeFileSync3(localConfigPath, JSON.stringify(sourceConfig, null, 2), "utf-8");
8652
8979
  return localConfigPath;
8653
8980
  }
@@ -8697,23 +9024,23 @@ function isRecord2(value) {
8697
9024
  function isStringArray3(value) {
8698
9025
  return Array.isArray(value) && value.every((item) => typeof item === "string");
8699
9026
  }
8700
- function asPositiveNumber(value, path18) {
9027
+ function asPositiveNumber(value, path19) {
8701
9028
  if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
8702
- throw new Error(`${path18} must be a non-negative number`);
9029
+ throw new Error(`${path19} must be a non-negative number`);
8703
9030
  }
8704
9031
  return value;
8705
9032
  }
8706
- function parseQueryType(value, path18) {
9033
+ function parseQueryType(value, path19) {
8707
9034
  if (value === "definition" || value === "implementation-intent" || value === "similarity" || value === "keyword-heavy") {
8708
9035
  return value;
8709
9036
  }
8710
9037
  throw new Error(
8711
- `${path18} must be one of: definition, implementation-intent, similarity, keyword-heavy`
9038
+ `${path19} must be one of: definition, implementation-intent, similarity, keyword-heavy`
8712
9039
  );
8713
9040
  }
8714
- function parseExpected(input, path18) {
9041
+ function parseExpected(input, path19) {
8715
9042
  if (!isRecord2(input)) {
8716
- throw new Error(`${path18} must be an object`);
9043
+ throw new Error(`${path19} must be an object`);
8717
9044
  }
8718
9045
  const filePathRaw = input.filePath;
8719
9046
  const acceptableFilesRaw = input.acceptableFiles;
@@ -8722,16 +9049,16 @@ function parseExpected(input, path18) {
8722
9049
  const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
8723
9050
  const acceptableFiles = isStringArray3(acceptableFilesRaw) ? acceptableFilesRaw : void 0;
8724
9051
  if (!filePath && (!acceptableFiles || acceptableFiles.length === 0)) {
8725
- throw new Error(`${path18} must include either expected.filePath or expected.acceptableFiles`);
9052
+ throw new Error(`${path19} must include either expected.filePath or expected.acceptableFiles`);
8726
9053
  }
8727
9054
  if (acceptableFilesRaw !== void 0 && !isStringArray3(acceptableFilesRaw)) {
8728
- throw new Error(`${path18}.acceptableFiles must be an array of strings`);
9055
+ throw new Error(`${path19}.acceptableFiles must be an array of strings`);
8729
9056
  }
8730
9057
  if (symbolRaw !== void 0 && typeof symbolRaw !== "string") {
8731
- throw new Error(`${path18}.symbol must be a string when provided`);
9058
+ throw new Error(`${path19}.symbol must be a string when provided`);
8732
9059
  }
8733
9060
  if (branchRaw !== void 0 && typeof branchRaw !== "string") {
8734
- throw new Error(`${path18}.branch must be a string when provided`);
9061
+ throw new Error(`${path19}.branch must be a string when provided`);
8735
9062
  }
8736
9063
  return {
8737
9064
  filePath,
@@ -8741,25 +9068,25 @@ function parseExpected(input, path18) {
8741
9068
  };
8742
9069
  }
8743
9070
  function parseQuery(input, index) {
8744
- const path18 = `queries[${index}]`;
9071
+ const path19 = `queries[${index}]`;
8745
9072
  if (!isRecord2(input)) {
8746
- throw new Error(`${path18} must be an object`);
9073
+ throw new Error(`${path19} must be an object`);
8747
9074
  }
8748
9075
  const id = input.id;
8749
9076
  const query = input.query;
8750
9077
  const queryType = input.queryType;
8751
9078
  const expected = input.expected;
8752
9079
  if (typeof id !== "string" || id.trim().length === 0) {
8753
- throw new Error(`${path18}.id must be a non-empty string`);
9080
+ throw new Error(`${path19}.id must be a non-empty string`);
8754
9081
  }
8755
9082
  if (typeof query !== "string" || query.trim().length === 0) {
8756
- throw new Error(`${path18}.query must be a non-empty string`);
9083
+ throw new Error(`${path19}.query must be a non-empty string`);
8757
9084
  }
8758
9085
  return {
8759
9086
  id,
8760
9087
  query,
8761
- queryType: parseQueryType(queryType, `${path18}.queryType`),
8762
- expected: parseExpected(expected, `${path18}.expected`)
9088
+ queryType: parseQueryType(queryType, `${path19}.queryType`),
9089
+ expected: parseExpected(expected, `${path19}.expected`)
8763
9090
  };
8764
9091
  }
8765
9092
  function parseGoldenDataset(raw, sourceLabel) {
@@ -8942,13 +9269,13 @@ async function runEvaluation(options) {
8942
9269
  };
8943
9270
  const outputDir = createRunDirectory(toAbsolute(options.projectRoot, options.outputRoot));
8944
9271
  const perQueryArtifact = buildPerQueryArtifact(perQuery);
8945
- writeJson(path12.join(outputDir, "summary.json"), summary);
8946
- writeJson(path12.join(outputDir, "per-query.json"), perQueryArtifact);
9272
+ writeJson(path13.join(outputDir, "summary.json"), summary);
9273
+ writeJson(path13.join(outputDir, "per-query.json"), perQueryArtifact);
8947
9274
  let comparison;
8948
9275
  if (againstPath) {
8949
9276
  const baseline = loadSummary(againstPath);
8950
9277
  comparison = compareSummaries(summary, baseline, againstPath);
8951
- writeJson(path12.join(outputDir, "compare.json"), comparison);
9278
+ writeJson(path13.join(outputDir, "compare.json"), comparison);
8952
9279
  }
8953
9280
  let gate;
8954
9281
  if (options.ciMode) {
@@ -8961,7 +9288,7 @@ async function runEvaluation(options) {
8961
9288
  if (existsSync8(resolvedBaseline)) {
8962
9289
  const baselineSummary = loadSummary(resolvedBaseline);
8963
9290
  comparison = compareSummaries(summary, baselineSummary, resolvedBaseline);
8964
- writeJson(path12.join(outputDir, "compare.json"), comparison);
9291
+ writeJson(path13.join(outputDir, "compare.json"), comparison);
8965
9292
  } else if (budget.failOnMissingBaseline) {
8966
9293
  throw new Error(
8967
9294
  `Budget baseline is missing: ${resolvedBaseline}. Set failOnMissingBaseline=false to allow CI run without baseline.`
@@ -8971,7 +9298,7 @@ async function runEvaluation(options) {
8971
9298
  gate = evaluateBudgetGate(budget, summary, comparison);
8972
9299
  }
8973
9300
  const markdown = createSummaryMarkdown(summary, comparison, gate);
8974
- writeText(path12.join(outputDir, "summary.md"), markdown);
9301
+ writeText(path13.join(outputDir, "summary.md"), markdown);
8975
9302
  return { outputDir, summary, perQuery, comparison, gate };
8976
9303
  } finally {
8977
9304
  await indexer.close();
@@ -9029,23 +9356,23 @@ async function runSweep(options, sweep) {
9029
9356
  bestByMrrAt10,
9030
9357
  bestByP95Latency
9031
9358
  };
9032
- writeJson(path12.join(outputDir, "compare.json"), aggregate);
9359
+ writeJson(path13.join(outputDir, "compare.json"), aggregate);
9033
9360
  const md = createSummaryMarkdown(
9034
9361
  bestByHitAt5?.summary ?? runs[0].summary,
9035
9362
  bestByHitAt5?.comparison,
9036
9363
  void 0,
9037
9364
  aggregate
9038
9365
  );
9039
- writeText(path12.join(outputDir, "summary.md"), md);
9040
- writeJson(path12.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9366
+ writeText(path13.join(outputDir, "summary.md"), md);
9367
+ writeJson(path13.join(outputDir, "summary.json"), bestByHitAt5?.summary ?? runs[0].summary);
9041
9368
  return { outputDir, aggregate };
9042
9369
  }
9043
9370
 
9044
9371
  // src/eval/cli.ts
9045
- import * as path14 from "path";
9372
+ import * as path15 from "path";
9046
9373
 
9047
9374
  // src/eval/cli-parser.ts
9048
- import * as path13 from "path";
9375
+ import * as path14 from "path";
9049
9376
  function printUsage() {
9050
9377
  console.log(`
9051
9378
  Usage:
@@ -9117,12 +9444,12 @@ function parseEvalArgs(argv, cwd) {
9117
9444
  const arg = argv[i];
9118
9445
  const next = argv[i + 1];
9119
9446
  if (arg === "--project" && next) {
9120
- parsed.projectRoot = path13.resolve(cwd, next);
9447
+ parsed.projectRoot = path14.resolve(cwd, next);
9121
9448
  i += 1;
9122
9449
  continue;
9123
9450
  }
9124
9451
  if (arg === "--config" && next) {
9125
- parsed.configPath = path13.resolve(cwd, next);
9452
+ parsed.configPath = path14.resolve(cwd, next);
9126
9453
  i += 1;
9127
9454
  continue;
9128
9455
  }
@@ -9318,22 +9645,22 @@ async function handleEvalCommand(args, cwd) {
9318
9645
  if (!parsed.againstPath.endsWith(".json")) {
9319
9646
  throw new Error("eval diff --against must point to a summary JSON file");
9320
9647
  }
9321
- const currentSummary = loadSummary(path14.resolve(parsed.projectRoot, currentPath), {
9648
+ const currentSummary = loadSummary(path15.resolve(parsed.projectRoot, currentPath), {
9322
9649
  allowLegacyDiversityMetrics: true
9323
9650
  });
9324
- const baselineSummary = loadSummary(path14.resolve(parsed.projectRoot, parsed.againstPath), {
9651
+ const baselineSummary = loadSummary(path15.resolve(parsed.projectRoot, parsed.againstPath), {
9325
9652
  allowLegacyDiversityMetrics: true
9326
9653
  });
9327
9654
  const comparison = compareSummaries(
9328
9655
  currentSummary,
9329
9656
  baselineSummary,
9330
- path14.resolve(parsed.projectRoot, parsed.againstPath)
9657
+ path15.resolve(parsed.projectRoot, parsed.againstPath)
9331
9658
  );
9332
- const outputDir = createRunDirectory(path14.resolve(parsed.projectRoot, parsed.outputRoot));
9659
+ const outputDir = createRunDirectory(path15.resolve(parsed.projectRoot, parsed.outputRoot));
9333
9660
  const summaryMd = createSummaryMarkdown(currentSummary, comparison);
9334
- writeJson(path14.join(outputDir, "compare.json"), comparison);
9335
- writeText(path14.join(outputDir, "summary.md"), summaryMd);
9336
- writeJson(path14.join(outputDir, "summary.json"), currentSummary);
9661
+ writeJson(path15.join(outputDir, "compare.json"), comparison);
9662
+ writeText(path15.join(outputDir, "summary.md"), summaryMd);
9663
+ writeJson(path15.join(outputDir, "summary.json"), currentSummary);
9337
9664
  console.log(`Eval diff complete. Artifacts: ${outputDir}`);
9338
9665
  return 0;
9339
9666
  }
@@ -9342,7 +9669,7 @@ async function handleEvalCommand(args, cwd) {
9342
9669
 
9343
9670
  // src/mcp-server.ts
9344
9671
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9345
- import * as path16 from "path";
9672
+ import * as path17 from "path";
9346
9673
  import { existsSync as existsSync10 } from "fs";
9347
9674
 
9348
9675
  // src/mcp-server/register-prompts.ts
@@ -9441,7 +9768,7 @@ import { z as z2 } from "zod";
9441
9768
  // src/config/merger.ts
9442
9769
  import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
9443
9770
  import * as os5 from "os";
9444
- import * as path15 from "path";
9771
+ import * as path16 from "path";
9445
9772
  var PROJECT_OVERRIDE_KEYS = [
9446
9773
  "embeddingProvider",
9447
9774
  "customProvider",
@@ -9474,8 +9801,8 @@ function mergeUniqueStringArray(values) {
9474
9801
  return [...new Set(values.map((value) => String(value).trim()))];
9475
9802
  }
9476
9803
  function normalizeKnowledgeBasePath(value) {
9477
- let normalized = path15.normalize(String(value).trim());
9478
- const root = path15.parse(normalized).root;
9804
+ let normalized = path16.normalize(String(value).trim());
9805
+ const root = path16.parse(normalized).root;
9479
9806
  while (normalized.length > root.length && /[\\/]$/.test(normalized)) {
9480
9807
  normalized = normalized.slice(0, -1);
9481
9808
  }
@@ -9525,8 +9852,8 @@ function loadJsonFile(filePath) {
9525
9852
  return null;
9526
9853
  }
9527
9854
  function materializeLocalProjectConfig(projectRoot, config) {
9528
- const localConfigPath = path15.join(projectRoot, ".opencode", "codebase-index.json");
9529
- mkdirSync4(path15.dirname(localConfigPath), { recursive: true });
9855
+ const localConfigPath = path16.join(projectRoot, ".opencode", "codebase-index.json");
9856
+ mkdirSync4(path16.dirname(localConfigPath), { recursive: true });
9530
9857
  writeFileSync4(localConfigPath, JSON.stringify(config, null, 2), "utf-8");
9531
9858
  return localConfigPath;
9532
9859
  }
@@ -9537,7 +9864,7 @@ function loadProjectConfigLayer(projectRoot) {
9537
9864
  return {};
9538
9865
  }
9539
9866
  const normalizedConfig = { ...projectConfig };
9540
- const projectConfigBaseDir = path15.dirname(path15.dirname(projectConfigPath));
9867
+ const projectConfigBaseDir = path16.dirname(path16.dirname(projectConfigPath));
9541
9868
  if (Array.isArray(normalizedConfig.knowledgeBases)) {
9542
9869
  normalizedConfig.knowledgeBases = resolveInheritedKnowledgeBaseEntries(
9543
9870
  normalizedConfig.knowledgeBases,
@@ -9756,6 +10083,57 @@ ${truncateContent(r.content)}
9756
10083
  return formatted.join("\n\n");
9757
10084
  }
9758
10085
 
10086
+ // src/tools/format-pr-impact.ts
10087
+ function formatPrImpact(result) {
10088
+ const lines = [];
10089
+ lines.push(`\u2192 Files changed: ${result.changedFiles.length}`);
10090
+ for (const file of result.changedFiles) {
10091
+ lines.push(` - ${file}`);
10092
+ }
10093
+ const directCount = result.directSymbols.length;
10094
+ const transitiveCount = result.transitiveCallers.length;
10095
+ const directionLabel = result.direction === "callees" ? "callees" : result.direction === "both" ? "reachable (callers + callees)" : "callers";
10096
+ lines.push(
10097
+ `\u2192 Symbols affected: ${result.totalAffected} (${directCount} direct, ${transitiveCount} transitive ${directionLabel})`
10098
+ );
10099
+ if (directCount > 0) {
10100
+ lines.push(
10101
+ ` Direct: ${result.directSymbols.map((s) => `${s.name} (${s.kind})`).join(", ")}`
10102
+ );
10103
+ }
10104
+ if (transitiveCount > 0) {
10105
+ lines.push(
10106
+ ` Transitive ${directionLabel}: ${result.transitiveCallers.map((s) => s.name).join(", ")}`
10107
+ );
10108
+ }
10109
+ if (result.communities.length > 0) {
10110
+ lines.push(
10111
+ `\u2192 Communities touched: ${result.communities.map((c) => c.label).join(", ")}`
10112
+ );
10113
+ for (const community of result.communities) {
10114
+ lines.push(
10115
+ ` - ${community.label}: ${community.symbolCount} symbols`
10116
+ );
10117
+ }
10118
+ } else {
10119
+ lines.push("\u2192 Communities touched: none");
10120
+ }
10121
+ lines.push(`\u2192 Risk: ${result.riskLevel} \u2014 ${result.riskReason}`);
10122
+ if (result.hubNodes.length > 0) {
10123
+ lines.push(" Hub nodes in change scope:");
10124
+ for (const hub of result.hubNodes) {
10125
+ lines.push(` - ${hub.name} (${hub.callerCount} callers) at ${hub.filePath}`);
10126
+ }
10127
+ }
10128
+ if (result.conflictingPRs && result.conflictingPRs.length > 0) {
10129
+ const conflictList = result.conflictingPRs.map(
10130
+ (c) => `PR #${c.pr} (also touches ${c.overlappingCommunities.join(", ")} community)`
10131
+ );
10132
+ lines.push(`\u2192 Potential conflicts with: ${conflictList.join(", ")}`);
10133
+ }
10134
+ return lines.join("\n");
10135
+ }
10136
+
9759
10137
  // src/mcp-server/shared.ts
9760
10138
  var MAX_CONTENT_LINES2 = 30;
9761
10139
  function truncateContent2(content) {
@@ -10050,19 +10428,49 @@ ${formatted.join("\n")}` }] };
10050
10428
  async (args) => {
10051
10429
  await runtime.ensureInitialized();
10052
10430
  const indexer = runtime.getIndexer();
10053
- const path18 = await indexer.findCallPath(args.from, args.to, args.maxDepth);
10054
- if (path18.length === 0) {
10431
+ const path19 = await indexer.findCallPath(args.from, args.to, args.maxDepth);
10432
+ if (path19.length === 0) {
10055
10433
  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.` }] };
10056
10434
  }
10057
- const formatted = path18.map((hop, i) => {
10435
+ const formatted = path19.map((hop, i) => {
10058
10436
  const prefix = i === 0 ? "[start]" : `--${hop.callType}-->`;
10059
10437
  const location = hop.filePath ? ` (${hop.filePath}:${hop.line})` : "";
10060
10438
  return `${prefix} ${hop.symbolName}${location}`;
10061
10439
  });
10062
- return { content: [{ type: "text", text: `Path (${path18.length} hops):
10440
+ return { content: [{ type: "text", text: `Path (${path19.length} hops):
10063
10441
  ${formatted.join("\n")}` }] };
10064
10442
  }
10065
10443
  );
10444
+ server.tool(
10445
+ "pr_impact",
10446
+ "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.",
10447
+ {
10448
+ pr: z2.number().optional().describe("Pull request number to analyze"),
10449
+ branch: z2.string().optional().describe("Branch name to analyze (defaults to current branch)"),
10450
+ maxDepth: z2.number().optional().default(5).describe("Maximum traversal depth for transitive callers (default: 5)"),
10451
+ hubThreshold: z2.number().optional().default(10).describe("Minimum caller count to flag a symbol as a hub node (default: 10)"),
10452
+ checkConflicts: z2.boolean().optional().default(false).describe("Check for conflicting open PRs touching the same communities (default: false)"),
10453
+ direction: z2.enum(["callers", "callees", "both"]).optional().default("both").describe("Call-graph traversal direction: 'callers' for upstream, 'callees' for downstream, 'both' for union (default: both)")
10454
+ },
10455
+ async (args) => {
10456
+ await runtime.ensureInitialized();
10457
+ const indexer = runtime.getIndexer();
10458
+ try {
10459
+ const result = await indexer.getPrImpact({
10460
+ pr: args.pr,
10461
+ branch: args.branch,
10462
+ maxDepth: args.maxDepth,
10463
+ hubThreshold: args.hubThreshold,
10464
+ checkConflicts: args.checkConflicts,
10465
+ direction: args.direction
10466
+ });
10467
+ return { content: [{ type: "text", text: formatPrImpact(result) }] };
10468
+ } catch (error) {
10469
+ const message = error instanceof Error ? error.message : String(error);
10470
+ return { content: [{ type: "text", text: `Error analyzing PR impact: ${message}` }] };
10471
+ }
10472
+ }
10473
+ );
10066
10474
  }
10067
10475
 
10068
10476
  // src/mcp-server.ts
@@ -10081,12 +10489,12 @@ function createMcpServer(projectRoot, config) {
10081
10489
  if (config.scope !== "project") {
10082
10490
  return false;
10083
10491
  }
10084
- const localIndexPath = path16.join(projectRoot, ".opencode", "index");
10492
+ const localIndexPath = path17.join(projectRoot, ".opencode", "index");
10085
10493
  const mainRepoRoot = resolveWorktreeMainRepoRoot(projectRoot);
10086
10494
  if (!mainRepoRoot) {
10087
10495
  return false;
10088
10496
  }
10089
- const inheritedIndexPath = path16.join(mainRepoRoot, ".opencode", "index");
10497
+ const inheritedIndexPath = path17.join(mainRepoRoot, ".opencode", "index");
10090
10498
  return !existsSync10(localIndexPath) && existsSync10(inheritedIndexPath);
10091
10499
  }
10092
10500
  async function ensureInitialized() {
@@ -10112,9 +10520,9 @@ function parseArgs(argv) {
10112
10520
  let config;
10113
10521
  for (let i = 2; i < argv.length; i++) {
10114
10522
  if (argv[i] === "--project" && argv[i + 1]) {
10115
- project = path17.resolve(argv[++i]);
10523
+ project = path18.resolve(argv[++i]);
10116
10524
  } else if (argv[i] === "--config" && argv[i + 1]) {
10117
- config = path17.resolve(argv[++i]);
10525
+ config = path18.resolve(argv[++i]);
10118
10526
  }
10119
10527
  }
10120
10528
  return { project, config };