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.
- package/README.md +194 -186
- package/dist/cli/ai-context.js +71 -71
- package/dist/cli/analyze.js +1 -1
- package/dist/cli/setup.js +8 -1
- package/dist/cli/view.d.ts +13 -0
- package/dist/cli/view.js +59 -0
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/html-graph-viewer.d.ts +15 -0
- package/dist/core/graph/html-graph-viewer.js +542 -0
- package/dist/core/graph/html-graph-viewer.test.d.ts +1 -0
- package/dist/core/graph/html-graph-viewer.test.js +67 -0
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/kuzu/kuzu-adapter.js +9 -9
- package/dist/core/kuzu/schema.js +256 -256
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/graph-queries.js +52 -52
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/prompts.js +82 -82
- package/dist/mcp/core/embedder.js +8 -4
- package/dist/mcp/local/local-backend.d.ts +6 -0
- package/dist/mcp/local/local-backend.js +224 -117
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +16 -16
- package/dist/mcp/tools.js +86 -77
- package/dist/server/api.d.ts +4 -2
- package/dist/server/api.js +253 -83
- package/hooks/claude/gitnexus-hook.cjs +135 -135
- package/hooks/claude/pre-tool-use.sh +78 -78
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +82 -82
- package/skills/debugging.md +85 -85
- package/skills/exploring.md +75 -75
- package/skills/impact-analysis.md +94 -94
- package/skills/refactoring.md +113 -113
- package/vendor/leiden/index.cjs +355 -355
- 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
|
-
|
|
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
|
|
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
|
|
548
|
-
|
|
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: {
|
package/dist/mcp/resources.js
CHANGED
|
@@ -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
|
/**
|