@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.
- 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.js +11 -6
- 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.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 +12 -6
- 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 +15 -1
- package/src/utils/queryHints.ts +46 -0
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
|
+
});
|
|
@@ -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
|
+
});
|