gitnexus 1.3.11 → 1.4.1
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 -194
- package/dist/cli/ai-context.js +87 -105
- package/dist/cli/analyze.js +0 -8
- package/dist/cli/index.js +25 -15
- package/dist/cli/setup.js +19 -17
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/ingestion/ast-cache.js +2 -3
- package/dist/core/ingestion/call-processor.js +5 -7
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/pipeline.js +2 -23
- package/dist/core/ingestion/tree-sitter-queries.js +484 -484
- package/dist/core/ingestion/utils.js +5 -1
- package/dist/core/ingestion/workers/worker-pool.js +0 -8
- package/dist/core/kuzu/kuzu-adapter.js +19 -11
- package/dist/core/kuzu/schema.js +287 -287
- package/dist/core/search/bm25-index.js +6 -7
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/diagrams.d.ts +27 -0
- package/dist/core/wiki/diagrams.js +163 -0
- package/dist/core/wiki/generator.d.ts +50 -2
- package/dist/core/wiki/generator.js +548 -49
- package/dist/core/wiki/graph-queries.d.ts +42 -0
- package/dist/core/wiki/graph-queries.js +276 -97
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +73 -11
- package/dist/core/wiki/prompts.d.ts +52 -8
- package/dist/core/wiki/prompts.js +200 -86
- package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
- package/dist/mcp/core/kuzu-adapter.js +44 -13
- package/dist/mcp/local/local-backend.js +128 -128
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/tools.js +103 -93
- package/hooks/claude/gitnexus-hook.cjs +155 -238
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +96 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/cli/lazy-action.d.ts +0 -6
- package/dist/cli/lazy-action.js +0 -18
- package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
- package/dist/mcp/compatible-stdio-transport.js +0 -200
|
@@ -29,6 +29,30 @@ export interface ProcessInfo {
|
|
|
29
29
|
type: string;
|
|
30
30
|
}>;
|
|
31
31
|
}
|
|
32
|
+
export interface CommunityFileGroup {
|
|
33
|
+
communityId: string;
|
|
34
|
+
label: string;
|
|
35
|
+
keywords: string[];
|
|
36
|
+
description: string;
|
|
37
|
+
cohesion: number;
|
|
38
|
+
symbolCount: number;
|
|
39
|
+
files: string[];
|
|
40
|
+
secondaryFiles: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface InterCommunityEdge {
|
|
43
|
+
fromLabel: string;
|
|
44
|
+
toLabel: string;
|
|
45
|
+
callCount: number;
|
|
46
|
+
sampleCalls: Array<{
|
|
47
|
+
fromName: string;
|
|
48
|
+
toName: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
export interface CrossCommunityProcess {
|
|
52
|
+
label: string;
|
|
53
|
+
communities: string[];
|
|
54
|
+
stepCount: number;
|
|
55
|
+
}
|
|
32
56
|
/**
|
|
33
57
|
* Initialize the KuzuDB connection for wiki generation.
|
|
34
58
|
*/
|
|
@@ -60,6 +84,11 @@ export declare function getInterModuleCallEdges(filePaths: string[]): Promise<{
|
|
|
60
84
|
outgoing: CallEdge[];
|
|
61
85
|
incoming: CallEdge[];
|
|
62
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Get files that are call-graph neighbors of the given files but not in the set.
|
|
89
|
+
* Used to assign new files to existing modules during incremental updates.
|
|
90
|
+
*/
|
|
91
|
+
export declare function getCallGraphNeighborFiles(filePaths: string[]): Promise<string[]>;
|
|
63
92
|
/**
|
|
64
93
|
* Get processes (execution flows) that pass through a set of files.
|
|
65
94
|
* Returns top N by step count.
|
|
@@ -78,3 +107,16 @@ export declare function getInterModuleEdgesForOverview(moduleFiles: Record<strin
|
|
|
78
107
|
to: string;
|
|
79
108
|
count: number;
|
|
80
109
|
}>>;
|
|
110
|
+
/**
|
|
111
|
+
* Get community-to-file mapping from Leiden-detected clusters.
|
|
112
|
+
* Each file is assigned to its majority community; secondary memberships tracked separately.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getCommunityFileMapping(): Promise<CommunityFileGroup[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Get call edges between communities for inter-module coupling analysis.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getInterCommunityCallEdges(): Promise<InterCommunityEdge[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Get cross-community execution flows.
|
|
121
|
+
*/
|
|
122
|
+
export declare function getCrossCommunityProcesses(): Promise<CrossCommunityProcess[]>;
|
|
@@ -10,7 +10,7 @@ const REPO_ID = '__wiki__';
|
|
|
10
10
|
* Initialize the KuzuDB connection for wiki generation.
|
|
11
11
|
*/
|
|
12
12
|
export async function initWikiDb(kuzuPath) {
|
|
13
|
-
await initKuzu(REPO_ID, kuzuPath);
|
|
13
|
+
await initKuzu(REPO_ID, kuzuPath, { pinned: true });
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Close the KuzuDB connection.
|
|
@@ -22,11 +22,11 @@ export async function closeWikiDb() {
|
|
|
22
22
|
* Get all source files with their exported symbol names and types.
|
|
23
23
|
*/
|
|
24
24
|
export async function getFilesWithExports() {
|
|
25
|
-
const rows = await executeQuery(REPO_ID, `
|
|
26
|
-
MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(n)
|
|
27
|
-
WHERE n.isExported = true
|
|
28
|
-
RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type
|
|
29
|
-
ORDER BY f.filePath
|
|
25
|
+
const rows = await executeQuery(REPO_ID, `
|
|
26
|
+
MATCH (f:File)-[:CodeRelation {type: 'DEFINES'}]->(n)
|
|
27
|
+
WHERE n.isExported = true
|
|
28
|
+
RETURN f.filePath AS filePath, n.name AS name, labels(n)[0] AS type
|
|
29
|
+
ORDER BY f.filePath
|
|
30
30
|
`);
|
|
31
31
|
const fileMap = new Map();
|
|
32
32
|
for (const row of rows) {
|
|
@@ -46,10 +46,10 @@ export async function getFilesWithExports() {
|
|
|
46
46
|
* Get all files tracked in the graph (including those with no exports).
|
|
47
47
|
*/
|
|
48
48
|
export async function getAllFiles() {
|
|
49
|
-
const rows = await executeQuery(REPO_ID, `
|
|
50
|
-
MATCH (f:File)
|
|
51
|
-
RETURN f.filePath AS filePath
|
|
52
|
-
ORDER BY f.filePath
|
|
49
|
+
const rows = await executeQuery(REPO_ID, `
|
|
50
|
+
MATCH (f:File)
|
|
51
|
+
RETURN f.filePath AS filePath
|
|
52
|
+
ORDER BY f.filePath
|
|
53
53
|
`);
|
|
54
54
|
return rows.map(r => r.filePath || r[0]);
|
|
55
55
|
}
|
|
@@ -57,11 +57,11 @@ export async function getAllFiles() {
|
|
|
57
57
|
* Get inter-file call edges (calls between different files).
|
|
58
58
|
*/
|
|
59
59
|
export async function getInterFileCallEdges() {
|
|
60
|
-
const rows = await executeQuery(REPO_ID, `
|
|
61
|
-
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
62
|
-
WHERE a.filePath <> b.filePath
|
|
63
|
-
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
64
|
-
b.filePath AS toFile, b.name AS toName
|
|
60
|
+
const rows = await executeQuery(REPO_ID, `
|
|
61
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
62
|
+
WHERE a.filePath <> b.filePath
|
|
63
|
+
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
64
|
+
b.filePath AS toFile, b.name AS toName
|
|
65
65
|
`);
|
|
66
66
|
return rows.map(r => ({
|
|
67
67
|
fromFile: r.fromFile || r[0],
|
|
@@ -77,11 +77,11 @@ export async function getIntraModuleCallEdges(filePaths) {
|
|
|
77
77
|
if (filePaths.length === 0)
|
|
78
78
|
return [];
|
|
79
79
|
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
80
|
-
const rows = await executeQuery(REPO_ID, `
|
|
81
|
-
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
82
|
-
WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
|
|
83
|
-
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
84
|
-
b.filePath AS toFile, b.name AS toName
|
|
80
|
+
const rows = await executeQuery(REPO_ID, `
|
|
81
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
82
|
+
WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
|
|
83
|
+
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
84
|
+
b.filePath AS toFile, b.name AS toName
|
|
85
85
|
`);
|
|
86
86
|
return rows.map(r => ({
|
|
87
87
|
fromFile: r.fromFile || r[0],
|
|
@@ -97,19 +97,19 @@ export async function getInterModuleCallEdges(filePaths) {
|
|
|
97
97
|
if (filePaths.length === 0)
|
|
98
98
|
return { outgoing: [], incoming: [] };
|
|
99
99
|
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
100
|
-
const outRows = await executeQuery(REPO_ID, `
|
|
101
|
-
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
102
|
-
WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
|
|
103
|
-
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
104
|
-
b.filePath AS toFile, b.name AS toName
|
|
105
|
-
LIMIT 30
|
|
100
|
+
const outRows = await executeQuery(REPO_ID, `
|
|
101
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
102
|
+
WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
|
|
103
|
+
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
104
|
+
b.filePath AS toFile, b.name AS toName
|
|
105
|
+
LIMIT 30
|
|
106
106
|
`);
|
|
107
|
-
const inRows = await executeQuery(REPO_ID, `
|
|
108
|
-
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
109
|
-
WHERE NOT a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
|
|
110
|
-
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
111
|
-
b.filePath AS toFile, b.name AS toName
|
|
112
|
-
LIMIT 30
|
|
107
|
+
const inRows = await executeQuery(REPO_ID, `
|
|
108
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
109
|
+
WHERE NOT a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
|
|
110
|
+
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
111
|
+
b.filePath AS toFile, b.name AS toName
|
|
112
|
+
LIMIT 30
|
|
113
113
|
`);
|
|
114
114
|
return {
|
|
115
115
|
outgoing: outRows.map(r => ({
|
|
@@ -126,6 +126,50 @@ export async function getInterModuleCallEdges(filePaths) {
|
|
|
126
126
|
})),
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Get files that are call-graph neighbors of the given files but not in the set.
|
|
131
|
+
* Used to assign new files to existing modules during incremental updates.
|
|
132
|
+
*/
|
|
133
|
+
export async function getCallGraphNeighborFiles(filePaths) {
|
|
134
|
+
if (filePaths.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
137
|
+
const rows = await executeQuery(REPO_ID, `
|
|
138
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]-(b)
|
|
139
|
+
WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
|
|
140
|
+
RETURN DISTINCT b.filePath AS neighborFile
|
|
141
|
+
`);
|
|
142
|
+
return rows.map(r => r.neighborFile || r[0]);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Shared helper: group step rows by process ID.
|
|
146
|
+
*/
|
|
147
|
+
function groupStepsByProcess(procRows, stepRows) {
|
|
148
|
+
const stepsByProc = new Map();
|
|
149
|
+
for (const s of stepRows) {
|
|
150
|
+
const procId = s.processId || s[4];
|
|
151
|
+
if (!stepsByProc.has(procId))
|
|
152
|
+
stepsByProc.set(procId, []);
|
|
153
|
+
stepsByProc.get(procId).push({
|
|
154
|
+
step: s.step || s[3] || 0,
|
|
155
|
+
name: s.name || s[0],
|
|
156
|
+
filePath: s.filePath || s[1],
|
|
157
|
+
type: s.type || s[2],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return procRows.map(row => {
|
|
161
|
+
const procId = row.id || row[0];
|
|
162
|
+
const steps = stepsByProc.get(procId) || [];
|
|
163
|
+
steps.sort((a, b) => a.step - b.step);
|
|
164
|
+
return {
|
|
165
|
+
id: procId,
|
|
166
|
+
label: row.label || row[1] || procId,
|
|
167
|
+
type: row.type || row[2] || 'unknown',
|
|
168
|
+
stepCount: row.stepCount || row[3] || 0,
|
|
169
|
+
steps,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
129
173
|
/**
|
|
130
174
|
* Get processes (execution flows) that pass through a set of files.
|
|
131
175
|
* Returns top N by step count.
|
|
@@ -134,78 +178,57 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
|
|
|
134
178
|
if (filePaths.length === 0)
|
|
135
179
|
return [];
|
|
136
180
|
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
137
|
-
// Find processes that have steps in the given files
|
|
138
|
-
const procRows = await executeQuery(REPO_ID, `
|
|
139
|
-
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
140
|
-
WHERE s.filePath IN [${fileList}]
|
|
141
|
-
RETURN DISTINCT p.id AS id, p.heuristicLabel AS label,
|
|
142
|
-
p.processType AS type, p.stepCount AS stepCount
|
|
143
|
-
ORDER BY stepCount DESC
|
|
144
|
-
LIMIT ${limit}
|
|
181
|
+
// Query 1: Find processes that have steps in the given files
|
|
182
|
+
const procRows = await executeQuery(REPO_ID, `
|
|
183
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
184
|
+
WHERE s.filePath IN [${fileList}]
|
|
185
|
+
RETURN DISTINCT p.id AS id, p.heuristicLabel AS label,
|
|
186
|
+
p.processType AS type, p.stepCount AS stepCount
|
|
187
|
+
ORDER BY stepCount DESC
|
|
188
|
+
LIMIT ${limit}
|
|
145
189
|
`);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
type,
|
|
162
|
-
stepCount,
|
|
163
|
-
steps: stepRows.map(s => ({
|
|
164
|
-
step: s.step || s[3] || 0,
|
|
165
|
-
name: s.name || s[0],
|
|
166
|
-
filePath: s.filePath || s[1],
|
|
167
|
-
type: s.type || s[2],
|
|
168
|
-
})),
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return processes;
|
|
190
|
+
if (procRows.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
// Query 2: Fetch ALL steps for ALL matched processes in one query
|
|
193
|
+
const procIdList = procRows.map(r => {
|
|
194
|
+
const id = r.id || r[0];
|
|
195
|
+
return `'${id.replace(/'/g, "''")}'`;
|
|
196
|
+
}).join(', ');
|
|
197
|
+
const stepRows = await executeQuery(REPO_ID, `
|
|
198
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
199
|
+
WHERE p.id IN [${procIdList}]
|
|
200
|
+
RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type,
|
|
201
|
+
r.step AS step, p.id AS processId
|
|
202
|
+
ORDER BY p.id, r.step
|
|
203
|
+
`);
|
|
204
|
+
return groupStepsByProcess(procRows, stepRows);
|
|
172
205
|
}
|
|
173
206
|
/**
|
|
174
207
|
* Get all processes in the graph (for overview page).
|
|
175
208
|
*/
|
|
176
209
|
export async function getAllProcesses(limit = 20) {
|
|
177
|
-
const procRows = await executeQuery(REPO_ID, `
|
|
178
|
-
MATCH (p:Process)
|
|
179
|
-
RETURN p.id AS id, p.heuristicLabel AS label,
|
|
180
|
-
p.processType AS type, p.stepCount AS stepCount
|
|
181
|
-
ORDER BY stepCount DESC
|
|
182
|
-
LIMIT ${limit}
|
|
210
|
+
const procRows = await executeQuery(REPO_ID, `
|
|
211
|
+
MATCH (p:Process)
|
|
212
|
+
RETURN p.id AS id, p.heuristicLabel AS label,
|
|
213
|
+
p.processType AS type, p.stepCount AS stepCount
|
|
214
|
+
ORDER BY stepCount DESC
|
|
215
|
+
LIMIT ${limit}
|
|
183
216
|
`);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
stepCount,
|
|
200
|
-
steps: stepRows.map(s => ({
|
|
201
|
-
step: s.step || s[3] || 0,
|
|
202
|
-
name: s.name || s[0],
|
|
203
|
-
filePath: s.filePath || s[1],
|
|
204
|
-
type: s.type || s[2],
|
|
205
|
-
})),
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
return processes;
|
|
217
|
+
if (procRows.length === 0)
|
|
218
|
+
return [];
|
|
219
|
+
// Batch fetch all steps for all matched processes
|
|
220
|
+
const procIdList = procRows.map(r => {
|
|
221
|
+
const id = r.id || r[0];
|
|
222
|
+
return `'${id.replace(/'/g, "''")}'`;
|
|
223
|
+
}).join(', ');
|
|
224
|
+
const stepRows = await executeQuery(REPO_ID, `
|
|
225
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
226
|
+
WHERE p.id IN [${procIdList}]
|
|
227
|
+
RETURN s.name AS name, s.filePath AS filePath, labels(s)[0] AS type,
|
|
228
|
+
r.step AS step, p.id AS processId
|
|
229
|
+
ORDER BY p.id, r.step
|
|
230
|
+
`);
|
|
231
|
+
return groupStepsByProcess(procRows, stepRows);
|
|
209
232
|
}
|
|
210
233
|
/**
|
|
211
234
|
* Get inter-module edges for overview architecture diagram.
|
|
@@ -236,3 +259,159 @@ export async function getInterModuleEdgesForOverview(moduleFiles) {
|
|
|
236
259
|
})
|
|
237
260
|
.sort((a, b) => b.count - a.count);
|
|
238
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Get community-to-file mapping from Leiden-detected clusters.
|
|
264
|
+
* Each file is assigned to its majority community; secondary memberships tracked separately.
|
|
265
|
+
*/
|
|
266
|
+
export async function getCommunityFileMapping() {
|
|
267
|
+
// Query 1: Fetch all Community nodes
|
|
268
|
+
const communityRows = await executeQuery(REPO_ID, `
|
|
269
|
+
MATCH (c:Community)
|
|
270
|
+
RETURN c.id AS id, c.heuristicLabel AS label, c.keywords AS keywords,
|
|
271
|
+
c.description AS description, c.cohesion AS cohesion,
|
|
272
|
+
c.symbolCount AS symbolCount
|
|
273
|
+
ORDER BY c.symbolCount DESC
|
|
274
|
+
`);
|
|
275
|
+
if (communityRows.length === 0)
|
|
276
|
+
return [];
|
|
277
|
+
// Query 2: Fetch all MEMBER_OF edges with file paths
|
|
278
|
+
const memberRows = await executeQuery(REPO_ID, `
|
|
279
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
280
|
+
WHERE n.filePath IS NOT NULL
|
|
281
|
+
RETURN n.filePath AS filePath, c.id AS communityId
|
|
282
|
+
`);
|
|
283
|
+
// Build file -> community count map
|
|
284
|
+
const fileCommunityCount = new Map();
|
|
285
|
+
for (const row of memberRows) {
|
|
286
|
+
const fp = row.filePath || row[0];
|
|
287
|
+
const cId = row.communityId || row[1];
|
|
288
|
+
if (!fileCommunityCount.has(fp))
|
|
289
|
+
fileCommunityCount.set(fp, new Map());
|
|
290
|
+
const counts = fileCommunityCount.get(fp);
|
|
291
|
+
counts.set(cId, (counts.get(cId) || 0) + 1);
|
|
292
|
+
}
|
|
293
|
+
// Assign each file to majority community
|
|
294
|
+
const communityFiles = new Map();
|
|
295
|
+
for (const row of communityRows) {
|
|
296
|
+
const cId = row.id || row[0];
|
|
297
|
+
communityFiles.set(cId, { primary: [], secondary: [] });
|
|
298
|
+
}
|
|
299
|
+
for (const [fp, counts] of fileCommunityCount) {
|
|
300
|
+
let maxCount = 0;
|
|
301
|
+
let maxCommunity = '';
|
|
302
|
+
for (const [cId, count] of counts) {
|
|
303
|
+
if (count > maxCount) {
|
|
304
|
+
maxCount = count;
|
|
305
|
+
maxCommunity = cId;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (maxCommunity && communityFiles.has(maxCommunity)) {
|
|
309
|
+
communityFiles.get(maxCommunity).primary.push(fp);
|
|
310
|
+
}
|
|
311
|
+
// Track secondary memberships
|
|
312
|
+
for (const [cId] of counts) {
|
|
313
|
+
if (cId !== maxCommunity && communityFiles.has(cId)) {
|
|
314
|
+
communityFiles.get(cId).secondary.push(fp);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Build result, merge tiny communities into "Other"
|
|
319
|
+
const results = [];
|
|
320
|
+
const otherFiles = [];
|
|
321
|
+
const otherSecondary = [];
|
|
322
|
+
for (const row of communityRows) {
|
|
323
|
+
const cId = row.id || row[0];
|
|
324
|
+
const entry = communityFiles.get(cId);
|
|
325
|
+
if (!entry)
|
|
326
|
+
continue;
|
|
327
|
+
if (entry.primary.length < 2) {
|
|
328
|
+
otherFiles.push(...entry.primary);
|
|
329
|
+
otherSecondary.push(...entry.secondary);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const keywords = row.keywords || row[2];
|
|
333
|
+
results.push({
|
|
334
|
+
communityId: cId,
|
|
335
|
+
label: row.label || row[1] || cId,
|
|
336
|
+
keywords: Array.isArray(keywords) ? keywords : typeof keywords === 'string' ? keywords.split(',').map((k) => k.trim()) : [],
|
|
337
|
+
description: row.description || row[3] || '',
|
|
338
|
+
cohesion: row.cohesion || row[4] || 0,
|
|
339
|
+
symbolCount: row.symbolCount || row[5] || 0,
|
|
340
|
+
files: entry.primary,
|
|
341
|
+
secondaryFiles: entry.secondary,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Add "Other" bucket if non-empty
|
|
345
|
+
if (otherFiles.length > 0) {
|
|
346
|
+
results.push({
|
|
347
|
+
communityId: '__other__',
|
|
348
|
+
label: 'Other small clusters',
|
|
349
|
+
keywords: [],
|
|
350
|
+
description: 'Files from small communities merged together',
|
|
351
|
+
cohesion: 0,
|
|
352
|
+
symbolCount: 0,
|
|
353
|
+
files: otherFiles,
|
|
354
|
+
secondaryFiles: otherSecondary,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// Cap at 30 communities
|
|
358
|
+
return results.slice(0, 30);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get call edges between communities for inter-module coupling analysis.
|
|
362
|
+
*/
|
|
363
|
+
export async function getInterCommunityCallEdges() {
|
|
364
|
+
const rows = await executeQuery(REPO_ID, `
|
|
365
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b),
|
|
366
|
+
(a)-[:CodeRelation {type: 'MEMBER_OF'}]->(ca:Community),
|
|
367
|
+
(b)-[:CodeRelation {type: 'MEMBER_OF'}]->(cb:Community)
|
|
368
|
+
WHERE ca.id <> cb.id
|
|
369
|
+
RETURN ca.heuristicLabel AS fromLabel, cb.heuristicLabel AS toLabel,
|
|
370
|
+
a.name AS fromName, b.name AS toName
|
|
371
|
+
`);
|
|
372
|
+
// Aggregate in JS
|
|
373
|
+
const edgeMap = new Map();
|
|
374
|
+
for (const row of rows) {
|
|
375
|
+
const from = row.fromLabel || row[0];
|
|
376
|
+
const to = row.toLabel || row[1];
|
|
377
|
+
const key = `${from}|||${to}`;
|
|
378
|
+
if (!edgeMap.has(key))
|
|
379
|
+
edgeMap.set(key, { count: 0, samples: [] });
|
|
380
|
+
const entry = edgeMap.get(key);
|
|
381
|
+
entry.count++;
|
|
382
|
+
if (entry.samples.length < 3) {
|
|
383
|
+
entry.samples.push({
|
|
384
|
+
fromName: row.fromName || row[2],
|
|
385
|
+
toName: row.toName || row[3],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const results = [];
|
|
390
|
+
for (const [key, val] of edgeMap) {
|
|
391
|
+
const [fromLabel, toLabel] = key.split('|||');
|
|
392
|
+
results.push({ fromLabel, toLabel, callCount: val.count, sampleCalls: val.samples });
|
|
393
|
+
}
|
|
394
|
+
return results
|
|
395
|
+
.sort((a, b) => b.callCount - a.callCount)
|
|
396
|
+
.slice(0, 40);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get cross-community execution flows.
|
|
400
|
+
*/
|
|
401
|
+
export async function getCrossCommunityProcesses() {
|
|
402
|
+
const rows = await executeQuery(REPO_ID, `
|
|
403
|
+
MATCH (p:Process)
|
|
404
|
+
WHERE p.processType = 'cross_community'
|
|
405
|
+
RETURN p.heuristicLabel AS label, p.communities AS communities, p.stepCount AS stepCount
|
|
406
|
+
ORDER BY p.stepCount DESC
|
|
407
|
+
LIMIT 15
|
|
408
|
+
`);
|
|
409
|
+
return rows.map(r => {
|
|
410
|
+
const communities = r.communities || r[1];
|
|
411
|
+
return {
|
|
412
|
+
label: r.label || r[0] || 'Unknown',
|
|
413
|
+
communities: Array.isArray(communities) ? communities : typeof communities === 'string' ? communities.split(',').map((c) => c.trim()) : [],
|
|
414
|
+
stepCount: r.stepCount || r[2] || 0,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|