@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.
Files changed (101) hide show
  1. package/README.md +60 -22
  2. package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
  3. package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
  4. package/dist/_shared/cgdb/schema-constants.js +70 -0
  5. package/dist/_shared/cgdb/schema-constants.js.map +1 -0
  6. package/dist/_shared/feature-clusters.d.ts +99 -0
  7. package/dist/_shared/feature-clusters.d.ts.map +1 -0
  8. package/dist/_shared/feature-clusters.js +2 -0
  9. package/dist/_shared/feature-clusters.js.map +1 -0
  10. package/dist/_shared/graph/types.d.ts +16 -2
  11. package/dist/_shared/graph/types.d.ts.map +1 -1
  12. package/dist/_shared/index.d.ts +3 -2
  13. package/dist/_shared/index.d.ts.map +1 -1
  14. package/dist/_shared/index.js +1 -1
  15. package/dist/_shared/index.js.map +1 -1
  16. package/dist/_shared/pipeline.d.ts +1 -1
  17. package/dist/_shared/pipeline.d.ts.map +1 -1
  18. package/dist/cli/ai-context.js +4 -0
  19. package/dist/cli/analyze.js +30 -27
  20. package/dist/cli/graphstore.js +21 -21
  21. package/dist/cli/index-repo.js +3 -3
  22. package/dist/cli/index.js +37 -0
  23. package/dist/cli/setup.js +9 -5
  24. package/dist/cli/tool.d.ts +25 -0
  25. package/dist/cli/tool.js +74 -0
  26. package/dist/cli/wiki.js +3 -3
  27. package/dist/config/supported-languages.d.ts +3 -3
  28. package/dist/config/supported-languages.js +3 -3
  29. package/dist/core/augmentation/engine.js +7 -7
  30. package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
  31. package/dist/core/cgdb/cgdb-adapter.js +1336 -0
  32. package/dist/core/cgdb/content-read.d.ts +46 -0
  33. package/dist/core/cgdb/content-read.js +64 -0
  34. package/dist/core/cgdb/csv-generator.d.ts +29 -0
  35. package/dist/core/cgdb/csv-generator.js +523 -0
  36. package/dist/core/cgdb/pool-adapter.d.ts +93 -0
  37. package/dist/core/cgdb/pool-adapter.js +550 -0
  38. package/dist/core/cgdb/schema.d.ts +63 -0
  39. package/dist/core/cgdb/schema.js +557 -0
  40. package/dist/core/embeddings/embedder.js +4 -2
  41. package/dist/core/embeddings/embedding-pipeline.js +4 -4
  42. package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
  43. package/dist/core/graphstore/cgdb-row-source.js +141 -0
  44. package/dist/core/graphstore/index.d.ts +2 -2
  45. package/dist/core/graphstore/index.js +4 -4
  46. package/dist/core/group/bridge-db.d.ts +2 -2
  47. package/dist/core/group/bridge-db.js +18 -18
  48. package/dist/core/group/bridge-schema.d.ts +4 -4
  49. package/dist/core/group/bridge-schema.js +4 -4
  50. package/dist/core/group/cross-impact.js +3 -3
  51. package/dist/core/group/service.d.ts +16 -0
  52. package/dist/core/group/service.js +360 -0
  53. package/dist/core/group/sync.js +4 -4
  54. package/dist/core/ingestion/emit-references.d.ts +1 -1
  55. package/dist/core/ingestion/emit-references.js +1 -1
  56. package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
  57. package/dist/core/ingestion/feature-cluster-processor.js +626 -0
  58. package/dist/core/ingestion/finalize-orchestrator.js +1 -1
  59. package/dist/core/ingestion/model/registration-table.js +1 -0
  60. package/dist/core/ingestion/model/resolve.d.ts +2 -2
  61. package/dist/core/ingestion/model/resolve.js +3 -3
  62. package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
  63. package/dist/core/ingestion/model/semantic-model.js +1 -1
  64. package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
  65. package/dist/core/ingestion/model/symbol-table.js +1 -1
  66. package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
  67. package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
  68. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  69. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  70. package/dist/core/ingestion/pipeline.d.ts +4 -0
  71. package/dist/core/ingestion/pipeline.js +9 -5
  72. package/dist/core/run-analyze.d.ts +1 -0
  73. package/dist/core/run-analyze.js +36 -30
  74. package/dist/core/search/bm25-index.d.ts +3 -3
  75. package/dist/core/search/bm25-index.js +9 -9
  76. package/dist/core/search/hybrid-search.js +2 -2
  77. package/dist/core/wiki/generator.d.ts +2 -2
  78. package/dist/core/wiki/generator.js +4 -4
  79. package/dist/core/wiki/graph-queries.d.ts +2 -2
  80. package/dist/core/wiki/graph-queries.js +5 -5
  81. package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
  82. package/dist/mcp/core/cgdb-adapter.js +5 -0
  83. package/dist/mcp/core/embedder.js +6 -3
  84. package/dist/mcp/local/local-backend.d.ts +14 -2
  85. package/dist/mcp/local/local-backend.js +396 -18
  86. package/dist/mcp/resources.js +139 -0
  87. package/dist/mcp/server.js +3 -3
  88. package/dist/mcp/tools.js +175 -3
  89. package/dist/server/analyze-worker.js +2 -2
  90. package/dist/server/api.js +147 -31
  91. package/dist/storage/repo-manager.d.ts +10 -5
  92. package/dist/storage/repo-manager.js +10 -6
  93. package/dist/types/pipeline.d.ts +2 -0
  94. package/hooks/claude/codragraph-hook.cjs +4 -4
  95. package/package.json +15 -6
  96. package/scripts/build.js +21 -21
  97. package/skills/codragraph-cli.md +17 -1
  98. package/skills/codragraph-guide.md +6 -2
  99. package/skills/codragraph-onboarding.md +2 -2
  100. package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
  101. 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 { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js';
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 ~/.cache/huggingface.
37
- env.cacheDir = process.env.HF_HOME ?? `${process.env.HOME}/.cache/huggingface`;
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/lbug/pool-adapter.js';
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
- lbugPath: string;
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 { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, isWriteQuery, } from '../../core/lbug/pool-adapter.js';
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/lbug/schema.js';
22
- import { decodeContentField } from '../../core/lbug/content-read.js';
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 closeLbug();
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 lbugPath = path.join(storagePath, 'lbug');
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 lbug doesn't, warn so the user knows to re-analyze.
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
- lbugPath,
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) && isLbugReady(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 closeLbug(repoId);
445
+ await closeCgdb(repoId);
384
446
  this.initializedRepos.delete(repoId);
385
447
  handle.indexedAt = meta.indexedAt;
386
- await initLbug(repoId, handle.lbugPath);
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 initLbug(repoId, handle.lbugPath);
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
- if ((method === 'impact' || method === 'query' || method === 'context') &&
549
- typeof p.repo === 'string' &&
550
- p.repo.startsWith('@')) {
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 { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
1038
+ const { searchFTSFromCgdb } = await import('../../core/search/bm25-index.js');
952
1039
  let bm25Results;
953
1040
  try {
954
- bm25Results = await searchFTSFromLbug(query, limit, repo.id);
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 (!isLbugReady(repo.id)) {
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 closeLbug(); // close all connections
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