@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.
- package/README.md +58 -20
- package/dist/_shared/cgdb/schema-constants.d.ts +2 -2
- package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -1
- package/dist/_shared/cgdb/schema-constants.js +3 -0
- package/dist/_shared/cgdb/schema-constants.js.map +1 -1
- 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 +1 -0
- package/dist/_shared/index.d.ts.map +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 +27 -24
- 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/config/supported-languages.d.ts +3 -3
- package/dist/config/supported-languages.js +3 -3
- package/dist/core/cgdb/cgdb-adapter.js +19 -3
- package/dist/core/cgdb/csv-generator.js +33 -2
- package/dist/core/cgdb/schema.d.ts +2 -1
- package/dist/core/cgdb/schema.js +55 -0
- package/dist/core/embeddings/embedder.js +4 -2
- package/dist/core/graphstore/index.d.ts +1 -1
- package/dist/core/graphstore/index.js +1 -1
- package/dist/core/group/service.d.ts +16 -0
- package/dist/core/group/service.js +360 -0
- 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 +12 -6
- package/dist/mcp/core/embedder.js +5 -2
- package/dist/mcp/local/local-backend.d.ts +12 -0
- package/dist/mcp/local/local-backend.js +381 -3
- package/dist/mcp/resources.js +139 -0
- package/dist/mcp/tools.js +174 -2
- package/dist/server/api.js +116 -0
- package/dist/storage/repo-manager.d.ts +6 -1
- package/dist/storage/repo-manager.js +5 -1
- package/dist/types/pipeline.d.ts +2 -0
- package/package.json +13 -4
- package/scripts/build.js +13 -12
- package/skills/codragraph-cli.md +17 -1
- package/skills/codragraph-guide.md +6 -2
- 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
|
-
|
|
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.
|
|
@@ -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);
|
package/dist/mcp/resources.js
CHANGED
|
@@ -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
|
}
|