@codragraph/cli 2.1.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 (67) hide show
  1. package/README.md +58 -20
  2. package/dist/_shared/cgdb/schema-constants.d.ts +2 -2
  3. package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -1
  4. package/dist/_shared/cgdb/schema-constants.js +3 -0
  5. package/dist/_shared/cgdb/schema-constants.js.map +1 -1
  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 +1 -0
  13. package/dist/_shared/index.d.ts.map +1 -1
  14. package/dist/_shared/index.js.map +1 -1
  15. package/dist/_shared/pipeline.d.ts +1 -1
  16. package/dist/_shared/pipeline.d.ts.map +1 -1
  17. package/dist/cli/ai-context.js +4 -0
  18. package/dist/cli/analyze.js +27 -24
  19. package/dist/cli/index.js +37 -0
  20. package/dist/cli/setup.js +9 -5
  21. package/dist/cli/tool.d.ts +25 -0
  22. package/dist/cli/tool.js +74 -0
  23. package/dist/config/supported-languages.d.ts +3 -3
  24. package/dist/config/supported-languages.js +3 -3
  25. package/dist/core/cgdb/cgdb-adapter.js +19 -3
  26. package/dist/core/cgdb/csv-generator.js +33 -2
  27. package/dist/core/cgdb/schema.d.ts +2 -1
  28. package/dist/core/cgdb/schema.js +55 -0
  29. package/dist/core/embeddings/embedder.js +4 -2
  30. package/dist/core/graphstore/index.d.ts +1 -1
  31. package/dist/core/graphstore/index.js +1 -1
  32. package/dist/core/group/service.d.ts +16 -0
  33. package/dist/core/group/service.js +360 -0
  34. package/dist/core/ingestion/emit-references.d.ts +1 -1
  35. package/dist/core/ingestion/emit-references.js +1 -1
  36. package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
  37. package/dist/core/ingestion/feature-cluster-processor.js +626 -0
  38. package/dist/core/ingestion/finalize-orchestrator.js +1 -1
  39. package/dist/core/ingestion/model/registration-table.js +1 -0
  40. package/dist/core/ingestion/model/resolve.d.ts +2 -2
  41. package/dist/core/ingestion/model/resolve.js +3 -3
  42. package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
  43. package/dist/core/ingestion/model/semantic-model.js +1 -1
  44. package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
  45. package/dist/core/ingestion/model/symbol-table.js +1 -1
  46. package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
  47. package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
  48. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  49. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  50. package/dist/core/ingestion/pipeline.d.ts +4 -0
  51. package/dist/core/ingestion/pipeline.js +9 -5
  52. package/dist/core/run-analyze.d.ts +1 -0
  53. package/dist/core/run-analyze.js +12 -6
  54. package/dist/mcp/core/embedder.js +5 -2
  55. package/dist/mcp/local/local-backend.d.ts +12 -0
  56. package/dist/mcp/local/local-backend.js +381 -3
  57. package/dist/mcp/resources.js +139 -0
  58. package/dist/mcp/tools.js +174 -2
  59. package/dist/server/api.js +116 -0
  60. package/dist/storage/repo-manager.d.ts +6 -1
  61. package/dist/storage/repo-manager.js +5 -1
  62. package/dist/types/pipeline.d.ts +2 -0
  63. package/package.json +13 -4
  64. package/scripts/build.js +13 -12
  65. package/skills/codragraph-cli.md +17 -1
  66. package/skills/codragraph-guide.md +6 -2
  67. package/skills/codragraph-onboarding.md +2 -2
@@ -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
  }
@@ -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.
@@ -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);
@@ -41,6 +41,12 @@ export function getResourceTemplates() {
41
41
  description: 'All functional areas (Leiden clusters)',
42
42
  mimeType: 'text/yaml',
43
43
  },
44
+ {
45
+ uriTemplate: 'codragraph://repo/{name}/feature-clusters',
46
+ name: 'Repo Feature Clusters',
47
+ description: 'Human-facing product/domain feature areas with members and dependencies',
48
+ mimeType: 'text/yaml',
49
+ },
44
50
  {
45
51
  uriTemplate: 'codragraph://repo/{name}/processes',
46
52
  name: 'Repo Processes',
@@ -59,6 +65,12 @@ export function getResourceTemplates() {
59
65
  description: 'Deep dive into a specific functional area',
60
66
  mimeType: 'text/yaml',
61
67
  },
68
+ {
69
+ uriTemplate: 'codragraph://repo/{name}/feature/{featureName}',
70
+ name: 'Feature Context',
71
+ description: 'Members, line ranges, dependencies, and flows for one feature cluster',
72
+ mimeType: 'text/yaml',
73
+ },
62
74
  {
63
75
  uriTemplate: 'codragraph://repo/{name}/process/{processName}',
64
76
  name: 'Process Trace',
@@ -194,6 +206,14 @@ export function parseResourceUri(uri) {
194
206
  param: rest.replace(/^cluster\//, ''),
195
207
  };
196
208
  }
209
+ if (rest.startsWith('feature/')) {
210
+ return {
211
+ kind: 'repo',
212
+ repoName,
213
+ resourceType: 'feature',
214
+ param: rest.replace(/^feature\//, ''),
215
+ };
216
+ }
197
217
  if (rest.startsWith('process/')) {
198
218
  return {
199
219
  kind: 'repo',
@@ -229,12 +249,16 @@ export async function readResource(uri, backend) {
229
249
  return getContextResource(backend, repoName);
230
250
  case 'clusters':
231
251
  return getClustersResource(backend, repoName);
252
+ case 'feature-clusters':
253
+ return getFeatureClustersResource(backend, repoName);
232
254
  case 'processes':
233
255
  return getProcessesResource(backend, repoName);
234
256
  case 'schema':
235
257
  return getSchemaResource();
236
258
  case 'cluster':
237
259
  return getClusterDetailResource(parsed.param, backend, repoName);
260
+ case 'feature':
261
+ return getFeatureDetailResource(parsed.param, backend, repoName);
238
262
  case 'process':
239
263
  return getProcessDetailResource(parsed.param, backend, repoName);
240
264
  case 'graphstore/log':
@@ -308,6 +332,7 @@ async function getContextResource(backend, repoName) {
308
332
  lines.push(` files: ${context.stats.fileCount}`);
309
333
  lines.push(` symbols: ${context.stats.functionCount}`);
310
334
  lines.push(` processes: ${context.stats.processCount}`);
335
+ lines.push(` feature_clusters: ${repo.stats?.featureClusters || 0}`);
311
336
  lines.push('');
312
337
  lines.push('tools_available:');
313
338
  lines.push(' - query: Process-grouped code intelligence (execution flows related to a concept)');
@@ -323,8 +348,10 @@ async function getContextResource(backend, repoName) {
323
348
  lines.push('resources_available:');
324
349
  lines.push(' - codragraph://repos: All indexed repositories');
325
350
  lines.push(` - codragraph://repo/${context.projectName}/clusters: All functional areas`);
351
+ lines.push(` - codragraph://repo/${context.projectName}/feature-clusters: Human-facing feature areas`);
326
352
  lines.push(` - codragraph://repo/${context.projectName}/processes: All execution flows`);
327
353
  lines.push(` - codragraph://repo/${context.projectName}/cluster/{name}: Module details`);
354
+ lines.push(` - codragraph://repo/${context.projectName}/feature/{name}: Feature context pack`);
328
355
  lines.push(` - codragraph://repo/${context.projectName}/process/{name}: Process trace`);
329
356
  lines.push(' - codragraph://group/{name}/contracts: Group contract registry (optional ?type=&repo=&unmatchedOnly=)');
330
357
  lines.push(' - codragraph://group/{name}/status: Group index / contract staleness');
@@ -362,6 +389,39 @@ async function getClustersResource(backend, repoName) {
362
389
  /**
363
390
  * Processes resource — queries graph directly via backend.queryProcesses()
364
391
  */
392
+ /**
393
+ * Feature clusters resource - human-facing product/domain areas.
394
+ */
395
+ async function getFeatureClustersResource(backend, repoName) {
396
+ try {
397
+ const result = await backend.queryFeatureClusters(repoName, 100);
398
+ if (!result.clusters || result.clusters.length === 0) {
399
+ return 'feature_clusters: []\n# No feature clusters detected. Run: codragraph analyze';
400
+ }
401
+ const displayLimit = 30;
402
+ const lines = ['feature_clusters:'];
403
+ for (const cluster of result.clusters.slice(0, displayLimit)) {
404
+ lines.push(` - name: "${cluster.name || cluster.slug || cluster.id}"`);
405
+ lines.push(` slug: "${cluster.slug || ''}"`);
406
+ lines.push(` kind: ${cluster.featureKind || 'feature'}`);
407
+ if (cluster.summary)
408
+ lines.push(` summary: "${cluster.summary}"`);
409
+ lines.push(` members: ${cluster.memberCount || 0}`);
410
+ if (cluster.routes?.length)
411
+ lines.push(` routes: ${cluster.routes.length}`);
412
+ if (cluster.tools?.length)
413
+ lines.push(` tools: ${cluster.tools.length}`);
414
+ lines.push(` confidence: ${Math.round((cluster.confidence || 0) * 100)}%`);
415
+ }
416
+ if (result.clusters.length > displayLimit) {
417
+ lines.push(`\n# Showing top ${displayLimit} of ${result.clusters.length} feature clusters. Use feature_context for details.`);
418
+ }
419
+ return lines.join('\n');
420
+ }
421
+ catch (err) {
422
+ return `error: ${err.message}`;
423
+ }
424
+ }
365
425
  async function getProcessesResource(backend, repoName) {
366
426
  try {
367
427
  const result = await backend.queryProcesses(repoName, 50);
@@ -402,6 +462,7 @@ nodes:
402
462
  - CodeElement: Catch-all for other code elements
403
463
  - Community: Auto-detected functional area (Leiden algorithm)
404
464
  - Process: Execution flow trace
465
+ - FeatureCluster: Human-facing feature/domain cluster for targeted context
405
466
 
406
467
  additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
407
468
 
@@ -411,6 +472,7 @@ node_properties:
411
472
  Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL), visibility (STRING), isStatic (BOOL), isAbstract (BOOL), isFinal (BOOL), isAsync (BOOL), parameterTypes (STRING[]), annotations (STRING[])"
412
473
  Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
413
474
  Constructor: "parameterCount (INT32), visibility (STRING), isStatic (BOOL), parameterTypes (STRING[])"
475
+ FeatureCluster: "name (STRING), slug (STRING), featureKind (STRING), summary (STRING), repo (STRING), service (STRING), memberCount (INT32), entryPointIds (STRING[]), routes (STRING[]), tools (STRING[]), testCoverageHints (STRING[]), lastIndexedCommit (STRING), confidence (DOUBLE), signals (STRING[])"
414
476
  Community: "heuristicLabel (STRING), cohesion (DOUBLE), symbolCount (INT32), keywords (STRING[]), description (STRING), enrichedBy (STRING)"
415
477
  Process: "heuristicLabel (STRING), processType (STRING — 'intra_community' or 'cross_community'), stepCount (INT32), communities (STRING[]), entryPointId (STRING), terminalId (STRING)"
416
478
 
@@ -428,6 +490,10 @@ relationships:
428
490
  - METHOD_IMPLEMENTS: ConcreteMethod implements InterfaceMethod (matched by name + parameterTypes)
429
491
  - MEMBER_OF: Symbol belongs to community
430
492
  - STEP_IN_PROCESS: Symbol is step N in process
493
+ - WRAPS: Wrapper/decorator relationship
494
+ - QUERIES: Data/query relationship
495
+ - FEATURE_MEMBER_OF: Symbol/file belongs to a FeatureCluster
496
+ - FEATURE_DEPENDS_ON: FeatureCluster depends on another FeatureCluster via member edges
431
497
 
432
498
  relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
433
499
 
@@ -487,6 +553,66 @@ async function getClusterDetailResource(name, backend, repoName) {
487
553
  /**
488
554
  * Process detail resource — queries graph directly via backend.queryProcessDetail()
489
555
  */
556
+ async function getFeatureDetailResource(name, backend, repoName) {
557
+ try {
558
+ const result = await backend.queryFeatureContext(name, repoName);
559
+ if (result.error) {
560
+ return `error: ${result.error}`;
561
+ }
562
+ const cluster = result.cluster;
563
+ const members = result.members || [];
564
+ const outgoing = result.dependencies?.outgoing || [];
565
+ const incoming = result.dependencies?.incoming || [];
566
+ const processes = result.processes || [];
567
+ const lines = [
568
+ `feature: "${cluster.name || cluster.slug || cluster.id}"`,
569
+ `slug: "${cluster.slug || ''}"`,
570
+ `kind: ${cluster.featureKind || 'feature'}`,
571
+ `members: ${cluster.memberCount || members.length}`,
572
+ `confidence: ${Math.round((cluster.confidence || 0) * 100)}%`,
573
+ ];
574
+ if (members.length > 0) {
575
+ lines.push('');
576
+ lines.push('members:');
577
+ for (const member of members.slice(0, 30)) {
578
+ lines.push(` - name: ${member.name}`);
579
+ lines.push(` type: ${member.type}`);
580
+ lines.push(` file: ${member.filePath || ''}`);
581
+ if (member.startLine !== undefined)
582
+ lines.push(` startLine: ${member.startLine}`);
583
+ if (member.endLine !== undefined)
584
+ lines.push(` endLine: ${member.endLine}`);
585
+ }
586
+ }
587
+ if (outgoing.length > 0 || incoming.length > 0) {
588
+ lines.push('');
589
+ lines.push('dependencies:');
590
+ if (outgoing.length > 0) {
591
+ lines.push(' outgoing:');
592
+ for (const dep of outgoing.slice(0, 15)) {
593
+ lines.push(` - ${dep.name || dep.slug || dep.id}`);
594
+ }
595
+ }
596
+ if (incoming.length > 0) {
597
+ lines.push(' incoming:');
598
+ for (const dep of incoming.slice(0, 15)) {
599
+ lines.push(` - ${dep.name || dep.slug || dep.id}`);
600
+ }
601
+ }
602
+ }
603
+ if (processes.length > 0) {
604
+ lines.push('');
605
+ lines.push('processes:');
606
+ for (const proc of processes.slice(0, 10)) {
607
+ lines.push(` - ${proc.heuristicLabel || proc.label || proc.id}`);
608
+ }
609
+ }
610
+ return lines.join('\n');
611
+ }
612
+ catch (err) {
613
+ return `error: ${err.message}`;
614
+ }
615
+ }
490
616
  async function getProcessDetailResource(name, backend, repoName) {
491
617
  try {
492
618
  const result = await backend.queryProcessDetail(name, repoName);
@@ -539,6 +665,12 @@ async function getSetupResource(backend) {
539
665
  '| `impact` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |',
540
666
  '| `detect_changes` | Git-diff impact — what do your current changes affect |',
541
667
  '| `rename` | Multi-file coordinated rename with confidence-tagged edits |',
668
+ '| `feature_clusters` | Product/domain feature map for targeted context |',
669
+ '| `feature_context` | Members, line ranges, dependencies, and flows for one feature |',
670
+ '| `cluster_query` | Cluster-first alias for `feature_clusters` |',
671
+ '| `cluster_context` | Cluster-first alias for `feature_context` |',
672
+ '| `context_pack` | Compact context pack for one feature cluster |',
673
+ '| `cluster_impact` | Feature-level blast radius across cluster dependencies |',
542
674
  '| `cypher` | Raw graph queries |',
543
675
  '| `list_repos` | Discover indexed repos |',
544
676
  '',
@@ -546,8 +678,15 @@ async function getSetupResource(backend) {
546
678
  '',
547
679
  `- \`codragraph://repo/${repo.name}/context\` — Stats, staleness check`,
548
680
  `- \`codragraph://repo/${repo.name}/clusters\` — All functional areas`,
681
+ `- \`codragraph://repo/${repo.name}/feature-clusters\` — Human-facing feature areas`,
682
+ `- \`codragraph://repo/${repo.name}/feature/{name}\` — Feature context pack`,
549
683
  `- \`codragraph://repo/${repo.name}/processes\` — All execution flows`,
550
684
  `- \`codragraph://repo/${repo.name}/schema\` — Graph schema for Cypher`,
685
+ '',
686
+ '## Cross-platform commands',
687
+ '',
688
+ '- Use `npx @codragraph/cli ...` or `codragraph ...` in Windows PowerShell, macOS bash/zsh, and Linux shells.',
689
+ '- Prefer `npm --prefix <package> <script>` from repo root for package checks instead of shell-specific `cd dir && ...` chains.',
551
690
  ];
552
691
  sections.push(lines.join('\n'));
553
692
  }