@codragraph/cli 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -22
- package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
- package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
- package/dist/_shared/cgdb/schema-constants.js +70 -0
- package/dist/_shared/cgdb/schema-constants.js.map +1 -0
- package/dist/_shared/feature-clusters.d.ts +99 -0
- package/dist/_shared/feature-clusters.d.ts.map +1 -0
- package/dist/_shared/feature-clusters.js +2 -0
- package/dist/_shared/feature-clusters.js.map +1 -0
- package/dist/_shared/graph/types.d.ts +16 -2
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +3 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/pipeline.d.ts +1 -1
- package/dist/_shared/pipeline.d.ts.map +1 -1
- package/dist/cli/ai-context.js +4 -0
- package/dist/cli/analyze.js +30 -27
- package/dist/cli/graphstore.js +21 -21
- package/dist/cli/index-repo.js +3 -3
- package/dist/cli/index.js +37 -0
- package/dist/cli/setup.js +9 -5
- package/dist/cli/tool.d.ts +25 -0
- package/dist/cli/tool.js +74 -0
- package/dist/cli/wiki.js +3 -3
- package/dist/config/supported-languages.d.ts +3 -3
- package/dist/config/supported-languages.js +3 -3
- package/dist/core/augmentation/engine.js +7 -7
- package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
- package/dist/core/cgdb/cgdb-adapter.js +1336 -0
- package/dist/core/cgdb/content-read.d.ts +46 -0
- package/dist/core/cgdb/content-read.js +64 -0
- package/dist/core/cgdb/csv-generator.d.ts +29 -0
- package/dist/core/cgdb/csv-generator.js +523 -0
- package/dist/core/cgdb/pool-adapter.d.ts +93 -0
- package/dist/core/cgdb/pool-adapter.js +550 -0
- package/dist/core/cgdb/schema.d.ts +63 -0
- package/dist/core/cgdb/schema.js +557 -0
- package/dist/core/embeddings/embedder.js +4 -2
- package/dist/core/embeddings/embedding-pipeline.js +4 -4
- package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
- package/dist/core/graphstore/cgdb-row-source.js +141 -0
- package/dist/core/graphstore/index.d.ts +2 -2
- package/dist/core/graphstore/index.js +4 -4
- package/dist/core/group/bridge-db.d.ts +2 -2
- package/dist/core/group/bridge-db.js +18 -18
- package/dist/core/group/bridge-schema.d.ts +4 -4
- package/dist/core/group/bridge-schema.js +4 -4
- package/dist/core/group/cross-impact.js +3 -3
- package/dist/core/group/service.d.ts +16 -0
- package/dist/core/group/service.js +360 -0
- package/dist/core/group/sync.js +4 -4
- package/dist/core/ingestion/emit-references.d.ts +1 -1
- package/dist/core/ingestion/emit-references.js +1 -1
- package/dist/core/ingestion/feature-cluster-processor.d.ts +62 -0
- package/dist/core/ingestion/feature-cluster-processor.js +626 -0
- package/dist/core/ingestion/finalize-orchestrator.js +1 -1
- package/dist/core/ingestion/model/registration-table.js +1 -0
- package/dist/core/ingestion/model/resolve.d.ts +2 -2
- package/dist/core/ingestion/model/resolve.js +3 -3
- package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
- package/dist/core/ingestion/model/semantic-model.js +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/model/symbol-table.js +1 -1
- package/dist/core/ingestion/pipeline-phases/feature-clusters.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/feature-clusters.js +88 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline.d.ts +4 -0
- package/dist/core/ingestion/pipeline.js +9 -5
- package/dist/core/run-analyze.d.ts +1 -0
- package/dist/core/run-analyze.js +36 -30
- package/dist/core/search/bm25-index.d.ts +3 -3
- package/dist/core/search/bm25-index.js +9 -9
- package/dist/core/search/hybrid-search.js +2 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +2 -2
- package/dist/core/wiki/graph-queries.js +5 -5
- package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
- package/dist/mcp/core/cgdb-adapter.js +5 -0
- package/dist/mcp/core/embedder.js +6 -3
- package/dist/mcp/local/local-backend.d.ts +14 -2
- package/dist/mcp/local/local-backend.js +396 -18
- package/dist/mcp/resources.js +139 -0
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +175 -3
- package/dist/server/analyze-worker.js +2 -2
- package/dist/server/api.js +147 -31
- package/dist/storage/repo-manager.d.ts +10 -5
- package/dist/storage/repo-manager.js +10 -6
- package/dist/types/pipeline.d.ts +2 -0
- package/hooks/claude/codragraph-hook.cjs +4 -4
- package/package.json +15 -6
- package/scripts/build.js +21 -21
- package/skills/codragraph-cli.md +17 -1
- package/skills/codragraph-guide.md +6 -2
- package/skills/codragraph-onboarding.md +2 -2
- package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
- package/vendor/tree-sitter-proto/src/node-types.json +1 -1
|
@@ -36,6 +36,116 @@ function filterQueryByServicePrefix(queryResult, servicePrefix) {
|
|
|
36
36
|
const processes = (queryResult.processes || []).filter((p) => allowed.has(String(p.id)));
|
|
37
37
|
return { processes, process_symbols: symbols };
|
|
38
38
|
}
|
|
39
|
+
function normalizeClusterToken(value) {
|
|
40
|
+
return String(value ?? '')
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
45
|
+
.replace(/^-+|-+$/g, '');
|
|
46
|
+
}
|
|
47
|
+
function featureClusterKey(cluster) {
|
|
48
|
+
return normalizeClusterToken(cluster.slug || cluster.name || cluster.id || 'unknown');
|
|
49
|
+
}
|
|
50
|
+
function normalizeStringList(value) {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return value
|
|
53
|
+
.filter((item) => item !== null && item !== undefined)
|
|
54
|
+
.map((item) => String(item).trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
58
|
+
return [];
|
|
59
|
+
return value
|
|
60
|
+
.replace(/^\[|\]$/g, '')
|
|
61
|
+
.split(/,(?=(?:[^']*'[^']*')*[^']*$)/)
|
|
62
|
+
.map((item) => item
|
|
63
|
+
.trim()
|
|
64
|
+
.replace(/^['"]|['"]$/g, '')
|
|
65
|
+
.replace(/\\,/g, ','))
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
function clusterOwnsEndpoint(cluster, endpoint) {
|
|
69
|
+
const entryPointIds = normalizeStringList(cluster.entryPointIds);
|
|
70
|
+
if (entryPointIds.includes(endpoint.symbolUid))
|
|
71
|
+
return true;
|
|
72
|
+
const key = featureClusterKey(cluster);
|
|
73
|
+
if (!key || key === 'unknown')
|
|
74
|
+
return false;
|
|
75
|
+
const filePath = normalizeClusterToken(endpoint.symbolRef.filePath);
|
|
76
|
+
const symbolName = normalizeClusterToken(endpoint.symbolRef.name);
|
|
77
|
+
return filePath.includes(key) || symbolName.includes(key);
|
|
78
|
+
}
|
|
79
|
+
function resolveEndpointCluster(endpoint, clusters) {
|
|
80
|
+
const sameRepo = clusters.filter((cluster) => cluster.repoPath === endpoint.repo);
|
|
81
|
+
return sameRepo.find((cluster) => clusterOwnsEndpoint(cluster, endpoint));
|
|
82
|
+
}
|
|
83
|
+
function buildCrossRepoClusterLinks(registry, clusters) {
|
|
84
|
+
if (!registry)
|
|
85
|
+
return [];
|
|
86
|
+
const links = [];
|
|
87
|
+
for (const link of registry.crossLinks) {
|
|
88
|
+
const sourceCluster = resolveEndpointCluster(link.from, clusters);
|
|
89
|
+
const targetCluster = resolveEndpointCluster(link.to, clusters);
|
|
90
|
+
if (!sourceCluster || !targetCluster)
|
|
91
|
+
continue;
|
|
92
|
+
links.push({
|
|
93
|
+
sourceRepo: link.from.repo,
|
|
94
|
+
sourceService: link.from.service,
|
|
95
|
+
sourceClusterId: sourceCluster.id,
|
|
96
|
+
sourceClusterName: String(sourceCluster.name || sourceCluster.slug || sourceCluster.id || ''),
|
|
97
|
+
targetRepo: link.to.repo,
|
|
98
|
+
targetService: link.to.service,
|
|
99
|
+
targetClusterId: targetCluster.id,
|
|
100
|
+
targetClusterName: String(targetCluster.name || targetCluster.slug || targetCluster.id || ''),
|
|
101
|
+
contractName: link.contractId,
|
|
102
|
+
relationship: 'shared-contract',
|
|
103
|
+
confidence: link.confidence,
|
|
104
|
+
evidence: [
|
|
105
|
+
`${link.type}:${link.contractId}`,
|
|
106
|
+
`${link.from.symbolRef.filePath} -> ${link.to.symbolRef.filePath}`,
|
|
107
|
+
`match:${link.matchType}`,
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return links;
|
|
112
|
+
}
|
|
113
|
+
function aggregateCrossRepoFeatureClusters(clusters, links) {
|
|
114
|
+
const byKey = new Map();
|
|
115
|
+
for (const cluster of clusters) {
|
|
116
|
+
const key = featureClusterKey(cluster);
|
|
117
|
+
const list = byKey.get(key) ?? [];
|
|
118
|
+
list.push(cluster);
|
|
119
|
+
byKey.set(key, list);
|
|
120
|
+
}
|
|
121
|
+
return [...byKey.entries()]
|
|
122
|
+
.map(([key, group]) => {
|
|
123
|
+
const clusterIds = new Set(group.map((cluster) => String(cluster.id || '')).filter(Boolean));
|
|
124
|
+
const crossRepoLinks = links.filter((link) => (link.sourceClusterId && clusterIds.has(link.sourceClusterId)) ||
|
|
125
|
+
(link.targetClusterId && clusterIds.has(link.targetClusterId)));
|
|
126
|
+
const routes = new Set();
|
|
127
|
+
const tools = new Set();
|
|
128
|
+
for (const cluster of group) {
|
|
129
|
+
normalizeStringList(cluster.routes).forEach((route) => routes.add(route));
|
|
130
|
+
normalizeStringList(cluster.tools).forEach((tool) => tools.add(tool));
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
key,
|
|
134
|
+
name: String(group[0]?.name || group[0]?.slug || key),
|
|
135
|
+
repos: group.map((cluster) => ({
|
|
136
|
+
repoPath: cluster.repoPath,
|
|
137
|
+
registryName: cluster.registryName,
|
|
138
|
+
cluster,
|
|
139
|
+
})),
|
|
140
|
+
repoCount: new Set(group.map((cluster) => cluster.repoPath)).size,
|
|
141
|
+
memberCount: group.reduce((sum, cluster) => sum + Number(cluster.memberCount ?? 0), 0),
|
|
142
|
+
routes: [...routes].sort(),
|
|
143
|
+
tools: [...tools].sort(),
|
|
144
|
+
crossRepoLinks,
|
|
145
|
+
};
|
|
146
|
+
})
|
|
147
|
+
.sort((a, b) => Number(b.memberCount) - Number(a.memberCount));
|
|
148
|
+
}
|
|
39
149
|
function isCrossLink(raw) {
|
|
40
150
|
if (!raw || typeof raw !== 'object')
|
|
41
151
|
return false;
|
|
@@ -346,6 +456,256 @@ export class GroupService {
|
|
|
346
456
|
per_repo: perRepo.map((r) => ({ repo: r.repo, count: r.processes.length })),
|
|
347
457
|
};
|
|
348
458
|
}
|
|
459
|
+
async groupFeatureClusters(params) {
|
|
460
|
+
const name = String(params.name ?? '').trim();
|
|
461
|
+
if (!name)
|
|
462
|
+
return { error: 'name is required' };
|
|
463
|
+
const featureClusters = this.port.featureClusters;
|
|
464
|
+
if (!featureClusters)
|
|
465
|
+
return { error: 'feature cluster query is unavailable' };
|
|
466
|
+
const query = typeof params.query === 'string' ? params.query.trim() : '';
|
|
467
|
+
const limit = typeof params.limit === 'number' && params.limit > 0 ? params.limit : 100;
|
|
468
|
+
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
469
|
+
const subgroupExact = params.subgroupExact === true;
|
|
470
|
+
const groupDir = getGroupDir(getDefaultCodragraphDir(), name);
|
|
471
|
+
let config;
|
|
472
|
+
try {
|
|
473
|
+
config = await loadGroupConfig(groupDir);
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
if (err instanceof GroupNotFoundError)
|
|
477
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
478
|
+
throw err;
|
|
479
|
+
}
|
|
480
|
+
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
481
|
+
const registryResult = await loadContractRegistryResilient(groupDir);
|
|
482
|
+
const registry = registryResult.ok ? registryResult.registry : null;
|
|
483
|
+
const perRepo = await Promise.all(memberEntries.map(async ([repoPath, registryName]) => {
|
|
484
|
+
try {
|
|
485
|
+
const repoObj = await this.port.resolveRepo(registryName);
|
|
486
|
+
const payload = (await featureClusters(repoObj, {
|
|
487
|
+
query,
|
|
488
|
+
limit,
|
|
489
|
+
}));
|
|
490
|
+
const clusters = (payload.clusters ?? []).map((cluster) => ({
|
|
491
|
+
...cluster,
|
|
492
|
+
repoPath,
|
|
493
|
+
registryName,
|
|
494
|
+
}));
|
|
495
|
+
return { repo: repoPath, registryName, clusters };
|
|
496
|
+
}
|
|
497
|
+
catch (e) {
|
|
498
|
+
return {
|
|
499
|
+
repo: repoPath,
|
|
500
|
+
registryName,
|
|
501
|
+
clusters: [],
|
|
502
|
+
error: e instanceof Error ? e.message : String(e),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}));
|
|
506
|
+
const clusters = perRepo
|
|
507
|
+
.flatMap((entry) => entry.clusters)
|
|
508
|
+
.sort((a, b) => Number(b.memberCount ?? 0) - Number(a.memberCount ?? 0))
|
|
509
|
+
.slice(0, limit);
|
|
510
|
+
const allClusters = perRepo.flatMap((entry) => entry.clusters);
|
|
511
|
+
const crossRepoLinks = buildCrossRepoClusterLinks(registry, allClusters);
|
|
512
|
+
const crossRepoClusters = aggregateCrossRepoFeatureClusters(allClusters, crossRepoLinks);
|
|
513
|
+
return {
|
|
514
|
+
group: name,
|
|
515
|
+
query,
|
|
516
|
+
clusters,
|
|
517
|
+
cross_repo_clusters: crossRepoClusters,
|
|
518
|
+
cross_repo_links: crossRepoLinks,
|
|
519
|
+
...(registryResult.ok === true && registryResult.skippedCorrupt > 0
|
|
520
|
+
? { skippedCorruptContracts: registryResult.skippedCorrupt }
|
|
521
|
+
: {}),
|
|
522
|
+
per_repo: perRepo.map((entry) => ({
|
|
523
|
+
repo: entry.repo,
|
|
524
|
+
registryName: entry.registryName,
|
|
525
|
+
count: entry.clusters.length,
|
|
526
|
+
...(entry.error ? { error: entry.error } : {}),
|
|
527
|
+
})),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async groupFeatureContext(params) {
|
|
531
|
+
const name = String(params.name ?? '').trim();
|
|
532
|
+
const clusterName = String(params.cluster ?? params.target ?? params.feature ?? '').trim();
|
|
533
|
+
if (!name || !clusterName)
|
|
534
|
+
return { error: 'name and cluster are required' };
|
|
535
|
+
const featureContext = this.port.featureContext;
|
|
536
|
+
if (!featureContext)
|
|
537
|
+
return { error: 'feature cluster context is unavailable' };
|
|
538
|
+
const limit = typeof params.limit === 'number' && params.limit > 0 ? params.limit : 100;
|
|
539
|
+
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
540
|
+
const subgroupExact = params.subgroupExact === true;
|
|
541
|
+
const groupDir = getGroupDir(getDefaultCodragraphDir(), name);
|
|
542
|
+
let config;
|
|
543
|
+
try {
|
|
544
|
+
config = await loadGroupConfig(groupDir);
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
if (err instanceof GroupNotFoundError)
|
|
548
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
552
|
+
const registryResult = await loadContractRegistryResilient(groupDir);
|
|
553
|
+
const registry = registryResult.ok ? registryResult.registry : null;
|
|
554
|
+
const results = await Promise.all(memberEntries.map(async ([repoPath, registryName]) => {
|
|
555
|
+
try {
|
|
556
|
+
const repoObj = await this.port.resolveRepo(registryName);
|
|
557
|
+
const payload = await featureContext(repoObj, {
|
|
558
|
+
name: clusterName,
|
|
559
|
+
limit,
|
|
560
|
+
});
|
|
561
|
+
return { repoPath, registryName, payload };
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
return {
|
|
565
|
+
repoPath,
|
|
566
|
+
registryName,
|
|
567
|
+
payload: { error: e instanceof Error ? e.message : String(e) },
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}));
|
|
571
|
+
const contexts = results.filter((result) => !result.payload?.error);
|
|
572
|
+
const memberIdsByRepo = new Map();
|
|
573
|
+
for (const result of contexts) {
|
|
574
|
+
const payload = result.payload;
|
|
575
|
+
memberIdsByRepo.set(result.repoPath, new Set((payload.members ?? []).map((member) => String(member.id || '')).filter(Boolean)));
|
|
576
|
+
}
|
|
577
|
+
const crossRepoLinks = (registry?.crossLinks ?? [])
|
|
578
|
+
.filter((link) => {
|
|
579
|
+
const fromIds = memberIdsByRepo.get(link.from.repo);
|
|
580
|
+
const toIds = memberIdsByRepo.get(link.to.repo);
|
|
581
|
+
return fromIds?.has(link.from.symbolUid) || toIds?.has(link.to.symbolUid);
|
|
582
|
+
})
|
|
583
|
+
.map((link) => ({
|
|
584
|
+
sourceRepo: link.from.repo,
|
|
585
|
+
sourceService: link.from.service,
|
|
586
|
+
targetRepo: link.to.repo,
|
|
587
|
+
targetService: link.to.service,
|
|
588
|
+
contractName: link.contractId,
|
|
589
|
+
relationship: 'shared-contract',
|
|
590
|
+
confidence: link.confidence,
|
|
591
|
+
evidence: [
|
|
592
|
+
`${link.type}:${link.contractId}`,
|
|
593
|
+
`${link.from.symbolRef.filePath} -> ${link.to.symbolRef.filePath}`,
|
|
594
|
+
`match:${link.matchType}`,
|
|
595
|
+
],
|
|
596
|
+
}));
|
|
597
|
+
return {
|
|
598
|
+
group: name,
|
|
599
|
+
cluster: clusterName,
|
|
600
|
+
results: contexts,
|
|
601
|
+
cross_repo_links: crossRepoLinks,
|
|
602
|
+
...(registryResult.ok === true && registryResult.skippedCorrupt > 0
|
|
603
|
+
? { skippedCorruptContracts: registryResult.skippedCorrupt }
|
|
604
|
+
: {}),
|
|
605
|
+
errors: results
|
|
606
|
+
.filter((result) => result.payload?.error)
|
|
607
|
+
.map((result) => ({
|
|
608
|
+
repoPath: result.repoPath,
|
|
609
|
+
registryName: result.registryName,
|
|
610
|
+
error: result.payload.error,
|
|
611
|
+
})),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async groupFeatureImpact(params) {
|
|
615
|
+
const name = String(params.name ?? '').trim();
|
|
616
|
+
const clusterName = String(params.cluster ?? params.target ?? params.feature ?? '').trim();
|
|
617
|
+
if (!name || !clusterName)
|
|
618
|
+
return { error: 'name and cluster are required' };
|
|
619
|
+
const featureImpact = this.port.featureImpact;
|
|
620
|
+
if (!featureImpact)
|
|
621
|
+
return { error: 'feature cluster impact is unavailable' };
|
|
622
|
+
const direction = params.direction === 'downstream' || params.direction === 'both'
|
|
623
|
+
? params.direction
|
|
624
|
+
: 'upstream';
|
|
625
|
+
const limit = typeof params.limit === 'number' && params.limit > 0 ? params.limit : 100;
|
|
626
|
+
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
627
|
+
const subgroupExact = params.subgroupExact === true;
|
|
628
|
+
const groupDir = getGroupDir(getDefaultCodragraphDir(), name);
|
|
629
|
+
let config;
|
|
630
|
+
try {
|
|
631
|
+
config = await loadGroupConfig(groupDir);
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
if (err instanceof GroupNotFoundError)
|
|
635
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
636
|
+
throw err;
|
|
637
|
+
}
|
|
638
|
+
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
639
|
+
const registryResult = await loadContractRegistryResilient(groupDir);
|
|
640
|
+
const registry = registryResult.ok ? registryResult.registry : null;
|
|
641
|
+
const results = await Promise.all(memberEntries.map(async ([repoPath, registryName]) => {
|
|
642
|
+
try {
|
|
643
|
+
const repoObj = await this.port.resolveRepo(registryName);
|
|
644
|
+
const payload = await featureImpact(repoObj, {
|
|
645
|
+
name: clusterName,
|
|
646
|
+
direction,
|
|
647
|
+
limit,
|
|
648
|
+
});
|
|
649
|
+
return { repoPath, registryName, payload };
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
return {
|
|
653
|
+
repoPath,
|
|
654
|
+
registryName,
|
|
655
|
+
payload: { error: e instanceof Error ? e.message : String(e) },
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}));
|
|
659
|
+
const successfulResults = results.filter((result) => !result.payload?.error);
|
|
660
|
+
const memberIdsByRepo = new Map();
|
|
661
|
+
for (const result of successfulResults) {
|
|
662
|
+
const payload = result.payload;
|
|
663
|
+
memberIdsByRepo.set(result.repoPath, new Set((payload.contextPack?.members ?? [])
|
|
664
|
+
.map((member) => String(member.id || ''))
|
|
665
|
+
.filter(Boolean)));
|
|
666
|
+
}
|
|
667
|
+
const crossRepoLinks = (registry?.crossLinks ?? [])
|
|
668
|
+
.filter((link) => {
|
|
669
|
+
const fromIds = memberIdsByRepo.get(link.from.repo);
|
|
670
|
+
const toIds = memberIdsByRepo.get(link.to.repo);
|
|
671
|
+
return fromIds?.has(link.from.symbolUid) || toIds?.has(link.to.symbolUid);
|
|
672
|
+
})
|
|
673
|
+
.map((link) => ({
|
|
674
|
+
sourceRepo: link.from.repo,
|
|
675
|
+
sourceService: link.from.service,
|
|
676
|
+
targetRepo: link.to.repo,
|
|
677
|
+
targetService: link.to.service,
|
|
678
|
+
contractName: link.contractId,
|
|
679
|
+
relationship: 'shared-contract',
|
|
680
|
+
confidence: link.confidence,
|
|
681
|
+
evidence: [
|
|
682
|
+
`${link.type}:${link.contractId}`,
|
|
683
|
+
`${link.from.symbolRef.filePath} -> ${link.to.symbolRef.filePath}`,
|
|
684
|
+
`match:${link.matchType}`,
|
|
685
|
+
],
|
|
686
|
+
}));
|
|
687
|
+
return {
|
|
688
|
+
group: name,
|
|
689
|
+
cluster: clusterName,
|
|
690
|
+
direction,
|
|
691
|
+
results: successfulResults,
|
|
692
|
+
cross_repo_links: crossRepoLinks,
|
|
693
|
+
summary: {
|
|
694
|
+
repos: successfulResults.length,
|
|
695
|
+
crossRepoLinks: crossRepoLinks.length,
|
|
696
|
+
},
|
|
697
|
+
...(registryResult.ok === true && registryResult.skippedCorrupt > 0
|
|
698
|
+
? { skippedCorruptContracts: registryResult.skippedCorrupt }
|
|
699
|
+
: {}),
|
|
700
|
+
errors: results
|
|
701
|
+
.filter((result) => result.payload?.error)
|
|
702
|
+
.map((result) => ({
|
|
703
|
+
repoPath: result.repoPath,
|
|
704
|
+
registryName: result.registryName,
|
|
705
|
+
error: result.payload.error,
|
|
706
|
+
})),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
349
709
|
async groupStatus(params) {
|
|
350
710
|
const name = String(params.name ?? '').trim();
|
|
351
711
|
if (!name)
|
package/dist/core/group/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Buffer } from 'node:buffer';
|
|
4
|
-
import {
|
|
4
|
+
import { initCgdb, closeCgdb, executeParameterized } from '../cgdb/pool-adapter.js';
|
|
5
5
|
import { readRegistry } from '../../storage/repo-manager.js';
|
|
6
6
|
import { HttpRouteExtractor } from './extractors/http-route-extractor.js';
|
|
7
7
|
import { GrpcExtractor } from './extractors/grpc-extractor.js';
|
|
@@ -78,9 +78,9 @@ export async function syncGroup(config, opts) {
|
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
const poolId = handle.id;
|
|
81
|
-
const
|
|
81
|
+
const cgdbPath = path.join(handle.storagePath, 'cgdb');
|
|
82
82
|
try {
|
|
83
|
-
await
|
|
83
|
+
await initCgdb(poolId, cgdbPath);
|
|
84
84
|
openPoolIds.push(poolId);
|
|
85
85
|
const executor = (query, params) => executeParameterized(poolId, query, params ?? {});
|
|
86
86
|
dbExecutors.set(groupPath, executor);
|
|
@@ -139,7 +139,7 @@ export async function syncGroup(config, opts) {
|
|
|
139
139
|
}
|
|
140
140
|
finally {
|
|
141
141
|
for (const id of [...new Set(openPoolIds)]) {
|
|
142
|
-
await
|
|
142
|
+
await closeCgdb(id).catch(() => { });
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - `confidence`: the pre-computed confidence from the Reference record.
|
|
16
16
|
* - `reason`: human-readable summary (`"scope-resolution: call | confidence 0.75"`).
|
|
17
17
|
* - `evidence`: the full `ResolutionEvidence[]` trace — additive graph
|
|
18
|
-
* property (see `GraphRelationship.evidence` in codragraph
|
|
18
|
+
* property (see `GraphRelationship.evidence` in @codragraph/shared),
|
|
19
19
|
* so queries that don't know about it are unaffected.
|
|
20
20
|
* - `step`: carries the reference's access-kind discriminant when
|
|
21
21
|
* available (`1` for read, `2` for write) so `ACCESSES` edges retain
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - `confidence`: the pre-computed confidence from the Reference record.
|
|
16
16
|
* - `reason`: human-readable summary (`"scope-resolution: call | confidence 0.75"`).
|
|
17
17
|
* - `evidence`: the full `ResolutionEvidence[]` trace — additive graph
|
|
18
|
-
* property (see `GraphRelationship.evidence` in codragraph
|
|
18
|
+
* property (see `GraphRelationship.evidence` in @codragraph/shared),
|
|
19
19
|
* so queries that don't know about it are unaffected.
|
|
20
20
|
* - `step`: carries the reference's access-kind discriminant when
|
|
21
21
|
* available (`1` for read, `2` for write) so `ACCESSES` edges retain
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Cluster Processor
|
|
3
|
+
*
|
|
4
|
+
* Builds a stable, human-facing feature layer over the raw code graph.
|
|
5
|
+
* Communities are graph-algorithm clusters; FeatureCluster nodes are
|
|
6
|
+
* product/domain clusters such as Settings, AI, Auth, Billing, MCP, or
|
|
7
|
+
* Ingestion. Agents can query this layer first to land directly on the
|
|
8
|
+
* files, symbols, and line ranges that matter for a task.
|
|
9
|
+
*/
|
|
10
|
+
import type { FeatureClusterKind } from '../../_shared/index.js';
|
|
11
|
+
import { KnowledgeGraph } from '../graph/types.js';
|
|
12
|
+
export interface FeatureClusterNode {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
featureKind: FeatureClusterKind;
|
|
17
|
+
summary: string;
|
|
18
|
+
description: string;
|
|
19
|
+
repo?: string;
|
|
20
|
+
service?: string;
|
|
21
|
+
signals: string[];
|
|
22
|
+
memberCount: number;
|
|
23
|
+
entryPointIds: string[];
|
|
24
|
+
routes: string[];
|
|
25
|
+
tools: string[];
|
|
26
|
+
testCoverageHints: string[];
|
|
27
|
+
lastIndexedCommit?: string;
|
|
28
|
+
confidence: number;
|
|
29
|
+
memberIds: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface FeatureClusterMembership {
|
|
32
|
+
nodeId: string;
|
|
33
|
+
clusterId: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
signals: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface FeatureClusterDependency {
|
|
38
|
+
sourceClusterId: string;
|
|
39
|
+
targetClusterId: string;
|
|
40
|
+
edgeCount: number;
|
|
41
|
+
relationshipTypes: string[];
|
|
42
|
+
confidence: number;
|
|
43
|
+
}
|
|
44
|
+
export interface FeatureClusterDetectionResult {
|
|
45
|
+
clusters: FeatureClusterNode[];
|
|
46
|
+
memberships: FeatureClusterMembership[];
|
|
47
|
+
dependencies: FeatureClusterDependency[];
|
|
48
|
+
stats: {
|
|
49
|
+
totalClusters: number;
|
|
50
|
+
totalMemberships: number;
|
|
51
|
+
totalDependencies: number;
|
|
52
|
+
nodesProcessed: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export interface FeatureClusterConfig {
|
|
56
|
+
minMembers: number;
|
|
57
|
+
maxClusters: number;
|
|
58
|
+
repo?: string;
|
|
59
|
+
service?: string;
|
|
60
|
+
lastIndexedCommit?: string;
|
|
61
|
+
}
|
|
62
|
+
export declare const processFeatureClusters: (knowledgeGraph: KnowledgeGraph, onProgress?: (message: string, progress: number) => void, config?: Partial<FeatureClusterConfig>) => Promise<FeatureClusterDetectionResult>;
|