@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
@@ -0,0 +1,103 @@
1
+ /**
2
+ * tldr command — "What's in this file?"
3
+ *
4
+ * Human-first alias for `describe`. Shows notation DSL for a file.
5
+ *
6
+ * Usage:
7
+ * grafema tldr src/auth.ts # File overview in DSL
8
+ * grafema tldr src/auth.ts --save # Save as src/auth.ts.tldr
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { resolve, join } from 'path';
13
+ import { existsSync, writeFileSync } from 'fs';
14
+ import {
15
+ RFDBServerBackend,
16
+ renderNotation,
17
+ extractSubgraph,
18
+ } from '@grafema/util';
19
+ import type { DescribeOptions } from '@grafema/util';
20
+ import { exitWithError } from '../utils/errorFormatter.js';
21
+ import { Spinner } from '../utils/spinner.js';
22
+
23
+ interface TldrCommandOptions {
24
+ project: string;
25
+ save?: boolean;
26
+ ext: string;
27
+ }
28
+
29
+ export const tldrCommand = new Command('tldr')
30
+ .description("What's in this file? — compact DSL overview")
31
+ .argument('<file>', 'File path to describe')
32
+ .option('-p, --project <path>', 'Project path', '.')
33
+ .option('-s, --save', 'Save output to <file>.<ext>')
34
+ .option('--ext <ext>', 'File extension for --save', '.tldr')
35
+ .addHelpText('after', `
36
+ Examples:
37
+ grafema tldr src/auth.ts File overview in DSL notation
38
+ grafema tldr src/auth.ts --save Save as src/auth.ts.tldr
39
+ grafema tldr src/app.ts --save --ext .md Save as src/app.ts.md
40
+ `)
41
+ .action(async (file: string, options: TldrCommandOptions) => {
42
+ const projectPath = resolve(options.project);
43
+ const grafemaDir = join(projectPath, '.grafema');
44
+ const dbPath = join(grafemaDir, 'graph.rfdb');
45
+
46
+ if (!existsSync(dbPath)) {
47
+ exitWithError('No graph database found', ['Run: grafema analyze']);
48
+ }
49
+
50
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
51
+ await backend.connect();
52
+
53
+ const spinner = new Spinner('Loading file overview...');
54
+ spinner.start();
55
+
56
+ try {
57
+ // Find MODULE node for the file
58
+ let node = null;
59
+ for await (const n of backend.queryNodes({ file, type: 'MODULE' })) {
60
+ node = n;
61
+ break;
62
+ }
63
+
64
+ if (!node) {
65
+ spinner.stop();
66
+ exitWithError(`File not found in graph: "${file}"`, [
67
+ 'Check that the file was included in analysis',
68
+ 'Run: grafema analyze',
69
+ ]);
70
+ return;
71
+ }
72
+
73
+ // Extract subgraph at depth 2 (nested + fold)
74
+ const subgraph = await extractSubgraph(backend, node.id, 2);
75
+
76
+ // Render notation
77
+ const describeOptions: DescribeOptions = {
78
+ depth: 2,
79
+ includeLocations: true,
80
+ };
81
+ const notation = renderNotation(subgraph, describeOptions);
82
+
83
+ spinner.stop();
84
+
85
+ const output = notation.trim()
86
+ ? notation
87
+ : `[${node.type}] ${node.name ?? node.id}\nNo relationships found.`;
88
+
89
+ console.log(output);
90
+
91
+ // Save to file if --save flag
92
+ if (options.save) {
93
+ const ext = options.ext.startsWith('.') ? options.ext : `.${options.ext}`;
94
+ const outputPath = `${file}${ext}`;
95
+ writeFileSync(outputPath, output + '\n', 'utf-8');
96
+ console.log('');
97
+ console.log(`Saved to ${outputPath}`);
98
+ }
99
+ } finally {
100
+ spinner.stop();
101
+ await backend.close();
102
+ }
103
+ });
@@ -10,14 +10,15 @@
10
10
  import { Command } from 'commander';
11
11
  import { isAbsolute, resolve, join } from 'path';
12
12
  import { existsSync } from 'fs';
13
- import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, traceValues, type ValueSource } from '@grafema/core';
14
- import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
13
+ import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, traceValues, traceDataflow, renderTraceNarrative, type ValueSource, type DataflowBackend } from '@grafema/util';
14
+ import { formatNodeDisplay } from '../utils/formatNode.js';
15
15
  import { exitWithError } from '../utils/errorFormatter.js';
16
16
 
17
17
  interface TraceOptions {
18
18
  project: string;
19
19
  json?: boolean;
20
20
  depth: string;
21
+ detail?: 'summary' | 'normal' | 'full';
21
22
  to?: string;
22
23
  fromRoute?: string;
23
24
  }
@@ -73,11 +74,6 @@ interface NodeInfo {
73
74
  value?: unknown;
74
75
  }
75
76
 
76
- interface TraceStep {
77
- node: NodeInfo;
78
- edgeType: string;
79
- depth: number;
80
- }
81
77
 
82
78
  export const traceCommand = new Command('trace')
83
79
  .description('Trace data flow for a variable or to a sink point')
@@ -85,6 +81,7 @@ export const traceCommand = new Command('trace')
85
81
  .option('-p, --project <path>', 'Project path', '.')
86
82
  .option('-j, --json', 'Output as JSON')
87
83
  .option('-d, --depth <n>', 'Max trace depth', '10')
84
+ .option('--detail <level>', 'Level of detail: summary, normal (default), full', 'normal')
88
85
  .option('-t, --to <sink>', 'Sink point: "fn#argIndex.property" (e.g., "addNode#0.type")')
89
86
  .option('-r, --from-route <pattern>', 'Trace from route response (e.g., "GET /status" or "/status")')
90
87
  .addHelpText('after', `
@@ -92,6 +89,7 @@ Examples:
92
89
  grafema trace "userId" Trace all variables named "userId"
93
90
  grafema trace "userId from authenticate" Trace userId within authenticate function
94
91
  grafema trace "config" --depth 5 Limit trace depth to 5 levels
92
+ grafema trace "config" --detail full Show complete chain (no compression)
95
93
  grafema trace "apiKey" --json Output trace as JSON
96
94
  grafema trace --to "addNode#0.type" Trace values reaching sink point
97
95
  grafema trace --from-route "GET /status" Trace values from route response
@@ -106,7 +104,7 @@ Examples:
106
104
  exitWithError('No graph database found', ['Run: grafema analyze']);
107
105
  }
108
106
 
109
- const backend = new RFDBServerBackend({ dbPath });
107
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
110
108
  await backend.connect();
111
109
 
112
110
  try {
@@ -143,27 +141,23 @@ Examples:
143
141
  return;
144
142
  }
145
143
 
146
- // Trace each variable
144
+ // Cast backend to DataflowBackend (runtime-compatible)
145
+ const dfDb = backend as unknown as DataflowBackend;
146
+
147
+ // Trace each variable using shared BFS
147
148
  for (const variable of variables) {
148
149
  console.log(formatNodeDisplay(variable, { projectPath }));
149
150
  console.log('');
150
151
 
151
- // Trace backwards through ASSIGNED_FROM
152
- const backwardTrace = await traceBackward(backend, variable.id, maxDepth);
153
-
154
- if (backwardTrace.length > 0) {
155
- console.log('Data sources (where value comes from):');
156
- displayTrace(backwardTrace, projectPath, ' ');
157
- }
158
-
159
- // Trace forward through ASSIGNED_FROM (where this value flows to)
160
- const forwardTrace = await traceForward(backend, variable.id, maxDepth);
152
+ const results = await traceDataflow(dfDb, variable.id, {
153
+ direction: 'both',
154
+ maxDepth,
155
+ });
161
156
 
162
- if (forwardTrace.length > 0) {
163
- console.log('');
164
- console.log('Data sinks (where value flows to):');
165
- displayTrace(forwardTrace, projectPath, ' ');
166
- }
157
+ const narrative = renderTraceNarrative(results, variable.name || variable.id, {
158
+ detail: options.detail || 'normal',
159
+ });
160
+ console.log(narrative);
167
161
 
168
162
  // Show value domain if available
169
163
  const sources = await getValueSources(backend, variable.id);
@@ -262,121 +256,6 @@ async function findVariables(
262
256
  return results;
263
257
  }
264
258
 
265
- /**
266
- * Trace backward through ASSIGNED_FROM edges
267
- */
268
- async function traceBackward(
269
- backend: RFDBServerBackend,
270
- startId: string,
271
- maxDepth: number
272
- ): Promise<TraceStep[]> {
273
- const trace: TraceStep[] = [];
274
- const visited = new Set<string>();
275
- const seenNodes = new Set<string>();
276
- const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
277
-
278
- while (queue.length > 0) {
279
- const { id, depth } = queue.shift()!;
280
-
281
- if (visited.has(id) || depth > maxDepth) continue;
282
- visited.add(id);
283
-
284
- try {
285
- const edges = await backend.getOutgoingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
286
-
287
- for (const edge of edges) {
288
- const targetNode = await backend.getNode(edge.dst);
289
- if (!targetNode) continue;
290
-
291
- if (seenNodes.has(targetNode.id)) continue;
292
- seenNodes.add(targetNode.id);
293
-
294
- const nodeInfo: NodeInfo = {
295
- id: targetNode.id,
296
- type: targetNode.type || 'UNKNOWN',
297
- name: targetNode.name || '',
298
- file: targetNode.file || '',
299
- line: targetNode.line,
300
- value: targetNode.value,
301
- };
302
-
303
- trace.push({
304
- node: nodeInfo,
305
- edgeType: edge.type,
306
- depth: depth + 1,
307
- });
308
-
309
- // Continue tracing unless we hit a leaf
310
- const leafTypes = ['LITERAL', 'PARAMETER', 'EXTERNAL_MODULE'];
311
- if (!leafTypes.includes(nodeInfo.type)) {
312
- queue.push({ id: targetNode.id, depth: depth + 1 });
313
- }
314
- }
315
- } catch {
316
- // Ignore errors
317
- }
318
- }
319
-
320
- return trace;
321
- }
322
-
323
- /**
324
- * Trace forward - find what uses this variable
325
- */
326
- async function traceForward(
327
- backend: RFDBServerBackend,
328
- startId: string,
329
- maxDepth: number
330
- ): Promise<TraceStep[]> {
331
- const trace: TraceStep[] = [];
332
- const visited = new Set<string>();
333
- const seenNodes = new Set<string>();
334
- const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
335
-
336
- while (queue.length > 0) {
337
- const { id, depth } = queue.shift()!;
338
-
339
- if (visited.has(id) || depth > maxDepth) continue;
340
- visited.add(id);
341
-
342
- try {
343
- // Find nodes that get their value FROM this node
344
- const edges = await backend.getIncomingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
345
-
346
- for (const edge of edges) {
347
- const sourceNode = await backend.getNode(edge.src);
348
- if (!sourceNode) continue;
349
-
350
- if (seenNodes.has(sourceNode.id)) continue;
351
- seenNodes.add(sourceNode.id);
352
-
353
- const nodeInfo: NodeInfo = {
354
- id: sourceNode.id,
355
- type: sourceNode.type || 'UNKNOWN',
356
- name: sourceNode.name || '',
357
- file: sourceNode.file || '',
358
- line: sourceNode.line,
359
- };
360
-
361
- trace.push({
362
- node: nodeInfo,
363
- edgeType: edge.type,
364
- depth: depth + 1,
365
- });
366
-
367
- // Continue forward
368
- if (depth < maxDepth - 1) {
369
- queue.push({ id: sourceNode.id, depth: depth + 1 });
370
- }
371
- }
372
- } catch {
373
- // Ignore errors
374
- }
375
- }
376
-
377
- return trace;
378
- }
379
-
380
259
  /**
381
260
  * Get immediate value sources (for "possible values" display)
382
261
  */
@@ -409,27 +288,6 @@ async function getValueSources(
409
288
  return sources;
410
289
  }
411
290
 
412
- /**
413
- * Display trace results with semantic IDs
414
- */
415
- function displayTrace(trace: TraceStep[], _projectPath: string, indent: string): void {
416
- // Group by depth
417
- const byDepth = new Map<number, TraceStep[]>();
418
- for (const step of trace) {
419
- if (!byDepth.has(step.depth)) {
420
- byDepth.set(step.depth, []);
421
- }
422
- byDepth.get(step.depth)!.push(step);
423
- }
424
-
425
- for (const [_depth, steps] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
426
- for (const step of steps) {
427
- const valueStr = step.node.value !== undefined ? ` = ${JSON.stringify(step.node.value)}` : '';
428
- console.log(`${indent}<- ${step.node.name || step.node.type} (${step.node.type})${valueStr}`);
429
- console.log(`${indent} ${formatNodeInline(step.node)}`);
430
- }
431
- }
432
- }
433
291
 
434
292
  // =============================================================================
435
293
  // SINK-BASED TRACE IMPLEMENTATION (REG-230)
@@ -596,7 +454,7 @@ async function extractProperty(
596
454
 
597
455
  /**
598
456
  * Trace a node to its literal values.
599
- * Uses shared traceValues utility from @grafema/core (REG-244).
457
+ * Uses shared traceValues utility from @grafema/util (REG-244).
600
458
  *
601
459
  * @param backend - RFDBServerBackend for graph queries
602
460
  * @param nodeId - Starting node ID
@@ -11,7 +11,7 @@
11
11
  import { Command } from 'commander';
12
12
  import { resolve, join } from 'path';
13
13
  import { existsSync } from 'fs';
14
- import { RFDBServerBackend } from '@grafema/core';
14
+ import { RFDBServerBackend } from '@grafema/util';
15
15
  import { exitWithError } from '../utils/errorFormatter.js';
16
16
 
17
17
  interface TypesOptions {
@@ -45,7 +45,7 @@ Use with query --type:
45
45
  exitWithError('No graph database found', ['Run: grafema analyze']);
46
46
  }
47
47
 
48
- const backend = new RFDBServerBackend({ dbPath });
48
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
49
49
  await backend.connect();
50
50
 
51
51
  try {
@@ -0,0 +1,215 @@
1
+ /**
2
+ * who command — "Who uses this?"
3
+ *
4
+ * Find all callers and references to a symbol.
5
+ *
6
+ * Usage:
7
+ * grafema who authenticate # Who calls authenticate()?
8
+ * grafema who UserService.findById # Who calls this method?
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { resolve, join, isAbsolute, relative } from 'path';
13
+ import { existsSync } from 'fs';
14
+ import { RFDBServerBackend } from '@grafema/util';
15
+ import { exitWithError } from '../utils/errorFormatter.js';
16
+ import { Spinner } from '../utils/spinner.js';
17
+
18
+ interface WhoCommandOptions {
19
+ project: string;
20
+ json?: boolean;
21
+ }
22
+
23
+ interface CallerInfo {
24
+ file: string;
25
+ line: number | undefined;
26
+ callerName: string;
27
+ resolved: boolean;
28
+ }
29
+
30
+ export const whoCommand = new Command('who')
31
+ .description('Who uses this? — find all callers/references to a symbol')
32
+ .argument('<symbol>', 'Function or method name')
33
+ .option('-p, --project <path>', 'Project path', '.')
34
+ .option('-j, --json', 'Output as JSON')
35
+ .addHelpText('after', `
36
+ Examples:
37
+ grafema who authenticate Who calls authenticate()?
38
+ grafema who UserService.findById Who calls this method?
39
+ grafema who handleRequest --json Output as JSON
40
+ `)
41
+ .action(async (symbol: string, options: WhoCommandOptions) => {
42
+ const projectPath = resolve(options.project);
43
+ const grafemaDir = join(projectPath, '.grafema');
44
+ const dbPath = join(grafemaDir, 'graph.rfdb');
45
+
46
+ if (!existsSync(dbPath)) {
47
+ exitWithError('No graph database found', ['Run: grafema analyze']);
48
+ }
49
+
50
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
51
+ await backend.connect();
52
+
53
+ const spinner = new Spinner('Searching for callers...');
54
+ spinner.start();
55
+
56
+ try {
57
+ const lowerSymbol = symbol.toLowerCase();
58
+ // If symbol has a dot, extract the part after the last dot for method matching
59
+ const dotIndex = symbol.lastIndexOf('.');
60
+ const methodPart = dotIndex >= 0 ? symbol.substring(dotIndex + 1).toLowerCase() : null;
61
+
62
+ // Strategy 1: Find CALL nodes that match the symbol name
63
+ // (same approach as MCP handleFindCalls)
64
+ const matchingCalls: CallerInfo[] = [];
65
+
66
+ for await (const node of backend.queryNodes({ type: 'CALL' as any })) {
67
+ const callName = (node.name || '').toLowerCase();
68
+
69
+ // Match exact name or method part after last dot
70
+ let isMatch = callName === lowerSymbol;
71
+ if (!isMatch && methodPart) {
72
+ const callMethodPart = callName.lastIndexOf('.') >= 0
73
+ ? callName.substring(callName.lastIndexOf('.') + 1)
74
+ : callName;
75
+ isMatch = callMethodPart === methodPart;
76
+ }
77
+ if (!isMatch && !methodPart) {
78
+ // Also match if the call name ends with the symbol (e.g., "obj.authenticate" matches "authenticate")
79
+ const callMethodPart = callName.lastIndexOf('.') >= 0
80
+ ? callName.substring(callName.lastIndexOf('.') + 1)
81
+ : null;
82
+ if (callMethodPart === lowerSymbol) isMatch = true;
83
+ }
84
+
85
+ if (!isMatch) continue;
86
+
87
+ // Check resolution status via CALLS edges
88
+ const edges = await backend.getOutgoingEdges(node.id, ['CALLS']);
89
+ const resolved = edges.length > 0;
90
+
91
+ // Extract caller name from semantic ID
92
+ // Format: "file->SCOPE->TYPE->name" — parent scope is the caller
93
+ const idParts = node.id.split('->');
94
+ let callerName = '<anonymous>';
95
+ // Walk up the scope chain to find a FUNCTION or METHOD parent
96
+ for (let i = idParts.length - 3; i >= 1; i--) {
97
+ if (idParts[i] === 'FUNCTION' || idParts[i] === 'METHOD') {
98
+ callerName = idParts[i + 1] || callerName;
99
+ break;
100
+ }
101
+ }
102
+
103
+ const file = node.file || '';
104
+
105
+ matchingCalls.push({
106
+ file,
107
+ line: node.line,
108
+ callerName,
109
+ resolved,
110
+ });
111
+ }
112
+
113
+ // Strategy 2: Find the target function/method node and check incoming CALLS/READS_FROM edges
114
+ const incomingCallers: CallerInfo[] = [];
115
+ let funcNode = null;
116
+
117
+ // Search for FUNCTION, METHOD, CLASS, or CONSTANT node matching the symbol
118
+ for (const nodeType of ['FUNCTION', 'METHOD', 'CLASS', 'CONSTANT'] as const) {
119
+ for await (const n of backend.queryNodes({ type: nodeType })) {
120
+ const name = (n.name || '').toLowerCase();
121
+ if (name === lowerSymbol || (methodPart && name === methodPart)) {
122
+ funcNode = n;
123
+ break;
124
+ }
125
+ }
126
+ if (funcNode) break;
127
+ }
128
+
129
+ if (funcNode) {
130
+ const incomingEdges = await backend.getIncomingEdges(funcNode.id, ['CALLS', 'READS_FROM', 'IMPORTS_FROM']);
131
+ for (const edge of incomingEdges) {
132
+ const srcNode = await backend.getNode(edge.src);
133
+ if (!srcNode) continue;
134
+
135
+ // Deduplicate — skip if we already found this call via Strategy 1
136
+ if (matchingCalls.some(c => c.file === (srcNode.file || '') && c.line === srcNode.line)) {
137
+ continue;
138
+ }
139
+
140
+ incomingCallers.push({
141
+ file: srcNode.file || '',
142
+ line: srcNode.line,
143
+ callerName: srcNode.name || '<anonymous>',
144
+ resolved: true,
145
+ });
146
+ }
147
+ }
148
+
149
+ // Strategy 3: Find IMPORT_BINDING nodes that import this symbol
150
+ // (for classes/exports that are imported across files/repos)
151
+ const importers: CallerInfo[] = [];
152
+ for await (const n of backend.queryNodes({ type: 'IMPORT_BINDING' as any })) {
153
+ const bindingName = (n.name || '').toLowerCase();
154
+ if (bindingName !== lowerSymbol && !(methodPart && bindingName === methodPart)) continue;
155
+
156
+ // Skip if already found via Strategy 1 or 2
157
+ if (matchingCalls.some(c => c.file === (n.file || '') && c.line === n.line)) continue;
158
+ if (incomingCallers.some(c => c.file === (n.file || '') && c.line === n.line)) continue;
159
+
160
+ importers.push({
161
+ file: n.file || '',
162
+ line: n.line,
163
+ callerName: `imports ${n.name || symbol}`,
164
+ resolved: true,
165
+ });
166
+ }
167
+
168
+ spinner.stop();
169
+
170
+ // Merge results
171
+ const allCallers = [...matchingCalls, ...incomingCallers, ...importers];
172
+
173
+ if (options.json) {
174
+ console.log(JSON.stringify({
175
+ symbol,
176
+ targetNode: funcNode ? { id: funcNode.id, type: funcNode.type, name: funcNode.name, file: funcNode.file } : null,
177
+ callers: allCallers.map(c => ({
178
+ file: c.file,
179
+ line: c.line,
180
+ caller: c.callerName,
181
+ resolved: c.resolved,
182
+ })),
183
+ total: allCallers.length,
184
+ }, null, 2));
185
+ return;
186
+ }
187
+
188
+ if (allCallers.length === 0) {
189
+ console.log(`${symbol} — no callers found`);
190
+ console.log('');
191
+ console.log('Hints:');
192
+ console.log(' - Check the symbol name is correct');
193
+ console.log(' - The function may be exported but unused in analyzed code');
194
+ console.log(' - Use: grafema query "<name>" to verify the symbol exists');
195
+ return;
196
+ }
197
+
198
+ console.log(`${symbol} — ${allCallers.length} caller${allCallers.length === 1 ? '' : 's'}`);
199
+ console.log('');
200
+
201
+ for (const caller of allCallers) {
202
+ const relFile = isAbsolute(caller.file)
203
+ ? relative(projectPath, caller.file)
204
+ : caller.file;
205
+ const location = caller.line ? `${relFile}:${caller.line}` : relFile;
206
+ const status = caller.resolved ? '[resolved]' : '[unresolved]';
207
+ const paddedLocation = location.padEnd(30);
208
+ const paddedCaller = caller.callerName.padEnd(20);
209
+ console.log(` ${paddedLocation} ${paddedCaller} ${status}`);
210
+ }
211
+ } finally {
212
+ spinner.stop();
213
+ await backend.close();
214
+ }
215
+ });