@grafema/cli 0.2.12-beta → 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 (131) 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.js +11 -6
  56. package/dist/commands/server.js.map +1 -1
  57. package/dist/commands/stats.js +2 -2
  58. package/dist/commands/stats.js.map +1 -1
  59. package/dist/commands/tldr.d.ts +12 -0
  60. package/dist/commands/tldr.d.ts.map +1 -0
  61. package/dist/commands/tldr.js +81 -0
  62. package/dist/commands/tldr.js.map +1 -0
  63. package/dist/commands/trace.d.ts +1 -1
  64. package/dist/commands/trace.d.ts.map +1 -1
  65. package/dist/commands/trace.js +17 -133
  66. package/dist/commands/trace.js.map +1 -1
  67. package/dist/commands/types.js +2 -2
  68. package/dist/commands/types.js.map +1 -1
  69. package/dist/commands/who.d.ts +12 -0
  70. package/dist/commands/who.d.ts.map +1 -0
  71. package/dist/commands/who.js +184 -0
  72. package/dist/commands/who.js.map +1 -0
  73. package/dist/commands/why.d.ts +12 -0
  74. package/dist/commands/why.d.ts.map +1 -0
  75. package/dist/commands/why.js +118 -0
  76. package/dist/commands/why.js.map +1 -0
  77. package/dist/commands/wtf.d.ts +12 -0
  78. package/dist/commands/wtf.d.ts.map +1 -0
  79. package/dist/commands/wtf.js +117 -0
  80. package/dist/commands/wtf.js.map +1 -0
  81. package/dist/plugins/builtinPlugins.d.ts +1 -9
  82. package/dist/plugins/builtinPlugins.d.ts.map +1 -1
  83. package/dist/plugins/builtinPlugins.js +2 -67
  84. package/dist/plugins/builtinPlugins.js.map +1 -1
  85. package/dist/plugins/pluginLoader.d.ts +1 -15
  86. package/dist/plugins/pluginLoader.d.ts.map +1 -1
  87. package/dist/plugins/pluginLoader.js +2 -100
  88. package/dist/plugins/pluginLoader.js.map +1 -1
  89. package/dist/plugins/pluginResolver.js +3 -3
  90. package/dist/utils/progressRenderer.d.ts +15 -1
  91. package/dist/utils/progressRenderer.d.ts.map +1 -1
  92. package/dist/utils/progressRenderer.js.map +1 -1
  93. package/dist/utils/queryHints.d.ts +6 -0
  94. package/dist/utils/queryHints.d.ts.map +1 -0
  95. package/dist/utils/queryHints.js +36 -0
  96. package/dist/utils/queryHints.js.map +1 -0
  97. package/package.json +4 -4
  98. package/skills/grafema-codebase-analysis/SKILL.md +1 -1
  99. package/src/cli.ts +14 -0
  100. package/src/commands/analyze.ts +2 -4
  101. package/src/commands/analyzeAction.ts +122 -168
  102. package/src/commands/check.ts +5 -5
  103. package/src/commands/context.ts +3 -3
  104. package/src/commands/coverage.ts +2 -2
  105. package/src/commands/describe.ts +160 -0
  106. package/src/commands/doctor/checks.ts +153 -10
  107. package/src/commands/doctor.ts +13 -9
  108. package/src/commands/explain.ts +2 -2
  109. package/src/commands/explore.tsx +2 -2
  110. package/src/commands/file.ts +3 -3
  111. package/src/commands/get.ts +2 -2
  112. package/src/commands/git-ingest.ts +49 -0
  113. package/src/commands/impact.ts +318 -55
  114. package/src/commands/init.ts +20 -22
  115. package/src/commands/ls.ts +2 -2
  116. package/src/commands/overview.ts +2 -2
  117. package/src/commands/query.ts +197 -7
  118. package/src/commands/schema.ts +2 -2
  119. package/src/commands/server.ts +12 -6
  120. package/src/commands/stats.ts +2 -2
  121. package/src/commands/tldr.ts +103 -0
  122. package/src/commands/trace.ts +19 -161
  123. package/src/commands/types.ts +2 -2
  124. package/src/commands/who.ts +215 -0
  125. package/src/commands/why.ts +134 -0
  126. package/src/commands/wtf.ts +140 -0
  127. package/src/plugins/builtinPlugins.ts +1 -108
  128. package/src/plugins/pluginLoader.ts +1 -123
  129. package/src/plugins/pluginResolver.js +3 -3
  130. package/src/utils/progressRenderer.ts +15 -1
  131. package/src/utils/queryHints.ts +46 -0
@@ -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
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * why command — "Why is it this way?"
3
+ *
4
+ * Query knowledge base for architectural decisions and facts about a symbol or module.
5
+ *
6
+ * Usage:
7
+ * grafema why auth-middleware # Why was auth middleware designed this way?
8
+ * grafema why UserService # Decisions about UserService
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { resolve, join } from 'path';
13
+ import { existsSync } from 'fs';
14
+ import { KnowledgeBase } from '@grafema/util';
15
+ import type { KBDecision, KBFact } from '@grafema/util';
16
+ import { exitWithError } from '../utils/errorFormatter.js';
17
+ import { Spinner } from '../utils/spinner.js';
18
+
19
+ interface WhyCommandOptions {
20
+ project: string;
21
+ json?: boolean;
22
+ }
23
+
24
+ export const whyCommand = new Command('why')
25
+ .description('Why is it this way? — query knowledge base decisions and facts')
26
+ .argument('<query>', 'Search text (symbol name, module, or topic)')
27
+ .option('-p, --project <path>', 'Project path', '.')
28
+ .option('-j, --json', 'Output as JSON')
29
+ .addHelpText('after', `
30
+ Examples:
31
+ grafema why auth-middleware Why was auth middleware designed this way?
32
+ grafema why UserService Decisions about UserService
33
+ grafema why "error handling" Facts about error handling approach
34
+ grafema why dataflow --json Output as JSON
35
+ `)
36
+ .action(async (query: string, options: WhyCommandOptions) => {
37
+ const projectPath = resolve(options.project);
38
+ const knowledgeDir = join(projectPath, 'knowledge');
39
+
40
+ if (!existsSync(knowledgeDir)) {
41
+ exitWithError('No knowledge base found', [
42
+ 'Knowledge directory not found: ' + knowledgeDir,
43
+ 'Use `add_knowledge` MCP tool to capture architectural decisions',
44
+ ]);
45
+ }
46
+
47
+ const spinner = new Spinner('Searching knowledge base...');
48
+ spinner.start();
49
+
50
+ try {
51
+ const kb = new KnowledgeBase(knowledgeDir);
52
+ await kb.load();
53
+
54
+ // Search DECISION nodes matching query text
55
+ const decisions = await kb.queryNodes({ type: 'DECISION', text: query }) as KBDecision[];
56
+
57
+ // Search FACT nodes matching query text
58
+ const facts = await kb.queryNodes({ type: 'FACT', text: query }) as KBFact[];
59
+
60
+ spinner.stop();
61
+
62
+ if (options.json) {
63
+ console.log(JSON.stringify({
64
+ query,
65
+ decisions: decisions.map(d => ({
66
+ id: d.id,
67
+ status: d.status,
68
+ content: d.content,
69
+ applies_to: d.applies_to,
70
+ relates_to: d.relates_to,
71
+ })),
72
+ facts: facts.map(f => ({
73
+ id: f.id,
74
+ confidence: f.confidence,
75
+ content: f.content,
76
+ relates_to: f.relates_to,
77
+ })),
78
+ total: decisions.length + facts.length,
79
+ }, null, 2));
80
+ return;
81
+ }
82
+
83
+ if (decisions.length === 0 && facts.length === 0) {
84
+ console.log(`No knowledge found for: "${query}"`);
85
+ console.log('');
86
+ console.log('No decisions or facts recorded matching this query.');
87
+ console.log('Use `add_knowledge` MCP tool to capture architectural decisions.');
88
+ return;
89
+ }
90
+
91
+ // Display decisions
92
+ if (decisions.length > 0) {
93
+ console.log(`Decisions (${decisions.length}):`);
94
+ console.log('');
95
+ for (const d of decisions) {
96
+ console.log(` [${d.status?.toUpperCase() || 'ACTIVE'}] ${d.id}`);
97
+ // Show first ~200 chars of content as summary
98
+ const summary = d.content.length > 200
99
+ ? d.content.substring(0, 200) + '...'
100
+ : d.content;
101
+ // Indent content lines
102
+ for (const line of summary.split('\n')) {
103
+ console.log(` ${line}`);
104
+ }
105
+ if (d.applies_to && d.applies_to.length > 0) {
106
+ console.log(` Applies to: ${d.applies_to.join(', ')}`);
107
+ }
108
+ console.log('');
109
+ }
110
+ }
111
+
112
+ // Display facts
113
+ if (facts.length > 0) {
114
+ console.log(`Facts (${facts.length}):`);
115
+ console.log('');
116
+ for (const f of facts) {
117
+ const confidence = f.confidence ? ` [${f.confidence}]` : '';
118
+ console.log(` ${f.id}${confidence}`);
119
+ const summary = f.content.length > 200
120
+ ? f.content.substring(0, 200) + '...'
121
+ : f.content;
122
+ for (const line of summary.split('\n')) {
123
+ console.log(` ${line}`);
124
+ }
125
+ if (f.relates_to && f.relates_to.length > 0) {
126
+ console.log(` Relates to: ${f.relates_to.join(', ')}`);
127
+ }
128
+ console.log('');
129
+ }
130
+ }
131
+ } finally {
132
+ spinner.stop();
133
+ }
134
+ });