@grafema/cli 0.1.1-alpha → 0.2.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/commands/analyze.d.ts.map +1 -1
  3. package/dist/commands/analyze.js +69 -11
  4. package/dist/commands/check.d.ts +6 -0
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +177 -1
  7. package/dist/commands/coverage.d.ts.map +1 -1
  8. package/dist/commands/coverage.js +7 -0
  9. package/dist/commands/doctor/checks.d.ts +55 -0
  10. package/dist/commands/doctor/checks.d.ts.map +1 -0
  11. package/dist/commands/doctor/checks.js +534 -0
  12. package/dist/commands/doctor/output.d.ts +20 -0
  13. package/dist/commands/doctor/output.d.ts.map +1 -0
  14. package/dist/commands/doctor/output.js +94 -0
  15. package/dist/commands/doctor/types.d.ts +42 -0
  16. package/dist/commands/doctor/types.d.ts.map +1 -0
  17. package/dist/commands/doctor/types.js +4 -0
  18. package/dist/commands/doctor.d.ts +17 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +80 -0
  21. package/dist/commands/explain.d.ts +16 -0
  22. package/dist/commands/explain.d.ts.map +1 -0
  23. package/dist/commands/explain.js +145 -0
  24. package/dist/commands/explore.d.ts +7 -1
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +204 -85
  27. package/dist/commands/get.d.ts.map +1 -1
  28. package/dist/commands/get.js +16 -4
  29. package/dist/commands/impact.d.ts.map +1 -1
  30. package/dist/commands/impact.js +48 -50
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +93 -15
  33. package/dist/commands/ls.d.ts +14 -0
  34. package/dist/commands/ls.d.ts.map +1 -0
  35. package/dist/commands/ls.js +132 -0
  36. package/dist/commands/overview.d.ts.map +1 -1
  37. package/dist/commands/overview.js +15 -2
  38. package/dist/commands/query.d.ts +98 -0
  39. package/dist/commands/query.d.ts.map +1 -1
  40. package/dist/commands/query.js +549 -136
  41. package/dist/commands/schema.d.ts +13 -0
  42. package/dist/commands/schema.d.ts.map +1 -0
  43. package/dist/commands/schema.js +279 -0
  44. package/dist/commands/server.d.ts.map +1 -1
  45. package/dist/commands/server.js +13 -6
  46. package/dist/commands/stats.d.ts.map +1 -1
  47. package/dist/commands/stats.js +7 -0
  48. package/dist/commands/trace.d.ts +73 -0
  49. package/dist/commands/trace.d.ts.map +1 -1
  50. package/dist/commands/trace.js +500 -5
  51. package/dist/commands/types.d.ts +12 -0
  52. package/dist/commands/types.d.ts.map +1 -0
  53. package/dist/commands/types.js +79 -0
  54. package/dist/utils/formatNode.d.ts +13 -0
  55. package/dist/utils/formatNode.d.ts.map +1 -1
  56. package/dist/utils/formatNode.js +35 -2
  57. package/package.json +3 -3
  58. package/src/cli.ts +10 -0
  59. package/src/commands/analyze.ts +84 -9
  60. package/src/commands/check.ts +201 -0
  61. package/src/commands/coverage.ts +7 -0
  62. package/src/commands/doctor/checks.ts +612 -0
  63. package/src/commands/doctor/output.ts +115 -0
  64. package/src/commands/doctor/types.ts +45 -0
  65. package/src/commands/doctor.ts +106 -0
  66. package/src/commands/explain.ts +173 -0
  67. package/src/commands/explore.tsx +247 -97
  68. package/src/commands/get.ts +20 -6
  69. package/src/commands/impact.ts +55 -61
  70. package/src/commands/init.ts +101 -14
  71. package/src/commands/ls.ts +166 -0
  72. package/src/commands/overview.ts +15 -2
  73. package/src/commands/query.ts +643 -149
  74. package/src/commands/schema.ts +345 -0
  75. package/src/commands/server.ts +13 -6
  76. package/src/commands/stats.ts +7 -0
  77. package/src/commands/trace.ts +647 -6
  78. package/src/commands/types.ts +94 -0
  79. package/src/utils/formatNode.ts +42 -2
@@ -9,10 +9,10 @@
9
9
  * For raw Datalog queries, use --raw flag
10
10
  */
11
11
  import { Command } from 'commander';
12
- import { resolve, join } from 'path';
12
+ import { resolve, join, relative, basename } from 'path';
13
13
  import { existsSync } from 'fs';
14
- import { RFDBServerBackend } from '@grafema/core';
15
- import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
14
+ import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
15
+ import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
16
16
  import { exitWithError } from '../utils/errorFormatter.js';
17
17
  export const queryCommand = new Command('query')
18
18
  .description('Search the code graph')
@@ -20,7 +20,47 @@ export const queryCommand = new Command('query')
20
20
  .option('-p, --project <path>', 'Project path', '.')
21
21
  .option('-j, --json', 'Output as JSON')
22
22
  .option('-l, --limit <n>', 'Limit results', '10')
23
- .option('--raw', 'Execute raw Datalog query')
23
+ .option('--raw', `Execute raw Datalog query
24
+
25
+ Predicates:
26
+ type(Id, Type) Find nodes by type or get type of node
27
+ node(Id, Type) Alias for type
28
+ edge(Src, Dst, Type) Find edges between nodes
29
+ attr(Id, Name, Value) Access node attributes (name, file, line, etc.)
30
+ path(Src, Dst) Check reachability between nodes
31
+ incoming(Dst, Src, T) Find incoming edges
32
+
33
+ Examples:
34
+ grafema query --raw 'type(X, "FUNCTION")'
35
+ grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
36
+ grafema query --raw 'edge(X, Y, "CALLS")'`)
37
+ .option('-t, --type <nodeType>', `Filter by exact node type (bypasses type aliases)
38
+
39
+ Use this when:
40
+ - Searching custom node types (jsx:component, redis:cache)
41
+ - You need exact type match without alias resolution
42
+ - Discovering nodes from plugins or custom analyzers
43
+
44
+ Examples:
45
+ grafema query --type http:request "/api"
46
+ grafema query --type FUNCTION "auth"
47
+ grafema query -t socketio:event "connect"`)
48
+ .addHelpText('after', `
49
+ Examples:
50
+ grafema query "auth" Search by name (partial match)
51
+ grafema query "function login" Search functions only
52
+ grafema query "class UserService" Search classes only
53
+ grafema query "route /api/users" Search HTTP routes by path
54
+ grafema query "response in fetchData" Search in specific function scope
55
+ grafema query "error in catch in fetchData" Search in nested scopes
56
+ grafema query "token in src/auth.ts" Search in specific file
57
+ grafema query "variable x in foo in app.ts" Combine type, name, and scopes
58
+ grafema query -l 20 "fetch" Return up to 20 results
59
+ grafema query --json "config" Output results as JSON
60
+ grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
61
+ grafema query -t http:request "/api" Search custom node types
62
+ grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
63
+ `)
24
64
  .action(async (pattern, options) => {
25
65
  const projectPath = resolve(options.project);
26
66
  const grafemaDir = join(projectPath, '.grafema');
@@ -36,15 +76,34 @@ export const queryCommand = new Command('query')
36
76
  await executeRawQuery(backend, pattern, options);
37
77
  return;
38
78
  }
39
- // Parse pattern
40
- const { type, name } = parsePattern(pattern);
41
79
  const limit = parseInt(options.limit, 10);
80
+ // Parse query with scope support
81
+ let query;
82
+ if (options.type) {
83
+ // Explicit --type bypasses pattern parsing for type
84
+ // But we still parse for scope support
85
+ const scopeParsed = parseQuery(pattern);
86
+ query = {
87
+ type: options.type,
88
+ name: scopeParsed.name,
89
+ file: scopeParsed.file,
90
+ scopes: scopeParsed.scopes,
91
+ };
92
+ }
93
+ else {
94
+ query = parseQuery(pattern);
95
+ }
42
96
  // Find matching nodes
43
- const nodes = await findNodes(backend, type, name, limit);
97
+ const nodes = await findNodes(backend, query, limit);
98
+ // Check if query has scope constraints for suggestion
99
+ const hasScope = query.file !== null || query.scopes.length > 0;
44
100
  if (nodes.length === 0) {
45
101
  console.log(`No results for "${pattern}"`);
46
- if (type) {
47
- console.log(` Try: grafema query "${name}" (search all types)`);
102
+ if (hasScope) {
103
+ console.log(` Try: grafema query "${query.name}" (search all scopes)`);
104
+ }
105
+ else if (query.type) {
106
+ console.log(` Try: grafema query "${query.name}" (search all types)`);
48
107
  }
49
108
  return;
50
109
  }
@@ -60,7 +119,7 @@ export const queryCommand = new Command('query')
60
119
  // Display results
61
120
  for (const node of nodes) {
62
121
  console.log('');
63
- displayNode(node, projectPath);
122
+ await displayNode(node, projectPath, backend);
64
123
  // Show callers and callees for functions
65
124
  if (node.type === 'FUNCTION' || node.type === 'CLASS') {
66
125
  const callers = await getCallers(backend, node.id, 5);
@@ -108,6 +167,18 @@ function parsePattern(pattern) {
108
167
  var: 'VARIABLE',
109
168
  const: 'CONSTANT',
110
169
  constant: 'CONSTANT',
170
+ // HTTP route aliases
171
+ route: 'http:route',
172
+ endpoint: 'http:route',
173
+ // HTTP request aliases
174
+ request: 'http:request',
175
+ fetch: 'http:request',
176
+ api: 'http:request',
177
+ // Socket.IO aliases
178
+ event: 'socketio:event',
179
+ emit: 'socketio:emit',
180
+ on: 'socketio:on',
181
+ listener: 'socketio:on',
111
182
  };
112
183
  if (typeMap[typeWord]) {
113
184
  return { type: typeMap[typeWord], name };
@@ -116,28 +187,311 @@ function parsePattern(pattern) {
116
187
  return { type: null, name: pattern.trim() };
117
188
  }
118
189
  /**
119
- * Find nodes by type and name
190
+ * Parse search pattern with scope support.
191
+ *
192
+ * Grammar:
193
+ * query := [type] name [" in " scope]*
194
+ * type := "function" | "class" | "variable" | etc.
195
+ * scope := <filename> | <functionName>
196
+ *
197
+ * File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
198
+ * Function scope detection: anything else
199
+ *
200
+ * IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
201
+ *
202
+ * Examples:
203
+ * "response" -> { type: null, name: "response", file: null, scopes: [] }
204
+ * "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
205
+ * "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
206
+ * "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
120
207
  */
121
- async function findNodes(backend, type, name, limit) {
208
+ export function parseQuery(pattern) {
209
+ // Split on " in " (space-padded) to get clauses
210
+ const clauses = pattern.split(/ in /);
211
+ // First clause is [type] name - use existing parsePattern logic
212
+ const firstClause = clauses[0];
213
+ const { type, name } = parsePattern(firstClause);
214
+ // Remaining clauses are scopes
215
+ let file = null;
216
+ const scopes = [];
217
+ for (let i = 1; i < clauses.length; i++) {
218
+ const scope = clauses[i].trim();
219
+ if (scope === '')
220
+ continue; // Skip empty clauses from trailing whitespace
221
+ if (isFileScope(scope)) {
222
+ file = scope;
223
+ }
224
+ else {
225
+ scopes.push(scope);
226
+ }
227
+ }
228
+ return { type, name, file, scopes };
229
+ }
230
+ /**
231
+ * Detect if a scope string looks like a file path.
232
+ *
233
+ * Heuristics:
234
+ * - Contains "/" -> file path
235
+ * - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
236
+ *
237
+ * Examples:
238
+ * "src/app.ts" -> true
239
+ * "app.js" -> true
240
+ * "fetchData" -> false
241
+ * "UserService" -> false
242
+ * "catch" -> false
243
+ */
244
+ export function isFileScope(scope) {
245
+ // Contains path separator
246
+ if (scope.includes('/'))
247
+ return true;
248
+ // Ends with common JS/TS extensions
249
+ const fileExtensions = /\.(ts|js|tsx|jsx|mjs|cjs)$/i;
250
+ if (fileExtensions.test(scope))
251
+ return true;
252
+ return false;
253
+ }
254
+ /**
255
+ * Check if a semantic ID matches the given scope constraints.
256
+ *
257
+ * Uses parseSemanticId from @grafema/core for robust ID parsing.
258
+ *
259
+ * Scope matching rules:
260
+ * - File scope: semantic ID must match the file path (full or basename)
261
+ * - Function/class scope: semantic ID must contain the scope in its scopePath
262
+ * - Multiple scopes: ALL must match (AND logic)
263
+ * - Scope order: independent - all scopes just need to be present
264
+ *
265
+ * Examples:
266
+ * ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
267
+ * Matches: scopes=["fetchData"] -> true
268
+ * Matches: scopes=["try"] -> true (matches "try#0")
269
+ * Matches: scopes=["fetchData", "try"] -> true (both present)
270
+ * Matches: scopes=["processData"] -> false (not in ID)
271
+ *
272
+ * @param semanticId - The full semantic ID to check
273
+ * @param file - File scope (null for any file)
274
+ * @param scopes - Array of scope names to match
275
+ * @returns true if ID matches all constraints
276
+ */
277
+ export function matchesScope(semanticId, file, scopes) {
278
+ const parsed = parseSemanticId(semanticId);
279
+ if (!parsed)
280
+ return false;
281
+ // File scope check
282
+ if (file !== null) {
283
+ // Full path match
284
+ if (parsed.file === file) {
285
+ // Exact match - OK
286
+ }
287
+ // Basename match: "app.ts" matches "src/app.ts"
288
+ else if (parsed.file.endsWith('/' + file)) {
289
+ // Partial path match - OK
290
+ }
291
+ // Also try if parsed.file ends with the file name (e.g., file is basename)
292
+ else if (basename(parsed.file) === file) {
293
+ // Basename exact match - OK
294
+ }
295
+ else {
296
+ return false;
297
+ }
298
+ }
299
+ // Function/class/block scope check
300
+ for (const scope of scopes) {
301
+ // Check if scope appears in the scopePath
302
+ // Handle numbered scopes: "try" matches "try#0"
303
+ const matches = parsed.scopePath.some(s => s === scope || s.startsWith(scope + '#'));
304
+ if (!matches)
305
+ return false;
306
+ }
307
+ return true;
308
+ }
309
+ /**
310
+ * Extract human-readable scope context from a semantic ID.
311
+ *
312
+ * Parses the ID and returns a description of the scope chain.
313
+ *
314
+ * Examples:
315
+ * "src/app.ts->fetchData->try#0->VARIABLE->response"
316
+ * -> "inside fetchData, inside try block"
317
+ *
318
+ * "src/app.ts->UserService->login->VARIABLE->token"
319
+ * -> "inside UserService, inside login"
320
+ *
321
+ * "src/app.ts->global->FUNCTION->main"
322
+ * -> null (no interesting scope)
323
+ *
324
+ * @param semanticId - The semantic ID to parse
325
+ * @returns Human-readable scope context or null
326
+ */
327
+ export function extractScopeContext(semanticId) {
328
+ const parsed = parseSemanticId(semanticId);
329
+ if (!parsed)
330
+ return null;
331
+ // Filter out "global" and format remaining scopes
332
+ const meaningfulScopes = parsed.scopePath.filter(s => s !== 'global');
333
+ if (meaningfulScopes.length === 0)
334
+ return null;
335
+ // Format each scope with context
336
+ const formatted = meaningfulScopes.map(scope => {
337
+ // Handle numbered scopes: "try#0" -> "try block"
338
+ if (scope.match(/^try#\d+$/))
339
+ return 'try block';
340
+ if (scope.match(/^catch#\d+$/))
341
+ return 'catch block';
342
+ if (scope.match(/^if#\d+$/))
343
+ return 'conditional';
344
+ if (scope.match(/^else#\d+$/))
345
+ return 'else block';
346
+ if (scope.match(/^for#\d+$/))
347
+ return 'loop';
348
+ if (scope.match(/^while#\d+$/))
349
+ return 'loop';
350
+ if (scope.match(/^switch#\d+$/))
351
+ return 'switch';
352
+ // Regular scope: function or class name
353
+ return scope;
354
+ });
355
+ // Build "inside X, inside Y" string
356
+ return 'inside ' + formatted.join(', inside ');
357
+ }
358
+ /**
359
+ * Check if a node matches the search pattern based on its type.
360
+ *
361
+ * Different node types have different searchable fields:
362
+ * - http:route: search method and path fields
363
+ * - http:request: search method and url fields
364
+ * - socketio:event: search name field (standard)
365
+ * - socketio:emit/on: search event field
366
+ * - Default: search name field
367
+ */
368
+ function matchesSearchPattern(node, nodeType, pattern) {
369
+ const lowerPattern = pattern.toLowerCase();
370
+ // HTTP routes: search method and path
371
+ if (nodeType === 'http:route') {
372
+ const method = (node.method || '').toLowerCase();
373
+ const path = (node.path || '').toLowerCase();
374
+ // Pattern could be: "POST", "/api/users", "POST /api", etc.
375
+ const patternParts = pattern.trim().split(/\s+/);
376
+ if (patternParts.length === 1) {
377
+ // Single term: match method OR path
378
+ const term = patternParts[0].toLowerCase();
379
+ return method === term || path.includes(term);
380
+ }
381
+ else {
382
+ // Multiple terms: first is method, rest is path pattern
383
+ const methodPattern = patternParts[0].toLowerCase();
384
+ const pathPattern = patternParts.slice(1).join(' ').toLowerCase();
385
+ // Method must match exactly (GET, POST, etc.)
386
+ const methodMatches = method === methodPattern;
387
+ // Path must contain the pattern
388
+ const pathMatches = path.includes(pathPattern);
389
+ return methodMatches && pathMatches;
390
+ }
391
+ }
392
+ // HTTP requests: search method and url
393
+ if (nodeType === 'http:request') {
394
+ const method = (node.method || '').toLowerCase();
395
+ const url = (node.url || '').toLowerCase();
396
+ // Pattern could be: "POST", "/api/users", "POST /api", etc.
397
+ const patternParts = pattern.trim().split(/\s+/);
398
+ if (patternParts.length === 1) {
399
+ // Single term: match method OR url
400
+ const term = patternParts[0].toLowerCase();
401
+ return method === term || url.includes(term);
402
+ }
403
+ else {
404
+ // Multiple terms: first is method, rest is url pattern
405
+ const methodPattern = patternParts[0].toLowerCase();
406
+ const urlPattern = patternParts.slice(1).join(' ').toLowerCase();
407
+ // Method must match exactly (GET, POST, etc.)
408
+ const methodMatches = method === methodPattern;
409
+ // URL must contain the pattern
410
+ const urlMatches = url.includes(urlPattern);
411
+ return methodMatches && urlMatches;
412
+ }
413
+ }
414
+ // Socket.IO event channels: search name field (standard)
415
+ if (nodeType === 'socketio:event') {
416
+ const nodeName = (node.name || '').toLowerCase();
417
+ return nodeName.includes(lowerPattern);
418
+ }
419
+ // Socket.IO emit/on: search event field
420
+ if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
421
+ const eventName = (node.event || '').toLowerCase();
422
+ return eventName.includes(lowerPattern);
423
+ }
424
+ // Default: search name field
425
+ const nodeName = (node.name || '').toLowerCase();
426
+ return nodeName.includes(lowerPattern);
427
+ }
428
+ /**
429
+ * Find nodes by query (type, name, file scope, function scopes)
430
+ */
431
+ async function findNodes(backend, query, limit) {
122
432
  const results = [];
123
- const searchTypes = type
124
- ? [type]
125
- : ['FUNCTION', 'CLASS', 'MODULE', 'VARIABLE', 'CONSTANT'];
433
+ const searchTypes = query.type
434
+ ? [query.type]
435
+ : [
436
+ 'FUNCTION',
437
+ 'CLASS',
438
+ 'MODULE',
439
+ 'VARIABLE',
440
+ 'CONSTANT',
441
+ 'http:route',
442
+ 'http:request',
443
+ 'socketio:event',
444
+ 'socketio:emit',
445
+ 'socketio:on'
446
+ ];
126
447
  for (const nodeType of searchTypes) {
127
448
  for await (const node of backend.queryNodes({ nodeType: nodeType })) {
128
- const nodeName = node.name || '';
129
- // Case-insensitive partial match
130
- if (nodeName.toLowerCase().includes(name.toLowerCase())) {
131
- results.push({
132
- id: node.id,
133
- type: node.type || nodeType,
134
- name: nodeName,
135
- file: node.file || '',
136
- line: node.line,
137
- });
138
- if (results.length >= limit)
139
- break;
449
+ // Type-aware field matching (name)
450
+ const nameMatches = matchesSearchPattern(node, nodeType, query.name);
451
+ if (!nameMatches)
452
+ continue;
453
+ // Scope matching (file and function scopes)
454
+ const scopeMatches = matchesScope(node.id, query.file, query.scopes);
455
+ if (!scopeMatches)
456
+ continue;
457
+ const nodeInfo = {
458
+ id: node.id,
459
+ type: node.type || nodeType,
460
+ name: node.name || '',
461
+ file: node.file || '',
462
+ line: node.line,
463
+ };
464
+ // Add scope context for display
465
+ nodeInfo.scopeContext = extractScopeContext(node.id);
466
+ // Include method and path for http:route nodes
467
+ if (nodeType === 'http:route') {
468
+ nodeInfo.method = node.method;
469
+ nodeInfo.path = node.path;
470
+ }
471
+ // Include method and url for http:request nodes
472
+ if (nodeType === 'http:request') {
473
+ nodeInfo.method = node.method;
474
+ nodeInfo.url = node.url;
475
+ }
476
+ // Include event field for Socket.IO nodes
477
+ if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
478
+ nodeInfo.event = node.event;
140
479
  }
480
+ // Include emit-specific fields
481
+ if (nodeType === 'socketio:emit') {
482
+ nodeInfo.room = node.room;
483
+ nodeInfo.namespace = node.namespace;
484
+ nodeInfo.broadcast = node.broadcast;
485
+ nodeInfo.objectName = node.objectName;
486
+ }
487
+ // Include listener-specific fields
488
+ if (nodeType === 'socketio:on') {
489
+ nodeInfo.objectName = node.objectName;
490
+ nodeInfo.handlerName = node.handlerName;
491
+ }
492
+ results.push(nodeInfo);
493
+ if (results.length >= limit)
494
+ break;
141
495
  }
142
496
  if (results.length >= limit)
143
497
  break;
@@ -163,8 +517,8 @@ async function getCallers(backend, nodeId, limit) {
163
517
  const callNode = await backend.getNode(edge.src);
164
518
  if (!callNode)
165
519
  continue;
166
- // Find the FUNCTION that contains this CALL
167
- const containingFunc = await findContainingFunction(backend, callNode.id);
520
+ // Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
521
+ const containingFunc = await findContainingFunctionCore(backend, callNode.id);
168
522
  if (containingFunc && !seen.has(containingFunc.id)) {
169
523
  seen.add(containingFunc.id);
170
524
  callers.push({
@@ -177,142 +531,201 @@ async function getCallers(backend, nodeId, limit) {
177
531
  }
178
532
  }
179
533
  }
180
- catch {
181
- // Ignore errors
182
- }
183
- return callers;
184
- }
185
- /**
186
- * Find the FUNCTION or CLASS that contains a node
187
- *
188
- * Path can be: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
189
- * So we need to follow both CONTAINS and HAS_SCOPE edges
190
- */
191
- async function findContainingFunction(backend, nodeId, maxDepth = 15) {
192
- const visited = new Set();
193
- const queue = [{ id: nodeId, depth: 0 }];
194
- while (queue.length > 0) {
195
- const { id, depth } = queue.shift();
196
- if (visited.has(id) || depth > maxDepth)
197
- continue;
198
- visited.add(id);
199
- try {
200
- // Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
201
- const edges = await backend.getIncomingEdges(id, null);
202
- for (const edge of edges) {
203
- const edgeType = edge.edgeType || edge.type;
204
- // Only follow structural edges
205
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
206
- continue;
207
- const parentNode = await backend.getNode(edge.src);
208
- if (!parentNode || visited.has(parentNode.id))
209
- continue;
210
- const parentType = parentNode.type;
211
- // FUNCTION, CLASS, or MODULE (for top-level calls)
212
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
213
- return {
214
- id: parentNode.id,
215
- type: parentType,
216
- name: parentNode.name || '<anonymous>',
217
- file: parentNode.file || '',
218
- line: parentNode.line,
219
- };
220
- }
221
- // Continue searching from this parent
222
- queue.push({ id: parentNode.id, depth: depth + 1 });
223
- }
224
- }
225
- catch {
226
- // Ignore errors
534
+ catch (error) {
535
+ if (process.env.DEBUG) {
536
+ console.error('[query] Error in getCallers:', error);
227
537
  }
228
538
  }
229
- return null;
539
+ return callers;
230
540
  }
231
541
  /**
232
542
  * Get functions that this node calls
233
543
  *
234
- * Logic: FUNCTION CONTAINS CALL → CALLS → TARGET
235
- * Find all CALL nodes inside this function, then find what they call
544
+ * Uses shared utility from @grafema/core which:
545
+ * - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
546
+ * - Finds both CALL and METHOD_CALL nodes
547
+ * - Only returns resolved calls (those with CALLS edges to targets)
236
548
  */
237
549
  async function getCallees(backend, nodeId, limit) {
238
550
  const callees = [];
239
551
  const seen = new Set();
240
552
  try {
241
- // Find all CALL nodes inside this function (via CONTAINS)
242
- const callNodes = await findCallsInFunction(backend, nodeId);
243
- for (const callNode of callNodes) {
553
+ // Use shared utility (now includes METHOD_CALL and correct graph traversal)
554
+ const calls = await findCallsInFunctionCore(backend, nodeId);
555
+ for (const call of calls) {
244
556
  if (callees.length >= limit)
245
557
  break;
246
- // Find what this CALL calls
247
- const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
248
- for (const edge of callEdges) {
249
- if (callees.length >= limit)
250
- break;
251
- const targetNode = await backend.getNode(edge.dst);
252
- if (!targetNode || seen.has(targetNode.id))
253
- continue;
254
- seen.add(targetNode.id);
558
+ // Only include resolved calls with targets
559
+ if (call.resolved && call.target && !seen.has(call.target.id)) {
560
+ seen.add(call.target.id);
255
561
  callees.push({
256
- id: targetNode.id,
257
- type: targetNode.type || 'UNKNOWN',
258
- name: targetNode.name || '<anonymous>',
259
- file: targetNode.file || '',
260
- line: targetNode.line,
562
+ id: call.target.id,
563
+ type: 'FUNCTION',
564
+ name: call.target.name || '<anonymous>',
565
+ file: call.target.file || '',
566
+ line: call.target.line,
261
567
  });
262
568
  }
263
569
  }
264
570
  }
265
- catch {
266
- // Ignore errors
571
+ catch (error) {
572
+ if (process.env.DEBUG) {
573
+ console.error('[query] Error in getCallees:', error);
574
+ }
267
575
  }
268
576
  return callees;
269
577
  }
270
578
  /**
271
- * Find all CALL nodes inside a function (recursively via CONTAINS)
579
+ * Display a node with semantic ID as primary identifier
272
580
  */
273
- async function findCallsInFunction(backend, nodeId, maxDepth = 10) {
274
- const calls = [];
275
- const visited = new Set();
276
- const queue = [{ id: nodeId, depth: 0 }];
277
- while (queue.length > 0) {
278
- const { id, depth } = queue.shift();
279
- if (visited.has(id) || depth > maxDepth)
280
- continue;
281
- visited.add(id);
282
- try {
283
- // Get children via CONTAINS
284
- const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
285
- for (const edge of edges) {
286
- const child = await backend.getNode(edge.dst);
287
- if (!child)
288
- continue;
289
- const childType = child.type;
290
- if (childType === 'CALL') {
291
- calls.push({
292
- id: child.id,
293
- type: 'CALL',
294
- name: child.name || '',
295
- file: child.file || '',
296
- line: child.line,
297
- });
298
- }
299
- // Continue searching in children (but not into nested functions)
300
- if (childType !== 'FUNCTION' && childType !== 'CLASS') {
301
- queue.push({ id: child.id, depth: depth + 1 });
302
- }
303
- }
581
+ async function displayNode(node, projectPath, backend) {
582
+ // Special formatting for HTTP routes
583
+ if (node.type === 'http:route' && node.method && node.path) {
584
+ console.log(formatHttpRouteDisplay(node, projectPath));
585
+ return;
586
+ }
587
+ // Special formatting for HTTP requests
588
+ if (node.type === 'http:request') {
589
+ console.log(formatHttpRequestDisplay(node, projectPath));
590
+ return;
591
+ }
592
+ // Special formatting for Socket.IO event channels
593
+ if (node.type === 'socketio:event') {
594
+ console.log(await formatSocketEventDisplay(node, projectPath, backend));
595
+ return;
596
+ }
597
+ // Special formatting for Socket.IO emit/on
598
+ if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
599
+ console.log(formatSocketIONodeDisplay(node, projectPath));
600
+ return;
601
+ }
602
+ console.log(formatNodeDisplay(node, { projectPath }));
603
+ // Add scope context if present
604
+ if (node.scopeContext) {
605
+ console.log(` Scope: ${node.scopeContext}`);
606
+ }
607
+ }
608
+ /**
609
+ * Format HTTP route for display
610
+ *
611
+ * Output:
612
+ * [http:route] POST /api/users
613
+ * Location: src/routes/users.js:15
614
+ */
615
+ function formatHttpRouteDisplay(node, projectPath) {
616
+ const lines = [];
617
+ // Line 1: [type] METHOD PATH
618
+ lines.push(`[${node.type}] ${node.method} ${node.path}`);
619
+ // Line 2: Location
620
+ if (node.file) {
621
+ const relPath = relative(projectPath, node.file);
622
+ const loc = node.line ? `${relPath}:${node.line}` : relPath;
623
+ lines.push(` Location: ${loc}`);
624
+ }
625
+ return lines.join('\n');
626
+ }
627
+ /**
628
+ * Format HTTP request for display
629
+ *
630
+ * Output:
631
+ * [http:request] GET /api/users
632
+ * Location: src/pages/Users.tsx:42
633
+ */
634
+ function formatHttpRequestDisplay(node, projectPath) {
635
+ const lines = [];
636
+ // Line 1: [type] METHOD URL
637
+ const method = node.method || 'GET';
638
+ const url = node.url || 'dynamic';
639
+ lines.push(`[${node.type}] ${method} ${url}`);
640
+ // Line 2: Location
641
+ if (node.file) {
642
+ const relPath = relative(projectPath, node.file);
643
+ const loc = node.line ? `${relPath}:${node.line}` : relPath;
644
+ lines.push(` Location: ${loc}`);
645
+ }
646
+ return lines.join('\n');
647
+ }
648
+ /**
649
+ * Format Socket.IO event channel for display
650
+ *
651
+ * Output:
652
+ * [socketio:event] slot:booked
653
+ * ID: socketio:event#slot:booked
654
+ * Emitted by: 3 locations
655
+ * Listened by: 5 locations
656
+ */
657
+ async function formatSocketEventDisplay(node, projectPath, backend) {
658
+ const lines = [];
659
+ // Line 1: [type] event_name
660
+ lines.push(`[${node.type}] ${node.name}`);
661
+ // Line 2: ID
662
+ lines.push(` ID: ${node.id}`);
663
+ // Query edges to get emitter and listener counts
664
+ try {
665
+ const incomingEdges = await backend.getIncomingEdges(node.id, ['EMITS_EVENT']);
666
+ const outgoingEdges = await backend.getOutgoingEdges(node.id, ['LISTENED_BY']);
667
+ if (incomingEdges.length > 0) {
668
+ lines.push(` Emitted by: ${incomingEdges.length} location${incomingEdges.length !== 1 ? 's' : ''}`);
669
+ }
670
+ if (outgoingEdges.length > 0) {
671
+ lines.push(` Listened by: ${outgoingEdges.length} location${outgoingEdges.length !== 1 ? 's' : ''}`);
304
672
  }
305
- catch {
306
- // Ignore
673
+ }
674
+ catch (error) {
675
+ if (process.env.DEBUG) {
676
+ console.error('[query] Error in formatSocketEventDisplay:', error);
307
677
  }
308
678
  }
309
- return calls;
679
+ return lines.join('\n');
310
680
  }
311
681
  /**
312
- * Display a node with semantic ID as primary identifier
682
+ * Format Socket.IO emit/on for display
683
+ *
684
+ * Output for emit:
685
+ * [socketio:emit] slot:booked
686
+ * ID: socketio:emit#slot:booked#server.js#28
687
+ * Location: server.js:28
688
+ * Room: gig:123 (if applicable)
689
+ * Namespace: /admin (if applicable)
690
+ * Broadcast: true (if applicable)
691
+ *
692
+ * Output for on:
693
+ * [socketio:on] slot:booked
694
+ * ID: socketio:on#slot:booked#client.js#13
695
+ * Location: client.js:13
696
+ * Handler: anonymous:27
313
697
  */
314
- function displayNode(node, projectPath) {
315
- console.log(formatNodeDisplay(node, { projectPath }));
698
+ function formatSocketIONodeDisplay(node, projectPath) {
699
+ const lines = [];
700
+ // Line 1: [type] event_name
701
+ const eventName = node.event || node.name || 'unknown';
702
+ lines.push(`[${node.type}] ${eventName}`);
703
+ // Line 2: ID
704
+ lines.push(` ID: ${node.id}`);
705
+ // Line 3: Location (if applicable)
706
+ if (node.file) {
707
+ const loc = formatLocation(node.file, node.line, projectPath);
708
+ if (loc) {
709
+ lines.push(` Location: ${loc}`);
710
+ }
711
+ }
712
+ // Emit-specific fields
713
+ if (node.type === 'socketio:emit') {
714
+ if (node.room) {
715
+ lines.push(` Room: ${node.room}`);
716
+ }
717
+ if (node.namespace) {
718
+ lines.push(` Namespace: ${node.namespace}`);
719
+ }
720
+ if (node.broadcast) {
721
+ lines.push(` Broadcast: true`);
722
+ }
723
+ }
724
+ // Listener-specific fields
725
+ if (node.type === 'socketio:on' && node.handlerName) {
726
+ lines.push(` Handler: ${node.handlerName}`);
727
+ }
728
+ return lines.join('\n');
316
729
  }
317
730
  /**
318
731
  * Execute raw Datalog query (backwards compat)