@grafema/cli 0.2.11 → 0.3.0-beta
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/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +2 -4
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts +5 -3
- package/dist/commands/analyzeAction.d.ts.map +1 -1
- package/dist/commands/analyzeAction.js +109 -151
- package/dist/commands/analyzeAction.js.map +1 -1
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +4 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.js +2 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/coverage.js +2 -2
- package/dist/commands/coverage.js.map +1 -1
- package/dist/commands/describe.d.ts +13 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +131 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts +6 -1
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +128 -13
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/doctor.d.ts +10 -9
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/file.js +2 -2
- package/dist/commands/file.js.map +1 -1
- package/dist/commands/get.js +2 -2
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/git-ingest.d.ts +6 -0
- package/dist/commands/git-ingest.d.ts.map +1 -0
- package/dist/commands/git-ingest.js +46 -0
- package/dist/commands/git-ingest.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +276 -50
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -22
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ls.js +2 -2
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/overview.js +2 -2
- package/dist/commands/overview.js.map +1 -1
- package/dist/commands/query.d.ts +1 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +169 -7
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/schema.js +2 -2
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +122 -76
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/stats.js +2 -2
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/tldr.d.ts +12 -0
- package/dist/commands/tldr.d.ts.map +1 -0
- package/dist/commands/tldr.js +81 -0
- package/dist/commands/tldr.js.map +1 -0
- package/dist/commands/trace.d.ts +1 -1
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +17 -133
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/types.js +2 -2
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/who.d.ts +12 -0
- package/dist/commands/who.d.ts.map +1 -0
- package/dist/commands/who.js +184 -0
- package/dist/commands/who.js.map +1 -0
- package/dist/commands/why.d.ts +12 -0
- package/dist/commands/why.d.ts.map +1 -0
- package/dist/commands/why.js +118 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/commands/wtf.d.ts +12 -0
- package/dist/commands/wtf.d.ts.map +1 -0
- package/dist/commands/wtf.js +117 -0
- package/dist/commands/wtf.js.map +1 -0
- package/dist/plugins/builtinPlugins.d.ts +1 -9
- package/dist/plugins/builtinPlugins.d.ts.map +1 -1
- package/dist/plugins/builtinPlugins.js +2 -67
- package/dist/plugins/builtinPlugins.js.map +1 -1
- package/dist/plugins/pluginLoader.d.ts +1 -15
- package/dist/plugins/pluginLoader.d.ts.map +1 -1
- package/dist/plugins/pluginLoader.js +2 -100
- package/dist/plugins/pluginLoader.js.map +1 -1
- package/dist/plugins/pluginResolver.js +3 -3
- package/dist/utils/progressRenderer.d.ts +15 -1
- package/dist/utils/progressRenderer.d.ts.map +1 -1
- package/dist/utils/progressRenderer.js +19 -3
- package/dist/utils/progressRenderer.js.map +1 -1
- package/dist/utils/queryHints.d.ts +6 -0
- package/dist/utils/queryHints.d.ts.map +1 -0
- package/dist/utils/queryHints.js +36 -0
- package/dist/utils/queryHints.js.map +1 -0
- package/package.json +4 -4
- package/skills/grafema-codebase-analysis/SKILL.md +1 -1
- package/src/cli.ts +14 -0
- package/src/commands/analyze.ts +2 -4
- package/src/commands/analyzeAction.ts +122 -168
- package/src/commands/check.ts +5 -5
- package/src/commands/context.ts +3 -3
- package/src/commands/coverage.ts +2 -2
- package/src/commands/describe.ts +160 -0
- package/src/commands/doctor/checks.ts +153 -10
- package/src/commands/doctor.ts +13 -9
- package/src/commands/explain.ts +2 -2
- package/src/commands/explore.tsx +2 -2
- package/src/commands/file.ts +3 -3
- package/src/commands/get.ts +2 -2
- package/src/commands/git-ingest.ts +49 -0
- package/src/commands/impact.ts +318 -55
- package/src/commands/init.ts +20 -22
- package/src/commands/ls.ts +2 -2
- package/src/commands/overview.ts +2 -2
- package/src/commands/query.ts +197 -7
- package/src/commands/schema.ts +2 -2
- package/src/commands/server.ts +136 -84
- package/src/commands/stats.ts +2 -2
- package/src/commands/tldr.ts +103 -0
- package/src/commands/trace.ts +19 -161
- package/src/commands/types.ts +2 -2
- package/src/commands/who.ts +215 -0
- package/src/commands/why.ts +134 -0
- package/src/commands/wtf.ts +140 -0
- package/src/plugins/builtinPlugins.ts +1 -108
- package/src/plugins/pluginLoader.ts +1 -123
- package/src/plugins/pluginResolver.js +3 -3
- package/src/utils/progressRenderer.ts +34 -4
- package/src/utils/queryHints.ts +46 -0
package/src/commands/impact.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
import { isAbsolute, resolve, join, dirname, relative } from 'path';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
|
-
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/
|
|
12
|
+
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/util';
|
|
13
13
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
14
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
15
|
|
|
@@ -58,21 +58,27 @@ Examples:
|
|
|
58
58
|
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const backend = new RFDBServerBackend({ dbPath });
|
|
61
|
+
const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
|
|
62
62
|
await backend.connect();
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
65
|
const { type, name } = parsePattern(pattern);
|
|
66
66
|
const maxDepth = parseInt(options.depth, 10);
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (!options.json) {
|
|
69
|
+
console.log(`Analyzing impact of changing ${name}...`);
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
70
72
|
|
|
71
73
|
// Find target node
|
|
72
74
|
const target = await findTarget(backend, type, name);
|
|
73
75
|
|
|
74
76
|
if (!target) {
|
|
75
|
-
|
|
77
|
+
if (options.json) {
|
|
78
|
+
process.stderr.write(`No ${type || 'node'} "${name}" found\n`);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`No ${type || 'node'} "${name}" found`);
|
|
81
|
+
}
|
|
76
82
|
return;
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -152,34 +158,238 @@ async function findTarget(
|
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
|
-
*
|
|
161
|
+
* Extract bare method name from a possibly-qualified name.
|
|
162
|
+
* "RFDBServerBackend.addNode" -> "addNode"
|
|
163
|
+
* "addNode" -> "addNode"
|
|
156
164
|
*/
|
|
157
|
-
|
|
165
|
+
function extractMethodName(fullName: string): string {
|
|
166
|
+
if (!fullName) return '';
|
|
167
|
+
const dotIdx = fullName.lastIndexOf('.');
|
|
168
|
+
return dotIdx >= 0 ? fullName.slice(dotIdx + 1) : fullName;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find the FUNCTION child node ID for `methodName` in a CLASS node.
|
|
173
|
+
* Returns the concrete function node ID if found, null otherwise.
|
|
174
|
+
*/
|
|
175
|
+
async function findMethodInClass(
|
|
176
|
+
backend: RFDBServerBackend,
|
|
177
|
+
classId: string,
|
|
178
|
+
methodName: string
|
|
179
|
+
): Promise<string | null> {
|
|
180
|
+
const containsEdges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
181
|
+
for (const edge of containsEdges) {
|
|
182
|
+
const child = await backend.getNode(edge.dst);
|
|
183
|
+
if (child && child.type === 'FUNCTION' && child.name === methodName) {
|
|
184
|
+
return child.id;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check whether `methodName` is declared in an INTERFACE node's `properties` array.
|
|
192
|
+
*
|
|
193
|
+
* Interface method signatures are stored as JSON on the INTERFACE node itself,
|
|
194
|
+
* NOT as separate FUNCTION graph nodes. There are no CALLS edges pointing to them.
|
|
195
|
+
* When found, returns the INTERFACE node's own ID as a proxy: including it in
|
|
196
|
+
* initialTargetIds causes the findByAttr fallback to fire and surface unresolved
|
|
197
|
+
* call sites whose receiver was typed as this interface.
|
|
198
|
+
*
|
|
199
|
+
* Returns the interface node ID (proxy) if declared, null otherwise.
|
|
200
|
+
*/
|
|
201
|
+
async function findInterfaceMethodProxy(
|
|
202
|
+
backend: RFDBServerBackend,
|
|
203
|
+
interfaceId: string,
|
|
204
|
+
methodName: string
|
|
205
|
+
): Promise<string | null> {
|
|
206
|
+
const node = await backend.getNode(interfaceId);
|
|
207
|
+
if (!node) return null;
|
|
208
|
+
const properties = (node as any).properties;
|
|
209
|
+
if (Array.isArray(properties)) {
|
|
210
|
+
for (const prop of properties) {
|
|
211
|
+
if (prop && prop.name === methodName) return interfaceId;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collect all ancestor class/interface IDs by walking outgoing DERIVES_FROM and
|
|
219
|
+
* IMPLEMENTS edges upward through the hierarchy.
|
|
220
|
+
*
|
|
221
|
+
* Depth-bounded to 5 hops. Visited set prevents infinite loops on malformed data.
|
|
222
|
+
*/
|
|
223
|
+
async function collectAncestors(
|
|
224
|
+
backend: RFDBServerBackend,
|
|
225
|
+
classId: string,
|
|
226
|
+
visited = new Set<string>(),
|
|
227
|
+
depth = 0
|
|
228
|
+
): Promise<string[]> {
|
|
229
|
+
if (depth > 5 || visited.has(classId)) return [];
|
|
230
|
+
visited.add(classId);
|
|
231
|
+
const ancestors: string[] = [];
|
|
232
|
+
|
|
233
|
+
const outgoing = await backend.getOutgoingEdges(classId, ['DERIVES_FROM', 'IMPLEMENTS']);
|
|
234
|
+
for (const edge of outgoing) {
|
|
235
|
+
ancestors.push(edge.dst);
|
|
236
|
+
const more = await collectAncestors(backend, edge.dst, visited, depth + 1);
|
|
237
|
+
ancestors.push(...more);
|
|
238
|
+
}
|
|
239
|
+
return ancestors;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Collect all descendant class IDs by recursively walking incoming DERIVES_FROM
|
|
244
|
+
* and IMPLEMENTS edges downward through the hierarchy.
|
|
245
|
+
*
|
|
246
|
+
* Depth-bounded to 5 hops. Visited set prevents infinite loops on malformed data.
|
|
247
|
+
*/
|
|
248
|
+
async function collectDescendants(
|
|
249
|
+
backend: RFDBServerBackend,
|
|
250
|
+
classId: string,
|
|
251
|
+
visited = new Set<string>(),
|
|
252
|
+
depth = 0
|
|
253
|
+
): Promise<string[]> {
|
|
254
|
+
if (depth > 5 || visited.has(classId)) return [];
|
|
255
|
+
visited.add(classId);
|
|
256
|
+
const descendants: string[] = [];
|
|
257
|
+
const incoming = await backend.getIncomingEdges(classId, ['DERIVES_FROM', 'IMPLEMENTS']);
|
|
258
|
+
for (const edge of incoming) {
|
|
259
|
+
descendants.push(edge.src);
|
|
260
|
+
const more = await collectDescendants(backend, edge.src, visited, depth + 1);
|
|
261
|
+
descendants.push(...more);
|
|
262
|
+
}
|
|
263
|
+
return descendants;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Given a concrete method node, find all related nodes in the class hierarchy
|
|
268
|
+
* that represent the same conceptual method (parent interfaces, abstract methods,
|
|
269
|
+
* sibling and descendant implementations).
|
|
270
|
+
*
|
|
271
|
+
* This enables CHA-style impact analysis: callers that type their receiver
|
|
272
|
+
* as an interface or abstract class will have CALLS edges pointing to the abstract
|
|
273
|
+
* node, not to the concrete one. Including those abstract nodes in the initial target
|
|
274
|
+
* set lets the BFS reach those call sites.
|
|
275
|
+
*
|
|
276
|
+
* Returns a set of node IDs:
|
|
277
|
+
* - The original targetId
|
|
278
|
+
* - FUNCTION child node IDs from CLASS ancestors/descendants that declare the method
|
|
279
|
+
* - INTERFACE node IDs (findByAttr-trigger proxies) from INTERFACE ancestors
|
|
280
|
+
*/
|
|
281
|
+
async function expandTargetSet(
|
|
282
|
+
backend: RFDBServerBackend,
|
|
283
|
+
targetId: string,
|
|
284
|
+
methodName: string
|
|
285
|
+
): Promise<Set<string>> {
|
|
286
|
+
const result = new Set<string>([targetId]);
|
|
287
|
+
|
|
288
|
+
if (!methodName) return result;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Find the containing class or interface
|
|
292
|
+
const containsEdges = await backend.getIncomingEdges(targetId, ['CONTAINS']);
|
|
293
|
+
const parentIds: string[] = [];
|
|
294
|
+
for (const edge of containsEdges) {
|
|
295
|
+
const parent = await backend.getNode(edge.src);
|
|
296
|
+
if (parent && (parent.type === 'CLASS' || parent.type === 'INTERFACE')) {
|
|
297
|
+
parentIds.push(parent.id);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// For each parent, walk the full hierarchy (ancestors + all descendants)
|
|
302
|
+
for (const classId of parentIds) {
|
|
303
|
+
const ancestors = await collectAncestors(backend, classId);
|
|
304
|
+
for (const ancestorId of ancestors) {
|
|
305
|
+
const ancestorNode = await backend.getNode(ancestorId);
|
|
306
|
+
if (!ancestorNode) continue;
|
|
307
|
+
|
|
308
|
+
if (ancestorNode.type === 'CLASS') {
|
|
309
|
+
const method = await findMethodInClass(backend, ancestorId, methodName);
|
|
310
|
+
if (method) result.add(method);
|
|
311
|
+
// All descendants of this ancestor may implement the method too
|
|
312
|
+
const descendants = await collectDescendants(backend, ancestorId);
|
|
313
|
+
for (const descId of descendants) {
|
|
314
|
+
const descNode = await backend.getNode(descId);
|
|
315
|
+
if (!descNode) continue;
|
|
316
|
+
if (descNode.type === 'CLASS') {
|
|
317
|
+
const descMethod = await findMethodInClass(backend, descId, methodName);
|
|
318
|
+
if (descMethod) result.add(descMethod);
|
|
319
|
+
} else if (descNode.type === 'INTERFACE') {
|
|
320
|
+
const proxy = await findInterfaceMethodProxy(backend, descId, methodName);
|
|
321
|
+
if (proxy) result.add(proxy);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else if (ancestorNode.type === 'INTERFACE') {
|
|
325
|
+
const proxy = await findInterfaceMethodProxy(backend, ancestorId, methodName);
|
|
326
|
+
if (proxy) result.add(proxy);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
process.stderr.write(`[grafema impact] Warning: hierarchy expansion failed: ${err}\n`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Determine the initial set of nodes to BFS from and the per-node method names
|
|
339
|
+
* for the findByAttr fallback.
|
|
340
|
+
*
|
|
341
|
+
* For CLASS targets: seeds from the class node + all its method nodes.
|
|
342
|
+
* For function/method targets: CHA-style expansion via expandTargetSet.
|
|
343
|
+
*/
|
|
344
|
+
async function resolveTargetSet(
|
|
345
|
+
backend: RFDBServerBackend,
|
|
346
|
+
target: NodeInfo
|
|
347
|
+
): Promise<{ targetIds: string[]; targetMethodNames: Map<string, string> }> {
|
|
348
|
+
const targetMethodNames = new Map<string, string>();
|
|
349
|
+
|
|
350
|
+
if (target.type === 'CLASS') {
|
|
351
|
+
const methods = await getClassMethods(backend, target.id);
|
|
352
|
+
for (const m of methods) {
|
|
353
|
+
if (m.name) targetMethodNames.set(m.id, m.name);
|
|
354
|
+
}
|
|
355
|
+
return { targetIds: [target.id, ...methods.map(m => m.id)], targetMethodNames };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const methodName = extractMethodName(target.name);
|
|
359
|
+
const expanded = await expandTargetSet(backend, target.id, methodName);
|
|
360
|
+
const targetIds = [...expanded];
|
|
361
|
+
if (methodName) {
|
|
362
|
+
for (const id of targetIds) targetMethodNames.set(id, methodName);
|
|
363
|
+
}
|
|
364
|
+
return { targetIds, targetMethodNames };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* BFS over caller graph starting from `targetIds`, collecting direct and transitive
|
|
369
|
+
* callers up to `maxDepth` hops.
|
|
370
|
+
*
|
|
371
|
+
* The `initialTargetIds` set gates the findByAttr fallback: it runs only for nodes
|
|
372
|
+
* in the initial seed, never for callers discovered during traversal.
|
|
373
|
+
*/
|
|
374
|
+
async function collectCallersBFS(
|
|
158
375
|
backend: RFDBServerBackend,
|
|
159
376
|
target: NodeInfo,
|
|
377
|
+
targetIds: string[],
|
|
378
|
+
targetMethodNames: Map<string, string>,
|
|
160
379
|
maxDepth: number,
|
|
161
380
|
projectPath: string
|
|
162
|
-
): Promise<
|
|
381
|
+
): Promise<{ directCallers: NodeInfo[]; transitiveCallers: NodeInfo[]; affectedModules: Map<string, number>; callChains: string[][] }> {
|
|
163
382
|
const directCallers: NodeInfo[] = [];
|
|
164
383
|
const transitiveCallers: NodeInfo[] = [];
|
|
165
384
|
const affectedModules = new Map<string, number>();
|
|
166
385
|
const callChains: string[][] = [];
|
|
167
386
|
const visited = new Set<string>();
|
|
387
|
+
const initialTargetIds = new Set(targetIds);
|
|
168
388
|
|
|
169
|
-
// If target is a CLASS, aggregate callers from all methods
|
|
170
|
-
let targetIds: string[];
|
|
171
|
-
if (target.type === 'CLASS') {
|
|
172
|
-
const methodIds = await getClassMethods(backend, target.id);
|
|
173
|
-
targetIds = [target.id, ...methodIds];
|
|
174
|
-
} else {
|
|
175
|
-
targetIds = [target.id];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// BFS to find all callers
|
|
179
389
|
const queue: Array<{ id: string; depth: number; chain: string[] }> = targetIds.map(id => ({
|
|
180
390
|
id,
|
|
181
391
|
depth: 0,
|
|
182
|
-
chain: [target.name]
|
|
392
|
+
chain: [target.name],
|
|
183
393
|
}));
|
|
184
394
|
|
|
185
395
|
while (queue.length > 0) {
|
|
@@ -191,19 +401,18 @@ async function analyzeImpact(
|
|
|
191
401
|
if (depth > maxDepth) continue;
|
|
192
402
|
|
|
193
403
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
404
|
+
const containingCalls = await findCallsToNode(
|
|
405
|
+
backend,
|
|
406
|
+
id,
|
|
407
|
+
initialTargetIds.has(id) ? targetMethodNames.get(id) : undefined
|
|
408
|
+
);
|
|
197
409
|
|
|
198
410
|
for (const callNode of containingCalls) {
|
|
199
|
-
// Find the function containing this call
|
|
200
411
|
const container = await findContainingFunctionCore(backend, callNode.id);
|
|
201
412
|
|
|
202
413
|
if (container && !visited.has(container.id)) {
|
|
203
|
-
//
|
|
204
|
-
if (target.type === 'CLASS' && targetIds.includes(container.id))
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
414
|
+
// Skip internal callers (methods of the same class being analyzed)
|
|
415
|
+
if (target.type === 'CLASS' && targetIds.includes(container.id)) continue;
|
|
207
416
|
|
|
208
417
|
const caller: NodeInfo = {
|
|
209
418
|
id: container.id,
|
|
@@ -219,45 +428,48 @@ async function analyzeImpact(
|
|
|
219
428
|
transitiveCallers.push(caller);
|
|
220
429
|
}
|
|
221
430
|
|
|
222
|
-
// Track affected modules
|
|
223
431
|
const modulePath = getModulePath(caller.file, projectPath);
|
|
224
432
|
affectedModules.set(modulePath, (affectedModules.get(modulePath) || 0) + 1);
|
|
225
433
|
|
|
226
|
-
// Track call chain
|
|
227
434
|
const newChain = [...chain, caller.name];
|
|
228
|
-
if (newChain.length <= 4)
|
|
229
|
-
callChains.push(newChain);
|
|
230
|
-
}
|
|
435
|
+
if (newChain.length <= 4) callChains.push(newChain);
|
|
231
436
|
|
|
232
|
-
// Continue BFS
|
|
233
437
|
queue.push({ id: container.id, depth: depth + 1, chain: newChain });
|
|
234
438
|
}
|
|
235
439
|
}
|
|
236
|
-
} catch {
|
|
237
|
-
|
|
440
|
+
} catch (err) {
|
|
441
|
+
process.stderr.write(`[grafema impact] Warning: query failed for node ${id}: ${err}\n`);
|
|
238
442
|
}
|
|
239
443
|
}
|
|
240
444
|
|
|
241
|
-
// Sort call chains by length
|
|
242
445
|
callChains.sort((a, b) => b.length - a.length);
|
|
446
|
+
return { directCallers, transitiveCallers, affectedModules, callChains };
|
|
447
|
+
}
|
|
243
448
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
449
|
+
/**
|
|
450
|
+
* Analyze impact of changing a node: resolve the target set, then BFS for callers.
|
|
451
|
+
*/
|
|
452
|
+
async function analyzeImpact(
|
|
453
|
+
backend: RFDBServerBackend,
|
|
454
|
+
target: NodeInfo,
|
|
455
|
+
maxDepth: number,
|
|
456
|
+
projectPath: string
|
|
457
|
+
): Promise<ImpactResult> {
|
|
458
|
+
const { targetIds, targetMethodNames } = await resolveTargetSet(backend, target);
|
|
459
|
+
const { directCallers, transitiveCallers, affectedModules, callChains } =
|
|
460
|
+
await collectCallersBFS(backend, target, targetIds, targetMethodNames, maxDepth, projectPath);
|
|
461
|
+
|
|
462
|
+
return { target, directCallers, transitiveCallers, affectedModules, callChains };
|
|
251
463
|
}
|
|
252
464
|
|
|
253
465
|
/**
|
|
254
|
-
* Get method
|
|
466
|
+
* Get method nodes for a class (id + name pairs for findByAttr fallback)
|
|
255
467
|
*/
|
|
256
468
|
async function getClassMethods(
|
|
257
469
|
backend: RFDBServerBackend,
|
|
258
470
|
classId: string
|
|
259
|
-
): Promise<string
|
|
260
|
-
const methods: string
|
|
471
|
+
): Promise<Array<{ id: string; name: string }>> {
|
|
472
|
+
const methods: Array<{ id: string; name: string }> = [];
|
|
261
473
|
|
|
262
474
|
try {
|
|
263
475
|
const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
@@ -265,32 +477,48 @@ async function getClassMethods(
|
|
|
265
477
|
for (const edge of edges) {
|
|
266
478
|
const node = await backend.getNode(edge.dst);
|
|
267
479
|
if (node && node.type === 'FUNCTION') {
|
|
268
|
-
methods.push(node.id);
|
|
480
|
+
methods.push({ id: node.id, name: node.name || '' });
|
|
269
481
|
}
|
|
270
482
|
}
|
|
271
|
-
} catch {
|
|
272
|
-
|
|
483
|
+
} catch (err) {
|
|
484
|
+
process.stderr.write(`[grafema impact] Warning: method enumeration failed for ${classId}: ${err}\n`);
|
|
273
485
|
}
|
|
274
486
|
|
|
275
487
|
return methods;
|
|
276
488
|
}
|
|
277
489
|
|
|
278
490
|
/**
|
|
279
|
-
* Find CALL nodes that reference a target
|
|
491
|
+
* Find CALL nodes that reference a target via CALLS edges.
|
|
492
|
+
*
|
|
493
|
+
* If methodName is provided, also searches for unresolved CALL nodes that
|
|
494
|
+
* have a matching `method` attribute but no CALLS edge (e.g., calls through
|
|
495
|
+
* abstract-typed or parameter-typed receivers that MethodCallResolver could
|
|
496
|
+
* not resolve).
|
|
497
|
+
*
|
|
498
|
+
* IMPORTANT: Only pass methodName for initial target IDs (depth 0 in BFS),
|
|
499
|
+
* never for transitive callers. The findByAttr query scans the entire graph
|
|
500
|
+
* and returns the same results regardless of which node is being queried --
|
|
501
|
+
* running it for every BFS node is redundant and costly.
|
|
502
|
+
*
|
|
503
|
+
* Known imprecision: findByAttr matches by bare method name only, not by
|
|
504
|
+
* class. All call sites for any method with the same name across all classes
|
|
505
|
+
* are returned. This is intentionally conservative (sound but imprecise).
|
|
280
506
|
*/
|
|
281
507
|
async function findCallsToNode(
|
|
282
508
|
backend: RFDBServerBackend,
|
|
283
|
-
targetId: string
|
|
509
|
+
targetId: string,
|
|
510
|
+
methodName?: string
|
|
284
511
|
): Promise<NodeInfo[]> {
|
|
285
512
|
const calls: NodeInfo[] = [];
|
|
513
|
+
const seen = new Set<string>();
|
|
286
514
|
|
|
287
515
|
try {
|
|
288
|
-
// Get incoming CALLS edges
|
|
289
516
|
const edges = await backend.getIncomingEdges(targetId, ['CALLS']);
|
|
290
517
|
|
|
291
518
|
for (const edge of edges) {
|
|
292
519
|
const callNode = await backend.getNode(edge.src);
|
|
293
|
-
if (callNode) {
|
|
520
|
+
if (callNode && !seen.has(callNode.id)) {
|
|
521
|
+
seen.add(callNode.id);
|
|
294
522
|
calls.push({
|
|
295
523
|
id: callNode.id,
|
|
296
524
|
type: callNode.type || 'CALL',
|
|
@@ -300,8 +528,43 @@ async function findCallsToNode(
|
|
|
300
528
|
});
|
|
301
529
|
}
|
|
302
530
|
}
|
|
303
|
-
} catch {
|
|
304
|
-
|
|
531
|
+
} catch (err) {
|
|
532
|
+
process.stderr.write(`[grafema impact] Warning: CALLS edge query failed for ${targetId}: ${err}\n`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Fallback: CALL nodes with matching method attribute but no CALLS edge.
|
|
536
|
+
// Only runs when methodName is provided (i.e., for initial target IDs only).
|
|
537
|
+
// Known imprecision: matches by bare method name across all classes — may include
|
|
538
|
+
// call sites from unrelated classes that happen to share the method name.
|
|
539
|
+
if (methodName) {
|
|
540
|
+
try {
|
|
541
|
+
const callNodeIds = await backend.findByAttr({ nodeType: 'CALL', method: methodName });
|
|
542
|
+
const newMatches: string[] = [];
|
|
543
|
+
for (const id of callNodeIds) {
|
|
544
|
+
if (!seen.has(id)) {
|
|
545
|
+
seen.add(id);
|
|
546
|
+
newMatches.push(id);
|
|
547
|
+
const callNode = await backend.getNode(id);
|
|
548
|
+
if (callNode) {
|
|
549
|
+
calls.push({
|
|
550
|
+
id: callNode.id,
|
|
551
|
+
type: callNode.type || 'CALL',
|
|
552
|
+
name: callNode.name || '',
|
|
553
|
+
file: callNode.file || '',
|
|
554
|
+
line: callNode.line,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (newMatches.length > 0) {
|
|
560
|
+
process.stderr.write(
|
|
561
|
+
`[grafema impact] Note: name-only fallback matched ${newMatches.length} unresolved call(s) for '${methodName}' — may include calls from unrelated classes\n`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
} catch (err) {
|
|
565
|
+
process.stderr.write(`[grafema impact] Warning: findByAttr fallback failed for '${methodName}': ${err}\n`);
|
|
566
|
+
|
|
567
|
+
}
|
|
305
568
|
}
|
|
306
569
|
|
|
307
570
|
return calls;
|
package/src/commands/init.ts
CHANGED
|
@@ -9,44 +9,42 @@ import { spawn } from 'child_process';
|
|
|
9
9
|
import { createInterface } from 'readline';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { stringify as stringifyYAML } from 'yaml';
|
|
12
|
-
import {
|
|
12
|
+
import { GRAFEMA_VERSION, getSchemaVersion } from '@grafema/util';
|
|
13
13
|
import { installSkill } from './setup-skill.js';
|
|
14
14
|
|
|
15
15
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Generate config.yaml content
|
|
19
|
-
*
|
|
18
|
+
* Generate config.yaml content.
|
|
19
|
+
* Minimal config — the Rust orchestrator has its own built-in analysis pipeline.
|
|
20
|
+
* Only emit fields the user might want to customize (include/exclude/services).
|
|
20
21
|
*/
|
|
21
|
-
function generateConfigYAML(): string {
|
|
22
|
-
|
|
22
|
+
function generateConfigYAML(isTypeScript: boolean): string {
|
|
23
|
+
const extensions = isTypeScript ? '*.{ts,tsx,js,jsx}' : '*.{js,jsx,mjs,cjs}';
|
|
23
24
|
const config = {
|
|
24
25
|
version: getSchemaVersion(GRAFEMA_VERSION),
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
root: '..',
|
|
27
|
+
include: [`src/**/${extensions}`],
|
|
28
|
+
exclude: [
|
|
29
|
+
'**/*.test.*',
|
|
30
|
+
'**/__tests__/**',
|
|
31
|
+
'**/node_modules/**',
|
|
32
|
+
'**/dist/**',
|
|
33
|
+
],
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
// Convert to YAML
|
|
30
36
|
const yaml = stringifyYAML(config, {
|
|
31
|
-
lineWidth: 0,
|
|
37
|
+
lineWidth: 0,
|
|
32
38
|
});
|
|
33
39
|
|
|
34
|
-
// Add header comment
|
|
35
40
|
return `# Grafema Configuration
|
|
36
41
|
# Documentation: https://github.com/grafema/grafema#configuration
|
|
37
42
|
|
|
38
43
|
${yaml}
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# include: # Only analyze files matching these patterns
|
|
44
|
-
# - "src/**/*.{ts,js,tsx,jsx}"
|
|
45
|
-
#
|
|
46
|
-
# exclude: # Skip files matching these patterns (takes precedence over include)
|
|
47
|
-
# - "**/*.test.ts"
|
|
48
|
-
# - "**/__tests__/**"
|
|
49
|
-
# - "**/node_modules/**"
|
|
44
|
+
# services: # Explicit service definitions (overrides auto-discovery)
|
|
45
|
+
# - name: "api"
|
|
46
|
+
# path: "."
|
|
47
|
+
# entryPoint: "src/index.ts"
|
|
50
48
|
`;
|
|
51
49
|
}
|
|
52
50
|
|
|
@@ -169,7 +167,7 @@ Examples:
|
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
// Write config
|
|
172
|
-
const configContent = generateConfigYAML();
|
|
170
|
+
const configContent = generateConfigYAML(isTypeScript);
|
|
173
171
|
writeFileSync(configPath, configContent);
|
|
174
172
|
console.log('✓ Created .grafema/config.yaml');
|
|
175
173
|
|
package/src/commands/ls.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Command } from 'commander';
|
|
|
14
14
|
import { resolve, join } from 'path';
|
|
15
15
|
import { toRelativeDisplay } from '../utils/pathUtils.js';
|
|
16
16
|
import { existsSync } from 'fs';
|
|
17
|
-
import { RFDBServerBackend } from '@grafema/
|
|
17
|
+
import { RFDBServerBackend } from '@grafema/util';
|
|
18
18
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
19
19
|
import { Spinner } from '../utils/spinner.js';
|
|
20
20
|
|
|
@@ -65,7 +65,7 @@ Discover available types:
|
|
|
65
65
|
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const backend = new RFDBServerBackend({ dbPath });
|
|
68
|
+
const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
|
|
69
69
|
await backend.connect();
|
|
70
70
|
|
|
71
71
|
const spinner = new Spinner('Querying graph...');
|
package/src/commands/overview.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { resolve, join } from 'path';
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
|
-
import { RFDBServerBackend } from '@grafema/
|
|
8
|
+
import { RFDBServerBackend } from '@grafema/util';
|
|
9
9
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
10
10
|
|
|
11
11
|
|
|
@@ -28,7 +28,7 @@ Examples:
|
|
|
28
28
|
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const backend = new RFDBServerBackend({ dbPath });
|
|
31
|
+
const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
|
|
32
32
|
await backend.connect();
|
|
33
33
|
|
|
34
34
|
try {
|