@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.
Files changed (133) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/analyze.d.ts.map +1 -1
  4. package/dist/commands/analyze.js +2 -4
  5. package/dist/commands/analyze.js.map +1 -1
  6. package/dist/commands/analyzeAction.d.ts +5 -3
  7. package/dist/commands/analyzeAction.d.ts.map +1 -1
  8. package/dist/commands/analyzeAction.js +109 -151
  9. package/dist/commands/analyzeAction.js.map +1 -1
  10. package/dist/commands/check.d.ts +1 -1
  11. package/dist/commands/check.js +4 -4
  12. package/dist/commands/check.js.map +1 -1
  13. package/dist/commands/context.js +2 -2
  14. package/dist/commands/context.js.map +1 -1
  15. package/dist/commands/coverage.js +2 -2
  16. package/dist/commands/coverage.js.map +1 -1
  17. package/dist/commands/describe.d.ts +13 -0
  18. package/dist/commands/describe.d.ts.map +1 -0
  19. package/dist/commands/describe.js +131 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/doctor/checks.d.ts +6 -1
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +128 -13
  24. package/dist/commands/doctor/checks.js.map +1 -1
  25. package/dist/commands/doctor.d.ts +10 -9
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +12 -10
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/explain.js +2 -2
  30. package/dist/commands/explain.js.map +1 -1
  31. package/dist/commands/file.js +2 -2
  32. package/dist/commands/file.js.map +1 -1
  33. package/dist/commands/get.js +2 -2
  34. package/dist/commands/get.js.map +1 -1
  35. package/dist/commands/git-ingest.d.ts +6 -0
  36. package/dist/commands/git-ingest.d.ts.map +1 -0
  37. package/dist/commands/git-ingest.js +46 -0
  38. package/dist/commands/git-ingest.js.map +1 -0
  39. package/dist/commands/impact.d.ts.map +1 -1
  40. package/dist/commands/impact.js +276 -50
  41. package/dist/commands/impact.js.map +1 -1
  42. package/dist/commands/init.d.ts.map +1 -1
  43. package/dist/commands/init.js +20 -22
  44. package/dist/commands/init.js.map +1 -1
  45. package/dist/commands/ls.js +2 -2
  46. package/dist/commands/ls.js.map +1 -1
  47. package/dist/commands/overview.js +2 -2
  48. package/dist/commands/overview.js.map +1 -1
  49. package/dist/commands/query.d.ts +1 -1
  50. package/dist/commands/query.d.ts.map +1 -1
  51. package/dist/commands/query.js +169 -7
  52. package/dist/commands/query.js.map +1 -1
  53. package/dist/commands/schema.js +2 -2
  54. package/dist/commands/schema.js.map +1 -1
  55. package/dist/commands/server.d.ts.map +1 -1
  56. package/dist/commands/server.js +122 -76
  57. package/dist/commands/server.js.map +1 -1
  58. package/dist/commands/stats.js +2 -2
  59. package/dist/commands/stats.js.map +1 -1
  60. package/dist/commands/tldr.d.ts +12 -0
  61. package/dist/commands/tldr.d.ts.map +1 -0
  62. package/dist/commands/tldr.js +81 -0
  63. package/dist/commands/tldr.js.map +1 -0
  64. package/dist/commands/trace.d.ts +1 -1
  65. package/dist/commands/trace.d.ts.map +1 -1
  66. package/dist/commands/trace.js +17 -133
  67. package/dist/commands/trace.js.map +1 -1
  68. package/dist/commands/types.js +2 -2
  69. package/dist/commands/types.js.map +1 -1
  70. package/dist/commands/who.d.ts +12 -0
  71. package/dist/commands/who.d.ts.map +1 -0
  72. package/dist/commands/who.js +184 -0
  73. package/dist/commands/who.js.map +1 -0
  74. package/dist/commands/why.d.ts +12 -0
  75. package/dist/commands/why.d.ts.map +1 -0
  76. package/dist/commands/why.js +118 -0
  77. package/dist/commands/why.js.map +1 -0
  78. package/dist/commands/wtf.d.ts +12 -0
  79. package/dist/commands/wtf.d.ts.map +1 -0
  80. package/dist/commands/wtf.js +117 -0
  81. package/dist/commands/wtf.js.map +1 -0
  82. package/dist/plugins/builtinPlugins.d.ts +1 -9
  83. package/dist/plugins/builtinPlugins.d.ts.map +1 -1
  84. package/dist/plugins/builtinPlugins.js +2 -67
  85. package/dist/plugins/builtinPlugins.js.map +1 -1
  86. package/dist/plugins/pluginLoader.d.ts +1 -15
  87. package/dist/plugins/pluginLoader.d.ts.map +1 -1
  88. package/dist/plugins/pluginLoader.js +2 -100
  89. package/dist/plugins/pluginLoader.js.map +1 -1
  90. package/dist/plugins/pluginResolver.js +3 -3
  91. package/dist/utils/progressRenderer.d.ts +15 -1
  92. package/dist/utils/progressRenderer.d.ts.map +1 -1
  93. package/dist/utils/progressRenderer.js +19 -3
  94. package/dist/utils/progressRenderer.js.map +1 -1
  95. package/dist/utils/queryHints.d.ts +6 -0
  96. package/dist/utils/queryHints.d.ts.map +1 -0
  97. package/dist/utils/queryHints.js +36 -0
  98. package/dist/utils/queryHints.js.map +1 -0
  99. package/package.json +4 -4
  100. package/skills/grafema-codebase-analysis/SKILL.md +1 -1
  101. package/src/cli.ts +14 -0
  102. package/src/commands/analyze.ts +2 -4
  103. package/src/commands/analyzeAction.ts +122 -168
  104. package/src/commands/check.ts +5 -5
  105. package/src/commands/context.ts +3 -3
  106. package/src/commands/coverage.ts +2 -2
  107. package/src/commands/describe.ts +160 -0
  108. package/src/commands/doctor/checks.ts +153 -10
  109. package/src/commands/doctor.ts +13 -9
  110. package/src/commands/explain.ts +2 -2
  111. package/src/commands/explore.tsx +2 -2
  112. package/src/commands/file.ts +3 -3
  113. package/src/commands/get.ts +2 -2
  114. package/src/commands/git-ingest.ts +49 -0
  115. package/src/commands/impact.ts +318 -55
  116. package/src/commands/init.ts +20 -22
  117. package/src/commands/ls.ts +2 -2
  118. package/src/commands/overview.ts +2 -2
  119. package/src/commands/query.ts +197 -7
  120. package/src/commands/schema.ts +2 -2
  121. package/src/commands/server.ts +136 -84
  122. package/src/commands/stats.ts +2 -2
  123. package/src/commands/tldr.ts +103 -0
  124. package/src/commands/trace.ts +19 -161
  125. package/src/commands/types.ts +2 -2
  126. package/src/commands/who.ts +215 -0
  127. package/src/commands/why.ts +134 -0
  128. package/src/commands/wtf.ts +140 -0
  129. package/src/plugins/builtinPlugins.ts +1 -108
  130. package/src/plugins/pluginLoader.ts +1 -123
  131. package/src/plugins/pluginResolver.js +3 -3
  132. package/src/utils/progressRenderer.ts +34 -4
  133. package/src/utils/queryHints.ts +46 -0
@@ -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/core';
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
- console.log(`Analyzing impact of changing ${name}...`);
69
- console.log('');
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
- console.log(`No ${type || 'node'} "${name}" found`);
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
- * Analyze impact of changing a node
161
+ * Extract bare method name from a possibly-qualified name.
162
+ * "RFDBServerBackend.addNode" -> "addNode"
163
+ * "addNode" -> "addNode"
156
164
  */
157
- async function analyzeImpact(
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<ImpactResult> {
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
- // Find what calls this node
195
- // First, find CALL nodes that have this as target
196
- const containingCalls = await findCallsToNode(backend, id);
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
- // Filter out internal callers (methods of the same class)
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
- // Ignore errors
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
- return {
245
- target,
246
- directCallers,
247
- transitiveCallers,
248
- affectedModules,
249
- callChains,
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 IDs for a class
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
- // Ignore errors
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
- // Ignore
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;
@@ -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 { DEFAULT_CONFIG, GRAFEMA_VERSION, getSchemaVersion } from '@grafema/core';
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 with commented future features.
19
- * Only includes implemented features (plugins).
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
- // Start with working default config
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
- // Plugin list (fully implemented)
26
- plugins: DEFAULT_CONFIG.plugins,
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, // Don't wrap long lines
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
- # File filtering patterns (optional)
40
- # By default, Grafema follows imports from package.json entry points.
41
- # Use these patterns to control which files are analyzed:
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
 
@@ -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/core';
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...');
@@ -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/core';
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 {