gitnexus 1.2.8 → 1.2.9

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.
Files changed (38) hide show
  1. package/README.md +194 -186
  2. package/dist/cli/ai-context.js +71 -71
  3. package/dist/cli/analyze.js +1 -1
  4. package/dist/cli/setup.js +8 -1
  5. package/dist/cli/view.d.ts +13 -0
  6. package/dist/cli/view.js +59 -0
  7. package/dist/core/augmentation/engine.js +20 -20
  8. package/dist/core/embeddings/embedding-pipeline.js +26 -26
  9. package/dist/core/graph/html-graph-viewer.d.ts +15 -0
  10. package/dist/core/graph/html-graph-viewer.js +542 -0
  11. package/dist/core/graph/html-graph-viewer.test.d.ts +1 -0
  12. package/dist/core/graph/html-graph-viewer.test.js +67 -0
  13. package/dist/core/ingestion/cluster-enricher.js +16 -16
  14. package/dist/core/kuzu/kuzu-adapter.js +9 -9
  15. package/dist/core/kuzu/schema.js +256 -256
  16. package/dist/core/search/bm25-index.js +5 -5
  17. package/dist/core/search/hybrid-search.js +3 -3
  18. package/dist/core/wiki/graph-queries.js +52 -52
  19. package/dist/core/wiki/html-viewer.js +192 -192
  20. package/dist/core/wiki/prompts.js +82 -82
  21. package/dist/mcp/core/embedder.js +8 -4
  22. package/dist/mcp/local/local-backend.d.ts +6 -0
  23. package/dist/mcp/local/local-backend.js +224 -117
  24. package/dist/mcp/resources.js +42 -42
  25. package/dist/mcp/server.js +16 -16
  26. package/dist/mcp/tools.js +86 -77
  27. package/dist/server/api.d.ts +4 -2
  28. package/dist/server/api.js +253 -83
  29. package/hooks/claude/gitnexus-hook.cjs +135 -135
  30. package/hooks/claude/pre-tool-use.sh +78 -78
  31. package/hooks/claude/session-start.sh +42 -42
  32. package/package.json +82 -82
  33. package/skills/debugging.md +85 -85
  34. package/skills/exploring.md +75 -75
  35. package/skills/impact-analysis.md +94 -94
  36. package/skills/refactoring.md +113 -113
  37. package/vendor/leiden/index.cjs +355 -355
  38. package/vendor/leiden/utils.cjs +392 -392
@@ -228,8 +228,10 @@ export class LocalBackend {
228
228
  switch (method) {
229
229
  case 'query':
230
230
  return this.query(repo, params);
231
- case 'cypher':
232
- return this.cypher(repo, params);
231
+ case 'cypher': {
232
+ const raw = await this.cypher(repo, params);
233
+ return this.formatCypherAsMarkdown(raw);
234
+ }
233
235
  case 'context':
234
236
  return this.context(repo, params);
235
237
  case 'impact':
@@ -320,22 +322,24 @@ export class LocalBackend {
320
322
  // Find processes this symbol participates in
321
323
  let processRows = [];
322
324
  try {
323
- processRows = await executeQuery(repo.id, `
324
- MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
325
- RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
325
+ processRows = await executeQuery(repo.id, `
326
+ MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
327
+ RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
326
328
  `);
327
329
  }
328
330
  catch { /* symbol might not be in any process */ }
329
- // Get cluster cohesion as internal ranking signal (never exposed)
331
+ // Get cluster membership + cohesion (cohesion used as internal ranking signal)
330
332
  let cohesion = 0;
333
+ let module;
331
334
  try {
332
- const cohesionRows = await executeQuery(repo.id, `
333
- MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
334
- RETURN c.cohesion AS cohesion
335
- LIMIT 1
335
+ const cohesionRows = await executeQuery(repo.id, `
336
+ MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
337
+ RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
338
+ LIMIT 1
336
339
  `);
337
340
  if (cohesionRows.length > 0) {
338
341
  cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
342
+ module = cohesionRows[0].module ?? cohesionRows[0][1];
339
343
  }
340
344
  }
341
345
  catch { /* no cluster info */ }
@@ -343,9 +347,9 @@ export class LocalBackend {
343
347
  let content;
344
348
  if (includeContent) {
345
349
  try {
346
- const contentRows = await executeQuery(repo.id, `
347
- MATCH (n {id: '${escaped}'})
348
- RETURN n.content AS content
350
+ const contentRows = await executeQuery(repo.id, `
351
+ MATCH (n {id: '${escaped}'})
352
+ RETURN n.content AS content
349
353
  `);
350
354
  if (contentRows.length > 0) {
351
355
  content = contentRows[0].content ?? contentRows[0][0];
@@ -360,6 +364,7 @@ export class LocalBackend {
360
364
  filePath: sym.filePath,
361
365
  startLine: sym.startLine,
362
366
  endLine: sym.endLine,
367
+ ...(module ? { module } : {}),
363
368
  ...(includeContent && content ? { content } : {}),
364
369
  };
365
370
  if (processRows.length === 0) {
@@ -450,11 +455,11 @@ export class LocalBackend {
450
455
  for (const bm25Result of bm25Results) {
451
456
  const fullPath = bm25Result.filePath;
452
457
  try {
453
- const symbolQuery = `
454
- MATCH (n)
455
- WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
456
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
457
- LIMIT 3
458
+ const symbolQuery = `
459
+ MATCH (n)
460
+ WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
461
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
462
+ LIMIT 3
458
463
  `;
459
464
  const symbols = await executeQuery(repo.id, symbolQuery);
460
465
  if (symbols.length > 0) {
@@ -497,17 +502,21 @@ export class LocalBackend {
497
502
  */
498
503
  async semanticSearch(repo, query, limit) {
499
504
  try {
505
+ // Check if embedding table exists before loading the model (avoids heavy model init when embeddings are off)
506
+ const tableCheck = await executeQuery(repo.id, `MATCH (e:CodeEmbedding) RETURN COUNT(*) AS cnt LIMIT 1`);
507
+ if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0)
508
+ return [];
500
509
  const queryVec = await embedQuery(query);
501
510
  const dims = getEmbeddingDims();
502
511
  const queryVecStr = `[${queryVec.join(',')}]`;
503
- const vectorQuery = `
504
- CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
505
- CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
506
- YIELD node AS emb, distance
507
- WITH emb, distance
508
- WHERE distance < 0.6
509
- RETURN emb.nodeId AS nodeId, distance
510
- ORDER BY distance
512
+ const vectorQuery = `
513
+ CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
514
+ CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
515
+ YIELD node AS emb, distance
516
+ WITH emb, distance
517
+ WHERE distance < 0.6
518
+ RETURN emb.nodeId AS nodeId, distance
519
+ ORDER BY distance
511
520
  `;
512
521
  const embResults = await executeQuery(repo.id, vectorQuery);
513
522
  if (embResults.length === 0)
@@ -544,11 +553,15 @@ export class LocalBackend {
544
553
  }
545
554
  return results;
546
555
  }
547
- catch (err) {
548
- console.error('GitNexus: Semantic search unavailable -', err.message);
556
+ catch {
557
+ // Expected when embeddings are disabled — silently fall back to BM25-only
549
558
  return [];
550
559
  }
551
560
  }
561
+ async executeCypher(repoName, query) {
562
+ const repo = await this.resolveRepo(repoName);
563
+ return this.cypher(repo, { query });
564
+ }
552
565
  async cypher(repo, params) {
553
566
  await this.ensureInitialized(repo.id);
554
567
  if (!isKuzuReady(repo.id)) {
@@ -562,6 +575,34 @@ export class LocalBackend {
562
575
  return { error: err.message || 'Query failed' };
563
576
  }
564
577
  }
578
+ /**
579
+ * Format raw Cypher result rows as a markdown table for LLM readability.
580
+ * Falls back to raw result if rows aren't tabular objects.
581
+ */
582
+ formatCypherAsMarkdown(result) {
583
+ if (!Array.isArray(result) || result.length === 0)
584
+ return result;
585
+ const firstRow = result[0];
586
+ if (typeof firstRow !== 'object' || firstRow === null)
587
+ return result;
588
+ const keys = Object.keys(firstRow);
589
+ if (keys.length === 0)
590
+ return result;
591
+ const header = '| ' + keys.join(' | ') + ' |';
592
+ const separator = '| ' + keys.map(() => '---').join(' | ') + ' |';
593
+ const dataRows = result.map((row) => '| ' + keys.map(k => {
594
+ const v = row[k];
595
+ if (v === null || v === undefined)
596
+ return '';
597
+ if (typeof v === 'object')
598
+ return JSON.stringify(v);
599
+ return String(v);
600
+ }).join(' | ') + ' |');
601
+ return {
602
+ markdown: [header, separator, ...dataRows].join('\n'),
603
+ row_count: result.length,
604
+ };
605
+ }
565
606
  /**
566
607
  * Aggregate same-named clusters: group by heuristicLabel, sum symbols,
567
608
  * weighted-average cohesion, filter out tiny clusters (<5 symbols).
@@ -612,11 +653,11 @@ export class LocalBackend {
612
653
  try {
613
654
  // Fetch more raw communities than the display limit so aggregation has enough data
614
655
  const rawLimit = Math.max(limit * 5, 200);
615
- const clusters = await executeQuery(repo.id, `
616
- MATCH (c:Community)
617
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
618
- ORDER BY c.symbolCount DESC
619
- LIMIT ${rawLimit}
656
+ const clusters = await executeQuery(repo.id, `
657
+ MATCH (c:Community)
658
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
659
+ ORDER BY c.symbolCount DESC
660
+ LIMIT ${rawLimit}
620
661
  `);
621
662
  const rawClusters = clusters.map((c) => ({
622
663
  id: c.id || c[0],
@@ -633,11 +674,11 @@ export class LocalBackend {
633
674
  }
634
675
  if (params.showProcesses !== false) {
635
676
  try {
636
- const processes = await executeQuery(repo.id, `
637
- MATCH (p:Process)
638
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
639
- ORDER BY p.stepCount DESC
640
- LIMIT ${limit}
677
+ const processes = await executeQuery(repo.id, `
678
+ MATCH (p:Process)
679
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
680
+ ORDER BY p.stepCount DESC
681
+ LIMIT ${limit}
641
682
  `);
642
683
  result.processes = processes.map((p) => ({
643
684
  id: p.id || p[0],
@@ -668,10 +709,10 @@ export class LocalBackend {
668
709
  let symbols;
669
710
  if (uid) {
670
711
  const escaped = uid.replace(/'/g, "''");
671
- symbols = await executeQuery(repo.id, `
672
- MATCH (n {id: '${escaped}'})
673
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
674
- LIMIT 1
712
+ symbols = await executeQuery(repo.id, `
713
+ MATCH (n {id: '${escaped}'})
714
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
715
+ LIMIT 1
675
716
  `);
676
717
  }
677
718
  else {
@@ -688,10 +729,10 @@ export class LocalBackend {
688
729
  else {
689
730
  whereClause = `WHERE n.name = '${escaped}'`;
690
731
  }
691
- symbols = await executeQuery(repo.id, `
692
- MATCH (n) ${whereClause}
693
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
694
- LIMIT 10
732
+ symbols = await executeQuery(repo.id, `
733
+ MATCH (n) ${whereClause}
734
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
735
+ LIMIT 10
695
736
  `);
696
737
  }
697
738
  if (symbols.length === 0) {
@@ -715,25 +756,25 @@ export class LocalBackend {
715
756
  const sym = symbols[0];
716
757
  const symId = (sym.id || sym[0]).replace(/'/g, "''");
717
758
  // Categorized incoming refs
718
- const incomingRows = await executeQuery(repo.id, `
719
- MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
720
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
721
- RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
722
- LIMIT 30
759
+ const incomingRows = await executeQuery(repo.id, `
760
+ MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
761
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
762
+ RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
763
+ LIMIT 30
723
764
  `);
724
765
  // Categorized outgoing refs
725
- const outgoingRows = await executeQuery(repo.id, `
726
- MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
727
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
728
- RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
729
- LIMIT 30
766
+ const outgoingRows = await executeQuery(repo.id, `
767
+ MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
768
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
769
+ RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
770
+ LIMIT 30
730
771
  `);
731
772
  // Process participation
732
773
  let processRows = [];
733
774
  try {
734
- processRows = await executeQuery(repo.id, `
735
- MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
736
- RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
775
+ processRows = await executeQuery(repo.id, `
776
+ MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
777
+ RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
737
778
  `);
738
779
  }
739
780
  catch { /* no process info */ }
@@ -787,10 +828,10 @@ export class LocalBackend {
787
828
  }
788
829
  if (type === 'cluster') {
789
830
  const escaped = name.replace(/'/g, "''");
790
- const clusterQuery = `
791
- MATCH (c:Community)
792
- WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
793
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
831
+ const clusterQuery = `
832
+ MATCH (c:Community)
833
+ WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
834
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
794
835
  `;
795
836
  const clusters = await executeQuery(repo.id, clusterQuery);
796
837
  if (clusters.length === 0)
@@ -805,11 +846,11 @@ export class LocalBackend {
805
846
  totalSymbols += s;
806
847
  weightedCohesion += (c.cohesion || 0) * s;
807
848
  }
808
- const members = await executeQuery(repo.id, `
809
- MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
810
- WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
811
- RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
812
- LIMIT 30
849
+ const members = await executeQuery(repo.id, `
850
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
851
+ WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
852
+ RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
853
+ LIMIT 30
813
854
  `);
814
855
  return {
815
856
  cluster: {
@@ -826,20 +867,20 @@ export class LocalBackend {
826
867
  };
827
868
  }
828
869
  if (type === 'process') {
829
- const processes = await executeQuery(repo.id, `
830
- MATCH (p:Process)
831
- WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
832
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
833
- LIMIT 1
870
+ const processes = await executeQuery(repo.id, `
871
+ MATCH (p:Process)
872
+ WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
873
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
874
+ LIMIT 1
834
875
  `);
835
876
  if (processes.length === 0)
836
877
  return { error: `Process '${name}' not found` };
837
878
  const proc = processes[0];
838
879
  const procId = proc.id || proc[0];
839
- const steps = await executeQuery(repo.id, `
840
- MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
841
- RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
842
- ORDER BY r.step
880
+ const steps = await executeQuery(repo.id, `
881
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
882
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
883
+ ORDER BY r.step
843
884
  `);
844
885
  return {
845
886
  process: {
@@ -900,10 +941,10 @@ export class LocalBackend {
900
941
  for (const file of changedFiles) {
901
942
  const escaped = file.replace(/\\/g, '/').replace(/'/g, "''");
902
943
  try {
903
- const symbols = await executeQuery(repo.id, `
904
- MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
905
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
906
- LIMIT 20
944
+ const symbols = await executeQuery(repo.id, `
945
+ MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
946
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
947
+ LIMIT 20
907
948
  `);
908
949
  for (const sym of symbols) {
909
950
  changedSymbols.push({
@@ -922,9 +963,9 @@ export class LocalBackend {
922
963
  for (const sym of changedSymbols) {
923
964
  const escaped = sym.id.replace(/'/g, "''");
924
965
  try {
925
- const procs = await executeQuery(repo.id, `
926
- MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
927
- RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
966
+ const procs = await executeQuery(repo.id, `
967
+ MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
968
+ RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
928
969
  `);
929
970
  for (const proc of procs) {
930
971
  const pid = proc.pid || proc[0];
@@ -1099,11 +1140,11 @@ export class LocalBackend {
1099
1140
  const minConfidence = params.minConfidence ?? 0;
1100
1141
  const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
1101
1142
  const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
1102
- const targetQuery = `
1103
- MATCH (n)
1104
- WHERE n.name = '${target.replace(/'/g, "''")}'
1105
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1106
- LIMIT 1
1143
+ const targetQuery = `
1144
+ MATCH (n)
1145
+ WHERE n.name = '${target.replace(/'/g, "''")}'
1146
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1147
+ LIMIT 1
1107
1148
  `;
1108
1149
  const targets = await executeQuery(repo.id, targetQuery);
1109
1150
  if (targets.length === 0)
@@ -1151,6 +1192,64 @@ export class LocalBackend {
1151
1192
  grouped[item.depth] = [];
1152
1193
  grouped[item.depth].push(item);
1153
1194
  }
1195
+ // ── Enrichment: affected processes, modules, risk ──────────────
1196
+ const directCount = (grouped[1] || []).length;
1197
+ let affectedProcesses = [];
1198
+ let affectedModules = [];
1199
+ if (impacted.length > 0) {
1200
+ const allIds = impacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', ');
1201
+ const d1Ids = (grouped[1] || []).map((i) => `'${i.id.replace(/'/g, "''")}'`).join(', ');
1202
+ // Affected processes: which execution flows are broken and at which step
1203
+ const [processRows, moduleRows, directModuleRows] = await Promise.all([
1204
+ executeQuery(repo.id, `
1205
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
1206
+ WHERE s.id IN [${allIds}]
1207
+ RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
1208
+ ORDER BY hits DESC
1209
+ LIMIT 20
1210
+ `).catch(() => []),
1211
+ executeQuery(repo.id, `
1212
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1213
+ WHERE s.id IN [${allIds}]
1214
+ RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
1215
+ ORDER BY hits DESC
1216
+ LIMIT 20
1217
+ `).catch(() => []),
1218
+ d1Ids ? executeQuery(repo.id, `
1219
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1220
+ WHERE s.id IN [${d1Ids}]
1221
+ RETURN DISTINCT c.heuristicLabel AS name
1222
+ `).catch(() => []) : Promise.resolve([]),
1223
+ ]);
1224
+ affectedProcesses = processRows.map((r) => ({
1225
+ name: r.name || r[0],
1226
+ hits: r.hits || r[1],
1227
+ broken_at_step: r.minStep ?? r[2],
1228
+ step_count: r.stepCount ?? r[3],
1229
+ }));
1230
+ const directModuleSet = new Set(directModuleRows.map((r) => r.name || r[0]));
1231
+ affectedModules = moduleRows.map((r) => {
1232
+ const name = r.name || r[0];
1233
+ return {
1234
+ name,
1235
+ hits: r.hits || r[1],
1236
+ impact: directModuleSet.has(name) ? 'direct' : 'indirect',
1237
+ };
1238
+ });
1239
+ }
1240
+ // Risk scoring
1241
+ const processCount = affectedProcesses.length;
1242
+ const moduleCount = affectedModules.length;
1243
+ let risk = 'LOW';
1244
+ if (directCount >= 30 || processCount >= 5 || moduleCount >= 5 || impacted.length >= 200) {
1245
+ risk = 'CRITICAL';
1246
+ }
1247
+ else if (directCount >= 15 || processCount >= 3 || moduleCount >= 3 || impacted.length >= 100) {
1248
+ risk = 'HIGH';
1249
+ }
1250
+ else if (directCount >= 5 || impacted.length >= 30) {
1251
+ risk = 'MEDIUM';
1252
+ }
1154
1253
  return {
1155
1254
  target: {
1156
1255
  id: symId,
@@ -1160,6 +1259,14 @@ export class LocalBackend {
1160
1259
  },
1161
1260
  direction,
1162
1261
  impactedCount: impacted.length,
1262
+ risk,
1263
+ summary: {
1264
+ direct: directCount,
1265
+ processes_affected: processCount,
1266
+ modules_affected: moduleCount,
1267
+ },
1268
+ affected_processes: affectedProcesses,
1269
+ affected_modules: affectedModules,
1163
1270
  byDepth: grouped,
1164
1271
  };
1165
1272
  }
@@ -1173,11 +1280,11 @@ export class LocalBackend {
1173
1280
  await this.ensureInitialized(repo.id);
1174
1281
  try {
1175
1282
  const rawLimit = Math.max(limit * 5, 200);
1176
- const clusters = await executeQuery(repo.id, `
1177
- MATCH (c:Community)
1178
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1179
- ORDER BY c.symbolCount DESC
1180
- LIMIT ${rawLimit}
1283
+ const clusters = await executeQuery(repo.id, `
1284
+ MATCH (c:Community)
1285
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1286
+ ORDER BY c.symbolCount DESC
1287
+ LIMIT ${rawLimit}
1181
1288
  `);
1182
1289
  const rawClusters = clusters.map((c) => ({
1183
1290
  id: c.id || c[0],
@@ -1200,11 +1307,11 @@ export class LocalBackend {
1200
1307
  const repo = await this.resolveRepo(repoName);
1201
1308
  await this.ensureInitialized(repo.id);
1202
1309
  try {
1203
- const processes = await executeQuery(repo.id, `
1204
- MATCH (p:Process)
1205
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1206
- ORDER BY p.stepCount DESC
1207
- LIMIT ${limit}
1310
+ const processes = await executeQuery(repo.id, `
1311
+ MATCH (p:Process)
1312
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1313
+ ORDER BY p.stepCount DESC
1314
+ LIMIT ${limit}
1208
1315
  `);
1209
1316
  return {
1210
1317
  processes: processes.map((p) => ({
@@ -1228,10 +1335,10 @@ export class LocalBackend {
1228
1335
  const repo = await this.resolveRepo(repoName);
1229
1336
  await this.ensureInitialized(repo.id);
1230
1337
  const escaped = name.replace(/'/g, "''");
1231
- const clusterQuery = `
1232
- MATCH (c:Community)
1233
- WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
1234
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1338
+ const clusterQuery = `
1339
+ MATCH (c:Community)
1340
+ WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
1341
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1235
1342
  `;
1236
1343
  const clusters = await executeQuery(repo.id, clusterQuery);
1237
1344
  if (clusters.length === 0)
@@ -1246,11 +1353,11 @@ export class LocalBackend {
1246
1353
  totalSymbols += s;
1247
1354
  weightedCohesion += (c.cohesion || 0) * s;
1248
1355
  }
1249
- const members = await executeQuery(repo.id, `
1250
- MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1251
- WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
1252
- RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1253
- LIMIT 30
1356
+ const members = await executeQuery(repo.id, `
1357
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1358
+ WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
1359
+ RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1360
+ LIMIT 30
1254
1361
  `);
1255
1362
  return {
1256
1363
  cluster: {
@@ -1274,20 +1381,20 @@ export class LocalBackend {
1274
1381
  const repo = await this.resolveRepo(repoName);
1275
1382
  await this.ensureInitialized(repo.id);
1276
1383
  const escaped = name.replace(/'/g, "''");
1277
- const processes = await executeQuery(repo.id, `
1278
- MATCH (p:Process)
1279
- WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
1280
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1281
- LIMIT 1
1384
+ const processes = await executeQuery(repo.id, `
1385
+ MATCH (p:Process)
1386
+ WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
1387
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1388
+ LIMIT 1
1282
1389
  `);
1283
1390
  if (processes.length === 0)
1284
1391
  return { error: `Process '${name}' not found` };
1285
1392
  const proc = processes[0];
1286
1393
  const procId = proc.id || proc[0];
1287
- const steps = await executeQuery(repo.id, `
1288
- MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
1289
- RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
1290
- ORDER BY r.step
1394
+ const steps = await executeQuery(repo.id, `
1395
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
1396
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
1397
+ ORDER BY r.step
1291
1398
  `);
1292
1399
  return {
1293
1400
  process: {
@@ -256,48 +256,48 @@ async function getProcessesResource(backend, repoName) {
256
256
  * Schema resource — graph structure for Cypher queries
257
257
  */
258
258
  function getSchemaResource() {
259
- return `# GitNexus Graph Schema
260
-
261
- nodes:
262
- - File: Source code files
263
- - Folder: Directory containers
264
- - Function: Functions and arrow functions
265
- - Class: Class definitions
266
- - Interface: Interface/type definitions
267
- - Method: Class methods
268
- - CodeElement: Catch-all for other code elements
269
- - Community: Auto-detected functional area (Leiden algorithm)
270
- - Process: Execution flow trace
271
-
272
- additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
273
-
274
- relationships:
275
- - CONTAINS: File/Folder contains child
276
- - DEFINES: File defines a symbol
277
- - CALLS: Function/method invocation
278
- - IMPORTS: Module imports
279
- - EXTENDS: Class inheritance
280
- - IMPLEMENTS: Interface implementation
281
- - MEMBER_OF: Symbol belongs to community
282
- - STEP_IN_PROCESS: Symbol is step N in process
283
-
284
- relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
285
-
286
- example_queries:
287
- find_callers: |
288
- MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
289
- RETURN caller.name, caller.filePath
290
-
291
- find_community_members: |
292
- MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
293
- WHERE c.heuristicLabel = "Auth"
294
- RETURN s.name, labels(s)[0] AS type
295
-
296
- trace_process: |
297
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
298
- WHERE p.heuristicLabel = "LoginFlow"
299
- RETURN s.name, r.step
300
- ORDER BY r.step
259
+ return `# GitNexus Graph Schema
260
+
261
+ nodes:
262
+ - File: Source code files
263
+ - Folder: Directory containers
264
+ - Function: Functions and arrow functions
265
+ - Class: Class definitions
266
+ - Interface: Interface/type definitions
267
+ - Method: Class methods
268
+ - CodeElement: Catch-all for other code elements
269
+ - Community: Auto-detected functional area (Leiden algorithm)
270
+ - Process: Execution flow trace
271
+
272
+ additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
273
+
274
+ relationships:
275
+ - CONTAINS: File/Folder contains child
276
+ - DEFINES: File defines a symbol
277
+ - CALLS: Function/method invocation
278
+ - IMPORTS: Module imports
279
+ - EXTENDS: Class inheritance
280
+ - IMPLEMENTS: Interface implementation
281
+ - MEMBER_OF: Symbol belongs to community
282
+ - STEP_IN_PROCESS: Symbol is step N in process
283
+
284
+ relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
285
+
286
+ example_queries:
287
+ find_callers: |
288
+ MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
289
+ RETURN caller.name, caller.filePath
290
+
291
+ find_community_members: |
292
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
293
+ WHERE c.heuristicLabel = "Auth"
294
+ RETURN s.name, labels(s)[0] AS type
295
+
296
+ trace_process: |
297
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
298
+ WHERE p.heuristicLabel = "LoginFlow"
299
+ RETURN s.name, r.step
300
+ ORDER BY r.step
301
301
  `;
302
302
  }
303
303
  /**