gitnexus 1.1.0 → 1.1.2
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 +196 -198
- package/dist/cli/ai-context.js +89 -89
- package/dist/cli/index.js +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/core/ingestion/pipeline.js +4 -1
- package/dist/core/ingestion/process-processor.js +27 -3
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/mcp/local/local-backend.d.ts +6 -24
- package/dist/mcp/local/local-backend.js +135 -124
- package/dist/mcp/resources.d.ts +1 -2
- package/dist/mcp/resources.js +61 -85
- package/dist/mcp/tools.js +82 -82
- package/package.json +80 -80
- package/skills/debugging.md +106 -106
- package/skills/exploring.md +126 -126
- package/skills/impact-analysis.md +117 -117
- package/skills/refactoring.md +120 -120
|
@@ -15,7 +15,7 @@ const DEFAULT_CONFIG = {
|
|
|
15
15
|
maxTraceDepth: 10,
|
|
16
16
|
maxBranching: 4,
|
|
17
17
|
maxProcesses: 75,
|
|
18
|
-
minSteps: 2
|
|
18
|
+
minSteps: 3, // 3+ steps = genuine multi-hop flow (2-step is just "A calls B")
|
|
19
19
|
};
|
|
20
20
|
// ============================================================================
|
|
21
21
|
// MAIN PROCESSOR
|
|
@@ -51,10 +51,13 @@ export const processProcesses = async (knowledgeGraph, memberships, onProgress,
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
onProgress?.(`Found ${allTraces.length} traces, deduplicating...`, 60);
|
|
54
|
-
// Step 3: Deduplicate similar traces
|
|
54
|
+
// Step 3: Deduplicate similar traces (subset removal)
|
|
55
55
|
const uniqueTraces = deduplicateTraces(allTraces);
|
|
56
|
+
// Step 3b: Deduplicate by entry+terminal pair (keep longest path per pair)
|
|
57
|
+
const endpointDeduped = deduplicateByEndpoints(uniqueTraces);
|
|
58
|
+
onProgress?.(`Deduped ${uniqueTraces.length} → ${endpointDeduped.length} unique endpoint pairs`, 70);
|
|
56
59
|
// Step 4: Limit to max processes (prioritize longer traces)
|
|
57
|
-
const limitedTraces =
|
|
60
|
+
const limitedTraces = endpointDeduped
|
|
58
61
|
.sort((a, b) => b.length - a.length)
|
|
59
62
|
.slice(0, cfg.maxProcesses);
|
|
60
63
|
onProgress?.(`Creating ${limitedTraces.length} process nodes...`, 80);
|
|
@@ -266,6 +269,27 @@ const deduplicateTraces = (traces) => {
|
|
|
266
269
|
return unique;
|
|
267
270
|
};
|
|
268
271
|
// ============================================================================
|
|
272
|
+
// HELPER: Deduplicate by entry+terminal endpoints
|
|
273
|
+
// ============================================================================
|
|
274
|
+
/**
|
|
275
|
+
* Keep only the longest trace per unique entry→terminal pair.
|
|
276
|
+
* Multiple paths between the same two endpoints are redundant for agents.
|
|
277
|
+
*/
|
|
278
|
+
const deduplicateByEndpoints = (traces) => {
|
|
279
|
+
if (traces.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const byEndpoints = new Map();
|
|
282
|
+
// Sort longest first so the first seen per key is the longest
|
|
283
|
+
const sorted = [...traces].sort((a, b) => b.length - a.length);
|
|
284
|
+
for (const trace of sorted) {
|
|
285
|
+
const key = `${trace[0]}::${trace[trace.length - 1]}`;
|
|
286
|
+
if (!byEndpoints.has(key)) {
|
|
287
|
+
byEndpoints.set(key, trace);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return Array.from(byEndpoints.values());
|
|
291
|
+
};
|
|
292
|
+
// ============================================================================
|
|
269
293
|
// HELPER: String utilities
|
|
270
294
|
// ============================================================================
|
|
271
295
|
const capitalize = (s) => {
|
|
@@ -11,11 +11,11 @@ import { queryFTS } from '../kuzu/kuzu-adapter.js';
|
|
|
11
11
|
*/
|
|
12
12
|
async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
|
|
13
13
|
const escapedQuery = query.replace(/'/g, "''");
|
|
14
|
-
const cypher = `
|
|
15
|
-
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := false)
|
|
16
|
-
RETURN node, score
|
|
17
|
-
ORDER BY score DESC
|
|
18
|
-
LIMIT ${limit}
|
|
14
|
+
const cypher = `
|
|
15
|
+
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := false)
|
|
16
|
+
RETURN node, score
|
|
17
|
+
ORDER BY score DESC
|
|
18
|
+
LIMIT ${limit}
|
|
19
19
|
`;
|
|
20
20
|
try {
|
|
21
21
|
const rows = await executor(cypher);
|
|
@@ -11,19 +11,9 @@ export interface CodebaseContext {
|
|
|
11
11
|
stats: {
|
|
12
12
|
fileCount: number;
|
|
13
13
|
functionCount: number;
|
|
14
|
-
classCount: number;
|
|
15
|
-
interfaceCount: number;
|
|
16
|
-
methodCount: number;
|
|
17
14
|
communityCount: number;
|
|
18
15
|
processCount: number;
|
|
19
16
|
};
|
|
20
|
-
hotspots: Array<{
|
|
21
|
-
name: string;
|
|
22
|
-
type: string;
|
|
23
|
-
filePath: string;
|
|
24
|
-
connections: number;
|
|
25
|
-
}>;
|
|
26
|
-
folderTree: string;
|
|
27
17
|
}
|
|
28
18
|
interface RepoHandle {
|
|
29
19
|
id: string;
|
|
@@ -57,24 +47,10 @@ export declare class LocalBackend {
|
|
|
57
47
|
*/
|
|
58
48
|
resolveRepo(repoParam?: string): RepoHandle;
|
|
59
49
|
private ensureInitialized;
|
|
60
|
-
get isReady(): boolean;
|
|
61
50
|
/**
|
|
62
51
|
* Get context for a specific repo (or the single repo if only one).
|
|
63
52
|
*/
|
|
64
53
|
getContext(repoId?: string): CodebaseContext | null;
|
|
65
|
-
/**
|
|
66
|
-
* Backwards-compatible getter — returns context of single repo or null.
|
|
67
|
-
*/
|
|
68
|
-
get context(): CodebaseContext | null;
|
|
69
|
-
getRepoPath(repoId?: string): string | null;
|
|
70
|
-
get repoPath(): string | null;
|
|
71
|
-
getMeta(repoId?: string): {
|
|
72
|
-
lastCommit: string;
|
|
73
|
-
indexedAt: string;
|
|
74
|
-
stats?: any;
|
|
75
|
-
} | null;
|
|
76
|
-
get meta(): any;
|
|
77
|
-
get storagePath(): string | null;
|
|
78
54
|
/**
|
|
79
55
|
* List all registered repos with their metadata.
|
|
80
56
|
*/
|
|
@@ -96,6 +72,12 @@ export declare class LocalBackend {
|
|
|
96
72
|
*/
|
|
97
73
|
private semanticSearch;
|
|
98
74
|
private cypher;
|
|
75
|
+
/**
|
|
76
|
+
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
|
77
|
+
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
|
78
|
+
* Raw communities stay intact in KuzuDB for Cypher queries.
|
|
79
|
+
*/
|
|
80
|
+
private aggregateClusters;
|
|
99
81
|
private overview;
|
|
100
82
|
private explore;
|
|
101
83
|
private impact;
|
|
@@ -57,14 +57,9 @@ export class LocalBackend {
|
|
|
57
57
|
stats: {
|
|
58
58
|
fileCount: s.files || 0,
|
|
59
59
|
functionCount: s.nodes || 0,
|
|
60
|
-
classCount: 0,
|
|
61
|
-
interfaceCount: 0,
|
|
62
|
-
methodCount: 0,
|
|
63
60
|
communityCount: s.communities || 0,
|
|
64
61
|
processCount: s.processes || 0,
|
|
65
62
|
},
|
|
66
|
-
hotspots: [],
|
|
67
|
-
folderTree: '',
|
|
68
63
|
});
|
|
69
64
|
}
|
|
70
65
|
return this.repos.size > 0;
|
|
@@ -137,9 +132,6 @@ export class LocalBackend {
|
|
|
137
132
|
this.initializedRepos.add(repoId);
|
|
138
133
|
}
|
|
139
134
|
// ─── Public Getters ──────────────────────────────────────────────
|
|
140
|
-
get isReady() {
|
|
141
|
-
return this.repos.size > 0;
|
|
142
|
-
}
|
|
143
135
|
/**
|
|
144
136
|
* Get context for a specific repo (or the single repo if only one).
|
|
145
137
|
*/
|
|
@@ -152,38 +144,6 @@ export class LocalBackend {
|
|
|
152
144
|
}
|
|
153
145
|
return null;
|
|
154
146
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Backwards-compatible getter — returns context of single repo or null.
|
|
157
|
-
*/
|
|
158
|
-
get context() {
|
|
159
|
-
return this.getContext();
|
|
160
|
-
}
|
|
161
|
-
getRepoPath(repoId) {
|
|
162
|
-
if (repoId)
|
|
163
|
-
return this.repos.get(repoId)?.repoPath ?? null;
|
|
164
|
-
if (this.repos.size === 1)
|
|
165
|
-
return this.repos.values().next().value?.repoPath ?? null;
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
get repoPath() {
|
|
169
|
-
return this.getRepoPath();
|
|
170
|
-
}
|
|
171
|
-
getMeta(repoId) {
|
|
172
|
-
const handle = repoId
|
|
173
|
-
? this.repos.get(repoId)
|
|
174
|
-
: this.repos.size === 1 ? this.repos.values().next().value : null;
|
|
175
|
-
if (!handle)
|
|
176
|
-
return null;
|
|
177
|
-
return { lastCommit: handle.lastCommit, indexedAt: handle.indexedAt, stats: handle.stats };
|
|
178
|
-
}
|
|
179
|
-
get meta() {
|
|
180
|
-
return this.getMeta();
|
|
181
|
-
}
|
|
182
|
-
get storagePath() {
|
|
183
|
-
if (this.repos.size === 1)
|
|
184
|
-
return this.repos.values().next().value?.storagePath ?? null;
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
147
|
/**
|
|
188
148
|
* List all registered repos with their metadata.
|
|
189
149
|
*/
|
|
@@ -274,10 +234,10 @@ export class LocalBackend {
|
|
|
274
234
|
// Add cluster membership context for each result with a nodeId
|
|
275
235
|
if (result.nodeId) {
|
|
276
236
|
try {
|
|
277
|
-
const clusterQuery = `
|
|
278
|
-
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
279
|
-
RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
|
|
280
|
-
LIMIT 1
|
|
237
|
+
const clusterQuery = `
|
|
238
|
+
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
239
|
+
RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
|
|
240
|
+
LIMIT 1
|
|
281
241
|
`;
|
|
282
242
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
283
243
|
if (clusters.length > 0) {
|
|
@@ -294,10 +254,10 @@ export class LocalBackend {
|
|
|
294
254
|
// Add relationships if depth is 'full' and we have a node ID
|
|
295
255
|
if (depth === 'full' && result.nodeId) {
|
|
296
256
|
try {
|
|
297
|
-
const relQuery = `
|
|
298
|
-
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m)
|
|
299
|
-
RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath
|
|
300
|
-
LIMIT 5
|
|
257
|
+
const relQuery = `
|
|
258
|
+
MATCH (n {id: '${result.nodeId.replace(/'/g, "''")}'})-[r:CodeRelation]->(m)
|
|
259
|
+
RETURN r.type AS type, m.name AS targetName, m.filePath AS targetPath
|
|
260
|
+
LIMIT 5
|
|
301
261
|
`;
|
|
302
262
|
const rels = await executeQuery(repo.id, relQuery);
|
|
303
263
|
result.connections = rels.map((rel) => ({
|
|
@@ -324,11 +284,11 @@ export class LocalBackend {
|
|
|
324
284
|
for (const bm25Result of bm25Results) {
|
|
325
285
|
const fileName = bm25Result.filePath.split('/').pop() || bm25Result.filePath;
|
|
326
286
|
try {
|
|
327
|
-
const symbolQuery = `
|
|
328
|
-
MATCH (n)
|
|
329
|
-
WHERE n.filePath CONTAINS '${fileName.replace(/'/g, "''")}'
|
|
330
|
-
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
|
|
331
|
-
LIMIT 3
|
|
287
|
+
const symbolQuery = `
|
|
288
|
+
MATCH (n)
|
|
289
|
+
WHERE n.filePath CONTAINS '${fileName.replace(/'/g, "''")}'
|
|
290
|
+
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
|
|
291
|
+
LIMIT 3
|
|
332
292
|
`;
|
|
333
293
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
334
294
|
if (symbols.length > 0) {
|
|
@@ -372,14 +332,14 @@ export class LocalBackend {
|
|
|
372
332
|
const queryVec = await embedQuery(query);
|
|
373
333
|
const dims = getEmbeddingDims();
|
|
374
334
|
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
375
|
-
const vectorQuery = `
|
|
376
|
-
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
377
|
-
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
378
|
-
YIELD node AS emb, distance
|
|
379
|
-
WITH emb, distance
|
|
380
|
-
WHERE distance < 0.6
|
|
381
|
-
RETURN emb.nodeId AS nodeId, distance
|
|
382
|
-
ORDER BY distance
|
|
335
|
+
const vectorQuery = `
|
|
336
|
+
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
337
|
+
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
338
|
+
YIELD node AS emb, distance
|
|
339
|
+
WITH emb, distance
|
|
340
|
+
WHERE distance < 0.6
|
|
341
|
+
RETURN emb.nodeId AS nodeId, distance
|
|
342
|
+
ORDER BY distance
|
|
383
343
|
`;
|
|
384
344
|
const embResults = await executeQuery(repo.id, vectorQuery);
|
|
385
345
|
if (embResults.length === 0)
|
|
@@ -430,6 +390,42 @@ export class LocalBackend {
|
|
|
430
390
|
return { error: err.message || 'Query failed' };
|
|
431
391
|
}
|
|
432
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
|
395
|
+
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
|
396
|
+
* Raw communities stay intact in KuzuDB for Cypher queries.
|
|
397
|
+
*/
|
|
398
|
+
aggregateClusters(clusters) {
|
|
399
|
+
const groups = new Map();
|
|
400
|
+
for (const c of clusters) {
|
|
401
|
+
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
402
|
+
const symbols = c.symbolCount || 0;
|
|
403
|
+
const cohesion = c.cohesion || 0;
|
|
404
|
+
const existing = groups.get(label);
|
|
405
|
+
if (!existing) {
|
|
406
|
+
groups.set(label, { ids: [c.id], totalSymbols: symbols, weightedCohesion: cohesion * symbols, largest: c });
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
existing.ids.push(c.id);
|
|
410
|
+
existing.totalSymbols += symbols;
|
|
411
|
+
existing.weightedCohesion += cohesion * symbols;
|
|
412
|
+
if (symbols > (existing.largest.symbolCount || 0)) {
|
|
413
|
+
existing.largest = c;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return Array.from(groups.entries())
|
|
418
|
+
.map(([label, g]) => ({
|
|
419
|
+
id: g.largest.id,
|
|
420
|
+
label,
|
|
421
|
+
heuristicLabel: label,
|
|
422
|
+
symbolCount: g.totalSymbols,
|
|
423
|
+
cohesion: g.totalSymbols > 0 ? g.weightedCohesion / g.totalSymbols : 0,
|
|
424
|
+
subCommunities: g.ids.length,
|
|
425
|
+
}))
|
|
426
|
+
.filter(c => c.symbolCount >= 5)
|
|
427
|
+
.sort((a, b) => b.symbolCount - a.symbolCount);
|
|
428
|
+
}
|
|
433
429
|
async overview(repo, params) {
|
|
434
430
|
await this.ensureInitialized(repo.id);
|
|
435
431
|
const limit = params.limit || 20;
|
|
@@ -442,19 +438,22 @@ export class LocalBackend {
|
|
|
442
438
|
};
|
|
443
439
|
if (params.showClusters !== false) {
|
|
444
440
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
441
|
+
// Fetch more raw communities than the display limit so aggregation has enough data
|
|
442
|
+
const rawLimit = Math.max(limit * 5, 200);
|
|
443
|
+
const clusters = await executeQuery(repo.id, `
|
|
444
|
+
MATCH (c:Community)
|
|
445
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
446
|
+
ORDER BY c.symbolCount DESC
|
|
447
|
+
LIMIT ${rawLimit}
|
|
450
448
|
`);
|
|
451
|
-
|
|
449
|
+
const rawClusters = clusters.map((c) => ({
|
|
452
450
|
id: c.id || c[0],
|
|
453
451
|
label: c.label || c[1],
|
|
454
452
|
heuristicLabel: c.heuristicLabel || c[2],
|
|
455
453
|
cohesion: c.cohesion || c[3],
|
|
456
454
|
symbolCount: c.symbolCount || c[4],
|
|
457
455
|
}));
|
|
456
|
+
result.clusters = this.aggregateClusters(rawClusters).slice(0, limit);
|
|
458
457
|
}
|
|
459
458
|
catch {
|
|
460
459
|
result.clusters = [];
|
|
@@ -462,11 +461,11 @@ export class LocalBackend {
|
|
|
462
461
|
}
|
|
463
462
|
if (params.showProcesses !== false) {
|
|
464
463
|
try {
|
|
465
|
-
const processes = await executeQuery(repo.id, `
|
|
466
|
-
MATCH (p:Process)
|
|
467
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
468
|
-
ORDER BY p.stepCount DESC
|
|
469
|
-
LIMIT ${limit}
|
|
464
|
+
const processes = await executeQuery(repo.id, `
|
|
465
|
+
MATCH (p:Process)
|
|
466
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
467
|
+
ORDER BY p.stepCount DESC
|
|
468
|
+
LIMIT ${limit}
|
|
470
469
|
`);
|
|
471
470
|
result.processes = processes.map((p) => ({
|
|
472
471
|
id: p.id || p[0],
|
|
@@ -486,33 +485,33 @@ export class LocalBackend {
|
|
|
486
485
|
await this.ensureInitialized(repo.id);
|
|
487
486
|
const { name, type } = params;
|
|
488
487
|
if (type === 'symbol') {
|
|
489
|
-
const symbolQuery = `
|
|
490
|
-
MATCH (n)
|
|
491
|
-
WHERE n.name = '${name.replace(/'/g, "''")}'
|
|
492
|
-
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
|
|
493
|
-
LIMIT 1
|
|
488
|
+
const symbolQuery = `
|
|
489
|
+
MATCH (n)
|
|
490
|
+
WHERE n.name = '${name.replace(/'/g, "''")}'
|
|
491
|
+
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
|
|
492
|
+
LIMIT 1
|
|
494
493
|
`;
|
|
495
494
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
496
495
|
if (symbols.length === 0)
|
|
497
496
|
return { error: `Symbol '${name}' not found` };
|
|
498
497
|
const sym = symbols[0];
|
|
499
498
|
const symId = sym.id || sym[0];
|
|
500
|
-
const callersQuery = `
|
|
501
|
-
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${symId}'})
|
|
502
|
-
RETURN caller.name AS name, caller.filePath AS filePath
|
|
503
|
-
LIMIT 10
|
|
499
|
+
const callersQuery = `
|
|
500
|
+
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${symId}'})
|
|
501
|
+
RETURN caller.name AS name, caller.filePath AS filePath
|
|
502
|
+
LIMIT 10
|
|
504
503
|
`;
|
|
505
504
|
const callers = await executeQuery(repo.id, callersQuery);
|
|
506
|
-
const calleesQuery = `
|
|
507
|
-
MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
|
|
508
|
-
RETURN callee.name AS name, callee.filePath AS filePath
|
|
509
|
-
LIMIT 10
|
|
505
|
+
const calleesQuery = `
|
|
506
|
+
MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
|
|
507
|
+
RETURN callee.name AS name, callee.filePath AS filePath
|
|
508
|
+
LIMIT 10
|
|
510
509
|
`;
|
|
511
510
|
const callees = await executeQuery(repo.id, calleesQuery);
|
|
512
|
-
const communityQuery = `
|
|
513
|
-
MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
514
|
-
RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
|
|
515
|
-
LIMIT 1
|
|
511
|
+
const communityQuery = `
|
|
512
|
+
MATCH (n {id: '${symId}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
513
|
+
RETURN c.label AS label, c.heuristicLabel AS heuristicLabel
|
|
514
|
+
LIMIT 1
|
|
516
515
|
`;
|
|
517
516
|
const communities = await executeQuery(repo.id, communityQuery);
|
|
518
517
|
return {
|
|
@@ -533,30 +532,47 @@ export class LocalBackend {
|
|
|
533
532
|
};
|
|
534
533
|
}
|
|
535
534
|
if (type === 'cluster') {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
const escaped = name.replace(/'/g, "''");
|
|
536
|
+
// Find ALL communities with this label (not just one)
|
|
537
|
+
const clusterQuery = `
|
|
538
|
+
MATCH (c:Community)
|
|
539
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
540
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
541
541
|
`;
|
|
542
542
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
543
543
|
if (clusters.length === 0)
|
|
544
544
|
return { error: `Cluster '${name}' not found` };
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
545
|
+
const rawClusters = clusters.map((c) => ({
|
|
546
|
+
id: c.id || c[0],
|
|
547
|
+
label: c.label || c[1],
|
|
548
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
549
|
+
cohesion: c.cohesion || c[3],
|
|
550
|
+
symbolCount: c.symbolCount || c[4],
|
|
551
|
+
}));
|
|
552
|
+
// Aggregate: sum symbols, weighted-average cohesion across sub-communities
|
|
553
|
+
let totalSymbols = 0;
|
|
554
|
+
let weightedCohesion = 0;
|
|
555
|
+
for (const c of rawClusters) {
|
|
556
|
+
const s = c.symbolCount || 0;
|
|
557
|
+
totalSymbols += s;
|
|
558
|
+
weightedCohesion += (c.cohesion || 0) * s;
|
|
559
|
+
}
|
|
560
|
+
// Fetch members from ALL matching sub-communities (DISTINCT to avoid dupes)
|
|
561
|
+
const membersQuery = `
|
|
562
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
563
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
564
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
565
|
+
LIMIT 30
|
|
551
566
|
`;
|
|
552
567
|
const members = await executeQuery(repo.id, membersQuery);
|
|
553
568
|
return {
|
|
554
569
|
cluster: {
|
|
555
|
-
id:
|
|
556
|
-
label:
|
|
557
|
-
heuristicLabel:
|
|
558
|
-
cohesion:
|
|
559
|
-
symbolCount:
|
|
570
|
+
id: rawClusters[0].id,
|
|
571
|
+
label: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
572
|
+
heuristicLabel: rawClusters[0].heuristicLabel || rawClusters[0].label,
|
|
573
|
+
cohesion: totalSymbols > 0 ? weightedCohesion / totalSymbols : 0,
|
|
574
|
+
symbolCount: totalSymbols,
|
|
575
|
+
subCommunities: rawClusters.length,
|
|
560
576
|
},
|
|
561
577
|
members: members.map((m) => ({
|
|
562
578
|
name: m.name || m[0],
|
|
@@ -566,21 +582,21 @@ export class LocalBackend {
|
|
|
566
582
|
};
|
|
567
583
|
}
|
|
568
584
|
if (type === 'process') {
|
|
569
|
-
const processQuery = `
|
|
570
|
-
MATCH (p:Process)
|
|
571
|
-
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
572
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, p.entryPointId AS entryPointId, p.terminalId AS terminalId
|
|
573
|
-
LIMIT 1
|
|
585
|
+
const processQuery = `
|
|
586
|
+
MATCH (p:Process)
|
|
587
|
+
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
588
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, p.entryPointId AS entryPointId, p.terminalId AS terminalId
|
|
589
|
+
LIMIT 1
|
|
574
590
|
`;
|
|
575
591
|
const processes = await executeQuery(repo.id, processQuery);
|
|
576
592
|
if (processes.length === 0)
|
|
577
593
|
return { error: `Process '${name}' not found` };
|
|
578
594
|
const proc = processes[0];
|
|
579
595
|
const procId = proc.id || proc[0];
|
|
580
|
-
const stepsQuery = `
|
|
581
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
582
|
-
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
583
|
-
ORDER BY r.step
|
|
596
|
+
const stepsQuery = `
|
|
597
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
598
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
599
|
+
ORDER BY r.step
|
|
584
600
|
`;
|
|
585
601
|
const steps = await executeQuery(repo.id, stepsQuery);
|
|
586
602
|
return {
|
|
@@ -612,11 +628,11 @@ export class LocalBackend {
|
|
|
612
628
|
const minConfidence = params.minConfidence ?? 0;
|
|
613
629
|
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
614
630
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
615
|
-
const targetQuery = `
|
|
616
|
-
MATCH (n)
|
|
617
|
-
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
618
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
619
|
-
LIMIT 1
|
|
631
|
+
const targetQuery = `
|
|
632
|
+
MATCH (n)
|
|
633
|
+
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
634
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
635
|
+
LIMIT 1
|
|
620
636
|
`;
|
|
621
637
|
const targets = await executeQuery(repo.id, targetQuery);
|
|
622
638
|
if (targets.length === 0)
|
|
@@ -772,14 +788,9 @@ export class LocalBackend {
|
|
|
772
788
|
stats: {
|
|
773
789
|
fileCount: newMeta.stats.files || 0,
|
|
774
790
|
functionCount: newMeta.stats.nodes || 0,
|
|
775
|
-
classCount: 0,
|
|
776
|
-
interfaceCount: 0,
|
|
777
|
-
methodCount: 0,
|
|
778
791
|
communityCount: newMeta.stats.communities || 0,
|
|
779
792
|
processCount: newMeta.stats.processes || 0,
|
|
780
793
|
},
|
|
781
|
-
hotspots: [],
|
|
782
|
-
folderTree: '',
|
|
783
794
|
});
|
|
784
795
|
console.error('GitNexus: Indexing complete!');
|
|
785
796
|
return {
|
package/dist/mcp/resources.d.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* MCP Resources (Multi-Repo)
|
|
3
3
|
*
|
|
4
4
|
* Provides structured on-demand data to AI agents.
|
|
5
|
-
*
|
|
6
|
-
* and backwards-compatible global URIs (gitnexus://context).
|
|
5
|
+
* All resources use repo-scoped URIs: gitnexus://repo/{name}/context
|
|
7
6
|
*/
|
|
8
7
|
import type { LocalBackend } from './local/local-backend.js';
|
|
9
8
|
export interface ResourceDefinition {
|