@codragraph/cli 2.0.0 → 2.1.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 +60 -22
- package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
- package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
- package/dist/_shared/cgdb/schema-constants.js +70 -0
- package/dist/_shared/cgdb/schema-constants.js.map +1 -0
- package/dist/_shared/feature-clusters.d.ts +99 -0
- package/dist/_shared/feature-clusters.d.ts.map +1 -0
- package/dist/_shared/feature-clusters.js +2 -0
- package/dist/_shared/feature-clusters.js.map +1 -0
- package/dist/_shared/graph/types.d.ts +16 -2
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +3 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/pipeline.d.ts +1 -1
- package/dist/_shared/pipeline.d.ts.map +1 -1
- package/dist/cli/ai-context.js +4 -0
- package/dist/cli/analyze.js +30 -27
- package/dist/cli/graphstore.js +21 -21
- package/dist/cli/index-repo.js +3 -3
- package/dist/cli/index.js +37 -0
- package/dist/cli/setup.js +9 -5
- package/dist/cli/tool.d.ts +25 -0
- package/dist/cli/tool.js +74 -0
- package/dist/cli/wiki.js +3 -3
- package/dist/config/supported-languages.d.ts +3 -3
- package/dist/config/supported-languages.js +3 -3
- package/dist/core/augmentation/engine.js +7 -7
- package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
- package/dist/core/cgdb/cgdb-adapter.js +1336 -0
- package/dist/core/cgdb/content-read.d.ts +46 -0
- package/dist/core/cgdb/content-read.js +64 -0
- package/dist/core/cgdb/csv-generator.d.ts +29 -0
- package/dist/core/cgdb/csv-generator.js +523 -0
- package/dist/core/cgdb/pool-adapter.d.ts +93 -0
- package/dist/core/cgdb/pool-adapter.js +550 -0
- package/dist/core/cgdb/schema.d.ts +63 -0
- package/dist/core/cgdb/schema.js +557 -0
- package/dist/core/embeddings/embedder.js +4 -2
- package/dist/core/embeddings/embedding-pipeline.js +4 -4
- package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
- package/dist/core/graphstore/cgdb-row-source.js +141 -0
- package/dist/core/graphstore/index.d.ts +2 -2
- package/dist/core/graphstore/index.js +4 -4
- package/dist/core/group/bridge-db.d.ts +2 -2
- package/dist/core/group/bridge-db.js +18 -18
- package/dist/core/group/bridge-schema.d.ts +4 -4
- package/dist/core/group/bridge-schema.js +4 -4
- package/dist/core/group/cross-impact.js +3 -3
- package/dist/core/group/service.d.ts +16 -0
- package/dist/core/group/service.js +360 -0
- package/dist/core/group/sync.js +4 -4
- package/dist/core/ingestion/emit-references.d.ts +1 -1
- package/dist/core/ingestion/emit-references.js +1 -1
- package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
- package/dist/core/ingestion/feature-cluster-processor.js +626 -0
- package/dist/core/ingestion/finalize-orchestrator.js +1 -1
- package/dist/core/ingestion/model/registration-table.js +1 -0
- package/dist/core/ingestion/model/resolve.d.ts +2 -2
- package/dist/core/ingestion/model/resolve.js +3 -3
- package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
- package/dist/core/ingestion/model/semantic-model.js +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/model/symbol-table.js +1 -1
- package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline.d.ts +4 -0
- package/dist/core/ingestion/pipeline.js +9 -5
- package/dist/core/run-analyze.d.ts +1 -0
- package/dist/core/run-analyze.js +36 -30
- package/dist/core/search/bm25-index.d.ts +3 -3
- package/dist/core/search/bm25-index.js +9 -9
- package/dist/core/search/hybrid-search.js +2 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +2 -2
- package/dist/core/wiki/graph-queries.js +5 -5
- package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
- package/dist/mcp/core/cgdb-adapter.js +5 -0
- package/dist/mcp/core/embedder.js +6 -3
- package/dist/mcp/local/local-backend.d.ts +14 -2
- package/dist/mcp/local/local-backend.js +396 -18
- package/dist/mcp/resources.js +139 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +175 -3
- package/dist/server/analyze-worker.js +2 -2
- package/dist/server/api.js +147 -31
- package/dist/storage/repo-manager.d.ts +10 -5
- package/dist/storage/repo-manager.js +10 -6
- package/dist/types/pipeline.d.ts +2 -0
- package/hooks/claude/codragraph-hook.cjs +4 -4
- package/package.json +15 -6
- package/scripts/build.js +21 -21
- package/skills/codragraph-cli.md +17 -1
- package/skills/codragraph-guide.md +6 -2
- package/skills/codragraph-onboarding.md +2 -2
- package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
- package/vendor/tree-sitter-proto/src/node-types.json +1 -1
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { pipeline, env } from '@huggingface/transformers';
|
|
8
8
|
import { isHttpMode, getHttpDimensions, httpEmbedQuery, } from '../../core/embeddings/http-client.js';
|
|
9
|
-
import {
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/cgdb/pool-adapter.js';
|
|
10
12
|
// Model config
|
|
11
13
|
const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
|
|
12
14
|
// Module-level state for singleton pattern
|
|
@@ -33,8 +35,9 @@ export const initEmbedder = async () => {
|
|
|
33
35
|
// Default cache to user-writable location. transformers.js defaults to
|
|
34
36
|
// ./node_modules/.cache inside its own install dir, which is unwritable
|
|
35
37
|
// when codragraph is installed globally (e.g. /usr/lib/node_modules/).
|
|
36
|
-
// Respect HF_HOME if set, otherwise fall back to
|
|
37
|
-
|
|
38
|
+
// Respect HF_HOME if set, otherwise fall back to a user-writable cache
|
|
39
|
+
// path using Node's OS-aware home directory resolution.
|
|
40
|
+
env.cacheDir = process.env.HF_HOME ?? join(homedir(), '.cache', 'huggingface');
|
|
38
41
|
console.error('CodraGraph: Loading embedding model (first search may take a moment)...');
|
|
39
42
|
// Try GPU first (DirectML on Windows, CUDA on Linux), fall back to CPU
|
|
40
43
|
const isWindows = process.platform === 'win32';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports multiple indexed repositories via a global registry.
|
|
6
6
|
* LadybugDB connections are opened lazily per repo on first query.
|
|
7
7
|
*/
|
|
8
|
-
import { isWriteQuery } from '../../core/
|
|
8
|
+
import { isWriteQuery } from '../../core/cgdb/pool-adapter.js';
|
|
9
9
|
export { isWriteQuery };
|
|
10
10
|
import { type RegistryEntry } from '../../storage/repo-manager.js';
|
|
11
11
|
import { GroupService } from '../../core/group/service.js';
|
|
@@ -53,7 +53,7 @@ interface RepoHandle {
|
|
|
53
53
|
name: string;
|
|
54
54
|
repoPath: string;
|
|
55
55
|
storagePath: string;
|
|
56
|
-
|
|
56
|
+
cgdbPath: string;
|
|
57
57
|
indexedAt: string;
|
|
58
58
|
lastCommit: string;
|
|
59
59
|
remoteUrl?: string;
|
|
@@ -323,6 +323,18 @@ export declare class LocalBackend {
|
|
|
323
323
|
* Query clusters (communities) directly from graph.
|
|
324
324
|
* Used by getClustersResource — avoids legacy overview() dispatch.
|
|
325
325
|
*/
|
|
326
|
+
/**
|
|
327
|
+
* Query feature clusters directly from graph.
|
|
328
|
+
* FeatureCluster is the human-facing project area layer above Communities.
|
|
329
|
+
*/
|
|
330
|
+
queryFeatureClusters(repoName?: string, limit?: number, query?: string): Promise<{
|
|
331
|
+
clusters: any[];
|
|
332
|
+
}>;
|
|
333
|
+
/**
|
|
334
|
+
* Query one feature cluster with members, dependencies, and process links.
|
|
335
|
+
*/
|
|
336
|
+
queryFeatureContext(name: string, repoName?: string, limit?: number): Promise<any>;
|
|
337
|
+
queryFeatureImpact(name: string, repoName?: string, direction?: 'upstream' | 'downstream' | 'both', limit?: number): Promise<any>;
|
|
326
338
|
queryClusters(repoName?: string, limit?: number): Promise<{
|
|
327
339
|
clusters: any[];
|
|
328
340
|
}>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import {
|
|
10
|
+
import { initCgdb, executeQuery, executeParameterized, closeCgdb, isCgdbReady, isWriteQuery, } from '../../core/cgdb/pool-adapter.js';
|
|
11
11
|
export { isWriteQuery };
|
|
12
12
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
13
13
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
@@ -18,8 +18,8 @@ import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-ma
|
|
|
18
18
|
import { GroupService } from '../../core/group/service.js';
|
|
19
19
|
import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
|
|
20
20
|
import { collectBestChunks } from '../../core/embeddings/types.js';
|
|
21
|
-
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/
|
|
22
|
-
import { decodeContentField } from '../../core/
|
|
21
|
+
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/cgdb/schema.js';
|
|
22
|
+
import { decodeContentField } from '../../core/cgdb/content-read.js';
|
|
23
23
|
import { PhaseTimer } from '../../core/search/phase-timer.js';
|
|
24
24
|
import { checkStaleness, checkCwdMatch } from '../../core/git-staleness.js';
|
|
25
25
|
// AI context generation is CLI-only (codragraph analyze)
|
|
@@ -57,6 +57,7 @@ export const VALID_NODE_LABELS = new Set([
|
|
|
57
57
|
'CodeElement',
|
|
58
58
|
'Community',
|
|
59
59
|
'Process',
|
|
60
|
+
'FeatureCluster',
|
|
60
61
|
'Struct',
|
|
61
62
|
'Enum',
|
|
62
63
|
'Macro',
|
|
@@ -95,6 +96,9 @@ export const VALID_RELATION_TYPES = new Set([
|
|
|
95
96
|
'HANDLES_TOOL',
|
|
96
97
|
'ENTRY_POINT_OF',
|
|
97
98
|
'WRAPS',
|
|
99
|
+
'QUERIES',
|
|
100
|
+
'FEATURE_MEMBER_OF',
|
|
101
|
+
'FEATURE_DEPENDS_ON',
|
|
98
102
|
]);
|
|
99
103
|
/**
|
|
100
104
|
* Per-relation-type confidence floor for impact analysis.
|
|
@@ -138,6 +142,61 @@ function logQueryError(context, err) {
|
|
|
138
142
|
const msg = err instanceof Error ? err.message : String(err);
|
|
139
143
|
console.error(`CodraGraph [${context}]: ${msg}`);
|
|
140
144
|
}
|
|
145
|
+
function clampNumber(value, min, max, fallback) {
|
|
146
|
+
const parsed = typeof value === 'number'
|
|
147
|
+
? value
|
|
148
|
+
: typeof value === 'string'
|
|
149
|
+
? Number.parseInt(value, 10)
|
|
150
|
+
: Number.NaN;
|
|
151
|
+
if (!Number.isFinite(parsed))
|
|
152
|
+
return fallback;
|
|
153
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
154
|
+
}
|
|
155
|
+
function normalizeStringArray(value) {
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
return value.map((item) => String(item).replace(/^['"]|['"]$/g, ''));
|
|
158
|
+
}
|
|
159
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
160
|
+
return [];
|
|
161
|
+
return value
|
|
162
|
+
.replace(/^\[|\]$/g, '')
|
|
163
|
+
.split(',')
|
|
164
|
+
.map((item) => item.trim().replace(/^['"]|['"]$/g, ''))
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
function uniqueStrings(values) {
|
|
168
|
+
return [...new Set(values.map((v) => (v ?? '').trim()).filter(Boolean))].sort();
|
|
169
|
+
}
|
|
170
|
+
function isDocsFilePath(filePath) {
|
|
171
|
+
const p = (filePath || '').toLowerCase().replace(/\\/g, '/');
|
|
172
|
+
return p.includes('/docs/') || p.endsWith('.md') || p.endsWith('.mdx');
|
|
173
|
+
}
|
|
174
|
+
function mapFeatureClusterRow(row) {
|
|
175
|
+
const rich = row.summary !== undefined ||
|
|
176
|
+
row.repo !== undefined ||
|
|
177
|
+
row.routes !== undefined ||
|
|
178
|
+
row[10] !== undefined;
|
|
179
|
+
return {
|
|
180
|
+
id: row.id || row[0],
|
|
181
|
+
name: row.name || row[1],
|
|
182
|
+
slug: row.slug || row[2],
|
|
183
|
+
featureKind: row.featureKind || row[3],
|
|
184
|
+
summary: row.summary ?? (rich ? row[4] : ''),
|
|
185
|
+
description: row.description ?? (rich ? row[5] : row[4]),
|
|
186
|
+
repo: row.repo ?? (rich ? row[6] : undefined),
|
|
187
|
+
service: row.service ?? (rich ? row[7] : undefined),
|
|
188
|
+
signals: normalizeStringArray(row.signals ?? (rich ? row[8] : row[5])),
|
|
189
|
+
memberCount: row.memberCount ?? (rich ? row[9] : row[6]) ?? 0,
|
|
190
|
+
entryPointIds: normalizeStringArray(row.entryPointIds ?? (rich ? row[10] : row[7])),
|
|
191
|
+
routes: normalizeStringArray(row.routes ?? (rich ? row[11] : [])),
|
|
192
|
+
tools: normalizeStringArray(row.tools ?? (rich ? row[12] : [])),
|
|
193
|
+
testCoverageHints: normalizeStringArray(row.testCoverageHints ?? (rich ? row[13] : [])),
|
|
194
|
+
lastIndexedCommit: row.lastIndexedCommit ?? (rich ? row[14] : undefined),
|
|
195
|
+
confidence: row.confidence ?? (rich ? row[15] : row[8]) ?? 0,
|
|
196
|
+
source: row.source || (rich ? row[16] : row[9]) || 'heuristic',
|
|
197
|
+
crossRepoLinks: [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
141
200
|
/**
|
|
142
201
|
* Structured per-query latency log for production aggregation (#553).
|
|
143
202
|
*
|
|
@@ -182,6 +241,9 @@ export class LocalBackend {
|
|
|
182
241
|
query: (r, p) => this.query(r, p),
|
|
183
242
|
impactByUid: (id, uid, d, o) => this.impactByUid(id, uid, d, o),
|
|
184
243
|
context: (r, p) => this.context(r, p),
|
|
244
|
+
featureClusters: (r, p) => this.queryFeatureClusters(r.name, p.limit, p.query),
|
|
245
|
+
featureContext: (r, p) => this.queryFeatureContext(p.name, r.name, p.limit),
|
|
246
|
+
featureImpact: (r, p) => this.queryFeatureImpact(p.name, r.name, p.direction ?? 'upstream', p.limit),
|
|
185
247
|
};
|
|
186
248
|
this.groupToolSvc = new GroupService(port);
|
|
187
249
|
}
|
|
@@ -189,7 +251,7 @@ export class LocalBackend {
|
|
|
189
251
|
}
|
|
190
252
|
/** Close all pooled LadybugDB connections (CLI one-shot; optional for long-lived MCP). */
|
|
191
253
|
async dispose() {
|
|
192
|
-
await
|
|
254
|
+
await closeCgdb();
|
|
193
255
|
}
|
|
194
256
|
// ─── Initialization ──────────────────────────────────────────────
|
|
195
257
|
/**
|
|
@@ -212,9 +274,9 @@ export class LocalBackend {
|
|
|
212
274
|
const id = this.repoId(entry.name, entry.path);
|
|
213
275
|
freshIds.add(id);
|
|
214
276
|
const storagePath = entry.storagePath;
|
|
215
|
-
const
|
|
277
|
+
const cgdbPath = path.join(storagePath, 'cgdb');
|
|
216
278
|
// Clean up any leftover KuzuDB files from before the LadybugDB migration.
|
|
217
|
-
// If kuzu exists but
|
|
279
|
+
// If kuzu exists but cgdb doesn't, warn so the user knows to re-analyze.
|
|
218
280
|
const kuzu = await cleanupOldKuzuFiles(storagePath);
|
|
219
281
|
if (kuzu.found && kuzu.needsReindex) {
|
|
220
282
|
console.error(`CodraGraph: "${entry.name}" has a stale KuzuDB index. Run: codragraph analyze ${entry.path}`);
|
|
@@ -224,7 +286,7 @@ export class LocalBackend {
|
|
|
224
286
|
name: entry.name,
|
|
225
287
|
repoPath: entry.path,
|
|
226
288
|
storagePath,
|
|
227
|
-
|
|
289
|
+
cgdbPath,
|
|
228
290
|
indexedAt: entry.indexedAt,
|
|
229
291
|
lastCommit: entry.lastCommit,
|
|
230
292
|
remoteUrl: entry.remoteUrl,
|
|
@@ -364,7 +426,7 @@ export class LocalBackend {
|
|
|
364
426
|
// Check if the index was rebuilt since we opened the connection (#297).
|
|
365
427
|
// Throttle staleness checks to at most once per 5 seconds per repo to
|
|
366
428
|
// avoid an fs.readFile round-trip on every tool invocation.
|
|
367
|
-
if (this.initializedRepos.has(repoId) &&
|
|
429
|
+
if (this.initializedRepos.has(repoId) && isCgdbReady(repoId)) {
|
|
368
430
|
const now = Date.now();
|
|
369
431
|
const lastCheck = this.lastStalenessCheck.get(repoId) ?? 0;
|
|
370
432
|
if (now - lastCheck < 5000)
|
|
@@ -380,10 +442,10 @@ export class LocalBackend {
|
|
|
380
442
|
// callers both detect staleness and double-close the pool.
|
|
381
443
|
const reinit = (async () => {
|
|
382
444
|
try {
|
|
383
|
-
await
|
|
445
|
+
await closeCgdb(repoId);
|
|
384
446
|
this.initializedRepos.delete(repoId);
|
|
385
447
|
handle.indexedAt = meta.indexedAt;
|
|
386
|
-
await
|
|
448
|
+
await initCgdb(repoId, handle.cgdbPath);
|
|
387
449
|
this.initializedRepos.add(repoId);
|
|
388
450
|
}
|
|
389
451
|
finally {
|
|
@@ -402,7 +464,7 @@ export class LocalBackend {
|
|
|
402
464
|
}
|
|
403
465
|
}
|
|
404
466
|
try {
|
|
405
|
-
await
|
|
467
|
+
await initCgdb(repoId, handle.cgdbPath);
|
|
406
468
|
this.initializedRepos.add(repoId);
|
|
407
469
|
}
|
|
408
470
|
catch (err) {
|
|
@@ -545,9 +607,18 @@ export class LocalBackend {
|
|
|
545
607
|
return this.handleGroupTool(method, params || {});
|
|
546
608
|
}
|
|
547
609
|
const p = params && typeof params === 'object' ? params : {};
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
610
|
+
const groupRepoMethods = new Set([
|
|
611
|
+
'impact',
|
|
612
|
+
'query',
|
|
613
|
+
'context',
|
|
614
|
+
'feature_clusters',
|
|
615
|
+
'feature_context',
|
|
616
|
+
'cluster_query',
|
|
617
|
+
'cluster_context',
|
|
618
|
+
'context_pack',
|
|
619
|
+
'cluster_impact',
|
|
620
|
+
]);
|
|
621
|
+
if (groupRepoMethods.has(method) && typeof p.repo === 'string' && p.repo.startsWith('@')) {
|
|
551
622
|
return this.callToolAtGroupRepo(method, p);
|
|
552
623
|
}
|
|
553
624
|
// Resolve repo from optional param (re-reads registry on miss)
|
|
@@ -582,6 +653,22 @@ export class LocalBackend {
|
|
|
582
653
|
return this.toolMap(repo, params);
|
|
583
654
|
case 'api_impact':
|
|
584
655
|
return this.apiImpact(repo, params);
|
|
656
|
+
case 'feature_clusters':
|
|
657
|
+
case 'cluster_query':
|
|
658
|
+
return this.queryFeatureClusters(params?.repo, clampNumber(params?.limit, 1, 500, 100), String(params?.query ?? ''));
|
|
659
|
+
case 'feature_context':
|
|
660
|
+
case 'cluster_context':
|
|
661
|
+
case 'context_pack':
|
|
662
|
+
return this.queryFeatureContext(String(params?.name ??
|
|
663
|
+
params?.slug ??
|
|
664
|
+
params?.id ??
|
|
665
|
+
''), params?.repo, clampNumber(params?.limit, 1, 500, 100));
|
|
666
|
+
case 'cluster_impact':
|
|
667
|
+
return this.queryFeatureImpact(String(params?.name ??
|
|
668
|
+
params?.slug ??
|
|
669
|
+
params?.id ??
|
|
670
|
+
''), params?.repo, params?.direction ??
|
|
671
|
+
'upstream', clampNumber(params?.limit, 1, 500, 100));
|
|
585
672
|
case 'harness_swarm_run': {
|
|
586
673
|
// Same lazy-import dance as harness_run (see comments below) — keeps
|
|
587
674
|
// codragraph-harness optional and avoids a circular build-time dep.
|
|
@@ -948,10 +1035,10 @@ export class LocalBackend {
|
|
|
948
1035
|
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
|
949
1036
|
*/
|
|
950
1037
|
async bm25Search(repo, query, limit) {
|
|
951
|
-
const {
|
|
1038
|
+
const { searchFTSFromCgdb } = await import('../../core/search/bm25-index.js');
|
|
952
1039
|
let bm25Results;
|
|
953
1040
|
try {
|
|
954
|
-
bm25Results = await
|
|
1041
|
+
bm25Results = await searchFTSFromCgdb(query, limit, repo.id);
|
|
955
1042
|
}
|
|
956
1043
|
catch (err) {
|
|
957
1044
|
console.error('CodraGraph: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
|
@@ -1088,7 +1175,7 @@ export class LocalBackend {
|
|
|
1088
1175
|
}
|
|
1089
1176
|
async cypher(repo, params) {
|
|
1090
1177
|
await this.ensureInitialized(repo.id);
|
|
1091
|
-
if (!
|
|
1178
|
+
if (!isCgdbReady(repo.id)) {
|
|
1092
1179
|
return { error: 'LadybugDB not ready. Index may be corrupted.' };
|
|
1093
1180
|
}
|
|
1094
1181
|
// Block write operations (defense-in-depth — DB is already read-only)
|
|
@@ -2685,6 +2772,57 @@ export class LocalBackend {
|
|
|
2685
2772
|
}
|
|
2686
2773
|
return svc.groupContext(contextArgs);
|
|
2687
2774
|
}
|
|
2775
|
+
if (method === 'feature_clusters' || method === 'cluster_query') {
|
|
2776
|
+
const args = {
|
|
2777
|
+
name: groupName,
|
|
2778
|
+
query: params.query,
|
|
2779
|
+
limit: params.limit,
|
|
2780
|
+
};
|
|
2781
|
+
if (memberRest !== undefined) {
|
|
2782
|
+
args.subgroup = memberRest;
|
|
2783
|
+
args.subgroupExact = true;
|
|
2784
|
+
}
|
|
2785
|
+
return svc.groupFeatureClusters(args);
|
|
2786
|
+
}
|
|
2787
|
+
if (method === 'feature_context' || method === 'cluster_context' || method === 'context_pack') {
|
|
2788
|
+
const clusterName = typeof params.name === 'string' && params.name.trim() !== ''
|
|
2789
|
+
? params.name.trim()
|
|
2790
|
+
: typeof params.slug === 'string' && params.slug.trim() !== ''
|
|
2791
|
+
? params.slug.trim()
|
|
2792
|
+
: typeof params.id === 'string'
|
|
2793
|
+
? params.id.trim()
|
|
2794
|
+
: '';
|
|
2795
|
+
const args = {
|
|
2796
|
+
name: groupName,
|
|
2797
|
+
cluster: clusterName,
|
|
2798
|
+
limit: params.limit,
|
|
2799
|
+
};
|
|
2800
|
+
if (memberRest !== undefined) {
|
|
2801
|
+
args.subgroup = memberRest;
|
|
2802
|
+
args.subgroupExact = true;
|
|
2803
|
+
}
|
|
2804
|
+
return svc.groupFeatureContext(args);
|
|
2805
|
+
}
|
|
2806
|
+
if (method === 'cluster_impact') {
|
|
2807
|
+
const clusterName = typeof params.name === 'string' && params.name.trim() !== ''
|
|
2808
|
+
? params.name.trim()
|
|
2809
|
+
: typeof params.slug === 'string' && params.slug.trim() !== ''
|
|
2810
|
+
? params.slug.trim()
|
|
2811
|
+
: typeof params.id === 'string'
|
|
2812
|
+
? params.id.trim()
|
|
2813
|
+
: '';
|
|
2814
|
+
const args = {
|
|
2815
|
+
name: groupName,
|
|
2816
|
+
cluster: clusterName,
|
|
2817
|
+
direction: params.direction,
|
|
2818
|
+
limit: params.limit,
|
|
2819
|
+
};
|
|
2820
|
+
if (memberRest !== undefined) {
|
|
2821
|
+
args.subgroup = memberRest;
|
|
2822
|
+
args.subgroupExact = true;
|
|
2823
|
+
}
|
|
2824
|
+
return svc.groupFeatureImpact(args);
|
|
2825
|
+
}
|
|
2688
2826
|
throw new Error(`Internal: unsupported group-repo tool ${method}`);
|
|
2689
2827
|
}
|
|
2690
2828
|
async groupList(params) {
|
|
@@ -3082,6 +3220,246 @@ export class LocalBackend {
|
|
|
3082
3220
|
* Query clusters (communities) directly from graph.
|
|
3083
3221
|
* Used by getClustersResource — avoids legacy overview() dispatch.
|
|
3084
3222
|
*/
|
|
3223
|
+
/**
|
|
3224
|
+
* Query feature clusters directly from graph.
|
|
3225
|
+
* FeatureCluster is the human-facing project area layer above Communities.
|
|
3226
|
+
*/
|
|
3227
|
+
async queryFeatureClusters(repoName, limit = 100, query = '') {
|
|
3228
|
+
const repo = await this.resolveRepo(repoName);
|
|
3229
|
+
await this.ensureInitialized(repo.id);
|
|
3230
|
+
const safeLimit = clampNumber(limit, 1, 500, 100);
|
|
3231
|
+
const needle = query.trim().toLowerCase();
|
|
3232
|
+
const fetchLimit = needle ? 500 : safeLimit;
|
|
3233
|
+
try {
|
|
3234
|
+
let clusters;
|
|
3235
|
+
try {
|
|
3236
|
+
clusters = await executeQuery(repo.id, `
|
|
3237
|
+
MATCH (c:FeatureCluster)
|
|
3238
|
+
RETURN c.id AS id, c.name AS name, c.slug AS slug, c.featureKind AS featureKind,
|
|
3239
|
+
c.summary AS summary, c.description AS description, c.repo AS repo,
|
|
3240
|
+
c.service AS service, c.signals AS signals, c.memberCount AS memberCount,
|
|
3241
|
+
c.entryPointIds AS entryPointIds, c.routes AS routes, c.tools AS tools,
|
|
3242
|
+
c.testCoverageHints AS testCoverageHints,
|
|
3243
|
+
c.lastIndexedCommit AS lastIndexedCommit, c.confidence AS confidence,
|
|
3244
|
+
c.source AS source
|
|
3245
|
+
ORDER BY c.memberCount DESC
|
|
3246
|
+
LIMIT ${fetchLimit}
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
catch {
|
|
3250
|
+
clusters = await executeQuery(repo.id, `
|
|
3251
|
+
MATCH (c:FeatureCluster)
|
|
3252
|
+
RETURN c.id AS id, c.name AS name, c.slug AS slug, c.featureKind AS featureKind,
|
|
3253
|
+
c.description AS description, c.signals AS signals, c.memberCount AS memberCount,
|
|
3254
|
+
c.entryPointIds AS entryPointIds, c.confidence AS confidence, c.source AS source
|
|
3255
|
+
ORDER BY c.memberCount DESC
|
|
3256
|
+
LIMIT ${fetchLimit}
|
|
3257
|
+
`);
|
|
3258
|
+
}
|
|
3259
|
+
return {
|
|
3260
|
+
clusters: clusters
|
|
3261
|
+
.map(mapFeatureClusterRow)
|
|
3262
|
+
.filter((cluster) => {
|
|
3263
|
+
if (!needle)
|
|
3264
|
+
return true;
|
|
3265
|
+
return [
|
|
3266
|
+
cluster.name,
|
|
3267
|
+
cluster.slug,
|
|
3268
|
+
cluster.summary,
|
|
3269
|
+
cluster.description,
|
|
3270
|
+
...(cluster.signals || []),
|
|
3271
|
+
...(cluster.routes || []),
|
|
3272
|
+
...(cluster.tools || []),
|
|
3273
|
+
]
|
|
3274
|
+
.join(' ')
|
|
3275
|
+
.toLowerCase()
|
|
3276
|
+
.includes(needle);
|
|
3277
|
+
})
|
|
3278
|
+
.slice(0, safeLimit),
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
catch {
|
|
3282
|
+
return { clusters: [] };
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Query one feature cluster with members, dependencies, and process links.
|
|
3287
|
+
*/
|
|
3288
|
+
async queryFeatureContext(name, repoName, limit = 100) {
|
|
3289
|
+
const key = name.trim();
|
|
3290
|
+
if (!key)
|
|
3291
|
+
return { error: 'Feature cluster name, slug, or id is required' };
|
|
3292
|
+
const repo = await this.resolveRepo(repoName);
|
|
3293
|
+
await this.ensureInitialized(repo.id);
|
|
3294
|
+
const safeLimit = clampNumber(limit, 1, 500, 100);
|
|
3295
|
+
let clusters;
|
|
3296
|
+
try {
|
|
3297
|
+
clusters = await executeParameterized(repo.id, `
|
|
3298
|
+
MATCH (c:FeatureCluster)
|
|
3299
|
+
WHERE c.id = $key OR c.name = $key OR c.slug = $key
|
|
3300
|
+
RETURN c.id AS id, c.name AS name, c.slug AS slug, c.featureKind AS featureKind,
|
|
3301
|
+
c.summary AS summary, c.description AS description, c.repo AS repo,
|
|
3302
|
+
c.service AS service, c.signals AS signals, c.memberCount AS memberCount,
|
|
3303
|
+
c.entryPointIds AS entryPointIds, c.routes AS routes, c.tools AS tools,
|
|
3304
|
+
c.testCoverageHints AS testCoverageHints,
|
|
3305
|
+
c.lastIndexedCommit AS lastIndexedCommit, c.confidence AS confidence,
|
|
3306
|
+
c.source AS source
|
|
3307
|
+
LIMIT 1
|
|
3308
|
+
`, { key });
|
|
3309
|
+
}
|
|
3310
|
+
catch {
|
|
3311
|
+
clusters = await executeParameterized(repo.id, `
|
|
3312
|
+
MATCH (c:FeatureCluster)
|
|
3313
|
+
WHERE c.id = $key OR c.name = $key OR c.slug = $key
|
|
3314
|
+
RETURN c.id AS id, c.name AS name, c.slug AS slug, c.featureKind AS featureKind,
|
|
3315
|
+
c.description AS description, c.signals AS signals, c.memberCount AS memberCount,
|
|
3316
|
+
c.entryPointIds AS entryPointIds, c.confidence AS confidence, c.source AS source
|
|
3317
|
+
LIMIT 1
|
|
3318
|
+
`, { key });
|
|
3319
|
+
}
|
|
3320
|
+
let cluster = clusters.length > 0 ? mapFeatureClusterRow(clusters[0]) : undefined;
|
|
3321
|
+
if (!cluster) {
|
|
3322
|
+
const fallback = await this.queryFeatureClusters(repoName, 1, key);
|
|
3323
|
+
cluster = fallback.clusters[0];
|
|
3324
|
+
}
|
|
3325
|
+
if (!cluster)
|
|
3326
|
+
return { error: `Feature cluster '${name}' not found` };
|
|
3327
|
+
const clusterId = cluster.id;
|
|
3328
|
+
const members = await executeParameterized(repo.id, `
|
|
3329
|
+
MATCH (n)-[r:CodeRelation {type: 'FEATURE_MEMBER_OF'}]->(c:FeatureCluster {id: $clusterId})
|
|
3330
|
+
RETURN DISTINCT n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath,
|
|
3331
|
+
n.startLine AS startLine, n.endLine AS endLine, r.confidence AS confidence,
|
|
3332
|
+
r.reason AS reason
|
|
3333
|
+
ORDER BY type, filePath, startLine
|
|
3334
|
+
LIMIT ${safeLimit}
|
|
3335
|
+
`, { clusterId });
|
|
3336
|
+
const outgoing = await executeParameterized(repo.id, `
|
|
3337
|
+
MATCH (c:FeatureCluster {id: $clusterId})-[r:CodeRelation {type: 'FEATURE_DEPENDS_ON'}]->(d:FeatureCluster)
|
|
3338
|
+
RETURN d.id AS id, d.name AS name, d.slug AS slug, r.confidence AS confidence, r.reason AS reason
|
|
3339
|
+
ORDER BY d.name
|
|
3340
|
+
`, { clusterId });
|
|
3341
|
+
const incoming = await executeParameterized(repo.id, `
|
|
3342
|
+
MATCH (s:FeatureCluster)-[r:CodeRelation {type: 'FEATURE_DEPENDS_ON'}]->(c:FeatureCluster {id: $clusterId})
|
|
3343
|
+
RETURN s.id AS id, s.name AS name, s.slug AS slug, r.confidence AS confidence, r.reason AS reason
|
|
3344
|
+
ORDER BY s.name
|
|
3345
|
+
`, { clusterId });
|
|
3346
|
+
const processes = await executeParameterized(repo.id, `
|
|
3347
|
+
MATCH (n)-[:CodeRelation {type: 'FEATURE_MEMBER_OF'}]->(c:FeatureCluster {id: $clusterId}),
|
|
3348
|
+
(n)-[:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
3349
|
+
RETURN DISTINCT p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel,
|
|
3350
|
+
p.processType AS processType, p.stepCount AS stepCount
|
|
3351
|
+
ORDER BY p.stepCount DESC
|
|
3352
|
+
LIMIT 25
|
|
3353
|
+
`, { clusterId });
|
|
3354
|
+
const mappedMembers = members.map((m) => {
|
|
3355
|
+
const id = m.id || m[0];
|
|
3356
|
+
const type = m.type || m[2];
|
|
3357
|
+
const filePath = m.filePath || m[3];
|
|
3358
|
+
const name = m.name || m[1];
|
|
3359
|
+
return {
|
|
3360
|
+
id,
|
|
3361
|
+
name,
|
|
3362
|
+
type,
|
|
3363
|
+
filePath,
|
|
3364
|
+
startLine: m.startLine ?? m[4],
|
|
3365
|
+
endLine: m.endLine ?? m[5],
|
|
3366
|
+
role: cluster.entryPointIds.includes(id)
|
|
3367
|
+
? 'entrypoint'
|
|
3368
|
+
: type === 'File' || type === 'Section'
|
|
3369
|
+
? 'supporting'
|
|
3370
|
+
: 'implementation',
|
|
3371
|
+
confidence: m.confidence ?? m[6] ?? 0,
|
|
3372
|
+
reason: m.reason || m[7],
|
|
3373
|
+
};
|
|
3374
|
+
});
|
|
3375
|
+
const outgoingDependencies = outgoing.map((d) => ({
|
|
3376
|
+
id: d.id || d[0],
|
|
3377
|
+
name: d.name || d[1],
|
|
3378
|
+
slug: d.slug || d[2],
|
|
3379
|
+
confidence: d.confidence ?? d[3] ?? 0,
|
|
3380
|
+
reason: d.reason || d[4],
|
|
3381
|
+
}));
|
|
3382
|
+
const incomingDependencies = incoming.map((d) => ({
|
|
3383
|
+
id: d.id || d[0],
|
|
3384
|
+
name: d.name || d[1],
|
|
3385
|
+
slug: d.slug || d[2],
|
|
3386
|
+
confidence: d.confidence ?? d[3] ?? 0,
|
|
3387
|
+
reason: d.reason || d[4],
|
|
3388
|
+
}));
|
|
3389
|
+
const testMembers = mappedMembers.filter((member) => isTestFilePath(member.filePath || ''));
|
|
3390
|
+
const docMembers = mappedMembers.filter((member) => member.type === 'Section' || isDocsFilePath(member.filePath));
|
|
3391
|
+
const warnings = [];
|
|
3392
|
+
if (testMembers.length === 0) {
|
|
3393
|
+
warnings.push('No obvious test members were found in this cluster.');
|
|
3394
|
+
}
|
|
3395
|
+
if (incomingDependencies.length + outgoingDependencies.length > 10) {
|
|
3396
|
+
warnings.push('This cluster has broad feature dependencies; check impact before large edits.');
|
|
3397
|
+
}
|
|
3398
|
+
return {
|
|
3399
|
+
cluster,
|
|
3400
|
+
members: mappedMembers,
|
|
3401
|
+
dependencies: {
|
|
3402
|
+
outgoing: outgoingDependencies,
|
|
3403
|
+
incoming: incomingDependencies,
|
|
3404
|
+
},
|
|
3405
|
+
entryPoints: mappedMembers.filter((member) => cluster.entryPointIds.includes(member.id) ||
|
|
3406
|
+
member.type === 'Route' ||
|
|
3407
|
+
member.type === 'Tool'),
|
|
3408
|
+
routes: mappedMembers.filter((member) => member.type === 'Route'),
|
|
3409
|
+
tools: mappedMembers.filter((member) => member.type === 'Tool'),
|
|
3410
|
+
processes: processes.map((p) => ({
|
|
3411
|
+
id: p.id || p[0],
|
|
3412
|
+
label: p.label || p[1],
|
|
3413
|
+
heuristicLabel: p.heuristicLabel || p[2],
|
|
3414
|
+
processType: p.processType || p[3],
|
|
3415
|
+
stepCount: p.stepCount || p[4],
|
|
3416
|
+
})),
|
|
3417
|
+
tests: testMembers,
|
|
3418
|
+
docs: docMembers,
|
|
3419
|
+
crossRepoLinks: cluster.crossRepoLinks || [],
|
|
3420
|
+
safeEditSurface: {
|
|
3421
|
+
files: uniqueStrings(mappedMembers.map((member) => member.filePath)),
|
|
3422
|
+
symbols: uniqueStrings(mappedMembers
|
|
3423
|
+
.filter((member) => !['File', 'Folder', 'Section'].includes(member.type || ''))
|
|
3424
|
+
.map((member) => member.name)),
|
|
3425
|
+
warnings,
|
|
3426
|
+
},
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
async queryFeatureImpact(name, repoName, direction = 'upstream', limit = 100) {
|
|
3430
|
+
const contextPack = await this.queryFeatureContext(name, repoName, limit);
|
|
3431
|
+
if (contextPack?.error)
|
|
3432
|
+
return contextPack;
|
|
3433
|
+
const incoming = contextPack.dependencies?.incoming ?? [];
|
|
3434
|
+
const outgoing = contextPack.dependencies?.outgoing ?? [];
|
|
3435
|
+
const impactedClusters = direction === 'downstream'
|
|
3436
|
+
? outgoing
|
|
3437
|
+
: direction === 'both'
|
|
3438
|
+
? [...incoming, ...outgoing]
|
|
3439
|
+
: incoming;
|
|
3440
|
+
const uniqueImpacted = Array.from(new Map(impactedClusters.map((cluster) => [cluster.id || cluster.name, cluster])).values());
|
|
3441
|
+
const affectedMembers = contextPack.members?.length ?? 0;
|
|
3442
|
+
const dependencyCount = uniqueImpacted.length;
|
|
3443
|
+
const riskLevel = dependencyCount >= 15 || affectedMembers >= 250
|
|
3444
|
+
? 'HIGH'
|
|
3445
|
+
: dependencyCount >= 5 || affectedMembers >= 75
|
|
3446
|
+
? 'MEDIUM'
|
|
3447
|
+
: 'LOW';
|
|
3448
|
+
return {
|
|
3449
|
+
cluster: contextPack.cluster,
|
|
3450
|
+
direction,
|
|
3451
|
+
impactedClusters: uniqueImpacted,
|
|
3452
|
+
safeEditSurface: contextPack.safeEditSurface,
|
|
3453
|
+
contextPack,
|
|
3454
|
+
impactSummary: {
|
|
3455
|
+
affectedMembers,
|
|
3456
|
+
dependencyCount,
|
|
3457
|
+
incomingDependencies: incoming.length,
|
|
3458
|
+
outgoingDependencies: outgoing.length,
|
|
3459
|
+
riskLevel,
|
|
3460
|
+
},
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3085
3463
|
async queryClusters(repoName, limit = 100) {
|
|
3086
3464
|
const repo = await this.resolveRepo(repoName);
|
|
3087
3465
|
await this.ensureInitialized(repo.id);
|
|
@@ -3222,7 +3600,7 @@ export class LocalBackend {
|
|
|
3222
3600
|
};
|
|
3223
3601
|
}
|
|
3224
3602
|
async disconnect() {
|
|
3225
|
-
await
|
|
3603
|
+
await closeCgdb(); // close all connections
|
|
3226
3604
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
3227
3605
|
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs,
|
|
3228
3606
|
// and importing the embedder module on Node v24+ crashes if onnxruntime
|