@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
|
@@ -0,0 +1,626 @@
|
|
|
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 { generateId } from '../../lib/utils.js';
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
minMembers: 2,
|
|
13
|
+
maxClusters: 250,
|
|
14
|
+
};
|
|
15
|
+
const CLUSTERABLE_LABELS = new Set([
|
|
16
|
+
'File',
|
|
17
|
+
'Function',
|
|
18
|
+
'Class',
|
|
19
|
+
'Interface',
|
|
20
|
+
'Method',
|
|
21
|
+
'CodeElement',
|
|
22
|
+
'Section',
|
|
23
|
+
'Route',
|
|
24
|
+
'Tool',
|
|
25
|
+
'Process',
|
|
26
|
+
'Struct',
|
|
27
|
+
'Enum',
|
|
28
|
+
'Macro',
|
|
29
|
+
'Typedef',
|
|
30
|
+
'Union',
|
|
31
|
+
'Namespace',
|
|
32
|
+
'Trait',
|
|
33
|
+
'Impl',
|
|
34
|
+
'TypeAlias',
|
|
35
|
+
'Const',
|
|
36
|
+
'Static',
|
|
37
|
+
'Variable',
|
|
38
|
+
'Property',
|
|
39
|
+
'Record',
|
|
40
|
+
'Delegate',
|
|
41
|
+
'Annotation',
|
|
42
|
+
'Constructor',
|
|
43
|
+
'Template',
|
|
44
|
+
'Module',
|
|
45
|
+
]);
|
|
46
|
+
const DEPENDENCY_REL_TYPES = new Set([
|
|
47
|
+
'CALLS',
|
|
48
|
+
'IMPORTS',
|
|
49
|
+
'EXTENDS',
|
|
50
|
+
'IMPLEMENTS',
|
|
51
|
+
'METHOD_OVERRIDES',
|
|
52
|
+
'METHOD_IMPLEMENTS',
|
|
53
|
+
'FETCHES',
|
|
54
|
+
'HANDLES_ROUTE',
|
|
55
|
+
'HANDLES_TOOL',
|
|
56
|
+
'ENTRY_POINT_OF',
|
|
57
|
+
'WRAPS',
|
|
58
|
+
'QUERIES',
|
|
59
|
+
]);
|
|
60
|
+
const GENERIC_SEGMENTS = new Set([
|
|
61
|
+
'src',
|
|
62
|
+
'source',
|
|
63
|
+
'lib',
|
|
64
|
+
'libs',
|
|
65
|
+
'app',
|
|
66
|
+
'apps',
|
|
67
|
+
'page',
|
|
68
|
+
'pages',
|
|
69
|
+
'component',
|
|
70
|
+
'components',
|
|
71
|
+
'container',
|
|
72
|
+
'containers',
|
|
73
|
+
'hook',
|
|
74
|
+
'hooks',
|
|
75
|
+
'service',
|
|
76
|
+
'services',
|
|
77
|
+
'util',
|
|
78
|
+
'utils',
|
|
79
|
+
'utility',
|
|
80
|
+
'utilities',
|
|
81
|
+
'helper',
|
|
82
|
+
'helpers',
|
|
83
|
+
'shared',
|
|
84
|
+
'common',
|
|
85
|
+
'core',
|
|
86
|
+
'package',
|
|
87
|
+
'packages',
|
|
88
|
+
'module',
|
|
89
|
+
'modules',
|
|
90
|
+
'feature',
|
|
91
|
+
'features',
|
|
92
|
+
'domain',
|
|
93
|
+
'domains',
|
|
94
|
+
'route',
|
|
95
|
+
'routes',
|
|
96
|
+
'api',
|
|
97
|
+
'server',
|
|
98
|
+
'client',
|
|
99
|
+
'web',
|
|
100
|
+
'test',
|
|
101
|
+
'tests',
|
|
102
|
+
'testing',
|
|
103
|
+
'__tests__',
|
|
104
|
+
'__mocks__',
|
|
105
|
+
'fixture',
|
|
106
|
+
'fixtures',
|
|
107
|
+
'type',
|
|
108
|
+
'types',
|
|
109
|
+
'model',
|
|
110
|
+
'models',
|
|
111
|
+
'schema',
|
|
112
|
+
'schemas',
|
|
113
|
+
'index',
|
|
114
|
+
'main',
|
|
115
|
+
'entry',
|
|
116
|
+
'handler',
|
|
117
|
+
'handlers',
|
|
118
|
+
'controller',
|
|
119
|
+
'controllers',
|
|
120
|
+
'provider',
|
|
121
|
+
'providers',
|
|
122
|
+
'adapter',
|
|
123
|
+
'adapters',
|
|
124
|
+
'dist',
|
|
125
|
+
'build',
|
|
126
|
+
'node_modules',
|
|
127
|
+
]);
|
|
128
|
+
const FEATURE_MARKERS = new Set([
|
|
129
|
+
'features',
|
|
130
|
+
'feature',
|
|
131
|
+
'domains',
|
|
132
|
+
'domain',
|
|
133
|
+
'modules',
|
|
134
|
+
'module',
|
|
135
|
+
'pages',
|
|
136
|
+
'page',
|
|
137
|
+
'app',
|
|
138
|
+
'apps',
|
|
139
|
+
'routes',
|
|
140
|
+
'route',
|
|
141
|
+
]);
|
|
142
|
+
const FEATURE_KEYWORDS = new Set([
|
|
143
|
+
'ai',
|
|
144
|
+
'agent',
|
|
145
|
+
'agents',
|
|
146
|
+
'auth',
|
|
147
|
+
'billing',
|
|
148
|
+
'chat',
|
|
149
|
+
'dashboard',
|
|
150
|
+
'embedding',
|
|
151
|
+
'embeddings',
|
|
152
|
+
'graph',
|
|
153
|
+
'graphstore',
|
|
154
|
+
'groups',
|
|
155
|
+
'harness',
|
|
156
|
+
'history',
|
|
157
|
+
'ingestion',
|
|
158
|
+
'mcp',
|
|
159
|
+
'pipeline',
|
|
160
|
+
'projects',
|
|
161
|
+
'recipes',
|
|
162
|
+
'search',
|
|
163
|
+
'settings',
|
|
164
|
+
'sync',
|
|
165
|
+
'tools',
|
|
166
|
+
'wiki',
|
|
167
|
+
]);
|
|
168
|
+
const ACRONYMS = {
|
|
169
|
+
ai: 'AI',
|
|
170
|
+
api: 'API',
|
|
171
|
+
cli: 'CLI',
|
|
172
|
+
db: 'DB',
|
|
173
|
+
fts: 'FTS',
|
|
174
|
+
grpc: 'gRPC',
|
|
175
|
+
http: 'HTTP',
|
|
176
|
+
llm: 'LLM',
|
|
177
|
+
mcp: 'MCP',
|
|
178
|
+
orm: 'ORM',
|
|
179
|
+
sdk: 'SDK',
|
|
180
|
+
sql: 'SQL',
|
|
181
|
+
ui: 'UI',
|
|
182
|
+
ux: 'UX',
|
|
183
|
+
};
|
|
184
|
+
export const processFeatureClusters = async (knowledgeGraph, onProgress, config = {}) => {
|
|
185
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
186
|
+
onProgress?.('Collecting feature signals...', 0);
|
|
187
|
+
const candidates = [];
|
|
188
|
+
for (const node of knowledgeGraph.iterNodes()) {
|
|
189
|
+
if (!CLUSTERABLE_LABELS.has(node.label))
|
|
190
|
+
continue;
|
|
191
|
+
const signals = collectSignals(node);
|
|
192
|
+
if (signals.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
const best = pickBestSignal(signals);
|
|
195
|
+
candidates.push({
|
|
196
|
+
node,
|
|
197
|
+
slug: best.slug,
|
|
198
|
+
confidence: confidenceFromWeight(best.weight),
|
|
199
|
+
signals: signals
|
|
200
|
+
.slice(0, 5)
|
|
201
|
+
.map((s) => `${s.kind}:${s.value}`)
|
|
202
|
+
.filter((s, idx, arr) => arr.indexOf(s) === idx),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
onProgress?.(`Collected feature signals for ${candidates.length} graph nodes...`, 25);
|
|
206
|
+
const clusterMap = new Map();
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
let acc = clusterMap.get(candidate.slug);
|
|
209
|
+
if (!acc) {
|
|
210
|
+
acc = {
|
|
211
|
+
slug: candidate.slug,
|
|
212
|
+
signals: new Map(),
|
|
213
|
+
members: [],
|
|
214
|
+
entryPointIds: new Set(),
|
|
215
|
+
totalConfidence: 0,
|
|
216
|
+
featureKind: inferFeatureKind(candidate.node, candidate.slug),
|
|
217
|
+
};
|
|
218
|
+
clusterMap.set(candidate.slug, acc);
|
|
219
|
+
}
|
|
220
|
+
acc.members.push(candidate);
|
|
221
|
+
acc.totalConfidence += candidate.confidence;
|
|
222
|
+
if (isEntryPoint(candidate.node)) {
|
|
223
|
+
acc.entryPointIds.add(candidate.node.id);
|
|
224
|
+
}
|
|
225
|
+
for (const signalText of candidate.signals) {
|
|
226
|
+
const [kind, ...valueParts] = signalText.split(':');
|
|
227
|
+
const value = valueParts.join(':');
|
|
228
|
+
const key = `${kind}:${value}`;
|
|
229
|
+
const existing = acc.signals.get(key);
|
|
230
|
+
if (existing) {
|
|
231
|
+
acc.signals.set(key, { ...existing, weight: existing.weight + 1 });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
acc.signals.set(key, {
|
|
235
|
+
kind: kind,
|
|
236
|
+
value,
|
|
237
|
+
weight: 1,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const selectedAccumulators = [...clusterMap.values()]
|
|
243
|
+
.filter((acc) => acc.members.length >= cfg.minMembers)
|
|
244
|
+
.sort((a, b) => b.members.length - a.members.length)
|
|
245
|
+
.slice(0, cfg.maxClusters);
|
|
246
|
+
const selectedSlugs = new Set(selectedAccumulators.map((acc) => acc.slug));
|
|
247
|
+
const clusterIdBySlug = new Map();
|
|
248
|
+
for (const acc of selectedAccumulators) {
|
|
249
|
+
clusterIdBySlug.set(acc.slug, generateId('FeatureCluster', acc.slug));
|
|
250
|
+
}
|
|
251
|
+
const memberships = [];
|
|
252
|
+
const memberToCluster = new Map();
|
|
253
|
+
const clusters = [];
|
|
254
|
+
for (const acc of selectedAccumulators) {
|
|
255
|
+
const clusterId = clusterIdBySlug.get(acc.slug);
|
|
256
|
+
const sortedSignals = [...acc.signals.values()]
|
|
257
|
+
.sort((a, b) => b.weight - a.weight)
|
|
258
|
+
.slice(0, 8)
|
|
259
|
+
.map((s) => `${s.kind}:${s.value}`);
|
|
260
|
+
const memberIds = acc.members.map((m) => m.node.id);
|
|
261
|
+
for (const member of acc.members) {
|
|
262
|
+
memberships.push({
|
|
263
|
+
nodeId: member.node.id,
|
|
264
|
+
clusterId,
|
|
265
|
+
confidence: member.confidence,
|
|
266
|
+
signals: member.signals,
|
|
267
|
+
});
|
|
268
|
+
memberToCluster.set(member.node.id, clusterId);
|
|
269
|
+
}
|
|
270
|
+
const name = formatClusterName(acc.slug);
|
|
271
|
+
const routes = collectMemberNames(acc.members, 'Route').slice(0, 25);
|
|
272
|
+
const tools = collectMemberNames(acc.members, 'Tool').slice(0, 25);
|
|
273
|
+
const testMembers = acc.members.filter((m) => isTestNode(m.node));
|
|
274
|
+
const docsMembers = acc.members.filter((m) => isDocsNode(m.node));
|
|
275
|
+
const summaryParts = [
|
|
276
|
+
`${name} feature cluster with ${acc.members.length} indexed member${acc.members.length === 1 ? '' : 's'}`,
|
|
277
|
+
];
|
|
278
|
+
if (routes.length > 0)
|
|
279
|
+
summaryParts.push(`${routes.length} route${routes.length === 1 ? '' : 's'}`);
|
|
280
|
+
if (tools.length > 0)
|
|
281
|
+
summaryParts.push(`${tools.length} tool${tools.length === 1 ? '' : 's'}`);
|
|
282
|
+
if (docsMembers.length > 0) {
|
|
283
|
+
summaryParts.push(`${docsMembers.length} doc member${docsMembers.length === 1 ? '' : 's'}`);
|
|
284
|
+
}
|
|
285
|
+
const summary = `${summaryParts.join(', ')}.`;
|
|
286
|
+
const testCoverageHints = testMembers.length > 0
|
|
287
|
+
? [
|
|
288
|
+
`${testMembers.length} test-related member${testMembers.length === 1 ? '' : 's'} detected.`,
|
|
289
|
+
...testMembers
|
|
290
|
+
.slice(0, 5)
|
|
291
|
+
.map((m) => String(m.node.properties.filePath || m.node.properties.name || m.node.id)),
|
|
292
|
+
]
|
|
293
|
+
: ['No obvious test members detected in this feature cluster.'];
|
|
294
|
+
const confidence = round(Math.min(0.98, acc.totalConfidence / Math.max(acc.members.length, 1)), 3);
|
|
295
|
+
clusters.push({
|
|
296
|
+
id: clusterId,
|
|
297
|
+
name,
|
|
298
|
+
slug: acc.slug,
|
|
299
|
+
featureKind: acc.featureKind,
|
|
300
|
+
summary,
|
|
301
|
+
description: `${name} feature cluster inferred from code structure, routes, symbols, docs, tests, and process signals.`,
|
|
302
|
+
repo: cfg.repo,
|
|
303
|
+
service: cfg.service,
|
|
304
|
+
signals: sortedSignals,
|
|
305
|
+
memberCount: acc.members.length,
|
|
306
|
+
entryPointIds: [...acc.entryPointIds].slice(0, 25),
|
|
307
|
+
routes,
|
|
308
|
+
tools,
|
|
309
|
+
testCoverageHints,
|
|
310
|
+
lastIndexedCommit: cfg.lastIndexedCommit,
|
|
311
|
+
confidence,
|
|
312
|
+
memberIds,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
onProgress?.(`Created ${clusters.length} feature clusters...`, 65);
|
|
316
|
+
const dependencies = buildDependencies(knowledgeGraph, memberToCluster, selectedSlugs);
|
|
317
|
+
onProgress?.('Feature clustering complete!', 100);
|
|
318
|
+
return {
|
|
319
|
+
clusters,
|
|
320
|
+
memberships,
|
|
321
|
+
dependencies,
|
|
322
|
+
stats: {
|
|
323
|
+
totalClusters: clusters.length,
|
|
324
|
+
totalMemberships: memberships.length,
|
|
325
|
+
totalDependencies: dependencies.length,
|
|
326
|
+
nodesProcessed: candidates.length,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
const collectSignals = (node) => {
|
|
331
|
+
const signals = [];
|
|
332
|
+
const filePath = normalizePath(node.properties.filePath || '');
|
|
333
|
+
const name = String(node.properties.name || '');
|
|
334
|
+
if (node.label === 'Route') {
|
|
335
|
+
signals.push(...routeSignals(name));
|
|
336
|
+
}
|
|
337
|
+
if (node.label === 'Tool') {
|
|
338
|
+
signals.push(...nameSignals(name, 'tool', 1.2));
|
|
339
|
+
}
|
|
340
|
+
if (node.label === 'Process') {
|
|
341
|
+
signals.push(...nameSignals(name, 'process', 0.9));
|
|
342
|
+
}
|
|
343
|
+
if (isTestNode(node)) {
|
|
344
|
+
const testSlug = fallbackSlugFromPath(filePath) || normalizeSlug(name);
|
|
345
|
+
if (isMeaningfulSlug(testSlug)) {
|
|
346
|
+
signals.push({ kind: 'test', value: testSlug, slug: testSlug, weight: 1.1 });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (isDocsNode(node)) {
|
|
350
|
+
const docsSlug = fallbackSlugFromPath(filePath) || normalizeSlug(name);
|
|
351
|
+
if (isMeaningfulSlug(docsSlug)) {
|
|
352
|
+
signals.push({ kind: 'docs', value: docsSlug, slug: docsSlug, weight: 1.0 });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
signals.push(...pathSignals(filePath));
|
|
356
|
+
signals.push(...packageBoundarySignals(filePath));
|
|
357
|
+
signals.push(...nameSignals(name, 'symbol', 0.45));
|
|
358
|
+
if (signals.length === 0 && filePath) {
|
|
359
|
+
const fallback = fallbackSlugFromPath(filePath);
|
|
360
|
+
if (fallback) {
|
|
361
|
+
signals.push({
|
|
362
|
+
kind: 'path',
|
|
363
|
+
value: fallback,
|
|
364
|
+
slug: fallback,
|
|
365
|
+
weight: 0.5,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return dedupeSignals(signals);
|
|
370
|
+
};
|
|
371
|
+
const pathSignals = (filePath) => {
|
|
372
|
+
if (!filePath)
|
|
373
|
+
return [];
|
|
374
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
375
|
+
if (parts.length === 0)
|
|
376
|
+
return [];
|
|
377
|
+
const signals = [];
|
|
378
|
+
const dirParts = parts.slice(0, -1);
|
|
379
|
+
for (let i = 0; i < dirParts.length; i++) {
|
|
380
|
+
const slug = normalizeSlug(dirParts[i]);
|
|
381
|
+
if (!isMeaningfulSlug(slug))
|
|
382
|
+
continue;
|
|
383
|
+
const previous = i > 0 ? normalizeSlug(dirParts[i - 1]) : '';
|
|
384
|
+
const markerBoost = FEATURE_MARKERS.has(previous) ? 1.2 : 0;
|
|
385
|
+
const keywordBoost = FEATURE_KEYWORDS.has(slug) ? 0.8 : 0;
|
|
386
|
+
signals.push({
|
|
387
|
+
kind: 'path',
|
|
388
|
+
value: slug,
|
|
389
|
+
slug,
|
|
390
|
+
weight: 2.0 + markerBoost + keywordBoost + i / Math.max(dirParts.length, 1),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const fileName = parts[parts.length - 1] || '';
|
|
394
|
+
const stem = fileName.replace(/\.[^.]+$/, '');
|
|
395
|
+
for (const token of splitIdentifier(stem)) {
|
|
396
|
+
const slug = normalizeSlug(token);
|
|
397
|
+
if (!isMeaningfulSlug(slug))
|
|
398
|
+
continue;
|
|
399
|
+
signals.push({
|
|
400
|
+
kind: 'path',
|
|
401
|
+
value: slug,
|
|
402
|
+
slug,
|
|
403
|
+
weight: FEATURE_KEYWORDS.has(slug) ? 1.6 : 0.75,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return signals;
|
|
407
|
+
};
|
|
408
|
+
const packageBoundarySignals = (filePath) => {
|
|
409
|
+
if (!filePath)
|
|
410
|
+
return [];
|
|
411
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
412
|
+
const signals = [];
|
|
413
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
414
|
+
const marker = normalizeSlug(parts[i]);
|
|
415
|
+
if (marker !== 'packages' && marker !== 'apps' && marker !== 'services')
|
|
416
|
+
continue;
|
|
417
|
+
const slug = normalizeSlug(parts[i + 1] || '');
|
|
418
|
+
if (!isMeaningfulSlug(slug))
|
|
419
|
+
continue;
|
|
420
|
+
signals.push({
|
|
421
|
+
kind: 'package',
|
|
422
|
+
value: `${marker}/${slug}`,
|
|
423
|
+
slug,
|
|
424
|
+
weight: 0.65,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return signals;
|
|
428
|
+
};
|
|
429
|
+
const nameSignals = (name, kind, baseWeight) => {
|
|
430
|
+
if (!name)
|
|
431
|
+
return [];
|
|
432
|
+
return splitIdentifier(name)
|
|
433
|
+
.map((token) => normalizeSlug(token))
|
|
434
|
+
.filter(isMeaningfulSlug)
|
|
435
|
+
.map((slug) => ({
|
|
436
|
+
kind,
|
|
437
|
+
value: slug,
|
|
438
|
+
slug,
|
|
439
|
+
weight: baseWeight + (FEATURE_KEYWORDS.has(slug) ? 0.8 : 0),
|
|
440
|
+
}));
|
|
441
|
+
};
|
|
442
|
+
const routeSignals = (routeName) => {
|
|
443
|
+
const routeParts = routeName
|
|
444
|
+
.split(/[/?#]/)
|
|
445
|
+
.map((p) => p.trim())
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.filter((p) => !p.startsWith(':') && !p.startsWith('['));
|
|
448
|
+
const firstMeaningful = routeParts
|
|
449
|
+
.map((p) => normalizeSlug(p))
|
|
450
|
+
.find((slug) => isMeaningfulSlug(slug));
|
|
451
|
+
if (!firstMeaningful)
|
|
452
|
+
return [];
|
|
453
|
+
return [
|
|
454
|
+
{
|
|
455
|
+
kind: 'route',
|
|
456
|
+
value: firstMeaningful,
|
|
457
|
+
slug: firstMeaningful,
|
|
458
|
+
weight: 4.5,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
};
|
|
462
|
+
const pickBestSignal = (signals) => {
|
|
463
|
+
const scored = new Map();
|
|
464
|
+
for (const signal of signals) {
|
|
465
|
+
const existing = scored.get(signal.slug);
|
|
466
|
+
if (!existing) {
|
|
467
|
+
scored.set(signal.slug, signal);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
scored.set(signal.slug, {
|
|
471
|
+
...existing,
|
|
472
|
+
weight: existing.weight + signal.weight,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return [...scored.values()].sort((a, b) => b.weight - a.weight)[0];
|
|
477
|
+
};
|
|
478
|
+
const buildDependencies = (graph, memberToCluster, selectedSlugs) => {
|
|
479
|
+
const byPair = new Map();
|
|
480
|
+
for (const rel of graph.iterRelationships()) {
|
|
481
|
+
if (!DEPENDENCY_REL_TYPES.has(rel.type))
|
|
482
|
+
continue;
|
|
483
|
+
const sourceClusterId = memberToCluster.get(rel.sourceId);
|
|
484
|
+
const targetClusterId = memberToCluster.get(rel.targetId);
|
|
485
|
+
if (!sourceClusterId || !targetClusterId || sourceClusterId === targetClusterId)
|
|
486
|
+
continue;
|
|
487
|
+
const sourceSlug = sourceClusterId.split(':').slice(1).join(':');
|
|
488
|
+
const targetSlug = targetClusterId.split(':').slice(1).join(':');
|
|
489
|
+
if (!selectedSlugs.has(sourceSlug) || !selectedSlugs.has(targetSlug))
|
|
490
|
+
continue;
|
|
491
|
+
const key = `${sourceClusterId}->${targetClusterId}`;
|
|
492
|
+
let acc = byPair.get(key);
|
|
493
|
+
if (!acc) {
|
|
494
|
+
acc = {
|
|
495
|
+
sourceClusterId,
|
|
496
|
+
targetClusterId,
|
|
497
|
+
edgeCount: 0,
|
|
498
|
+
relationshipTypes: new Set(),
|
|
499
|
+
confidenceSum: 0,
|
|
500
|
+
};
|
|
501
|
+
byPair.set(key, acc);
|
|
502
|
+
}
|
|
503
|
+
acc.edgeCount++;
|
|
504
|
+
acc.relationshipTypes.add(rel.type);
|
|
505
|
+
acc.confidenceSum += rel.confidence ?? 1;
|
|
506
|
+
}
|
|
507
|
+
return [...byPair.values()]
|
|
508
|
+
.map((acc) => ({
|
|
509
|
+
sourceClusterId: acc.sourceClusterId,
|
|
510
|
+
targetClusterId: acc.targetClusterId,
|
|
511
|
+
edgeCount: acc.edgeCount,
|
|
512
|
+
relationshipTypes: [...acc.relationshipTypes].sort(),
|
|
513
|
+
confidence: round(acc.confidenceSum / Math.max(acc.edgeCount, 1), 3),
|
|
514
|
+
}))
|
|
515
|
+
.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
516
|
+
};
|
|
517
|
+
const isEntryPoint = (node) => {
|
|
518
|
+
if (node.label === 'Route' || node.label === 'Tool')
|
|
519
|
+
return true;
|
|
520
|
+
if (node.properties.isExported)
|
|
521
|
+
return true;
|
|
522
|
+
const name = String(node.properties.name || '');
|
|
523
|
+
return /^(handle|on|use|run|execute|create|update|delete|get|post|put|patch|main)/i.test(name);
|
|
524
|
+
};
|
|
525
|
+
const isTestNode = (node) => {
|
|
526
|
+
const filePath = normalizePath(node.properties.filePath || '');
|
|
527
|
+
return (filePath.includes('/test/') ||
|
|
528
|
+
filePath.includes('/tests/') ||
|
|
529
|
+
filePath.includes('__tests__') ||
|
|
530
|
+
filePath.includes('__mocks__') ||
|
|
531
|
+
/\.(test|spec)\.[jt]sx?$/.test(filePath));
|
|
532
|
+
};
|
|
533
|
+
const isDocsNode = (node) => {
|
|
534
|
+
const filePath = normalizePath(node.properties.filePath || '');
|
|
535
|
+
return node.label === 'Section' || filePath.includes('/docs/') || /\.mdx?$/.test(filePath);
|
|
536
|
+
};
|
|
537
|
+
const collectMemberNames = (members, label) => {
|
|
538
|
+
const names = new Set();
|
|
539
|
+
for (const member of members) {
|
|
540
|
+
if (member.node.label !== label)
|
|
541
|
+
continue;
|
|
542
|
+
const name = String(member.node.properties.name || member.node.id);
|
|
543
|
+
if (name)
|
|
544
|
+
names.add(name);
|
|
545
|
+
}
|
|
546
|
+
return [...names].sort();
|
|
547
|
+
};
|
|
548
|
+
const inferFeatureKind = (node, slug) => {
|
|
549
|
+
const pathValue = normalizePath(node.properties.filePath || '');
|
|
550
|
+
if (node.label === 'Route' || pathValue.includes('/routes/') || pathValue.includes('/api/')) {
|
|
551
|
+
return 'api';
|
|
552
|
+
}
|
|
553
|
+
if (node.label === 'Tool' || slug === 'cli' || slug === 'mcp')
|
|
554
|
+
return 'tooling';
|
|
555
|
+
if (pathValue.includes('/pages/') || pathValue.includes('/app/'))
|
|
556
|
+
return 'page';
|
|
557
|
+
if (pathValue.includes('/test/') ||
|
|
558
|
+
pathValue.includes('/tests/') ||
|
|
559
|
+
pathValue.includes('__tests__')) {
|
|
560
|
+
return 'test';
|
|
561
|
+
}
|
|
562
|
+
if (pathValue.includes('/docs/') || pathValue.endsWith('.md'))
|
|
563
|
+
return 'docs';
|
|
564
|
+
if (['config', 'build', 'ci', 'deploy', 'infra'].includes(slug))
|
|
565
|
+
return 'infrastructure';
|
|
566
|
+
if (['schema', 'model', 'data', 'database', 'sql', 'orm'].includes(slug))
|
|
567
|
+
return 'data';
|
|
568
|
+
if (['auth', 'billing', 'settings', 'projects', 'recipes', 'dashboard'].includes(slug)) {
|
|
569
|
+
return 'domain';
|
|
570
|
+
}
|
|
571
|
+
return 'feature';
|
|
572
|
+
};
|
|
573
|
+
const confidenceFromWeight = (weight) => {
|
|
574
|
+
return round(Math.min(0.98, 0.5 + Math.min(weight, 5) / 10), 3);
|
|
575
|
+
};
|
|
576
|
+
const normalizePath = (filePath) => filePath.replace(/\\/g, '/').toLowerCase();
|
|
577
|
+
const normalizeSlug = (value) => {
|
|
578
|
+
const clean = value
|
|
579
|
+
.replace(/\.[^.]+$/, '')
|
|
580
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
581
|
+
.toLowerCase()
|
|
582
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
583
|
+
.replace(/^-+|-+$/g, '');
|
|
584
|
+
return clean;
|
|
585
|
+
};
|
|
586
|
+
const isMeaningfulSlug = (slug) => {
|
|
587
|
+
return slug.length >= 2 && !GENERIC_SEGMENTS.has(slug) && !/^\d+$/.test(slug);
|
|
588
|
+
};
|
|
589
|
+
const splitIdentifier = (value) => {
|
|
590
|
+
return value
|
|
591
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
592
|
+
.split(/[^A-Za-z0-9]+/)
|
|
593
|
+
.map((s) => s.trim())
|
|
594
|
+
.filter(Boolean);
|
|
595
|
+
};
|
|
596
|
+
const fallbackSlugFromPath = (filePath) => {
|
|
597
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
598
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
599
|
+
const slug = normalizeSlug(parts[i]);
|
|
600
|
+
if (isMeaningfulSlug(slug))
|
|
601
|
+
return slug;
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
};
|
|
605
|
+
const dedupeSignals = (signals) => {
|
|
606
|
+
const byKey = new Map();
|
|
607
|
+
for (const signal of signals) {
|
|
608
|
+
const key = `${signal.kind}:${signal.slug}`;
|
|
609
|
+
const existing = byKey.get(key);
|
|
610
|
+
if (!existing || existing.weight < signal.weight) {
|
|
611
|
+
byKey.set(key, signal);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return [...byKey.values()].sort((a, b) => b.weight - a.weight);
|
|
615
|
+
};
|
|
616
|
+
const formatClusterName = (slug) => {
|
|
617
|
+
return slug
|
|
618
|
+
.split('-')
|
|
619
|
+
.filter(Boolean)
|
|
620
|
+
.map((part) => ACRONYMS[part] || part.charAt(0).toUpperCase() + part.slice(1))
|
|
621
|
+
.join(' ');
|
|
622
|
+
};
|
|
623
|
+
const round = (value, digits) => {
|
|
624
|
+
const scale = 10 ** digits;
|
|
625
|
+
return Math.round(value * scale) / scale;
|
|
626
|
+
};
|
|
@@ -53,7 +53,7 @@ export function finalizeScopeModel(parsedFiles, options = {}) {
|
|
|
53
53
|
const finalizeOut = finalize(finalizeInput, hooks);
|
|
54
54
|
// ── Step 2: Workspace-wide indexes built from the per-file unions.
|
|
55
55
|
// These are pure aggregations — no algorithm beyond what the builders
|
|
56
|
-
// in codragraph
|
|
56
|
+
// in @codragraph/shared already encapsulate (first-write-wins, qname
|
|
57
57
|
// collision buckets, etc.).
|
|
58
58
|
const allScopes = [];
|
|
59
59
|
const allDefs = [];
|
|
@@ -35,7 +35,7 @@ export { gatherAncestors };
|
|
|
35
35
|
* All strategies respect `argCount` for overload narrowing.
|
|
36
36
|
* `ancestryOverride` replaces the default walk; caller must compute it correctly.
|
|
37
37
|
*
|
|
38
|
-
* Strategy summary (full docs in
|
|
38
|
+
* Strategy summary (full docs in packages/shared/src/mro-strategy.ts):
|
|
39
39
|
* - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
|
|
40
40
|
* - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
|
|
41
41
|
* - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
|
|
@@ -44,7 +44,7 @@ export { gatherAncestors };
|
|
|
44
44
|
* Internal API: exported for call-processor resolvers and tests.
|
|
45
45
|
* External callers should use resolveMemberCall instead.
|
|
46
46
|
*
|
|
47
|
-
* @see
|
|
47
|
+
* @see packages/shared/src/mro-strategy.ts § 'ruby-mixin'
|
|
48
48
|
* @see call-processor.ts § resolveMemberCall
|
|
49
49
|
*/
|
|
50
50
|
export declare const lookupMethodByOwnerWithMRO: (ownerNodeId: string, methodName: string, heritageMap: HeritageMap, model: SemanticModel, strategy: MroStrategy, argCount?: number,
|
|
@@ -240,7 +240,7 @@ const buildParentMapFromHeritage = (startNodeId, heritageMap) => {
|
|
|
240
240
|
* All strategies respect `argCount` for overload narrowing.
|
|
241
241
|
* `ancestryOverride` replaces the default walk; caller must compute it correctly.
|
|
242
242
|
*
|
|
243
|
-
* Strategy summary (full docs in
|
|
243
|
+
* Strategy summary (full docs in packages/shared/src/mro-strategy.ts):
|
|
244
244
|
* - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
|
|
245
245
|
* - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
|
|
246
246
|
* - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
|
|
@@ -249,7 +249,7 @@ const buildParentMapFromHeritage = (startNodeId, heritageMap) => {
|
|
|
249
249
|
* Internal API: exported for call-processor resolvers and tests.
|
|
250
250
|
* External callers should use resolveMemberCall instead.
|
|
251
251
|
*
|
|
252
|
-
* @see
|
|
252
|
+
* @see packages/shared/src/mro-strategy.ts § 'ruby-mixin'
|
|
253
253
|
* @see call-processor.ts § resolveMemberCall
|
|
254
254
|
*/
|
|
255
255
|
export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap, model, strategy, argCount,
|
|
@@ -269,7 +269,7 @@ ancestryOverride) => {
|
|
|
269
269
|
// Instance dispatch: prepend (reverse) → direct → include (reverse) → transitive BFS.
|
|
270
270
|
// Singleton dispatch: caller supplies ancestryOverride (extend providers only);
|
|
271
271
|
// simple left-to-right scan. Miss NEVER falls through to file-scoped fallback.
|
|
272
|
-
// See
|
|
272
|
+
// See packages/shared/src/mro-strategy.ts § 'ruby-mixin' for full strategy docs.
|
|
273
273
|
if (strategy === 'ruby-mixin') {
|
|
274
274
|
if (ancestryOverride) {
|
|
275
275
|
// Singleton dispatch: scan pre-computed ancestry only. Miss null-routes.
|