@grafema/cli 0.2.11 → 0.3.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +2 -4
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts +5 -3
- package/dist/commands/analyzeAction.d.ts.map +1 -1
- package/dist/commands/analyzeAction.js +109 -151
- package/dist/commands/analyzeAction.js.map +1 -1
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +4 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.js +2 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/coverage.js +2 -2
- package/dist/commands/coverage.js.map +1 -1
- package/dist/commands/describe.d.ts +13 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +131 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts +6 -1
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +128 -13
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/doctor.d.ts +10 -9
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/file.js +2 -2
- package/dist/commands/file.js.map +1 -1
- package/dist/commands/get.js +2 -2
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/git-ingest.d.ts +6 -0
- package/dist/commands/git-ingest.d.ts.map +1 -0
- package/dist/commands/git-ingest.js +46 -0
- package/dist/commands/git-ingest.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +276 -50
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -22
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ls.js +2 -2
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/overview.js +2 -2
- package/dist/commands/overview.js.map +1 -1
- package/dist/commands/query.d.ts +1 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +169 -7
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/schema.js +2 -2
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +122 -76
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/stats.js +2 -2
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/tldr.d.ts +12 -0
- package/dist/commands/tldr.d.ts.map +1 -0
- package/dist/commands/tldr.js +81 -0
- package/dist/commands/tldr.js.map +1 -0
- package/dist/commands/trace.d.ts +1 -1
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +17 -133
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/types.js +2 -2
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/who.d.ts +12 -0
- package/dist/commands/who.d.ts.map +1 -0
- package/dist/commands/who.js +184 -0
- package/dist/commands/who.js.map +1 -0
- package/dist/commands/why.d.ts +12 -0
- package/dist/commands/why.d.ts.map +1 -0
- package/dist/commands/why.js +118 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/commands/wtf.d.ts +12 -0
- package/dist/commands/wtf.d.ts.map +1 -0
- package/dist/commands/wtf.js +117 -0
- package/dist/commands/wtf.js.map +1 -0
- package/dist/plugins/builtinPlugins.d.ts +1 -9
- package/dist/plugins/builtinPlugins.d.ts.map +1 -1
- package/dist/plugins/builtinPlugins.js +2 -67
- package/dist/plugins/builtinPlugins.js.map +1 -1
- package/dist/plugins/pluginLoader.d.ts +1 -15
- package/dist/plugins/pluginLoader.d.ts.map +1 -1
- package/dist/plugins/pluginLoader.js +2 -100
- package/dist/plugins/pluginLoader.js.map +1 -1
- package/dist/plugins/pluginResolver.js +3 -3
- package/dist/utils/progressRenderer.d.ts +15 -1
- package/dist/utils/progressRenderer.d.ts.map +1 -1
- package/dist/utils/progressRenderer.js +19 -3
- package/dist/utils/progressRenderer.js.map +1 -1
- package/dist/utils/queryHints.d.ts +6 -0
- package/dist/utils/queryHints.d.ts.map +1 -0
- package/dist/utils/queryHints.js +36 -0
- package/dist/utils/queryHints.js.map +1 -0
- package/package.json +4 -4
- package/skills/grafema-codebase-analysis/SKILL.md +1 -1
- package/src/cli.ts +14 -0
- package/src/commands/analyze.ts +2 -4
- package/src/commands/analyzeAction.ts +122 -168
- package/src/commands/check.ts +5 -5
- package/src/commands/context.ts +3 -3
- package/src/commands/coverage.ts +2 -2
- package/src/commands/describe.ts +160 -0
- package/src/commands/doctor/checks.ts +153 -10
- package/src/commands/doctor.ts +13 -9
- package/src/commands/explain.ts +2 -2
- package/src/commands/explore.tsx +2 -2
- package/src/commands/file.ts +3 -3
- package/src/commands/get.ts +2 -2
- package/src/commands/git-ingest.ts +49 -0
- package/src/commands/impact.ts +318 -55
- package/src/commands/init.ts +20 -22
- package/src/commands/ls.ts +2 -2
- package/src/commands/overview.ts +2 -2
- package/src/commands/query.ts +197 -7
- package/src/commands/schema.ts +2 -2
- package/src/commands/server.ts +136 -84
- package/src/commands/stats.ts +2 -2
- package/src/commands/tldr.ts +103 -0
- package/src/commands/trace.ts +19 -161
- package/src/commands/types.ts +2 -2
- package/src/commands/who.ts +215 -0
- package/src/commands/why.ts +134 -0
- package/src/commands/wtf.ts +140 -0
- package/src/plugins/builtinPlugins.ts +1 -108
- package/src/plugins/pluginLoader.ts +1 -123
- package/src/plugins/pluginResolver.js +3 -3
- package/src/utils/progressRenderer.ts +34 -4
- package/src/utils/queryHints.ts +46 -0
|
@@ -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
|
+
});
|
package/src/commands/trace.ts
CHANGED
|
@@ -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/
|
|
14
|
-
import { formatNodeDisplay
|
|
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
|
-
//
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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/
|
|
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
|
package/src/commands/types.ts
CHANGED
|
@@ -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/
|
|
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
|
+
});
|