@vohongtho.infotech/code-intel 0.5.0 → 0.7.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'module';
2
2
  import { fileURLToPath } from 'url';
3
- import path27 from 'path';
4
- import fs19, { existsSync } from 'fs';
3
+ import path29 from 'path';
4
+ import fs23, { existsSync } from 'fs';
5
5
  import { Parser, Language, Query } from 'web-tree-sitter';
6
6
  import { NodeSDK } from '@opentelemetry/sdk-node';
7
7
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
@@ -126,11 +126,11 @@ var init_shared = __esm({
126
126
  }
127
127
  });
128
128
  function findBundledWasmDir() {
129
- const fileDir = path27.dirname(fileURLToPath(import.meta.url));
129
+ const fileDir = path29.dirname(fileURLToPath(import.meta.url));
130
130
  const candidates = [
131
- path27.join(fileDir, "wasm"),
131
+ path29.join(fileDir, "wasm"),
132
132
  // dist/index.js → dist/wasm/
133
- path27.join(fileDir, "../wasm")
133
+ path29.join(fileDir, "../wasm")
134
134
  // dist/cli/main.js → dist/wasm/
135
135
  ];
136
136
  for (const candidate of candidates) {
@@ -171,7 +171,7 @@ function wasmPath(lang) {
171
171
  }
172
172
  const bundled = BUNDLED_WASM_MAP[lang];
173
173
  if (bundled) {
174
- const bundledPath = path27.join(_bundledWasmDir, bundled);
174
+ const bundledPath = path29.join(_bundledWasmDir, bundled);
175
175
  if (existsSync(bundledPath)) return bundledPath;
176
176
  }
177
177
  return null;
@@ -184,14 +184,14 @@ async function initParser() {
184
184
  }
185
185
  async function getLanguage(lang) {
186
186
  if (languageCache.has(lang)) return languageCache.get(lang);
187
- const path28 = wasmPath(lang);
188
- if (!path28) {
187
+ const path30 = wasmPath(lang);
188
+ if (!path30) {
189
189
  languageCache.set(lang, null);
190
190
  return null;
191
191
  }
192
192
  try {
193
193
  await initParser();
194
- const language = await Language.load(path28);
194
+ const language = await Language.load(path30);
195
195
  languageCache.set(lang, language);
196
196
  return language;
197
197
  } catch {
@@ -1162,7 +1162,7 @@ var init_logger = __esm({
1162
1162
  };
1163
1163
  }
1164
1164
  /** Global log directory: ~/.code-intel/logs */
1165
- static LOG_DIR = path27.join(os12.homedir(), ".code-intel", "logs");
1165
+ static LOG_DIR = path29.join(os12.homedir(), ".code-intel", "logs");
1166
1166
  static getLogger() {
1167
1167
  if (!_Logger.instance) {
1168
1168
  const isProduction = process.env.NODE_ENV === "production";
@@ -1171,12 +1171,12 @@ var init_logger = __esm({
1171
1171
  transports.push(new winston.transports.Console());
1172
1172
  if (!isProduction) {
1173
1173
  try {
1174
- if (!fs19.existsSync(_Logger.LOG_DIR)) {
1175
- fs19.mkdirSync(_Logger.LOG_DIR, { recursive: true });
1174
+ if (!fs23.existsSync(_Logger.LOG_DIR)) {
1175
+ fs23.mkdirSync(_Logger.LOG_DIR, { recursive: true });
1176
1176
  }
1177
1177
  transports.push(
1178
1178
  new DailyRotateFile({
1179
- filename: path27.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
1179
+ filename: path29.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
1180
1180
  datePattern: "YYYY-MM-DD",
1181
1181
  maxSize: "20m",
1182
1182
  maxFiles: "14d"
@@ -1961,7 +1961,7 @@ var init_parse_phase = __esm({
1961
1961
  const batch = filePaths.slice(i, i + CONCURRENCY);
1962
1962
  await Promise.all(batch.map(async (filePath) => {
1963
1963
  try {
1964
- const source = await fs19.promises.readFile(filePath, "utf-8");
1964
+ const source = await fs23.promises.readFile(filePath, "utf-8");
1965
1965
  context2.fileCache.set(filePath, source);
1966
1966
  } catch {
1967
1967
  }
@@ -1974,14 +1974,14 @@ var init_parse_phase = __esm({
1974
1974
  const lang = detectLanguage(filePath);
1975
1975
  if (!lang) {
1976
1976
  if (context2.verbose) {
1977
- const relativePath2 = path27.relative(context2.workspaceRoot, filePath);
1977
+ const relativePath2 = path29.relative(context2.workspaceRoot, filePath);
1978
1978
  logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
1979
1979
  }
1980
1980
  continue;
1981
1981
  }
1982
1982
  const source = context2.fileCache.get(filePath);
1983
1983
  if (!source) continue;
1984
- const relativePath = path27.relative(context2.workspaceRoot, filePath);
1984
+ const relativePath = path29.relative(context2.workspaceRoot, filePath);
1985
1985
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
1986
1986
  const fileNode = context2.graph.getNode(fileNodeId);
1987
1987
  if (fileNode) {
@@ -2221,11 +2221,11 @@ var init_resolve_phase = __esm({
2221
2221
  let heritageEdges = 0;
2222
2222
  const fileIndex = /* @__PURE__ */ new Map();
2223
2223
  for (const fp of filePaths) {
2224
- const rel = path27.relative(workspaceRoot, fp);
2224
+ const rel = path29.relative(workspaceRoot, fp);
2225
2225
  fileIndex.set(rel, fp);
2226
2226
  const noExt = rel.replace(/\.\w+$/, "");
2227
2227
  if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
2228
- const base = path27.basename(rel, path27.extname(rel));
2228
+ const base = path29.basename(rel, path29.extname(rel));
2229
2229
  if (!fileIndex.has(base)) fileIndex.set(base, fp);
2230
2230
  }
2231
2231
  const symbolIndex = /* @__PURE__ */ new Map();
@@ -2256,7 +2256,7 @@ var init_resolve_phase = __esm({
2256
2256
  for (const filePath of filePaths) {
2257
2257
  const lang = detectLanguage(filePath);
2258
2258
  if (!lang) continue;
2259
- const relativePath = path27.relative(workspaceRoot, filePath);
2259
+ const relativePath = path29.relative(workspaceRoot, filePath);
2260
2260
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2261
2261
  const source = fileCache.get(filePath);
2262
2262
  if (!source) continue;
@@ -2269,13 +2269,13 @@ var init_resolve_phase = __esm({
2269
2269
  let resolvedRelPath = null;
2270
2270
  if (cleaned.startsWith(".")) {
2271
2271
  const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
2272
- const fromDir = path27.dirname(relativePath);
2272
+ const fromDir = path29.dirname(relativePath);
2273
2273
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
2274
- const candidate = path27.join(fromDir, cleanedNoJs + ext);
2275
- const normalized = path27.normalize(candidate);
2274
+ const candidate = path29.join(fromDir, cleanedNoJs + ext);
2275
+ const normalized = path29.normalize(candidate);
2276
2276
  if (fileIndex.has(normalized)) {
2277
2277
  const absPath = fileIndex.get(normalized);
2278
- resolvedRelPath = path27.relative(workspaceRoot, absPath);
2278
+ resolvedRelPath = path29.relative(workspaceRoot, absPath);
2279
2279
  break;
2280
2280
  }
2281
2281
  }
@@ -2589,27 +2589,27 @@ __export(group_registry_exports, {
2589
2589
  saveSyncResult: () => saveSyncResult
2590
2590
  });
2591
2591
  function groupFile(name) {
2592
- return path27.join(GROUPS_DIR, `${name}.json`);
2592
+ return path29.join(GROUPS_DIR, `${name}.json`);
2593
2593
  }
2594
2594
  function loadGroup(name) {
2595
2595
  try {
2596
- return JSON.parse(fs19.readFileSync(groupFile(name), "utf-8"));
2596
+ return JSON.parse(fs23.readFileSync(groupFile(name), "utf-8"));
2597
2597
  } catch {
2598
2598
  return null;
2599
2599
  }
2600
2600
  }
2601
2601
  function saveGroup(group) {
2602
- fs19.mkdirSync(GROUPS_DIR, { recursive: true });
2603
- fs19.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
2602
+ fs23.mkdirSync(GROUPS_DIR, { recursive: true });
2603
+ fs23.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
2604
2604
  }
2605
2605
  function listGroups() {
2606
2606
  const groups = [];
2607
2607
  try {
2608
- for (const file of fs19.readdirSync(GROUPS_DIR)) {
2608
+ for (const file of fs23.readdirSync(GROUPS_DIR)) {
2609
2609
  if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
2610
2610
  try {
2611
2611
  const g = JSON.parse(
2612
- fs19.readFileSync(path27.join(GROUPS_DIR, file), "utf-8")
2612
+ fs23.readFileSync(path29.join(GROUPS_DIR, file), "utf-8")
2613
2613
  );
2614
2614
  groups.push(g);
2615
2615
  } catch {
@@ -2621,16 +2621,16 @@ function listGroups() {
2621
2621
  }
2622
2622
  function deleteGroup(name) {
2623
2623
  try {
2624
- fs19.unlinkSync(groupFile(name));
2624
+ fs23.unlinkSync(groupFile(name));
2625
2625
  } catch {
2626
2626
  }
2627
2627
  try {
2628
- fs19.unlinkSync(path27.join(GROUPS_DIR, `${name}.sync.json`));
2628
+ fs23.unlinkSync(path29.join(GROUPS_DIR, `${name}.sync.json`));
2629
2629
  } catch {
2630
2630
  }
2631
2631
  }
2632
2632
  function groupExists(name) {
2633
- return fs19.existsSync(groupFile(name));
2633
+ return fs23.existsSync(groupFile(name));
2634
2634
  }
2635
2635
  function addMember(groupName, member) {
2636
2636
  const group = loadGroup(groupName);
@@ -2656,16 +2656,16 @@ function removeMember(groupName, groupPath) {
2656
2656
  return group;
2657
2657
  }
2658
2658
  function saveSyncResult(result) {
2659
- fs19.mkdirSync(GROUPS_DIR, { recursive: true });
2660
- fs19.writeFileSync(
2661
- path27.join(GROUPS_DIR, `${result.groupName}.sync.json`),
2659
+ fs23.mkdirSync(GROUPS_DIR, { recursive: true });
2660
+ fs23.writeFileSync(
2661
+ path29.join(GROUPS_DIR, `${result.groupName}.sync.json`),
2662
2662
  JSON.stringify(result, null, 2) + "\n"
2663
2663
  );
2664
2664
  }
2665
2665
  function loadSyncResult(groupName) {
2666
2666
  try {
2667
2667
  return JSON.parse(
2668
- fs19.readFileSync(path27.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
2668
+ fs23.readFileSync(path29.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
2669
2669
  );
2670
2670
  } catch {
2671
2671
  return null;
@@ -2674,7 +2674,7 @@ function loadSyncResult(groupName) {
2674
2674
  var GROUPS_DIR;
2675
2675
  var init_group_registry = __esm({
2676
2676
  "src/multi-repo/group-registry.ts"() {
2677
- GROUPS_DIR = path27.join(os12.homedir(), ".code-intel", "groups");
2677
+ GROUPS_DIR = path29.join(os12.homedir(), ".code-intel", "groups");
2678
2678
  }
2679
2679
  });
2680
2680
 
@@ -2927,9 +2927,9 @@ var init_orphan_files = __esm({
2927
2927
  // src/health/health-score.ts
2928
2928
  var health_score_exports = {};
2929
2929
  __export(health_score_exports, {
2930
- computeHealthReport: () => computeHealthReport
2930
+ computeHealthReport: () => computeHealthReport2
2931
2931
  });
2932
- function computeHealthReport(graph, godNodeConfig) {
2932
+ function computeHealthReport2(graph, godNodeConfig) {
2933
2933
  const deadCode = detectDeadCode(graph);
2934
2934
  const cycles = detectCircularDeps(graph);
2935
2935
  const godNodes = detectGodNodes(graph, godNodeConfig);
@@ -3735,10 +3735,10 @@ var init_codes = __esm({
3735
3735
  }
3736
3736
  });
3737
3737
  function secureMkdir(dir) {
3738
- fs19.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
3738
+ fs23.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
3739
3739
  if (process.platform !== "win32") {
3740
3740
  try {
3741
- fs19.chmodSync(dir, SECURE_DIR_MODE);
3741
+ fs23.chmodSync(dir, SECURE_DIR_MODE);
3742
3742
  } catch {
3743
3743
  }
3744
3744
  }
@@ -3746,17 +3746,17 @@ function secureMkdir(dir) {
3746
3746
  function secureChmodFile(file) {
3747
3747
  if (process.platform === "win32") return;
3748
3748
  try {
3749
- fs19.chmodSync(file, SECURE_FILE_MODE);
3749
+ fs23.chmodSync(file, SECURE_FILE_MODE);
3750
3750
  } catch {
3751
3751
  }
3752
3752
  }
3753
3753
  function tightenDbFiles(dir) {
3754
3754
  if (process.platform === "win32") return;
3755
- if (!fs19.existsSync(dir)) return;
3756
- for (const name of fs19.readdirSync(dir)) {
3755
+ if (!fs23.existsSync(dir)) return;
3756
+ for (const name of fs23.readdirSync(dir)) {
3757
3757
  if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
3758
3758
  try {
3759
- fs19.chmodSync(path27.join(dir, name), SECURE_FILE_MODE);
3759
+ fs23.chmodSync(path29.join(dir, name), SECURE_FILE_MODE);
3760
3760
  } catch {
3761
3761
  }
3762
3762
  }
@@ -3770,7 +3770,7 @@ var init_fs_secure = __esm({
3770
3770
  }
3771
3771
  });
3772
3772
  function getUsersDBPath() {
3773
- return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "users.db");
3773
+ return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "users.db");
3774
3774
  }
3775
3775
  function getOrCreateUsersDB() {
3776
3776
  if (!_usersDB) {
@@ -3786,7 +3786,7 @@ var init_users_db = __esm({
3786
3786
  UsersDB = class {
3787
3787
  db;
3788
3788
  constructor(dbPath) {
3789
- const dir = path27.dirname(dbPath);
3789
+ const dir = path29.dirname(dbPath);
3790
3790
  secureMkdir(dir);
3791
3791
  this.db = new Database3(dbPath);
3792
3792
  this.db.pragma("journal_mode = WAL");
@@ -4063,7 +4063,7 @@ function getScryptN() {
4063
4063
  return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
4064
4064
  }
4065
4065
  function getSecretsPath() {
4066
- return process.env["CODE_INTEL_SECRETS_PATH"] ?? path27.join(os12.homedir(), ".code-intel", ".secrets");
4066
+ return process.env["CODE_INTEL_SECRETS_PATH"] ?? path29.join(os12.homedir(), ".code-intel", ".secrets");
4067
4067
  }
4068
4068
  function getMasterPassword() {
4069
4069
  const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
@@ -4094,8 +4094,8 @@ function decryptSecrets(encrypted) {
4094
4094
  return JSON.parse(plaintext.toString("utf8"));
4095
4095
  }
4096
4096
  function loadSecrets(secretsPath = getSecretsPath()) {
4097
- if (!fs19.existsSync(secretsPath)) return {};
4098
- const blob = fs19.readFileSync(secretsPath);
4097
+ if (!fs23.existsSync(secretsPath)) return {};
4098
+ const blob = fs23.readFileSync(secretsPath);
4099
4099
  return decryptSecrets(blob);
4100
4100
  }
4101
4101
  function getSecret(key, secretsPath = getSecretsPath()) {
@@ -4214,6 +4214,9 @@ function requireAuth(req, res, next) {
4214
4214
  }
4215
4215
  next();
4216
4216
  }
4217
+ function meetsRole(userRole, required) {
4218
+ return (ROLE_RANK[userRole] ?? 0) >= (ROLE_RANK[required] ?? 0);
4219
+ }
4217
4220
  function requireRole(...roles) {
4218
4221
  return (req, res, next) => {
4219
4222
  if (!req.user) {
@@ -4228,7 +4231,8 @@ function requireRole(...roles) {
4228
4231
  });
4229
4232
  return;
4230
4233
  }
4231
- if (!roles.includes(req.user.role)) {
4234
+ const allowed = roles.some((r) => meetsRole(req.user.role, r));
4235
+ if (!allowed) {
4232
4236
  res.status(403).json({
4233
4237
  error: {
4234
4238
  code: ErrorCodes.FORBIDDEN,
@@ -4337,7 +4341,7 @@ function clearSessionCookie() {
4337
4341
  async function verifyPassword(plain, hash) {
4338
4342
  return bcrypt.compare(plain, hash);
4339
4343
  }
4340
- var sessionStore, SESSION_COOKIE_NAME;
4344
+ var sessionStore, SESSION_COOKIE_NAME, ROLE_RANK;
4341
4345
  var init_middleware = __esm({
4342
4346
  "src/auth/middleware.ts"() {
4343
4347
  init_users_db();
@@ -4345,6 +4349,12 @@ var init_middleware = __esm({
4345
4349
  init_secret_store();
4346
4350
  sessionStore = /* @__PURE__ */ new Map();
4347
4351
  SESSION_COOKIE_NAME = "code_intel_session";
4352
+ ROLE_RANK = {
4353
+ viewer: 1,
4354
+ "repo-owner": 2,
4355
+ analyst: 3,
4356
+ admin: 4
4357
+ };
4348
4358
  }
4349
4359
  });
4350
4360
  function verifyWebSocketHandshake(req) {
@@ -4652,7 +4662,7 @@ init_shared();
4652
4662
  init_shared();
4653
4663
  init_typescript();
4654
4664
  function resolveRelative(rawPath, fromFile, workspace) {
4655
- const fromDir = path27.dirname(fromFile);
4665
+ const fromDir = path29.dirname(fromFile);
4656
4666
  const cleaned = rawPath.replace(/['"]/g, "");
4657
4667
  const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
4658
4668
  const resolved = workspace.resolve(fromDir, cleaned);
@@ -4704,7 +4714,7 @@ var pythonModule = {
4704
4714
  resolveImport(rawPath, fromFile, workspace) {
4705
4715
  const cleaned = rawPath.replace(/['"]/g, "");
4706
4716
  const parts = cleaned.split(".");
4707
- const fromDir = path27.dirname(fromFile);
4717
+ const fromDir = path29.dirname(fromFile);
4708
4718
  const relPath = parts.join("/");
4709
4719
  for (const suffix of ["/__init__.py", ".py"]) {
4710
4720
  const r = workspace.resolve(fromDir, relPath + suffix);
@@ -4783,7 +4793,7 @@ var cModule = {
4783
4793
  inheritanceStrategy: "none",
4784
4794
  resolveImport(rawPath, fromFile, workspace) {
4785
4795
  const cleaned = rawPath.replace(/[<>"']/g, "");
4786
- const fromDir = path27.dirname(fromFile);
4796
+ const fromDir = path29.dirname(fromFile);
4787
4797
  return workspace.resolve(fromDir, cleaned);
4788
4798
  },
4789
4799
  isExported(_node) {
@@ -4806,7 +4816,7 @@ var cppModule = {
4806
4816
  inheritanceStrategy: "depth-first",
4807
4817
  resolveImport(rawPath, fromFile, workspace) {
4808
4818
  const cleaned = rawPath.replace(/[<>"']/g, "");
4809
- const fromDir = path27.dirname(fromFile);
4819
+ const fromDir = path29.dirname(fromFile);
4810
4820
  return workspace.resolve(fromDir, cleaned);
4811
4821
  },
4812
4822
  isExported(_node) {
@@ -4968,7 +4978,7 @@ var dartModule = {
4968
4978
  const pkg = cleaned.replace("package:", "");
4969
4979
  return workspace.findByPackage(pkg);
4970
4980
  }
4971
- const fromDir = path27.dirname(fromFile);
4981
+ const fromDir = path29.dirname(fromFile);
4972
4982
  return workspace.resolve(fromDir, cleaned);
4973
4983
  },
4974
4984
  isExported(node) {
@@ -5323,25 +5333,25 @@ function validateDAG(phases) {
5323
5333
  const visiting = /* @__PURE__ */ new Set();
5324
5334
  const visited = /* @__PURE__ */ new Set();
5325
5335
  const phaseMap = new Map(phases.map((p) => [p.name, p]));
5326
- function dfs(name, path28) {
5336
+ function dfs(name, path30) {
5327
5337
  if (visiting.has(name)) {
5328
- const cycleStart = path28.indexOf(name);
5329
- const cycle = path28.slice(cycleStart).concat(name);
5338
+ const cycleStart = path30.indexOf(name);
5339
+ const cycle = path30.slice(cycleStart).concat(name);
5330
5340
  errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
5331
5341
  return true;
5332
5342
  }
5333
5343
  if (visited.has(name)) return false;
5334
5344
  visiting.add(name);
5335
- path28.push(name);
5345
+ path30.push(name);
5336
5346
  const phase = phaseMap.get(name);
5337
5347
  if (phase) {
5338
5348
  for (const dep of phase.dependencies) {
5339
- if (dfs(dep, path28)) return true;
5349
+ if (dfs(dep, path30)) return true;
5340
5350
  }
5341
5351
  }
5342
5352
  visiting.delete(name);
5343
5353
  visited.add(name);
5344
- path28.pop();
5354
+ path30.pop();
5345
5355
  return false;
5346
5356
  }
5347
5357
  for (const phase of phases) {
@@ -5554,7 +5564,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
5554
5564
  ]);
5555
5565
  function loadIgnorePatterns(workspaceRoot) {
5556
5566
  try {
5557
- const raw = fs19.readFileSync(path27.join(workspaceRoot, ".codeintelignore"), "utf-8");
5567
+ const raw = fs23.readFileSync(path29.join(workspaceRoot, ".codeintelignore"), "utf-8");
5558
5568
  const extras = /* @__PURE__ */ new Set();
5559
5569
  for (const line of raw.split("\n")) {
5560
5570
  const trimmed = line.trim();
@@ -5578,7 +5588,7 @@ var scanPhase = {
5578
5588
  function walk(dir) {
5579
5589
  let entries;
5580
5590
  try {
5581
- entries = fs19.readdirSync(dir, { withFileTypes: true });
5591
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
5582
5592
  } catch {
5583
5593
  return;
5584
5594
  }
@@ -5587,15 +5597,15 @@ var scanPhase = {
5587
5597
  if (entry.name.startsWith(".")) continue;
5588
5598
  if (IGNORED_DIRS.has(entry.name)) continue;
5589
5599
  if (extraIgnore.has(entry.name)) continue;
5590
- walk(path27.join(dir, entry.name));
5600
+ walk(path29.join(dir, entry.name));
5591
5601
  } else if (entry.isFile()) {
5592
5602
  const name = entry.name;
5593
5603
  if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
5594
- const ext = path27.extname(name);
5604
+ const ext = path29.extname(name);
5595
5605
  if (!extensions.has(ext)) continue;
5596
- const fullPath = path27.join(dir, name);
5606
+ const fullPath = path29.join(dir, name);
5597
5607
  try {
5598
- const stat = fs19.statSync(fullPath);
5608
+ const stat = fs23.statSync(fullPath);
5599
5609
  if (stat.size > MAX_FILE_SIZE_BYTES) continue;
5600
5610
  } catch {
5601
5611
  continue;
@@ -5622,20 +5632,20 @@ var structurePhase = {
5622
5632
  const dirs = /* @__PURE__ */ new Set();
5623
5633
  let structDone = 0;
5624
5634
  for (const filePath of context2.filePaths) {
5625
- const relativePath = path27.relative(context2.workspaceRoot, filePath);
5635
+ const relativePath = path29.relative(context2.workspaceRoot, filePath);
5626
5636
  const lang = detectLanguage(filePath);
5627
5637
  context2.graph.addNode({
5628
5638
  id: generateNodeId("file", relativePath, relativePath),
5629
5639
  kind: "file",
5630
- name: path27.basename(filePath),
5640
+ name: path29.basename(filePath),
5631
5641
  filePath: relativePath,
5632
5642
  metadata: lang ? { language: lang } : void 0
5633
5643
  });
5634
- let dir = path27.dirname(relativePath);
5644
+ let dir = path29.dirname(relativePath);
5635
5645
  while (dir && dir !== "." && dir !== "") {
5636
5646
  if (dirs.has(dir)) break;
5637
5647
  dirs.add(dir);
5638
- dir = path27.dirname(dir);
5648
+ dir = path29.dirname(dir);
5639
5649
  }
5640
5650
  structDone++;
5641
5651
  context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
@@ -5644,7 +5654,7 @@ var structurePhase = {
5644
5654
  context2.graph.addNode({
5645
5655
  id: generateNodeId("directory", dir, dir),
5646
5656
  kind: "directory",
5647
- name: path27.basename(dir),
5657
+ name: path29.basename(dir),
5648
5658
  filePath: dir
5649
5659
  });
5650
5660
  }
@@ -5755,22 +5765,22 @@ var flowPhase = {
5755
5765
  const queue = [{ nodeId: ep.id, path: [ep.id] }];
5756
5766
  const visited = /* @__PURE__ */ new Set();
5757
5767
  while (queue.length > 0 && flowCount < maxFlows) {
5758
- const { nodeId, path: path28 } = queue.shift();
5759
- if (path28.length > maxDepth) continue;
5768
+ const { nodeId, path: path30 } = queue.shift();
5769
+ if (path30.length > maxDepth) continue;
5760
5770
  const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
5761
- if (callEdges.length === 0 && path28.length >= 3) {
5771
+ if (callEdges.length === 0 && path30.length >= 3) {
5762
5772
  const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
5763
5773
  graph.addNode({
5764
5774
  id: flowId,
5765
5775
  kind: "flow",
5766
5776
  name: `${ep.name} flow ${flowCount}`,
5767
5777
  filePath: ep.filePath,
5768
- metadata: { steps: path28, entryPoint: ep.name }
5778
+ metadata: { steps: path30, entryPoint: ep.name }
5769
5779
  });
5770
- for (let i = 0; i < path28.length; i++) {
5780
+ for (let i = 0; i < path30.length; i++) {
5771
5781
  graph.addEdge({
5772
- id: generateEdgeId(path28[i], flowId, `step_of_${i}`),
5773
- source: path28[i],
5782
+ id: generateEdgeId(path30[i], flowId, `step_of_${i}`),
5783
+ source: path30[i],
5774
5784
  target: flowId,
5775
5785
  kind: "step_of",
5776
5786
  weight: 1,
@@ -5783,7 +5793,7 @@ var flowPhase = {
5783
5793
  for (const edge of callEdges) {
5784
5794
  if (visited.has(edge.target)) continue;
5785
5795
  visited.add(edge.target);
5786
- queue.push({ nodeId: edge.target, path: [...path28, edge.target] });
5796
+ queue.push({ nodeId: edge.target, path: [...path30, edge.target] });
5787
5797
  }
5788
5798
  }
5789
5799
  }
@@ -5801,7 +5811,7 @@ var LLMGovernanceLogger = class {
5801
5811
  }
5802
5812
  /** Path to the JSONL log file. */
5803
5813
  getLogPath() {
5804
- return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
5814
+ return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
5805
5815
  }
5806
5816
  /**
5807
5817
  * Append an entry to the governance log.
@@ -5817,8 +5827,8 @@ var LLMGovernanceLogger = class {
5817
5827
  ...entry
5818
5828
  };
5819
5829
  const logPath = this.getLogPath();
5820
- fs19.mkdirSync(path27.dirname(logPath), { recursive: true });
5821
- fs19.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
5830
+ fs23.mkdirSync(path29.dirname(logPath), { recursive: true });
5831
+ fs23.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
5822
5832
  } catch {
5823
5833
  }
5824
5834
  }
@@ -5828,7 +5838,7 @@ var LLMGovernanceLogger = class {
5828
5838
  */
5829
5839
  readLog(limit = 100) {
5830
5840
  try {
5831
- const raw = fs19.readFileSync(this.getLogPath(), "utf-8");
5841
+ const raw = fs23.readFileSync(this.getLogPath(), "utf-8");
5832
5842
  const lines = raw.split("\n").filter((l) => l.trim().length > 0).slice(-limit);
5833
5843
  return lines.map((l) => JSON.parse(l));
5834
5844
  } catch {
@@ -5972,17 +5982,17 @@ function traceFlow(entryId, graph, maxDepth = 10, maxBranching = 4) {
5972
5982
  const queue = [{ nodeId: entryId, path: [entryId] }];
5973
5983
  const visited = /* @__PURE__ */ new Set();
5974
5984
  while (queue.length > 0 && flows.length < maxFlows) {
5975
- const { nodeId, path: path28 } = queue.shift();
5976
- if (path28.length > maxDepth) continue;
5985
+ const { nodeId, path: path30 } = queue.shift();
5986
+ if (path30.length > maxDepth) continue;
5977
5987
  const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
5978
- if (callEdges.length === 0 && path28.length >= 3) {
5979
- flows.push({ entryPointId: entryId, steps: [...path28] });
5988
+ if (callEdges.length === 0 && path30.length >= 3) {
5989
+ flows.push({ entryPointId: entryId, steps: [...path30] });
5980
5990
  continue;
5981
5991
  }
5982
5992
  for (const edge of callEdges) {
5983
5993
  if (visited.has(edge.target)) continue;
5984
5994
  visited.add(edge.target);
5985
- queue.push({ nodeId: edge.target, path: [...path28, edge.target] });
5995
+ queue.push({ nodeId: edge.target, path: [...path30, edge.target] });
5986
5996
  }
5987
5997
  }
5988
5998
  }
@@ -6216,7 +6226,7 @@ init_embedder();
6216
6226
  async function hybridSearch(graph, query, limit, options = {}) {
6217
6227
  const { vectorDbPath, bm25Limit = 50, vectorLimit = 50 } = options;
6218
6228
  const bm25Promise = Promise.resolve(textSearch(graph, query, bm25Limit));
6219
- const hasVectorDb = Boolean(vectorDbPath && fs19.existsSync(vectorDbPath));
6229
+ const hasVectorDb = Boolean(vectorDbPath && fs23.existsSync(vectorDbPath));
6220
6230
  if (!hasVectorDb) {
6221
6231
  const bm25Results2 = await bm25Promise;
6222
6232
  return {
@@ -6273,7 +6283,7 @@ var DbManager = class {
6273
6283
  this.dbPath = dbPath;
6274
6284
  }
6275
6285
  async init() {
6276
- fs19.mkdirSync(path27.dirname(this.dbPath), { recursive: true });
6286
+ fs23.mkdirSync(path29.dirname(this.dbPath), { recursive: true });
6277
6287
  this.db = new Database(this.dbPath);
6278
6288
  await this.db.init();
6279
6289
  this.conn = new Connection(this.db);
@@ -6330,7 +6340,8 @@ var NODE_TABLE_MAP = {
6330
6340
  constant: "const_nodes",
6331
6341
  route: "route_nodes",
6332
6342
  cluster: "cluster_nodes",
6333
- flow: "flow_nodes"
6343
+ flow: "flow_nodes",
6344
+ vulnerability: "vuln_nodes"
6334
6345
  };
6335
6346
  var ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
6336
6347
  function getCreateNodeTableDDL(tableName) {
@@ -6362,7 +6373,7 @@ function getCreateEdgeTableDDL() {
6362
6373
  )`];
6363
6374
  }
6364
6375
  function writeNodeCSVs(graph, outputDir) {
6365
- fs19.mkdirSync(outputDir, { recursive: true });
6376
+ fs23.mkdirSync(outputDir, { recursive: true });
6366
6377
  const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
6367
6378
  const tableBuffers = /* @__PURE__ */ new Map();
6368
6379
  const tableFilePaths = /* @__PURE__ */ new Map();
@@ -6370,7 +6381,7 @@ function writeNodeCSVs(graph, outputDir) {
6370
6381
  const table = NODE_TABLE_MAP[node.kind];
6371
6382
  if (!tableBuffers.has(table)) {
6372
6383
  tableBuffers.set(table, [header]);
6373
- tableFilePaths.set(table, path27.join(outputDir, `${table}.csv`));
6384
+ tableFilePaths.set(table, path29.join(outputDir, `${table}.csv`));
6374
6385
  }
6375
6386
  tableBuffers.get(table).push(
6376
6387
  csvRow([
@@ -6390,12 +6401,12 @@ function writeNodeCSVs(graph, outputDir) {
6390
6401
  );
6391
6402
  }
6392
6403
  for (const [table, lines] of tableBuffers) {
6393
- fs19.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
6404
+ fs23.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
6394
6405
  }
6395
6406
  return tableFilePaths;
6396
6407
  }
6397
6408
  function writeEdgeCSV(graph, outputDir) {
6398
- fs19.mkdirSync(outputDir, { recursive: true });
6409
+ fs23.mkdirSync(outputDir, { recursive: true });
6399
6410
  const header = "from_id,to_id,kind,weight,label\n";
6400
6411
  const groups = /* @__PURE__ */ new Map();
6401
6412
  for (const edge of graph.allEdges()) {
@@ -6406,7 +6417,7 @@ function writeEdgeCSV(graph, outputDir) {
6406
6417
  const toTable = NODE_TABLE_MAP[targetNode.kind];
6407
6418
  const key = `${fromTable}->${toTable}`;
6408
6419
  if (!groups.has(key)) {
6409
- const filePath = path27.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
6420
+ const filePath = path29.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
6410
6421
  groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
6411
6422
  }
6412
6423
  groups.get(key).lines.push(
@@ -6421,7 +6432,7 @@ function writeEdgeCSV(graph, outputDir) {
6421
6432
  }
6422
6433
  const result = [];
6423
6434
  for (const group of groups.values()) {
6424
- fs19.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
6435
+ fs23.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
6425
6436
  result.push({ fromTable: group.from, toTable: group.to, filePath: group.filePath });
6426
6437
  }
6427
6438
  return result;
@@ -6449,7 +6460,7 @@ async function loadGraphToDB(graph, dbManager) {
6449
6460
  } catch {
6450
6461
  }
6451
6462
  }
6452
- const tmpDir = fs19.mkdtempSync(path27.join(os12.tmpdir(), "code-intel-csv-"));
6463
+ const tmpDir = fs23.mkdtempSync(path29.join(os12.tmpdir(), "code-intel-csv-"));
6453
6464
  try {
6454
6465
  const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
6455
6466
  const edgeGroups = writeEdgeCSV(graph, tmpDir);
@@ -6468,8 +6479,8 @@ async function loadGraphToDB(graph, dbManager) {
6468
6479
  }
6469
6480
  let nodeCount = 0;
6470
6481
  for (const [table, csvPath] of nodeTableFiles) {
6471
- if (!fs19.existsSync(csvPath)) continue;
6472
- const stat = fs19.statSync(csvPath);
6482
+ if (!fs23.existsSync(csvPath)) continue;
6483
+ const stat = fs23.statSync(csvPath);
6473
6484
  if (stat.size < 50) continue;
6474
6485
  try {
6475
6486
  await dbManager.execute(
@@ -6482,8 +6493,8 @@ async function loadGraphToDB(graph, dbManager) {
6482
6493
  }
6483
6494
  let edgeCount = 0;
6484
6495
  for (const group of edgeGroups) {
6485
- if (!fs19.existsSync(group.filePath)) continue;
6486
- const stat = fs19.statSync(group.filePath);
6496
+ if (!fs23.existsSync(group.filePath)) continue;
6497
+ const stat = fs23.statSync(group.filePath);
6487
6498
  if (stat.size < 50) continue;
6488
6499
  try {
6489
6500
  await dbManager.execute(
@@ -6497,7 +6508,7 @@ async function loadGraphToDB(graph, dbManager) {
6497
6508
  return { nodeCount, edgeCount };
6498
6509
  } finally {
6499
6510
  try {
6500
- fs19.rmSync(tmpDir, { recursive: true, force: true });
6511
+ fs23.rmSync(tmpDir, { recursive: true, force: true });
6501
6512
  } catch {
6502
6513
  }
6503
6514
  }
@@ -6549,19 +6560,19 @@ function buildNodeProps(node) {
6549
6560
  function escCypher(s) {
6550
6561
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
6551
6562
  }
6552
- var GLOBAL_DIR = path27.join(os12.homedir(), ".code-intel");
6553
- var REPOS_FILE = path27.join(GLOBAL_DIR, "repos.json");
6563
+ var GLOBAL_DIR = path29.join(os12.homedir(), ".code-intel");
6564
+ var REPOS_FILE = path29.join(GLOBAL_DIR, "repos.json");
6554
6565
  function loadRegistry() {
6555
6566
  try {
6556
- const data = fs19.readFileSync(REPOS_FILE, "utf-8");
6567
+ const data = fs23.readFileSync(REPOS_FILE, "utf-8");
6557
6568
  return JSON.parse(data);
6558
6569
  } catch {
6559
6570
  return [];
6560
6571
  }
6561
6572
  }
6562
6573
  function saveRegistry(entries) {
6563
- fs19.mkdirSync(GLOBAL_DIR, { recursive: true });
6564
- fs19.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
6574
+ fs23.mkdirSync(GLOBAL_DIR, { recursive: true });
6575
+ fs23.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
6565
6576
  }
6566
6577
  function upsertRepo(entry) {
6567
6578
  const entries = loadRegistry();
@@ -6578,23 +6589,23 @@ function removeRepo(repoPath) {
6578
6589
  saveRegistry(entries);
6579
6590
  }
6580
6591
  function saveMetadata(repoDir, metadata) {
6581
- const metaDir = path27.join(repoDir, ".code-intel");
6582
- fs19.mkdirSync(metaDir, { recursive: true });
6583
- fs19.writeFileSync(path27.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
6592
+ const metaDir = path29.join(repoDir, ".code-intel");
6593
+ fs23.mkdirSync(metaDir, { recursive: true });
6594
+ fs23.writeFileSync(path29.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
6584
6595
  }
6585
6596
  function loadMetadata(repoDir) {
6586
6597
  try {
6587
- const data = fs19.readFileSync(path27.join(repoDir, ".code-intel", "meta.json"), "utf-8");
6598
+ const data = fs23.readFileSync(path29.join(repoDir, ".code-intel", "meta.json"), "utf-8");
6588
6599
  return JSON.parse(data);
6589
6600
  } catch {
6590
6601
  return null;
6591
6602
  }
6592
6603
  }
6593
6604
  function getDbPath(repoDir) {
6594
- return path27.join(repoDir, ".code-intel", "graph.db");
6605
+ return path29.join(repoDir, ".code-intel", "graph.db");
6595
6606
  }
6596
6607
  function getVectorDbPath(repoDir) {
6597
- return path27.join(repoDir, ".code-intel", "vector.db");
6608
+ return path29.join(repoDir, ".code-intel", "vector.db");
6598
6609
  }
6599
6610
 
6600
6611
  // src/mcp-server/server.ts
@@ -6672,6 +6683,205 @@ async function loadGraphFromDB(graph, db) {
6672
6683
 
6673
6684
  // src/multi-repo/group-sync.ts
6674
6685
  init_logger();
6686
+ function scanForFiles(root, matcher, maxDepth = 2) {
6687
+ const results = [];
6688
+ function walk(dir, depth) {
6689
+ if (depth > maxDepth) return;
6690
+ let entries;
6691
+ try {
6692
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
6693
+ } catch {
6694
+ return;
6695
+ }
6696
+ for (const entry of entries) {
6697
+ const full = path29.join(dir, entry.name);
6698
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
6699
+ walk(full, depth + 1);
6700
+ } else if (entry.isFile() && matcher(entry.name)) {
6701
+ results.push(full);
6702
+ }
6703
+ }
6704
+ }
6705
+ walk(root, 0);
6706
+ return results;
6707
+ }
6708
+
6709
+ // src/multi-repo/schema-parsers/openapi-parser.ts
6710
+ var OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
6711
+ "openapi.yaml",
6712
+ "openapi.json",
6713
+ "openapi.yml",
6714
+ "swagger.yaml",
6715
+ "swagger.json",
6716
+ "swagger.yml"
6717
+ ]);
6718
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
6719
+ function tryParseFile(filePath) {
6720
+ const ext = path29.extname(filePath).toLowerCase();
6721
+ const content = fs23.readFileSync(filePath, "utf-8");
6722
+ if (ext === ".json") {
6723
+ try {
6724
+ return JSON.parse(content);
6725
+ } catch {
6726
+ return null;
6727
+ }
6728
+ }
6729
+ return null;
6730
+ }
6731
+ async function parseOpenAPIContracts(repoRoot) {
6732
+ const files = scanForFiles(repoRoot, (name) => OPENAPI_FILENAMES.has(name));
6733
+ const contracts = [];
6734
+ for (const filePath of files) {
6735
+ const spec = tryParseFile(filePath);
6736
+ if (!spec) continue;
6737
+ const paths = spec["paths"];
6738
+ if (!paths || typeof paths !== "object") continue;
6739
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
6740
+ if (!pathItem || typeof pathItem !== "object") continue;
6741
+ const ops = pathItem;
6742
+ for (const method of HTTP_METHODS) {
6743
+ if (!(method in ops)) continue;
6744
+ const operation = ops[method];
6745
+ if (!operation) continue;
6746
+ const requestBody = operation["requestBody"];
6747
+ const requestSchema = requestBody?.["content"] ? requestBody["content"]["application/json"]?.["schema"] : void 0;
6748
+ const responses = operation["responses"];
6749
+ const ok200 = responses?.["200"];
6750
+ const responseSchema = ok200?.["content"] ? ok200["content"]["application/json"]?.["schema"] : void 0;
6751
+ contracts.push({
6752
+ name: `${method.toUpperCase()} ${pathStr}`,
6753
+ kind: "route",
6754
+ method: method.toUpperCase(),
6755
+ path: pathStr,
6756
+ ...requestSchema ? { requestSchema } : {},
6757
+ ...responseSchema ? { responseSchema } : {},
6758
+ filePath
6759
+ });
6760
+ }
6761
+ }
6762
+ }
6763
+ return contracts;
6764
+ }
6765
+ function extractFieldNames(block) {
6766
+ return block.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
6767
+ const m = line.match(/^(\w+)\s*[(:]/);
6768
+ return m ? m[1] : null;
6769
+ }).filter((f) => f !== null);
6770
+ }
6771
+ async function parseGraphQLContracts(repoRoot) {
6772
+ const files = scanForFiles(repoRoot, (name) => name.endsWith(".graphql") || name.endsWith(".gql"));
6773
+ const contracts = [];
6774
+ for (const filePath of files) {
6775
+ const content = fs23.readFileSync(filePath, "utf-8");
6776
+ const typeRegex = /type\s+(\w+)\s*\{([^}]+)\}/g;
6777
+ let match;
6778
+ while ((match = typeRegex.exec(content)) !== null) {
6779
+ const typeName = match[1];
6780
+ const body = match[2];
6781
+ const fields = extractFieldNames(body);
6782
+ const lcName = typeName.toLowerCase();
6783
+ if (lcName === "query") {
6784
+ for (const field of fields) {
6785
+ contracts.push({ name: `query.${field}`, kind: "graphql", operation: "query", fields, filePath });
6786
+ }
6787
+ } else if (lcName === "mutation") {
6788
+ for (const field of fields) {
6789
+ contracts.push({ name: `mutation.${field}`, kind: "graphql", operation: "mutation", fields, filePath });
6790
+ }
6791
+ } else if (lcName === "subscription") {
6792
+ for (const field of fields) {
6793
+ contracts.push({ name: `subscription.${field}`, kind: "graphql", operation: "subscription", fields, filePath });
6794
+ }
6795
+ } else {
6796
+ contracts.push({ name: `type.${typeName}`, kind: "graphql", operation: "type", fields, filePath });
6797
+ }
6798
+ }
6799
+ }
6800
+ return contracts;
6801
+ }
6802
+ async function parseProtoContracts(repoRoot) {
6803
+ const files = scanForFiles(repoRoot, (name) => name.endsWith(".proto"));
6804
+ const contracts = [];
6805
+ for (const filePath of files) {
6806
+ const content = fs23.readFileSync(filePath, "utf-8");
6807
+ const serviceRegex = /service\s+(\w+)\s*\{([^}]+)\}/g;
6808
+ let serviceMatch;
6809
+ while ((serviceMatch = serviceRegex.exec(content)) !== null) {
6810
+ const serviceName = serviceMatch[1];
6811
+ const body = serviceMatch[2];
6812
+ const rpcRegex = /rpc\s+(\w+)\s*\((\w+)\)\s*returns\s*\((\w+)\)/g;
6813
+ let rpcMatch;
6814
+ while ((rpcMatch = rpcRegex.exec(body)) !== null) {
6815
+ const rpcName = rpcMatch[1];
6816
+ const inputType = rpcMatch[2];
6817
+ const outputType = rpcMatch[3];
6818
+ contracts.push({
6819
+ name: `${serviceName}.${rpcName}`,
6820
+ kind: "grpc",
6821
+ serviceName,
6822
+ rpcName,
6823
+ inputType,
6824
+ outputType,
6825
+ filePath
6826
+ });
6827
+ }
6828
+ }
6829
+ }
6830
+ return contracts;
6831
+ }
6832
+
6833
+ // src/multi-repo/type-similarity.ts
6834
+ function normalizeType(t) {
6835
+ return t.toLowerCase().replace(/[\[\]?<>\s]/g, "").trim();
6836
+ }
6837
+ function paramTypeSimilarity(paramsA, paramsB) {
6838
+ if (paramsA.length === 0 && paramsB.length === 0) return 1;
6839
+ if (paramsA.length === 0 || paramsB.length === 0) return 0;
6840
+ const setA = new Set(paramsA.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
6841
+ const setB = new Set(paramsB.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
6842
+ if (setA.size === 0 && setB.size === 0) return 1;
6843
+ if (setA.size === 0 || setB.size === 0) return 0;
6844
+ const intersection = [...setA].filter((x) => setB.has(x)).length;
6845
+ const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
6846
+ return intersection / union;
6847
+ }
6848
+ function returnTypeSimilarity(typeA, typeB) {
6849
+ if (!typeA || !typeB) return 0.5;
6850
+ const a = normalizeType(typeA);
6851
+ const b = normalizeType(typeB);
6852
+ if (a === b) return 1;
6853
+ const compatible = [
6854
+ ["string", "str"],
6855
+ ["number", "int"],
6856
+ ["number", "float"],
6857
+ ["number", "double"],
6858
+ ["boolean", "bool"],
6859
+ ["void", "unit"],
6860
+ ["void", "none"]
6861
+ ];
6862
+ for (const [x, y] of compatible) {
6863
+ if (a === x && b === y || a === y && b === x) return 0.8;
6864
+ }
6865
+ return 0;
6866
+ }
6867
+ function paramCountSimilarity(countA, countB) {
6868
+ const maxCount = Math.max(countA, countB, 1);
6869
+ return 1 - Math.abs(countA - countB) / maxCount;
6870
+ }
6871
+ function computeContractSimilarity(a, b, nameSim) {
6872
+ const paramsA = a.parameters ?? [];
6873
+ const paramsB = b.parameters ?? [];
6874
+ const ptSim = paramTypeSimilarity(paramsA, paramsB);
6875
+ const rtSim = returnTypeSimilarity(a.returnType, b.returnType);
6876
+ const pcSim = paramCountSimilarity(paramsA.length, paramsB.length);
6877
+ let score = 0.4 * nameSim + 0.3 * ptSim + 0.2 * rtSim + 0.1 * pcSim;
6878
+ if (ptSim > 0.8) {
6879
+ score = Math.min(1, score * 1.2);
6880
+ }
6881
+ return score;
6882
+ }
6883
+
6884
+ // src/multi-repo/group-sync.ts
6675
6885
  function extractContracts(graph, repoName, repoPath) {
6676
6886
  const contracts = [];
6677
6887
  for (const node of graph.allNodes()) {
@@ -6684,7 +6894,10 @@ function extractContracts(graph, repoName, repoPath) {
6684
6894
  nodeId: node.id,
6685
6895
  nodeKind: node.kind,
6686
6896
  filePath: node.filePath,
6687
- signature: node.content?.split("\n")[0]?.trim()
6897
+ signature: node.content?.split("\n")[0]?.trim(),
6898
+ parameters: node.metadata?.parameters ?? node.metadata?.params,
6899
+ returnType: node.metadata?.returnType,
6900
+ exported: node.exported
6688
6901
  });
6689
6902
  }
6690
6903
  if (node.kind === "route") {
@@ -6746,13 +6959,15 @@ function matchContracts(allContracts) {
6746
6959
  const consumer = consumerByName.get(provider.name);
6747
6960
  if (consumer) {
6748
6961
  const sameKind = provider.kind === consumer.kind;
6962
+ const typedScore = computeContractSimilarity(provider, consumer, 1);
6963
+ const confidence = sameKind ? Math.max(typedScore, 0.9) : Math.max(typedScore, 0.6);
6749
6964
  links.push({
6750
6965
  providerRepo: provider.repoName,
6751
6966
  providerContract: provider.name,
6752
6967
  consumerRepo: consumer.repoName,
6753
6968
  consumerContract: consumer.name,
6754
6969
  matchKind: provider.kind === "route" ? "route-match" : "name-match",
6755
- confidence: sameKind ? 0.9 : 0.6
6970
+ confidence: Math.min(1, confidence)
6756
6971
  });
6757
6972
  } else {
6758
6973
  const providerLC = provider.name.toLowerCase();
@@ -6793,8 +7008,8 @@ async function syncGroup(group) {
6793
7008
  logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
6794
7009
  continue;
6795
7010
  }
6796
- const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
6797
- if (!fs19.existsSync(dbPath)) {
7011
+ const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
7012
+ if (!fs23.existsSync(dbPath)) {
6798
7013
  logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
6799
7014
  continue;
6800
7015
  }
@@ -6810,6 +7025,44 @@ async function syncGroup(group) {
6810
7025
  continue;
6811
7026
  }
6812
7027
  const contracts = extractContracts(graph, member.registryName, regEntry.path);
7028
+ const [openapiContracts, graphqlContracts, protoContracts] = await Promise.all([
7029
+ parseOpenAPIContracts(regEntry.path).catch(() => []),
7030
+ parseGraphQLContracts(regEntry.path).catch(() => []),
7031
+ parseProtoContracts(regEntry.path).catch(() => [])
7032
+ ]);
7033
+ for (const c of openapiContracts) {
7034
+ contracts.push({
7035
+ repoName: member.registryName,
7036
+ repoPath: regEntry.path,
7037
+ kind: "route",
7038
+ name: c.name,
7039
+ nodeId: `openapi:${c.method}:${c.path}`,
7040
+ nodeKind: "route",
7041
+ filePath: c.filePath
7042
+ });
7043
+ }
7044
+ for (const c of graphqlContracts) {
7045
+ contracts.push({
7046
+ repoName: member.registryName,
7047
+ repoPath: regEntry.path,
7048
+ kind: "graphql",
7049
+ name: c.name,
7050
+ nodeId: `graphql:${c.name}`,
7051
+ nodeKind: "graphql",
7052
+ filePath: c.filePath
7053
+ });
7054
+ }
7055
+ for (const c of protoContracts) {
7056
+ contracts.push({
7057
+ repoName: member.registryName,
7058
+ repoPath: regEntry.path,
7059
+ kind: "grpc",
7060
+ name: c.name,
7061
+ nodeId: `grpc:${c.serviceName}:${c.rpcName}`,
7062
+ nodeKind: "grpc",
7063
+ filePath: c.filePath
7064
+ });
7065
+ }
6813
7066
  logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
6814
7067
  allContracts.push(...contracts);
6815
7068
  }
@@ -6829,8 +7082,8 @@ async function queryGroup(group, query, limit = 20) {
6829
7082
  for (const member of group.members) {
6830
7083
  const regEntry = registry.find((r) => r.name === member.registryName);
6831
7084
  if (!regEntry) continue;
6832
- const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
6833
- if (!fs19.existsSync(dbPath)) continue;
7085
+ const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
7086
+ if (!fs23.existsSync(dbPath)) continue;
6834
7087
  const graph = createKnowledgeGraph();
6835
7088
  const db = new DbManager(dbPath);
6836
7089
  try {
@@ -6860,6 +7113,624 @@ async function queryGroup(group, query, limit = 20) {
6860
7113
 
6861
7114
  // src/mcp-server/server.ts
6862
7115
  init_tracing();
7116
+
7117
+ // src/query/explain-relationship.ts
7118
+ function explainRelationship(graph, from, to) {
7119
+ const allNodes = [...graph.allNodes()];
7120
+ const fromNode = allNodes.find((n) => n.name === from);
7121
+ if (!fromNode) {
7122
+ const firstChar = from[0]?.toLowerCase() ?? "";
7123
+ const fromLower = from.toLowerCase();
7124
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
7125
+ return { error: `Symbol not found: ${from}`, suggestions };
7126
+ }
7127
+ const toNode = allNodes.find((n) => n.name === to);
7128
+ if (!toNode) {
7129
+ const firstChar = to[0]?.toLowerCase() ?? "";
7130
+ const toLower = to.toLowerCase();
7131
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
7132
+ return { error: `Symbol not found: ${to}`, suggestions };
7133
+ }
7134
+ const paths = [];
7135
+ const queue = [{
7136
+ id: fromNode.id,
7137
+ nodeNames: [fromNode.name],
7138
+ lastEdgeKind: "",
7139
+ visited: /* @__PURE__ */ new Set([fromNode.id])
7140
+ }];
7141
+ while (queue.length > 0 && paths.length < 10) {
7142
+ const entry = queue.shift();
7143
+ const { id, nodeNames, visited } = entry;
7144
+ if (nodeNames.length > 6) continue;
7145
+ for (const edge of graph.findEdgesFrom(id)) {
7146
+ const targetNode = graph.getNode(edge.target);
7147
+ if (!targetNode) continue;
7148
+ if (visited.has(edge.target)) continue;
7149
+ const newNames = [...nodeNames, targetNode.name];
7150
+ if (edge.target === toNode.id) {
7151
+ paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
7152
+ if (paths.length >= 10) break;
7153
+ continue;
7154
+ }
7155
+ if (newNames.length < 6) {
7156
+ const newVisited = new Set(visited);
7157
+ newVisited.add(edge.target);
7158
+ queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
7159
+ }
7160
+ }
7161
+ }
7162
+ const fromImports = /* @__PURE__ */ new Set();
7163
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
7164
+ if (edge.kind === "imports") fromImports.add(edge.target);
7165
+ }
7166
+ const sharedImportIds = [];
7167
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
7168
+ if (edge.kind === "imports" && fromImports.has(edge.target)) {
7169
+ sharedImportIds.push(edge.target);
7170
+ }
7171
+ }
7172
+ const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
7173
+ let heritage = null;
7174
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
7175
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
7176
+ heritage = `${from} ${edge.kind} ${to}`;
7177
+ break;
7178
+ }
7179
+ }
7180
+ if (!heritage) {
7181
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
7182
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
7183
+ heritage = `${to} ${edge.kind} ${from}`;
7184
+ break;
7185
+ }
7186
+ }
7187
+ }
7188
+ const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
7189
+ const heritageStr = heritage ?? "none";
7190
+ const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
7191
+ const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
7192
+ return { paths, sharedImports, heritage, summary };
7193
+ }
7194
+
7195
+ // src/query/pr-impact.ts
7196
+ function parseDiffFiles(diff) {
7197
+ const files = [];
7198
+ for (const line of diff.split("\n")) {
7199
+ const match = line.match(/^\+\+\+ b\/(.+)/);
7200
+ if (match) {
7201
+ files.push(match[1]);
7202
+ }
7203
+ }
7204
+ return files;
7205
+ }
7206
+ function computePRImpact(graph, changedFiles, maxHops) {
7207
+ const changedSymbolIds = /* @__PURE__ */ new Set();
7208
+ for (const node of graph.allNodes()) {
7209
+ if (!node.filePath) continue;
7210
+ for (const changedFile of changedFiles) {
7211
+ if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
7212
+ changedSymbolIds.add(node.id);
7213
+ break;
7214
+ }
7215
+ }
7216
+ }
7217
+ const allBlastRadiusNodes = /* @__PURE__ */ new Set();
7218
+ const changedSymbols = [];
7219
+ for (const symbolId of changedSymbolIds) {
7220
+ const symbolNode = graph.getNode(symbolId);
7221
+ if (!symbolNode) continue;
7222
+ const blastRadius = /* @__PURE__ */ new Set();
7223
+ const queue = [{ id: symbolId, depth: 0 }];
7224
+ const visited = /* @__PURE__ */ new Set();
7225
+ while (queue.length > 0) {
7226
+ const { id, depth } = queue.shift();
7227
+ if (visited.has(id) || depth > maxHops) continue;
7228
+ visited.add(id);
7229
+ if (id !== symbolId) blastRadius.add(id);
7230
+ for (const edge of graph.findEdgesTo(id)) {
7231
+ if (edge.kind === "calls" || edge.kind === "imports") {
7232
+ queue.push({ id: edge.source, depth: depth + 1 });
7233
+ }
7234
+ }
7235
+ }
7236
+ for (const id of blastRadius) allBlastRadiusNodes.add(id);
7237
+ const blastCount = blastRadius.size;
7238
+ let risk;
7239
+ if (blastCount > 50) {
7240
+ risk = "HIGH";
7241
+ } else if (blastCount >= 10) {
7242
+ risk = "MEDIUM";
7243
+ } else {
7244
+ risk = "LOW";
7245
+ }
7246
+ let callerCount = 0;
7247
+ for (const edge of graph.findEdgesTo(symbolId)) {
7248
+ if (edge.kind === "calls") callerCount++;
7249
+ }
7250
+ let testCoverage = false;
7251
+ for (const edge of graph.findEdgesTo(symbolId)) {
7252
+ if (edge.kind === "imports") {
7253
+ const callerNode = graph.getNode(edge.source);
7254
+ if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
7255
+ testCoverage = true;
7256
+ break;
7257
+ }
7258
+ }
7259
+ }
7260
+ changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
7261
+ }
7262
+ const impactedSymbols = [];
7263
+ for (const id of allBlastRadiusNodes) {
7264
+ if (changedSymbolIds.has(id)) continue;
7265
+ const node = graph.getNode(id);
7266
+ if (node) {
7267
+ impactedSymbols.push({ name: node.name, filePath: node.filePath });
7268
+ }
7269
+ }
7270
+ const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
7271
+ for (const s of changedSymbols) {
7272
+ riskSummary[s.risk]++;
7273
+ }
7274
+ const coverageGaps = [];
7275
+ for (const s of changedSymbols) {
7276
+ if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
7277
+ coverageGaps.push(`${s.name} has no test coverage`);
7278
+ }
7279
+ }
7280
+ const fileImpactCount = /* @__PURE__ */ new Map();
7281
+ for (const sym of impactedSymbols) {
7282
+ if (sym.filePath) {
7283
+ fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
7284
+ }
7285
+ }
7286
+ const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
7287
+ return {
7288
+ changedSymbols,
7289
+ impactedSymbols,
7290
+ riskSummary,
7291
+ coverageGaps,
7292
+ filesToReview,
7293
+ crossRepoImpact: null
7294
+ };
7295
+ }
7296
+
7297
+ // src/query/similar-symbols.ts
7298
+ function levenshtein(a, b) {
7299
+ const m = a.length;
7300
+ const n = b.length;
7301
+ const dp = Array.from(
7302
+ { length: m + 1 },
7303
+ (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
7304
+ );
7305
+ for (let i = 1; i <= m; i++) {
7306
+ for (let j = 1; j <= n; j++) {
7307
+ if (a[i - 1] === b[j - 1]) {
7308
+ dp[i][j] = dp[i - 1][j - 1];
7309
+ } else {
7310
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
7311
+ }
7312
+ }
7313
+ }
7314
+ return dp[m][n];
7315
+ }
7316
+ function findSimilarSymbols(graph, symbolName, limit) {
7317
+ const clampedLimit = Math.min(Math.max(1, limit), 50);
7318
+ const allNodes = [...graph.allNodes()];
7319
+ const targetNode = allNodes.find((n) => n.name === symbolName);
7320
+ if (!targetNode) {
7321
+ return { similar: [] };
7322
+ }
7323
+ let targetCluster = null;
7324
+ for (const edge of graph.findEdgesFrom(targetNode.id)) {
7325
+ if (edge.kind === "belongs_to") {
7326
+ const clusterNode = graph.getNode(edge.target);
7327
+ if (clusterNode) {
7328
+ targetCluster = clusterNode.name;
7329
+ break;
7330
+ }
7331
+ }
7332
+ }
7333
+ if (!targetCluster) {
7334
+ for (const edge of graph.findEdgesTo(targetNode.id)) {
7335
+ if (edge.kind === "belongs_to") {
7336
+ const clusterNode = graph.getNode(edge.source);
7337
+ if (clusterNode) {
7338
+ targetCluster = clusterNode.name;
7339
+ break;
7340
+ }
7341
+ }
7342
+ }
7343
+ }
7344
+ const results = [];
7345
+ for (const node of allNodes) {
7346
+ if (node.id === targetNode.id) continue;
7347
+ const maxLen = Math.max(symbolName.length, node.name.length);
7348
+ const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
7349
+ const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
7350
+ const combined = 0.5 * nameSim + 0.5 * structuralSim;
7351
+ const reasons = [];
7352
+ if (nameSim >= 0.6) reasons.push("similar name");
7353
+ if (node.kind === targetNode.kind) reasons.push("same kind");
7354
+ if (targetCluster !== null) {
7355
+ let nodeCluster = null;
7356
+ for (const edge of graph.findEdgesFrom(node.id)) {
7357
+ if (edge.kind === "belongs_to") {
7358
+ const clusterNode = graph.getNode(edge.target);
7359
+ if (clusterNode) {
7360
+ nodeCluster = clusterNode.name;
7361
+ break;
7362
+ }
7363
+ }
7364
+ }
7365
+ if (!nodeCluster) {
7366
+ for (const edge of graph.findEdgesTo(node.id)) {
7367
+ if (edge.kind === "belongs_to") {
7368
+ const clusterNode = graph.getNode(edge.source);
7369
+ if (clusterNode) {
7370
+ nodeCluster = clusterNode.name;
7371
+ break;
7372
+ }
7373
+ }
7374
+ }
7375
+ }
7376
+ if (nodeCluster !== null && nodeCluster === targetCluster) {
7377
+ reasons.push("same module");
7378
+ }
7379
+ }
7380
+ if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
7381
+ if (!reasons.includes("same module")) reasons.push("same module");
7382
+ }
7383
+ results.push({ name: node.name, similarity: combined, reasons });
7384
+ }
7385
+ results.sort((a, b) => b.similarity - a.similarity);
7386
+ return { similar: results.slice(0, clampedLimit) };
7387
+ }
7388
+
7389
+ // src/query/health-report.ts
7390
+ function computeHealthReport(graph, scope) {
7391
+ const wholeRepo = scope === ".";
7392
+ function inScope(filePath) {
7393
+ if (wholeRepo) return true;
7394
+ return filePath.startsWith(scope) || filePath.includes(scope);
7395
+ }
7396
+ const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
7397
+ const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
7398
+ const deadCode = [];
7399
+ for (const node of scopedNodes) {
7400
+ if (!deadCodeKinds.has(node.kind)) continue;
7401
+ if (node.exported === true) continue;
7402
+ let hasIncoming = false;
7403
+ for (const _edge of graph.findEdgesTo(node.id)) {
7404
+ hasIncoming = true;
7405
+ break;
7406
+ }
7407
+ if (!hasIncoming) {
7408
+ deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
7409
+ if (deadCode.length >= 20) break;
7410
+ }
7411
+ }
7412
+ const cycles = [];
7413
+ const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
7414
+ const importAdj = /* @__PURE__ */ new Map();
7415
+ for (const node of scopedNodes) {
7416
+ importAdj.set(node.id, []);
7417
+ }
7418
+ for (const edge of graph.findEdgesByKind("imports")) {
7419
+ if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
7420
+ importAdj.get(edge.source).push(edge.target);
7421
+ }
7422
+ }
7423
+ const visited = /* @__PURE__ */ new Set();
7424
+ const inStack = /* @__PURE__ */ new Set();
7425
+ const stackPath = [];
7426
+ function dfs(nodeId) {
7427
+ if (cycles.length >= 5) return;
7428
+ visited.add(nodeId);
7429
+ inStack.add(nodeId);
7430
+ stackPath.push(nodeId);
7431
+ for (const neighborId of importAdj.get(nodeId) ?? []) {
7432
+ if (cycles.length >= 5) break;
7433
+ if (inStack.has(neighborId)) {
7434
+ const cycleStart = stackPath.indexOf(neighborId);
7435
+ const cyclePath = stackPath.slice(cycleStart).map((id) => {
7436
+ const node = graph.getNode(id);
7437
+ return node ? node.name : id;
7438
+ });
7439
+ cycles.push(cyclePath);
7440
+ } else if (!visited.has(neighborId)) {
7441
+ dfs(neighborId);
7442
+ }
7443
+ }
7444
+ stackPath.pop();
7445
+ inStack.delete(nodeId);
7446
+ }
7447
+ for (const node of scopedNodes) {
7448
+ if (cycles.length >= 5) break;
7449
+ if (!visited.has(node.id)) {
7450
+ dfs(node.id);
7451
+ }
7452
+ }
7453
+ const godNodes = [];
7454
+ for (const node of scopedNodes) {
7455
+ let edgeCount = 0;
7456
+ for (const _edge of graph.findEdgesFrom(node.id)) {
7457
+ edgeCount++;
7458
+ }
7459
+ if (edgeCount > 10) {
7460
+ godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
7461
+ }
7462
+ }
7463
+ godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
7464
+ godNodes.splice(10);
7465
+ const filePathToNodes = /* @__PURE__ */ new Map();
7466
+ for (const node of scopedNodes) {
7467
+ if (!node.filePath) continue;
7468
+ let arr = filePathToNodes.get(node.filePath);
7469
+ if (!arr) {
7470
+ arr = [];
7471
+ filePathToNodes.set(node.filePath, arr);
7472
+ }
7473
+ arr.push(node.id);
7474
+ }
7475
+ const orphanFiles = [];
7476
+ for (const [filePath, nodeIds] of filePathToNodes) {
7477
+ if (orphanFiles.length >= 10) break;
7478
+ let hasAnyEdge = false;
7479
+ for (const nodeId of nodeIds) {
7480
+ let hasOut = false;
7481
+ for (const _edge of graph.findEdgesFrom(nodeId)) {
7482
+ hasOut = true;
7483
+ break;
7484
+ }
7485
+ let hasIn = false;
7486
+ for (const _edge of graph.findEdgesTo(nodeId)) {
7487
+ hasIn = true;
7488
+ break;
7489
+ }
7490
+ if (hasOut || hasIn) {
7491
+ hasAnyEdge = true;
7492
+ break;
7493
+ }
7494
+ }
7495
+ if (!hasAnyEdge) {
7496
+ orphanFiles.push(filePath);
7497
+ }
7498
+ }
7499
+ const hotspotCandidates = [];
7500
+ for (const node of scopedNodes) {
7501
+ const visitedBfs = /* @__PURE__ */ new Set();
7502
+ const queue = [{ id: node.id, depth: 0 }];
7503
+ while (queue.length > 0) {
7504
+ const item = queue.shift();
7505
+ if (item.depth > 5 || visitedBfs.has(item.id)) continue;
7506
+ visitedBfs.add(item.id);
7507
+ for (const edge of graph.findEdgesTo(item.id)) {
7508
+ if (edge.kind === "calls" || edge.kind === "imports") {
7509
+ if (!visitedBfs.has(edge.source)) {
7510
+ queue.push({ id: edge.source, depth: item.depth + 1 });
7511
+ }
7512
+ }
7513
+ }
7514
+ }
7515
+ const blastRadius = visitedBfs.size - 1;
7516
+ hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
7517
+ }
7518
+ hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
7519
+ const complexityHotspots = hotspotCandidates.slice(0, 5);
7520
+ const healthScore = Math.max(
7521
+ 0,
7522
+ Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
7523
+ );
7524
+ return {
7525
+ healthScore,
7526
+ deadCode,
7527
+ cycles,
7528
+ godNodes,
7529
+ orphanFiles,
7530
+ complexityHotspots
7531
+ };
7532
+ }
7533
+
7534
+ // src/query/suggest-tests.ts
7535
+ function getSuggestedCases(symbolName) {
7536
+ const lower = symbolName.toLowerCase();
7537
+ if (/parse|validate|check|verify/.test(lower)) {
7538
+ return [
7539
+ "Valid input \u2192 success",
7540
+ "Invalid input \u2192 throws error",
7541
+ "Edge case: empty/null input \u2192 handled gracefully"
7542
+ ];
7543
+ }
7544
+ if (/create|add|insert|save/.test(lower)) {
7545
+ return [
7546
+ "Success: valid data \u2192 created",
7547
+ "Duplicate: existing item \u2192 error or no-op",
7548
+ "Missing required fields \u2192 validation error"
7549
+ ];
7550
+ }
7551
+ if (/delete|remove|destroy/.test(lower)) {
7552
+ return [
7553
+ "Existing item \u2192 deleted successfully",
7554
+ "Non-existent item \u2192 no error or 404",
7555
+ "Unauthorized access \u2192 rejected"
7556
+ ];
7557
+ }
7558
+ if (/get|find|fetch|load/.test(lower)) {
7559
+ return [
7560
+ "Found: returns correct data",
7561
+ "Not found: returns null or throws",
7562
+ "Empty collection: returns []"
7563
+ ];
7564
+ }
7565
+ return [
7566
+ "Happy path: valid input \u2192 expected output",
7567
+ "Error case: invalid input \u2192 error handled",
7568
+ "Edge case: boundary values \u2192 correct behavior"
7569
+ ];
7570
+ }
7571
+ function suggestTests(graph, symbolName) {
7572
+ let targetNode = void 0;
7573
+ for (const node of graph.allNodes()) {
7574
+ if (node.name === symbolName) {
7575
+ targetNode = node;
7576
+ break;
7577
+ }
7578
+ }
7579
+ if (!targetNode) {
7580
+ return { error: `Symbol not found: ${symbolName}` };
7581
+ }
7582
+ const targetId = targetNode.id;
7583
+ const callPaths = [];
7584
+ const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
7585
+ while (pathQueue.length > 0 && callPaths.length < 5) {
7586
+ const { id, path: path30, depth } = pathQueue.shift();
7587
+ let hasCallers = false;
7588
+ for (const edge of graph.findEdgesTo(id)) {
7589
+ if (edge.kind !== "calls") continue;
7590
+ const callerNode = graph.getNode(edge.source);
7591
+ if (!callerNode) continue;
7592
+ hasCallers = true;
7593
+ const newPath = [callerNode.name, ...path30];
7594
+ if (depth + 1 >= 3 || callPaths.length >= 5) {
7595
+ if (callPaths.length < 5) callPaths.push(newPath);
7596
+ continue;
7597
+ }
7598
+ pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
7599
+ }
7600
+ if (!hasCallers && path30.length > 1) {
7601
+ callPaths.push(path30);
7602
+ }
7603
+ }
7604
+ if (callPaths.length === 0) {
7605
+ for (const edge of graph.findEdgesTo(targetId)) {
7606
+ if (edge.kind !== "calls") continue;
7607
+ const callerNode = graph.getNode(edge.source);
7608
+ if (!callerNode) continue;
7609
+ callPaths.push([callerNode.name, symbolName]);
7610
+ if (callPaths.length >= 5) break;
7611
+ }
7612
+ }
7613
+ const existingTestFiles = /* @__PURE__ */ new Set();
7614
+ for (const edge of graph.findEdgesTo(targetId)) {
7615
+ if (edge.kind !== "imports") continue;
7616
+ const importerNode = graph.getNode(edge.source);
7617
+ if (!importerNode) continue;
7618
+ if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
7619
+ existingTestFiles.add(importerNode.filePath);
7620
+ }
7621
+ }
7622
+ const existingTests = [...existingTestFiles];
7623
+ const untestedCallers = [];
7624
+ for (const edge of graph.findEdgesTo(targetId)) {
7625
+ if (edge.kind !== "calls") continue;
7626
+ const callerNode = graph.getNode(edge.source);
7627
+ if (!callerNode) continue;
7628
+ if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
7629
+ continue;
7630
+ }
7631
+ let callerHasTest = false;
7632
+ for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
7633
+ if (callerImportEdge.kind !== "imports") continue;
7634
+ const importerOfCaller = graph.getNode(callerImportEdge.source);
7635
+ if (!importerOfCaller) continue;
7636
+ if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
7637
+ callerHasTest = true;
7638
+ break;
7639
+ }
7640
+ }
7641
+ if (!callerHasTest) {
7642
+ untestedCallers.push(callerNode.name);
7643
+ }
7644
+ }
7645
+ const suggestedCases = getSuggestedCases(symbolName);
7646
+ return {
7647
+ callPaths,
7648
+ suggestedCases,
7649
+ existingTests,
7650
+ untestedCallers
7651
+ };
7652
+ }
7653
+
7654
+ // src/query/cluster-summary.ts
7655
+ function getPathPrefix(filePath) {
7656
+ const parts = filePath.replace(/\\/g, "/").split("/");
7657
+ return parts.slice(0, 2).join("/");
7658
+ }
7659
+ function summarizeCluster(graph, cluster) {
7660
+ const clusterNodes = [...graph.allNodes()].filter(
7661
+ (n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
7662
+ );
7663
+ if (clusterNodes.length === 0) {
7664
+ return { error: `Cluster not found: ${cluster}` };
7665
+ }
7666
+ const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
7667
+ const callerCountMap = /* @__PURE__ */ new Map();
7668
+ for (const node of clusterNodes) {
7669
+ let count = 0;
7670
+ for (const _edge of graph.findEdgesTo(node.id)) {
7671
+ count++;
7672
+ }
7673
+ callerCountMap.set(node.id, count);
7674
+ }
7675
+ const sortedByCallers = [...clusterNodes].sort(
7676
+ (a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
7677
+ );
7678
+ const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
7679
+ name: n.name,
7680
+ callerCount: callerCountMap.get(n.id) ?? 0
7681
+ }));
7682
+ const depsSet = /* @__PURE__ */ new Set();
7683
+ for (const node of clusterNodes) {
7684
+ for (const edge of graph.findEdgesFrom(node.id)) {
7685
+ if (edge.kind !== "imports") continue;
7686
+ const targetNode = graph.getNode(edge.target);
7687
+ if (!targetNode) continue;
7688
+ if (!clusterNodeIds.has(targetNode.id)) {
7689
+ const prefix = getPathPrefix(targetNode.filePath);
7690
+ depsSet.add(prefix);
7691
+ }
7692
+ }
7693
+ }
7694
+ const dependencies = [...depsSet];
7695
+ const dependentsSet = /* @__PURE__ */ new Set();
7696
+ for (const node of clusterNodes) {
7697
+ for (const edge of graph.findEdgesTo(node.id)) {
7698
+ if (edge.kind !== "imports") continue;
7699
+ const sourceNode = graph.getNode(edge.source);
7700
+ if (!sourceNode) continue;
7701
+ if (!clusterNodeIds.has(sourceNode.id)) {
7702
+ const prefix = getPathPrefix(sourceNode.filePath);
7703
+ dependentsSet.add(prefix);
7704
+ }
7705
+ }
7706
+ }
7707
+ const dependents = [...dependentsSet];
7708
+ const healthResult = computeHealthReport(graph, cluster);
7709
+ const health = { score: healthResult.healthScore };
7710
+ const symbolCount = {};
7711
+ for (const node of clusterNodes) {
7712
+ symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
7713
+ }
7714
+ let purpose;
7715
+ const topNode = sortedByCallers[0];
7716
+ if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
7717
+ purpose = topNode.metadata["summary"];
7718
+ } else {
7719
+ const clusterName = cluster.split("/").pop() ?? cluster;
7720
+ purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
7721
+ }
7722
+ return {
7723
+ cluster,
7724
+ purpose,
7725
+ keySymbols,
7726
+ dependencies,
7727
+ dependents,
7728
+ health,
7729
+ symbolCount
7730
+ };
7731
+ }
7732
+
7733
+ // src/mcp-server/server.ts
6863
7734
  function createMcpServer(graph, repoName, workspaceRoot) {
6864
7735
  const server = new Server(
6865
7736
  { name: "code-intel", version: "0.1.0" },
@@ -6889,7 +7760,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6889
7760
  type: "object",
6890
7761
  properties: {
6891
7762
  query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
6892
- limit: { type: "number", description: "Max results to return (default: 20)" },
7763
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7764
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
6893
7765
  ..._tokenProp
6894
7766
  },
6895
7767
  required: ["query"]
@@ -6932,6 +7804,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6932
7804
  type: "object",
6933
7805
  properties: {
6934
7806
  file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
7807
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7808
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
6935
7809
  ..._tokenProp
6936
7810
  },
6937
7811
  required: ["file_path"]
@@ -6961,7 +7835,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6961
7835
  type: "string",
6962
7836
  description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
6963
7837
  },
6964
- limit: { type: "number", description: "Max results (default: 100)" },
7838
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7839
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
6965
7840
  ..._tokenProp
6966
7841
  }
6967
7842
  }
@@ -6978,7 +7853,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6978
7853
  inputSchema: {
6979
7854
  type: "object",
6980
7855
  properties: {
6981
- limit: { type: "number", description: "Max clusters to return (default: 50)" },
7856
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7857
+ limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
6982
7858
  ..._tokenProp
6983
7859
  }
6984
7860
  }
@@ -6989,7 +7865,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6989
7865
  inputSchema: {
6990
7866
  type: "object",
6991
7867
  properties: {
6992
- limit: { type: "number", description: "Max flows to return (default: 50)" },
7868
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7869
+ limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
6993
7870
  ..._tokenProp
6994
7871
  }
6995
7872
  }
@@ -7111,6 +7988,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
7111
7988
  },
7112
7989
  required: ["name"]
7113
7990
  }
7991
+ },
7992
+ // ── Reasoning / analysis tools ────────────────────────────────────────
7993
+ {
7994
+ name: "explain_relationship",
7995
+ description: "Explain how two symbols are connected: directed paths, shared imports, and heritage (extends/implements). Returns up to 10 paths with at most 5 hops each.",
7996
+ inputSchema: {
7997
+ type: "object",
7998
+ properties: {
7999
+ from: { type: "string", description: "Source symbol name" },
8000
+ to: { type: "string", description: "Target symbol name" },
8001
+ ..._tokenProp
8002
+ },
8003
+ required: ["from", "to"]
8004
+ }
8005
+ },
8006
+ {
8007
+ name: "pr_impact",
8008
+ description: "Given changed files or a unified diff, compute full blast radius with risk scores (HIGH/MEDIUM/LOW), test coverage gaps, and top files to review.",
8009
+ inputSchema: {
8010
+ type: "object",
8011
+ properties: {
8012
+ changedFiles: {
8013
+ type: "array",
8014
+ items: { type: "string" },
8015
+ description: "List of changed file paths (relative or absolute)"
8016
+ },
8017
+ diff: {
8018
+ type: "string",
8019
+ description: "Raw unified diff text. Changed files are extracted automatically."
8020
+ },
8021
+ maxHops: {
8022
+ type: "number",
8023
+ description: "Maximum BFS depth for blast radius (default: 5)"
8024
+ },
8025
+ ..._tokenProp
8026
+ }
8027
+ }
8028
+ },
8029
+ {
8030
+ name: "similar_symbols",
8031
+ description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
8032
+ inputSchema: {
8033
+ type: "object",
8034
+ properties: {
8035
+ symbol: { type: "string", description: "Symbol name to find similar symbols for" },
8036
+ limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
8037
+ ..._tokenProp
8038
+ },
8039
+ required: ["symbol"]
8040
+ }
8041
+ },
8042
+ {
8043
+ name: "health_report",
8044
+ description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
8045
+ inputSchema: {
8046
+ type: "object",
8047
+ properties: {
8048
+ scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
8049
+ ..._tokenProp
8050
+ }
8051
+ }
8052
+ },
8053
+ {
8054
+ name: "suggest_tests",
8055
+ description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
8056
+ inputSchema: {
8057
+ type: "object",
8058
+ properties: {
8059
+ symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
8060
+ ..._tokenProp
8061
+ },
8062
+ required: ["symbol"]
8063
+ }
8064
+ },
8065
+ {
8066
+ name: "cluster_summary",
8067
+ description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
8068
+ inputSchema: {
8069
+ type: "object",
8070
+ properties: {
8071
+ cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
8072
+ ..._tokenProp
8073
+ },
8074
+ required: ["cluster"]
8075
+ }
7114
8076
  }
7115
8077
  ]
7116
8078
  }));
@@ -7181,8 +8143,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7181
8143
  for (const edge of graph.allEdges()) {
7182
8144
  edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
7183
8145
  }
7184
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
7185
- const healthReport = computeHealthReport2(graph);
8146
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
8147
+ const healthReport = computeHealthReport3(graph);
7186
8148
  const health = {
7187
8149
  score: Math.round(healthReport.score),
7188
8150
  grade: healthReport.grade,
@@ -7207,10 +8169,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7207
8169
  // ── search ─────────────────────────────────────────────────────────────
7208
8170
  case "search": {
7209
8171
  const query = a.query;
7210
- const limit = a.limit ?? 20;
8172
+ const offset = a.offset ?? 0;
8173
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
7211
8174
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
7212
- const { results, searchMode } = await hybridSearch(graph, query, limit, { vectorDbPath: vdbPath });
7213
- return { content: [{ type: "text", text: JSON.stringify({ results, searchMode, suggested_next_tools: ["inspect", "query", "blast_radius"] }, null, 2) }] };
8175
+ const fetchLimit = Math.min(offset + effectiveLimit, 500);
8176
+ const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
8177
+ const total = allResults.length;
8178
+ const results = allResults.slice(offset, offset + effectiveLimit);
8179
+ const hasMore = offset + effectiveLimit < total;
8180
+ const suggestNextTools = [];
8181
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
8182
+ if (suggestEnabled && results.length > 0) {
8183
+ const topName = results[0].name;
8184
+ suggestNextTools.push(
8185
+ { tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
8186
+ { tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
8187
+ );
8188
+ }
8189
+ return {
8190
+ content: [{
8191
+ type: "text",
8192
+ text: JSON.stringify({
8193
+ results,
8194
+ searchMode,
8195
+ total,
8196
+ offset,
8197
+ limit: effectiveLimit,
8198
+ hasMore,
8199
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
8200
+ }, null, 2)
8201
+ }]
8202
+ };
7214
8203
  }
7215
8204
  // ── inspect ────────────────────────────────────────────────────────────
7216
8205
  case "inspect": {
@@ -7219,6 +8208,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7219
8208
  if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
7220
8209
  const incoming = [...graph.findEdgesTo(node.id)];
7221
8210
  const outgoing = [...graph.findEdgesFrom(node.id)];
8211
+ const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
8212
+ id: e.source,
8213
+ name: graph.getNode(e.source)?.name,
8214
+ file: graph.getNode(e.source)?.filePath
8215
+ }));
8216
+ const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
8217
+ id: e.target,
8218
+ name: graph.getNode(e.target)?.name,
8219
+ file: graph.getNode(e.target)?.filePath
8220
+ }));
8221
+ const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
8222
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
8223
+ const suggestNextTools = [];
8224
+ if (suggestEnabled) {
8225
+ const topCallerName = callers[0]?.name;
8226
+ suggestNextTools.push(
8227
+ ...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
8228
+ ...cluster ? [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster } }] : [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster: node.filePath } }]
8229
+ );
8230
+ }
7222
8231
  return {
7223
8232
  content: [{
7224
8233
  type: "text",
@@ -7232,16 +8241,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7232
8241
  endLine: node.endLine,
7233
8242
  exported: node.exported
7234
8243
  },
7235
- callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
7236
- id: e.source,
7237
- name: graph.getNode(e.source)?.name,
7238
- file: graph.getNode(e.source)?.filePath
7239
- })),
7240
- callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
7241
- id: e.target,
7242
- name: graph.getNode(e.target)?.name,
7243
- file: graph.getNode(e.target)?.filePath
7244
- })),
8244
+ callers,
8245
+ callees,
7245
8246
  imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
7246
8247
  importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
7247
8248
  extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
@@ -7250,8 +8251,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7250
8251
  name: graph.getNode(e.target)?.name,
7251
8252
  kind: graph.getNode(e.target)?.kind
7252
8253
  })),
7253
- cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
7254
- content: node.content?.slice(0, 500)
8254
+ cluster,
8255
+ content: node.content?.slice(0, 500),
8256
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
7255
8257
  }, null, 2)
7256
8258
  }]
7257
8259
  };
@@ -7287,6 +8289,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7287
8289
  return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
7288
8290
  });
7289
8291
  const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
8292
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
8293
+ const suggestNextTools = [];
8294
+ if (suggestEnabled) {
8295
+ const highestRiskSymbol = node.name;
8296
+ const firstFilePath = affectedDetails[0]?.filePath ?? "";
8297
+ suggestNextTools.push(
8298
+ { tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
8299
+ { tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
8300
+ );
8301
+ }
7290
8302
  return {
7291
8303
  content: [{
7292
8304
  type: "text",
@@ -7294,7 +8306,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7294
8306
  target: node.name,
7295
8307
  affectedCount: affected.size,
7296
8308
  riskLevel: risk,
7297
- affected: affectedDetails
8309
+ affected: affectedDetails,
8310
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
7298
8311
  }, null, 2)
7299
8312
  }]
7300
8313
  };
@@ -7302,17 +8315,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7302
8315
  // ── file_symbols ───────────────────────────────────────────────────────
7303
8316
  case "file_symbols": {
7304
8317
  const filePath = a.file_path;
7305
- const matches = [];
8318
+ const offset = a.offset ?? 0;
8319
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8320
+ const allMatches = [];
7306
8321
  for (const node of graph.allNodes()) {
7307
8322
  if (node.filePath && node.filePath.includes(filePath)) {
7308
- matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
8323
+ allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
7309
8324
  }
7310
8325
  }
7311
- if (matches.length === 0) {
8326
+ if (allMatches.length === 0) {
7312
8327
  return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
7313
8328
  }
7314
- matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
7315
- return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
8329
+ allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
8330
+ const total = allMatches.length;
8331
+ const matches = allMatches.slice(offset, offset + effectiveLimit);
8332
+ const hasMore = offset + effectiveLimit < total;
8333
+ return {
8334
+ content: [{
8335
+ type: "text",
8336
+ text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8337
+ }]
8338
+ };
7316
8339
  }
7317
8340
  // ── find_path ──────────────────────────────────────────────────────────
7318
8341
  case "find_path": {
@@ -7358,15 +8381,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7358
8381
  // ── list_exports ───────────────────────────────────────────────────────
7359
8382
  case "list_exports": {
7360
8383
  const kindFilter = a.kind;
7361
- const limit = a.limit ?? 100;
7362
- const exports$1 = [];
8384
+ const offset = a.offset ?? 0;
8385
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8386
+ const allExports = [];
7363
8387
  for (const node of graph.allNodes()) {
7364
8388
  if (!node.exported) continue;
7365
8389
  if (kindFilter && node.kind !== kindFilter) continue;
7366
- exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
7367
- if (exports$1.length >= limit) break;
8390
+ allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
7368
8391
  }
7369
- return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
8392
+ const total = allExports.length;
8393
+ const exports$1 = allExports.slice(offset, offset + effectiveLimit);
8394
+ const hasMore = offset + effectiveLimit < total;
8395
+ return {
8396
+ content: [{
8397
+ type: "text",
8398
+ text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8399
+ }]
8400
+ };
7370
8401
  }
7371
8402
  // ── routes ─────────────────────────────────────────────────────────────
7372
8403
  case "routes": {
@@ -7380,8 +8411,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7380
8411
  }
7381
8412
  // ── clusters ───────────────────────────────────────────────────────────
7382
8413
  case "clusters": {
7383
- const limit = a.limit ?? 50;
7384
- const clusters = [];
8414
+ const offset = a.offset ?? 0;
8415
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8416
+ const allClusters = [];
7385
8417
  for (const node of graph.allNodes()) {
7386
8418
  if (node.kind === "cluster") {
7387
8419
  const members = [];
@@ -7393,35 +8425,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7393
8425
  }
7394
8426
  }
7395
8427
  }
7396
- clusters.push({
8428
+ allClusters.push({
7397
8429
  id: node.id,
7398
8430
  name: node.name,
7399
8431
  memberCount: node.metadata?.memberCount ?? members.length,
7400
8432
  topSymbols: members.slice(0, 10)
7401
8433
  });
7402
- if (clusters.length >= limit) break;
7403
8434
  }
7404
8435
  }
7405
- return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
8436
+ const total = allClusters.length;
8437
+ const clusters = allClusters.slice(offset, offset + effectiveLimit);
8438
+ const hasMore = offset + effectiveLimit < total;
8439
+ return {
8440
+ content: [{
8441
+ type: "text",
8442
+ text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8443
+ }]
8444
+ };
7406
8445
  }
7407
8446
  // ── flows ──────────────────────────────────────────────────────────────
7408
8447
  case "flows": {
7409
- const limit = a.limit ?? 50;
7410
- const flows = [];
8448
+ const offset = a.offset ?? 0;
8449
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8450
+ const allFlows = [];
7411
8451
  for (const node of graph.allNodes()) {
7412
8452
  if (node.kind === "flow") {
7413
8453
  const steps = node.metadata?.steps;
7414
- flows.push({
8454
+ allFlows.push({
7415
8455
  id: node.id,
7416
8456
  name: node.name,
7417
8457
  entryPoint: node.metadata?.entryPoint,
7418
8458
  steps: steps ?? [],
7419
8459
  stepCount: Array.isArray(steps) ? steps.length : 0
7420
8460
  });
7421
- if (flows.length >= limit) break;
7422
8461
  }
7423
8462
  }
7424
- return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
8463
+ const total = allFlows.length;
8464
+ const flows = allFlows.slice(offset, offset + effectiveLimit);
8465
+ const hasMore = offset + effectiveLimit < total;
8466
+ return {
8467
+ content: [{
8468
+ type: "text",
8469
+ text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8470
+ }]
8471
+ };
7425
8472
  }
7426
8473
  // ── detect_changes ─────────────────────────────────────────────────────
7427
8474
  case "detect_changes": {
@@ -7449,7 +8496,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7449
8496
  for (const { filePath: changedFile, changedLines } of changedFiles) {
7450
8497
  for (const node of graph.allNodes()) {
7451
8498
  if (!node.filePath) continue;
7452
- const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path27.sep, "");
8499
+ const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path29.sep, "");
7453
8500
  const normChanged = changedFile.replace(/^a\/|^b\//, "");
7454
8501
  if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
7455
8502
  if (node.startLine !== void 0 && node.endLine !== void 0) {
@@ -7671,6 +8718,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
7671
8718
  }]
7672
8719
  };
7673
8720
  }
8721
+ // ── explain_relationship ───────────────────────────────────────────────
8722
+ case "explain_relationship": {
8723
+ const fromName = a.from;
8724
+ const toName = a.to;
8725
+ const result = explainRelationship(graph, fromName, toName);
8726
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8727
+ }
8728
+ // ── pr_impact ──────────────────────────────────────────────────────────
8729
+ case "pr_impact": {
8730
+ const maxHops = a.maxHops ?? 5;
8731
+ let changedFiles = a.changedFiles ?? [];
8732
+ if (a.diff && typeof a.diff === "string") {
8733
+ const diffFiles = parseDiffFiles(a.diff);
8734
+ changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
8735
+ }
8736
+ if (changedFiles.length === 0) {
8737
+ return {
8738
+ content: [{
8739
+ type: "text",
8740
+ text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
8741
+ }]
8742
+ };
8743
+ }
8744
+ const result = computePRImpact(graph, changedFiles, maxHops);
8745
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8746
+ }
8747
+ // ── similar_symbols ────────────────────────────────────────────────────
8748
+ case "similar_symbols": {
8749
+ const symbolName = a.symbol;
8750
+ const limit = a.limit ?? 10;
8751
+ const result = findSimilarSymbols(graph, symbolName, limit);
8752
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8753
+ }
8754
+ // ── health_report ──────────────────────────────────────────────────────
8755
+ case "health_report": {
8756
+ const scope = a.scope ?? ".";
8757
+ const result = computeHealthReport(graph, scope);
8758
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8759
+ }
8760
+ // ── suggest_tests ──────────────────────────────────────────────────────
8761
+ case "suggest_tests": {
8762
+ const sym = a.symbol;
8763
+ const result = suggestTests(graph, sym);
8764
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8765
+ }
8766
+ // ── cluster_summary ────────────────────────────────────────────────────
8767
+ case "cluster_summary": {
8768
+ const cluster = a.cluster;
8769
+ const result = summarizeCluster(graph, cluster);
8770
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8771
+ }
7674
8772
  default:
7675
8773
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
7676
8774
  }
@@ -7764,7 +8862,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
7764
8862
  var JobsDB = class {
7765
8863
  db;
7766
8864
  constructor(dbPath) {
7767
- fs19.mkdirSync(path27.dirname(dbPath), { recursive: true });
8865
+ fs23.mkdirSync(path29.dirname(dbPath), { recursive: true });
7768
8866
  this.db = new Database3(dbPath);
7769
8867
  this.db.pragma("journal_mode = WAL");
7770
8868
  this.db.pragma("foreign_keys = ON");
@@ -7906,7 +9004,7 @@ var JobsDB = class {
7906
9004
  }
7907
9005
  };
7908
9006
  function getJobsDBPath() {
7909
- return path27.join(os12.homedir(), ".code-intel", "jobs.db");
9007
+ return path29.join(os12.homedir(), ".code-intel", "jobs.db");
7910
9008
  }
7911
9009
  var _jobsDB = null;
7912
9010
  function getOrCreateJobsDB() {
@@ -7998,7 +9096,7 @@ var BACKUP_VERSION = "1.0";
7998
9096
  var ALGORITHM = "aes-256-gcm";
7999
9097
  var IV_LENGTH = 16;
8000
9098
  function getBackupDir() {
8001
- return path27.join(os12.homedir(), ".code-intel", "backups");
9099
+ return path29.join(os12.homedir(), ".code-intel", "backups");
8002
9100
  }
8003
9101
  function getBackupKey() {
8004
9102
  const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
@@ -8029,30 +9127,30 @@ var BackupService = class {
8029
9127
  constructor(backupDir) {
8030
9128
  this.backupDir = backupDir ?? getBackupDir();
8031
9129
  this.key = getBackupKey();
8032
- fs19.mkdirSync(this.backupDir, { recursive: true });
9130
+ fs23.mkdirSync(this.backupDir, { recursive: true });
8033
9131
  }
8034
9132
  /**
8035
9133
  * Create a backup for a repository.
8036
9134
  * Returns the backup entry.
8037
9135
  */
8038
9136
  createBackup(repoPath) {
8039
- const codeIntelDir = path27.join(repoPath, ".code-intel");
9137
+ const codeIntelDir = path29.join(repoPath, ".code-intel");
8040
9138
  const id = v4();
8041
9139
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8042
9140
  const filesToBackup = [];
8043
9141
  const candidates = ["graph.db", "vector.db", "meta.json"];
8044
9142
  for (const f of candidates) {
8045
- const fp = path27.join(codeIntelDir, f);
8046
- if (fs19.existsSync(fp)) {
9143
+ const fp = path29.join(codeIntelDir, f);
9144
+ if (fs23.existsSync(fp)) {
8047
9145
  filesToBackup.push({ name: f, localPath: fp });
8048
9146
  }
8049
9147
  }
8050
- const registryPath = path27.join(os12.homedir(), ".code-intel", "registry.json");
8051
- if (fs19.existsSync(registryPath)) {
9148
+ const registryPath = path29.join(os12.homedir(), ".code-intel", "registry.json");
9149
+ if (fs23.existsSync(registryPath)) {
8052
9150
  filesToBackup.push({ name: "registry.json", localPath: registryPath });
8053
9151
  }
8054
- const usersDbPath = path27.join(os12.homedir(), ".code-intel", "users.db");
8055
- if (fs19.existsSync(usersDbPath)) {
9152
+ const usersDbPath = path29.join(os12.homedir(), ".code-intel", "users.db");
9153
+ if (fs23.existsSync(usersDbPath)) {
8056
9154
  filesToBackup.push({ name: "users.db", localPath: usersDbPath });
8057
9155
  }
8058
9156
  if (filesToBackup.length === 0) {
@@ -8063,7 +9161,7 @@ var BackupService = class {
8063
9161
  createdAt,
8064
9162
  version: BACKUP_VERSION,
8065
9163
  files: filesToBackup.map((f) => {
8066
- const data = fs19.readFileSync(f.localPath);
9164
+ const data = fs23.readFileSync(f.localPath);
8067
9165
  return {
8068
9166
  name: f.name,
8069
9167
  sha256: crypto5.createHash("sha256").update(data).digest("hex"),
@@ -8077,7 +9175,7 @@ var BackupService = class {
8077
9175
  manifestLenBuf.writeUInt32BE(manifestBuf.length, 0);
8078
9176
  parts.push(manifestLenBuf, manifestBuf);
8079
9177
  for (const f of filesToBackup) {
8080
- const data = fs19.readFileSync(f.localPath);
9178
+ const data = fs23.readFileSync(f.localPath);
8081
9179
  const nameBuf = Buffer.from(f.name, "utf-8");
8082
9180
  const nameLenBuf = Buffer.alloc(2);
8083
9181
  nameLenBuf.writeUInt16BE(nameBuf.length, 0);
@@ -8088,8 +9186,8 @@ var BackupService = class {
8088
9186
  const plaintext = Buffer.concat(parts);
8089
9187
  const encrypted = encryptBuffer(plaintext, this.key);
8090
9188
  const backupFileName = `backup-${id}.cib`;
8091
- const backupPath = path27.join(this.backupDir, backupFileName);
8092
- fs19.writeFileSync(backupPath, encrypted);
9189
+ const backupPath = path29.join(this.backupDir, backupFileName);
9190
+ fs23.writeFileSync(backupPath, encrypted);
8093
9191
  const entry = {
8094
9192
  id,
8095
9193
  createdAt,
@@ -8116,9 +9214,9 @@ var BackupService = class {
8116
9214
  async uploadToS3(entry) {
8117
9215
  const cfg = getS3Config();
8118
9216
  if (!cfg) throw new Error("S3 not configured. Set CODE_INTEL_BACKUP_S3_BUCKET, CODE_INTEL_BACKUP_S3_ACCESS_KEY_ID, CODE_INTEL_BACKUP_S3_SECRET_ACCESS_KEY.");
8119
- const fileName = path27.basename(entry.path);
9217
+ const fileName = path29.basename(entry.path);
8120
9218
  const s3Key = `${cfg.prefix}${fileName}`;
8121
- const body = fs19.readFileSync(entry.path);
9219
+ const body = fs23.readFileSync(entry.path);
8122
9220
  const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
8123
9221
  if (result.statusCode < 200 || result.statusCode >= 300) {
8124
9222
  throw new Error(`S3 upload failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
@@ -8135,8 +9233,8 @@ var BackupService = class {
8135
9233
  if (result.statusCode < 200 || result.statusCode >= 300) {
8136
9234
  throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
8137
9235
  }
8138
- fs19.mkdirSync(path27.dirname(destPath), { recursive: true });
8139
- fs19.writeFileSync(destPath, Buffer.from(result.body, "binary"));
9236
+ fs23.mkdirSync(path29.dirname(destPath), { recursive: true });
9237
+ fs23.writeFileSync(destPath, Buffer.from(result.body, "binary"));
8140
9238
  }
8141
9239
  /**
8142
9240
  * List backup objects in S3 with the configured prefix.
@@ -8182,10 +9280,10 @@ var BackupService = class {
8182
9280
  if (!entry) {
8183
9281
  throw new Error(`Backup "${backupId}" not found.`);
8184
9282
  }
8185
- if (!fs19.existsSync(entry.path)) {
9283
+ if (!fs23.existsSync(entry.path)) {
8186
9284
  throw new Error(`Backup file not found at: ${entry.path}`);
8187
9285
  }
8188
- const encrypted = fs19.readFileSync(entry.path);
9286
+ const encrypted = fs23.readFileSync(entry.path);
8189
9287
  let plaintext;
8190
9288
  try {
8191
9289
  plaintext = decryptBuffer(encrypted, this.key);
@@ -8199,8 +9297,8 @@ var BackupService = class {
8199
9297
  offset += manifestLen;
8200
9298
  const manifest = JSON.parse(manifestStr);
8201
9299
  const restoreBase = targetRepoPath ?? entry.repoPath;
8202
- const codeIntelDir = path27.join(restoreBase, ".code-intel");
8203
- fs19.mkdirSync(codeIntelDir, { recursive: true });
9300
+ const codeIntelDir = path29.join(restoreBase, ".code-intel");
9301
+ fs23.mkdirSync(codeIntelDir, { recursive: true });
8204
9302
  for (const fileEntry of manifest.files) {
8205
9303
  const nameLen = plaintext.readUInt16BE(offset);
8206
9304
  offset += 2;
@@ -8217,18 +9315,18 @@ var BackupService = class {
8217
9315
  }
8218
9316
  let destPath;
8219
9317
  if (name === "registry.json" || name === "users.db") {
8220
- destPath = path27.join(os12.homedir(), ".code-intel", name);
9318
+ destPath = path29.join(os12.homedir(), ".code-intel", name);
8221
9319
  } else {
8222
- destPath = path27.join(codeIntelDir, name);
9320
+ destPath = path29.join(codeIntelDir, name);
8223
9321
  }
8224
- fs19.writeFileSync(destPath, data);
9322
+ fs23.writeFileSync(destPath, data);
8225
9323
  }
8226
9324
  }
8227
9325
  /**
8228
9326
  * Apply retention policy: keep N daily, M weekly, L monthly backups.
8229
9327
  */
8230
9328
  applyRetention(options = { daily: 7, weekly: 4, monthly: 12 }) {
8231
- const entries = this._loadIndex().filter((e) => fs19.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
9329
+ const entries = this._loadIndex().filter((e) => fs23.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
8232
9330
  const keep = /* @__PURE__ */ new Set();
8233
9331
  const now = /* @__PURE__ */ new Date();
8234
9332
  const dailyCutoff = new Date(now);
@@ -8258,7 +9356,7 @@ var BackupService = class {
8258
9356
  for (const e of entries) {
8259
9357
  if (!keep.has(e.id)) {
8260
9358
  try {
8261
- fs19.unlinkSync(e.path);
9359
+ fs23.unlinkSync(e.path);
8262
9360
  deleted++;
8263
9361
  } catch {
8264
9362
  }
@@ -8270,17 +9368,17 @@ var BackupService = class {
8270
9368
  }
8271
9369
  // ── Index helpers ──────────────────────────────────────────────────────────
8272
9370
  _indexPath() {
8273
- return path27.join(this.backupDir, "index.json");
9371
+ return path29.join(this.backupDir, "index.json");
8274
9372
  }
8275
9373
  _loadIndex() {
8276
9374
  try {
8277
- return JSON.parse(fs19.readFileSync(this._indexPath(), "utf-8"));
9375
+ return JSON.parse(fs23.readFileSync(this._indexPath(), "utf-8"));
8278
9376
  } catch {
8279
9377
  return [];
8280
9378
  }
8281
9379
  }
8282
9380
  _saveIndex(entries) {
8283
- fs19.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
9381
+ fs23.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
8284
9382
  }
8285
9383
  _appendIndex(entry) {
8286
9384
  const entries = this._loadIndex();
@@ -8939,6 +10037,30 @@ var openApiSpec = {
8939
10037
  }
8940
10038
  }
8941
10039
  },
10040
+ "/groups/{name}/topology": {
10041
+ get: {
10042
+ tags: ["Groups"],
10043
+ summary: "Get the topology of repos and cross-repo contract edges for a group",
10044
+ parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
10045
+ responses: {
10046
+ "200": {
10047
+ description: "Repos and cross-repo edges",
10048
+ content: {
10049
+ "application/json": {
10050
+ schema: {
10051
+ type: "object",
10052
+ properties: {
10053
+ repos: { type: "array", items: { type: "object", properties: { name: { type: "string" }, groupPath: { type: "string" }, nodeCount: { type: "integer" }, edgeCount: { type: "integer" } } } },
10054
+ edges: { type: "array", items: { type: "object", properties: { source: { type: "string" }, target: { type: "string" }, contractName: { type: "string" }, confidence: { type: "number" }, kind: { type: "string" } } } }
10055
+ }
10056
+ }
10057
+ }
10058
+ }
10059
+ },
10060
+ "404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
10061
+ }
10062
+ }
10063
+ },
8942
10064
  "/query": {
8943
10065
  post: {
8944
10066
  tags: ["GQL"],
@@ -9049,11 +10171,11 @@ var openApiSpec = {
9049
10171
  };
9050
10172
 
9051
10173
  // src/http/app.ts
9052
- var __dirname$1 = path27.dirname(fileURLToPath(import.meta.url));
10174
+ var __dirname$1 = path29.dirname(fileURLToPath(import.meta.url));
9053
10175
  var WEB_DIST = (() => {
9054
- const bundled = path27.resolve(__dirname$1, "..", "web");
9055
- if (fs19.existsSync(bundled)) return bundled;
9056
- return path27.resolve(__dirname$1, "..", "..", "..", "web", "dist");
10176
+ const bundled = path29.resolve(__dirname$1, "..", "web");
10177
+ if (fs23.existsSync(bundled)) return bundled;
10178
+ return path29.resolve(__dirname$1, "..", "..", "..", "web", "dist");
9057
10179
  })();
9058
10180
  function getAllowedOrigins() {
9059
10181
  const env = process.env["CODE_INTEL_CORS_ORIGINS"];
@@ -9584,8 +10706,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9584
10706
  const registry = loadRegistry();
9585
10707
  const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
9586
10708
  if (!entry) return null;
9587
- const dbPath = path27.join(entry.path, ".code-intel", "graph.db");
9588
- if (!fs19.existsSync(dbPath)) return null;
10709
+ const dbPath = path29.join(entry.path, ".code-intel", "graph.db");
10710
+ if (!fs23.existsSync(dbPath)) return null;
9589
10711
  const repoGraph = createKnowledgeGraph();
9590
10712
  const db = new DbManager(dbPath);
9591
10713
  try {
@@ -9672,7 +10794,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9672
10794
  return;
9673
10795
  }
9674
10796
  try {
9675
- const content = fs19.readFileSync(file_path, "utf-8");
10797
+ const content = fs23.readFileSync(file_path, "utf-8");
9676
10798
  res.json({ content });
9677
10799
  } catch {
9678
10800
  res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "File not found" } });
@@ -9930,8 +11052,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9930
11052
  for (const member of group.members) {
9931
11053
  const regEntry = registry.find((r) => r.name === member.registryName);
9932
11054
  if (!regEntry) continue;
9933
- const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
9934
- if (!fs19.existsSync(dbPath)) continue;
11055
+ const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
11056
+ if (!fs23.existsSync(dbPath)) continue;
9935
11057
  const db = new DbManager(dbPath);
9936
11058
  try {
9937
11059
  await db.init();
@@ -9943,6 +11065,45 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9943
11065
  }
9944
11066
  res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
9945
11067
  });
11068
+ app.get("/api/v1/groups/:name/topology", requireAuth, requireRole("viewer"), async (req, res) => {
11069
+ const groupName = req.params["name"];
11070
+ const group = loadGroup(groupName);
11071
+ if (!group) {
11072
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
11073
+ return;
11074
+ }
11075
+ const syncResult = loadSyncResult(groupName);
11076
+ const registry = loadRegistry();
11077
+ const repos = await Promise.all(group.members.map(async (member) => {
11078
+ const regEntry = registry.find((r) => r.name === member.registryName);
11079
+ let nodeCount = 0;
11080
+ let edgeCount = 0;
11081
+ if (regEntry) {
11082
+ const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
11083
+ if (fs23.existsSync(dbPath)) {
11084
+ try {
11085
+ const db = new DbManager(dbPath);
11086
+ await db.init();
11087
+ const g = createKnowledgeGraph();
11088
+ await loadGraphFromDB(g, db);
11089
+ db.close();
11090
+ nodeCount = g.size.nodes;
11091
+ edgeCount = g.size.edges;
11092
+ } catch {
11093
+ }
11094
+ }
11095
+ }
11096
+ return { name: member.registryName, groupPath: member.groupPath, nodeCount, edgeCount };
11097
+ }));
11098
+ const edges = syncResult ? syncResult.links.map((link) => ({
11099
+ source: link.providerRepo,
11100
+ target: link.consumerRepo,
11101
+ contractName: link.providerContract,
11102
+ confidence: link.confidence,
11103
+ kind: "contract"
11104
+ })) : [];
11105
+ res.json({ repos, edges });
11106
+ });
9946
11107
  app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
9947
11108
  const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
9948
11109
  if (!file) {
@@ -9968,14 +11129,14 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9968
11129
  });
9969
11130
  return;
9970
11131
  }
9971
- let rawResolved = path27.normalize(file);
9972
- if (!path27.isAbsolute(rawResolved) && workspaceRoot) {
9973
- rawResolved = path27.join(workspaceRoot, rawResolved);
11132
+ let rawResolved = path29.normalize(file);
11133
+ if (!path29.isAbsolute(rawResolved) && workspaceRoot) {
11134
+ rawResolved = path29.join(workspaceRoot, rawResolved);
9974
11135
  }
9975
- const resolvedFile = path27.resolve(rawResolved);
11136
+ const resolvedFile = path29.resolve(rawResolved);
9976
11137
  function isInsideDir(fileAbs, dir) {
9977
- const rel = path27.relative(path27.resolve(dir), fileAbs);
9978
- return !rel.startsWith("..") && !path27.isAbsolute(rel);
11138
+ const rel = path29.relative(path29.resolve(dir), fileAbs);
11139
+ return !rel.startsWith("..") && !path29.isAbsolute(rel);
9979
11140
  }
9980
11141
  if (workspaceRoot) {
9981
11142
  if (!isInsideDir(resolvedFile, workspaceRoot)) {
@@ -10012,7 +11173,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
10012
11173
  }
10013
11174
  let fileContent;
10014
11175
  try {
10015
- fileContent = fs19.readFileSync(resolvedFile, "utf-8");
11176
+ fileContent = fs23.readFileSync(resolvedFile, "utf-8");
10016
11177
  } catch {
10017
11178
  res.status(404).json({
10018
11179
  error: {
@@ -10043,7 +11204,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
10043
11204
  const contextStart = Math.max(1, startLine - 20);
10044
11205
  const contextEnd = Math.min(lines.length, endLine + 20);
10045
11206
  const content = lines.slice(contextStart - 1, contextEnd).join("\n");
10046
- const ext = path27.extname(resolvedFile).toLowerCase();
11207
+ const ext = path29.extname(resolvedFile).toLowerCase();
10047
11208
  const languageMap = {
10048
11209
  ".ts": "typescript",
10049
11210
  ".tsx": "typescript",
@@ -10178,10 +11339,10 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
10178
11339
  res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err), requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
10179
11340
  }
10180
11341
  });
10181
- if (fs19.existsSync(WEB_DIST)) {
11342
+ if (fs23.existsSync(WEB_DIST)) {
10182
11343
  app.use(express.static(WEB_DIST));
10183
11344
  app.get("/{*path}", (_req, res) => {
10184
- res.sendFile(path27.join(WEB_DIST, "index.html"));
11345
+ res.sendFile(path29.join(WEB_DIST, "index.html"));
10185
11346
  });
10186
11347
  }
10187
11348
  app.use("/admin", requireRole("admin"));