@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/cli/main.js CHANGED
@@ -7,8 +7,8 @@ import { SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, SEMRESATTRS_SERVICE_NAME } from '@o
7
7
  import { SpanStatusCode, trace, context } from '@opentelemetry/api';
8
8
  import winston from 'winston';
9
9
  import DailyRotateFile from 'winston-daily-rotate-file';
10
- import fs28, { readFileSync, existsSync } from 'fs';
11
- import path30, { dirname, join } from 'path';
10
+ import fs33, { readFileSync, existsSync } from 'fs';
11
+ import path33, { dirname, join } from 'path';
12
12
  import os12 from 'os';
13
13
  import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client';
14
14
  import { createRequire } from 'module';
@@ -326,7 +326,7 @@ var init_logger = __esm({
326
326
  };
327
327
  }
328
328
  /** Global log directory: ~/.code-intel/logs */
329
- static LOG_DIR = path30.join(os12.homedir(), ".code-intel", "logs");
329
+ static LOG_DIR = path33.join(os12.homedir(), ".code-intel", "logs");
330
330
  static getLogger() {
331
331
  if (!_Logger.instance) {
332
332
  const isProduction = process.env.NODE_ENV === "production";
@@ -335,12 +335,12 @@ var init_logger = __esm({
335
335
  transports.push(new winston.transports.Console());
336
336
  if (!isProduction) {
337
337
  try {
338
- if (!fs28.existsSync(_Logger.LOG_DIR)) {
339
- fs28.mkdirSync(_Logger.LOG_DIR, { recursive: true });
338
+ if (!fs33.existsSync(_Logger.LOG_DIR)) {
339
+ fs33.mkdirSync(_Logger.LOG_DIR, { recursive: true });
340
340
  }
341
341
  transports.push(
342
342
  new DailyRotateFile({
343
- filename: path30.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
343
+ filename: path33.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
344
344
  datePattern: "YYYY-MM-DD",
345
345
  maxSize: "20m",
346
346
  maxFiles: "14d"
@@ -392,6 +392,131 @@ var init_logger = __esm({
392
392
  }
393
393
  });
394
394
 
395
+ // src/graph/knowledge-graph.ts
396
+ function createKnowledgeGraph() {
397
+ const nodes = /* @__PURE__ */ new Map();
398
+ const edges = /* @__PURE__ */ new Map();
399
+ const edgesByKind = /* @__PURE__ */ new Map();
400
+ const edgesFromNode = /* @__PURE__ */ new Map();
401
+ const edgesToNode = /* @__PURE__ */ new Map();
402
+ function indexEdge(edge) {
403
+ let kindSet = edgesByKind.get(edge.kind);
404
+ if (!kindSet) {
405
+ kindSet = /* @__PURE__ */ new Set();
406
+ edgesByKind.set(edge.kind, kindSet);
407
+ }
408
+ kindSet.add(edge.id);
409
+ let fromSet = edgesFromNode.get(edge.source);
410
+ if (!fromSet) {
411
+ fromSet = /* @__PURE__ */ new Set();
412
+ edgesFromNode.set(edge.source, fromSet);
413
+ }
414
+ fromSet.add(edge.id);
415
+ let toSet = edgesToNode.get(edge.target);
416
+ if (!toSet) {
417
+ toSet = /* @__PURE__ */ new Set();
418
+ edgesToNode.set(edge.target, toSet);
419
+ }
420
+ toSet.add(edge.id);
421
+ }
422
+ function unindexEdge(edge) {
423
+ edgesByKind.get(edge.kind)?.delete(edge.id);
424
+ edgesFromNode.get(edge.source)?.delete(edge.id);
425
+ edgesToNode.get(edge.target)?.delete(edge.id);
426
+ }
427
+ return {
428
+ addNode(node) {
429
+ nodes.set(node.id, node);
430
+ },
431
+ addEdge(edge) {
432
+ edges.set(edge.id, edge);
433
+ indexEdge(edge);
434
+ },
435
+ getNode(id) {
436
+ return nodes.get(id);
437
+ },
438
+ getEdge(id) {
439
+ return edges.get(id);
440
+ },
441
+ *findEdgesByKind(kind) {
442
+ const ids = edgesByKind.get(kind);
443
+ if (!ids) return;
444
+ for (const id of ids) {
445
+ const edge = edges.get(id);
446
+ if (edge) yield edge;
447
+ }
448
+ },
449
+ *findEdgesFrom(sourceId) {
450
+ const ids = edgesFromNode.get(sourceId);
451
+ if (!ids) return;
452
+ for (const id of ids) {
453
+ const edge = edges.get(id);
454
+ if (edge) yield edge;
455
+ }
456
+ },
457
+ *findEdgesTo(targetId) {
458
+ const ids = edgesToNode.get(targetId);
459
+ if (!ids) return;
460
+ for (const id of ids) {
461
+ const edge = edges.get(id);
462
+ if (edge) yield edge;
463
+ }
464
+ },
465
+ removeNodeCascade(id) {
466
+ const fromEdges = edgesFromNode.get(id);
467
+ if (fromEdges) {
468
+ for (const edgeId of [...fromEdges]) {
469
+ const edge = edges.get(edgeId);
470
+ if (edge) {
471
+ unindexEdge(edge);
472
+ edges.delete(edgeId);
473
+ }
474
+ }
475
+ }
476
+ const toEdges = edgesToNode.get(id);
477
+ if (toEdges) {
478
+ for (const edgeId of [...toEdges]) {
479
+ const edge = edges.get(edgeId);
480
+ if (edge) {
481
+ unindexEdge(edge);
482
+ edges.delete(edgeId);
483
+ }
484
+ }
485
+ }
486
+ edgesFromNode.delete(id);
487
+ edgesToNode.delete(id);
488
+ nodes.delete(id);
489
+ },
490
+ removeEdge(id) {
491
+ const edge = edges.get(id);
492
+ if (edge) {
493
+ unindexEdge(edge);
494
+ edges.delete(id);
495
+ }
496
+ },
497
+ *allNodes() {
498
+ yield* nodes.values();
499
+ },
500
+ *allEdges() {
501
+ yield* edges.values();
502
+ },
503
+ get size() {
504
+ return { nodes: nodes.size, edges: edges.size };
505
+ },
506
+ clear() {
507
+ nodes.clear();
508
+ edges.clear();
509
+ edgesByKind.clear();
510
+ edgesFromNode.clear();
511
+ edgesToNode.clear();
512
+ }
513
+ };
514
+ }
515
+ var init_knowledge_graph = __esm({
516
+ "src/graph/knowledge-graph.ts"() {
517
+ }
518
+ });
519
+
395
520
  // src/pipeline/dag-validator.ts
396
521
  function validateDAG(phases) {
397
522
  const errors = [];
@@ -415,25 +540,25 @@ function validateDAG(phases) {
415
540
  const visiting = /* @__PURE__ */ new Set();
416
541
  const visited = /* @__PURE__ */ new Set();
417
542
  const phaseMap = new Map(phases.map((p) => [p.name, p]));
418
- function dfs(name, path31) {
543
+ function dfs(name, path34) {
419
544
  if (visiting.has(name)) {
420
- const cycleStart = path31.indexOf(name);
421
- const cycle = path31.slice(cycleStart).concat(name);
545
+ const cycleStart = path34.indexOf(name);
546
+ const cycle = path34.slice(cycleStart).concat(name);
422
547
  errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
423
548
  return true;
424
549
  }
425
550
  if (visited.has(name)) return false;
426
551
  visiting.add(name);
427
- path31.push(name);
552
+ path34.push(name);
428
553
  const phase = phaseMap.get(name);
429
554
  if (phase) {
430
555
  for (const dep of phase.dependencies) {
431
- if (dfs(dep, path31)) return true;
556
+ if (dfs(dep, path34)) return true;
432
557
  }
433
558
  }
434
559
  visiting.delete(name);
435
560
  visited.add(name);
436
- path31.pop();
561
+ path34.pop();
437
562
  return false;
438
563
  }
439
564
  for (const phase of phases) {
@@ -693,11 +818,11 @@ var init_id_generator = __esm({
693
818
  }
694
819
  });
695
820
  function findBundledWasmDir() {
696
- const fileDir = path30.dirname(fileURLToPath(import.meta.url));
821
+ const fileDir = path33.dirname(fileURLToPath(import.meta.url));
697
822
  const candidates = [
698
- path30.join(fileDir, "wasm"),
823
+ path33.join(fileDir, "wasm"),
699
824
  // dist/index.js → dist/wasm/
700
- path30.join(fileDir, "../wasm")
825
+ path33.join(fileDir, "../wasm")
701
826
  // dist/cli/main.js → dist/wasm/
702
827
  ];
703
828
  for (const candidate of candidates) {
@@ -738,7 +863,7 @@ function wasmPath(lang) {
738
863
  }
739
864
  const bundled = BUNDLED_WASM_MAP[lang];
740
865
  if (bundled) {
741
- const bundledPath = path30.join(_bundledWasmDir, bundled);
866
+ const bundledPath = path33.join(_bundledWasmDir, bundled);
742
867
  if (existsSync(bundledPath)) return bundledPath;
743
868
  }
744
869
  return null;
@@ -751,14 +876,14 @@ async function initParser() {
751
876
  }
752
877
  async function getLanguage(lang) {
753
878
  if (languageCache.has(lang)) return languageCache.get(lang);
754
- const path31 = wasmPath(lang);
755
- if (!path31) {
879
+ const path34 = wasmPath(lang);
880
+ if (!path34) {
756
881
  languageCache.set(lang, null);
757
882
  return null;
758
883
  }
759
884
  try {
760
885
  await initParser();
761
- const language = await Language.load(path31);
886
+ const language = await Language.load(path34);
762
887
  languageCache.set(lang, language);
763
888
  return language;
764
889
  } catch {
@@ -2183,7 +2308,7 @@ var init_parse_phase = __esm({
2183
2308
  const batch = filePaths.slice(i, i + CONCURRENCY);
2184
2309
  await Promise.all(batch.map(async (filePath) => {
2185
2310
  try {
2186
- const source = await fs28.promises.readFile(filePath, "utf-8");
2311
+ const source = await fs33.promises.readFile(filePath, "utf-8");
2187
2312
  context2.fileCache.set(filePath, source);
2188
2313
  } catch {
2189
2314
  }
@@ -2196,14 +2321,14 @@ var init_parse_phase = __esm({
2196
2321
  const lang = detectLanguage(filePath);
2197
2322
  if (!lang) {
2198
2323
  if (context2.verbose) {
2199
- const relativePath2 = path30.relative(context2.workspaceRoot, filePath);
2324
+ const relativePath2 = path33.relative(context2.workspaceRoot, filePath);
2200
2325
  logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
2201
2326
  }
2202
2327
  continue;
2203
2328
  }
2204
2329
  const source = context2.fileCache.get(filePath);
2205
2330
  if (!source) continue;
2206
- const relativePath = path30.relative(context2.workspaceRoot, filePath);
2331
+ const relativePath = path33.relative(context2.workspaceRoot, filePath);
2207
2332
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2208
2333
  const fileNode = context2.graph.getNode(fileNodeId);
2209
2334
  if (fileNode) {
@@ -2449,11 +2574,11 @@ var init_resolve_phase = __esm({
2449
2574
  let heritageEdges = 0;
2450
2575
  const fileIndex = /* @__PURE__ */ new Map();
2451
2576
  for (const fp of filePaths) {
2452
- const rel = path30.relative(workspaceRoot, fp);
2577
+ const rel = path33.relative(workspaceRoot, fp);
2453
2578
  fileIndex.set(rel, fp);
2454
2579
  const noExt = rel.replace(/\.\w+$/, "");
2455
2580
  if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
2456
- const base = path30.basename(rel, path30.extname(rel));
2581
+ const base = path33.basename(rel, path33.extname(rel));
2457
2582
  if (!fileIndex.has(base)) fileIndex.set(base, fp);
2458
2583
  }
2459
2584
  const symbolIndex = /* @__PURE__ */ new Map();
@@ -2484,7 +2609,7 @@ var init_resolve_phase = __esm({
2484
2609
  for (const filePath of filePaths) {
2485
2610
  const lang = detectLanguage(filePath);
2486
2611
  if (!lang) continue;
2487
- const relativePath = path30.relative(workspaceRoot, filePath);
2612
+ const relativePath = path33.relative(workspaceRoot, filePath);
2488
2613
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2489
2614
  const source = fileCache.get(filePath);
2490
2615
  if (!source) continue;
@@ -2497,13 +2622,13 @@ var init_resolve_phase = __esm({
2497
2622
  let resolvedRelPath = null;
2498
2623
  if (cleaned.startsWith(".")) {
2499
2624
  const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
2500
- const fromDir = path30.dirname(relativePath);
2625
+ const fromDir = path33.dirname(relativePath);
2501
2626
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
2502
- const candidate = path30.join(fromDir, cleanedNoJs + ext);
2503
- const normalized = path30.normalize(candidate);
2627
+ const candidate = path33.join(fromDir, cleanedNoJs + ext);
2628
+ const normalized = path33.normalize(candidate);
2504
2629
  if (fileIndex.has(normalized)) {
2505
2630
  const absPath = fileIndex.get(normalized);
2506
- resolvedRelPath = path30.relative(workspaceRoot, absPath);
2631
+ resolvedRelPath = path33.relative(workspaceRoot, absPath);
2507
2632
  break;
2508
2633
  }
2509
2634
  }
@@ -2968,7 +3093,7 @@ var init_db_manager = __esm({
2968
3093
  this.dbPath = dbPath;
2969
3094
  }
2970
3095
  async init() {
2971
- fs28.mkdirSync(path30.dirname(this.dbPath), { recursive: true });
3096
+ fs33.mkdirSync(path33.dirname(this.dbPath), { recursive: true });
2972
3097
  this.db = new Database$1(this.dbPath);
2973
3098
  await this.db.init();
2974
3099
  this.conn = new Connection(this.db);
@@ -3058,13 +3183,14 @@ var init_schema = __esm({
3058
3183
  constant: "const_nodes",
3059
3184
  route: "route_nodes",
3060
3185
  cluster: "cluster_nodes",
3061
- flow: "flow_nodes"
3186
+ flow: "flow_nodes",
3187
+ vulnerability: "vuln_nodes"
3062
3188
  };
3063
3189
  ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
3064
3190
  }
3065
3191
  });
3066
3192
  function writeNodeCSVs(graph, outputDir) {
3067
- fs28.mkdirSync(outputDir, { recursive: true });
3193
+ fs33.mkdirSync(outputDir, { recursive: true });
3068
3194
  const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
3069
3195
  const tableBuffers = /* @__PURE__ */ new Map();
3070
3196
  const tableFilePaths = /* @__PURE__ */ new Map();
@@ -3072,7 +3198,7 @@ function writeNodeCSVs(graph, outputDir) {
3072
3198
  const table = NODE_TABLE_MAP[node.kind];
3073
3199
  if (!tableBuffers.has(table)) {
3074
3200
  tableBuffers.set(table, [header]);
3075
- tableFilePaths.set(table, path30.join(outputDir, `${table}.csv`));
3201
+ tableFilePaths.set(table, path33.join(outputDir, `${table}.csv`));
3076
3202
  }
3077
3203
  tableBuffers.get(table).push(
3078
3204
  csvRow([
@@ -3092,12 +3218,12 @@ function writeNodeCSVs(graph, outputDir) {
3092
3218
  );
3093
3219
  }
3094
3220
  for (const [table, lines] of tableBuffers) {
3095
- fs28.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
3221
+ fs33.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
3096
3222
  }
3097
3223
  return tableFilePaths;
3098
3224
  }
3099
3225
  function writeEdgeCSV(graph, outputDir) {
3100
- fs28.mkdirSync(outputDir, { recursive: true });
3226
+ fs33.mkdirSync(outputDir, { recursive: true });
3101
3227
  const header = "from_id,to_id,kind,weight,label\n";
3102
3228
  const groups = /* @__PURE__ */ new Map();
3103
3229
  for (const edge of graph.allEdges()) {
@@ -3108,7 +3234,7 @@ function writeEdgeCSV(graph, outputDir) {
3108
3234
  const toTable = NODE_TABLE_MAP[targetNode.kind];
3109
3235
  const key = `${fromTable}->${toTable}`;
3110
3236
  if (!groups.has(key)) {
3111
- const filePath = path30.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
3237
+ const filePath = path33.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
3112
3238
  groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
3113
3239
  }
3114
3240
  groups.get(key).lines.push(
@@ -3123,7 +3249,7 @@ function writeEdgeCSV(graph, outputDir) {
3123
3249
  }
3124
3250
  const result = [];
3125
3251
  for (const group of groups.values()) {
3126
- fs28.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
3252
+ fs33.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
3127
3253
  result.push({ fromTable: group.from, toTable: group.to, filePath: group.filePath });
3128
3254
  }
3129
3255
  return result;
@@ -3166,7 +3292,7 @@ async function loadGraphToDB(graph, dbManager) {
3166
3292
  } catch {
3167
3293
  }
3168
3294
  }
3169
- const tmpDir = fs28.mkdtempSync(path30.join(os12.tmpdir(), "code-intel-csv-"));
3295
+ const tmpDir = fs33.mkdtempSync(path33.join(os12.tmpdir(), "code-intel-csv-"));
3170
3296
  try {
3171
3297
  const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
3172
3298
  const edgeGroups = writeEdgeCSV(graph, tmpDir);
@@ -3185,8 +3311,8 @@ async function loadGraphToDB(graph, dbManager) {
3185
3311
  }
3186
3312
  let nodeCount = 0;
3187
3313
  for (const [table, csvPath] of nodeTableFiles) {
3188
- if (!fs28.existsSync(csvPath)) continue;
3189
- const stat = fs28.statSync(csvPath);
3314
+ if (!fs33.existsSync(csvPath)) continue;
3315
+ const stat = fs33.statSync(csvPath);
3190
3316
  if (stat.size < 50) continue;
3191
3317
  try {
3192
3318
  await dbManager.execute(
@@ -3199,8 +3325,8 @@ async function loadGraphToDB(graph, dbManager) {
3199
3325
  }
3200
3326
  let edgeCount = 0;
3201
3327
  for (const group of edgeGroups) {
3202
- if (!fs28.existsSync(group.filePath)) continue;
3203
- const stat = fs28.statSync(group.filePath);
3328
+ if (!fs33.existsSync(group.filePath)) continue;
3329
+ const stat = fs33.statSync(group.filePath);
3204
3330
  if (stat.size < 50) continue;
3205
3331
  try {
3206
3332
  await dbManager.execute(
@@ -3214,7 +3340,7 @@ async function loadGraphToDB(graph, dbManager) {
3214
3340
  return { nodeCount, edgeCount };
3215
3341
  } finally {
3216
3342
  try {
3217
- fs28.rmSync(tmpDir, { recursive: true, force: true });
3343
+ fs33.rmSync(tmpDir, { recursive: true, force: true });
3218
3344
  } catch {
3219
3345
  }
3220
3346
  }
@@ -3316,15 +3442,15 @@ var init_graph_loader = __esm({
3316
3442
  });
3317
3443
  function loadRegistry() {
3318
3444
  try {
3319
- const data = fs28.readFileSync(REPOS_FILE, "utf-8");
3445
+ const data = fs33.readFileSync(REPOS_FILE, "utf-8");
3320
3446
  return JSON.parse(data);
3321
3447
  } catch {
3322
3448
  return [];
3323
3449
  }
3324
3450
  }
3325
3451
  function saveRegistry(entries) {
3326
- fs28.mkdirSync(GLOBAL_DIR, { recursive: true });
3327
- fs28.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
3452
+ fs33.mkdirSync(GLOBAL_DIR, { recursive: true });
3453
+ fs33.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
3328
3454
  }
3329
3455
  function upsertRepo(entry) {
3330
3456
  const entries = loadRegistry();
@@ -3343,28 +3469,28 @@ function removeRepo(repoPath) {
3343
3469
  var GLOBAL_DIR, REPOS_FILE;
3344
3470
  var init_repo_registry = __esm({
3345
3471
  "src/storage/repo-registry.ts"() {
3346
- GLOBAL_DIR = path30.join(os12.homedir(), ".code-intel");
3347
- REPOS_FILE = path30.join(GLOBAL_DIR, "repos.json");
3472
+ GLOBAL_DIR = path33.join(os12.homedir(), ".code-intel");
3473
+ REPOS_FILE = path33.join(GLOBAL_DIR, "repos.json");
3348
3474
  }
3349
3475
  });
3350
3476
  function saveMetadata(repoDir, metadata) {
3351
- const metaDir = path30.join(repoDir, ".code-intel");
3352
- fs28.mkdirSync(metaDir, { recursive: true });
3353
- fs28.writeFileSync(path30.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
3477
+ const metaDir = path33.join(repoDir, ".code-intel");
3478
+ fs33.mkdirSync(metaDir, { recursive: true });
3479
+ fs33.writeFileSync(path33.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
3354
3480
  }
3355
3481
  function loadMetadata(repoDir) {
3356
3482
  try {
3357
- const data = fs28.readFileSync(path30.join(repoDir, ".code-intel", "meta.json"), "utf-8");
3483
+ const data = fs33.readFileSync(path33.join(repoDir, ".code-intel", "meta.json"), "utf-8");
3358
3484
  return JSON.parse(data);
3359
3485
  } catch {
3360
3486
  return null;
3361
3487
  }
3362
3488
  }
3363
3489
  function getDbPath(repoDir) {
3364
- return path30.join(repoDir, ".code-intel", "graph.db");
3490
+ return path33.join(repoDir, ".code-intel", "graph.db");
3365
3491
  }
3366
3492
  function getVectorDbPath(repoDir) {
3367
- return path30.join(repoDir, ".code-intel", "vector.db");
3493
+ return path33.join(repoDir, ".code-intel", "vector.db");
3368
3494
  }
3369
3495
  var init_metadata = __esm({
3370
3496
  "src/storage/metadata.ts"() {
@@ -3420,27 +3546,27 @@ __export(group_registry_exports, {
3420
3546
  saveSyncResult: () => saveSyncResult
3421
3547
  });
3422
3548
  function groupFile(name) {
3423
- return path30.join(GROUPS_DIR, `${name}.json`);
3549
+ return path33.join(GROUPS_DIR, `${name}.json`);
3424
3550
  }
3425
3551
  function loadGroup(name) {
3426
3552
  try {
3427
- return JSON.parse(fs28.readFileSync(groupFile(name), "utf-8"));
3553
+ return JSON.parse(fs33.readFileSync(groupFile(name), "utf-8"));
3428
3554
  } catch {
3429
3555
  return null;
3430
3556
  }
3431
3557
  }
3432
3558
  function saveGroup(group) {
3433
- fs28.mkdirSync(GROUPS_DIR, { recursive: true });
3434
- fs28.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
3559
+ fs33.mkdirSync(GROUPS_DIR, { recursive: true });
3560
+ fs33.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
3435
3561
  }
3436
3562
  function listGroups() {
3437
3563
  const groups = [];
3438
3564
  try {
3439
- for (const file of fs28.readdirSync(GROUPS_DIR)) {
3565
+ for (const file of fs33.readdirSync(GROUPS_DIR)) {
3440
3566
  if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
3441
3567
  try {
3442
3568
  const g = JSON.parse(
3443
- fs28.readFileSync(path30.join(GROUPS_DIR, file), "utf-8")
3569
+ fs33.readFileSync(path33.join(GROUPS_DIR, file), "utf-8")
3444
3570
  );
3445
3571
  groups.push(g);
3446
3572
  } catch {
@@ -3452,16 +3578,16 @@ function listGroups() {
3452
3578
  }
3453
3579
  function deleteGroup(name) {
3454
3580
  try {
3455
- fs28.unlinkSync(groupFile(name));
3581
+ fs33.unlinkSync(groupFile(name));
3456
3582
  } catch {
3457
3583
  }
3458
3584
  try {
3459
- fs28.unlinkSync(path30.join(GROUPS_DIR, `${name}.sync.json`));
3585
+ fs33.unlinkSync(path33.join(GROUPS_DIR, `${name}.sync.json`));
3460
3586
  } catch {
3461
3587
  }
3462
3588
  }
3463
3589
  function groupExists(name) {
3464
- return fs28.existsSync(groupFile(name));
3590
+ return fs33.existsSync(groupFile(name));
3465
3591
  }
3466
3592
  function addMember(groupName, member) {
3467
3593
  const group = loadGroup(groupName);
@@ -3487,16 +3613,16 @@ function removeMember(groupName, groupPath) {
3487
3613
  return group;
3488
3614
  }
3489
3615
  function saveSyncResult(result) {
3490
- fs28.mkdirSync(GROUPS_DIR, { recursive: true });
3491
- fs28.writeFileSync(
3492
- path30.join(GROUPS_DIR, `${result.groupName}.sync.json`),
3616
+ fs33.mkdirSync(GROUPS_DIR, { recursive: true });
3617
+ fs33.writeFileSync(
3618
+ path33.join(GROUPS_DIR, `${result.groupName}.sync.json`),
3493
3619
  JSON.stringify(result, null, 2) + "\n"
3494
3620
  );
3495
3621
  }
3496
3622
  function loadSyncResult(groupName) {
3497
3623
  try {
3498
3624
  return JSON.parse(
3499
- fs28.readFileSync(path30.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
3625
+ fs33.readFileSync(path33.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
3500
3626
  );
3501
3627
  } catch {
3502
3628
  return null;
@@ -3505,90 +3631,597 @@ function loadSyncResult(groupName) {
3505
3631
  var GROUPS_DIR;
3506
3632
  var init_group_registry = __esm({
3507
3633
  "src/multi-repo/group-registry.ts"() {
3508
- GROUPS_DIR = path30.join(os12.homedir(), ".code-intel", "groups");
3634
+ GROUPS_DIR = path33.join(os12.homedir(), ".code-intel", "groups");
3509
3635
  }
3510
3636
  });
3511
3637
 
3512
- // src/errors/codes.ts
3513
- var ErrorCodes, AppError;
3514
- var init_codes = __esm({
3515
- "src/errors/codes.ts"() {
3516
- ErrorCodes = {
3517
- UNAUTHORIZED: "CI-1000",
3518
- FORBIDDEN: "CI-1001",
3519
- NOT_FOUND: "CI-1002",
3520
- ANALYSIS_IN_PROGRESS: "CI-1003",
3521
- INDEX_NOT_FOUND: "CI-1004",
3522
- DB_CORRUPTED: "CI-1042",
3523
- RATE_LIMIT_EXCEEDED: "CI-1100",
3524
- PAYLOAD_TOO_LARGE: "CI-1101",
3525
- INVALID_REQUEST: "CI-1200",
3526
- INTERNAL_ERROR: "CI-5000"
3527
- };
3528
- AppError = class extends Error {
3529
- constructor(code, message, hint, statusCode = 500, docs) {
3530
- super(message);
3531
- this.code = code;
3532
- this.hint = hint;
3533
- this.statusCode = statusCode;
3534
- this.docs = docs;
3535
- this.name = "AppError";
3638
+ // src/multi-repo/graph-from-db.ts
3639
+ function parseRow(row, kind) {
3640
+ return {
3641
+ id: String(row["id"] ?? ""),
3642
+ kind,
3643
+ name: String(row["name"] ?? ""),
3644
+ filePath: String(row["file_path"] ?? ""),
3645
+ startLine: row["start_line"] != null ? Number(row["start_line"]) : void 0,
3646
+ endLine: row["end_line"] != null ? Number(row["end_line"]) : void 0,
3647
+ exported: row["exported"] != null ? Boolean(row["exported"]) : void 0,
3648
+ content: row["content"] ? String(row["content"]) : void 0,
3649
+ metadata: row["metadata"] ? (() => {
3650
+ try {
3651
+ return JSON.parse(String(row["metadata"]));
3652
+ } catch {
3653
+ return void 0;
3536
3654
  }
3537
- code;
3538
- hint;
3539
- statusCode;
3540
- docs;
3541
- };
3542
- }
3543
- });
3544
- function secureMkdir(dir) {
3545
- fs28.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
3546
- if (process.platform !== "win32") {
3655
+ })() : void 0
3656
+ };
3657
+ }
3658
+ async function loadGraphFromDB(graph, db) {
3659
+ for (const table of ALL_NODE_TABLES) {
3660
+ const kind = TABLE_TO_KIND[table];
3661
+ if (!kind) continue;
3662
+ let rows = [];
3547
3663
  try {
3548
- fs28.chmodSync(dir, SECURE_DIR_MODE);
3664
+ rows = await db.query(`MATCH (n:${table}) RETURN n.id, n.name, n.file_path, n.start_line, n.end_line, n.exported, n.content, n.metadata`);
3549
3665
  } catch {
3666
+ continue;
3667
+ }
3668
+ for (const row of rows) {
3669
+ const node = parseRow({
3670
+ id: row["n.id"],
3671
+ name: row["n.name"],
3672
+ file_path: row["n.file_path"],
3673
+ start_line: row["n.start_line"],
3674
+ end_line: row["n.end_line"],
3675
+ exported: row["n.exported"],
3676
+ content: row["n.content"],
3677
+ metadata: row["n.metadata"]
3678
+ }, kind);
3679
+ if (node.id && node.name) graph.addNode(node);
3550
3680
  }
3551
3681
  }
3552
- }
3553
- function secureChmodFile(file) {
3554
- if (process.platform === "win32") return;
3555
3682
  try {
3556
- fs28.chmodSync(file, SECURE_FILE_MODE);
3683
+ const edgeRows = await db.query(
3684
+ `MATCH (a)-[e:code_edges]->(b) RETURN a.id, b.id, e.kind, e.weight, e.label`
3685
+ );
3686
+ for (const row of edgeRows) {
3687
+ const sourceId = String(row["a.id"] ?? "");
3688
+ const targetId = String(row["b.id"] ?? "");
3689
+ const kind = String(row["e.kind"] ?? "");
3690
+ if (!sourceId || !targetId || !kind) continue;
3691
+ const edge = {
3692
+ id: `${sourceId}::${kind}::${targetId}`,
3693
+ source: sourceId,
3694
+ target: targetId,
3695
+ kind,
3696
+ weight: row["e.weight"] != null ? Number(row["e.weight"]) : void 0,
3697
+ label: row["e.label"] ? String(row["e.label"]) : void 0
3698
+ };
3699
+ graph.addEdge(edge);
3700
+ }
3557
3701
  } catch {
3558
3702
  }
3559
3703
  }
3560
- function secureWriteFile(file, data) {
3561
- secureMkdir(path30.dirname(file));
3562
- fs28.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
3563
- secureChmodFile(file);
3564
- }
3565
- function tightenDbFiles(dir) {
3566
- if (process.platform === "win32") return;
3567
- if (!fs28.existsSync(dir)) return;
3568
- for (const name of fs28.readdirSync(dir)) {
3569
- if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
3570
- try {
3571
- fs28.chmodSync(path30.join(dir, name), SECURE_FILE_MODE);
3572
- } catch {
3704
+ var TABLE_TO_KIND;
3705
+ var init_graph_from_db = __esm({
3706
+ "src/multi-repo/graph-from-db.ts"() {
3707
+ init_schema();
3708
+ TABLE_TO_KIND = Object.fromEntries(
3709
+ Object.entries(NODE_TABLE_MAP).map(([kind, table]) => [table, kind])
3710
+ );
3711
+ }
3712
+ });
3713
+ function scanForFiles(root, matcher, maxDepth = 2) {
3714
+ const results = [];
3715
+ function walk2(dir, depth) {
3716
+ if (depth > maxDepth) return;
3717
+ let entries;
3718
+ try {
3719
+ entries = fs33.readdirSync(dir, { withFileTypes: true });
3720
+ } catch {
3721
+ return;
3722
+ }
3723
+ for (const entry of entries) {
3724
+ const full = path33.join(dir, entry.name);
3725
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
3726
+ walk2(full, depth + 1);
3727
+ } else if (entry.isFile() && matcher(entry.name)) {
3728
+ results.push(full);
3573
3729
  }
3574
3730
  }
3575
3731
  }
3732
+ walk2(root, 0);
3733
+ return results;
3576
3734
  }
3577
- var SECURE_DIR_MODE, SECURE_FILE_MODE;
3578
- var init_fs_secure = __esm({
3579
- "src/shared/fs-secure.ts"() {
3580
- SECURE_DIR_MODE = 448;
3581
- SECURE_FILE_MODE = 384;
3735
+ var init_file_scanner = __esm({
3736
+ "src/multi-repo/schema-parsers/file-scanner.ts"() {
3582
3737
  }
3583
3738
  });
3584
- function getUsersDBPath() {
3585
- return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path30.join(os12.homedir(), ".code-intel", "users.db");
3739
+ function tryParseFile(filePath) {
3740
+ const ext = path33.extname(filePath).toLowerCase();
3741
+ const content = fs33.readFileSync(filePath, "utf-8");
3742
+ if (ext === ".json") {
3743
+ try {
3744
+ return JSON.parse(content);
3745
+ } catch {
3746
+ return null;
3747
+ }
3748
+ }
3749
+ return null;
3586
3750
  }
3587
- function getOrCreateUsersDB() {
3588
- if (!_usersDB) {
3589
- _usersDB = new UsersDB(getUsersDBPath());
3751
+ async function parseOpenAPIContracts(repoRoot) {
3752
+ const files = scanForFiles(repoRoot, (name) => OPENAPI_FILENAMES.has(name));
3753
+ const contracts = [];
3754
+ for (const filePath of files) {
3755
+ const spec = tryParseFile(filePath);
3756
+ if (!spec) continue;
3757
+ const paths = spec["paths"];
3758
+ if (!paths || typeof paths !== "object") continue;
3759
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
3760
+ if (!pathItem || typeof pathItem !== "object") continue;
3761
+ const ops = pathItem;
3762
+ for (const method of HTTP_METHODS) {
3763
+ if (!(method in ops)) continue;
3764
+ const operation = ops[method];
3765
+ if (!operation) continue;
3766
+ const requestBody = operation["requestBody"];
3767
+ const requestSchema = requestBody?.["content"] ? requestBody["content"]["application/json"]?.["schema"] : void 0;
3768
+ const responses = operation["responses"];
3769
+ const ok200 = responses?.["200"];
3770
+ const responseSchema = ok200?.["content"] ? ok200["content"]["application/json"]?.["schema"] : void 0;
3771
+ contracts.push({
3772
+ name: `${method.toUpperCase()} ${pathStr}`,
3773
+ kind: "route",
3774
+ method: method.toUpperCase(),
3775
+ path: pathStr,
3776
+ ...requestSchema ? { requestSchema } : {},
3777
+ ...responseSchema ? { responseSchema } : {},
3778
+ filePath
3779
+ });
3780
+ }
3781
+ }
3590
3782
  }
3591
- return _usersDB;
3783
+ return contracts;
3784
+ }
3785
+ var OPENAPI_FILENAMES, HTTP_METHODS;
3786
+ var init_openapi_parser = __esm({
3787
+ "src/multi-repo/schema-parsers/openapi-parser.ts"() {
3788
+ init_file_scanner();
3789
+ OPENAPI_FILENAMES = /* @__PURE__ */ new Set([
3790
+ "openapi.yaml",
3791
+ "openapi.json",
3792
+ "openapi.yml",
3793
+ "swagger.yaml",
3794
+ "swagger.json",
3795
+ "swagger.yml"
3796
+ ]);
3797
+ HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
3798
+ }
3799
+ });
3800
+ function extractFieldNames(block) {
3801
+ return block.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
3802
+ const m = line.match(/^(\w+)\s*[(:]/);
3803
+ return m ? m[1] : null;
3804
+ }).filter((f) => f !== null);
3805
+ }
3806
+ async function parseGraphQLContracts(repoRoot) {
3807
+ const files = scanForFiles(repoRoot, (name) => name.endsWith(".graphql") || name.endsWith(".gql"));
3808
+ const contracts = [];
3809
+ for (const filePath of files) {
3810
+ const content = fs33.readFileSync(filePath, "utf-8");
3811
+ const typeRegex = /type\s+(\w+)\s*\{([^}]+)\}/g;
3812
+ let match;
3813
+ while ((match = typeRegex.exec(content)) !== null) {
3814
+ const typeName = match[1];
3815
+ const body = match[2];
3816
+ const fields = extractFieldNames(body);
3817
+ const lcName = typeName.toLowerCase();
3818
+ if (lcName === "query") {
3819
+ for (const field of fields) {
3820
+ contracts.push({ name: `query.${field}`, kind: "graphql", operation: "query", fields, filePath });
3821
+ }
3822
+ } else if (lcName === "mutation") {
3823
+ for (const field of fields) {
3824
+ contracts.push({ name: `mutation.${field}`, kind: "graphql", operation: "mutation", fields, filePath });
3825
+ }
3826
+ } else if (lcName === "subscription") {
3827
+ for (const field of fields) {
3828
+ contracts.push({ name: `subscription.${field}`, kind: "graphql", operation: "subscription", fields, filePath });
3829
+ }
3830
+ } else {
3831
+ contracts.push({ name: `type.${typeName}`, kind: "graphql", operation: "type", fields, filePath });
3832
+ }
3833
+ }
3834
+ }
3835
+ return contracts;
3836
+ }
3837
+ var init_graphql_parser = __esm({
3838
+ "src/multi-repo/schema-parsers/graphql-parser.ts"() {
3839
+ init_file_scanner();
3840
+ }
3841
+ });
3842
+ async function parseProtoContracts(repoRoot) {
3843
+ const files = scanForFiles(repoRoot, (name) => name.endsWith(".proto"));
3844
+ const contracts = [];
3845
+ for (const filePath of files) {
3846
+ const content = fs33.readFileSync(filePath, "utf-8");
3847
+ const serviceRegex = /service\s+(\w+)\s*\{([^}]+)\}/g;
3848
+ let serviceMatch;
3849
+ while ((serviceMatch = serviceRegex.exec(content)) !== null) {
3850
+ const serviceName = serviceMatch[1];
3851
+ const body = serviceMatch[2];
3852
+ const rpcRegex = /rpc\s+(\w+)\s*\((\w+)\)\s*returns\s*\((\w+)\)/g;
3853
+ let rpcMatch;
3854
+ while ((rpcMatch = rpcRegex.exec(body)) !== null) {
3855
+ const rpcName = rpcMatch[1];
3856
+ const inputType = rpcMatch[2];
3857
+ const outputType = rpcMatch[3];
3858
+ contracts.push({
3859
+ name: `${serviceName}.${rpcName}`,
3860
+ kind: "grpc",
3861
+ serviceName,
3862
+ rpcName,
3863
+ inputType,
3864
+ outputType,
3865
+ filePath
3866
+ });
3867
+ }
3868
+ }
3869
+ }
3870
+ return contracts;
3871
+ }
3872
+ var init_proto_parser = __esm({
3873
+ "src/multi-repo/schema-parsers/proto-parser.ts"() {
3874
+ init_file_scanner();
3875
+ }
3876
+ });
3877
+
3878
+ // src/multi-repo/type-similarity.ts
3879
+ function normalizeType(t) {
3880
+ return t.toLowerCase().replace(/[\[\]?<>\s]/g, "").trim();
3881
+ }
3882
+ function paramTypeSimilarity(paramsA, paramsB) {
3883
+ if (paramsA.length === 0 && paramsB.length === 0) return 1;
3884
+ if (paramsA.length === 0 || paramsB.length === 0) return 0;
3885
+ const setA = new Set(paramsA.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
3886
+ const setB = new Set(paramsB.map((p) => normalizeType(p.type ?? "")).filter(Boolean));
3887
+ if (setA.size === 0 && setB.size === 0) return 1;
3888
+ if (setA.size === 0 || setB.size === 0) return 0;
3889
+ const intersection = [...setA].filter((x) => setB.has(x)).length;
3890
+ const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
3891
+ return intersection / union;
3892
+ }
3893
+ function returnTypeSimilarity(typeA, typeB) {
3894
+ if (!typeA || !typeB) return 0.5;
3895
+ const a = normalizeType(typeA);
3896
+ const b = normalizeType(typeB);
3897
+ if (a === b) return 1;
3898
+ const compatible = [
3899
+ ["string", "str"],
3900
+ ["number", "int"],
3901
+ ["number", "float"],
3902
+ ["number", "double"],
3903
+ ["boolean", "bool"],
3904
+ ["void", "unit"],
3905
+ ["void", "none"]
3906
+ ];
3907
+ for (const [x, y] of compatible) {
3908
+ if (a === x && b === y || a === y && b === x) return 0.8;
3909
+ }
3910
+ return 0;
3911
+ }
3912
+ function paramCountSimilarity(countA, countB) {
3913
+ const maxCount = Math.max(countA, countB, 1);
3914
+ return 1 - Math.abs(countA - countB) / maxCount;
3915
+ }
3916
+ function computeContractSimilarity(a, b, nameSim) {
3917
+ const paramsA = a.parameters ?? [];
3918
+ const paramsB = b.parameters ?? [];
3919
+ const ptSim = paramTypeSimilarity(paramsA, paramsB);
3920
+ const rtSim = returnTypeSimilarity(a.returnType, b.returnType);
3921
+ const pcSim = paramCountSimilarity(paramsA.length, paramsB.length);
3922
+ let score = 0.4 * nameSim + 0.3 * ptSim + 0.2 * rtSim + 0.1 * pcSim;
3923
+ if (ptSim > 0.8) {
3924
+ score = Math.min(1, score * 1.2);
3925
+ }
3926
+ return score;
3927
+ }
3928
+ var init_type_similarity = __esm({
3929
+ "src/multi-repo/type-similarity.ts"() {
3930
+ }
3931
+ });
3932
+
3933
+ // src/multi-repo/group-sync.ts
3934
+ var group_sync_exports = {};
3935
+ __export(group_sync_exports, {
3936
+ syncGroup: () => syncGroup
3937
+ });
3938
+ function extractContracts(graph, repoName, repoPath) {
3939
+ const contracts = [];
3940
+ for (const node of graph.allNodes()) {
3941
+ if (node.exported === true && ["function", "class", "interface", "method", "type_alias", "constant", "enum", "struct", "trait"].includes(node.kind)) {
3942
+ contracts.push({
3943
+ repoName,
3944
+ repoPath,
3945
+ kind: "export",
3946
+ name: node.name,
3947
+ nodeId: node.id,
3948
+ nodeKind: node.kind,
3949
+ filePath: node.filePath,
3950
+ signature: node.content?.split("\n")[0]?.trim(),
3951
+ parameters: node.metadata?.parameters ?? node.metadata?.params,
3952
+ returnType: node.metadata?.returnType,
3953
+ exported: node.exported
3954
+ });
3955
+ }
3956
+ if (node.kind === "route") {
3957
+ contracts.push({
3958
+ repoName,
3959
+ repoPath,
3960
+ kind: "route",
3961
+ name: node.name,
3962
+ nodeId: node.id,
3963
+ nodeKind: node.kind,
3964
+ filePath: node.filePath,
3965
+ signature: node.content?.split("\n")[0]?.trim()
3966
+ });
3967
+ }
3968
+ if (["interface", "type_alias"].includes(node.kind)) {
3969
+ const nameLower = node.name.toLowerCase();
3970
+ if (nameLower.includes("event") || nameLower.includes("message")) {
3971
+ contracts.push({
3972
+ repoName,
3973
+ repoPath,
3974
+ kind: "event",
3975
+ name: node.name,
3976
+ nodeId: node.id,
3977
+ nodeKind: node.kind,
3978
+ filePath: node.filePath
3979
+ });
3980
+ } else if (nameLower.includes("schema") || nameLower.includes("dto") || nameLower.includes("request") || nameLower.includes("response")) {
3981
+ contracts.push({
3982
+ repoName,
3983
+ repoPath,
3984
+ kind: "schema",
3985
+ name: node.name,
3986
+ nodeId: node.id,
3987
+ nodeKind: node.kind,
3988
+ filePath: node.filePath
3989
+ });
3990
+ }
3991
+ }
3992
+ }
3993
+ return contracts;
3994
+ }
3995
+ function matchContracts(allContracts) {
3996
+ const links = [];
3997
+ const byRepo = /* @__PURE__ */ new Map();
3998
+ for (const c of allContracts) {
3999
+ const arr = byRepo.get(c.repoName) ?? [];
4000
+ arr.push(c);
4001
+ byRepo.set(c.repoName, arr);
4002
+ }
4003
+ const repoNames = [...byRepo.keys()];
4004
+ for (let i = 0; i < repoNames.length; i++) {
4005
+ for (let j = 0; j < repoNames.length; j++) {
4006
+ if (i === j) continue;
4007
+ const providerContracts = byRepo.get(repoNames[i]);
4008
+ const consumerContracts = byRepo.get(repoNames[j]);
4009
+ const consumerByName = /* @__PURE__ */ new Map();
4010
+ for (const c of consumerContracts) consumerByName.set(c.name, c);
4011
+ for (const provider of providerContracts) {
4012
+ const consumer = consumerByName.get(provider.name);
4013
+ if (consumer) {
4014
+ const sameKind = provider.kind === consumer.kind;
4015
+ const typedScore = computeContractSimilarity(provider, consumer, 1);
4016
+ const confidence = sameKind ? Math.max(typedScore, 0.9) : Math.max(typedScore, 0.6);
4017
+ links.push({
4018
+ providerRepo: provider.repoName,
4019
+ providerContract: provider.name,
4020
+ consumerRepo: consumer.repoName,
4021
+ consumerContract: consumer.name,
4022
+ matchKind: provider.kind === "route" ? "route-match" : "name-match",
4023
+ confidence: Math.min(1, confidence)
4024
+ });
4025
+ } else {
4026
+ const providerLC = provider.name.toLowerCase();
4027
+ for (const c of consumerContracts) {
4028
+ if (c.name.toLowerCase().includes(providerLC) || providerLC.includes(c.name.toLowerCase())) {
4029
+ if (c.name.length >= 4 && provider.name.length >= 4) {
4030
+ links.push({
4031
+ providerRepo: provider.repoName,
4032
+ providerContract: provider.name,
4033
+ consumerRepo: c.repoName,
4034
+ consumerContract: c.name,
4035
+ matchKind: "name-match",
4036
+ confidence: 0.4
4037
+ });
4038
+ }
4039
+ }
4040
+ }
4041
+ }
4042
+ }
4043
+ }
4044
+ }
4045
+ const seen = /* @__PURE__ */ new Map();
4046
+ for (const link of links) {
4047
+ const key = `${link.providerRepo}:${link.providerContract}:${link.consumerRepo}:${link.consumerContract}`;
4048
+ const existing = seen.get(key);
4049
+ if (!existing || link.confidence > existing.confidence) {
4050
+ seen.set(key, link);
4051
+ }
4052
+ }
4053
+ return [...seen.values()].sort((a, b) => b.confidence - a.confidence);
4054
+ }
4055
+ async function syncGroup(group) {
4056
+ const registry = loadRegistry();
4057
+ const allContracts = [];
4058
+ for (const member of group.members) {
4059
+ const regEntry = registry.find((r) => r.name === member.registryName);
4060
+ if (!regEntry) {
4061
+ logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
4062
+ continue;
4063
+ }
4064
+ const dbPath = path33.join(regEntry.path, ".code-intel", "graph.db");
4065
+ if (!fs33.existsSync(dbPath)) {
4066
+ logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
4067
+ continue;
4068
+ }
4069
+ const graph = createKnowledgeGraph();
4070
+ const db = new DbManager(dbPath);
4071
+ try {
4072
+ await db.init();
4073
+ await loadGraphFromDB(graph, db);
4074
+ db.close();
4075
+ } catch (err) {
4076
+ db.close();
4077
+ logger_default.warn(` \u26A0 Could not load graph for "${member.registryName}": ${err instanceof Error ? err.message : err}`);
4078
+ continue;
4079
+ }
4080
+ const contracts = extractContracts(graph, member.registryName, regEntry.path);
4081
+ const [openapiContracts, graphqlContracts, protoContracts] = await Promise.all([
4082
+ parseOpenAPIContracts(regEntry.path).catch(() => []),
4083
+ parseGraphQLContracts(regEntry.path).catch(() => []),
4084
+ parseProtoContracts(regEntry.path).catch(() => [])
4085
+ ]);
4086
+ for (const c of openapiContracts) {
4087
+ contracts.push({
4088
+ repoName: member.registryName,
4089
+ repoPath: regEntry.path,
4090
+ kind: "route",
4091
+ name: c.name,
4092
+ nodeId: `openapi:${c.method}:${c.path}`,
4093
+ nodeKind: "route",
4094
+ filePath: c.filePath
4095
+ });
4096
+ }
4097
+ for (const c of graphqlContracts) {
4098
+ contracts.push({
4099
+ repoName: member.registryName,
4100
+ repoPath: regEntry.path,
4101
+ kind: "graphql",
4102
+ name: c.name,
4103
+ nodeId: `graphql:${c.name}`,
4104
+ nodeKind: "graphql",
4105
+ filePath: c.filePath
4106
+ });
4107
+ }
4108
+ for (const c of protoContracts) {
4109
+ contracts.push({
4110
+ repoName: member.registryName,
4111
+ repoPath: regEntry.path,
4112
+ kind: "grpc",
4113
+ name: c.name,
4114
+ nodeId: `grpc:${c.serviceName}:${c.rpcName}`,
4115
+ nodeKind: "grpc",
4116
+ filePath: c.filePath
4117
+ });
4118
+ }
4119
+ logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
4120
+ allContracts.push(...contracts);
4121
+ }
4122
+ const links = matchContracts(allContracts);
4123
+ return {
4124
+ groupName: group.name,
4125
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
4126
+ memberCount: group.members.length,
4127
+ contracts: allContracts,
4128
+ links
4129
+ };
4130
+ }
4131
+ var init_group_sync = __esm({
4132
+ "src/multi-repo/group-sync.ts"() {
4133
+ init_repo_registry();
4134
+ init_db_manager();
4135
+ init_knowledge_graph();
4136
+ init_graph_from_db();
4137
+ init_logger();
4138
+ init_openapi_parser();
4139
+ init_graphql_parser();
4140
+ init_proto_parser();
4141
+ init_type_similarity();
4142
+ }
4143
+ });
4144
+
4145
+ // src/errors/codes.ts
4146
+ var ErrorCodes, AppError;
4147
+ var init_codes = __esm({
4148
+ "src/errors/codes.ts"() {
4149
+ ErrorCodes = {
4150
+ UNAUTHORIZED: "CI-1000",
4151
+ FORBIDDEN: "CI-1001",
4152
+ NOT_FOUND: "CI-1002",
4153
+ ANALYSIS_IN_PROGRESS: "CI-1003",
4154
+ INDEX_NOT_FOUND: "CI-1004",
4155
+ DB_CORRUPTED: "CI-1042",
4156
+ RATE_LIMIT_EXCEEDED: "CI-1100",
4157
+ PAYLOAD_TOO_LARGE: "CI-1101",
4158
+ INVALID_REQUEST: "CI-1200",
4159
+ INTERNAL_ERROR: "CI-5000"
4160
+ };
4161
+ AppError = class extends Error {
4162
+ constructor(code, message, hint, statusCode = 500, docs) {
4163
+ super(message);
4164
+ this.code = code;
4165
+ this.hint = hint;
4166
+ this.statusCode = statusCode;
4167
+ this.docs = docs;
4168
+ this.name = "AppError";
4169
+ }
4170
+ code;
4171
+ hint;
4172
+ statusCode;
4173
+ docs;
4174
+ };
4175
+ }
4176
+ });
4177
+ function secureMkdir(dir) {
4178
+ fs33.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
4179
+ if (process.platform !== "win32") {
4180
+ try {
4181
+ fs33.chmodSync(dir, SECURE_DIR_MODE);
4182
+ } catch {
4183
+ }
4184
+ }
4185
+ }
4186
+ function secureChmodFile(file) {
4187
+ if (process.platform === "win32") return;
4188
+ try {
4189
+ fs33.chmodSync(file, SECURE_FILE_MODE);
4190
+ } catch {
4191
+ }
4192
+ }
4193
+ function secureWriteFile(file, data) {
4194
+ secureMkdir(path33.dirname(file));
4195
+ fs33.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
4196
+ secureChmodFile(file);
4197
+ }
4198
+ function tightenDbFiles(dir) {
4199
+ if (process.platform === "win32") return;
4200
+ if (!fs33.existsSync(dir)) return;
4201
+ for (const name of fs33.readdirSync(dir)) {
4202
+ if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
4203
+ try {
4204
+ fs33.chmodSync(path33.join(dir, name), SECURE_FILE_MODE);
4205
+ } catch {
4206
+ }
4207
+ }
4208
+ }
4209
+ }
4210
+ var SECURE_DIR_MODE, SECURE_FILE_MODE;
4211
+ var init_fs_secure = __esm({
4212
+ "src/shared/fs-secure.ts"() {
4213
+ SECURE_DIR_MODE = 448;
4214
+ SECURE_FILE_MODE = 384;
4215
+ }
4216
+ });
4217
+ function getUsersDBPath() {
4218
+ return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path33.join(os12.homedir(), ".code-intel", "users.db");
4219
+ }
4220
+ function getOrCreateUsersDB() {
4221
+ if (!_usersDB) {
4222
+ _usersDB = new UsersDB(getUsersDBPath());
4223
+ }
4224
+ return _usersDB;
3592
4225
  }
3593
4226
  var BCRYPT_ROUNDS, UsersDB, _usersDB;
3594
4227
  var init_users_db = __esm({
@@ -3598,7 +4231,7 @@ var init_users_db = __esm({
3598
4231
  UsersDB = class {
3599
4232
  db;
3600
4233
  constructor(dbPath) {
3601
- const dir = path30.dirname(dbPath);
4234
+ const dir = path33.dirname(dbPath);
3602
4235
  secureMkdir(dir);
3603
4236
  this.db = new Database(dbPath);
3604
4237
  this.db.pragma("journal_mode = WAL");
@@ -3875,7 +4508,7 @@ function getScryptN() {
3875
4508
  return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
3876
4509
  }
3877
4510
  function getSecretsPath() {
3878
- return process.env["CODE_INTEL_SECRETS_PATH"] ?? path30.join(os12.homedir(), ".code-intel", ".secrets");
4511
+ return process.env["CODE_INTEL_SECRETS_PATH"] ?? path33.join(os12.homedir(), ".code-intel", ".secrets");
3879
4512
  }
3880
4513
  function getMasterPassword() {
3881
4514
  const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
@@ -3917,12 +4550,12 @@ function decryptSecrets(encrypted) {
3917
4550
  return JSON.parse(plaintext.toString("utf8"));
3918
4551
  }
3919
4552
  function loadSecrets(secretsPath = getSecretsPath()) {
3920
- if (!fs28.existsSync(secretsPath)) return {};
3921
- const blob = fs28.readFileSync(secretsPath);
4553
+ if (!fs33.existsSync(secretsPath)) return {};
4554
+ const blob = fs33.readFileSync(secretsPath);
3922
4555
  return decryptSecrets(blob);
3923
4556
  }
3924
4557
  function saveSecrets(blob, secretsPath = getSecretsPath()) {
3925
- secureMkdir(path30.dirname(secretsPath));
4558
+ secureMkdir(path33.dirname(secretsPath));
3926
4559
  const encrypted = encryptSecrets(blob);
3927
4560
  secureWriteFile(secretsPath, encrypted);
3928
4561
  secureChmodFile(secretsPath);
@@ -4056,6 +4689,9 @@ function requireAuth(req, res, next) {
4056
4689
  }
4057
4690
  next();
4058
4691
  }
4692
+ function meetsRole(userRole, required) {
4693
+ return (ROLE_RANK[userRole] ?? 0) >= (ROLE_RANK[required] ?? 0);
4694
+ }
4059
4695
  function requireRole(...roles) {
4060
4696
  return (req, res, next) => {
4061
4697
  if (!req.user) {
@@ -4070,7 +4706,8 @@ function requireRole(...roles) {
4070
4706
  });
4071
4707
  return;
4072
4708
  }
4073
- if (!roles.includes(req.user.role)) {
4709
+ const allowed = roles.some((r) => meetsRole(req.user.role, r));
4710
+ if (!allowed) {
4074
4711
  res.status(403).json({
4075
4712
  error: {
4076
4713
  code: ErrorCodes.FORBIDDEN,
@@ -4179,7 +4816,7 @@ function clearSessionCookie() {
4179
4816
  async function verifyPassword(plain, hash) {
4180
4817
  return bcrypt.compare(plain, hash);
4181
4818
  }
4182
- var sessionStore, SESSION_COOKIE_NAME;
4819
+ var sessionStore, SESSION_COOKIE_NAME, ROLE_RANK;
4183
4820
  var init_middleware = __esm({
4184
4821
  "src/auth/middleware.ts"() {
4185
4822
  init_users_db();
@@ -4187,6 +4824,12 @@ var init_middleware = __esm({
4187
4824
  init_secret_store();
4188
4825
  sessionStore = /* @__PURE__ */ new Map();
4189
4826
  SESSION_COOKIE_NAME = "code_intel_session";
4827
+ ROLE_RANK = {
4828
+ viewer: 1,
4829
+ "repo-owner": 2,
4830
+ analyst: 3,
4831
+ admin: 4
4832
+ };
4190
4833
  }
4191
4834
  });
4192
4835
 
@@ -5267,6 +5910,117 @@ var init_websocket_server = __esm({
5267
5910
  }
5268
5911
  });
5269
5912
 
5913
+ // src/query/pr-impact.ts
5914
+ var pr_impact_exports = {};
5915
+ __export(pr_impact_exports, {
5916
+ computePRImpact: () => computePRImpact,
5917
+ parseDiffFiles: () => parseDiffFiles
5918
+ });
5919
+ function parseDiffFiles(diff) {
5920
+ const files = [];
5921
+ for (const line of diff.split("\n")) {
5922
+ const match = line.match(/^\+\+\+ b\/(.+)/);
5923
+ if (match) {
5924
+ files.push(match[1]);
5925
+ }
5926
+ }
5927
+ return files;
5928
+ }
5929
+ function computePRImpact(graph, changedFiles, maxHops) {
5930
+ const changedSymbolIds = /* @__PURE__ */ new Set();
5931
+ for (const node of graph.allNodes()) {
5932
+ if (!node.filePath) continue;
5933
+ for (const changedFile of changedFiles) {
5934
+ if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
5935
+ changedSymbolIds.add(node.id);
5936
+ break;
5937
+ }
5938
+ }
5939
+ }
5940
+ const allBlastRadiusNodes = /* @__PURE__ */ new Set();
5941
+ const changedSymbols = [];
5942
+ for (const symbolId of changedSymbolIds) {
5943
+ const symbolNode = graph.getNode(symbolId);
5944
+ if (!symbolNode) continue;
5945
+ const blastRadius = /* @__PURE__ */ new Set();
5946
+ const queue = [{ id: symbolId, depth: 0 }];
5947
+ const visited = /* @__PURE__ */ new Set();
5948
+ while (queue.length > 0) {
5949
+ const { id, depth } = queue.shift();
5950
+ if (visited.has(id) || depth > maxHops) continue;
5951
+ visited.add(id);
5952
+ if (id !== symbolId) blastRadius.add(id);
5953
+ for (const edge of graph.findEdgesTo(id)) {
5954
+ if (edge.kind === "calls" || edge.kind === "imports") {
5955
+ queue.push({ id: edge.source, depth: depth + 1 });
5956
+ }
5957
+ }
5958
+ }
5959
+ for (const id of blastRadius) allBlastRadiusNodes.add(id);
5960
+ const blastCount = blastRadius.size;
5961
+ let risk;
5962
+ if (blastCount > 50) {
5963
+ risk = "HIGH";
5964
+ } else if (blastCount >= 10) {
5965
+ risk = "MEDIUM";
5966
+ } else {
5967
+ risk = "LOW";
5968
+ }
5969
+ let callerCount = 0;
5970
+ for (const edge of graph.findEdgesTo(symbolId)) {
5971
+ if (edge.kind === "calls") callerCount++;
5972
+ }
5973
+ let testCoverage = false;
5974
+ for (const edge of graph.findEdgesTo(symbolId)) {
5975
+ if (edge.kind === "imports") {
5976
+ const callerNode = graph.getNode(edge.source);
5977
+ if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
5978
+ testCoverage = true;
5979
+ break;
5980
+ }
5981
+ }
5982
+ }
5983
+ changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
5984
+ }
5985
+ const impactedSymbols = [];
5986
+ for (const id of allBlastRadiusNodes) {
5987
+ if (changedSymbolIds.has(id)) continue;
5988
+ const node = graph.getNode(id);
5989
+ if (node) {
5990
+ impactedSymbols.push({ name: node.name, filePath: node.filePath });
5991
+ }
5992
+ }
5993
+ const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
5994
+ for (const s of changedSymbols) {
5995
+ riskSummary[s.risk]++;
5996
+ }
5997
+ const coverageGaps = [];
5998
+ for (const s of changedSymbols) {
5999
+ if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
6000
+ coverageGaps.push(`${s.name} has no test coverage`);
6001
+ }
6002
+ }
6003
+ const fileImpactCount = /* @__PURE__ */ new Map();
6004
+ for (const sym of impactedSymbols) {
6005
+ if (sym.filePath) {
6006
+ fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
6007
+ }
6008
+ }
6009
+ const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
6010
+ return {
6011
+ changedSymbols,
6012
+ impactedSymbols,
6013
+ riskSummary,
6014
+ coverageGaps,
6015
+ filesToReview,
6016
+ crossRepoImpact: null
6017
+ };
6018
+ }
6019
+ var init_pr_impact = __esm({
6020
+ "src/query/pr-impact.ts"() {
6021
+ }
6022
+ });
6023
+
5270
6024
  // src/health/dead-code.ts
5271
6025
  function detectDeadCode(graph) {
5272
6026
  const results = [];
@@ -5516,9 +6270,9 @@ var init_orphan_files = __esm({
5516
6270
  // src/health/health-score.ts
5517
6271
  var health_score_exports = {};
5518
6272
  __export(health_score_exports, {
5519
- computeHealthReport: () => computeHealthReport
6273
+ computeHealthReport: () => computeHealthReport2
5520
6274
  });
5521
- function computeHealthReport(graph, godNodeConfig) {
6275
+ function computeHealthReport2(graph, godNodeConfig) {
5522
6276
  const deadCode = detectDeadCode(graph);
5523
6277
  const cycles = detectCircularDeps(graph);
5524
6278
  const godNodes = detectGodNodes(graph, godNodeConfig);
@@ -5622,10 +6376,10 @@ var init_file_watcher = __esm({
5622
6376
  }
5623
6377
  // ── private ─────────────────────────────────────────────────────────────────
5624
6378
  readCodeIntelIgnore() {
5625
- const ignoreFile = path30.join(this.workspaceRoot, ".codeintelignore");
6379
+ const ignoreFile = path33.join(this.workspaceRoot, ".codeintelignore");
5626
6380
  try {
5627
- if (!fs28.existsSync(ignoreFile)) return [];
5628
- return fs28.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
6381
+ if (!fs33.existsSync(ignoreFile)) return [];
6382
+ return fs33.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5629
6383
  } catch {
5630
6384
  return [];
5631
6385
  }
@@ -5677,7 +6431,7 @@ var init_incremental_indexer = __esm({
5677
6431
  }
5678
6432
  const nodeIdsToRemove = /* @__PURE__ */ new Set();
5679
6433
  for (const absPath of changedFiles) {
5680
- const relPath2 = path30.relative(workspaceRoot, absPath);
6434
+ const relPath2 = path33.relative(workspaceRoot, absPath);
5681
6435
  for (const id of nodesByFilePath.get(relPath2) ?? []) nodeIdsToRemove.add(id);
5682
6436
  for (const id of nodesByFilePath.get(absPath) ?? []) nodeIdsToRemove.add(id);
5683
6437
  }
@@ -5685,12 +6439,12 @@ var init_incremental_indexer = __esm({
5685
6439
  graph.removeNodeCascade(id);
5686
6440
  nodesRemoved++;
5687
6441
  }
5688
- if (fs28.existsSync(dbPath)) {
6442
+ if (fs33.existsSync(dbPath)) {
5689
6443
  try {
5690
6444
  const db = new DbManager(dbPath);
5691
6445
  await db.init();
5692
6446
  for (const absPath of changedFiles) {
5693
- const relPath2 = path30.relative(workspaceRoot, absPath);
6447
+ const relPath2 = path33.relative(workspaceRoot, absPath);
5694
6448
  await removeNodesForFile(relPath2, db);
5695
6449
  }
5696
6450
  db.close();
@@ -5700,7 +6454,7 @@ var init_incremental_indexer = __esm({
5700
6454
  }
5701
6455
  const existingFiles = changedFiles.filter((f) => {
5702
6456
  try {
5703
- return fs28.statSync(f).isFile();
6457
+ return fs33.statSync(f).isFile();
5704
6458
  } catch {
5705
6459
  return false;
5706
6460
  }
@@ -5722,13 +6476,13 @@ var init_incremental_indexer = __esm({
5722
6476
  await runPipeline([noopScan, parsePhase, resolvePhase], context2);
5723
6477
  }
5724
6478
  const nodesAdded = Math.max(0, graph.size.nodes - (nodesBeforeParse - nodesRemoved));
5725
- if (fs28.existsSync(dbPath) && existingFiles.length > 0) {
6479
+ if (fs33.existsSync(dbPath) && existingFiles.length > 0) {
5726
6480
  try {
5727
6481
  const db = new DbManager(dbPath);
5728
6482
  await db.init();
5729
- const changedRelPaths = new Set(changedFiles.map((f) => path30.relative(workspaceRoot, f)));
6483
+ const changedRelPaths = new Set(changedFiles.map((f) => path33.relative(workspaceRoot, f)));
5730
6484
  const nodesToUpsert = [...graph.allNodes()].filter(
5731
- (n) => changedRelPaths.has(n.filePath) || changedRelPaths.has(path30.relative(workspaceRoot, n.filePath))
6485
+ (n) => changedRelPaths.has(n.filePath) || changedRelPaths.has(path33.relative(workspaceRoot, n.filePath))
5732
6486
  );
5733
6487
  await upsertNodes(nodesToUpsert, db);
5734
6488
  db.close();
@@ -5759,35 +6513,35 @@ __export(saved_queries_exports, {
5759
6513
  saveQuery: () => saveQuery
5760
6514
  });
5761
6515
  function getQueriesDir(workspaceRoot) {
5762
- return path30.join(workspaceRoot, ".code-intel", "queries");
6516
+ return path33.join(workspaceRoot, ".code-intel", "queries");
5763
6517
  }
5764
6518
  function ensureQueriesDir(workspaceRoot) {
5765
6519
  const dir = getQueriesDir(workspaceRoot);
5766
- if (!fs28.existsSync(dir)) {
5767
- fs28.mkdirSync(dir, { recursive: true });
6520
+ if (!fs33.existsSync(dir)) {
6521
+ fs33.mkdirSync(dir, { recursive: true });
5768
6522
  }
5769
6523
  return dir;
5770
6524
  }
5771
6525
  function saveQuery(workspaceRoot, name, gql) {
5772
6526
  const dir = ensureQueriesDir(workspaceRoot);
5773
- const filePath = path30.join(dir, `${name}.gql`);
5774
- fs28.writeFileSync(filePath, gql, "utf-8");
6527
+ const filePath = path33.join(dir, `${name}.gql`);
6528
+ fs33.writeFileSync(filePath, gql, "utf-8");
5775
6529
  }
5776
6530
  function loadQuery(workspaceRoot, name) {
5777
6531
  const dir = getQueriesDir(workspaceRoot);
5778
- const filePath = path30.join(dir, `${name}.gql`);
5779
- if (!fs28.existsSync(filePath)) return null;
5780
- return fs28.readFileSync(filePath, "utf-8");
6532
+ const filePath = path33.join(dir, `${name}.gql`);
6533
+ if (!fs33.existsSync(filePath)) return null;
6534
+ return fs33.readFileSync(filePath, "utf-8");
5781
6535
  }
5782
6536
  function listQueries(workspaceRoot) {
5783
6537
  const dir = getQueriesDir(workspaceRoot);
5784
- if (!fs28.existsSync(dir)) return [];
5785
- const files = fs28.readdirSync(dir).filter((f) => f.endsWith(".gql"));
6538
+ if (!fs33.existsSync(dir)) return [];
6539
+ const files = fs33.readdirSync(dir).filter((f) => f.endsWith(".gql"));
5786
6540
  return files.map((f) => {
5787
- const filePath = path30.join(dir, f);
6541
+ const filePath = path33.join(dir, f);
5788
6542
  const name = f.replace(/\.gql$/, "");
5789
- const content = fs28.readFileSync(filePath, "utf-8");
5790
- const stat = fs28.statSync(filePath);
6543
+ const content = fs33.readFileSync(filePath, "utf-8");
6544
+ const stat = fs33.statSync(filePath);
5791
6545
  return {
5792
6546
  name,
5793
6547
  content,
@@ -5798,146 +6552,85 @@ function listQueries(workspaceRoot) {
5798
6552
  }
5799
6553
  function deleteQuery(workspaceRoot, name) {
5800
6554
  const dir = getQueriesDir(workspaceRoot);
5801
- const filePath = path30.join(dir, `${name}.gql`);
5802
- if (!fs28.existsSync(filePath)) return false;
5803
- fs28.unlinkSync(filePath);
6555
+ const filePath = path33.join(dir, `${name}.gql`);
6556
+ if (!fs33.existsSync(filePath)) return false;
6557
+ fs33.unlinkSync(filePath);
5804
6558
  return true;
5805
6559
  }
5806
6560
  function queryExists(workspaceRoot, name) {
5807
6561
  const dir = getQueriesDir(workspaceRoot);
5808
- const filePath = path30.join(dir, `${name}.gql`);
5809
- return fs28.existsSync(filePath);
6562
+ const filePath = path33.join(dir, `${name}.gql`);
6563
+ return fs33.existsSync(filePath);
5810
6564
  }
5811
6565
  var init_saved_queries = __esm({
5812
6566
  "src/query/saved-queries.ts"() {
5813
6567
  }
5814
6568
  });
5815
6569
 
5816
- // src/cli/main.ts
5817
- init_logger();
5818
-
5819
- // src/graph/knowledge-graph.ts
5820
- function createKnowledgeGraph() {
5821
- const nodes = /* @__PURE__ */ new Map();
5822
- const edges = /* @__PURE__ */ new Map();
5823
- const edgesByKind = /* @__PURE__ */ new Map();
5824
- const edgesFromNode = /* @__PURE__ */ new Map();
5825
- const edgesToNode = /* @__PURE__ */ new Map();
5826
- function indexEdge(edge) {
5827
- let kindSet = edgesByKind.get(edge.kind);
5828
- if (!kindSet) {
5829
- kindSet = /* @__PURE__ */ new Set();
5830
- edgesByKind.set(edge.kind, kindSet);
5831
- }
5832
- kindSet.add(edge.id);
5833
- let fromSet = edgesFromNode.get(edge.source);
5834
- if (!fromSet) {
5835
- fromSet = /* @__PURE__ */ new Set();
5836
- edgesFromNode.set(edge.source, fromSet);
5837
- }
5838
- fromSet.add(edge.id);
5839
- let toSet = edgesToNode.get(edge.target);
5840
- if (!toSet) {
5841
- toSet = /* @__PURE__ */ new Set();
5842
- edgesToNode.set(edge.target, toSet);
5843
- }
5844
- toSet.add(edge.id);
5845
- }
5846
- function unindexEdge(edge) {
5847
- edgesByKind.get(edge.kind)?.delete(edge.id);
5848
- edgesFromNode.get(edge.source)?.delete(edge.id);
5849
- edgesToNode.get(edge.target)?.delete(edge.id);
5850
- }
5851
- return {
5852
- addNode(node) {
5853
- nodes.set(node.id, node);
5854
- },
5855
- addEdge(edge) {
5856
- edges.set(edge.id, edge);
5857
- indexEdge(edge);
5858
- },
5859
- getNode(id) {
5860
- return nodes.get(id);
5861
- },
5862
- getEdge(id) {
5863
- return edges.get(id);
5864
- },
5865
- *findEdgesByKind(kind) {
5866
- const ids = edgesByKind.get(kind);
5867
- if (!ids) return;
5868
- for (const id of ids) {
5869
- const edge = edges.get(id);
5870
- if (edge) yield edge;
5871
- }
5872
- },
5873
- *findEdgesFrom(sourceId) {
5874
- const ids = edgesFromNode.get(sourceId);
5875
- if (!ids) return;
5876
- for (const id of ids) {
5877
- const edge = edges.get(id);
5878
- if (edge) yield edge;
5879
- }
5880
- },
5881
- *findEdgesTo(targetId) {
5882
- const ids = edgesToNode.get(targetId);
5883
- if (!ids) return;
5884
- for (const id of ids) {
5885
- const edge = edges.get(id);
5886
- if (edge) yield edge;
5887
- }
5888
- },
5889
- removeNodeCascade(id) {
5890
- const fromEdges = edgesFromNode.get(id);
5891
- if (fromEdges) {
5892
- for (const edgeId of [...fromEdges]) {
5893
- const edge = edges.get(edgeId);
5894
- if (edge) {
5895
- unindexEdge(edge);
5896
- edges.delete(edgeId);
6570
+ // src/cli/sarif-builder.ts
6571
+ var sarif_builder_exports = {};
6572
+ __export(sarif_builder_exports, {
6573
+ buildSARIF: () => buildSARIF
6574
+ });
6575
+ function buildSARIF(result, version) {
6576
+ const results = [];
6577
+ for (const sym of result.changedSymbols) {
6578
+ if (sym.risk !== "HIGH" && sym.risk !== "MEDIUM") continue;
6579
+ const ruleId = sym.risk === "HIGH" ? "HIGH-RISK-SYMBOL" : "MEDIUM-RISK-SYMBOL";
6580
+ const level = sym.risk === "HIGH" ? "error" : "warning";
6581
+ const uri = result.filesToReview[0] ?? "unknown";
6582
+ results.push({
6583
+ ruleId,
6584
+ level,
6585
+ message: {
6586
+ text: `${sym.name} has blast radius of ${sym.callerCount} callers (${sym.risk} risk)`
6587
+ },
6588
+ locations: [
6589
+ {
6590
+ physicalLocation: {
6591
+ artifactLocation: { uri },
6592
+ region: { startLine: 1 }
5897
6593
  }
5898
6594
  }
5899
- }
5900
- const toEdges = edgesToNode.get(id);
5901
- if (toEdges) {
5902
- for (const edgeId of [...toEdges]) {
5903
- const edge = edges.get(edgeId);
5904
- if (edge) {
5905
- unindexEdge(edge);
5906
- edges.delete(edgeId);
6595
+ ]
6596
+ });
6597
+ }
6598
+ return {
6599
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
6600
+ version: "2.1.0",
6601
+ runs: [
6602
+ {
6603
+ tool: {
6604
+ driver: {
6605
+ name: "code-intel",
6606
+ version,
6607
+ rules: [
6608
+ {
6609
+ id: "HIGH-RISK-SYMBOL",
6610
+ name: "HighRiskSymbol",
6611
+ shortDescription: { text: "Symbol has high blast radius" }
6612
+ },
6613
+ {
6614
+ id: "MEDIUM-RISK-SYMBOL",
6615
+ name: "MediumRiskSymbol",
6616
+ shortDescription: { text: "Symbol has medium blast radius" }
6617
+ }
6618
+ ]
5907
6619
  }
5908
- }
5909
- }
5910
- edgesFromNode.delete(id);
5911
- edgesToNode.delete(id);
5912
- nodes.delete(id);
5913
- },
5914
- removeEdge(id) {
5915
- const edge = edges.get(id);
5916
- if (edge) {
5917
- unindexEdge(edge);
5918
- edges.delete(id);
6620
+ },
6621
+ results
5919
6622
  }
5920
- },
5921
- *allNodes() {
5922
- yield* nodes.values();
5923
- },
5924
- *allEdges() {
5925
- yield* edges.values();
5926
- },
5927
- get size() {
5928
- return { nodes: nodes.size, edges: edges.size };
5929
- },
5930
- clear() {
5931
- nodes.clear();
5932
- edges.clear();
5933
- edgesByKind.clear();
5934
- edgesFromNode.clear();
5935
- edgesToNode.clear();
5936
- }
6623
+ ]
5937
6624
  };
5938
6625
  }
6626
+ var init_sarif_builder = __esm({
6627
+ "src/cli/sarif-builder.ts"() {
6628
+ }
6629
+ });
5939
6630
 
5940
6631
  // src/cli/main.ts
6632
+ init_logger();
6633
+ init_knowledge_graph();
5941
6634
  init_orchestrator();
5942
6635
 
5943
6636
  // src/pipeline/phases/scan-phase.ts
@@ -5976,7 +6669,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
5976
6669
  ]);
5977
6670
  function loadIgnorePatterns(workspaceRoot) {
5978
6671
  try {
5979
- const raw = fs28.readFileSync(path30.join(workspaceRoot, ".codeintelignore"), "utf-8");
6672
+ const raw = fs33.readFileSync(path33.join(workspaceRoot, ".codeintelignore"), "utf-8");
5980
6673
  const extras = /* @__PURE__ */ new Set();
5981
6674
  for (const line of raw.split("\n")) {
5982
6675
  const trimmed = line.trim();
@@ -6000,7 +6693,7 @@ var scanPhase = {
6000
6693
  function walk2(dir) {
6001
6694
  let entries;
6002
6695
  try {
6003
- entries = fs28.readdirSync(dir, { withFileTypes: true });
6696
+ entries = fs33.readdirSync(dir, { withFileTypes: true });
6004
6697
  } catch {
6005
6698
  return;
6006
6699
  }
@@ -6009,15 +6702,15 @@ var scanPhase = {
6009
6702
  if (entry.name.startsWith(".")) continue;
6010
6703
  if (IGNORED_DIRS.has(entry.name)) continue;
6011
6704
  if (extraIgnore.has(entry.name)) continue;
6012
- walk2(path30.join(dir, entry.name));
6705
+ walk2(path33.join(dir, entry.name));
6013
6706
  } else if (entry.isFile()) {
6014
6707
  const name = entry.name;
6015
6708
  if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
6016
- const ext = path30.extname(name);
6709
+ const ext = path33.extname(name);
6017
6710
  if (!extensions.has(ext)) continue;
6018
- const fullPath = path30.join(dir, name);
6711
+ const fullPath = path33.join(dir, name);
6019
6712
  try {
6020
- const stat = fs28.statSync(fullPath);
6713
+ const stat = fs33.statSync(fullPath);
6021
6714
  if (stat.size > MAX_FILE_SIZE_BYTES) continue;
6022
6715
  } catch {
6023
6716
  continue;
@@ -6044,20 +6737,20 @@ var structurePhase = {
6044
6737
  const dirs = /* @__PURE__ */ new Set();
6045
6738
  let structDone = 0;
6046
6739
  for (const filePath of context2.filePaths) {
6047
- const relativePath = path30.relative(context2.workspaceRoot, filePath);
6740
+ const relativePath = path33.relative(context2.workspaceRoot, filePath);
6048
6741
  const lang = detectLanguage(filePath);
6049
6742
  context2.graph.addNode({
6050
6743
  id: generateNodeId("file", relativePath, relativePath),
6051
6744
  kind: "file",
6052
- name: path30.basename(filePath),
6745
+ name: path33.basename(filePath),
6053
6746
  filePath: relativePath,
6054
6747
  metadata: lang ? { language: lang } : void 0
6055
6748
  });
6056
- let dir = path30.dirname(relativePath);
6749
+ let dir = path33.dirname(relativePath);
6057
6750
  while (dir && dir !== "." && dir !== "") {
6058
6751
  if (dirs.has(dir)) break;
6059
6752
  dirs.add(dir);
6060
- dir = path30.dirname(dir);
6753
+ dir = path33.dirname(dir);
6061
6754
  }
6062
6755
  structDone++;
6063
6756
  context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
@@ -6066,7 +6759,7 @@ var structurePhase = {
6066
6759
  context2.graph.addNode({
6067
6760
  id: generateNodeId("directory", dir, dir),
6068
6761
  kind: "directory",
6069
- name: path30.basename(dir),
6762
+ name: path33.basename(dir),
6070
6763
  filePath: dir
6071
6764
  });
6072
6765
  }
@@ -6177,22 +6870,22 @@ var flowPhase = {
6177
6870
  const queue = [{ nodeId: ep.id, path: [ep.id] }];
6178
6871
  const visited = /* @__PURE__ */ new Set();
6179
6872
  while (queue.length > 0 && flowCount < maxFlows) {
6180
- const { nodeId, path: path31 } = queue.shift();
6181
- if (path31.length > maxDepth) continue;
6873
+ const { nodeId, path: path34 } = queue.shift();
6874
+ if (path34.length > maxDepth) continue;
6182
6875
  const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
6183
- if (callEdges.length === 0 && path31.length >= 3) {
6876
+ if (callEdges.length === 0 && path34.length >= 3) {
6184
6877
  const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
6185
6878
  graph.addNode({
6186
6879
  id: flowId,
6187
6880
  kind: "flow",
6188
6881
  name: `${ep.name} flow ${flowCount}`,
6189
6882
  filePath: ep.filePath,
6190
- metadata: { steps: path31, entryPoint: ep.name }
6883
+ metadata: { steps: path34, entryPoint: ep.name }
6191
6884
  });
6192
- for (let i = 0; i < path31.length; i++) {
6885
+ for (let i = 0; i < path34.length; i++) {
6193
6886
  graph.addEdge({
6194
- id: generateEdgeId(path31[i], flowId, `step_of_${i}`),
6195
- source: path31[i],
6887
+ id: generateEdgeId(path34[i], flowId, `step_of_${i}`),
6888
+ source: path34[i],
6196
6889
  target: flowId,
6197
6890
  kind: "step_of",
6198
6891
  weight: 1,
@@ -6205,7 +6898,7 @@ var flowPhase = {
6205
6898
  for (const edge of callEdges) {
6206
6899
  if (visited.has(edge.target)) continue;
6207
6900
  visited.add(edge.target);
6208
- queue.push({ nodeId: edge.target, path: [...path31, edge.target] });
6901
+ queue.push({ nodeId: edge.target, path: [...path34, edge.target] });
6209
6902
  }
6210
6903
  }
6211
6904
  }
@@ -6223,7 +6916,7 @@ var LLMGovernanceLogger = class {
6223
6916
  }
6224
6917
  /** Path to the JSONL log file. */
6225
6918
  getLogPath() {
6226
- return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path30.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
6919
+ return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path33.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
6227
6920
  }
6228
6921
  /**
6229
6922
  * Append an entry to the governance log.
@@ -6239,8 +6932,8 @@ var LLMGovernanceLogger = class {
6239
6932
  ...entry
6240
6933
  };
6241
6934
  const logPath = this.getLogPath();
6242
- fs28.mkdirSync(path30.dirname(logPath), { recursive: true });
6243
- fs28.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
6935
+ fs33.mkdirSync(path33.dirname(logPath), { recursive: true });
6936
+ fs33.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
6244
6937
  } catch {
6245
6938
  }
6246
6939
  }
@@ -6250,7 +6943,7 @@ var LLMGovernanceLogger = class {
6250
6943
  */
6251
6944
  readLog(limit = 100) {
6252
6945
  try {
6253
- const raw = fs28.readFileSync(this.getLogPath(), "utf-8");
6946
+ const raw = fs33.readFileSync(this.getLogPath(), "utf-8");
6254
6947
  const lines = raw.split("\n").filter((l) => l.trim().length > 0).slice(-limit);
6255
6948
  return lines.map((l) => JSON.parse(l));
6256
6949
  } catch {
@@ -6564,7 +7257,7 @@ var LANG_QUERIES2 = {
6564
7257
  };
6565
7258
  function workerScriptPath() {
6566
7259
  const thisFile = fileURLToPath(import.meta.url);
6567
- return path30.join(path30.dirname(thisFile), "parse-worker.js");
7260
+ return path33.join(path33.dirname(thisFile), "parse-worker.js");
6568
7261
  }
6569
7262
  var parsePhaseParallel = {
6570
7263
  name: "parse",
@@ -6580,14 +7273,14 @@ var parsePhaseParallel = {
6580
7273
  const batch = filePaths.slice(i, i + CONCURRENCY);
6581
7274
  await Promise.all(batch.map(async (filePath) => {
6582
7275
  try {
6583
- const source = await fs28.promises.readFile(filePath, "utf-8");
7276
+ const source = await fs33.promises.readFile(filePath, "utf-8");
6584
7277
  context2.fileCache.set(filePath, source);
6585
7278
  } catch {
6586
7279
  }
6587
7280
  }));
6588
7281
  }
6589
7282
  const workerScript = workerScriptPath();
6590
- const workerScriptExists = fs28.existsSync(workerScript);
7283
+ const workerScriptExists = fs33.existsSync(workerScript);
6591
7284
  if (!workerScriptExists || workerCount === 1) {
6592
7285
  logger_default.info(`[parse-parallel] falling back to sequential (workerCount=${workerCount}, scriptExists=${workerScriptExists})`);
6593
7286
  const { parsePhase: parsePhase2 } = await Promise.resolve().then(() => (init_parse_phase(), parse_phase_exports));
@@ -6599,7 +7292,7 @@ var parsePhaseParallel = {
6599
7292
  if (!lang) continue;
6600
7293
  const source = context2.fileCache.get(filePath);
6601
7294
  if (!source) continue;
6602
- const relativePath = path30.relative(context2.workspaceRoot, filePath);
7295
+ const relativePath = path33.relative(context2.workspaceRoot, filePath);
6603
7296
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
6604
7297
  const fileNode = context2.graph.getNode(fileNodeId);
6605
7298
  if (fileNode) fileNode.content = source.slice(0, 2e3);
@@ -6642,7 +7335,7 @@ var parsePhaseParallel = {
6642
7335
  symbolCount += res.nodes.length;
6643
7336
  if (res.usedTreeSitter) treeSitterCount++;
6644
7337
  else regexCount++;
6645
- const relativePath = path30.relative(context2.workspaceRoot, res.taskId);
7338
+ const relativePath = path33.relative(context2.workspaceRoot, res.taskId);
6646
7339
  const funcs = res.nodes.filter((n) => n.kind === "function" || n.kind === "method").map((n) => ({ id: n.id, startLine: n.startLine ?? 0, endLine: n.endLine })).sort((a, b) => a.startLine - b.startLine);
6647
7340
  if (funcs.length > 0) context2.fileFunctionIndex.set(relativePath, funcs);
6648
7341
  parseDone++;
@@ -6669,7 +7362,7 @@ init_id_generator();
6669
7362
  init_logger();
6670
7363
  function workerScriptPath2() {
6671
7364
  const thisFile = fileURLToPath(import.meta.url);
6672
- return path30.join(path30.dirname(thisFile), "resolve-worker.js");
7365
+ return path33.join(path33.dirname(thisFile), "resolve-worker.js");
6673
7366
  }
6674
7367
  var resolvePhaseParallel = {
6675
7368
  name: "resolve",
@@ -6681,11 +7374,11 @@ var resolvePhaseParallel = {
6681
7374
  const fileFunctionIndex = context2.fileFunctionIndex ?? /* @__PURE__ */ new Map();
6682
7375
  const fileIndex = {};
6683
7376
  for (const fp of filePaths) {
6684
- const rel = path30.relative(workspaceRoot, fp);
7377
+ const rel = path33.relative(workspaceRoot, fp);
6685
7378
  fileIndex[rel] = fp;
6686
7379
  const noExt = rel.replace(/\.\w+$/, "");
6687
7380
  if (!fileIndex[noExt]) fileIndex[noExt] = fp;
6688
- const base = path30.basename(rel, path30.extname(rel));
7381
+ const base = path33.basename(rel, path33.extname(rel));
6689
7382
  if (!fileIndex[base]) fileIndex[base] = fp;
6690
7383
  }
6691
7384
  const symbolIndex = {};
@@ -6699,427 +7392,202 @@ var resolvePhaseParallel = {
6699
7392
  }
6700
7393
  const snapshot = { symbolIndex, fileSymbolIndex, fileIndex, workspaceRoot };
6701
7394
  const workerScript = workerScriptPath2();
6702
- const workerScriptExists = fs28.existsSync(workerScript);
6703
- const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os12.cpus().length - 1);
6704
- if (!workerScriptExists || workerCount === 1) {
6705
- logger_default.info(`[resolve-parallel] falling back to sequential`);
6706
- const { resolvePhase: resolvePhase2 } = await Promise.resolve().then(() => (init_resolve_phase(), resolve_phase_exports));
6707
- return resolvePhase2.execute(context2, /* @__PURE__ */ new Map());
6708
- }
6709
- const tasks = [];
6710
- for (const filePath of filePaths) {
6711
- const lang = detectLanguage(filePath);
6712
- if (!lang) continue;
6713
- const source = fileCache.get(filePath);
6714
- if (!source) continue;
6715
- const relativePath = path30.relative(workspaceRoot, filePath);
6716
- const fileNodeId = generateNodeId("file", relativePath, relativePath);
6717
- const funcList = fileFunctionIndex.get(relativePath) ?? [];
6718
- tasks.push({ taskId: filePath, filePath, relativePath, fileNodeId, source, funcList });
6719
- }
6720
- let importEdges = 0;
6721
- let callEdges = 0;
6722
- let heritageEdges = 0;
6723
- let fileDone = 0;
6724
- const BATCH_SIZE = 100;
6725
- const workers = [];
6726
- for (let i = 0; i < workerCount; i++) {
6727
- workers.push({ w: new Worker(workerScript, { workerData: snapshot }), busy: false });
6728
- }
6729
- const pendingResolvers = /* @__PURE__ */ new Map();
6730
- for (const { w } of workers) {
6731
- w.on("message", (result) => {
6732
- const resolve = pendingResolvers.get(result.taskId);
6733
- if (resolve) {
6734
- pendingResolvers.delete(result.taskId);
6735
- resolve(result);
6736
- }
6737
- });
6738
- w.on("error", (err) => logger_default.warn(`[resolve-worker] error: ${err.message}`));
6739
- }
6740
- let workerIdx = 0;
6741
- function runTask(task) {
6742
- return new Promise((resolve) => {
6743
- pendingResolvers.set(task.taskId, resolve);
6744
- const { w } = workers[workerIdx % workers.length];
6745
- workerIdx++;
6746
- w.postMessage(task);
6747
- });
6748
- }
6749
- const seen = /* @__PURE__ */ new Set();
6750
- for (let i = 0; i < tasks.length; i += BATCH_SIZE) {
6751
- const batch = tasks.slice(i, i + BATCH_SIZE);
6752
- const results = await Promise.all(batch.map((t) => runTask(t)));
6753
- for (const res of results) {
6754
- if (res.error) logger_default.warn(`[resolve-parallel] task error: ${res.error}`);
6755
- for (const edge of res.edges) {
6756
- if (seen.has(edge.id)) continue;
6757
- seen.add(edge.id);
6758
- graph.addEdge(edge);
6759
- if (edge.kind === "imports") importEdges++;
6760
- else if (edge.kind === "calls") callEdges++;
6761
- else heritageEdges++;
6762
- }
6763
- fileDone++;
6764
- context2.onPhaseProgress?.("resolve", fileDone, tasks.length);
6765
- }
6766
- }
6767
- await Promise.all(workers.map(({ w }) => w.terminate()));
6768
- return {
6769
- status: "completed",
6770
- duration: Date.now() - start,
6771
- message: `Resolved ${importEdges} imports, ${callEdges} calls, ${heritageEdges} heritage edges. Graph: ${graph.size.nodes} nodes, ${graph.size.edges} edges`
6772
- };
6773
- }
6774
- };
6775
-
6776
- // src/search/text-search.ts
6777
- function textSearch(graph, query, limit = 20) {
6778
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
6779
- const results = [];
6780
- const isTestPath = (fp) => fp.includes("test") || fp.includes("spec") || fp.includes("__test");
6781
- const isDistPath = (fp) => fp.includes("/dist") || fp.includes("\\dist") || fp.includes(".d.ts");
6782
- for (const node of graph.allNodes()) {
6783
- if (["directory", "cluster", "flow"].includes(node.kind)) continue;
6784
- let score = 0;
6785
- const nameLC = node.name.toLowerCase();
6786
- const pathLC = node.filePath.toLowerCase();
6787
- for (const term of terms) {
6788
- if (nameLC === term) score += 10;
6789
- else if (nameLC.startsWith(term)) score += 7;
6790
- else if (nameLC.includes(term)) score += 5;
6791
- if (pathLC.includes(term)) score += 2;
6792
- if (node.content?.toLowerCase().includes(term)) score += 3;
6793
- }
6794
- if (score > 0) {
6795
- if (isDistPath(node.filePath)) score -= 8;
6796
- if (isTestPath(node.filePath)) score -= 4;
6797
- if (["function", "class", "interface", "method"].includes(node.kind)) score += 1;
6798
- }
6799
- if (score > 0) {
6800
- results.push({
6801
- nodeId: node.id,
6802
- name: node.name,
6803
- kind: node.kind,
6804
- filePath: node.filePath,
6805
- score,
6806
- snippet: node.content?.slice(0, 200)
6807
- });
6808
- }
6809
- }
6810
- results.sort((a, b) => b.score - a.score);
6811
- return results.slice(0, limit);
6812
- }
6813
- function reciprocalRankFusion(...rankings) {
6814
- const K = 60;
6815
- const scoreMap = /* @__PURE__ */ new Map();
6816
- for (const ranking of rankings) {
6817
- for (let rank = 0; rank < ranking.length; rank++) {
6818
- const result = ranking[rank];
6819
- const existing = scoreMap.get(result.nodeId);
6820
- const rrfContribution = 1 / (K + rank + 1);
6821
- if (existing) {
6822
- existing.rrfScore += rrfContribution;
6823
- } else {
6824
- scoreMap.set(result.nodeId, {
6825
- result,
6826
- rrfScore: rrfContribution
6827
- });
6828
- }
6829
- }
6830
- }
6831
- return [...scoreMap.values()].sort((a, b) => b.rrfScore - a.rrfScore).map((entry) => ({ ...entry.result, score: entry.rrfScore }));
6832
- }
6833
- init_vector_index();
6834
- init_embedder();
6835
- async function hybridSearch(graph, query, limit, options = {}) {
6836
- const { vectorDbPath, bm25Limit = 50, vectorLimit = 50 } = options;
6837
- const bm25Promise = Promise.resolve(textSearch(graph, query, bm25Limit));
6838
- const hasVectorDb = Boolean(vectorDbPath && fs28.existsSync(vectorDbPath));
6839
- if (!hasVectorDb) {
6840
- const bm25Results2 = await bm25Promise;
6841
- return {
6842
- results: bm25Results2.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
6843
- searchMode: "bm25"
6844
- };
6845
- }
6846
- const vectorPromise = runVectorSearch(vectorDbPath, query, vectorLimit);
6847
- const [bm25Results, vectorResults] = await Promise.all([bm25Promise, vectorPromise]);
6848
- if (vectorResults === null || vectorResults.length === 0) {
6849
- return {
6850
- results: bm25Results.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
6851
- searchMode: "bm25"
6852
- };
6853
- }
6854
- const vectorAsSearchResults = vectorResults.map((h) => ({
6855
- nodeId: h.nodeId,
6856
- name: h.name,
6857
- kind: h.kind,
6858
- filePath: h.filePath,
6859
- score: h.score,
6860
- snippet: graph.getNode(h.nodeId)?.content?.slice(0, 200)
6861
- }));
6862
- const merged = reciprocalRankFusion(bm25Results, vectorAsSearchResults);
6863
- return {
6864
- results: merged.slice(0, limit).map((r) => ({ ...r, searchMode: "hybrid" })),
6865
- searchMode: "hybrid"
6866
- };
6867
- }
6868
- async function runVectorSearch(vectorDbPath, query, topK) {
6869
- try {
6870
- const idx = new VectorIndex(vectorDbPath);
6871
- await idx.init();
6872
- const built = await idx.isBuilt();
6873
- if (!built) {
6874
- idx.close();
6875
- return null;
6876
- }
6877
- const embedder = await getEmbedder();
6878
- const out = await embedder(query, { pooling: "mean", normalize: true });
6879
- const queryEmbedding = Array.from(out.data);
6880
- const hits = await idx.search(queryEmbedding, topK);
6881
- idx.close();
6882
- return hits;
6883
- } catch {
6884
- return null;
6885
- }
6886
- }
6887
-
6888
- // src/http/app.ts
6889
- init_storage();
6890
- init_metadata();
6891
- init_vector_index();
6892
- init_group_registry();
6893
-
6894
- // src/multi-repo/group-sync.ts
6895
- init_repo_registry();
6896
- init_db_manager();
6897
-
6898
- // src/multi-repo/graph-from-db.ts
6899
- init_schema();
6900
- var TABLE_TO_KIND = Object.fromEntries(
6901
- Object.entries(NODE_TABLE_MAP).map(([kind, table]) => [table, kind])
6902
- );
6903
- function parseRow(row, kind) {
6904
- return {
6905
- id: String(row["id"] ?? ""),
6906
- kind,
6907
- name: String(row["name"] ?? ""),
6908
- filePath: String(row["file_path"] ?? ""),
6909
- startLine: row["start_line"] != null ? Number(row["start_line"]) : void 0,
6910
- endLine: row["end_line"] != null ? Number(row["end_line"]) : void 0,
6911
- exported: row["exported"] != null ? Boolean(row["exported"]) : void 0,
6912
- content: row["content"] ? String(row["content"]) : void 0,
6913
- metadata: row["metadata"] ? (() => {
6914
- try {
6915
- return JSON.parse(String(row["metadata"]));
6916
- } catch {
6917
- return void 0;
6918
- }
6919
- })() : void 0
6920
- };
6921
- }
6922
- async function loadGraphFromDB(graph, db) {
6923
- for (const table of ALL_NODE_TABLES) {
6924
- const kind = TABLE_TO_KIND[table];
6925
- if (!kind) continue;
6926
- let rows = [];
6927
- try {
6928
- rows = await db.query(`MATCH (n:${table}) RETURN n.id, n.name, n.file_path, n.start_line, n.end_line, n.exported, n.content, n.metadata`);
6929
- } catch {
6930
- continue;
6931
- }
6932
- for (const row of rows) {
6933
- const node = parseRow({
6934
- id: row["n.id"],
6935
- name: row["n.name"],
6936
- file_path: row["n.file_path"],
6937
- start_line: row["n.start_line"],
6938
- end_line: row["n.end_line"],
6939
- exported: row["n.exported"],
6940
- content: row["n.content"],
6941
- metadata: row["n.metadata"]
6942
- }, kind);
6943
- if (node.id && node.name) graph.addNode(node);
7395
+ const workerScriptExists = fs33.existsSync(workerScript);
7396
+ const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os12.cpus().length - 1);
7397
+ if (!workerScriptExists || workerCount === 1) {
7398
+ logger_default.info(`[resolve-parallel] falling back to sequential`);
7399
+ const { resolvePhase: resolvePhase2 } = await Promise.resolve().then(() => (init_resolve_phase(), resolve_phase_exports));
7400
+ return resolvePhase2.execute(context2, /* @__PURE__ */ new Map());
6944
7401
  }
6945
- }
6946
- try {
6947
- const edgeRows = await db.query(
6948
- `MATCH (a)-[e:code_edges]->(b) RETURN a.id, b.id, e.kind, e.weight, e.label`
6949
- );
6950
- for (const row of edgeRows) {
6951
- const sourceId = String(row["a.id"] ?? "");
6952
- const targetId = String(row["b.id"] ?? "");
6953
- const kind = String(row["e.kind"] ?? "");
6954
- if (!sourceId || !targetId || !kind) continue;
6955
- const edge = {
6956
- id: `${sourceId}::${kind}::${targetId}`,
6957
- source: sourceId,
6958
- target: targetId,
6959
- kind,
6960
- weight: row["e.weight"] != null ? Number(row["e.weight"]) : void 0,
6961
- label: row["e.label"] ? String(row["e.label"]) : void 0
6962
- };
6963
- graph.addEdge(edge);
7402
+ const tasks = [];
7403
+ for (const filePath of filePaths) {
7404
+ const lang = detectLanguage(filePath);
7405
+ if (!lang) continue;
7406
+ const source = fileCache.get(filePath);
7407
+ if (!source) continue;
7408
+ const relativePath = path33.relative(workspaceRoot, filePath);
7409
+ const fileNodeId = generateNodeId("file", relativePath, relativePath);
7410
+ const funcList = fileFunctionIndex.get(relativePath) ?? [];
7411
+ tasks.push({ taskId: filePath, filePath, relativePath, fileNodeId, source, funcList });
6964
7412
  }
6965
- } catch {
6966
- }
6967
- }
6968
-
6969
- // src/multi-repo/group-sync.ts
6970
- init_logger();
6971
- function extractContracts(graph, repoName, repoPath) {
6972
- const contracts = [];
6973
- for (const node of graph.allNodes()) {
6974
- if (node.exported === true && ["function", "class", "interface", "method", "type_alias", "constant", "enum", "struct", "trait"].includes(node.kind)) {
6975
- contracts.push({
6976
- repoName,
6977
- repoPath,
6978
- kind: "export",
6979
- name: node.name,
6980
- nodeId: node.id,
6981
- nodeKind: node.kind,
6982
- filePath: node.filePath,
6983
- signature: node.content?.split("\n")[0]?.trim()
6984
- });
7413
+ let importEdges = 0;
7414
+ let callEdges = 0;
7415
+ let heritageEdges = 0;
7416
+ let fileDone = 0;
7417
+ const BATCH_SIZE = 100;
7418
+ const workers = [];
7419
+ for (let i = 0; i < workerCount; i++) {
7420
+ workers.push({ w: new Worker(workerScript, { workerData: snapshot }), busy: false });
6985
7421
  }
6986
- if (node.kind === "route") {
6987
- contracts.push({
6988
- repoName,
6989
- repoPath,
6990
- kind: "route",
6991
- name: node.name,
6992
- nodeId: node.id,
6993
- nodeKind: node.kind,
6994
- filePath: node.filePath,
6995
- signature: node.content?.split("\n")[0]?.trim()
7422
+ const pendingResolvers = /* @__PURE__ */ new Map();
7423
+ for (const { w } of workers) {
7424
+ w.on("message", (result) => {
7425
+ const resolve = pendingResolvers.get(result.taskId);
7426
+ if (resolve) {
7427
+ pendingResolvers.delete(result.taskId);
7428
+ resolve(result);
7429
+ }
6996
7430
  });
7431
+ w.on("error", (err) => logger_default.warn(`[resolve-worker] error: ${err.message}`));
6997
7432
  }
6998
- if (["interface", "type_alias"].includes(node.kind)) {
6999
- const nameLower = node.name.toLowerCase();
7000
- if (nameLower.includes("event") || nameLower.includes("message")) {
7001
- contracts.push({
7002
- repoName,
7003
- repoPath,
7004
- kind: "event",
7005
- name: node.name,
7006
- nodeId: node.id,
7007
- nodeKind: node.kind,
7008
- filePath: node.filePath
7009
- });
7010
- } else if (nameLower.includes("schema") || nameLower.includes("dto") || nameLower.includes("request") || nameLower.includes("response")) {
7011
- contracts.push({
7012
- repoName,
7013
- repoPath,
7014
- kind: "schema",
7015
- name: node.name,
7016
- nodeId: node.id,
7017
- nodeKind: node.kind,
7018
- filePath: node.filePath
7019
- });
7020
- }
7433
+ let workerIdx = 0;
7434
+ function runTask(task) {
7435
+ return new Promise((resolve) => {
7436
+ pendingResolvers.set(task.taskId, resolve);
7437
+ const { w } = workers[workerIdx % workers.length];
7438
+ workerIdx++;
7439
+ w.postMessage(task);
7440
+ });
7021
7441
  }
7022
- }
7023
- return contracts;
7024
- }
7025
- function matchContracts(allContracts) {
7026
- const links = [];
7027
- const byRepo = /* @__PURE__ */ new Map();
7028
- for (const c of allContracts) {
7029
- const arr = byRepo.get(c.repoName) ?? [];
7030
- arr.push(c);
7031
- byRepo.set(c.repoName, arr);
7032
- }
7033
- const repoNames = [...byRepo.keys()];
7034
- for (let i = 0; i < repoNames.length; i++) {
7035
- for (let j = 0; j < repoNames.length; j++) {
7036
- if (i === j) continue;
7037
- const providerContracts = byRepo.get(repoNames[i]);
7038
- const consumerContracts = byRepo.get(repoNames[j]);
7039
- const consumerByName = /* @__PURE__ */ new Map();
7040
- for (const c of consumerContracts) consumerByName.set(c.name, c);
7041
- for (const provider of providerContracts) {
7042
- const consumer = consumerByName.get(provider.name);
7043
- if (consumer) {
7044
- const sameKind = provider.kind === consumer.kind;
7045
- links.push({
7046
- providerRepo: provider.repoName,
7047
- providerContract: provider.name,
7048
- consumerRepo: consumer.repoName,
7049
- consumerContract: consumer.name,
7050
- matchKind: provider.kind === "route" ? "route-match" : "name-match",
7051
- confidence: sameKind ? 0.9 : 0.6
7052
- });
7053
- } else {
7054
- const providerLC = provider.name.toLowerCase();
7055
- for (const c of consumerContracts) {
7056
- if (c.name.toLowerCase().includes(providerLC) || providerLC.includes(c.name.toLowerCase())) {
7057
- if (c.name.length >= 4 && provider.name.length >= 4) {
7058
- links.push({
7059
- providerRepo: provider.repoName,
7060
- providerContract: provider.name,
7061
- consumerRepo: c.repoName,
7062
- consumerContract: c.name,
7063
- matchKind: "name-match",
7064
- confidence: 0.4
7065
- });
7066
- }
7067
- }
7068
- }
7442
+ const seen = /* @__PURE__ */ new Set();
7443
+ for (let i = 0; i < tasks.length; i += BATCH_SIZE) {
7444
+ const batch = tasks.slice(i, i + BATCH_SIZE);
7445
+ const results = await Promise.all(batch.map((t) => runTask(t)));
7446
+ for (const res of results) {
7447
+ if (res.error) logger_default.warn(`[resolve-parallel] task error: ${res.error}`);
7448
+ for (const edge of res.edges) {
7449
+ if (seen.has(edge.id)) continue;
7450
+ seen.add(edge.id);
7451
+ graph.addEdge(edge);
7452
+ if (edge.kind === "imports") importEdges++;
7453
+ else if (edge.kind === "calls") callEdges++;
7454
+ else heritageEdges++;
7069
7455
  }
7456
+ fileDone++;
7457
+ context2.onPhaseProgress?.("resolve", fileDone, tasks.length);
7070
7458
  }
7071
7459
  }
7460
+ await Promise.all(workers.map(({ w }) => w.terminate()));
7461
+ return {
7462
+ status: "completed",
7463
+ duration: Date.now() - start,
7464
+ message: `Resolved ${importEdges} imports, ${callEdges} calls, ${heritageEdges} heritage edges. Graph: ${graph.size.nodes} nodes, ${graph.size.edges} edges`
7465
+ };
7072
7466
  }
7073
- const seen = /* @__PURE__ */ new Map();
7074
- for (const link of links) {
7075
- const key = `${link.providerRepo}:${link.providerContract}:${link.consumerRepo}:${link.consumerContract}`;
7076
- const existing = seen.get(key);
7077
- if (!existing || link.confidence > existing.confidence) {
7078
- seen.set(key, link);
7467
+ };
7468
+
7469
+ // src/search/text-search.ts
7470
+ function textSearch(graph, query, limit = 20) {
7471
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
7472
+ const results = [];
7473
+ const isTestPath = (fp) => fp.includes("test") || fp.includes("spec") || fp.includes("__test");
7474
+ const isDistPath = (fp) => fp.includes("/dist") || fp.includes("\\dist") || fp.includes(".d.ts");
7475
+ for (const node of graph.allNodes()) {
7476
+ if (["directory", "cluster", "flow"].includes(node.kind)) continue;
7477
+ let score = 0;
7478
+ const nameLC = node.name.toLowerCase();
7479
+ const pathLC = node.filePath.toLowerCase();
7480
+ for (const term of terms) {
7481
+ if (nameLC === term) score += 10;
7482
+ else if (nameLC.startsWith(term)) score += 7;
7483
+ else if (nameLC.includes(term)) score += 5;
7484
+ if (pathLC.includes(term)) score += 2;
7485
+ if (node.content?.toLowerCase().includes(term)) score += 3;
7079
7486
  }
7080
- }
7081
- return [...seen.values()].sort((a, b) => b.confidence - a.confidence);
7082
- }
7083
- async function syncGroup(group) {
7084
- const registry = loadRegistry();
7085
- const allContracts = [];
7086
- for (const member of group.members) {
7087
- const regEntry = registry.find((r) => r.name === member.registryName);
7088
- if (!regEntry) {
7089
- logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
7090
- continue;
7487
+ if (score > 0) {
7488
+ if (isDistPath(node.filePath)) score -= 8;
7489
+ if (isTestPath(node.filePath)) score -= 4;
7490
+ if (["function", "class", "interface", "method"].includes(node.kind)) score += 1;
7091
7491
  }
7092
- const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
7093
- if (!fs28.existsSync(dbPath)) {
7094
- logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
7095
- continue;
7492
+ if (score > 0) {
7493
+ results.push({
7494
+ nodeId: node.id,
7495
+ name: node.name,
7496
+ kind: node.kind,
7497
+ filePath: node.filePath,
7498
+ score,
7499
+ snippet: node.content?.slice(0, 200)
7500
+ });
7096
7501
  }
7097
- const graph = createKnowledgeGraph();
7098
- const db = new DbManager(dbPath);
7099
- try {
7100
- await db.init();
7101
- await loadGraphFromDB(graph, db);
7102
- db.close();
7103
- } catch (err) {
7104
- db.close();
7105
- logger_default.warn(` \u26A0 Could not load graph for "${member.registryName}": ${err instanceof Error ? err.message : err}`);
7106
- continue;
7502
+ }
7503
+ results.sort((a, b) => b.score - a.score);
7504
+ return results.slice(0, limit);
7505
+ }
7506
+ function reciprocalRankFusion(...rankings) {
7507
+ const K = 60;
7508
+ const scoreMap = /* @__PURE__ */ new Map();
7509
+ for (const ranking of rankings) {
7510
+ for (let rank = 0; rank < ranking.length; rank++) {
7511
+ const result = ranking[rank];
7512
+ const existing = scoreMap.get(result.nodeId);
7513
+ const rrfContribution = 1 / (K + rank + 1);
7514
+ if (existing) {
7515
+ existing.rrfScore += rrfContribution;
7516
+ } else {
7517
+ scoreMap.set(result.nodeId, {
7518
+ result,
7519
+ rrfScore: rrfContribution
7520
+ });
7521
+ }
7107
7522
  }
7108
- const contracts = extractContracts(graph, member.registryName, regEntry.path);
7109
- logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
7110
- allContracts.push(...contracts);
7111
7523
  }
7112
- const links = matchContracts(allContracts);
7524
+ return [...scoreMap.values()].sort((a, b) => b.rrfScore - a.rrfScore).map((entry) => ({ ...entry.result, score: entry.rrfScore }));
7525
+ }
7526
+ init_vector_index();
7527
+ init_embedder();
7528
+ async function hybridSearch(graph, query, limit, options = {}) {
7529
+ const { vectorDbPath, bm25Limit = 50, vectorLimit = 50 } = options;
7530
+ const bm25Promise = Promise.resolve(textSearch(graph, query, bm25Limit));
7531
+ const hasVectorDb = Boolean(vectorDbPath && fs33.existsSync(vectorDbPath));
7532
+ if (!hasVectorDb) {
7533
+ const bm25Results2 = await bm25Promise;
7534
+ return {
7535
+ results: bm25Results2.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
7536
+ searchMode: "bm25"
7537
+ };
7538
+ }
7539
+ const vectorPromise = runVectorSearch(vectorDbPath, query, vectorLimit);
7540
+ const [bm25Results, vectorResults] = await Promise.all([bm25Promise, vectorPromise]);
7541
+ if (vectorResults === null || vectorResults.length === 0) {
7542
+ return {
7543
+ results: bm25Results.slice(0, limit).map((r) => ({ ...r, searchMode: "bm25" })),
7544
+ searchMode: "bm25"
7545
+ };
7546
+ }
7547
+ const vectorAsSearchResults = vectorResults.map((h) => ({
7548
+ nodeId: h.nodeId,
7549
+ name: h.name,
7550
+ kind: h.kind,
7551
+ filePath: h.filePath,
7552
+ score: h.score,
7553
+ snippet: graph.getNode(h.nodeId)?.content?.slice(0, 200)
7554
+ }));
7555
+ const merged = reciprocalRankFusion(bm25Results, vectorAsSearchResults);
7113
7556
  return {
7114
- groupName: group.name,
7115
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
7116
- memberCount: group.members.length,
7117
- contracts: allContracts,
7118
- links
7557
+ results: merged.slice(0, limit).map((r) => ({ ...r, searchMode: "hybrid" })),
7558
+ searchMode: "hybrid"
7119
7559
  };
7120
7560
  }
7561
+ async function runVectorSearch(vectorDbPath, query, topK) {
7562
+ try {
7563
+ const idx = new VectorIndex(vectorDbPath);
7564
+ await idx.init();
7565
+ const built = await idx.isBuilt();
7566
+ if (!built) {
7567
+ idx.close();
7568
+ return null;
7569
+ }
7570
+ const embedder = await getEmbedder();
7571
+ const out = await embedder(query, { pooling: "mean", normalize: true });
7572
+ const queryEmbedding = Array.from(out.data);
7573
+ const hits = await idx.search(queryEmbedding, topK);
7574
+ idx.close();
7575
+ return hits;
7576
+ } catch {
7577
+ return null;
7578
+ }
7579
+ }
7580
+
7581
+ // src/http/app.ts
7582
+ init_storage();
7583
+ init_metadata();
7584
+ init_vector_index();
7585
+ init_group_registry();
7586
+ init_group_sync();
7121
7587
  init_repo_registry();
7122
7588
  init_db_manager();
7589
+ init_knowledge_graph();
7590
+ init_graph_from_db();
7123
7591
  async function queryGroup(group, query, limit = 20) {
7124
7592
  const registry = loadRegistry();
7125
7593
  const perRepo = [];
@@ -7127,8 +7595,8 @@ async function queryGroup(group, query, limit = 20) {
7127
7595
  for (const member of group.members) {
7128
7596
  const regEntry = registry.find((r) => r.name === member.registryName);
7129
7597
  if (!regEntry) continue;
7130
- const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
7131
- if (!fs28.existsSync(dbPath)) continue;
7598
+ const dbPath = path33.join(regEntry.path, ".code-intel", "graph.db");
7599
+ if (!fs33.existsSync(dbPath)) continue;
7132
7600
  const graph = createKnowledgeGraph();
7133
7601
  const db = new DbManager(dbPath);
7134
7602
  try {
@@ -7157,6 +7625,8 @@ async function queryGroup(group, query, limit = 20) {
7157
7625
  }
7158
7626
 
7159
7627
  // src/http/app.ts
7628
+ init_knowledge_graph();
7629
+ init_graph_from_db();
7160
7630
  init_repo_registry();
7161
7631
  init_logger();
7162
7632
  init_codes();
@@ -7168,7 +7638,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
7168
7638
  var JobsDB = class {
7169
7639
  db;
7170
7640
  constructor(dbPath) {
7171
- fs28.mkdirSync(path30.dirname(dbPath), { recursive: true });
7641
+ fs33.mkdirSync(path33.dirname(dbPath), { recursive: true });
7172
7642
  this.db = new Database(dbPath);
7173
7643
  this.db.pragma("journal_mode = WAL");
7174
7644
  this.db.pragma("foreign_keys = ON");
@@ -7310,7 +7780,7 @@ var JobsDB = class {
7310
7780
  }
7311
7781
  };
7312
7782
  function getJobsDBPath() {
7313
- return path30.join(os12.homedir(), ".code-intel", "jobs.db");
7783
+ return path33.join(os12.homedir(), ".code-intel", "jobs.db");
7314
7784
  }
7315
7785
  var _jobsDB = null;
7316
7786
  function getOrCreateJobsDB() {
@@ -7402,7 +7872,7 @@ var BACKUP_VERSION = "1.0";
7402
7872
  var ALGORITHM = "aes-256-gcm";
7403
7873
  var IV_LENGTH = 16;
7404
7874
  function getBackupDir() {
7405
- return path30.join(os12.homedir(), ".code-intel", "backups");
7875
+ return path33.join(os12.homedir(), ".code-intel", "backups");
7406
7876
  }
7407
7877
  function getBackupKey() {
7408
7878
  const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
@@ -7433,30 +7903,30 @@ var BackupService = class {
7433
7903
  constructor(backupDir) {
7434
7904
  this.backupDir = backupDir ?? getBackupDir();
7435
7905
  this.key = getBackupKey();
7436
- fs28.mkdirSync(this.backupDir, { recursive: true });
7906
+ fs33.mkdirSync(this.backupDir, { recursive: true });
7437
7907
  }
7438
7908
  /**
7439
7909
  * Create a backup for a repository.
7440
7910
  * Returns the backup entry.
7441
7911
  */
7442
7912
  createBackup(repoPath) {
7443
- const codeIntelDir = path30.join(repoPath, ".code-intel");
7913
+ const codeIntelDir = path33.join(repoPath, ".code-intel");
7444
7914
  const id = v4();
7445
7915
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7446
7916
  const filesToBackup = [];
7447
7917
  const candidates = ["graph.db", "vector.db", "meta.json"];
7448
7918
  for (const f of candidates) {
7449
- const fp = path30.join(codeIntelDir, f);
7450
- if (fs28.existsSync(fp)) {
7919
+ const fp = path33.join(codeIntelDir, f);
7920
+ if (fs33.existsSync(fp)) {
7451
7921
  filesToBackup.push({ name: f, localPath: fp });
7452
7922
  }
7453
7923
  }
7454
- const registryPath = path30.join(os12.homedir(), ".code-intel", "registry.json");
7455
- if (fs28.existsSync(registryPath)) {
7924
+ const registryPath = path33.join(os12.homedir(), ".code-intel", "registry.json");
7925
+ if (fs33.existsSync(registryPath)) {
7456
7926
  filesToBackup.push({ name: "registry.json", localPath: registryPath });
7457
7927
  }
7458
- const usersDbPath = path30.join(os12.homedir(), ".code-intel", "users.db");
7459
- if (fs28.existsSync(usersDbPath)) {
7928
+ const usersDbPath = path33.join(os12.homedir(), ".code-intel", "users.db");
7929
+ if (fs33.existsSync(usersDbPath)) {
7460
7930
  filesToBackup.push({ name: "users.db", localPath: usersDbPath });
7461
7931
  }
7462
7932
  if (filesToBackup.length === 0) {
@@ -7467,7 +7937,7 @@ var BackupService = class {
7467
7937
  createdAt,
7468
7938
  version: BACKUP_VERSION,
7469
7939
  files: filesToBackup.map((f) => {
7470
- const data = fs28.readFileSync(f.localPath);
7940
+ const data = fs33.readFileSync(f.localPath);
7471
7941
  return {
7472
7942
  name: f.name,
7473
7943
  sha256: crypto5.createHash("sha256").update(data).digest("hex"),
@@ -7481,7 +7951,7 @@ var BackupService = class {
7481
7951
  manifestLenBuf.writeUInt32BE(manifestBuf.length, 0);
7482
7952
  parts.push(manifestLenBuf, manifestBuf);
7483
7953
  for (const f of filesToBackup) {
7484
- const data = fs28.readFileSync(f.localPath);
7954
+ const data = fs33.readFileSync(f.localPath);
7485
7955
  const nameBuf = Buffer.from(f.name, "utf-8");
7486
7956
  const nameLenBuf = Buffer.alloc(2);
7487
7957
  nameLenBuf.writeUInt16BE(nameBuf.length, 0);
@@ -7492,8 +7962,8 @@ var BackupService = class {
7492
7962
  const plaintext = Buffer.concat(parts);
7493
7963
  const encrypted = encryptBuffer(plaintext, this.key);
7494
7964
  const backupFileName = `backup-${id}.cib`;
7495
- const backupPath = path30.join(this.backupDir, backupFileName);
7496
- fs28.writeFileSync(backupPath, encrypted);
7965
+ const backupPath = path33.join(this.backupDir, backupFileName);
7966
+ fs33.writeFileSync(backupPath, encrypted);
7497
7967
  const entry = {
7498
7968
  id,
7499
7969
  createdAt,
@@ -7520,9 +7990,9 @@ var BackupService = class {
7520
7990
  async uploadToS3(entry) {
7521
7991
  const cfg = getS3Config();
7522
7992
  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.");
7523
- const fileName = path30.basename(entry.path);
7993
+ const fileName = path33.basename(entry.path);
7524
7994
  const s3Key = `${cfg.prefix}${fileName}`;
7525
- const body = fs28.readFileSync(entry.path);
7995
+ const body = fs33.readFileSync(entry.path);
7526
7996
  const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
7527
7997
  if (result.statusCode < 200 || result.statusCode >= 300) {
7528
7998
  throw new Error(`S3 upload failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
@@ -7539,8 +8009,8 @@ var BackupService = class {
7539
8009
  if (result.statusCode < 200 || result.statusCode >= 300) {
7540
8010
  throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
7541
8011
  }
7542
- fs28.mkdirSync(path30.dirname(destPath), { recursive: true });
7543
- fs28.writeFileSync(destPath, Buffer.from(result.body, "binary"));
8012
+ fs33.mkdirSync(path33.dirname(destPath), { recursive: true });
8013
+ fs33.writeFileSync(destPath, Buffer.from(result.body, "binary"));
7544
8014
  }
7545
8015
  /**
7546
8016
  * List backup objects in S3 with the configured prefix.
@@ -7586,10 +8056,10 @@ var BackupService = class {
7586
8056
  if (!entry) {
7587
8057
  throw new Error(`Backup "${backupId}" not found.`);
7588
8058
  }
7589
- if (!fs28.existsSync(entry.path)) {
8059
+ if (!fs33.existsSync(entry.path)) {
7590
8060
  throw new Error(`Backup file not found at: ${entry.path}`);
7591
8061
  }
7592
- const encrypted = fs28.readFileSync(entry.path);
8062
+ const encrypted = fs33.readFileSync(entry.path);
7593
8063
  let plaintext;
7594
8064
  try {
7595
8065
  plaintext = decryptBuffer(encrypted, this.key);
@@ -7603,8 +8073,8 @@ var BackupService = class {
7603
8073
  offset += manifestLen;
7604
8074
  const manifest = JSON.parse(manifestStr);
7605
8075
  const restoreBase = targetRepoPath ?? entry.repoPath;
7606
- const codeIntelDir = path30.join(restoreBase, ".code-intel");
7607
- fs28.mkdirSync(codeIntelDir, { recursive: true });
8076
+ const codeIntelDir = path33.join(restoreBase, ".code-intel");
8077
+ fs33.mkdirSync(codeIntelDir, { recursive: true });
7608
8078
  for (const fileEntry of manifest.files) {
7609
8079
  const nameLen = plaintext.readUInt16BE(offset);
7610
8080
  offset += 2;
@@ -7621,18 +8091,18 @@ var BackupService = class {
7621
8091
  }
7622
8092
  let destPath;
7623
8093
  if (name === "registry.json" || name === "users.db") {
7624
- destPath = path30.join(os12.homedir(), ".code-intel", name);
8094
+ destPath = path33.join(os12.homedir(), ".code-intel", name);
7625
8095
  } else {
7626
- destPath = path30.join(codeIntelDir, name);
8096
+ destPath = path33.join(codeIntelDir, name);
7627
8097
  }
7628
- fs28.writeFileSync(destPath, data);
8098
+ fs33.writeFileSync(destPath, data);
7629
8099
  }
7630
8100
  }
7631
8101
  /**
7632
8102
  * Apply retention policy: keep N daily, M weekly, L monthly backups.
7633
8103
  */
7634
8104
  applyRetention(options = { daily: 7, weekly: 4, monthly: 12 }) {
7635
- const entries = this._loadIndex().filter((e) => fs28.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
8105
+ const entries = this._loadIndex().filter((e) => fs33.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
7636
8106
  const keep = /* @__PURE__ */ new Set();
7637
8107
  const now = /* @__PURE__ */ new Date();
7638
8108
  const dailyCutoff = new Date(now);
@@ -7662,7 +8132,7 @@ var BackupService = class {
7662
8132
  for (const e of entries) {
7663
8133
  if (!keep.has(e.id)) {
7664
8134
  try {
7665
- fs28.unlinkSync(e.path);
8135
+ fs33.unlinkSync(e.path);
7666
8136
  deleted++;
7667
8137
  } catch {
7668
8138
  }
@@ -7674,17 +8144,17 @@ var BackupService = class {
7674
8144
  }
7675
8145
  // ── Index helpers ──────────────────────────────────────────────────────────
7676
8146
  _indexPath() {
7677
- return path30.join(this.backupDir, "index.json");
8147
+ return path33.join(this.backupDir, "index.json");
7678
8148
  }
7679
8149
  _loadIndex() {
7680
8150
  try {
7681
- return JSON.parse(fs28.readFileSync(this._indexPath(), "utf-8"));
8151
+ return JSON.parse(fs33.readFileSync(this._indexPath(), "utf-8"));
7682
8152
  } catch {
7683
8153
  return [];
7684
8154
  }
7685
8155
  }
7686
8156
  _saveIndex(entries) {
7687
- fs28.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
8157
+ fs33.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
7688
8158
  }
7689
8159
  _appendIndex(entry) {
7690
8160
  const entries = this._loadIndex();
@@ -8191,6 +8661,30 @@ var openApiSpec = {
8191
8661
  }
8192
8662
  }
8193
8663
  },
8664
+ "/groups/{name}/topology": {
8665
+ get: {
8666
+ tags: ["Groups"],
8667
+ summary: "Get the topology of repos and cross-repo contract edges for a group",
8668
+ parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
8669
+ responses: {
8670
+ "200": {
8671
+ description: "Repos and cross-repo edges",
8672
+ content: {
8673
+ "application/json": {
8674
+ schema: {
8675
+ type: "object",
8676
+ properties: {
8677
+ repos: { type: "array", items: { type: "object", properties: { name: { type: "string" }, groupPath: { type: "string" }, nodeCount: { type: "integer" }, edgeCount: { type: "integer" } } } },
8678
+ edges: { type: "array", items: { type: "object", properties: { source: { type: "string" }, target: { type: "string" }, contractName: { type: "string" }, confidence: { type: "number" }, kind: { type: "string" } } } }
8679
+ }
8680
+ }
8681
+ }
8682
+ }
8683
+ },
8684
+ "404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
8685
+ }
8686
+ }
8687
+ },
8194
8688
  "/query": {
8195
8689
  post: {
8196
8690
  tags: ["GQL"],
@@ -8301,11 +8795,11 @@ var openApiSpec = {
8301
8795
  };
8302
8796
 
8303
8797
  // src/http/app.ts
8304
- var __dirname$1 = path30.dirname(fileURLToPath(import.meta.url));
8798
+ var __dirname$1 = path33.dirname(fileURLToPath(import.meta.url));
8305
8799
  var WEB_DIST = (() => {
8306
- const bundled = path30.resolve(__dirname$1, "..", "web");
8307
- if (fs28.existsSync(bundled)) return bundled;
8308
- return path30.resolve(__dirname$1, "..", "..", "..", "web", "dist");
8800
+ const bundled = path33.resolve(__dirname$1, "..", "web");
8801
+ if (fs33.existsSync(bundled)) return bundled;
8802
+ return path33.resolve(__dirname$1, "..", "..", "..", "web", "dist");
8309
8803
  })();
8310
8804
  function getAllowedOrigins() {
8311
8805
  const env = process.env["CODE_INTEL_CORS_ORIGINS"];
@@ -8836,8 +9330,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8836
9330
  const registry = loadRegistry();
8837
9331
  const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
8838
9332
  if (!entry) return null;
8839
- const dbPath = path30.join(entry.path, ".code-intel", "graph.db");
8840
- if (!fs28.existsSync(dbPath)) return null;
9333
+ const dbPath = path33.join(entry.path, ".code-intel", "graph.db");
9334
+ if (!fs33.existsSync(dbPath)) return null;
8841
9335
  const repoGraph = createKnowledgeGraph();
8842
9336
  const db = new DbManager(dbPath);
8843
9337
  try {
@@ -8924,7 +9418,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8924
9418
  return;
8925
9419
  }
8926
9420
  try {
8927
- const content = fs28.readFileSync(file_path, "utf-8");
9421
+ const content = fs33.readFileSync(file_path, "utf-8");
8928
9422
  res.json({ content });
8929
9423
  } catch {
8930
9424
  res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "File not found" } });
@@ -9182,8 +9676,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9182
9676
  for (const member of group.members) {
9183
9677
  const regEntry = registry.find((r) => r.name === member.registryName);
9184
9678
  if (!regEntry) continue;
9185
- const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
9186
- if (!fs28.existsSync(dbPath)) continue;
9679
+ const dbPath = path33.join(regEntry.path, ".code-intel", "graph.db");
9680
+ if (!fs33.existsSync(dbPath)) continue;
9187
9681
  const db = new DbManager(dbPath);
9188
9682
  try {
9189
9683
  await db.init();
@@ -9195,6 +9689,45 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9195
9689
  }
9196
9690
  res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
9197
9691
  });
9692
+ app.get("/api/v1/groups/:name/topology", requireAuth, requireRole("viewer"), async (req, res) => {
9693
+ const groupName = req.params["name"];
9694
+ const group = loadGroup(groupName);
9695
+ if (!group) {
9696
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
9697
+ return;
9698
+ }
9699
+ const syncResult = loadSyncResult(groupName);
9700
+ const registry = loadRegistry();
9701
+ const repos = await Promise.all(group.members.map(async (member) => {
9702
+ const regEntry = registry.find((r) => r.name === member.registryName);
9703
+ let nodeCount = 0;
9704
+ let edgeCount = 0;
9705
+ if (regEntry) {
9706
+ const dbPath = path33.join(regEntry.path, ".code-intel", "graph.db");
9707
+ if (fs33.existsSync(dbPath)) {
9708
+ try {
9709
+ const db = new DbManager(dbPath);
9710
+ await db.init();
9711
+ const g = createKnowledgeGraph();
9712
+ await loadGraphFromDB(g, db);
9713
+ db.close();
9714
+ nodeCount = g.size.nodes;
9715
+ edgeCount = g.size.edges;
9716
+ } catch {
9717
+ }
9718
+ }
9719
+ }
9720
+ return { name: member.registryName, groupPath: member.groupPath, nodeCount, edgeCount };
9721
+ }));
9722
+ const edges = syncResult ? syncResult.links.map((link) => ({
9723
+ source: link.providerRepo,
9724
+ target: link.consumerRepo,
9725
+ contractName: link.providerContract,
9726
+ confidence: link.confidence,
9727
+ kind: "contract"
9728
+ })) : [];
9729
+ res.json({ repos, edges });
9730
+ });
9198
9731
  app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
9199
9732
  const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
9200
9733
  if (!file) {
@@ -9220,14 +9753,14 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9220
9753
  });
9221
9754
  return;
9222
9755
  }
9223
- let rawResolved = path30.normalize(file);
9224
- if (!path30.isAbsolute(rawResolved) && workspaceRoot) {
9225
- rawResolved = path30.join(workspaceRoot, rawResolved);
9756
+ let rawResolved = path33.normalize(file);
9757
+ if (!path33.isAbsolute(rawResolved) && workspaceRoot) {
9758
+ rawResolved = path33.join(workspaceRoot, rawResolved);
9226
9759
  }
9227
- const resolvedFile = path30.resolve(rawResolved);
9760
+ const resolvedFile = path33.resolve(rawResolved);
9228
9761
  function isInsideDir(fileAbs, dir) {
9229
- const rel = path30.relative(path30.resolve(dir), fileAbs);
9230
- return !rel.startsWith("..") && !path30.isAbsolute(rel);
9762
+ const rel = path33.relative(path33.resolve(dir), fileAbs);
9763
+ return !rel.startsWith("..") && !path33.isAbsolute(rel);
9231
9764
  }
9232
9765
  if (workspaceRoot) {
9233
9766
  if (!isInsideDir(resolvedFile, workspaceRoot)) {
@@ -9264,7 +9797,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9264
9797
  }
9265
9798
  let fileContent;
9266
9799
  try {
9267
- fileContent = fs28.readFileSync(resolvedFile, "utf-8");
9800
+ fileContent = fs33.readFileSync(resolvedFile, "utf-8");
9268
9801
  } catch {
9269
9802
  res.status(404).json({
9270
9803
  error: {
@@ -9295,7 +9828,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9295
9828
  const contextStart = Math.max(1, startLine - 20);
9296
9829
  const contextEnd = Math.min(lines.length, endLine + 20);
9297
9830
  const content = lines.slice(contextStart - 1, contextEnd).join("\n");
9298
- const ext = path30.extname(resolvedFile).toLowerCase();
9831
+ const ext = path33.extname(resolvedFile).toLowerCase();
9299
9832
  const languageMap = {
9300
9833
  ".ts": "typescript",
9301
9834
  ".tsx": "typescript",
@@ -9430,10 +9963,10 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
9430
9963
  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() } });
9431
9964
  }
9432
9965
  });
9433
- if (fs28.existsSync(WEB_DIST)) {
9966
+ if (fs33.existsSync(WEB_DIST)) {
9434
9967
  app.use(express.static(WEB_DIST));
9435
9968
  app.get("/{*path}", (_req, res) => {
9436
- res.sendFile(path30.join(WEB_DIST, "index.html"));
9969
+ res.sendFile(path33.join(WEB_DIST, "index.html"));
9437
9970
  });
9438
9971
  }
9439
9972
  app.use("/admin", requireRole("admin"));
@@ -9580,16 +10113,536 @@ async function startHttpServer(graph, repoName, port = 4747, workspaceRoot, watc
9580
10113
  wsServer = new WsServer2(httpServer);
9581
10114
  } catch {
9582
10115
  }
9583
- resolve({ wsServer });
9584
- });
9585
- });
10116
+ resolve({ wsServer });
10117
+ });
10118
+ });
10119
+ }
10120
+ init_storage();
10121
+ init_repo_registry();
10122
+ init_metadata();
10123
+ init_group_registry();
10124
+ init_group_sync();
10125
+ init_tracing();
10126
+ init_metrics();
10127
+
10128
+ // src/query/explain-relationship.ts
10129
+ function explainRelationship(graph, from, to) {
10130
+ const allNodes = [...graph.allNodes()];
10131
+ const fromNode = allNodes.find((n) => n.name === from);
10132
+ if (!fromNode) {
10133
+ const firstChar = from[0]?.toLowerCase() ?? "";
10134
+ const fromLower = from.toLowerCase();
10135
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
10136
+ return { error: `Symbol not found: ${from}`, suggestions };
10137
+ }
10138
+ const toNode = allNodes.find((n) => n.name === to);
10139
+ if (!toNode) {
10140
+ const firstChar = to[0]?.toLowerCase() ?? "";
10141
+ const toLower = to.toLowerCase();
10142
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
10143
+ return { error: `Symbol not found: ${to}`, suggestions };
10144
+ }
10145
+ const paths = [];
10146
+ const queue = [{
10147
+ id: fromNode.id,
10148
+ nodeNames: [fromNode.name],
10149
+ lastEdgeKind: "",
10150
+ visited: /* @__PURE__ */ new Set([fromNode.id])
10151
+ }];
10152
+ while (queue.length > 0 && paths.length < 10) {
10153
+ const entry = queue.shift();
10154
+ const { id, nodeNames, visited } = entry;
10155
+ if (nodeNames.length > 6) continue;
10156
+ for (const edge of graph.findEdgesFrom(id)) {
10157
+ const targetNode = graph.getNode(edge.target);
10158
+ if (!targetNode) continue;
10159
+ if (visited.has(edge.target)) continue;
10160
+ const newNames = [...nodeNames, targetNode.name];
10161
+ if (edge.target === toNode.id) {
10162
+ paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
10163
+ if (paths.length >= 10) break;
10164
+ continue;
10165
+ }
10166
+ if (newNames.length < 6) {
10167
+ const newVisited = new Set(visited);
10168
+ newVisited.add(edge.target);
10169
+ queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
10170
+ }
10171
+ }
10172
+ }
10173
+ const fromImports = /* @__PURE__ */ new Set();
10174
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
10175
+ if (edge.kind === "imports") fromImports.add(edge.target);
10176
+ }
10177
+ const sharedImportIds = [];
10178
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
10179
+ if (edge.kind === "imports" && fromImports.has(edge.target)) {
10180
+ sharedImportIds.push(edge.target);
10181
+ }
10182
+ }
10183
+ const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
10184
+ let heritage = null;
10185
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
10186
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
10187
+ heritage = `${from} ${edge.kind} ${to}`;
10188
+ break;
10189
+ }
10190
+ }
10191
+ if (!heritage) {
10192
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
10193
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
10194
+ heritage = `${to} ${edge.kind} ${from}`;
10195
+ break;
10196
+ }
10197
+ }
10198
+ }
10199
+ const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
10200
+ const heritageStr = heritage ?? "none";
10201
+ const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
10202
+ const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
10203
+ return { paths, sharedImports, heritage, summary };
10204
+ }
10205
+
10206
+ // src/mcp-server/server.ts
10207
+ init_pr_impact();
10208
+
10209
+ // src/query/similar-symbols.ts
10210
+ function levenshtein(a, b) {
10211
+ const m = a.length;
10212
+ const n = b.length;
10213
+ const dp = Array.from(
10214
+ { length: m + 1 },
10215
+ (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
10216
+ );
10217
+ for (let i = 1; i <= m; i++) {
10218
+ for (let j = 1; j <= n; j++) {
10219
+ if (a[i - 1] === b[j - 1]) {
10220
+ dp[i][j] = dp[i - 1][j - 1];
10221
+ } else {
10222
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
10223
+ }
10224
+ }
10225
+ }
10226
+ return dp[m][n];
10227
+ }
10228
+ function findSimilarSymbols(graph, symbolName, limit) {
10229
+ const clampedLimit = Math.min(Math.max(1, limit), 50);
10230
+ const allNodes = [...graph.allNodes()];
10231
+ const targetNode = allNodes.find((n) => n.name === symbolName);
10232
+ if (!targetNode) {
10233
+ return { similar: [] };
10234
+ }
10235
+ let targetCluster = null;
10236
+ for (const edge of graph.findEdgesFrom(targetNode.id)) {
10237
+ if (edge.kind === "belongs_to") {
10238
+ const clusterNode = graph.getNode(edge.target);
10239
+ if (clusterNode) {
10240
+ targetCluster = clusterNode.name;
10241
+ break;
10242
+ }
10243
+ }
10244
+ }
10245
+ if (!targetCluster) {
10246
+ for (const edge of graph.findEdgesTo(targetNode.id)) {
10247
+ if (edge.kind === "belongs_to") {
10248
+ const clusterNode = graph.getNode(edge.source);
10249
+ if (clusterNode) {
10250
+ targetCluster = clusterNode.name;
10251
+ break;
10252
+ }
10253
+ }
10254
+ }
10255
+ }
10256
+ const results = [];
10257
+ for (const node of allNodes) {
10258
+ if (node.id === targetNode.id) continue;
10259
+ const maxLen = Math.max(symbolName.length, node.name.length);
10260
+ const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
10261
+ const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
10262
+ const combined = 0.5 * nameSim + 0.5 * structuralSim;
10263
+ const reasons = [];
10264
+ if (nameSim >= 0.6) reasons.push("similar name");
10265
+ if (node.kind === targetNode.kind) reasons.push("same kind");
10266
+ if (targetCluster !== null) {
10267
+ let nodeCluster = null;
10268
+ for (const edge of graph.findEdgesFrom(node.id)) {
10269
+ if (edge.kind === "belongs_to") {
10270
+ const clusterNode = graph.getNode(edge.target);
10271
+ if (clusterNode) {
10272
+ nodeCluster = clusterNode.name;
10273
+ break;
10274
+ }
10275
+ }
10276
+ }
10277
+ if (!nodeCluster) {
10278
+ for (const edge of graph.findEdgesTo(node.id)) {
10279
+ if (edge.kind === "belongs_to") {
10280
+ const clusterNode = graph.getNode(edge.source);
10281
+ if (clusterNode) {
10282
+ nodeCluster = clusterNode.name;
10283
+ break;
10284
+ }
10285
+ }
10286
+ }
10287
+ }
10288
+ if (nodeCluster !== null && nodeCluster === targetCluster) {
10289
+ reasons.push("same module");
10290
+ }
10291
+ }
10292
+ if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
10293
+ if (!reasons.includes("same module")) reasons.push("same module");
10294
+ }
10295
+ results.push({ name: node.name, similarity: combined, reasons });
10296
+ }
10297
+ results.sort((a, b) => b.similarity - a.similarity);
10298
+ return { similar: results.slice(0, clampedLimit) };
10299
+ }
10300
+
10301
+ // src/query/health-report.ts
10302
+ function computeHealthReport(graph, scope) {
10303
+ const wholeRepo = scope === ".";
10304
+ function inScope(filePath) {
10305
+ if (wholeRepo) return true;
10306
+ return filePath.startsWith(scope) || filePath.includes(scope);
10307
+ }
10308
+ const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
10309
+ const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
10310
+ const deadCode = [];
10311
+ for (const node of scopedNodes) {
10312
+ if (!deadCodeKinds.has(node.kind)) continue;
10313
+ if (node.exported === true) continue;
10314
+ let hasIncoming = false;
10315
+ for (const _edge of graph.findEdgesTo(node.id)) {
10316
+ hasIncoming = true;
10317
+ break;
10318
+ }
10319
+ if (!hasIncoming) {
10320
+ deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
10321
+ if (deadCode.length >= 20) break;
10322
+ }
10323
+ }
10324
+ const cycles = [];
10325
+ const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
10326
+ const importAdj = /* @__PURE__ */ new Map();
10327
+ for (const node of scopedNodes) {
10328
+ importAdj.set(node.id, []);
10329
+ }
10330
+ for (const edge of graph.findEdgesByKind("imports")) {
10331
+ if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
10332
+ importAdj.get(edge.source).push(edge.target);
10333
+ }
10334
+ }
10335
+ const visited = /* @__PURE__ */ new Set();
10336
+ const inStack = /* @__PURE__ */ new Set();
10337
+ const stackPath = [];
10338
+ function dfs(nodeId) {
10339
+ if (cycles.length >= 5) return;
10340
+ visited.add(nodeId);
10341
+ inStack.add(nodeId);
10342
+ stackPath.push(nodeId);
10343
+ for (const neighborId of importAdj.get(nodeId) ?? []) {
10344
+ if (cycles.length >= 5) break;
10345
+ if (inStack.has(neighborId)) {
10346
+ const cycleStart = stackPath.indexOf(neighborId);
10347
+ const cyclePath = stackPath.slice(cycleStart).map((id) => {
10348
+ const node = graph.getNode(id);
10349
+ return node ? node.name : id;
10350
+ });
10351
+ cycles.push(cyclePath);
10352
+ } else if (!visited.has(neighborId)) {
10353
+ dfs(neighborId);
10354
+ }
10355
+ }
10356
+ stackPath.pop();
10357
+ inStack.delete(nodeId);
10358
+ }
10359
+ for (const node of scopedNodes) {
10360
+ if (cycles.length >= 5) break;
10361
+ if (!visited.has(node.id)) {
10362
+ dfs(node.id);
10363
+ }
10364
+ }
10365
+ const godNodes = [];
10366
+ for (const node of scopedNodes) {
10367
+ let edgeCount = 0;
10368
+ for (const _edge of graph.findEdgesFrom(node.id)) {
10369
+ edgeCount++;
10370
+ }
10371
+ if (edgeCount > 10) {
10372
+ godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
10373
+ }
10374
+ }
10375
+ godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
10376
+ godNodes.splice(10);
10377
+ const filePathToNodes = /* @__PURE__ */ new Map();
10378
+ for (const node of scopedNodes) {
10379
+ if (!node.filePath) continue;
10380
+ let arr = filePathToNodes.get(node.filePath);
10381
+ if (!arr) {
10382
+ arr = [];
10383
+ filePathToNodes.set(node.filePath, arr);
10384
+ }
10385
+ arr.push(node.id);
10386
+ }
10387
+ const orphanFiles = [];
10388
+ for (const [filePath, nodeIds] of filePathToNodes) {
10389
+ if (orphanFiles.length >= 10) break;
10390
+ let hasAnyEdge = false;
10391
+ for (const nodeId of nodeIds) {
10392
+ let hasOut = false;
10393
+ for (const _edge of graph.findEdgesFrom(nodeId)) {
10394
+ hasOut = true;
10395
+ break;
10396
+ }
10397
+ let hasIn = false;
10398
+ for (const _edge of graph.findEdgesTo(nodeId)) {
10399
+ hasIn = true;
10400
+ break;
10401
+ }
10402
+ if (hasOut || hasIn) {
10403
+ hasAnyEdge = true;
10404
+ break;
10405
+ }
10406
+ }
10407
+ if (!hasAnyEdge) {
10408
+ orphanFiles.push(filePath);
10409
+ }
10410
+ }
10411
+ const hotspotCandidates = [];
10412
+ for (const node of scopedNodes) {
10413
+ const visitedBfs = /* @__PURE__ */ new Set();
10414
+ const queue = [{ id: node.id, depth: 0 }];
10415
+ while (queue.length > 0) {
10416
+ const item = queue.shift();
10417
+ if (item.depth > 5 || visitedBfs.has(item.id)) continue;
10418
+ visitedBfs.add(item.id);
10419
+ for (const edge of graph.findEdgesTo(item.id)) {
10420
+ if (edge.kind === "calls" || edge.kind === "imports") {
10421
+ if (!visitedBfs.has(edge.source)) {
10422
+ queue.push({ id: edge.source, depth: item.depth + 1 });
10423
+ }
10424
+ }
10425
+ }
10426
+ }
10427
+ const blastRadius = visitedBfs.size - 1;
10428
+ hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
10429
+ }
10430
+ hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
10431
+ const complexityHotspots = hotspotCandidates.slice(0, 5);
10432
+ const healthScore = Math.max(
10433
+ 0,
10434
+ Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
10435
+ );
10436
+ return {
10437
+ healthScore,
10438
+ deadCode,
10439
+ cycles,
10440
+ godNodes,
10441
+ orphanFiles,
10442
+ complexityHotspots
10443
+ };
10444
+ }
10445
+
10446
+ // src/query/suggest-tests.ts
10447
+ function getSuggestedCases(symbolName) {
10448
+ const lower = symbolName.toLowerCase();
10449
+ if (/parse|validate|check|verify/.test(lower)) {
10450
+ return [
10451
+ "Valid input \u2192 success",
10452
+ "Invalid input \u2192 throws error",
10453
+ "Edge case: empty/null input \u2192 handled gracefully"
10454
+ ];
10455
+ }
10456
+ if (/create|add|insert|save/.test(lower)) {
10457
+ return [
10458
+ "Success: valid data \u2192 created",
10459
+ "Duplicate: existing item \u2192 error or no-op",
10460
+ "Missing required fields \u2192 validation error"
10461
+ ];
10462
+ }
10463
+ if (/delete|remove|destroy/.test(lower)) {
10464
+ return [
10465
+ "Existing item \u2192 deleted successfully",
10466
+ "Non-existent item \u2192 no error or 404",
10467
+ "Unauthorized access \u2192 rejected"
10468
+ ];
10469
+ }
10470
+ if (/get|find|fetch|load/.test(lower)) {
10471
+ return [
10472
+ "Found: returns correct data",
10473
+ "Not found: returns null or throws",
10474
+ "Empty collection: returns []"
10475
+ ];
10476
+ }
10477
+ return [
10478
+ "Happy path: valid input \u2192 expected output",
10479
+ "Error case: invalid input \u2192 error handled",
10480
+ "Edge case: boundary values \u2192 correct behavior"
10481
+ ];
10482
+ }
10483
+ function suggestTests(graph, symbolName) {
10484
+ let targetNode = void 0;
10485
+ for (const node of graph.allNodes()) {
10486
+ if (node.name === symbolName) {
10487
+ targetNode = node;
10488
+ break;
10489
+ }
10490
+ }
10491
+ if (!targetNode) {
10492
+ return { error: `Symbol not found: ${symbolName}` };
10493
+ }
10494
+ const targetId = targetNode.id;
10495
+ const callPaths = [];
10496
+ const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
10497
+ while (pathQueue.length > 0 && callPaths.length < 5) {
10498
+ const { id, path: path34, depth } = pathQueue.shift();
10499
+ let hasCallers = false;
10500
+ for (const edge of graph.findEdgesTo(id)) {
10501
+ if (edge.kind !== "calls") continue;
10502
+ const callerNode = graph.getNode(edge.source);
10503
+ if (!callerNode) continue;
10504
+ hasCallers = true;
10505
+ const newPath = [callerNode.name, ...path34];
10506
+ if (depth + 1 >= 3 || callPaths.length >= 5) {
10507
+ if (callPaths.length < 5) callPaths.push(newPath);
10508
+ continue;
10509
+ }
10510
+ pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
10511
+ }
10512
+ if (!hasCallers && path34.length > 1) {
10513
+ callPaths.push(path34);
10514
+ }
10515
+ }
10516
+ if (callPaths.length === 0) {
10517
+ for (const edge of graph.findEdgesTo(targetId)) {
10518
+ if (edge.kind !== "calls") continue;
10519
+ const callerNode = graph.getNode(edge.source);
10520
+ if (!callerNode) continue;
10521
+ callPaths.push([callerNode.name, symbolName]);
10522
+ if (callPaths.length >= 5) break;
10523
+ }
10524
+ }
10525
+ const existingTestFiles = /* @__PURE__ */ new Set();
10526
+ for (const edge of graph.findEdgesTo(targetId)) {
10527
+ if (edge.kind !== "imports") continue;
10528
+ const importerNode = graph.getNode(edge.source);
10529
+ if (!importerNode) continue;
10530
+ if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
10531
+ existingTestFiles.add(importerNode.filePath);
10532
+ }
10533
+ }
10534
+ const existingTests = [...existingTestFiles];
10535
+ const untestedCallers = [];
10536
+ for (const edge of graph.findEdgesTo(targetId)) {
10537
+ if (edge.kind !== "calls") continue;
10538
+ const callerNode = graph.getNode(edge.source);
10539
+ if (!callerNode) continue;
10540
+ if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
10541
+ continue;
10542
+ }
10543
+ let callerHasTest = false;
10544
+ for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
10545
+ if (callerImportEdge.kind !== "imports") continue;
10546
+ const importerOfCaller = graph.getNode(callerImportEdge.source);
10547
+ if (!importerOfCaller) continue;
10548
+ if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
10549
+ callerHasTest = true;
10550
+ break;
10551
+ }
10552
+ }
10553
+ if (!callerHasTest) {
10554
+ untestedCallers.push(callerNode.name);
10555
+ }
10556
+ }
10557
+ const suggestedCases = getSuggestedCases(symbolName);
10558
+ return {
10559
+ callPaths,
10560
+ suggestedCases,
10561
+ existingTests,
10562
+ untestedCallers
10563
+ };
10564
+ }
10565
+
10566
+ // src/query/cluster-summary.ts
10567
+ function getPathPrefix(filePath) {
10568
+ const parts = filePath.replace(/\\/g, "/").split("/");
10569
+ return parts.slice(0, 2).join("/");
10570
+ }
10571
+ function summarizeCluster(graph, cluster) {
10572
+ const clusterNodes = [...graph.allNodes()].filter(
10573
+ (n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
10574
+ );
10575
+ if (clusterNodes.length === 0) {
10576
+ return { error: `Cluster not found: ${cluster}` };
10577
+ }
10578
+ const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
10579
+ const callerCountMap = /* @__PURE__ */ new Map();
10580
+ for (const node of clusterNodes) {
10581
+ let count = 0;
10582
+ for (const _edge of graph.findEdgesTo(node.id)) {
10583
+ count++;
10584
+ }
10585
+ callerCountMap.set(node.id, count);
10586
+ }
10587
+ const sortedByCallers = [...clusterNodes].sort(
10588
+ (a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
10589
+ );
10590
+ const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
10591
+ name: n.name,
10592
+ callerCount: callerCountMap.get(n.id) ?? 0
10593
+ }));
10594
+ const depsSet = /* @__PURE__ */ new Set();
10595
+ for (const node of clusterNodes) {
10596
+ for (const edge of graph.findEdgesFrom(node.id)) {
10597
+ if (edge.kind !== "imports") continue;
10598
+ const targetNode = graph.getNode(edge.target);
10599
+ if (!targetNode) continue;
10600
+ if (!clusterNodeIds.has(targetNode.id)) {
10601
+ const prefix = getPathPrefix(targetNode.filePath);
10602
+ depsSet.add(prefix);
10603
+ }
10604
+ }
10605
+ }
10606
+ const dependencies = [...depsSet];
10607
+ const dependentsSet = /* @__PURE__ */ new Set();
10608
+ for (const node of clusterNodes) {
10609
+ for (const edge of graph.findEdgesTo(node.id)) {
10610
+ if (edge.kind !== "imports") continue;
10611
+ const sourceNode = graph.getNode(edge.source);
10612
+ if (!sourceNode) continue;
10613
+ if (!clusterNodeIds.has(sourceNode.id)) {
10614
+ const prefix = getPathPrefix(sourceNode.filePath);
10615
+ dependentsSet.add(prefix);
10616
+ }
10617
+ }
10618
+ }
10619
+ const dependents = [...dependentsSet];
10620
+ const healthResult = computeHealthReport(graph, cluster);
10621
+ const health = { score: healthResult.healthScore };
10622
+ const symbolCount = {};
10623
+ for (const node of clusterNodes) {
10624
+ symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
10625
+ }
10626
+ let purpose;
10627
+ const topNode = sortedByCallers[0];
10628
+ if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
10629
+ purpose = topNode.metadata["summary"];
10630
+ } else {
10631
+ const clusterName = cluster.split("/").pop() ?? cluster;
10632
+ purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
10633
+ }
10634
+ return {
10635
+ cluster,
10636
+ purpose,
10637
+ keySymbols,
10638
+ dependencies,
10639
+ dependents,
10640
+ health,
10641
+ symbolCount
10642
+ };
9586
10643
  }
9587
- init_storage();
9588
- init_repo_registry();
9589
- init_metadata();
9590
- init_group_registry();
9591
- init_tracing();
9592
- init_metrics();
10644
+
10645
+ // src/mcp-server/server.ts
9593
10646
  function createMcpServer(graph, repoName, workspaceRoot) {
9594
10647
  const server = new Server(
9595
10648
  { name: "code-intel", version: "0.1.0" },
@@ -9619,7 +10672,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9619
10672
  type: "object",
9620
10673
  properties: {
9621
10674
  query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
9622
- limit: { type: "number", description: "Max results to return (default: 20)" },
10675
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10676
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
9623
10677
  ..._tokenProp
9624
10678
  },
9625
10679
  required: ["query"]
@@ -9662,6 +10716,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9662
10716
  type: "object",
9663
10717
  properties: {
9664
10718
  file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
10719
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10720
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
9665
10721
  ..._tokenProp
9666
10722
  },
9667
10723
  required: ["file_path"]
@@ -9691,7 +10747,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9691
10747
  type: "string",
9692
10748
  description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
9693
10749
  },
9694
- limit: { type: "number", description: "Max results (default: 100)" },
10750
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10751
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
9695
10752
  ..._tokenProp
9696
10753
  }
9697
10754
  }
@@ -9708,7 +10765,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9708
10765
  inputSchema: {
9709
10766
  type: "object",
9710
10767
  properties: {
9711
- limit: { type: "number", description: "Max clusters to return (default: 50)" },
10768
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10769
+ limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
9712
10770
  ..._tokenProp
9713
10771
  }
9714
10772
  }
@@ -9719,7 +10777,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9719
10777
  inputSchema: {
9720
10778
  type: "object",
9721
10779
  properties: {
9722
- limit: { type: "number", description: "Max flows to return (default: 50)" },
10780
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10781
+ limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
9723
10782
  ..._tokenProp
9724
10783
  }
9725
10784
  }
@@ -9841,6 +10900,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
9841
10900
  },
9842
10901
  required: ["name"]
9843
10902
  }
10903
+ },
10904
+ // ── Reasoning / analysis tools ────────────────────────────────────────
10905
+ {
10906
+ name: "explain_relationship",
10907
+ 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.",
10908
+ inputSchema: {
10909
+ type: "object",
10910
+ properties: {
10911
+ from: { type: "string", description: "Source symbol name" },
10912
+ to: { type: "string", description: "Target symbol name" },
10913
+ ..._tokenProp
10914
+ },
10915
+ required: ["from", "to"]
10916
+ }
10917
+ },
10918
+ {
10919
+ name: "pr_impact",
10920
+ 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.",
10921
+ inputSchema: {
10922
+ type: "object",
10923
+ properties: {
10924
+ changedFiles: {
10925
+ type: "array",
10926
+ items: { type: "string" },
10927
+ description: "List of changed file paths (relative or absolute)"
10928
+ },
10929
+ diff: {
10930
+ type: "string",
10931
+ description: "Raw unified diff text. Changed files are extracted automatically."
10932
+ },
10933
+ maxHops: {
10934
+ type: "number",
10935
+ description: "Maximum BFS depth for blast radius (default: 5)"
10936
+ },
10937
+ ..._tokenProp
10938
+ }
10939
+ }
10940
+ },
10941
+ {
10942
+ name: "similar_symbols",
10943
+ description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
10944
+ inputSchema: {
10945
+ type: "object",
10946
+ properties: {
10947
+ symbol: { type: "string", description: "Symbol name to find similar symbols for" },
10948
+ limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
10949
+ ..._tokenProp
10950
+ },
10951
+ required: ["symbol"]
10952
+ }
10953
+ },
10954
+ {
10955
+ name: "health_report",
10956
+ description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
10957
+ inputSchema: {
10958
+ type: "object",
10959
+ properties: {
10960
+ scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
10961
+ ..._tokenProp
10962
+ }
10963
+ }
10964
+ },
10965
+ {
10966
+ name: "suggest_tests",
10967
+ description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
10968
+ inputSchema: {
10969
+ type: "object",
10970
+ properties: {
10971
+ symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
10972
+ ..._tokenProp
10973
+ },
10974
+ required: ["symbol"]
10975
+ }
10976
+ },
10977
+ {
10978
+ name: "cluster_summary",
10979
+ description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
10980
+ inputSchema: {
10981
+ type: "object",
10982
+ properties: {
10983
+ cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
10984
+ ..._tokenProp
10985
+ },
10986
+ required: ["cluster"]
10987
+ }
9844
10988
  }
9845
10989
  ]
9846
10990
  }));
@@ -9911,8 +11055,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9911
11055
  for (const edge of graph.allEdges()) {
9912
11056
  edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
9913
11057
  }
9914
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
9915
- const healthReport = computeHealthReport2(graph);
11058
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
11059
+ const healthReport = computeHealthReport3(graph);
9916
11060
  const health = {
9917
11061
  score: Math.round(healthReport.score),
9918
11062
  grade: healthReport.grade,
@@ -9937,10 +11081,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9937
11081
  // ── search ─────────────────────────────────────────────────────────────
9938
11082
  case "search": {
9939
11083
  const query = a.query;
9940
- const limit = a.limit ?? 20;
11084
+ const offset = a.offset ?? 0;
11085
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
9941
11086
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
9942
- const { results, searchMode } = await hybridSearch(graph, query, limit, { vectorDbPath: vdbPath });
9943
- return { content: [{ type: "text", text: JSON.stringify({ results, searchMode, suggested_next_tools: ["inspect", "query", "blast_radius"] }, null, 2) }] };
11087
+ const fetchLimit = Math.min(offset + effectiveLimit, 500);
11088
+ const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
11089
+ const total = allResults.length;
11090
+ const results = allResults.slice(offset, offset + effectiveLimit);
11091
+ const hasMore = offset + effectiveLimit < total;
11092
+ const suggestNextTools = [];
11093
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
11094
+ if (suggestEnabled && results.length > 0) {
11095
+ const topName = results[0].name;
11096
+ suggestNextTools.push(
11097
+ { tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
11098
+ { tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
11099
+ );
11100
+ }
11101
+ return {
11102
+ content: [{
11103
+ type: "text",
11104
+ text: JSON.stringify({
11105
+ results,
11106
+ searchMode,
11107
+ total,
11108
+ offset,
11109
+ limit: effectiveLimit,
11110
+ hasMore,
11111
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
11112
+ }, null, 2)
11113
+ }]
11114
+ };
9944
11115
  }
9945
11116
  // ── inspect ────────────────────────────────────────────────────────────
9946
11117
  case "inspect": {
@@ -9949,6 +11120,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9949
11120
  if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
9950
11121
  const incoming = [...graph.findEdgesTo(node.id)];
9951
11122
  const outgoing = [...graph.findEdgesFrom(node.id)];
11123
+ const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
11124
+ id: e.source,
11125
+ name: graph.getNode(e.source)?.name,
11126
+ file: graph.getNode(e.source)?.filePath
11127
+ }));
11128
+ const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
11129
+ id: e.target,
11130
+ name: graph.getNode(e.target)?.name,
11131
+ file: graph.getNode(e.target)?.filePath
11132
+ }));
11133
+ const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
11134
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
11135
+ const suggestNextTools = [];
11136
+ if (suggestEnabled) {
11137
+ const topCallerName = callers[0]?.name;
11138
+ suggestNextTools.push(
11139
+ ...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
11140
+ ...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 } }]
11141
+ );
11142
+ }
9952
11143
  return {
9953
11144
  content: [{
9954
11145
  type: "text",
@@ -9962,16 +11153,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9962
11153
  endLine: node.endLine,
9963
11154
  exported: node.exported
9964
11155
  },
9965
- callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
9966
- id: e.source,
9967
- name: graph.getNode(e.source)?.name,
9968
- file: graph.getNode(e.source)?.filePath
9969
- })),
9970
- callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
9971
- id: e.target,
9972
- name: graph.getNode(e.target)?.name,
9973
- file: graph.getNode(e.target)?.filePath
9974
- })),
11156
+ callers,
11157
+ callees,
9975
11158
  imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
9976
11159
  importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
9977
11160
  extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
@@ -9980,8 +11163,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9980
11163
  name: graph.getNode(e.target)?.name,
9981
11164
  kind: graph.getNode(e.target)?.kind
9982
11165
  })),
9983
- cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
9984
- content: node.content?.slice(0, 500)
11166
+ cluster,
11167
+ content: node.content?.slice(0, 500),
11168
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
9985
11169
  }, null, 2)
9986
11170
  }]
9987
11171
  };
@@ -10017,6 +11201,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10017
11201
  return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
10018
11202
  });
10019
11203
  const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
11204
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
11205
+ const suggestNextTools = [];
11206
+ if (suggestEnabled) {
11207
+ const highestRiskSymbol = node.name;
11208
+ const firstFilePath = affectedDetails[0]?.filePath ?? "";
11209
+ suggestNextTools.push(
11210
+ { tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
11211
+ { tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
11212
+ );
11213
+ }
10020
11214
  return {
10021
11215
  content: [{
10022
11216
  type: "text",
@@ -10024,7 +11218,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10024
11218
  target: node.name,
10025
11219
  affectedCount: affected.size,
10026
11220
  riskLevel: risk,
10027
- affected: affectedDetails
11221
+ affected: affectedDetails,
11222
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
10028
11223
  }, null, 2)
10029
11224
  }]
10030
11225
  };
@@ -10032,17 +11227,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10032
11227
  // ── file_symbols ───────────────────────────────────────────────────────
10033
11228
  case "file_symbols": {
10034
11229
  const filePath = a.file_path;
10035
- const matches = [];
11230
+ const offset = a.offset ?? 0;
11231
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
11232
+ const allMatches = [];
10036
11233
  for (const node of graph.allNodes()) {
10037
11234
  if (node.filePath && node.filePath.includes(filePath)) {
10038
- matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
11235
+ allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
10039
11236
  }
10040
11237
  }
10041
- if (matches.length === 0) {
11238
+ if (allMatches.length === 0) {
10042
11239
  return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
10043
11240
  }
10044
- matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
10045
- return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
11241
+ allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
11242
+ const total = allMatches.length;
11243
+ const matches = allMatches.slice(offset, offset + effectiveLimit);
11244
+ const hasMore = offset + effectiveLimit < total;
11245
+ return {
11246
+ content: [{
11247
+ type: "text",
11248
+ text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
11249
+ }]
11250
+ };
10046
11251
  }
10047
11252
  // ── find_path ──────────────────────────────────────────────────────────
10048
11253
  case "find_path": {
@@ -10088,15 +11293,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10088
11293
  // ── list_exports ───────────────────────────────────────────────────────
10089
11294
  case "list_exports": {
10090
11295
  const kindFilter = a.kind;
10091
- const limit = a.limit ?? 100;
10092
- const exports$1 = [];
11296
+ const offset = a.offset ?? 0;
11297
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
11298
+ const allExports = [];
10093
11299
  for (const node of graph.allNodes()) {
10094
11300
  if (!node.exported) continue;
10095
11301
  if (kindFilter && node.kind !== kindFilter) continue;
10096
- exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
10097
- if (exports$1.length >= limit) break;
11302
+ allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
10098
11303
  }
10099
- return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
11304
+ const total = allExports.length;
11305
+ const exports$1 = allExports.slice(offset, offset + effectiveLimit);
11306
+ const hasMore = offset + effectiveLimit < total;
11307
+ return {
11308
+ content: [{
11309
+ type: "text",
11310
+ text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
11311
+ }]
11312
+ };
10100
11313
  }
10101
11314
  // ── routes ─────────────────────────────────────────────────────────────
10102
11315
  case "routes": {
@@ -10110,8 +11323,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10110
11323
  }
10111
11324
  // ── clusters ───────────────────────────────────────────────────────────
10112
11325
  case "clusters": {
10113
- const limit = a.limit ?? 50;
10114
- const clusters = [];
11326
+ const offset = a.offset ?? 0;
11327
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
11328
+ const allClusters = [];
10115
11329
  for (const node of graph.allNodes()) {
10116
11330
  if (node.kind === "cluster") {
10117
11331
  const members = [];
@@ -10123,35 +11337,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10123
11337
  }
10124
11338
  }
10125
11339
  }
10126
- clusters.push({
11340
+ allClusters.push({
10127
11341
  id: node.id,
10128
11342
  name: node.name,
10129
11343
  memberCount: node.metadata?.memberCount ?? members.length,
10130
11344
  topSymbols: members.slice(0, 10)
10131
11345
  });
10132
- if (clusters.length >= limit) break;
10133
11346
  }
10134
11347
  }
10135
- return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
11348
+ const total = allClusters.length;
11349
+ const clusters = allClusters.slice(offset, offset + effectiveLimit);
11350
+ const hasMore = offset + effectiveLimit < total;
11351
+ return {
11352
+ content: [{
11353
+ type: "text",
11354
+ text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
11355
+ }]
11356
+ };
10136
11357
  }
10137
11358
  // ── flows ──────────────────────────────────────────────────────────────
10138
11359
  case "flows": {
10139
- const limit = a.limit ?? 50;
10140
- const flows = [];
11360
+ const offset = a.offset ?? 0;
11361
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
11362
+ const allFlows = [];
10141
11363
  for (const node of graph.allNodes()) {
10142
11364
  if (node.kind === "flow") {
10143
11365
  const steps = node.metadata?.steps;
10144
- flows.push({
11366
+ allFlows.push({
10145
11367
  id: node.id,
10146
11368
  name: node.name,
10147
11369
  entryPoint: node.metadata?.entryPoint,
10148
11370
  steps: steps ?? [],
10149
11371
  stepCount: Array.isArray(steps) ? steps.length : 0
10150
11372
  });
10151
- if (flows.length >= limit) break;
10152
11373
  }
10153
11374
  }
10154
- return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
11375
+ const total = allFlows.length;
11376
+ const flows = allFlows.slice(offset, offset + effectiveLimit);
11377
+ const hasMore = offset + effectiveLimit < total;
11378
+ return {
11379
+ content: [{
11380
+ type: "text",
11381
+ text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
11382
+ }]
11383
+ };
10155
11384
  }
10156
11385
  // ── detect_changes ─────────────────────────────────────────────────────
10157
11386
  case "detect_changes": {
@@ -10179,7 +11408,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10179
11408
  for (const { filePath: changedFile, changedLines } of changedFiles) {
10180
11409
  for (const node of graph.allNodes()) {
10181
11410
  if (!node.filePath) continue;
10182
- const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path30.sep, "");
11411
+ const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path33.sep, "");
10183
11412
  const normChanged = changedFile.replace(/^a\/|^b\//, "");
10184
11413
  if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
10185
11414
  if (node.startLine !== void 0 && node.endLine !== void 0) {
@@ -10401,6 +11630,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
10401
11630
  }]
10402
11631
  };
10403
11632
  }
11633
+ // ── explain_relationship ───────────────────────────────────────────────
11634
+ case "explain_relationship": {
11635
+ const fromName = a.from;
11636
+ const toName = a.to;
11637
+ const result = explainRelationship(graph, fromName, toName);
11638
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11639
+ }
11640
+ // ── pr_impact ──────────────────────────────────────────────────────────
11641
+ case "pr_impact": {
11642
+ const maxHops = a.maxHops ?? 5;
11643
+ let changedFiles = a.changedFiles ?? [];
11644
+ if (a.diff && typeof a.diff === "string") {
11645
+ const diffFiles = parseDiffFiles(a.diff);
11646
+ changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
11647
+ }
11648
+ if (changedFiles.length === 0) {
11649
+ return {
11650
+ content: [{
11651
+ type: "text",
11652
+ text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
11653
+ }]
11654
+ };
11655
+ }
11656
+ const result = computePRImpact(graph, changedFiles, maxHops);
11657
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11658
+ }
11659
+ // ── similar_symbols ────────────────────────────────────────────────────
11660
+ case "similar_symbols": {
11661
+ const symbolName = a.symbol;
11662
+ const limit = a.limit ?? 10;
11663
+ const result = findSimilarSymbols(graph, symbolName, limit);
11664
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11665
+ }
11666
+ // ── health_report ──────────────────────────────────────────────────────
11667
+ case "health_report": {
11668
+ const scope = a.scope ?? ".";
11669
+ const result = computeHealthReport(graph, scope);
11670
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11671
+ }
11672
+ // ── suggest_tests ──────────────────────────────────────────────────────
11673
+ case "suggest_tests": {
11674
+ const sym = a.symbol;
11675
+ const result = suggestTests(graph, sym);
11676
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11677
+ }
11678
+ // ── cluster_summary ────────────────────────────────────────────────────
11679
+ case "cluster_summary": {
11680
+ const cluster = a.cluster;
11681
+ const result = summarizeCluster(graph, cluster);
11682
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11683
+ }
10404
11684
  default:
10405
11685
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
10406
11686
  }
@@ -10487,20 +11767,20 @@ function parseDiff(diffText) {
10487
11767
  // src/cli/main.ts
10488
11768
  init_metadata();
10489
11769
  async function writeSkillFiles(graph, workspaceRoot, projectName) {
10490
- const outputDir = path30.join(workspaceRoot, ".claude", "skills", "code-intel");
11770
+ const outputDir = path33.join(workspaceRoot, ".claude", "skills", "code-intel");
10491
11771
  const areas = buildAreaMap(graph, workspaceRoot);
10492
11772
  if (areas.length === 0) return { skills: [], outputDir };
10493
- fs28.rmSync(outputDir, { recursive: true, force: true });
10494
- fs28.mkdirSync(outputDir, { recursive: true });
11773
+ fs33.rmSync(outputDir, { recursive: true, force: true });
11774
+ fs33.mkdirSync(outputDir, { recursive: true });
10495
11775
  const skills = [];
10496
11776
  const usedNames = /* @__PURE__ */ new Set();
10497
11777
  for (const area of areas) {
10498
11778
  const kebab = uniqueKebab(area.label, usedNames);
10499
11779
  usedNames.add(kebab);
10500
11780
  const content = renderSkill(area, projectName, kebab);
10501
- const dir = path30.join(outputDir, kebab);
10502
- fs28.mkdirSync(dir, { recursive: true });
10503
- fs28.writeFileSync(path30.join(dir, "SKILL.md"), content, "utf-8");
11781
+ const dir = path33.join(outputDir, kebab);
11782
+ fs33.mkdirSync(dir, { recursive: true });
11783
+ fs33.writeFileSync(path33.join(dir, "SKILL.md"), content, "utf-8");
10504
11784
  skills.push({ name: kebab, label: area.label, symbolCount: area.nodes.length, fileCount: area.files.size });
10505
11785
  }
10506
11786
  return { skills, outputDir };
@@ -10680,8 +11960,8 @@ var BLOCK_START = "<!-- code-intel:start -->";
10680
11960
  var BLOCK_END = "<!-- code-intel:end -->";
10681
11961
  function writeContextFiles(workspaceRoot, projectName, stats, skills) {
10682
11962
  const block = buildBlock(projectName, stats, skills);
10683
- upsertFile(path30.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
10684
- upsertFile(path30.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
11963
+ upsertFile(path33.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
11964
+ upsertFile(path33.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
10685
11965
  }
10686
11966
  function buildBlock(projectName, stats, skills) {
10687
11967
  const skillRows = skills.map(
@@ -10735,7 +12015,7 @@ ${skillTable}
10735
12015
  ${BLOCK_END}`;
10736
12016
  }
10737
12017
  function upsertFile(filePath, block, fileName) {
10738
- if (!fs28.existsSync(filePath)) {
12018
+ if (!fs33.existsSync(filePath)) {
10739
12019
  const newContent = [
10740
12020
  `# ${fileName}`,
10741
12021
  "",
@@ -10746,17 +12026,17 @@ function upsertFile(filePath, block, fileName) {
10746
12026
  "<!-- Add your own custom notes below this line. They will never be overwritten by code-intel. -->",
10747
12027
  ""
10748
12028
  ].join("\n");
10749
- fs28.writeFileSync(filePath, newContent, "utf-8");
12029
+ fs33.writeFileSync(filePath, newContent, "utf-8");
10750
12030
  return;
10751
12031
  }
10752
- const existing = fs28.readFileSync(filePath, "utf-8");
12032
+ const existing = fs33.readFileSync(filePath, "utf-8");
10753
12033
  const startIdx = findLineMarker(existing, BLOCK_START);
10754
12034
  const endIdx = findLineMarker(existing, BLOCK_END, startIdx === -1 ? 0 : startIdx);
10755
12035
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
10756
12036
  const before = existing.slice(0, startIdx);
10757
12037
  const after = existing.slice(endIdx + BLOCK_END.length);
10758
12038
  const updated = (before + block + after).trimEnd() + "\n";
10759
- fs28.writeFileSync(filePath, updated, "utf-8");
12039
+ fs33.writeFileSync(filePath, updated, "utf-8");
10760
12040
  return;
10761
12041
  }
10762
12042
  const appended = [
@@ -10769,7 +12049,7 @@ function upsertFile(filePath, block, fileName) {
10769
12049
  block,
10770
12050
  ""
10771
12051
  ].join("\n");
10772
- fs28.writeFileSync(filePath, appended, "utf-8");
12052
+ fs33.writeFileSync(filePath, appended, "utf-8");
10773
12053
  }
10774
12054
  function findLineMarker(content, marker, startFrom = 0) {
10775
12055
  let idx = content.indexOf(marker, startFrom);
@@ -10811,14 +12091,14 @@ function getChangedFilesSince(workspaceRoot, baseHash) {
10811
12091
  function filterChangedByMtime(allFilePaths, workspaceRoot, storedMtimes) {
10812
12092
  const changed = [];
10813
12093
  for (const absPath of allFilePaths) {
10814
- const rel = path30.relative(workspaceRoot, absPath);
12094
+ const rel = path33.relative(workspaceRoot, absPath);
10815
12095
  const stored = storedMtimes[rel];
10816
12096
  if (stored === void 0) {
10817
12097
  changed.push(absPath);
10818
12098
  continue;
10819
12099
  }
10820
12100
  try {
10821
- const { mtimeMs } = fs28.statSync(absPath);
12101
+ const { mtimeMs } = fs33.statSync(absPath);
10822
12102
  if (mtimeMs > stored) changed.push(absPath);
10823
12103
  } catch {
10824
12104
  changed.push(absPath);
@@ -10830,8 +12110,8 @@ function buildMtimeSnapshot(filePaths, workspaceRoot) {
10830
12110
  const snap = {};
10831
12111
  for (const absPath of filePaths) {
10832
12112
  try {
10833
- const { mtimeMs } = fs28.statSync(absPath);
10834
- snap[path30.relative(workspaceRoot, absPath)] = mtimeMs;
12113
+ const { mtimeMs } = fs33.statSync(absPath);
12114
+ snap[path33.relative(workspaceRoot, absPath)] = mtimeMs;
10835
12115
  } catch {
10836
12116
  }
10837
12117
  }
@@ -10842,8 +12122,8 @@ function decideIncremental(workspaceRoot, allFilePaths, prevCommitHash, storedMt
10842
12122
  if (prevCommitHash) {
10843
12123
  const changed = getChangedFilesSince(workspaceRoot, prevCommitHash);
10844
12124
  if (changed !== null) {
10845
- const scanSet = new Set(allFilePaths.map((p) => path30.relative(workspaceRoot, p)));
10846
- const changedInScan = changed.filter((rel) => scanSet.has(rel)).map((rel) => path30.join(workspaceRoot, rel));
12125
+ const scanSet = new Set(allFilePaths.map((p) => path33.relative(workspaceRoot, p)));
12126
+ const changedInScan = changed.filter((rel) => scanSet.has(rel)).map((rel) => path33.join(workspaceRoot, rel));
10847
12127
  if (total > 0 && changedInScan.length / total > 0.2) {
10848
12128
  return { incremental: false, fallbackReason: `changed files (${changedInScan.length}) > 20% of total (${total})` };
10849
12129
  }
@@ -10864,7 +12144,135 @@ function decideIncremental(workspaceRoot, allFilePaths, prevCommitHash, storedMt
10864
12144
  }
10865
12145
 
10866
12146
  // src/cli/main.ts
12147
+ init_graph_from_db();
10867
12148
  init_group_registry();
12149
+ init_group_sync();
12150
+ function expandGlob(root, pattern) {
12151
+ const parts = pattern.replace(/\/\*\*?$/, "").split("/").filter(Boolean);
12152
+ if (parts.length === 0) return [];
12153
+ const dir = path33.join(root, ...parts);
12154
+ if (!fs33.existsSync(dir)) return [];
12155
+ return fs33.readdirSync(dir).map((entry) => path33.join(dir, entry)).filter((p) => {
12156
+ try {
12157
+ return fs33.statSync(p).isDirectory();
12158
+ } catch {
12159
+ return false;
12160
+ }
12161
+ });
12162
+ }
12163
+ function resolvePackages(root, patterns) {
12164
+ const packages = [];
12165
+ for (const pattern of patterns) {
12166
+ const dirs = expandGlob(root, pattern);
12167
+ for (const dir of dirs) {
12168
+ const pkgJsonPath = path33.join(dir, "package.json");
12169
+ if (!fs33.existsSync(pkgJsonPath)) continue;
12170
+ try {
12171
+ const pkgJson = JSON.parse(fs33.readFileSync(pkgJsonPath, "utf-8"));
12172
+ const name = pkgJson.name ?? path33.basename(dir);
12173
+ packages.push({ name, path: dir });
12174
+ } catch {
12175
+ }
12176
+ }
12177
+ }
12178
+ return packages;
12179
+ }
12180
+ async function detectWorkspace(root) {
12181
+ const turboJsonPath = path33.join(root, "turbo.json");
12182
+ if (fs33.existsSync(turboJsonPath)) {
12183
+ let patterns = [];
12184
+ const pkgJsonPath = path33.join(root, "package.json");
12185
+ if (fs33.existsSync(pkgJsonPath)) {
12186
+ try {
12187
+ const pkgJson = JSON.parse(fs33.readFileSync(pkgJsonPath, "utf-8"));
12188
+ if (pkgJson.workspaces) {
12189
+ patterns = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces.packages;
12190
+ }
12191
+ } catch {
12192
+ }
12193
+ }
12194
+ if (patterns.length === 0) {
12195
+ const fallbackDir = path33.join(root, "packages");
12196
+ if (fs33.existsSync(fallbackDir)) patterns = ["packages/*"];
12197
+ }
12198
+ const packages = resolvePackages(root, patterns);
12199
+ return { type: "turborepo", root, packages };
12200
+ }
12201
+ const npmPkgJsonPath = path33.join(root, "package.json");
12202
+ if (fs33.existsSync(npmPkgJsonPath)) {
12203
+ try {
12204
+ const pkgJson = JSON.parse(fs33.readFileSync(npmPkgJsonPath, "utf-8"));
12205
+ if (pkgJson.workspaces) {
12206
+ const patterns = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces.packages;
12207
+ const packages = resolvePackages(root, patterns);
12208
+ return { type: "npm", root, packages };
12209
+ }
12210
+ } catch {
12211
+ }
12212
+ }
12213
+ const pnpmYamlPath = path33.join(root, "pnpm-workspace.yaml");
12214
+ if (fs33.existsSync(pnpmYamlPath)) {
12215
+ const patterns = [];
12216
+ try {
12217
+ const content = fs33.readFileSync(pnpmYamlPath, "utf-8");
12218
+ let inPackages = false;
12219
+ for (const line of content.split("\n")) {
12220
+ if (/^packages\s*:/.test(line)) {
12221
+ inPackages = true;
12222
+ continue;
12223
+ }
12224
+ if (inPackages) {
12225
+ if (/^\s*-\s+/.test(line)) {
12226
+ patterns.push(line.replace(/^\s*-\s+/, "").replace(/['"]/g, "").trim());
12227
+ } else if (line.trim() && !/^\s/.test(line)) {
12228
+ inPackages = false;
12229
+ }
12230
+ }
12231
+ }
12232
+ } catch {
12233
+ }
12234
+ const packages = resolvePackages(root, patterns);
12235
+ return { type: "pnpm", root, packages };
12236
+ }
12237
+ const nxJsonPath = path33.join(root, "nx.json");
12238
+ if (fs33.existsSync(nxJsonPath)) {
12239
+ const packages = [];
12240
+ const scanForProjects = (dir, depth) => {
12241
+ if (depth > 2) return;
12242
+ let entries;
12243
+ try {
12244
+ entries = fs33.readdirSync(dir);
12245
+ } catch {
12246
+ return;
12247
+ }
12248
+ for (const entry of entries) {
12249
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
12250
+ const fullPath = path33.join(dir, entry);
12251
+ try {
12252
+ if (!fs33.statSync(fullPath).isDirectory()) continue;
12253
+ } catch {
12254
+ continue;
12255
+ }
12256
+ const projectJsonPath = path33.join(fullPath, "project.json");
12257
+ if (fs33.existsSync(projectJsonPath)) {
12258
+ try {
12259
+ const proj = JSON.parse(fs33.readFileSync(projectJsonPath, "utf-8"));
12260
+ const name = proj.name ?? path33.basename(fullPath);
12261
+ packages.push({ name, path: fullPath });
12262
+ } catch {
12263
+ }
12264
+ } else {
12265
+ scanForProjects(fullPath, depth + 1);
12266
+ }
12267
+ }
12268
+ };
12269
+ scanForProjects(root, 1);
12270
+ return { type: "nx", root, packages };
12271
+ }
12272
+ return null;
12273
+ }
12274
+
12275
+ // src/cli/main.ts
10868
12276
  init_users_db();
10869
12277
  var migrations = [
10870
12278
  {
@@ -10957,17 +12365,17 @@ var MigrationRunner = class {
10957
12365
  autoBackupBeforeMigration() {
10958
12366
  try {
10959
12367
  const dbFile = this.db.name;
10960
- if (!dbFile || !fs28.existsSync(dbFile)) return;
10961
- const backupDir = path30.join(os12.homedir(), ".code-intel", "backups", "pre-migration");
10962
- fs28.mkdirSync(backupDir, { recursive: true });
12368
+ if (!dbFile || !fs33.existsSync(dbFile)) return;
12369
+ const backupDir = path33.join(os12.homedir(), ".code-intel", "backups", "pre-migration");
12370
+ fs33.mkdirSync(backupDir, { recursive: true });
10963
12371
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
10964
- const baseName = path30.basename(dbFile, ".db");
10965
- const backupPath = path30.join(backupDir, `${baseName}-pre-migration-${ts}.db`);
12372
+ const baseName = path33.basename(dbFile, ".db");
12373
+ const backupPath = path33.join(backupDir, `${baseName}-pre-migration-${ts}.db`);
10966
12374
  try {
10967
12375
  this.db.pragma("wal_checkpoint(TRUNCATE)");
10968
12376
  } catch {
10969
12377
  }
10970
- fs28.copyFileSync(dbFile, backupPath);
12378
+ fs33.copyFileSync(dbFile, backupPath);
10971
12379
  } catch {
10972
12380
  }
10973
12381
  }
@@ -11183,7 +12591,7 @@ program.name("code-intel").description("Code Intelligence Platform \u2014 Static
11183
12591
  Docs: https://github.com/vohongtho/code-intel-platform
11184
12592
  `);
11185
12593
  async function analyzeWorkspace(targetPath, options) {
11186
- const workspaceRoot = path30.resolve(targetPath);
12594
+ const workspaceRoot = path33.resolve(targetPath);
11187
12595
  if (!options?.silent) console.log(`Analyzing: ${workspaceRoot}`);
11188
12596
  logger_default.info(`analyze started: ${workspaceRoot}`);
11189
12597
  if (options?.force) {
@@ -11204,14 +12612,14 @@ async function analyzeWorkspace(targetPath, options) {
11204
12612
  ];
11205
12613
  for (const f of wipeFiles) {
11206
12614
  try {
11207
- if (fs28.existsSync(f)) fs28.unlinkSync(f);
12615
+ if (fs33.existsSync(f)) fs33.unlinkSync(f);
11208
12616
  } catch {
11209
12617
  }
11210
12618
  }
11211
12619
  }
11212
12620
  if (!options?.skipGit) {
11213
- const gitDir = path30.join(workspaceRoot, ".git");
11214
- if (!fs28.existsSync(gitDir)) {
12621
+ const gitDir = path33.join(workspaceRoot, ".git");
12622
+ if (!fs33.existsSync(gitDir)) {
11215
12623
  logger_default.warn(`${workspaceRoot} is not a Git repository`);
11216
12624
  }
11217
12625
  }
@@ -11322,17 +12730,17 @@ async function analyzeWorkspace(targetPath, options) {
11322
12730
  const result = await runPipeline(phases, context2);
11323
12731
  if (isIncremental && incrementalChangedFiles && incrementalChangedFiles.length > 0) {
11324
12732
  const dbPath = getDbPath(workspaceRoot);
11325
- if (fs28.existsSync(dbPath)) {
12733
+ if (fs33.existsSync(dbPath)) {
11326
12734
  try {
11327
12735
  const db = new DbManager(dbPath);
11328
12736
  await db.init();
11329
12737
  for (const absPath of incrementalChangedFiles) {
11330
- const rel = path30.relative(workspaceRoot, absPath);
12738
+ const rel = path33.relative(workspaceRoot, absPath);
11331
12739
  await removeNodesForFile(rel, db);
11332
12740
  }
11333
12741
  const { upsertNodes: upsertNodesBatch } = await Promise.resolve().then(() => (init_graph_loader(), graph_loader_exports));
11334
12742
  const changedRelPaths = new Set(
11335
- incrementalChangedFiles.map((f) => path30.relative(workspaceRoot, f))
12743
+ incrementalChangedFiles.map((f) => path33.relative(workspaceRoot, f))
11336
12744
  );
11337
12745
  const nodesToUpsert = [...graph.allNodes()].filter(
11338
12746
  (n) => changedRelPaths.has(n.filePath)
@@ -11357,7 +12765,7 @@ async function analyzeWorkspace(targetPath, options) {
11357
12765
  mergedMtimes = newMtimes;
11358
12766
  }
11359
12767
  const currentCommitHash = getCurrentCommitHash(workspaceRoot) ?? void 0;
11360
- const repoName = path30.basename(workspaceRoot);
12768
+ const repoName = path33.basename(workspaceRoot);
11361
12769
  const indexVersion = v4();
11362
12770
  saveMetadata(workspaceRoot, {
11363
12771
  indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11395,7 +12803,7 @@ async function analyzeWorkspace(targetPath, options) {
11395
12803
  ];
11396
12804
  for (const f of newStaleFiles) {
11397
12805
  try {
11398
- if (fs28.existsSync(f)) fs28.unlinkSync(f);
12806
+ if (fs33.existsSync(f)) fs33.unlinkSync(f);
11399
12807
  } catch {
11400
12808
  }
11401
12809
  }
@@ -11412,21 +12820,21 @@ async function analyzeWorkspace(targetPath, options) {
11412
12820
  ];
11413
12821
  for (const f of staleFiles) {
11414
12822
  try {
11415
- if (fs28.existsSync(f)) fs28.unlinkSync(f);
12823
+ if (fs33.existsSync(f)) fs33.unlinkSync(f);
11416
12824
  } catch {
11417
12825
  }
11418
12826
  }
11419
12827
  for (const f of newStaleFiles) {
11420
12828
  if (f === dbPathNew) continue;
11421
- if (fs28.existsSync(f)) {
12829
+ if (fs33.existsSync(f)) {
11422
12830
  const dest = f.replace(dbPathNew, dbPath);
11423
12831
  try {
11424
- fs28.renameSync(f, dest);
12832
+ fs33.renameSync(f, dest);
11425
12833
  } catch {
11426
12834
  }
11427
12835
  }
11428
12836
  }
11429
- fs28.renameSync(dbPathNew, dbPath);
12837
+ fs33.renameSync(dbPathNew, dbPath);
11430
12838
  stopSpinner();
11431
12839
  logger_default.info(`DB persisted: ${nodeCount} nodes, ${edgeCount} edges`);
11432
12840
  if (!options?.silent) {
@@ -11447,7 +12855,7 @@ async function analyzeWorkspace(targetPath, options) {
11447
12855
  const staleVdb = [vdbPath, `${vdbPath}-shm`, `${vdbPath}-wal`];
11448
12856
  for (const f of staleVdb) {
11449
12857
  try {
11450
- if (fs28.existsSync(f)) fs28.unlinkSync(f);
12858
+ if (fs33.existsSync(f)) fs33.unlinkSync(f);
11451
12859
  } catch {
11452
12860
  }
11453
12861
  }
@@ -11517,6 +12925,32 @@ async function analyzeWorkspace(targetPath, options) {
11517
12925
  \u2705 Done in ${durStr} \u2014 ${graph.size.nodes} nodes \xB7 ${graph.size.edges} edges \xB7 ${context2.filePaths.length} files`);
11518
12926
  }
11519
12927
  logger_default.info(`analyze complete: ${graph.size.nodes} nodes, ${graph.size.edges} edges, ${context2.filePaths.length} files, ${result.totalDuration}ms`);
12928
+ if (!options?.noGroupSync) {
12929
+ const registry = loadRegistry();
12930
+ const repoEntry = registry.find((r) => r.path === workspaceRoot);
12931
+ if (repoEntry) {
12932
+ const allGroups = listGroups();
12933
+ for (const g of allGroups) {
12934
+ const group = loadGroup(g.name);
12935
+ if (!group) continue;
12936
+ const isMember = group.members.some((m) => m.registryName === repoEntry.name);
12937
+ if (!isMember) continue;
12938
+ if (!options?.silent) console.log(` \u2839 Syncing group '${g.name}'\u2026`);
12939
+ try {
12940
+ const { syncGroup: doSyncGroup } = await Promise.resolve().then(() => (init_group_sync(), group_sync_exports));
12941
+ const { saveSyncResult: doSaveSyncResult } = await Promise.resolve().then(() => (init_group_registry(), group_registry_exports));
12942
+ const syncResult = await doSyncGroup(group);
12943
+ doSaveSyncResult(syncResult);
12944
+ group.lastSync = syncResult.syncedAt;
12945
+ const { saveGroup: doSaveGroup } = await Promise.resolve().then(() => (init_group_registry(), group_registry_exports));
12946
+ doSaveGroup(group);
12947
+ if (!options?.silent) console.log(` \u2705 Group '${g.name}' synced`);
12948
+ } catch (err) {
12949
+ if (!options?.silent) console.warn(` \u26A0 Group sync failed: ${err instanceof Error ? err.message : String(err)}`);
12950
+ }
12951
+ }
12952
+ }
12953
+ }
11520
12954
  return { graph, result, repoName, workspaceRoot };
11521
12955
  }
11522
12956
  program.command("setup").description("Configure MCP server for your editors (one-time setup)").addHelpText("after", `
@@ -11545,8 +12979,8 @@ program.command("setup").description("Configure MCP server for your editors (one
11545
12979
  const configFile = `${configDir}/claude_desktop_config.json`;
11546
12980
  try {
11547
12981
  let existing = {};
11548
- if (fs28.existsSync(configFile)) {
11549
- existing = JSON.parse(fs28.readFileSync(configFile, "utf-8"));
12982
+ if (fs33.existsSync(configFile)) {
12983
+ existing = JSON.parse(fs33.readFileSync(configFile, "utf-8"));
11550
12984
  }
11551
12985
  const merged = {
11552
12986
  ...existing,
@@ -11555,8 +12989,8 @@ program.command("setup").description("Configure MCP server for your editors (one
11555
12989
  ...mcpConfig.mcpServers
11556
12990
  }
11557
12991
  };
11558
- fs28.mkdirSync(configDir, { recursive: true });
11559
- fs28.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
12992
+ fs33.mkdirSync(configDir, { recursive: true });
12993
+ fs33.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
11560
12994
  console.log(`
11561
12995
  \u2705 Written to ${configFile}`);
11562
12996
  } catch (err) {
@@ -11570,7 +13004,7 @@ program.command("setup").description("Configure MCP server for your editors (one
11570
13004
  console.log('\n To verify in VS Code: open Command Palette \u2192 "MCP: List Servers" and confirm code-intel is Running.');
11571
13005
  console.log("\n Next: run `code-intel analyze` inside your project to build the knowledge graph.\n");
11572
13006
  });
11573
- program.command("analyze").description("Index a repository and build the knowledge graph").argument("[path]", "Path to the repository (default: current directory)", ".").option("--force", "Force full re-index, ignoring cached data").option("--incremental", "Only re-parse files changed since last analysis (git diff or mtime)").option("--parallel", "Use worker threads for parse + resolve phases (faster on multi-core)").option("--skills", "Generate .claude/skills/ SKILL.md files from detected clusters").option("--embeddings", "Build vector embeddings for semantic search (slower, recommended)").option("--skip-embeddings", "Skip embedding generation (faster, text-search only)").option("--skip-agents-md", "Preserve any custom edits inside AGENTS.md / CLAUDE.md").option("--skip-git", "Allow indexing directories that are not Git repositories").option("--verbose", "Log every file skipped due to missing parser support").option("--summarize", "Generate AI summaries for function/class/method/interface nodes (opt-in)").option("--llm-provider <provider>", "LLM provider for --summarize: openai | anthropic | ollama (default: ollama)").option("--llm-model <model>", "LLM model name (e.g. gpt-4o-mini, claude-haiku-4-5, llama3)").option("--llm-batch-size <n>", "Concurrent LLM calls per batch (default: 20)", "20").option("--llm-max-nodes <n>", "Max nodes to summarize per run (cost guard)").addHelpText("after", `
13007
+ program.command("analyze").description("Index a repository and build the knowledge graph").argument("[path]", "Path to the repository (default: current directory)", ".").option("--force", "Force full re-index, ignoring cached data").option("--incremental", "Only re-parse files changed since last analysis (git diff or mtime)").option("--parallel", "Use worker threads for parse + resolve phases (faster on multi-core)").option("--skills", "Generate .claude/skills/ SKILL.md files from detected clusters").option("--embeddings", "Build vector embeddings for semantic search (slower, recommended)").option("--skip-embeddings", "Skip embedding generation (faster, text-search only)").option("--skip-agents-md", "Preserve any custom edits inside AGENTS.md / CLAUDE.md").option("--skip-git", "Allow indexing directories that are not Git repositories").option("--verbose", "Log every file skipped due to missing parser support").option("--summarize", "Generate AI summaries for function/class/method/interface nodes (opt-in)").option("--llm-provider <provider>", "LLM provider for --summarize: openai | anthropic | ollama (default: ollama)").option("--llm-model <model>", "LLM model name (e.g. gpt-4o-mini, claude-haiku-4-5, llama3)").option("--llm-batch-size <n>", "Concurrent LLM calls per batch (default: 20)", "20").option("--llm-max-nodes <n>", "Max nodes to summarize per run (cost guard)").option("--no-group-sync", "Skip automatic group sync after analysis").addHelpText("after", `
11574
13008
  Parses your source code with tree-sitter, builds a Knowledge Graph of
11575
13009
  symbols and their relationships, persists it to .code-intel/graph.db,
11576
13010
  and auto-generates AGENTS.md + CLAUDE.md context blocks.
@@ -11626,10 +13060,10 @@ program.command("mcp").description("Start MCP server over stdio \u2014 exposes a
11626
13060
  $ code-intel mcp
11627
13061
  $ code-intel mcp ./my-project
11628
13062
  `).action(async (targetPath) => {
11629
- const workspaceRoot = path30.resolve(targetPath);
11630
- const repoName = path30.basename(workspaceRoot);
13063
+ const workspaceRoot = path33.resolve(targetPath);
13064
+ const repoName = path33.basename(workspaceRoot);
11631
13065
  const dbPath = getDbPath(workspaceRoot);
11632
- const existingIndex = fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
13066
+ const existingIndex = fs33.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
11633
13067
  if (existingIndex) {
11634
13068
  const graph = createKnowledgeGraph();
11635
13069
  const db = new DbManager(dbPath);
@@ -11660,10 +13094,10 @@ program.command("serve").description("Start the local HTTP server + web UI for g
11660
13094
  $ code-intel serve --port 8080
11661
13095
  $ code-intel serve --force
11662
13096
  `).action(async (targetPath, options) => {
11663
- const workspaceRoot = path30.resolve(targetPath);
11664
- const repoName = path30.basename(workspaceRoot);
13097
+ const workspaceRoot = path33.resolve(targetPath);
13098
+ const repoName = path33.basename(workspaceRoot);
11665
13099
  const dbPath = getDbPath(workspaceRoot);
11666
- const existingIndex = !options.force && fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
13100
+ const existingIndex = !options.force && fs33.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
11667
13101
  if (existingIndex) {
11668
13102
  const meta = loadMetadata(workspaceRoot);
11669
13103
  if (meta.parser === "regex" || meta.parser === void 0) {
@@ -11697,10 +13131,10 @@ program.command("watch").description("Start HTTP server + file watcher (auto-rei
11697
13131
  `).action(async (targetPath, options) => {
11698
13132
  const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_file_watcher(), file_watcher_exports));
11699
13133
  const { IncrementalIndexer: IncrementalIndexer2 } = await Promise.resolve().then(() => (init_incremental_indexer(), incremental_indexer_exports));
11700
- const workspaceRoot = path30.resolve(targetPath);
11701
- const repoName = path30.basename(workspaceRoot);
13134
+ const workspaceRoot = path33.resolve(targetPath);
13135
+ const repoName = path33.basename(workspaceRoot);
11702
13136
  const dbPath = getDbPath(workspaceRoot);
11703
- const existingIndex = !options.force && fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
13137
+ const existingIndex = !options.force && fs33.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
11704
13138
  let graph;
11705
13139
  if (existingIndex) {
11706
13140
  const meta = loadMetadata(workspaceRoot);
@@ -11735,7 +13169,7 @@ program.command("watch").description("Start HTTP server + file watcher (auto-rei
11735
13169
  type: "graph:updated",
11736
13170
  indexVersion: meta?.indexVersion ?? "unknown",
11737
13171
  stats: { nodes: graph.size.nodes, edges: graph.size.edges },
11738
- changedFiles: changedFiles.map((f) => path30.relative(workspaceRoot, f)),
13172
+ changedFiles: changedFiles.map((f) => path33.relative(workspaceRoot, f)),
11739
13173
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11740
13174
  });
11741
13175
  }
@@ -11786,7 +13220,7 @@ program.command("status").description("Show index freshness and statistics for a
11786
13220
  $ code-intel status
11787
13221
  $ code-intel status ./my-project
11788
13222
  `).action((targetPath) => {
11789
- const workspaceRoot = path30.resolve(targetPath);
13223
+ const workspaceRoot = path33.resolve(targetPath);
11790
13224
  const meta = loadMetadata(workspaceRoot);
11791
13225
  if (!meta) {
11792
13226
  console.log(`
@@ -11810,18 +13244,18 @@ function trashDirName(repoPath) {
11810
13244
  return `.code-intel-trash-${date}`;
11811
13245
  }
11812
13246
  function softDeleteCodeIntel(repoPath) {
11813
- const codeIntelDir = path30.join(repoPath, ".code-intel");
11814
- if (!fs28.existsSync(codeIntelDir)) return;
13247
+ const codeIntelDir = path33.join(repoPath, ".code-intel");
13248
+ if (!fs33.existsSync(codeIntelDir)) return;
11815
13249
  const trashName = trashDirName();
11816
- const trashDir = path30.join(repoPath, trashName);
13250
+ const trashDir = path33.join(repoPath, trashName);
11817
13251
  let dest = trashDir;
11818
13252
  let counter = 1;
11819
- while (fs28.existsSync(dest)) {
13253
+ while (fs33.existsSync(dest)) {
11820
13254
  dest = `${trashDir}-${counter++}`;
11821
13255
  }
11822
- fs28.renameSync(codeIntelDir, dest);
11823
- fs28.writeFileSync(
11824
- path30.join(dest, "TRASH_META.json"),
13256
+ fs33.renameSync(codeIntelDir, dest);
13257
+ fs33.writeFileSync(
13258
+ path33.join(dest, "TRASH_META.json"),
11825
13259
  JSON.stringify({ deletedAt: (/* @__PURE__ */ new Date()).toISOString(), repoPath, permanent: false }, null, 2)
11826
13260
  );
11827
13261
  console.log(` \u2713 Moved to trash: ${dest}`);
@@ -11830,15 +13264,15 @@ function softDeleteCodeIntel(repoPath) {
11830
13264
  function purgeStaleTrashes(repoPath) {
11831
13265
  const cutoff = Date.now() - TRASH_TTL_DAYS * 24 * 60 * 60 * 1e3;
11832
13266
  try {
11833
- for (const entry of fs28.readdirSync(repoPath)) {
13267
+ for (const entry of fs33.readdirSync(repoPath)) {
11834
13268
  if (!entry.startsWith(".code-intel-trash-")) continue;
11835
- const fullPath = path30.join(repoPath, entry);
11836
- const metaPath = path30.join(fullPath, "TRASH_META.json");
11837
- if (fs28.existsSync(metaPath)) {
13269
+ const fullPath = path33.join(repoPath, entry);
13270
+ const metaPath = path33.join(fullPath, "TRASH_META.json");
13271
+ if (fs33.existsSync(metaPath)) {
11838
13272
  try {
11839
- const meta = JSON.parse(fs28.readFileSync(metaPath, "utf-8"));
13273
+ const meta = JSON.parse(fs33.readFileSync(metaPath, "utf-8"));
11840
13274
  if (new Date(meta.deletedAt).getTime() < cutoff) {
11841
- fs28.rmSync(fullPath, { recursive: true, force: true });
13275
+ fs33.rmSync(fullPath, { recursive: true, force: true });
11842
13276
  console.log(` \u2713 Auto-purged stale trash: ${fullPath}`);
11843
13277
  }
11844
13278
  } catch {
@@ -11865,18 +13299,18 @@ program.command("clean").description("Soft-delete the knowledge graph index for
11865
13299
  if (opts.listTrash) {
11866
13300
  const repos = loadRegistry();
11867
13301
  const roots = repos.map((r) => r.path);
11868
- if (roots.length === 0) roots.push(path30.resolve("."));
13302
+ if (roots.length === 0) roots.push(path33.resolve("."));
11869
13303
  let found = 0;
11870
13304
  for (const root of roots) {
11871
13305
  try {
11872
- for (const entry of fs28.readdirSync(root)) {
13306
+ for (const entry of fs33.readdirSync(root)) {
11873
13307
  if (!entry.startsWith(".code-intel-trash-")) continue;
11874
- const fullPath = path30.join(root, entry);
11875
- const metaPath = path30.join(fullPath, "TRASH_META.json");
13308
+ const fullPath = path33.join(root, entry);
13309
+ const metaPath = path33.join(fullPath, "TRASH_META.json");
11876
13310
  let deletedAt = "unknown";
11877
- if (fs28.existsSync(metaPath)) {
13311
+ if (fs33.existsSync(metaPath)) {
11878
13312
  try {
11879
- deletedAt = JSON.parse(fs28.readFileSync(metaPath, "utf-8")).deletedAt;
13313
+ deletedAt = JSON.parse(fs33.readFileSync(metaPath, "utf-8")).deletedAt;
11880
13314
  } catch {
11881
13315
  }
11882
13316
  }
@@ -11904,9 +13338,9 @@ program.command("clean").description("Soft-delete the knowledge graph index for
11904
13338
  }
11905
13339
  for (const r of repos) {
11906
13340
  if (opts.purge) {
11907
- const codeIntelDir = path30.join(r.path, ".code-intel");
11908
- if (fs28.existsSync(codeIntelDir)) {
11909
- fs28.rmSync(codeIntelDir, { recursive: true, force: true });
13341
+ const codeIntelDir = path33.join(r.path, ".code-intel");
13342
+ if (fs33.existsSync(codeIntelDir)) {
13343
+ fs33.rmSync(codeIntelDir, { recursive: true, force: true });
11910
13344
  console.log(` \u2713 Hard-deleted ${codeIntelDir}`);
11911
13345
  }
11912
13346
  } else {
@@ -11920,11 +13354,11 @@ program.command("clean").description("Soft-delete the knowledge graph index for
11920
13354
  `);
11921
13355
  return;
11922
13356
  }
11923
- const workspaceRoot = path30.resolve(targetPath);
13357
+ const workspaceRoot = path33.resolve(targetPath);
11924
13358
  if (opts.purge) {
11925
- const codeIntelDir = path30.join(workspaceRoot, ".code-intel");
11926
- if (fs28.existsSync(codeIntelDir)) {
11927
- fs28.rmSync(codeIntelDir, { recursive: true, force: true });
13359
+ const codeIntelDir = path33.join(workspaceRoot, ".code-intel");
13360
+ if (fs33.existsSync(codeIntelDir)) {
13361
+ fs33.rmSync(codeIntelDir, { recursive: true, force: true });
11928
13362
  console.log(`
11929
13363
  \u2713 Hard-deleted ${codeIntelDir}`);
11930
13364
  }
@@ -11936,16 +13370,16 @@ program.command("clean").description("Soft-delete the knowledge graph index for
11936
13370
  console.log(" Index cleaned.\n");
11937
13371
  });
11938
13372
  async function loadOrAnalyzeWorkspace(targetPath) {
11939
- const workspaceRoot = path30.resolve(targetPath);
13373
+ const workspaceRoot = path33.resolve(targetPath);
11940
13374
  const dbPath = getDbPath(workspaceRoot);
11941
- const existingIndex = fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
13375
+ const existingIndex = fs33.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
11942
13376
  if (existingIndex) {
11943
13377
  const graph = createKnowledgeGraph();
11944
13378
  const db = new DbManager(dbPath);
11945
13379
  await db.init();
11946
13380
  await loadGraphFromDB(graph, db);
11947
13381
  db.close();
11948
- return { graph, workspaceRoot, repoName: path30.basename(workspaceRoot) };
13382
+ return { graph, workspaceRoot, repoName: path33.basename(workspaceRoot) };
11949
13383
  }
11950
13384
  return analyzeWorkspace(targetPath, { silent: true });
11951
13385
  }
@@ -12373,9 +13807,9 @@ groupCmd.command("status <name>").description("Check index freshness and sync st
12373
13807
  console.log(` \u2717 ${m.groupPath.padEnd(35)} [${m.registryName}] \u2014 NOT IN REGISTRY`);
12374
13808
  continue;
12375
13809
  }
12376
- const metaPath = path30.join(regEntry.path, ".code-intel", "meta.json");
13810
+ const metaPath = path33.join(regEntry.path, ".code-intel", "meta.json");
12377
13811
  try {
12378
- const meta = JSON.parse(fs28.readFileSync(metaPath, "utf-8"));
13812
+ const meta = JSON.parse(fs33.readFileSync(metaPath, "utf-8"));
12379
13813
  const indexedAt = meta.indexedAt;
12380
13814
  const ageMin = Math.round((now - new Date(indexedAt).getTime()) / 6e4);
12381
13815
  const stale = ageMin > 1440 ? " \u26A0 STALE (>24h)" : "";
@@ -12391,6 +13825,94 @@ groupCmd.command("status <name>").description("Check index freshness and sync st
12391
13825
  }
12392
13826
  }
12393
13827
  });
13828
+ groupCmd.command("init-workspace [path]").description("Auto-discover workspace packages and create a group").option("--name <name>", "Group name (default: workspace root dirname)").option("--no-analyze", "Register packages without running analysis").option("--yes", "Skip confirmation prompt").option("--parallel <n>", "Concurrent analyses (default: 2)", "2").action(async (targetPath, opts) => {
13829
+ const root = path33.resolve(targetPath ?? ".");
13830
+ const ws = await detectWorkspace(root);
13831
+ if (!ws) {
13832
+ console.error(`
13833
+ \u2717 No workspace config found in: ${root}`);
13834
+ console.error(` Expected: package.json with workspaces, pnpm-workspace.yaml, nx.json, or turbo.json
13835
+ `);
13836
+ process.exit(1);
13837
+ }
13838
+ const groupName = opts.name ?? path33.basename(root);
13839
+ console.log(`
13840
+ \u25C8 Workspace detected: ${ws.type}`);
13841
+ console.log(` Group name : ${groupName}`);
13842
+ console.log(` Packages (${ws.packages.length}):
13843
+ `);
13844
+ for (const pkg of ws.packages) {
13845
+ console.log(` ${pkg.name.padEnd(35)} ${pkg.path}`);
13846
+ }
13847
+ if (!opts.yes) {
13848
+ const answer = await new Promise((resolve) => {
13849
+ process.stdout.write("\n Proceed? [y/N] ");
13850
+ process.stdin.once("data", (data) => resolve(data.toString().trim().toLowerCase()));
13851
+ });
13852
+ if (answer !== "y" && answer !== "yes") {
13853
+ console.log("\n Aborted.\n");
13854
+ process.exit(0);
13855
+ }
13856
+ }
13857
+ if (groupExists(groupName)) {
13858
+ console.log(`
13859
+ \u2139 Group "${groupName}" already exists \u2014 will update.`);
13860
+ } else {
13861
+ saveGroup({ name: groupName, createdAt: (/* @__PURE__ */ new Date()).toISOString(), members: [] });
13862
+ console.log(`
13863
+ \u2705 Group "${groupName}" created.`);
13864
+ }
13865
+ const parallel = Math.max(1, parseInt(opts.parallel, 10));
13866
+ const results = [];
13867
+ if (opts.analyze !== false) {
13868
+ console.log(`
13869
+ Analyzing ${ws.packages.length} packages (parallel: ${parallel})\u2026
13870
+ `);
13871
+ for (let i = 0; i < ws.packages.length; i += parallel) {
13872
+ const chunk = ws.packages.slice(i, i + parallel);
13873
+ await Promise.all(chunk.map(async (pkg, j) => {
13874
+ const idx = i + j + 1;
13875
+ console.log(` [${idx}/${ws.packages.length}] Analyzing ${pkg.name}\u2026`);
13876
+ try {
13877
+ await analyzeWorkspace(pkg.path, { silent: true });
13878
+ results.push({ name: pkg.name, status: "\u2705" });
13879
+ } catch (err) {
13880
+ results.push({ name: pkg.name, status: `\u2717 ${err instanceof Error ? err.message : String(err)}` });
13881
+ }
13882
+ }));
13883
+ }
13884
+ } else {
13885
+ for (const pkg of ws.packages) {
13886
+ results.push({ name: pkg.name, status: "--no-analyze" });
13887
+ }
13888
+ }
13889
+ for (const pkg of ws.packages) {
13890
+ try {
13891
+ upsertRepo({ name: pkg.name, path: pkg.path, indexedAt: (/* @__PURE__ */ new Date()).toISOString(), stats: { nodes: 0, edges: 0, files: 0 } });
13892
+ addMember(groupName, { groupPath: pkg.name, registryName: pkg.name });
13893
+ } catch {
13894
+ }
13895
+ }
13896
+ console.log(`
13897
+ \u27F3 Syncing group "${groupName}"\u2026`);
13898
+ try {
13899
+ const group = loadGroup(groupName);
13900
+ const syncResult = await syncGroup(group);
13901
+ saveSyncResult(syncResult);
13902
+ group.lastSync = syncResult.syncedAt;
13903
+ saveGroup(group);
13904
+ console.log(` \u2705 Sync complete \u2014 ${syncResult.contracts.length} contracts, ${syncResult.links.length} cross-links`);
13905
+ } catch (err) {
13906
+ console.warn(` \u26A0 Sync failed: ${err instanceof Error ? err.message : String(err)}`);
13907
+ }
13908
+ console.log(`
13909
+ Summary:
13910
+ `);
13911
+ for (const r of results) {
13912
+ console.log(` ${r.status.padEnd(4)} ${r.name}`);
13913
+ }
13914
+ console.log("");
13915
+ });
12394
13916
  var userCmd = program.command("user").description("Manage local user accounts");
12395
13917
  userCmd.command("create <username>").description("Create a new local user account").requiredOption("--role <role>", "Role: admin | analyst | viewer | repo-owner").option("--password <password>", "Password (prompted if omitted)").addHelpText("after", `
12396
13918
  Examples:
@@ -12592,7 +14114,7 @@ backupCmd.command("create [path]").description("Create an encrypted backup of th
12592
14114
  $ code-intel backup create
12593
14115
  $ code-intel backup create ./my-project
12594
14116
  `).action((targetPath = ".") => {
12595
- const repoPath = path30.resolve(targetPath);
14117
+ const repoPath = path33.resolve(targetPath);
12596
14118
  const svc = new BackupService();
12597
14119
  try {
12598
14120
  const entry = svc.createBackup(repoPath);
@@ -12621,7 +14143,7 @@ backupCmd.command("list").description("List all available backups").action(() =>
12621
14143
  Backups (${entries.length}):
12622
14144
  `);
12623
14145
  for (const e of entries) {
12624
- const exists = fs28.existsSync(e.path);
14146
+ const exists = fs33.existsSync(e.path);
12625
14147
  const status = exists ? "\u2713" : "\u2717 (missing)";
12626
14148
  console.log(` ${status} ${e.id.slice(0, 8)} ${e.createdAt} ${(e.size / 1024).toFixed(1)} KB \u2192 ${e.repoPath}`);
12627
14149
  }
@@ -12634,7 +14156,7 @@ backupCmd.command("restore <id>").description("Restore a backup by ID").option("
12634
14156
  `).action((id, opts) => {
12635
14157
  const svc = new BackupService();
12636
14158
  try {
12637
- const targetPath = opts.target ? path30.resolve(opts.target) : void 0;
14159
+ const targetPath = opts.target ? path33.resolve(opts.target) : void 0;
12638
14160
  svc.restoreBackup(id, targetPath);
12639
14161
  console.log(`
12640
14162
  \u2705 Backup "${id}" restored successfully.
@@ -12653,8 +14175,8 @@ program.command("migrate").description("Manage database schema migrations").opti
12653
14175
  $ code-intel migrate
12654
14176
  $ code-intel migrate --rollback
12655
14177
  `).action((opts) => {
12656
- const dbPath = opts.db ?? path30.join(os12.homedir(), ".code-intel", "users.db");
12657
- if (!fs28.existsSync(dbPath)) {
14178
+ const dbPath = opts.db ?? path33.join(os12.homedir(), ".code-intel", "users.db");
14179
+ if (!fs33.existsSync(dbPath)) {
12658
14180
  console.error(`
12659
14181
  \u2717 Database not found: ${dbPath}
12660
14182
  Run \`code-intel serve\` or \`code-intel user create\` first.
@@ -12769,15 +14291,15 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
12769
14291
  }
12770
14292
  try {
12771
14293
  const tokens = await pollDeviceFlow3(config, deviceResponse);
12772
- const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
14294
+ const tokenPath = path33.join(os12.homedir(), ".code-intel", "oidc-token.json");
12773
14295
  const tokenData = {
12774
14296
  accessToken: tokens.accessToken,
12775
14297
  refreshToken: tokens.refreshToken,
12776
14298
  server: serverUrl,
12777
14299
  storedAt: (/* @__PURE__ */ new Date()).toISOString()
12778
14300
  };
12779
- fs28.mkdirSync(path30.dirname(tokenPath), { recursive: true });
12780
- fs28.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
14301
+ fs33.mkdirSync(path33.dirname(tokenPath), { recursive: true });
14302
+ fs33.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
12781
14303
  console.log(` \u2705 Authenticated successfully!`);
12782
14304
  console.log(` Token stored at: ${tokenPath}`);
12783
14305
  console.log(` Use CODE_INTEL_TOKEN env var or --token flag to use it with CLI/MCP.
@@ -12790,13 +14312,13 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
12790
14312
  }
12791
14313
  });
12792
14314
  authCmd.command("status").description("Show the current OIDC authentication status").action(() => {
12793
- const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
12794
- if (!fs28.existsSync(tokenPath)) {
14315
+ const tokenPath = path33.join(os12.homedir(), ".code-intel", "oidc-token.json");
14316
+ if (!fs33.existsSync(tokenPath)) {
12795
14317
  console.log("\n Not authenticated via OIDC. Run: code-intel auth login\n");
12796
14318
  return;
12797
14319
  }
12798
14320
  try {
12799
- const data = JSON.parse(fs28.readFileSync(tokenPath, "utf-8"));
14321
+ const data = JSON.parse(fs33.readFileSync(tokenPath, "utf-8"));
12800
14322
  console.log(`
12801
14323
  \u2705 OIDC token stored`);
12802
14324
  console.log(` Server : ${data.server ?? "unknown"}`);
@@ -12808,9 +14330,9 @@ authCmd.command("status").description("Show the current OIDC authentication stat
12808
14330
  }
12809
14331
  });
12810
14332
  authCmd.command("logout").description("Remove locally stored OIDC token").action(() => {
12811
- const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
12812
- if (fs28.existsSync(tokenPath)) {
12813
- fs28.unlinkSync(tokenPath);
14333
+ const tokenPath = path33.join(os12.homedir(), ".code-intel", "oidc-token.json");
14334
+ if (fs33.existsSync(tokenPath)) {
14335
+ fs33.unlinkSync(tokenPath);
12814
14336
  console.log("\n \u2705 OIDC token removed. You are now logged out.\n");
12815
14337
  } else {
12816
14338
  console.log("\n No stored token found.\n");
@@ -12934,8 +14456,8 @@ program.command("config-validate <file>").description("Validate a JSON config fi
12934
14456
  $ code-intel config-validate ./config.json
12935
14457
  $ code-intel config-validate ~/.code-intel/config.json
12936
14458
  `).action((file) => {
12937
- const filePath = path30.resolve(file);
12938
- if (!fs28.existsSync(filePath)) {
14459
+ const filePath = path33.resolve(file);
14460
+ if (!fs33.existsSync(filePath)) {
12939
14461
  console.error(`
12940
14462
  \u2717 File not found: ${filePath}
12941
14463
  `);
@@ -12943,7 +14465,7 @@ program.command("config-validate <file>").description("Validate a JSON config fi
12943
14465
  }
12944
14466
  let cfg;
12945
14467
  try {
12946
- cfg = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
14468
+ cfg = JSON.parse(fs33.readFileSync(filePath, "utf-8"));
12947
14469
  } catch (err) {
12948
14470
  console.error(`
12949
14471
  \u2717 Could not parse JSON: ${err instanceof Error ? err.message : err}
@@ -12964,7 +14486,7 @@ ${err instanceof Error ? err.message : err}
12964
14486
  });
12965
14487
  (function ensurePermissions() {
12966
14488
  try {
12967
- const dir = path30.join(os12.homedir(), ".code-intel");
14489
+ const dir = path33.join(os12.homedir(), ".code-intel");
12968
14490
  secureMkdir(dir);
12969
14491
  tightenDbFiles(dir);
12970
14492
  } catch {
@@ -12981,22 +14503,22 @@ program.command("health").description("Run code health checks: dead code, circul
12981
14503
  $ code-intel health --json
12982
14504
  $ code-intel health --threshold 80
12983
14505
  `).action(async (targetPath, opts) => {
12984
- const workspaceRoot = path30.resolve(targetPath);
14506
+ const workspaceRoot = path33.resolve(targetPath);
12985
14507
  const dbPath = getDbPath(workspaceRoot);
12986
14508
  const meta = loadMetadata(workspaceRoot);
12987
- if (!meta || !fs28.existsSync(dbPath)) {
14509
+ if (!meta || !fs33.existsSync(dbPath)) {
12988
14510
  console.error(`
12989
14511
  \u2717 ${workspaceRoot} is not indexed.`);
12990
14512
  console.error(" Run `code-intel analyze` first to build the index.\n");
12991
14513
  process.exit(1);
12992
14514
  }
12993
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
14515
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
12994
14516
  const graph = createKnowledgeGraph();
12995
14517
  const db = new DbManager(dbPath);
12996
14518
  await db.init();
12997
14519
  await loadGraphFromDB(graph, db);
12998
14520
  db.close();
12999
- const report = computeHealthReport2(graph);
14521
+ const report = computeHealthReport3(graph);
13000
14522
  if (opts.json) {
13001
14523
  console.log(JSON.stringify({
13002
14524
  score: report.score,
@@ -13069,7 +14591,7 @@ program.command("query").description("Execute a GQL (Graph Query Language) query
13069
14591
  $ code-intel query --list
13070
14592
  $ code-intel query --delete auth-search
13071
14593
  `).action(async (gqlArg, opts) => {
13072
- const workspaceRoot = path30.resolve(opts.path);
14594
+ const workspaceRoot = path33.resolve(opts.path);
13073
14595
  const { saveQuery: saveQuery2, loadQuery: loadQuery2, listQueries: listQueries2, deleteQuery: deleteQuery2 } = await Promise.resolve().then(() => (init_saved_queries(), saved_queries_exports));
13074
14596
  if (opts.list) {
13075
14597
  const queries = listQueries2(workspaceRoot);
@@ -13128,14 +14650,14 @@ program.command("query").description("Execute a GQL (Graph Query Language) query
13128
14650
  }
13129
14651
  gqlInput = content;
13130
14652
  } else if (opts.file) {
13131
- const filePath = path30.resolve(opts.file);
13132
- if (!fs28.existsSync(filePath)) {
14653
+ const filePath = path33.resolve(opts.file);
14654
+ if (!fs33.existsSync(filePath)) {
13133
14655
  console.error(`
13134
14656
  \u2717 File not found: ${filePath}
13135
14657
  `);
13136
14658
  process.exit(1);
13137
14659
  }
13138
- gqlInput = fs28.readFileSync(filePath, "utf-8");
14660
+ gqlInput = fs33.readFileSync(filePath, "utf-8");
13139
14661
  } else if (gqlArg) {
13140
14662
  gqlInput = gqlArg;
13141
14663
  } else {
@@ -13249,6 +14771,101 @@ program.command("query").description("Execute a GQL (Graph Query Language) query
13249
14771
  `);
13250
14772
  }
13251
14773
  });
14774
+ program.command("pr-impact").description("Compute PR blast radius and risk scores using git diff").option("--base <ref>", "Base git ref (default: main)", "main").option("--head <ref>", "Head git ref (default: HEAD)", "HEAD").option("--fail-on <level>", "Exit code 1 if this risk level found: HIGH|MEDIUM", "").option("--format <fmt>", "Output format: text|json|sarif (default: text)", "text").option("--path <path>", "Repo path (default: current dir)").addHelpText("after", `
14775
+ Computes the blast radius for files changed between two git refs.
14776
+
14777
+ Examples:
14778
+ $ code-intel pr-impact --base main --head HEAD
14779
+ $ code-intel pr-impact --base main --head HEAD --fail-on HIGH
14780
+ $ code-intel pr-impact --base main --head HEAD --format sarif
14781
+ $ code-intel pr-impact --base main --head HEAD --format json
14782
+ `).action(async (opts) => {
14783
+ const repoPath = path33.resolve(opts.path ?? ".");
14784
+ const { execSync: execSync3 } = await import('child_process');
14785
+ let diff;
14786
+ try {
14787
+ diff = execSync3(`git diff --name-only ${opts.base}..${opts.head}`, {
14788
+ cwd: repoPath,
14789
+ encoding: "utf-8"
14790
+ });
14791
+ } catch (err) {
14792
+ console.error(`
14793
+ \u2717 git diff failed: ${err.message}
14794
+ `);
14795
+ process.exit(1);
14796
+ }
14797
+ const changedFiles = diff.trim().split("\n").filter(Boolean);
14798
+ if (changedFiles.length === 0) {
14799
+ if (opts.format === "json") {
14800
+ console.log(JSON.stringify({ changedSymbols: [], impactedSymbols: [], riskSummary: { HIGH: 0, MEDIUM: 0, LOW: 0 }, coverageGaps: [], filesToReview: [], crossRepoImpact: null }, null, 2));
14801
+ } else {
14802
+ console.log("\n \u25C8 PR Impact Report\n\n No changed files detected.\n");
14803
+ }
14804
+ return;
14805
+ }
14806
+ const { graph } = await loadOrAnalyzeWorkspace(repoPath);
14807
+ const { computePRImpact: computePRImpact2 } = await Promise.resolve().then(() => (init_pr_impact(), pr_impact_exports));
14808
+ const result = computePRImpact2(graph, changedFiles, 5);
14809
+ const fmt = (opts.format ?? "text").toLowerCase();
14810
+ if (fmt === "json") {
14811
+ console.log(JSON.stringify(result, null, 2));
14812
+ } else if (fmt === "sarif") {
14813
+ const { buildSARIF: buildSARIF2 } = await Promise.resolve().then(() => (init_sarif_builder(), sarif_builder_exports));
14814
+ const sarif = buildSARIF2(result, _pkg.version);
14815
+ console.log(JSON.stringify(sarif, null, 2));
14816
+ } else {
14817
+ const highSymbols = result.changedSymbols.filter((s) => s.risk === "HIGH");
14818
+ const medSymbols = result.changedSymbols.filter((s) => s.risk === "MEDIUM");
14819
+ const lowSymbols = result.changedSymbols.filter((s) => s.risk === "LOW");
14820
+ console.log("\n \u25C8 PR Impact Report\n");
14821
+ console.log(` Changed files: ${changedFiles.length}`);
14822
+ console.log(` High-risk symbols: ${result.riskSummary.HIGH} | Medium: ${result.riskSummary.MEDIUM} | Low: ${result.riskSummary.LOW}
14823
+ `);
14824
+ if (highSymbols.length > 0) {
14825
+ console.log(" HIGH RISK:");
14826
+ for (const s of highSymbols) {
14827
+ const testNote = s.testCoverage ? "" : " no tests";
14828
+ console.log(` \u25C6 ${s.name.padEnd(24)} callers: ${s.callerCount}${testNote}`);
14829
+ }
14830
+ console.log("");
14831
+ }
14832
+ if (medSymbols.length > 0) {
14833
+ console.log(" MEDIUM RISK:");
14834
+ for (const s of medSymbols) {
14835
+ const testNote = s.testCoverage ? "" : " no tests";
14836
+ console.log(` \u25C6 ${s.name.padEnd(24)} callers: ${s.callerCount}${testNote}`);
14837
+ }
14838
+ console.log("");
14839
+ }
14840
+ if (lowSymbols.length > 0) {
14841
+ console.log(" LOW RISK:");
14842
+ for (const s of lowSymbols) {
14843
+ console.log(` \u25C6 ${s.name.padEnd(24)} callers: ${s.callerCount}`);
14844
+ }
14845
+ console.log("");
14846
+ }
14847
+ if (result.filesToReview.length > 0) {
14848
+ console.log(" Files to review:");
14849
+ for (const f of result.filesToReview) {
14850
+ console.log(` ${f}`);
14851
+ }
14852
+ console.log("");
14853
+ }
14854
+ if (result.coverageGaps.length > 0) {
14855
+ console.log(" Coverage gaps:");
14856
+ for (const gap of result.coverageGaps) {
14857
+ console.log(` ${gap}`);
14858
+ }
14859
+ console.log("");
14860
+ }
14861
+ }
14862
+ const failOn = (opts.failOn ?? "").toUpperCase();
14863
+ if (failOn === "HIGH" && result.riskSummary.HIGH > 0) {
14864
+ process.exit(1);
14865
+ } else if (failOn === "MEDIUM" && (result.riskSummary.HIGH > 0 || result.riskSummary.MEDIUM > 0)) {
14866
+ process.exit(1);
14867
+ }
14868
+ });
13252
14869
  program.parse();
13253
14870
  //# sourceMappingURL=main.js.map
13254
14871
  //# sourceMappingURL=main.js.map