@grafema/cli 0.1.1-alpha → 0.2.1-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 (79) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/commands/analyze.d.ts.map +1 -1
  3. package/dist/commands/analyze.js +69 -11
  4. package/dist/commands/check.d.ts +6 -0
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +177 -1
  7. package/dist/commands/coverage.d.ts.map +1 -1
  8. package/dist/commands/coverage.js +7 -0
  9. package/dist/commands/doctor/checks.d.ts +55 -0
  10. package/dist/commands/doctor/checks.d.ts.map +1 -0
  11. package/dist/commands/doctor/checks.js +534 -0
  12. package/dist/commands/doctor/output.d.ts +20 -0
  13. package/dist/commands/doctor/output.d.ts.map +1 -0
  14. package/dist/commands/doctor/output.js +94 -0
  15. package/dist/commands/doctor/types.d.ts +42 -0
  16. package/dist/commands/doctor/types.d.ts.map +1 -0
  17. package/dist/commands/doctor/types.js +4 -0
  18. package/dist/commands/doctor.d.ts +17 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +80 -0
  21. package/dist/commands/explain.d.ts +16 -0
  22. package/dist/commands/explain.d.ts.map +1 -0
  23. package/dist/commands/explain.js +145 -0
  24. package/dist/commands/explore.d.ts +7 -1
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +204 -85
  27. package/dist/commands/get.d.ts.map +1 -1
  28. package/dist/commands/get.js +16 -4
  29. package/dist/commands/impact.d.ts.map +1 -1
  30. package/dist/commands/impact.js +48 -50
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +93 -15
  33. package/dist/commands/ls.d.ts +14 -0
  34. package/dist/commands/ls.d.ts.map +1 -0
  35. package/dist/commands/ls.js +132 -0
  36. package/dist/commands/overview.d.ts.map +1 -1
  37. package/dist/commands/overview.js +15 -2
  38. package/dist/commands/query.d.ts +98 -0
  39. package/dist/commands/query.d.ts.map +1 -1
  40. package/dist/commands/query.js +549 -136
  41. package/dist/commands/schema.d.ts +13 -0
  42. package/dist/commands/schema.d.ts.map +1 -0
  43. package/dist/commands/schema.js +279 -0
  44. package/dist/commands/server.d.ts.map +1 -1
  45. package/dist/commands/server.js +13 -6
  46. package/dist/commands/stats.d.ts.map +1 -1
  47. package/dist/commands/stats.js +7 -0
  48. package/dist/commands/trace.d.ts +73 -0
  49. package/dist/commands/trace.d.ts.map +1 -1
  50. package/dist/commands/trace.js +500 -5
  51. package/dist/commands/types.d.ts +12 -0
  52. package/dist/commands/types.d.ts.map +1 -0
  53. package/dist/commands/types.js +79 -0
  54. package/dist/utils/formatNode.d.ts +13 -0
  55. package/dist/utils/formatNode.d.ts.map +1 -1
  56. package/dist/utils/formatNode.js +35 -2
  57. package/package.json +3 -3
  58. package/src/cli.ts +10 -0
  59. package/src/commands/analyze.ts +84 -9
  60. package/src/commands/check.ts +201 -0
  61. package/src/commands/coverage.ts +7 -0
  62. package/src/commands/doctor/checks.ts +612 -0
  63. package/src/commands/doctor/output.ts +115 -0
  64. package/src/commands/doctor/types.ts +45 -0
  65. package/src/commands/doctor.ts +106 -0
  66. package/src/commands/explain.ts +173 -0
  67. package/src/commands/explore.tsx +247 -97
  68. package/src/commands/get.ts +20 -6
  69. package/src/commands/impact.ts +55 -61
  70. package/src/commands/init.ts +101 -14
  71. package/src/commands/ls.ts +166 -0
  72. package/src/commands/overview.ts +15 -2
  73. package/src/commands/query.ts +643 -149
  74. package/src/commands/schema.ts +345 -0
  75. package/src/commands/server.ts +13 -6
  76. package/src/commands/stats.ts +7 -0
  77. package/src/commands/trace.ts +647 -6
  78. package/src/commands/types.ts +94 -0
  79. package/src/utils/formatNode.ts +42 -2
@@ -1,5 +1,11 @@
1
1
  /**
2
- * Explore command - Interactive TUI for graph navigation
2
+ * Explore command - Interactive TUI or batch mode for graph navigation
3
+ *
4
+ * Interactive mode: grafema explore [start]
5
+ * Batch mode:
6
+ * grafema explore --query "functionName"
7
+ * grafema explore --callers "functionName"
8
+ * grafema explore --callees "functionName"
3
9
  */
4
10
 
5
11
  import { Command } from 'commander';
@@ -8,11 +14,22 @@ import { existsSync } from 'fs';
8
14
  import { execSync } from 'child_process';
9
15
  import React, { useState, useEffect } from 'react';
10
16
  import { render, Box, Text, useInput, useApp } from 'ink';
11
- import { RFDBServerBackend } from '@grafema/core';
17
+ import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore, findCallsInFunction as findCallsInFunctionCore } from '@grafema/core';
12
18
  import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
13
19
  import { exitWithError } from '../utils/errorFormatter.js';
14
20
 
15
21
  // Types
22
+ interface ExploreOptions {
23
+ project: string;
24
+ // Batch mode flags
25
+ query?: string;
26
+ callers?: string;
27
+ callees?: string;
28
+ depth?: string;
29
+ json?: boolean;
30
+ format?: 'json' | 'text';
31
+ }
32
+
16
33
  interface NodeInfo {
17
34
  id: string;
18
35
  type: string;
@@ -769,11 +786,17 @@ async function getCallers(backend: RFDBServerBackend, nodeId: string, limit: num
769
786
  const callNode = await backend.getNode(edge.src);
770
787
  if (!callNode) continue;
771
788
 
772
- const containingFunc = await findContainingFunction(backend, callNode.id);
789
+ const containingFunc = await findContainingFunctionCore(backend, callNode.id);
773
790
 
774
791
  if (containingFunc && !seen.has(containingFunc.id)) {
775
792
  seen.add(containingFunc.id);
776
- callers.push(containingFunc);
793
+ callers.push({
794
+ id: containingFunc.id,
795
+ type: containingFunc.type,
796
+ name: containingFunc.name,
797
+ file: containingFunc.file || '',
798
+ line: containingFunc.line,
799
+ });
777
800
  }
778
801
  }
779
802
  } catch {
@@ -788,21 +811,22 @@ async function getCallees(backend: RFDBServerBackend, nodeId: string, limit: num
788
811
  const seen = new Set<string>();
789
812
 
790
813
  try {
791
- const callNodes = await findCallsInFunction(backend, nodeId);
814
+ // Use shared utility from @grafema/core
815
+ const calls = await findCallsInFunctionCore(backend, nodeId);
792
816
 
793
- for (const callNode of callNodes) {
817
+ for (const call of calls) {
794
818
  if (callees.length >= limit) break;
795
819
 
796
- const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
797
-
798
- for (const edge of callEdges) {
799
- if (callees.length >= limit) break;
800
-
801
- const targetNode = await backend.getNode(edge.dst);
802
- if (!targetNode || seen.has(targetNode.id)) continue;
803
-
804
- seen.add(targetNode.id);
805
- callees.push(extractNodeInfo(targetNode));
820
+ // Only include resolved calls with targets
821
+ if (call.resolved && call.target && !seen.has(call.target.id)) {
822
+ seen.add(call.target.id);
823
+ callees.push({
824
+ id: call.target.id,
825
+ type: 'FUNCTION',
826
+ name: call.target.name || '',
827
+ file: call.target.file || '',
828
+ line: call.target.line,
829
+ });
806
830
  }
807
831
  }
808
832
  } catch {
@@ -812,82 +836,6 @@ async function getCallees(backend: RFDBServerBackend, nodeId: string, limit: num
812
836
  return callees;
813
837
  }
814
838
 
815
- async function findContainingFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo | null> {
816
- const visited = new Set<string>();
817
- const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
818
-
819
- while (queue.length > 0) {
820
- const { id, depth } = queue.shift()!;
821
- if (visited.has(id) || depth > 15) continue;
822
- visited.add(id);
823
-
824
- try {
825
- const edges = await backend.getIncomingEdges(id, null);
826
-
827
- for (const edge of edges) {
828
- const edgeType = (edge as any).edgeType || (edge as any).type;
829
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
830
-
831
- const parent = await backend.getNode(edge.src);
832
- if (!parent || visited.has(parent.id)) continue;
833
-
834
- const parentType = (parent as any).type || (parent as any).nodeType;
835
-
836
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
837
- return extractNodeInfo(parent);
838
- }
839
-
840
- queue.push({ id: parent.id, depth: depth + 1 });
841
- }
842
- } catch {
843
- // Ignore
844
- }
845
- }
846
-
847
- return null;
848
- }
849
-
850
- async function findCallsInFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo[]> {
851
- const calls: NodeInfo[] = [];
852
- const visited = new Set<string>();
853
- const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
854
-
855
- while (queue.length > 0) {
856
- const { id, depth } = queue.shift()!;
857
- if (visited.has(id) || depth > 10) continue;
858
- visited.add(id);
859
-
860
- try {
861
- const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
862
-
863
- for (const edge of edges) {
864
- const child = await backend.getNode(edge.dst);
865
- if (!child) continue;
866
-
867
- const childType = (child as any).type || (child as any).nodeType;
868
-
869
- if (childType === 'CALL') {
870
- calls.push({
871
- id: child.id,
872
- type: 'CALL',
873
- name: (child as any).name || '',
874
- file: (child as any).file || '',
875
- line: (child as any).line,
876
- });
877
- }
878
-
879
- if (childType !== 'FUNCTION' && childType !== 'CLASS') {
880
- queue.push({ id: child.id, depth: depth + 1 });
881
- }
882
- }
883
- } catch {
884
- // Ignore
885
- }
886
- }
887
-
888
- return calls;
889
- }
890
-
891
839
  async function searchNode(backend: RFDBServerBackend, query: string): Promise<NodeInfo | null> {
892
840
  const results = await searchNodes(backend, query, 1);
893
841
  return results[0] || null;
@@ -1020,18 +968,199 @@ async function findStartNode(backend: RFDBServerBackend, startName: string | nul
1020
968
  return bestNode;
1021
969
  }
1022
970
 
1023
- // Command
971
+ // =============================================================================
972
+ // Batch Mode Implementation
973
+ // =============================================================================
974
+
975
+ /**
976
+ * Run explore in batch mode - for AI agents, CI, and scripts
977
+ */
978
+ async function runBatchExplore(
979
+ backend: RFDBServerBackend,
980
+ options: ExploreOptions,
981
+ projectPath: string
982
+ ): Promise<void> {
983
+ const depth = parseInt(options.depth || '3', 10) || 3;
984
+ const useJson = options.json || options.format === 'json' || options.format !== 'text';
985
+
986
+ try {
987
+ if (options.query) {
988
+ // Search mode
989
+ const results = await searchNodes(backend, options.query, 20);
990
+ outputResults(results, 'search', useJson, projectPath);
991
+ } else if (options.callers) {
992
+ // Callers mode
993
+ const target = await searchNode(backend, options.callers);
994
+ if (!target) {
995
+ exitWithError(`Function "${options.callers}" not found`, [
996
+ 'Try: grafema query "partial-name"',
997
+ ]);
998
+ }
999
+ const callers = await getCallersRecursive(backend, target.id, depth);
1000
+ outputResults(callers, 'callers', useJson, projectPath, target);
1001
+ } else if (options.callees) {
1002
+ // Callees mode
1003
+ const target = await searchNode(backend, options.callees);
1004
+ if (!target) {
1005
+ exitWithError(`Function "${options.callees}" not found`, [
1006
+ 'Try: grafema query "partial-name"',
1007
+ ]);
1008
+ }
1009
+ const callees = await getCalleesRecursive(backend, target.id, depth);
1010
+ outputResults(callees, 'callees', useJson, projectPath, target);
1011
+ }
1012
+ } catch (err) {
1013
+ exitWithError(`Explore failed: ${(err as Error).message}`);
1014
+ }
1015
+ }
1016
+
1017
+ /**
1018
+ * Output results in JSON or text format
1019
+ */
1020
+ function outputResults(
1021
+ nodes: NodeInfo[],
1022
+ mode: 'search' | 'callers' | 'callees',
1023
+ useJson: boolean,
1024
+ projectPath: string,
1025
+ target?: NodeInfo
1026
+ ): void {
1027
+ if (useJson) {
1028
+ const output = {
1029
+ mode,
1030
+ target: target ? formatNodeForJson(target, projectPath) : undefined,
1031
+ count: nodes.length,
1032
+ results: nodes.map(n => formatNodeForJson(n, projectPath)),
1033
+ };
1034
+ console.log(JSON.stringify(output, null, 2));
1035
+ } else {
1036
+ // Text format
1037
+ if (target) {
1038
+ console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
1039
+ console.log(`File: ${relative(projectPath, target.file)}${target.line ? `:${target.line}` : ''}`);
1040
+ console.log('');
1041
+ }
1042
+
1043
+ if (nodes.length === 0) {
1044
+ console.log(` (no ${mode} found)`);
1045
+ } else {
1046
+ for (const node of nodes) {
1047
+ const loc = relative(projectPath, node.file);
1048
+ console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
1049
+ }
1050
+ }
1051
+
1052
+ console.log('');
1053
+ console.log(`Total: ${nodes.length}`);
1054
+ }
1055
+ }
1056
+
1057
+ function formatNodeForJson(node: NodeInfo, projectPath: string): object {
1058
+ return {
1059
+ id: node.id,
1060
+ type: node.type,
1061
+ name: node.name,
1062
+ file: relative(projectPath, node.file),
1063
+ line: node.line,
1064
+ async: node.async,
1065
+ exported: node.exported,
1066
+ };
1067
+ }
1068
+
1069
+ /**
1070
+ * Get callers recursively up to specified depth
1071
+ */
1072
+ async function getCallersRecursive(
1073
+ backend: RFDBServerBackend,
1074
+ nodeId: string,
1075
+ maxDepth: number
1076
+ ): Promise<NodeInfo[]> {
1077
+ const results: NodeInfo[] = [];
1078
+ const visited = new Set<string>();
1079
+ const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
1080
+
1081
+ while (queue.length > 0) {
1082
+ const { id, depth } = queue.shift()!;
1083
+ if (visited.has(id) || depth > maxDepth) continue;
1084
+ visited.add(id);
1085
+
1086
+ const callers = await getCallers(backend, id, 50);
1087
+ for (const caller of callers) {
1088
+ if (!visited.has(caller.id)) {
1089
+ results.push(caller);
1090
+ if (depth < maxDepth) {
1091
+ queue.push({ id: caller.id, depth: depth + 1 });
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ return results;
1098
+ }
1099
+
1100
+ /**
1101
+ * Get callees recursively up to specified depth
1102
+ */
1103
+ async function getCalleesRecursive(
1104
+ backend: RFDBServerBackend,
1105
+ nodeId: string,
1106
+ maxDepth: number
1107
+ ): Promise<NodeInfo[]> {
1108
+ const results: NodeInfo[] = [];
1109
+ const visited = new Set<string>();
1110
+ const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
1111
+
1112
+ while (queue.length > 0) {
1113
+ const { id, depth } = queue.shift()!;
1114
+ if (visited.has(id) || depth > maxDepth) continue;
1115
+ visited.add(id);
1116
+
1117
+ const callees = await getCallees(backend, id, 50);
1118
+ for (const callee of callees) {
1119
+ if (!visited.has(callee.id)) {
1120
+ results.push(callee);
1121
+ if (depth < maxDepth) {
1122
+ queue.push({ id: callee.id, depth: depth + 1 });
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ return results;
1129
+ }
1130
+
1131
+ // =============================================================================
1132
+ // Command Definition
1133
+ // =============================================================================
1134
+
1024
1135
  export const exploreCommand = new Command('explore')
1025
- .description('Interactive graph navigation')
1026
- .argument('[start]', 'Starting function name')
1136
+ .description('Interactive graph navigation (TUI) or batch query mode')
1137
+ .argument('[start]', 'Starting function name (for interactive mode)')
1027
1138
  .option('-p, --project <path>', 'Project path', '.')
1028
- .action(async (start: string | undefined, options: { project: string }) => {
1139
+ .option('-q, --query <name>', 'Batch: search for nodes by name')
1140
+ .option('--callers <name>', 'Batch: show callers of function')
1141
+ .option('--callees <name>', 'Batch: show callees of function')
1142
+ .option('-d, --depth <n>', 'Batch: traversal depth', '3')
1143
+ .option('-j, --json', 'Output as JSON (default for batch mode)')
1144
+ .option('--format <type>', 'Output format: json or text')
1145
+ .addHelpText('after', `
1146
+ Examples:
1147
+ grafema explore Interactive TUI mode
1148
+ grafema explore "authenticate" Start TUI at specific function
1149
+ grafema explore --query "User" Batch: search for nodes
1150
+ grafema explore --callers "login" Batch: show who calls login
1151
+ grafema explore --callees "main" Batch: show what main calls
1152
+ grafema explore --callers "auth" -d 5 Batch: callers with depth 5
1153
+ grafema explore --query "api" --format text Batch: text output
1154
+ `)
1155
+ .action(async (start: string | undefined, options: ExploreOptions) => {
1029
1156
  const projectPath = resolve(options.project);
1030
1157
  const grafemaDir = join(projectPath, '.grafema');
1031
1158
  const dbPath = join(grafemaDir, 'graph.rfdb');
1032
1159
 
1033
1160
  if (!existsSync(dbPath)) {
1034
- exitWithError('No graph database found', ['Run: grafema analyze']);
1161
+ exitWithError('No database found', [
1162
+ 'Run: grafema analyze',
1163
+ ]);
1035
1164
  }
1036
1165
 
1037
1166
  const backend = new RFDBServerBackend({ dbPath });
@@ -1039,6 +1168,27 @@ export const exploreCommand = new Command('explore')
1039
1168
  try {
1040
1169
  await backend.connect();
1041
1170
 
1171
+ // Detect batch mode
1172
+ const isBatchMode = !!(options.query || options.callers || options.callees);
1173
+
1174
+ if (isBatchMode) {
1175
+ await runBatchExplore(backend, options, projectPath);
1176
+ return;
1177
+ }
1178
+
1179
+ // Interactive mode - check TTY
1180
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
1181
+
1182
+ if (!isTTY) {
1183
+ exitWithError('Interactive mode requires a terminal', [
1184
+ 'Batch mode: grafema explore --query "functionName"',
1185
+ 'Batch mode: grafema explore --callers "functionName"',
1186
+ 'Batch mode: grafema explore --callees "functionName"',
1187
+ 'Alternative: grafema query "functionName"',
1188
+ 'Alternative: grafema impact "functionName"',
1189
+ ]);
1190
+ }
1191
+
1042
1192
  const startNode = await findStartNode(backend, start || null);
1043
1193
 
1044
1194
  const { waitUntilExit } = render(
@@ -24,14 +24,16 @@ interface NodeInfo {
24
24
  name: string;
25
25
  file: string;
26
26
  line?: number;
27
+ method?: string;
28
+ path?: string;
29
+ url?: string;
27
30
  [key: string]: unknown;
28
31
  }
29
32
 
30
33
  interface Edge {
31
34
  src: string;
32
35
  dst: string;
33
- edgeType: string;
34
- type?: string;
36
+ type: string;
35
37
  }
36
38
 
37
39
  interface EdgeWithName {
@@ -45,6 +47,13 @@ export const getCommand = new Command('get')
45
47
  .argument('<semantic-id>', 'Semantic ID of the node (e.g., "file.js->scope->TYPE->name")')
46
48
  .option('-p, --project <path>', 'Project path', '.')
47
49
  .option('-j, --json', 'Output as JSON')
50
+ .addHelpText('after', `
51
+ Examples:
52
+ grafema get "src/auth.js->authenticate->FUNCTION" Get function node
53
+ grafema get "src/models/User.js->User->CLASS" Get class node
54
+ grafema get "src/api.js->config->VARIABLE" Get variable node
55
+ grafema get "src/auth.js->authenticate->FUNCTION" -j Output as JSON with edges
56
+ `)
48
57
  .action(async (semanticId: string, options: GetOptions) => {
49
58
  const projectPath = resolve(options.project);
50
59
  const grafemaDir = join(projectPath, '.grafema');
@@ -95,7 +104,7 @@ async function outputJSON(
95
104
  // Fetch target node names for all edges
96
105
  const incomingWithNames = await Promise.all(
97
106
  incomingEdges.map(async (edge) => ({
98
- edgeType: edge.edgeType || edge.type || 'UNKNOWN',
107
+ edgeType: edge.type || 'UNKNOWN',
99
108
  targetId: edge.src,
100
109
  targetName: await getNodeName(backend, edge.src),
101
110
  }))
@@ -103,7 +112,7 @@ async function outputJSON(
103
112
 
104
113
  const outgoingWithNames = await Promise.all(
105
114
  outgoingEdges.map(async (edge) => ({
106
- edgeType: edge.edgeType || edge.type || 'UNKNOWN',
115
+ edgeType: edge.type || 'UNKNOWN',
107
116
  targetId: edge.dst,
108
117
  targetName: await getNodeName(backend, edge.dst),
109
118
  }))
@@ -147,6 +156,9 @@ async function outputText(
147
156
  name: node.name || '',
148
157
  file: node.file || '',
149
158
  line: node.line,
159
+ method: node.method,
160
+ path: node.path,
161
+ url: node.url,
150
162
  };
151
163
 
152
164
  // Display node details
@@ -190,7 +202,7 @@ async function displayEdges(
190
202
  const byType = new Map<string, EdgeWithName[]>();
191
203
 
192
204
  for (const edge of edges) {
193
- const edgeType = edge.edgeType || edge.type || 'UNKNOWN';
205
+ const edgeType = edge.type || 'UNKNOWN';
194
206
  const targetId = getTargetId(edge);
195
207
  const targetName = await getNodeName(backend, targetId);
196
208
 
@@ -246,11 +258,13 @@ async function getNodeName(backend: RFDBServerBackend, nodeId: string): Promise<
246
258
  }
247
259
 
248
260
  /**
249
- * Extract metadata fields (exclude standard fields)
261
+ * Extract metadata fields (exclude standard and display fields)
250
262
  */
251
263
  function getMetadataFields(node: any): Record<string, unknown> {
252
264
  const standardFields = new Set([
253
265
  'id', 'type', 'nodeType', 'name', 'file', 'line',
266
+ // Display fields shown in primary line for HTTP nodes
267
+ 'method', 'path', 'url',
254
268
  ]);
255
269
 
256
270
  const metadata: Record<string, unknown> = {};
@@ -10,7 +10,7 @@ import { Command } from 'commander';
10
10
  import { resolve, join, dirname } from 'path';
11
11
  import { relative } from 'path';
12
12
  import { existsSync } from 'fs';
13
- import { RFDBServerBackend } from '@grafema/core';
13
+ import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore, type CallerInfo } from '@grafema/core';
14
14
  import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
15
15
  import { exitWithError } from '../utils/errorFormatter.js';
16
16
 
@@ -42,6 +42,14 @@ export const impactCommand = new Command('impact')
42
42
  .option('-p, --project <path>', 'Project path', '.')
43
43
  .option('-j, --json', 'Output as JSON')
44
44
  .option('-d, --depth <n>', 'Max traversal depth', '10')
45
+ .addHelpText('after', `
46
+ Examples:
47
+ grafema impact "authenticate" Analyze impact of changing authenticate
48
+ grafema impact "function login" Impact of specific function
49
+ grafema impact "class UserService" Impact of class changes
50
+ grafema impact "validate" -d 3 Limit analysis depth to 3 levels
51
+ grafema impact "auth" --json Output impact analysis as JSON
52
+ `)
45
53
  .action(async (pattern: string, options: ImpactOptions) => {
46
54
  const projectPath = resolve(options.project);
47
55
  const grafemaDir = join(projectPath, '.grafema');
@@ -159,10 +167,21 @@ async function analyzeImpact(
159
167
  const callChains: string[][] = [];
160
168
  const visited = new Set<string>();
161
169
 
170
+ // If target is a CLASS, aggregate callers from all methods
171
+ let targetIds: string[];
172
+ if (target.type === 'CLASS') {
173
+ const methodIds = await getClassMethods(backend, target.id);
174
+ targetIds = [target.id, ...methodIds];
175
+ } else {
176
+ targetIds = [target.id];
177
+ }
178
+
162
179
  // BFS to find all callers
163
- const queue: Array<{ id: string; depth: number; chain: string[] }> = [
164
- { id: target.id, depth: 0, chain: [target.name] }
165
- ];
180
+ const queue: Array<{ id: string; depth: number; chain: string[] }> = targetIds.map(id => ({
181
+ id,
182
+ depth: 0,
183
+ chain: [target.name]
184
+ }));
166
185
 
167
186
  while (queue.length > 0) {
168
187
  const { id, depth, chain } = queue.shift()!;
@@ -179,14 +198,19 @@ async function analyzeImpact(
179
198
 
180
199
  for (const callNode of containingCalls) {
181
200
  // Find the function containing this call
182
- const container = await findContainingFunction(backend, callNode.id);
201
+ const container = await findContainingFunctionCore(backend, callNode.id);
183
202
 
184
203
  if (container && !visited.has(container.id)) {
204
+ // Filter out internal callers (methods of the same class)
205
+ if (target.type === 'CLASS' && targetIds.includes(container.id)) {
206
+ continue;
207
+ }
208
+
185
209
  const caller: NodeInfo = {
186
210
  id: container.id,
187
211
  type: container.type,
188
212
  name: container.name,
189
- file: container.file,
213
+ file: container.file || '',
190
214
  line: container.line,
191
215
  };
192
216
 
@@ -227,6 +251,31 @@ async function analyzeImpact(
227
251
  };
228
252
  }
229
253
 
254
+ /**
255
+ * Get method IDs for a class
256
+ */
257
+ async function getClassMethods(
258
+ backend: RFDBServerBackend,
259
+ classId: string
260
+ ): Promise<string[]> {
261
+ const methods: string[] = [];
262
+
263
+ try {
264
+ const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
265
+
266
+ for (const edge of edges) {
267
+ const node = await backend.getNode(edge.dst);
268
+ if (node && node.type === 'FUNCTION') {
269
+ methods.push(node.id);
270
+ }
271
+ }
272
+ } catch {
273
+ // Ignore errors
274
+ }
275
+
276
+ return methods;
277
+ }
278
+
230
279
  /**
231
280
  * Find CALL nodes that reference a target
232
281
  */
@@ -259,61 +308,6 @@ async function findCallsToNode(
259
308
  return calls;
260
309
  }
261
310
 
262
- /**
263
- * Find the function that contains a call node
264
- *
265
- * Path: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
266
- */
267
- async function findContainingFunction(
268
- backend: RFDBServerBackend,
269
- nodeId: string,
270
- maxDepth: number = 15
271
- ): Promise<NodeInfo | null> {
272
- const visited = new Set<string>();
273
- const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
274
-
275
- while (queue.length > 0) {
276
- const { id, depth } = queue.shift()!;
277
-
278
- if (visited.has(id) || depth > maxDepth) continue;
279
- visited.add(id);
280
-
281
- try {
282
- // Get incoming edges: CONTAINS, HAS_SCOPE
283
- const edges = await backend.getIncomingEdges(id, null);
284
-
285
- for (const edge of edges) {
286
- const edgeType = (edge as any).edgeType || (edge as any).type;
287
-
288
- // Only follow structural edges
289
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
290
-
291
- const parent = await backend.getNode(edge.src);
292
- if (!parent || visited.has(parent.id)) continue;
293
-
294
- const parentType = parent.type;
295
-
296
- // FUNCTION, CLASS, or MODULE (for top-level calls)
297
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
298
- return {
299
- id: parent.id,
300
- type: parentType,
301
- name: parent.name || '',
302
- file: parent.file || '',
303
- line: parent.line,
304
- };
305
- }
306
-
307
- queue.push({ id: parent.id, depth: depth + 1 });
308
- }
309
- } catch {
310
- // Ignore
311
- }
312
- }
313
-
314
- return null;
315
- }
316
-
317
311
  /**
318
312
  * Get module path relative to project
319
313
  */