@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
@@ -10,10 +10,10 @@
10
10
  */
11
11
 
12
12
  import { Command } from 'commander';
13
- import { resolve, join, relative } from 'path';
13
+ import { resolve, join, relative, basename } from 'path';
14
14
  import { existsSync } from 'fs';
15
- import { RFDBServerBackend } from '@grafema/core';
16
- import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
15
+ import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
16
+ import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
17
17
  import { exitWithError } from '../utils/errorFormatter.js';
18
18
 
19
19
  interface QueryOptions {
@@ -21,6 +21,7 @@ interface QueryOptions {
21
21
  json?: boolean;
22
22
  limit: string;
23
23
  raw?: boolean;
24
+ type?: string; // Explicit node type (bypasses type aliases)
24
25
  }
25
26
 
26
27
  interface NodeInfo {
@@ -29,16 +30,94 @@ interface NodeInfo {
29
30
  name: string;
30
31
  file: string;
31
32
  line?: number;
33
+ method?: string; // For http:route, http:request
34
+ path?: string; // For http:route
35
+ url?: string; // For http:request
36
+ event?: string; // For socketio:emit, socketio:on, socketio:event
37
+ room?: string; // For socketio:emit
38
+ namespace?: string; // For socketio:emit
39
+ broadcast?: boolean; // For socketio:emit
40
+ objectName?: string; // For socketio:emit, socketio:on
41
+ handlerName?: string; // For socketio:on
42
+ /** Human-readable scope context */
43
+ scopeContext?: string | null;
32
44
  [key: string]: unknown;
33
45
  }
34
46
 
47
+ /**
48
+ * Parsed query with optional scope constraints.
49
+ *
50
+ * Supports patterns like:
51
+ * "response" -> { name: "response" }
52
+ * "variable response" -> { type: "VARIABLE", name: "response" }
53
+ * "response in fetchData" -> { name: "response", scopes: ["fetchData"] }
54
+ * "response in src/app.ts" -> { name: "response", file: "src/app.ts" }
55
+ * "response in catch in fetchData" -> { name: "response", scopes: ["fetchData", "catch"] }
56
+ */
57
+ export interface ParsedQuery {
58
+ /** Node type (e.g., "FUNCTION", "VARIABLE") or null for any */
59
+ type: string | null;
60
+ /** Node name to search (partial match) */
61
+ name: string;
62
+ /** File scope - filter to nodes in this file */
63
+ file: string | null;
64
+ /** Scope chain - filter to nodes inside these scopes (function/class/block names) */
65
+ scopes: string[];
66
+ }
67
+
35
68
  export const queryCommand = new Command('query')
36
69
  .description('Search the code graph')
37
70
  .argument('<pattern>', 'Search pattern: "function X", "class Y", or just "X"')
38
71
  .option('-p, --project <path>', 'Project path', '.')
39
72
  .option('-j, --json', 'Output as JSON')
40
73
  .option('-l, --limit <n>', 'Limit results', '10')
41
- .option('--raw', 'Execute raw Datalog query')
74
+ .option(
75
+ '--raw',
76
+ `Execute raw Datalog query
77
+
78
+ Predicates:
79
+ type(Id, Type) Find nodes by type or get type of node
80
+ node(Id, Type) Alias for type
81
+ edge(Src, Dst, Type) Find edges between nodes
82
+ attr(Id, Name, Value) Access node attributes (name, file, line, etc.)
83
+ path(Src, Dst) Check reachability between nodes
84
+ incoming(Dst, Src, T) Find incoming edges
85
+
86
+ Examples:
87
+ grafema query --raw 'type(X, "FUNCTION")'
88
+ grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
89
+ grafema query --raw 'edge(X, Y, "CALLS")'`
90
+ )
91
+ .option(
92
+ '-t, --type <nodeType>',
93
+ `Filter by exact node type (bypasses type aliases)
94
+
95
+ Use this when:
96
+ - Searching custom node types (jsx:component, redis:cache)
97
+ - You need exact type match without alias resolution
98
+ - Discovering nodes from plugins or custom analyzers
99
+
100
+ Examples:
101
+ grafema query --type http:request "/api"
102
+ grafema query --type FUNCTION "auth"
103
+ grafema query -t socketio:event "connect"`
104
+ )
105
+ .addHelpText('after', `
106
+ Examples:
107
+ grafema query "auth" Search by name (partial match)
108
+ grafema query "function login" Search functions only
109
+ grafema query "class UserService" Search classes only
110
+ grafema query "route /api/users" Search HTTP routes by path
111
+ grafema query "response in fetchData" Search in specific function scope
112
+ grafema query "error in catch in fetchData" Search in nested scopes
113
+ grafema query "token in src/auth.ts" Search in specific file
114
+ grafema query "variable x in foo in app.ts" Combine type, name, and scopes
115
+ grafema query -l 20 "fetch" Return up to 20 results
116
+ grafema query --json "config" Output results as JSON
117
+ grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
118
+ grafema query -t http:request "/api" Search custom node types
119
+ grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
120
+ `)
42
121
  .action(async (pattern: string, options: QueryOptions) => {
43
122
  const projectPath = resolve(options.project);
44
123
  const grafemaDir = join(projectPath, '.grafema');
@@ -58,17 +137,37 @@ export const queryCommand = new Command('query')
58
137
  return;
59
138
  }
60
139
 
61
- // Parse pattern
62
- const { type, name } = parsePattern(pattern);
63
140
  const limit = parseInt(options.limit, 10);
64
141
 
142
+ // Parse query with scope support
143
+ let query: ParsedQuery;
144
+
145
+ if (options.type) {
146
+ // Explicit --type bypasses pattern parsing for type
147
+ // But we still parse for scope support
148
+ const scopeParsed = parseQuery(pattern);
149
+ query = {
150
+ type: options.type,
151
+ name: scopeParsed.name,
152
+ file: scopeParsed.file,
153
+ scopes: scopeParsed.scopes,
154
+ };
155
+ } else {
156
+ query = parseQuery(pattern);
157
+ }
158
+
65
159
  // Find matching nodes
66
- const nodes = await findNodes(backend, type, name, limit);
160
+ const nodes = await findNodes(backend, query, limit);
161
+
162
+ // Check if query has scope constraints for suggestion
163
+ const hasScope = query.file !== null || query.scopes.length > 0;
67
164
 
68
165
  if (nodes.length === 0) {
69
166
  console.log(`No results for "${pattern}"`);
70
- if (type) {
71
- console.log(` Try: grafema query "${name}" (search all types)`);
167
+ if (hasScope) {
168
+ console.log(` Try: grafema query "${query.name}" (search all scopes)`);
169
+ } else if (query.type) {
170
+ console.log(` Try: grafema query "${query.name}" (search all types)`);
72
171
  }
73
172
  return;
74
173
  }
@@ -88,7 +187,7 @@ export const queryCommand = new Command('query')
88
187
  // Display results
89
188
  for (const node of nodes) {
90
189
  console.log('');
91
- displayNode(node, projectPath);
190
+ await displayNode(node, projectPath, backend);
92
191
 
93
192
  // Show callers and callees for functions
94
193
  if (node.type === 'FUNCTION' || node.type === 'CLASS') {
@@ -143,6 +242,18 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
143
242
  var: 'VARIABLE',
144
243
  const: 'CONSTANT',
145
244
  constant: 'CONSTANT',
245
+ // HTTP route aliases
246
+ route: 'http:route',
247
+ endpoint: 'http:route',
248
+ // HTTP request aliases
249
+ request: 'http:request',
250
+ fetch: 'http:request',
251
+ api: 'http:request',
252
+ // Socket.IO aliases
253
+ event: 'socketio:event',
254
+ emit: 'socketio:emit',
255
+ on: 'socketio:on',
256
+ listener: 'socketio:on',
146
257
  };
147
258
 
148
259
  if (typeMap[typeWord]) {
@@ -154,33 +265,349 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
154
265
  }
155
266
 
156
267
  /**
157
- * Find nodes by type and name
268
+ * Parse search pattern with scope support.
269
+ *
270
+ * Grammar:
271
+ * query := [type] name [" in " scope]*
272
+ * type := "function" | "class" | "variable" | etc.
273
+ * scope := <filename> | <functionName>
274
+ *
275
+ * File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
276
+ * Function scope detection: anything else
277
+ *
278
+ * IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
279
+ *
280
+ * Examples:
281
+ * "response" -> { type: null, name: "response", file: null, scopes: [] }
282
+ * "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
283
+ * "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
284
+ * "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
285
+ */
286
+ export function parseQuery(pattern: string): ParsedQuery {
287
+ // Split on " in " (space-padded) to get clauses
288
+ const clauses = pattern.split(/ in /);
289
+
290
+ // First clause is [type] name - use existing parsePattern logic
291
+ const firstClause = clauses[0];
292
+ const { type, name } = parsePattern(firstClause);
293
+
294
+ // Remaining clauses are scopes
295
+ let file: string | null = null;
296
+ const scopes: string[] = [];
297
+
298
+ for (let i = 1; i < clauses.length; i++) {
299
+ const scope = clauses[i].trim();
300
+ if (scope === '') continue; // Skip empty clauses from trailing whitespace
301
+ if (isFileScope(scope)) {
302
+ file = scope;
303
+ } else {
304
+ scopes.push(scope);
305
+ }
306
+ }
307
+
308
+ return { type, name, file, scopes };
309
+ }
310
+
311
+ /**
312
+ * Detect if a scope string looks like a file path.
313
+ *
314
+ * Heuristics:
315
+ * - Contains "/" -> file path
316
+ * - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
317
+ *
318
+ * Examples:
319
+ * "src/app.ts" -> true
320
+ * "app.js" -> true
321
+ * "fetchData" -> false
322
+ * "UserService" -> false
323
+ * "catch" -> false
324
+ */
325
+ export function isFileScope(scope: string): boolean {
326
+ // Contains path separator
327
+ if (scope.includes('/')) return true;
328
+
329
+ // Ends with common JS/TS extensions
330
+ const fileExtensions = /\.(ts|js|tsx|jsx|mjs|cjs)$/i;
331
+ if (fileExtensions.test(scope)) return true;
332
+
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * Check if a semantic ID matches the given scope constraints.
338
+ *
339
+ * Uses parseSemanticId from @grafema/core for robust ID parsing.
340
+ *
341
+ * Scope matching rules:
342
+ * - File scope: semantic ID must match the file path (full or basename)
343
+ * - Function/class scope: semantic ID must contain the scope in its scopePath
344
+ * - Multiple scopes: ALL must match (AND logic)
345
+ * - Scope order: independent - all scopes just need to be present
346
+ *
347
+ * Examples:
348
+ * ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
349
+ * Matches: scopes=["fetchData"] -> true
350
+ * Matches: scopes=["try"] -> true (matches "try#0")
351
+ * Matches: scopes=["fetchData", "try"] -> true (both present)
352
+ * Matches: scopes=["processData"] -> false (not in ID)
353
+ *
354
+ * @param semanticId - The full semantic ID to check
355
+ * @param file - File scope (null for any file)
356
+ * @param scopes - Array of scope names to match
357
+ * @returns true if ID matches all constraints
358
+ */
359
+ export function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean {
360
+ const parsed = parseSemanticId(semanticId);
361
+ if (!parsed) return false;
362
+
363
+ // File scope check
364
+ if (file !== null) {
365
+ // Full path match
366
+ if (parsed.file === file) {
367
+ // Exact match - OK
368
+ }
369
+ // Basename match: "app.ts" matches "src/app.ts"
370
+ else if (parsed.file.endsWith('/' + file)) {
371
+ // Partial path match - OK
372
+ }
373
+ // Also try if parsed.file ends with the file name (e.g., file is basename)
374
+ else if (basename(parsed.file) === file) {
375
+ // Basename exact match - OK
376
+ }
377
+ else {
378
+ return false;
379
+ }
380
+ }
381
+
382
+ // Function/class/block scope check
383
+ for (const scope of scopes) {
384
+ // Check if scope appears in the scopePath
385
+ // Handle numbered scopes: "try" matches "try#0"
386
+ const matches = parsed.scopePath.some(s =>
387
+ s === scope || s.startsWith(scope + '#')
388
+ );
389
+ if (!matches) return false;
390
+ }
391
+
392
+ return true;
393
+ }
394
+
395
+ /**
396
+ * Extract human-readable scope context from a semantic ID.
397
+ *
398
+ * Parses the ID and returns a description of the scope chain.
399
+ *
400
+ * Examples:
401
+ * "src/app.ts->fetchData->try#0->VARIABLE->response"
402
+ * -> "inside fetchData, inside try block"
403
+ *
404
+ * "src/app.ts->UserService->login->VARIABLE->token"
405
+ * -> "inside UserService, inside login"
406
+ *
407
+ * "src/app.ts->global->FUNCTION->main"
408
+ * -> null (no interesting scope)
409
+ *
410
+ * @param semanticId - The semantic ID to parse
411
+ * @returns Human-readable scope context or null
412
+ */
413
+ export function extractScopeContext(semanticId: string): string | null {
414
+ const parsed = parseSemanticId(semanticId);
415
+ if (!parsed) return null;
416
+
417
+ // Filter out "global" and format remaining scopes
418
+ const meaningfulScopes = parsed.scopePath.filter(s => s !== 'global');
419
+ if (meaningfulScopes.length === 0) return null;
420
+
421
+ // Format each scope with context
422
+ const formatted = meaningfulScopes.map(scope => {
423
+ // Handle numbered scopes: "try#0" -> "try block"
424
+ if (scope.match(/^try#\d+$/)) return 'try block';
425
+ if (scope.match(/^catch#\d+$/)) return 'catch block';
426
+ if (scope.match(/^if#\d+$/)) return 'conditional';
427
+ if (scope.match(/^else#\d+$/)) return 'else block';
428
+ if (scope.match(/^for#\d+$/)) return 'loop';
429
+ if (scope.match(/^while#\d+$/)) return 'loop';
430
+ if (scope.match(/^switch#\d+$/)) return 'switch';
431
+
432
+ // Regular scope: function or class name
433
+ return scope;
434
+ });
435
+
436
+ // Build "inside X, inside Y" string
437
+ return 'inside ' + formatted.join(', inside ');
438
+ }
439
+
440
+ /**
441
+ * Check if a node matches the search pattern based on its type.
442
+ *
443
+ * Different node types have different searchable fields:
444
+ * - http:route: search method and path fields
445
+ * - http:request: search method and url fields
446
+ * - socketio:event: search name field (standard)
447
+ * - socketio:emit/on: search event field
448
+ * - Default: search name field
449
+ */
450
+ function matchesSearchPattern(
451
+ node: {
452
+ name?: string;
453
+ method?: string;
454
+ path?: string;
455
+ url?: string;
456
+ event?: string;
457
+ [key: string]: unknown
458
+ },
459
+ nodeType: string,
460
+ pattern: string
461
+ ): boolean {
462
+ const lowerPattern = pattern.toLowerCase();
463
+
464
+ // HTTP routes: search method and path
465
+ if (nodeType === 'http:route') {
466
+ const method = (node.method || '').toLowerCase();
467
+ const path = (node.path || '').toLowerCase();
468
+
469
+ // Pattern could be: "POST", "/api/users", "POST /api", etc.
470
+ const patternParts = pattern.trim().split(/\s+/);
471
+
472
+ if (patternParts.length === 1) {
473
+ // Single term: match method OR path
474
+ const term = patternParts[0].toLowerCase();
475
+ return method === term || path.includes(term);
476
+ } else {
477
+ // Multiple terms: first is method, rest is path pattern
478
+ const methodPattern = patternParts[0].toLowerCase();
479
+ const pathPattern = patternParts.slice(1).join(' ').toLowerCase();
480
+
481
+ // Method must match exactly (GET, POST, etc.)
482
+ const methodMatches = method === methodPattern;
483
+ // Path must contain the pattern
484
+ const pathMatches = path.includes(pathPattern);
485
+
486
+ return methodMatches && pathMatches;
487
+ }
488
+ }
489
+
490
+ // HTTP requests: search method and url
491
+ if (nodeType === 'http:request') {
492
+ const method = (node.method || '').toLowerCase();
493
+ const url = (node.url || '').toLowerCase();
494
+
495
+ // Pattern could be: "POST", "/api/users", "POST /api", etc.
496
+ const patternParts = pattern.trim().split(/\s+/);
497
+
498
+ if (patternParts.length === 1) {
499
+ // Single term: match method OR url
500
+ const term = patternParts[0].toLowerCase();
501
+ return method === term || url.includes(term);
502
+ } else {
503
+ // Multiple terms: first is method, rest is url pattern
504
+ const methodPattern = patternParts[0].toLowerCase();
505
+ const urlPattern = patternParts.slice(1).join(' ').toLowerCase();
506
+
507
+ // Method must match exactly (GET, POST, etc.)
508
+ const methodMatches = method === methodPattern;
509
+ // URL must contain the pattern
510
+ const urlMatches = url.includes(urlPattern);
511
+
512
+ return methodMatches && urlMatches;
513
+ }
514
+ }
515
+
516
+ // Socket.IO event channels: search name field (standard)
517
+ if (nodeType === 'socketio:event') {
518
+ const nodeName = (node.name || '').toLowerCase();
519
+ return nodeName.includes(lowerPattern);
520
+ }
521
+
522
+ // Socket.IO emit/on: search event field
523
+ if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
524
+ const eventName = (node.event || '').toLowerCase();
525
+ return eventName.includes(lowerPattern);
526
+ }
527
+
528
+ // Default: search name field
529
+ const nodeName = (node.name || '').toLowerCase();
530
+ return nodeName.includes(lowerPattern);
531
+ }
532
+
533
+ /**
534
+ * Find nodes by query (type, name, file scope, function scopes)
158
535
  */
159
536
  async function findNodes(
160
537
  backend: RFDBServerBackend,
161
- type: string | null,
162
- name: string,
538
+ query: ParsedQuery,
163
539
  limit: number
164
540
  ): Promise<NodeInfo[]> {
165
541
  const results: NodeInfo[] = [];
166
- const searchTypes = type
167
- ? [type]
168
- : ['FUNCTION', 'CLASS', 'MODULE', 'VARIABLE', 'CONSTANT'];
542
+ const searchTypes = query.type
543
+ ? [query.type]
544
+ : [
545
+ 'FUNCTION',
546
+ 'CLASS',
547
+ 'MODULE',
548
+ 'VARIABLE',
549
+ 'CONSTANT',
550
+ 'http:route',
551
+ 'http:request',
552
+ 'socketio:event',
553
+ 'socketio:emit',
554
+ 'socketio:on'
555
+ ];
169
556
 
170
557
  for (const nodeType of searchTypes) {
171
558
  for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
172
- const nodeName = node.name || '';
173
- // Case-insensitive partial match
174
- if (nodeName.toLowerCase().includes(name.toLowerCase())) {
175
- results.push({
176
- id: node.id,
177
- type: node.type || nodeType,
178
- name: nodeName,
179
- file: node.file || '',
180
- line: node.line,
181
- });
182
- if (results.length >= limit) break;
559
+ // Type-aware field matching (name)
560
+ const nameMatches = matchesSearchPattern(node, nodeType, query.name);
561
+ if (!nameMatches) continue;
562
+
563
+ // Scope matching (file and function scopes)
564
+ const scopeMatches = matchesScope(node.id, query.file, query.scopes);
565
+ if (!scopeMatches) continue;
566
+
567
+ const nodeInfo: NodeInfo = {
568
+ id: node.id,
569
+ type: node.type || nodeType,
570
+ name: node.name || '',
571
+ file: node.file || '',
572
+ line: node.line,
573
+ };
574
+
575
+ // Add scope context for display
576
+ nodeInfo.scopeContext = extractScopeContext(node.id);
577
+
578
+ // Include method and path for http:route nodes
579
+ if (nodeType === 'http:route') {
580
+ nodeInfo.method = node.method as string | undefined;
581
+ nodeInfo.path = node.path as string | undefined;
582
+ }
583
+
584
+ // Include method and url for http:request nodes
585
+ if (nodeType === 'http:request') {
586
+ nodeInfo.method = node.method as string | undefined;
587
+ nodeInfo.url = node.url as string | undefined;
588
+ }
589
+
590
+ // Include event field for Socket.IO nodes
591
+ if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
592
+ nodeInfo.event = node.event as string | undefined;
183
593
  }
594
+
595
+ // Include emit-specific fields
596
+ if (nodeType === 'socketio:emit') {
597
+ nodeInfo.room = node.room as string | undefined;
598
+ nodeInfo.namespace = node.namespace as string | undefined;
599
+ nodeInfo.broadcast = node.broadcast as boolean | undefined;
600
+ nodeInfo.objectName = node.objectName as string | undefined;
601
+ }
602
+
603
+ // Include listener-specific fields
604
+ if (nodeType === 'socketio:on') {
605
+ nodeInfo.objectName = node.objectName as string | undefined;
606
+ nodeInfo.handlerName = node.handlerName as string | undefined;
607
+ }
608
+
609
+ results.push(nodeInfo);
610
+ if (results.length >= limit) break;
184
611
  }
185
612
  if (results.length >= limit) break;
186
613
  }
@@ -213,8 +640,8 @@ async function getCallers(
213
640
  const callNode = await backend.getNode(edge.src);
214
641
  if (!callNode) continue;
215
642
 
216
- // Find the FUNCTION that contains this CALL
217
- const containingFunc = await findContainingFunction(backend, callNode.id);
643
+ // Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
644
+ const containingFunc = await findContainingFunctionCore(backend, callNode.id);
218
645
 
219
646
  if (containingFunc && !seen.has(containingFunc.id)) {
220
647
  seen.add(containingFunc.id);
@@ -227,173 +654,240 @@ async function getCallers(
227
654
  });
228
655
  }
229
656
  }
230
- } catch {
231
- // Ignore errors
657
+ } catch (error) {
658
+ if (process.env.DEBUG) {
659
+ console.error('[query] Error in getCallers:', error);
660
+ }
232
661
  }
233
662
 
234
663
  return callers;
235
664
  }
236
665
 
237
666
  /**
238
- * Find the FUNCTION or CLASS that contains a node
667
+ * Get functions that this node calls
239
668
  *
240
- * Path can be: CALL CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
241
- * So we need to follow both CONTAINS and HAS_SCOPE edges
669
+ * Uses shared utility from @grafema/core which:
670
+ * - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
671
+ * - Finds both CALL and METHOD_CALL nodes
672
+ * - Only returns resolved calls (those with CALLS edges to targets)
242
673
  */
243
- async function findContainingFunction(
674
+ async function getCallees(
244
675
  backend: RFDBServerBackend,
245
676
  nodeId: string,
246
- maxDepth: number = 15
247
- ): Promise<NodeInfo | null> {
248
- const visited = new Set<string>();
249
- const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
677
+ limit: number
678
+ ): Promise<NodeInfo[]> {
679
+ const callees: NodeInfo[] = [];
680
+ const seen = new Set<string>();
250
681
 
251
- while (queue.length > 0) {
252
- const { id, depth } = queue.shift()!;
682
+ try {
683
+ // Use shared utility (now includes METHOD_CALL and correct graph traversal)
684
+ const calls = await findCallsInFunctionCore(backend, nodeId);
253
685
 
254
- if (visited.has(id) || depth > maxDepth) continue;
255
- visited.add(id);
686
+ for (const call of calls) {
687
+ if (callees.length >= limit) break;
256
688
 
257
- try {
258
- // Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
259
- const edges = await backend.getIncomingEdges(id, null);
689
+ // Only include resolved calls with targets
690
+ if (call.resolved && call.target && !seen.has(call.target.id)) {
691
+ seen.add(call.target.id);
692
+ callees.push({
693
+ id: call.target.id,
694
+ type: 'FUNCTION',
695
+ name: call.target.name || '<anonymous>',
696
+ file: call.target.file || '',
697
+ line: call.target.line,
698
+ });
699
+ }
700
+ }
701
+ } catch (error) {
702
+ if (process.env.DEBUG) {
703
+ console.error('[query] Error in getCallees:', error);
704
+ }
705
+ }
260
706
 
261
- for (const edge of edges) {
262
- const edgeType = (edge as any).edgeType || (edge as any).type;
707
+ return callees;
708
+ }
263
709
 
264
- // Only follow structural edges
265
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
710
+ /**
711
+ * Display a node with semantic ID as primary identifier
712
+ */
713
+ async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBServerBackend): Promise<void> {
714
+ // Special formatting for HTTP routes
715
+ if (node.type === 'http:route' && node.method && node.path) {
716
+ console.log(formatHttpRouteDisplay(node, projectPath));
717
+ return;
718
+ }
266
719
 
267
- const parentNode = await backend.getNode(edge.src);
268
- if (!parentNode || visited.has(parentNode.id)) continue;
720
+ // Special formatting for HTTP requests
721
+ if (node.type === 'http:request') {
722
+ console.log(formatHttpRequestDisplay(node, projectPath));
723
+ return;
724
+ }
269
725
 
270
- const parentType = parentNode.type;
726
+ // Special formatting for Socket.IO event channels
727
+ if (node.type === 'socketio:event') {
728
+ console.log(await formatSocketEventDisplay(node, projectPath, backend));
729
+ return;
730
+ }
271
731
 
272
- // FUNCTION, CLASS, or MODULE (for top-level calls)
273
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
274
- return {
275
- id: parentNode.id,
276
- type: parentType,
277
- name: parentNode.name || '<anonymous>',
278
- file: parentNode.file || '',
279
- line: parentNode.line,
280
- };
281
- }
732
+ // Special formatting for Socket.IO emit/on
733
+ if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
734
+ console.log(formatSocketIONodeDisplay(node, projectPath));
735
+ return;
736
+ }
282
737
 
283
- // Continue searching from this parent
284
- queue.push({ id: parentNode.id, depth: depth + 1 });
285
- }
286
- } catch {
287
- // Ignore errors
288
- }
738
+ console.log(formatNodeDisplay(node, { projectPath }));
739
+
740
+ // Add scope context if present
741
+ if (node.scopeContext) {
742
+ console.log(` Scope: ${node.scopeContext}`);
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Format HTTP route for display
748
+ *
749
+ * Output:
750
+ * [http:route] POST /api/users
751
+ * Location: src/routes/users.js:15
752
+ */
753
+ function formatHttpRouteDisplay(node: NodeInfo, projectPath: string): string {
754
+ const lines: string[] = [];
755
+
756
+ // Line 1: [type] METHOD PATH
757
+ lines.push(`[${node.type}] ${node.method} ${node.path}`);
758
+
759
+ // Line 2: Location
760
+ if (node.file) {
761
+ const relPath = relative(projectPath, node.file);
762
+ const loc = node.line ? `${relPath}:${node.line}` : relPath;
763
+ lines.push(` Location: ${loc}`);
289
764
  }
290
765
 
291
- return null;
766
+ return lines.join('\n');
292
767
  }
293
768
 
294
769
  /**
295
- * Get functions that this node calls
770
+ * Format HTTP request for display
296
771
  *
297
- * Logic: FUNCTION → CONTAINS → CALL → CALLS → TARGET
298
- * Find all CALL nodes inside this function, then find what they call
772
+ * Output:
773
+ * [http:request] GET /api/users
774
+ * Location: src/pages/Users.tsx:42
299
775
  */
300
- async function getCallees(
301
- backend: RFDBServerBackend,
302
- nodeId: string,
303
- limit: number
304
- ): Promise<NodeInfo[]> {
305
- const callees: NodeInfo[] = [];
306
- const seen = new Set<string>();
776
+ function formatHttpRequestDisplay(node: NodeInfo, projectPath: string): string {
777
+ const lines: string[] = [];
778
+
779
+ // Line 1: [type] METHOD URL
780
+ const method = node.method || 'GET';
781
+ const url = node.url || 'dynamic';
782
+ lines.push(`[${node.type}] ${method} ${url}`);
783
+
784
+ // Line 2: Location
785
+ if (node.file) {
786
+ const relPath = relative(projectPath, node.file);
787
+ const loc = node.line ? `${relPath}:${node.line}` : relPath;
788
+ lines.push(` Location: ${loc}`);
789
+ }
307
790
 
308
- try {
309
- // Find all CALL nodes inside this function (via CONTAINS)
310
- const callNodes = await findCallsInFunction(backend, nodeId);
791
+ return lines.join('\n');
792
+ }
311
793
 
312
- for (const callNode of callNodes) {
313
- if (callees.length >= limit) break;
794
+ /**
795
+ * Format Socket.IO event channel for display
796
+ *
797
+ * Output:
798
+ * [socketio:event] slot:booked
799
+ * ID: socketio:event#slot:booked
800
+ * Emitted by: 3 locations
801
+ * Listened by: 5 locations
802
+ */
803
+ async function formatSocketEventDisplay(
804
+ node: NodeInfo,
805
+ projectPath: string,
806
+ backend: RFDBServerBackend
807
+ ): Promise<string> {
808
+ const lines: string[] = [];
809
+
810
+ // Line 1: [type] event_name
811
+ lines.push(`[${node.type}] ${node.name}`);
314
812
 
315
- // Find what this CALL calls
316
- const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
813
+ // Line 2: ID
814
+ lines.push(` ID: ${node.id}`);
317
815
 
318
- for (const edge of callEdges) {
319
- if (callees.length >= limit) break;
816
+ // Query edges to get emitter and listener counts
817
+ try {
818
+ const incomingEdges = await backend.getIncomingEdges(node.id, ['EMITS_EVENT']);
819
+ const outgoingEdges = await backend.getOutgoingEdges(node.id, ['LISTENED_BY']);
320
820
 
321
- const targetNode = await backend.getNode(edge.dst);
322
- if (!targetNode || seen.has(targetNode.id)) continue;
821
+ if (incomingEdges.length > 0) {
822
+ lines.push(` Emitted by: ${incomingEdges.length} location${incomingEdges.length !== 1 ? 's' : ''}`);
823
+ }
323
824
 
324
- seen.add(targetNode.id);
325
- callees.push({
326
- id: targetNode.id,
327
- type: targetNode.type || 'UNKNOWN',
328
- name: targetNode.name || '<anonymous>',
329
- file: targetNode.file || '',
330
- line: targetNode.line,
331
- });
332
- }
825
+ if (outgoingEdges.length > 0) {
826
+ lines.push(` Listened by: ${outgoingEdges.length} location${outgoingEdges.length !== 1 ? 's' : ''}`);
827
+ }
828
+ } catch (error) {
829
+ if (process.env.DEBUG) {
830
+ console.error('[query] Error in formatSocketEventDisplay:', error);
333
831
  }
334
- } catch {
335
- // Ignore errors
336
832
  }
337
833
 
338
- return callees;
834
+ return lines.join('\n');
339
835
  }
340
836
 
341
837
  /**
342
- * Find all CALL nodes inside a function (recursively via CONTAINS)
838
+ * Format Socket.IO emit/on for display
839
+ *
840
+ * Output for emit:
841
+ * [socketio:emit] slot:booked
842
+ * ID: socketio:emit#slot:booked#server.js#28
843
+ * Location: server.js:28
844
+ * Room: gig:123 (if applicable)
845
+ * Namespace: /admin (if applicable)
846
+ * Broadcast: true (if applicable)
847
+ *
848
+ * Output for on:
849
+ * [socketio:on] slot:booked
850
+ * ID: socketio:on#slot:booked#client.js#13
851
+ * Location: client.js:13
852
+ * Handler: anonymous:27
343
853
  */
344
- async function findCallsInFunction(
345
- backend: RFDBServerBackend,
346
- nodeId: string,
347
- maxDepth: number = 10
348
- ): Promise<NodeInfo[]> {
349
- const calls: NodeInfo[] = [];
350
- const visited = new Set<string>();
351
- const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
854
+ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string {
855
+ const lines: string[] = [];
352
856
 
353
- while (queue.length > 0) {
354
- const { id, depth } = queue.shift()!;
857
+ // Line 1: [type] event_name
858
+ const eventName = node.event || node.name || 'unknown';
859
+ lines.push(`[${node.type}] ${eventName}`);
355
860
 
356
- if (visited.has(id) || depth > maxDepth) continue;
357
- visited.add(id);
861
+ // Line 2: ID
862
+ lines.push(` ID: ${node.id}`);
358
863
 
359
- try {
360
- // Get children via CONTAINS
361
- const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
362
-
363
- for (const edge of edges) {
364
- const child = await backend.getNode(edge.dst);
365
- if (!child) continue;
366
-
367
- const childType = child.type;
368
-
369
- if (childType === 'CALL') {
370
- calls.push({
371
- id: child.id,
372
- type: 'CALL',
373
- name: child.name || '',
374
- file: child.file || '',
375
- line: child.line,
376
- });
377
- }
864
+ // Line 3: Location (if applicable)
865
+ if (node.file) {
866
+ const loc = formatLocation(node.file, node.line, projectPath);
867
+ if (loc) {
868
+ lines.push(` Location: ${loc}`);
869
+ }
870
+ }
378
871
 
379
- // Continue searching in children (but not into nested functions)
380
- if (childType !== 'FUNCTION' && childType !== 'CLASS') {
381
- queue.push({ id: child.id, depth: depth + 1 });
382
- }
383
- }
384
- } catch {
385
- // Ignore
872
+ // Emit-specific fields
873
+ if (node.type === 'socketio:emit') {
874
+ if (node.room) {
875
+ lines.push(` Room: ${node.room}`);
876
+ }
877
+ if (node.namespace) {
878
+ lines.push(` Namespace: ${node.namespace}`);
879
+ }
880
+ if (node.broadcast) {
881
+ lines.push(` Broadcast: true`);
386
882
  }
387
883
  }
388
884
 
389
- return calls;
390
- }
885
+ // Listener-specific fields
886
+ if (node.type === 'socketio:on' && node.handlerName) {
887
+ lines.push(` Handler: ${node.handlerName}`);
888
+ }
391
889
 
392
- /**
393
- * Display a node with semantic ID as primary identifier
394
- */
395
- function displayNode(node: NodeInfo, projectPath: string): void {
396
- console.log(formatNodeDisplay(node, { projectPath }));
890
+ return lines.join('\n');
397
891
  }
398
892
 
399
893
  /**