@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.
- package/dist/cli.js +10 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +69 -11
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +177 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +7 -0
- package/dist/commands/doctor/checks.d.ts +55 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -0
- package/dist/commands/doctor/checks.js +534 -0
- package/dist/commands/doctor/output.d.ts +20 -0
- package/dist/commands/doctor/output.d.ts.map +1 -0
- package/dist/commands/doctor/output.js +94 -0
- package/dist/commands/doctor/types.d.ts +42 -0
- package/dist/commands/doctor/types.d.ts.map +1 -0
- package/dist/commands/doctor/types.js +4 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +80 -0
- package/dist/commands/explain.d.ts +16 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +145 -0
- package/dist/commands/explore.d.ts +7 -1
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +204 -85
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +16 -4
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +48 -50
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +93 -15
- package/dist/commands/ls.d.ts +14 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +132 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +15 -2
- package/dist/commands/query.d.ts +98 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +549 -136
- package/dist/commands/schema.d.ts +13 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +279 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -6
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +7 -0
- package/dist/commands/trace.d.ts +73 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +500 -5
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +79 -0
- package/dist/utils/formatNode.d.ts +13 -0
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +35 -2
- package/package.json +3 -3
- package/src/cli.ts +10 -0
- package/src/commands/analyze.ts +84 -9
- package/src/commands/check.ts +201 -0
- package/src/commands/coverage.ts +7 -0
- package/src/commands/doctor/checks.ts +612 -0
- package/src/commands/doctor/output.ts +115 -0
- package/src/commands/doctor/types.ts +45 -0
- package/src/commands/doctor.ts +106 -0
- package/src/commands/explain.ts +173 -0
- package/src/commands/explore.tsx +247 -97
- package/src/commands/get.ts +20 -6
- package/src/commands/impact.ts +55 -61
- package/src/commands/init.ts +101 -14
- package/src/commands/ls.ts +166 -0
- package/src/commands/overview.ts +15 -2
- package/src/commands/query.ts +643 -149
- package/src/commands/schema.ts +345 -0
- package/src/commands/server.ts +13 -6
- package/src/commands/stats.ts +7 -0
- package/src/commands/trace.ts +647 -6
- package/src/commands/types.ts +94 -0
- package/src/utils/formatNode.ts +42 -2
package/dist/commands/trace.js
CHANGED
|
@@ -4,19 +4,32 @@
|
|
|
4
4
|
* Usage:
|
|
5
5
|
* grafema trace "userId from authenticate"
|
|
6
6
|
* grafema trace "config"
|
|
7
|
+
* grafema trace --to "addNode#0.type" (sink-based trace)
|
|
7
8
|
*/
|
|
8
9
|
import { Command } from 'commander';
|
|
9
10
|
import { resolve, join } from 'path';
|
|
10
11
|
import { existsSync } from 'fs';
|
|
11
|
-
import { RFDBServerBackend, parseSemanticId } from '@grafema/core';
|
|
12
|
+
import { RFDBServerBackend, parseSemanticId, traceValues } from '@grafema/core';
|
|
12
13
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
13
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
14
15
|
export const traceCommand = new Command('trace')
|
|
15
|
-
.description('Trace data flow for a variable')
|
|
16
|
-
.argument('
|
|
16
|
+
.description('Trace data flow for a variable or to a sink point')
|
|
17
|
+
.argument('[pattern]', 'Pattern: "varName from functionName" or just "varName"')
|
|
17
18
|
.option('-p, --project <path>', 'Project path', '.')
|
|
18
19
|
.option('-j, --json', 'Output as JSON')
|
|
19
20
|
.option('-d, --depth <n>', 'Max trace depth', '10')
|
|
21
|
+
.option('-t, --to <sink>', 'Sink point: "fn#argIndex.property" (e.g., "addNode#0.type")')
|
|
22
|
+
.option('-r, --from-route <pattern>', 'Trace from route response (e.g., "GET /status" or "/status")')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
Examples:
|
|
25
|
+
grafema trace "userId" Trace all variables named "userId"
|
|
26
|
+
grafema trace "userId from authenticate" Trace userId within authenticate function
|
|
27
|
+
grafema trace "config" --depth 5 Limit trace depth to 5 levels
|
|
28
|
+
grafema trace "apiKey" --json Output trace as JSON
|
|
29
|
+
grafema trace --to "addNode#0.type" Trace values reaching sink point
|
|
30
|
+
grafema trace --from-route "GET /status" Trace values from route response
|
|
31
|
+
grafema trace -r "/status" Trace by path only
|
|
32
|
+
`)
|
|
20
33
|
.action(async (pattern, options) => {
|
|
21
34
|
const projectPath = resolve(options.project);
|
|
22
35
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -27,6 +40,21 @@ export const traceCommand = new Command('trace')
|
|
|
27
40
|
const backend = new RFDBServerBackend({ dbPath });
|
|
28
41
|
await backend.connect();
|
|
29
42
|
try {
|
|
43
|
+
// Handle sink-based trace if --to option is provided
|
|
44
|
+
if (options.to) {
|
|
45
|
+
await handleSinkTrace(backend, options.to, projectPath, options.json);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Handle route-based trace if --from-route option is provided
|
|
49
|
+
if (options.fromRoute) {
|
|
50
|
+
const maxDepth = parseInt(options.depth, 10);
|
|
51
|
+
await handleRouteTrace(backend, options.fromRoute, projectPath, options.json, maxDepth);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Regular trace requires pattern
|
|
55
|
+
if (!pattern) {
|
|
56
|
+
exitWithError('Pattern required', ['Provide a pattern, use --to for sink trace, or --from-route for route trace']);
|
|
57
|
+
}
|
|
30
58
|
// Parse pattern: "varName from functionName" or just "varName"
|
|
31
59
|
const { varName, scopeName } = parseTracePattern(pattern);
|
|
32
60
|
const maxDepth = parseInt(options.depth, 10);
|
|
@@ -141,6 +169,7 @@ async function findVariables(backend, varName, scopeName) {
|
|
|
141
169
|
async function traceBackward(backend, startId, maxDepth) {
|
|
142
170
|
const trace = [];
|
|
143
171
|
const visited = new Set();
|
|
172
|
+
const seenNodes = new Set();
|
|
144
173
|
const queue = [{ id: startId, depth: 0 }];
|
|
145
174
|
while (queue.length > 0) {
|
|
146
175
|
const { id, depth } = queue.shift();
|
|
@@ -153,6 +182,9 @@ async function traceBackward(backend, startId, maxDepth) {
|
|
|
153
182
|
const targetNode = await backend.getNode(edge.dst);
|
|
154
183
|
if (!targetNode)
|
|
155
184
|
continue;
|
|
185
|
+
if (seenNodes.has(targetNode.id))
|
|
186
|
+
continue;
|
|
187
|
+
seenNodes.add(targetNode.id);
|
|
156
188
|
const nodeInfo = {
|
|
157
189
|
id: targetNode.id,
|
|
158
190
|
type: targetNode.type || 'UNKNOWN',
|
|
@@ -163,7 +195,7 @@ async function traceBackward(backend, startId, maxDepth) {
|
|
|
163
195
|
};
|
|
164
196
|
trace.push({
|
|
165
197
|
node: nodeInfo,
|
|
166
|
-
edgeType: edge.
|
|
198
|
+
edgeType: edge.type,
|
|
167
199
|
depth: depth + 1,
|
|
168
200
|
});
|
|
169
201
|
// Continue tracing unless we hit a leaf
|
|
@@ -185,6 +217,7 @@ async function traceBackward(backend, startId, maxDepth) {
|
|
|
185
217
|
async function traceForward(backend, startId, maxDepth) {
|
|
186
218
|
const trace = [];
|
|
187
219
|
const visited = new Set();
|
|
220
|
+
const seenNodes = new Set();
|
|
188
221
|
const queue = [{ id: startId, depth: 0 }];
|
|
189
222
|
while (queue.length > 0) {
|
|
190
223
|
const { id, depth } = queue.shift();
|
|
@@ -198,6 +231,9 @@ async function traceForward(backend, startId, maxDepth) {
|
|
|
198
231
|
const sourceNode = await backend.getNode(edge.src);
|
|
199
232
|
if (!sourceNode)
|
|
200
233
|
continue;
|
|
234
|
+
if (seenNodes.has(sourceNode.id))
|
|
235
|
+
continue;
|
|
236
|
+
seenNodes.add(sourceNode.id);
|
|
201
237
|
const nodeInfo = {
|
|
202
238
|
id: sourceNode.id,
|
|
203
239
|
type: sourceNode.type || 'UNKNOWN',
|
|
@@ -207,7 +243,7 @@ async function traceForward(backend, startId, maxDepth) {
|
|
|
207
243
|
};
|
|
208
244
|
trace.push({
|
|
209
245
|
node: nodeInfo,
|
|
210
|
-
edgeType: edge.
|
|
246
|
+
edgeType: edge.type,
|
|
211
247
|
depth: depth + 1,
|
|
212
248
|
});
|
|
213
249
|
// Continue forward
|
|
@@ -268,3 +304,462 @@ function displayTrace(trace, _projectPath, indent) {
|
|
|
268
304
|
}
|
|
269
305
|
}
|
|
270
306
|
}
|
|
307
|
+
// =============================================================================
|
|
308
|
+
// SINK-BASED TRACE IMPLEMENTATION (REG-230)
|
|
309
|
+
// =============================================================================
|
|
310
|
+
/**
|
|
311
|
+
* Parse sink specification string into structured format
|
|
312
|
+
*
|
|
313
|
+
* Format: "functionName#argIndex.property.path"
|
|
314
|
+
* Examples:
|
|
315
|
+
* - "addNode#0.type" -> {functionName: "addNode", argIndex: 0, propertyPath: ["type"]}
|
|
316
|
+
* - "fn#0" -> {functionName: "fn", argIndex: 0, propertyPath: []}
|
|
317
|
+
* - "add_node_v2#1.config.options" -> {functionName: "add_node_v2", argIndex: 1, propertyPath: ["config", "options"]}
|
|
318
|
+
*
|
|
319
|
+
* @throws Error if spec is invalid
|
|
320
|
+
*/
|
|
321
|
+
export function parseSinkSpec(spec) {
|
|
322
|
+
if (!spec || spec.trim() === '') {
|
|
323
|
+
throw new Error('Invalid sink spec: empty string');
|
|
324
|
+
}
|
|
325
|
+
const trimmed = spec.trim();
|
|
326
|
+
// Must contain # separator
|
|
327
|
+
const hashIndex = trimmed.indexOf('#');
|
|
328
|
+
if (hashIndex === -1) {
|
|
329
|
+
throw new Error('Invalid sink spec: missing # separator');
|
|
330
|
+
}
|
|
331
|
+
// Extract function name (before #)
|
|
332
|
+
const functionName = trimmed.substring(0, hashIndex);
|
|
333
|
+
if (!functionName || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(functionName)) {
|
|
334
|
+
throw new Error('Invalid sink spec: invalid function name');
|
|
335
|
+
}
|
|
336
|
+
// Extract argument index and optional property path (after #)
|
|
337
|
+
const afterHash = trimmed.substring(hashIndex + 1);
|
|
338
|
+
if (!afterHash) {
|
|
339
|
+
throw new Error('Invalid sink spec: missing argument index');
|
|
340
|
+
}
|
|
341
|
+
// Split by first dot to separate argIndex from property path
|
|
342
|
+
const dotIndex = afterHash.indexOf('.');
|
|
343
|
+
const argIndexStr = dotIndex === -1 ? afterHash : afterHash.substring(0, dotIndex);
|
|
344
|
+
const propertyPathStr = dotIndex === -1 ? '' : afterHash.substring(dotIndex + 1);
|
|
345
|
+
// Parse argument index
|
|
346
|
+
if (!/^\d+$/.test(argIndexStr)) {
|
|
347
|
+
throw new Error('Invalid sink spec: argument index must be numeric');
|
|
348
|
+
}
|
|
349
|
+
const argIndex = parseInt(argIndexStr, 10);
|
|
350
|
+
if (argIndex < 0) {
|
|
351
|
+
throw new Error('Invalid sink spec: negative argument index');
|
|
352
|
+
}
|
|
353
|
+
// Parse property path (split by dots)
|
|
354
|
+
const propertyPath = propertyPathStr ? propertyPathStr.split('.').filter(p => p) : [];
|
|
355
|
+
return {
|
|
356
|
+
functionName,
|
|
357
|
+
argIndex,
|
|
358
|
+
propertyPath,
|
|
359
|
+
raw: trimmed,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Find all call sites for a function by name
|
|
364
|
+
*
|
|
365
|
+
* Handles both:
|
|
366
|
+
* - Direct calls: fn() where name === targetFunctionName
|
|
367
|
+
* - Method calls: obj.fn() where method attribute === targetFunctionName
|
|
368
|
+
*/
|
|
369
|
+
export async function findCallSites(backend, targetFunctionName) {
|
|
370
|
+
const callSites = [];
|
|
371
|
+
for await (const node of backend.queryNodes({ nodeType: 'CALL' })) {
|
|
372
|
+
const nodeName = node.name || '';
|
|
373
|
+
const nodeMethod = node.method || '';
|
|
374
|
+
// Match direct calls (name === targetFunctionName)
|
|
375
|
+
// Or method calls (method === targetFunctionName)
|
|
376
|
+
if (nodeName === targetFunctionName || nodeMethod === targetFunctionName) {
|
|
377
|
+
callSites.push({
|
|
378
|
+
id: node.id,
|
|
379
|
+
calleeFunction: targetFunctionName,
|
|
380
|
+
file: node.file || '',
|
|
381
|
+
line: node.line || 0,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return callSites;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Extract the argument node ID at a specific index from a call site
|
|
389
|
+
*
|
|
390
|
+
* Follows PASSES_ARGUMENT edges and matches by argIndex metadata
|
|
391
|
+
*
|
|
392
|
+
* @returns Node ID of the argument, or null if not found
|
|
393
|
+
*/
|
|
394
|
+
export async function extractArgument(backend, callSiteId, argIndex) {
|
|
395
|
+
const edges = await backend.getOutgoingEdges(callSiteId, ['PASSES_ARGUMENT']);
|
|
396
|
+
for (const edge of edges) {
|
|
397
|
+
// argIndex is stored in edge metadata
|
|
398
|
+
const edgeArgIndex = edge.metadata?.argIndex;
|
|
399
|
+
if (edgeArgIndex === argIndex) {
|
|
400
|
+
return edge.dst;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Extract a property from a node by following HAS_PROPERTY edges
|
|
407
|
+
*
|
|
408
|
+
* If node is a VARIABLE, first traces through ASSIGNED_FROM to find OBJECT_LITERAL
|
|
409
|
+
*
|
|
410
|
+
* @returns Node ID of the property value, or null if not found
|
|
411
|
+
*/
|
|
412
|
+
async function extractProperty(backend, nodeId, propertyName) {
|
|
413
|
+
const node = await backend.getNode(nodeId);
|
|
414
|
+
if (!node)
|
|
415
|
+
return null;
|
|
416
|
+
const nodeType = node.type || node.nodeType;
|
|
417
|
+
// If it's an OBJECT_LITERAL, follow HAS_PROPERTY directly
|
|
418
|
+
if (nodeType === 'OBJECT_LITERAL') {
|
|
419
|
+
const edges = await backend.getOutgoingEdges(nodeId, ['HAS_PROPERTY']);
|
|
420
|
+
for (const edge of edges) {
|
|
421
|
+
if (edge.metadata?.propertyName === propertyName) {
|
|
422
|
+
return edge.dst;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
// If it's a VARIABLE, first trace to the object literal
|
|
428
|
+
if (nodeType === 'VARIABLE' || nodeType === 'CONSTANT') {
|
|
429
|
+
const assignedEdges = await backend.getOutgoingEdges(nodeId, ['ASSIGNED_FROM']);
|
|
430
|
+
for (const edge of assignedEdges) {
|
|
431
|
+
const result = await extractProperty(backend, edge.dst, propertyName);
|
|
432
|
+
if (result)
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Trace a node to its literal values.
|
|
440
|
+
* Uses shared traceValues utility from @grafema/core (REG-244).
|
|
441
|
+
*
|
|
442
|
+
* @param backend - RFDBServerBackend for graph queries
|
|
443
|
+
* @param nodeId - Starting node ID
|
|
444
|
+
* @param _visited - Kept for API compatibility (internal cycle detection in shared utility)
|
|
445
|
+
* @param maxDepth - Maximum traversal depth
|
|
446
|
+
* @returns Array of traced values with sources
|
|
447
|
+
*/
|
|
448
|
+
async function traceToLiterals(backend, nodeId, _visited = new Set(), maxDepth = 10) {
|
|
449
|
+
// RFDBServerBackend implements TraceValuesGraphBackend interface
|
|
450
|
+
const traced = await traceValues(backend, nodeId, {
|
|
451
|
+
maxDepth,
|
|
452
|
+
followDerivesFrom: true,
|
|
453
|
+
detectNondeterministic: true,
|
|
454
|
+
});
|
|
455
|
+
// Map to expected format (strip reason field)
|
|
456
|
+
return traced.map(t => ({
|
|
457
|
+
value: t.value,
|
|
458
|
+
source: t.source,
|
|
459
|
+
isUnknown: t.isUnknown,
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve a sink specification to all possible values
|
|
464
|
+
*
|
|
465
|
+
* This is the main entry point for sink-based trace.
|
|
466
|
+
* It finds all call sites, extracts the specified argument,
|
|
467
|
+
* optionally follows property path, and traces to literal values.
|
|
468
|
+
*/
|
|
469
|
+
export async function resolveSink(backend, sink) {
|
|
470
|
+
// Find all call sites for the function
|
|
471
|
+
const callSites = await findCallSites(backend, sink.functionName);
|
|
472
|
+
const resolvedCallSites = [];
|
|
473
|
+
const valueMap = new Map();
|
|
474
|
+
let hasUnknown = false;
|
|
475
|
+
let totalSources = 0;
|
|
476
|
+
for (const callSite of callSites) {
|
|
477
|
+
resolvedCallSites.push(callSite);
|
|
478
|
+
// Extract the argument at the specified index
|
|
479
|
+
const argNodeId = await extractArgument(backend, callSite.id, sink.argIndex);
|
|
480
|
+
if (!argNodeId) {
|
|
481
|
+
// Argument doesn't exist at this call site
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
// If property path specified, navigate to that property
|
|
485
|
+
let targetNodeId = argNodeId;
|
|
486
|
+
for (const propName of sink.propertyPath) {
|
|
487
|
+
const propNodeId = await extractProperty(backend, targetNodeId, propName);
|
|
488
|
+
if (!propNodeId) {
|
|
489
|
+
// Property not found, mark as unknown
|
|
490
|
+
hasUnknown = true;
|
|
491
|
+
targetNodeId = '';
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
targetNodeId = propNodeId;
|
|
495
|
+
}
|
|
496
|
+
if (!targetNodeId)
|
|
497
|
+
continue;
|
|
498
|
+
// Trace to literal values
|
|
499
|
+
const literals = await traceToLiterals(backend, targetNodeId);
|
|
500
|
+
for (const lit of literals) {
|
|
501
|
+
if (lit.isUnknown) {
|
|
502
|
+
hasUnknown = true;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
totalSources++;
|
|
506
|
+
const valueKey = JSON.stringify(lit.value);
|
|
507
|
+
if (valueMap.has(valueKey)) {
|
|
508
|
+
valueMap.get(valueKey).sources.push(lit.source);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
valueMap.set(valueKey, {
|
|
512
|
+
value: lit.value,
|
|
513
|
+
sources: [lit.source],
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Convert map to array
|
|
519
|
+
const possibleValues = Array.from(valueMap.values());
|
|
520
|
+
return {
|
|
521
|
+
sink,
|
|
522
|
+
resolvedCallSites,
|
|
523
|
+
possibleValues,
|
|
524
|
+
statistics: {
|
|
525
|
+
callSites: callSites.length,
|
|
526
|
+
totalSources,
|
|
527
|
+
uniqueValues: possibleValues.length,
|
|
528
|
+
unknownElements: hasUnknown,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Handle sink trace command (--to option)
|
|
534
|
+
*/
|
|
535
|
+
async function handleSinkTrace(backend, sinkSpec, projectPath, jsonOutput) {
|
|
536
|
+
// Parse the sink specification
|
|
537
|
+
const sink = parseSinkSpec(sinkSpec);
|
|
538
|
+
// Resolve the sink
|
|
539
|
+
const result = await resolveSink(backend, sink);
|
|
540
|
+
if (jsonOutput) {
|
|
541
|
+
console.log(JSON.stringify(result, null, 2));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Human-readable output
|
|
545
|
+
console.log(`Sink: ${sink.raw}`);
|
|
546
|
+
console.log(`Resolved to ${result.statistics.callSites} call site(s)`);
|
|
547
|
+
console.log('');
|
|
548
|
+
if (result.possibleValues.length === 0) {
|
|
549
|
+
if (result.statistics.unknownElements) {
|
|
550
|
+
console.log('Possible values: <unknown> (runtime/parameter values)');
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
console.log('No values found');
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
console.log('Possible values:');
|
|
558
|
+
for (const pv of result.possibleValues) {
|
|
559
|
+
const sourcesCount = pv.sources.length;
|
|
560
|
+
console.log(` - ${JSON.stringify(pv.value)} (${sourcesCount} source${sourcesCount === 1 ? '' : 's'})`);
|
|
561
|
+
for (const src of pv.sources.slice(0, 3)) {
|
|
562
|
+
const relativePath = src.file.startsWith(projectPath)
|
|
563
|
+
? src.file.substring(projectPath.length + 1)
|
|
564
|
+
: src.file;
|
|
565
|
+
console.log(` <- ${relativePath}:${src.line}`);
|
|
566
|
+
}
|
|
567
|
+
if (pv.sources.length > 3) {
|
|
568
|
+
console.log(` ... and ${pv.sources.length - 3} more`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (result.statistics.unknownElements) {
|
|
572
|
+
console.log('');
|
|
573
|
+
console.log('Note: Some values could not be determined (runtime/parameter inputs)');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// =============================================================================
|
|
577
|
+
// ROUTE-BASED TRACE IMPLEMENTATION (REG-326)
|
|
578
|
+
// =============================================================================
|
|
579
|
+
/**
|
|
580
|
+
* Find route by pattern.
|
|
581
|
+
*
|
|
582
|
+
* Supports:
|
|
583
|
+
* - "METHOD /path" format (e.g., "GET /status")
|
|
584
|
+
* - "/path" format (e.g., "/status")
|
|
585
|
+
*
|
|
586
|
+
* Matching strategy:
|
|
587
|
+
* 1. Try exact "METHOD PATH" match
|
|
588
|
+
* 2. Try "/PATH" only match (any method)
|
|
589
|
+
*
|
|
590
|
+
* @param backend - Graph backend
|
|
591
|
+
* @param pattern - Route pattern (with or without method)
|
|
592
|
+
* @returns Route node or null if not found
|
|
593
|
+
*/
|
|
594
|
+
async function findRouteByPattern(backend, pattern) {
|
|
595
|
+
const trimmed = pattern.trim();
|
|
596
|
+
for await (const node of backend.queryNodes({ type: 'http:route' })) {
|
|
597
|
+
const method = node.method || '';
|
|
598
|
+
const path = node.path || '';
|
|
599
|
+
// Match "METHOD /path"
|
|
600
|
+
if (`${method} ${path}` === trimmed) {
|
|
601
|
+
return {
|
|
602
|
+
id: node.id,
|
|
603
|
+
type: node.type || 'http:route',
|
|
604
|
+
name: `${method} ${path}`,
|
|
605
|
+
file: node.file || '',
|
|
606
|
+
line: node.line
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// Match "/path" only (ignore method)
|
|
610
|
+
if (path === trimmed) {
|
|
611
|
+
return {
|
|
612
|
+
id: node.id,
|
|
613
|
+
type: node.type || 'http:route',
|
|
614
|
+
name: `${method} ${path}`,
|
|
615
|
+
file: node.file || '',
|
|
616
|
+
line: node.line
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Handle route-based trace (--from-route option).
|
|
624
|
+
*
|
|
625
|
+
* Flow:
|
|
626
|
+
* 1. Find route by pattern
|
|
627
|
+
* 2. Get RESPONDS_WITH edges from route
|
|
628
|
+
* 3. For each response node: call traceValues()
|
|
629
|
+
* 4. Format and display results grouped by response call
|
|
630
|
+
*
|
|
631
|
+
* @param backend - Graph backend
|
|
632
|
+
* @param pattern - Route pattern (e.g., "GET /status" or "/status")
|
|
633
|
+
* @param projectPath - Project root path
|
|
634
|
+
* @param jsonOutput - Whether to output as JSON
|
|
635
|
+
* @param maxDepth - Maximum trace depth (default 10)
|
|
636
|
+
*/
|
|
637
|
+
async function handleRouteTrace(backend, pattern, projectPath, jsonOutput, maxDepth = 10) {
|
|
638
|
+
// Find route
|
|
639
|
+
const route = await findRouteByPattern(backend, pattern);
|
|
640
|
+
if (!route) {
|
|
641
|
+
console.log(`Route not found: ${pattern}`);
|
|
642
|
+
console.log('');
|
|
643
|
+
console.log('Hint: Use "grafema query" to list available routes');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Get RESPONDS_WITH edges
|
|
647
|
+
const respondsWithEdges = await backend.getOutgoingEdges(route.id, ['RESPONDS_WITH']);
|
|
648
|
+
if (respondsWithEdges.length === 0) {
|
|
649
|
+
if (jsonOutput) {
|
|
650
|
+
console.log(JSON.stringify({
|
|
651
|
+
route: {
|
|
652
|
+
name: route.name,
|
|
653
|
+
file: route.file,
|
|
654
|
+
line: route.line
|
|
655
|
+
},
|
|
656
|
+
responses: [],
|
|
657
|
+
message: 'No response data found'
|
|
658
|
+
}, null, 2));
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
console.log(`Route: ${route.name} (${route.file}:${route.line || '?'})`);
|
|
662
|
+
console.log('');
|
|
663
|
+
console.log('No response data found for this route.');
|
|
664
|
+
console.log('');
|
|
665
|
+
console.log('Hint: Make sure ExpressResponseAnalyzer is in your config.');
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// Build response data
|
|
670
|
+
const responses = [];
|
|
671
|
+
// Trace each response
|
|
672
|
+
for (let i = 0; i < respondsWithEdges.length; i++) {
|
|
673
|
+
const edge = respondsWithEdges[i];
|
|
674
|
+
const responseNode = await backend.getNode(edge.dst);
|
|
675
|
+
if (!responseNode)
|
|
676
|
+
continue;
|
|
677
|
+
const responseMethod = edge.metadata?.responseMethod || 'unknown';
|
|
678
|
+
// Trace values from this response node
|
|
679
|
+
const traced = await traceValues(backend, responseNode.id, {
|
|
680
|
+
maxDepth,
|
|
681
|
+
followDerivesFrom: true,
|
|
682
|
+
detectNondeterministic: true
|
|
683
|
+
});
|
|
684
|
+
// Format traced values
|
|
685
|
+
const sources = await Promise.all(traced.map(async (t) => {
|
|
686
|
+
const relativePath = t.source.file.startsWith(projectPath)
|
|
687
|
+
? t.source.file.substring(projectPath.length + 1)
|
|
688
|
+
: t.source.file;
|
|
689
|
+
if (t.isUnknown) {
|
|
690
|
+
return {
|
|
691
|
+
type: 'UNKNOWN',
|
|
692
|
+
reason: t.reason || 'runtime input',
|
|
693
|
+
file: relativePath,
|
|
694
|
+
line: t.source.line,
|
|
695
|
+
id: t.source.id
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
else if (t.value !== undefined) {
|
|
699
|
+
return {
|
|
700
|
+
type: 'LITERAL',
|
|
701
|
+
value: t.value,
|
|
702
|
+
file: relativePath,
|
|
703
|
+
line: t.source.line,
|
|
704
|
+
id: t.source.id
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
// Look up node to get type and name
|
|
709
|
+
const sourceNode = await backend.getNode(t.source.id);
|
|
710
|
+
return {
|
|
711
|
+
type: sourceNode?.type || 'VALUE',
|
|
712
|
+
name: sourceNode?.name || '<unnamed>',
|
|
713
|
+
file: relativePath,
|
|
714
|
+
line: t.source.line,
|
|
715
|
+
id: t.source.id
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}));
|
|
719
|
+
responses.push({
|
|
720
|
+
index: i + 1,
|
|
721
|
+
method: responseMethod,
|
|
722
|
+
line: responseNode.line || 0,
|
|
723
|
+
sources: sources.length > 0 ? sources : []
|
|
724
|
+
});
|
|
725
|
+
if (!jsonOutput) {
|
|
726
|
+
// Display human-readable output
|
|
727
|
+
console.log(`Response ${i + 1} (res.${responseMethod} at line ${responseNode.line || '?'}):`);
|
|
728
|
+
if (sources.length === 0) {
|
|
729
|
+
console.log(' No data sources found (response may be external or complex)');
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
console.log(' Data sources:');
|
|
733
|
+
for (const src of sources) {
|
|
734
|
+
if (src.type === 'UNKNOWN') {
|
|
735
|
+
console.log(` [UNKNOWN] ${src.reason} at ${src.file}:${src.line}`);
|
|
736
|
+
}
|
|
737
|
+
else if (src.type === 'LITERAL') {
|
|
738
|
+
console.log(` [LITERAL] ${JSON.stringify(src.value)} at ${src.file}:${src.line}`);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.log(` [${src.type}] ${src.name} at ${src.file}:${src.line}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
console.log('');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// Output results
|
|
749
|
+
if (jsonOutput) {
|
|
750
|
+
console.log(JSON.stringify({
|
|
751
|
+
route: {
|
|
752
|
+
name: route.name,
|
|
753
|
+
file: route.file,
|
|
754
|
+
line: route.line
|
|
755
|
+
},
|
|
756
|
+
responses
|
|
757
|
+
}, null, 2));
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
// Human-readable output header
|
|
761
|
+
if (responses.length > 0 && !jsonOutput) {
|
|
762
|
+
// Already printed above, just for clarity
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types command - List all node types in the graph
|
|
3
|
+
*
|
|
4
|
+
* Shows all node types present in the analyzed codebase with counts.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Discovering what types exist (standard and custom)
|
|
7
|
+
* - Understanding graph composition
|
|
8
|
+
* - Finding types to use with --type flag
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
export declare const typesCommand: Command;
|
|
12
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/commands/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,eAAO,MAAM,YAAY,SAuErB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types command - List all node types in the graph
|
|
3
|
+
*
|
|
4
|
+
* Shows all node types present in the analyzed codebase with counts.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Discovering what types exist (standard and custom)
|
|
7
|
+
* - Understanding graph composition
|
|
8
|
+
* - Finding types to use with --type flag
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { resolve, join } from 'path';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
14
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
|
+
export const typesCommand = new Command('types')
|
|
16
|
+
.description('List all node types in the graph')
|
|
17
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
18
|
+
.option('-j, --json', 'Output as JSON')
|
|
19
|
+
.option('-s, --sort <by>', 'Sort by: count (default) or name', 'count')
|
|
20
|
+
.addHelpText('after', `
|
|
21
|
+
Examples:
|
|
22
|
+
grafema types List all node types with counts
|
|
23
|
+
grafema types --json Output as JSON for scripting
|
|
24
|
+
grafema types --sort name Sort alphabetically by type name
|
|
25
|
+
grafema types -s count Sort by count (default, descending)
|
|
26
|
+
|
|
27
|
+
Use with query --type:
|
|
28
|
+
grafema types # See available types
|
|
29
|
+
grafema query --type jsx:component "Button" # Query specific type
|
|
30
|
+
`)
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const projectPath = resolve(options.project);
|
|
33
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
34
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
35
|
+
if (!existsSync(dbPath)) {
|
|
36
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
37
|
+
}
|
|
38
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
39
|
+
await backend.connect();
|
|
40
|
+
try {
|
|
41
|
+
const nodeCounts = await backend.countNodesByType();
|
|
42
|
+
const entries = Object.entries(nodeCounts);
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
console.log('No nodes in graph. Run: grafema analyze');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Sort entries
|
|
48
|
+
const sortedEntries = options.sort === 'name'
|
|
49
|
+
? entries.sort((a, b) => a[0].localeCompare(b[0]))
|
|
50
|
+
: entries.sort((a, b) => b[1] - a[1]); // count descending
|
|
51
|
+
if (options.json) {
|
|
52
|
+
const result = {
|
|
53
|
+
types: sortedEntries.map(([type, count]) => ({ type, count })),
|
|
54
|
+
totalTypes: sortedEntries.length,
|
|
55
|
+
totalNodes: sortedEntries.reduce((sum, [, count]) => sum + count, 0),
|
|
56
|
+
};
|
|
57
|
+
console.log(JSON.stringify(result, null, 2));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log('Node Types in Graph:');
|
|
61
|
+
console.log('');
|
|
62
|
+
// Calculate max type length for alignment
|
|
63
|
+
const maxTypeLen = Math.max(...sortedEntries.map(([type]) => type.length));
|
|
64
|
+
for (const [type, count] of sortedEntries) {
|
|
65
|
+
const paddedType = type.padEnd(maxTypeLen);
|
|
66
|
+
const formattedCount = count.toLocaleString();
|
|
67
|
+
console.log(` ${paddedType} ${formattedCount}`);
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
const totalNodes = sortedEntries.reduce((sum, [, count]) => sum + count, 0);
|
|
71
|
+
console.log(`Total: ${sortedEntries.length} types, ${totalNodes.toLocaleString()} nodes`);
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log('Tip: Use grafema query --type <type> "pattern" to search within a type');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await backend.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -29,7 +29,20 @@ export interface DisplayableNode {
|
|
|
29
29
|
file: string;
|
|
30
30
|
/** Line number (optional) */
|
|
31
31
|
line?: number;
|
|
32
|
+
/** HTTP method (for http:route, http:request) */
|
|
33
|
+
method?: string;
|
|
34
|
+
/** Path or URL (for http:route, http:request) */
|
|
35
|
+
path?: string;
|
|
36
|
+
/** URL (for http:request) */
|
|
37
|
+
url?: string;
|
|
32
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the display name for a node based on its type.
|
|
41
|
+
*
|
|
42
|
+
* HTTP nodes use method + path/url instead of name.
|
|
43
|
+
* Other nodes use their name field.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getNodeDisplayName(node: DisplayableNode): string;
|
|
33
46
|
/**
|
|
34
47
|
* Format a node for primary display (multi-line)
|
|
35
48
|
*
|