@grafema/cli 0.2.5-beta → 0.2.7

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 (105) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.js +6 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyze.d.ts +3 -10
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +5 -347
  7. package/dist/commands/analyze.js.map +1 -1
  8. package/dist/commands/analyzeAction.d.ts +28 -0
  9. package/dist/commands/analyzeAction.d.ts.map +1 -0
  10. package/dist/commands/analyzeAction.js +243 -0
  11. package/dist/commands/analyzeAction.js.map +1 -0
  12. package/dist/commands/check.js +2 -2
  13. package/dist/commands/check.js.map +1 -1
  14. package/dist/commands/context.d.ts +16 -0
  15. package/dist/commands/context.d.ts.map +1 -0
  16. package/dist/commands/context.js +238 -0
  17. package/dist/commands/context.js.map +1 -0
  18. package/dist/commands/doctor/checks.js +1 -1
  19. package/dist/commands/doctor/checks.js.map +1 -1
  20. package/dist/commands/explain.d.ts.map +1 -1
  21. package/dist/commands/explain.js +4 -3
  22. package/dist/commands/explain.js.map +1 -1
  23. package/dist/commands/file.d.ts +15 -0
  24. package/dist/commands/file.d.ts.map +1 -0
  25. package/dist/commands/file.js +144 -0
  26. package/dist/commands/file.js.map +1 -0
  27. package/dist/commands/impact.d.ts.map +1 -1
  28. package/dist/commands/impact.js +2 -3
  29. package/dist/commands/impact.js.map +1 -1
  30. package/dist/commands/init.d.ts.map +1 -1
  31. package/dist/commands/init.js +13 -1
  32. package/dist/commands/init.js.map +1 -1
  33. package/dist/commands/ls.d.ts.map +1 -1
  34. package/dist/commands/ls.js +3 -2
  35. package/dist/commands/ls.js.map +1 -1
  36. package/dist/commands/query.d.ts +8 -0
  37. package/dist/commands/query.d.ts.map +1 -1
  38. package/dist/commands/query.js +158 -51
  39. package/dist/commands/query.js.map +1 -1
  40. package/dist/commands/schema.d.ts.map +1 -1
  41. package/dist/commands/schema.js +3 -2
  42. package/dist/commands/schema.js.map +1 -1
  43. package/dist/commands/server.d.ts.map +1 -1
  44. package/dist/commands/server.js +8 -59
  45. package/dist/commands/server.js.map +1 -1
  46. package/dist/commands/setup-skill.d.ts +17 -0
  47. package/dist/commands/setup-skill.d.ts.map +1 -0
  48. package/dist/commands/setup-skill.js +131 -0
  49. package/dist/commands/setup-skill.js.map +1 -0
  50. package/dist/commands/trace.d.ts.map +1 -1
  51. package/dist/commands/trace.js +20 -10
  52. package/dist/commands/trace.js.map +1 -1
  53. package/dist/plugins/builtinPlugins.d.ts +10 -0
  54. package/dist/plugins/builtinPlugins.d.ts.map +1 -0
  55. package/dist/plugins/builtinPlugins.js +68 -0
  56. package/dist/plugins/builtinPlugins.js.map +1 -0
  57. package/dist/plugins/pluginLoader.d.ts +16 -0
  58. package/dist/plugins/pluginLoader.d.ts.map +1 -0
  59. package/dist/plugins/pluginLoader.js +101 -0
  60. package/dist/plugins/pluginLoader.js.map +1 -0
  61. package/dist/plugins/pluginResolver.js +38 -0
  62. package/dist/utils/codePreview.d.ts +1 -0
  63. package/dist/utils/codePreview.d.ts.map +1 -1
  64. package/dist/utils/codePreview.js +5 -3
  65. package/dist/utils/codePreview.js.map +1 -1
  66. package/dist/utils/formatNode.d.ts +1 -1
  67. package/dist/utils/formatNode.d.ts.map +1 -1
  68. package/dist/utils/formatNode.js +2 -2
  69. package/dist/utils/formatNode.js.map +1 -1
  70. package/dist/utils/pathUtils.d.ts +2 -0
  71. package/dist/utils/pathUtils.d.ts.map +1 -0
  72. package/dist/utils/pathUtils.js +9 -0
  73. package/dist/utils/pathUtils.js.map +1 -0
  74. package/dist/utils/progressRenderer.d.ts +4 -0
  75. package/dist/utils/progressRenderer.d.ts.map +1 -1
  76. package/dist/utils/progressRenderer.js +23 -4
  77. package/dist/utils/progressRenderer.js.map +1 -1
  78. package/package.json +7 -9
  79. package/skills/grafema-codebase-analysis/SKILL.md +295 -0
  80. package/skills/grafema-codebase-analysis/references/node-edge-types.md +123 -0
  81. package/skills/grafema-codebase-analysis/references/query-patterns.md +205 -0
  82. package/src/cli.ts +8 -2
  83. package/src/commands/analyze.ts +5 -435
  84. package/src/commands/analyzeAction.ts +284 -0
  85. package/src/commands/check.ts +2 -2
  86. package/src/commands/context.ts +309 -0
  87. package/src/commands/doctor/checks.ts +1 -1
  88. package/src/commands/explain.ts +4 -3
  89. package/src/commands/explore.tsx +7 -5
  90. package/src/commands/file.ts +179 -0
  91. package/src/commands/impact.ts +2 -3
  92. package/src/commands/init.ts +13 -1
  93. package/src/commands/ls.ts +3 -2
  94. package/src/commands/query.ts +167 -52
  95. package/src/commands/schema.ts +3 -2
  96. package/src/commands/server.ts +8 -64
  97. package/src/commands/setup-skill.ts +162 -0
  98. package/src/commands/trace.ts +18 -9
  99. package/src/plugins/builtinPlugins.ts +108 -0
  100. package/src/plugins/pluginLoader.ts +123 -0
  101. package/src/plugins/pluginResolver.js +38 -0
  102. package/src/utils/codePreview.ts +7 -3
  103. package/src/utils/formatNode.ts +3 -3
  104. package/src/utils/pathUtils.ts +9 -0
  105. package/src/utils/progressRenderer.ts +25 -4
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Analyze command action — connects to RFDB, loads plugins, runs Orchestrator.
3
+ *
4
+ * Extracted from analyze.ts (REG-435) to keep command definition separate
5
+ * from execution logic.
6
+ */
7
+
8
+ import { resolve, join } from 'path';
9
+ import { existsSync, mkdirSync } from 'fs';
10
+ import {
11
+ Orchestrator,
12
+ RFDBServerBackend,
13
+ DiagnosticReporter,
14
+ DiagnosticWriter,
15
+ createLogger,
16
+ loadConfig,
17
+ StrictModeFailure,
18
+ } from '@grafema/core';
19
+ import type { LogLevel, GraphBackend } from '@grafema/types';
20
+ import { ProgressRenderer } from '../utils/progressRenderer.js';
21
+ import { loadCustomPlugins, createPlugins } from '../plugins/pluginLoader.js';
22
+
23
+ export interface NodeEdgeCountBackend {
24
+ nodeCount: () => Promise<number>;
25
+ edgeCount: () => Promise<number>;
26
+ }
27
+
28
+ export async function fetchNodeEdgeCounts(backend: NodeEdgeCountBackend): Promise<{ nodeCount: number; edgeCount: number }> {
29
+ const [nodeCount, edgeCount] = await Promise.all([backend.nodeCount(), backend.edgeCount()]);
30
+ return { nodeCount, edgeCount };
31
+ }
32
+
33
+ export function exitWithCode(code: number, exitFn: (code: number) => void = process.exit): void {
34
+ exitFn(code);
35
+ }
36
+
37
+ /**
38
+ * Determine log level from CLI options.
39
+ * Priority: --log-level > --quiet > --verbose > default ('silent')
40
+ *
41
+ * By default, logs are silent to allow clean progress UI.
42
+ * Use --verbose to see detailed logs (disables interactive progress).
43
+ */
44
+ function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: string }): LogLevel {
45
+ if (options.logLevel) {
46
+ const validLevels: LogLevel[] = ['silent', 'errors', 'warnings', 'info', 'debug'];
47
+ if (validLevels.includes(options.logLevel as LogLevel)) {
48
+ return options.logLevel as LogLevel;
49
+ }
50
+ }
51
+ if (options.quiet) return 'silent';
52
+ if (options.verbose) return 'info'; // --verbose shows logs instead of progress UI
53
+ return 'silent'; // Default: silent logs, clean progress UI
54
+ }
55
+
56
+ export async function analyzeAction(path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; logFile?: string; strict?: boolean; autoStart?: boolean }): Promise<void> {
57
+ const projectPath = resolve(path);
58
+ const grafemaDir = join(projectPath, '.grafema');
59
+ const dbPath = join(grafemaDir, 'graph.rfdb');
60
+
61
+ if (!existsSync(grafemaDir)) {
62
+ mkdirSync(grafemaDir, { recursive: true });
63
+ }
64
+
65
+ // Two log levels for CLI output:
66
+ // - info: important results (shows unless --quiet)
67
+ // - debug: verbose details (shows only with --verbose)
68
+ const info = options.quiet ? () => {} : console.log;
69
+ const debug = options.verbose ? console.log : () => {};
70
+
71
+ // Create logger based on CLI flags
72
+ const logLevel = getLogLevel(options);
73
+ const logFile = options.logFile ? resolve(options.logFile) : undefined;
74
+ const logger = createLogger(logLevel, logFile ? { logFile } : undefined);
75
+
76
+ if (logFile) {
77
+ debug(`Log file: ${logFile}`);
78
+ }
79
+ debug(`Analyzing project: ${projectPath}`);
80
+
81
+ // Connect to RFDB server
82
+ // Default: require explicit `grafema server start`
83
+ // Use --auto-start for CI or backwards compatibility
84
+ // In normal mode (not verbose), suppress backend logs for clean progress UI
85
+ const backend = new RFDBServerBackend({
86
+ dbPath,
87
+ autoStart: options.autoStart ?? false,
88
+ silent: !options.verbose // Silent in normal mode (show progress), verbose shows logs
89
+ });
90
+
91
+ try {
92
+ await backend.connect();
93
+ } catch (err) {
94
+ if (!options.autoStart && err instanceof Error && err.message.includes('not running')) {
95
+ console.error('');
96
+ console.error('RFDB server is not running.');
97
+ console.error('');
98
+ console.error('Start the server first:');
99
+ console.error(' grafema server start');
100
+ console.error('');
101
+ console.error('Or use --auto-start flag:');
102
+ console.error(' grafema analyze --auto-start');
103
+ console.error('');
104
+ process.exit(1);
105
+ }
106
+ throw err;
107
+ }
108
+
109
+ if (options.clear) {
110
+ debug('Clearing existing database...');
111
+ await backend.clear();
112
+ }
113
+
114
+ const config = loadConfig(projectPath, logger);
115
+
116
+ // Extract services from config (REG-174)
117
+ if (config.services.length > 0) {
118
+ debug(`Loaded ${config.services.length} service(s) from config`);
119
+ for (const svc of config.services) {
120
+ const entry = svc.entryPoint ? ` (entry: ${svc.entryPoint})` : '';
121
+ debug(` - ${svc.name}: ${svc.path}${entry}`);
122
+ }
123
+ }
124
+
125
+ // Load custom plugins from .grafema/plugins/
126
+ const customPlugins = await loadCustomPlugins(projectPath, debug);
127
+ const plugins = createPlugins(config.plugins, customPlugins, options.verbose);
128
+
129
+ debug(`Loaded ${plugins.length} plugins`);
130
+
131
+ // Resolve strict mode: CLI flag overrides config
132
+ const strictMode = options.strict ?? config.strict ?? false;
133
+ if (strictMode) {
134
+ debug('Strict mode enabled - analysis will fail on unresolved references');
135
+ }
136
+
137
+ const startTime = Date.now();
138
+
139
+ // Create progress renderer for CLI output
140
+ // In quiet mode, use a no-op renderer (skip rendering)
141
+ // In verbose mode, use non-interactive (newlines per update)
142
+ // In normal mode, use interactive (spinner with line overwrite)
143
+ const renderer = options.quiet
144
+ ? null
145
+ : new ProgressRenderer({
146
+ isInteractive: !options.verbose && process.stdout.isTTY,
147
+ });
148
+
149
+ // Poll graph stats periodically to show node/edge counts in progress
150
+ let statsInterval: NodeJS.Timeout | null = null;
151
+ if (renderer && !options.quiet) {
152
+ statsInterval = setInterval(async () => {
153
+ try {
154
+ const stats = await fetchNodeEdgeCounts(backend);
155
+ renderer.setStats(stats.nodeCount, stats.edgeCount);
156
+ } catch {
157
+ // Ignore stats errors during analysis
158
+ }
159
+ }, 500); // Poll every 500ms
160
+ statsInterval.unref?.();
161
+ }
162
+
163
+ const orchestrator = new Orchestrator({
164
+ graph: backend as unknown as GraphBackend,
165
+ plugins,
166
+ serviceFilter: options.service || null,
167
+ entrypoint: options.entrypoint,
168
+ forceAnalysis: options.clear || false,
169
+ logger,
170
+ services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
171
+ strictMode, // REG-330: Pass strict mode flag
172
+ onProgress: (progress) => {
173
+ renderer?.update(progress);
174
+ },
175
+ });
176
+
177
+ let exitCode = 0;
178
+
179
+ try {
180
+ await orchestrator.run(projectPath);
181
+ await backend.flush();
182
+
183
+ const elapsedSeconds = (Date.now() - startTime) / 1000;
184
+ const stats = await fetchNodeEdgeCounts(backend);
185
+
186
+ // Clear progress line in interactive mode, then show results
187
+ if (renderer && process.stdout.isTTY) {
188
+ process.stdout.write('\r\x1b[K'); // Clear line
189
+ }
190
+ info('');
191
+ info(renderer ? renderer.finish(elapsedSeconds) : `Analysis complete in ${elapsedSeconds.toFixed(2)}s`);
192
+ info(` Nodes: ${stats.nodeCount}`);
193
+ info(` Edges: ${stats.edgeCount}`);
194
+
195
+ // Get diagnostics and report summary
196
+ const diagnostics = orchestrator.getDiagnostics();
197
+ const reporter = new DiagnosticReporter(diagnostics);
198
+
199
+ // Print summary if there are any issues
200
+ if (diagnostics.count() > 0) {
201
+ info('');
202
+ info(reporter.categorizedSummary());
203
+
204
+ // In verbose mode, print full report
205
+ if (options.verbose) {
206
+ debug('');
207
+ debug(reporter.report({ format: 'text', includeSummary: false }));
208
+ }
209
+ }
210
+
211
+ // Always write diagnostics.log (required for `grafema check` command)
212
+ const writer = new DiagnosticWriter();
213
+ await writer.write(diagnostics, grafemaDir);
214
+ if (options.debug) {
215
+ debug(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
216
+ }
217
+
218
+ // Determine exit code based on severity
219
+ if (diagnostics.hasFatal()) {
220
+ exitCode = 1;
221
+ } else if (diagnostics.hasErrors()) {
222
+ exitCode = 2; // Completed with errors
223
+ } else {
224
+ exitCode = 0; // Success (maybe warnings)
225
+ }
226
+ } catch (e) {
227
+ const diagnostics = orchestrator.getDiagnostics();
228
+ const reporter = new DiagnosticReporter(diagnostics);
229
+
230
+ // Clear progress line in interactive mode
231
+ if (renderer && process.stdout.isTTY) {
232
+ process.stdout.write('\r\x1b[K');
233
+ }
234
+
235
+ // Check if this is a strict mode failure (REG-332: structured output)
236
+ if (e instanceof StrictModeFailure) {
237
+ // Format ONLY from diagnostics, not from error.message
238
+ console.error('');
239
+ console.error(`✗ Strict mode: ${e.count} unresolved reference(s) found during ENRICHMENT.`);
240
+ console.error('');
241
+ console.error(reporter.formatStrict(e.diagnostics, {
242
+ verbose: options.verbose,
243
+ suppressedCount: e.suppressedCount, // REG-332
244
+ }));
245
+ console.error('');
246
+ console.error('Run without --strict for graceful degradation, or fix the underlying issues.');
247
+ } else {
248
+ // Generic error handling (non-strict)
249
+ const error = e instanceof Error ? e : new Error(String(e));
250
+ console.error('');
251
+ console.error(`✗ Analysis failed: ${error.message}`);
252
+ console.error('');
253
+ console.error('→ Run with --debug for detailed diagnostics');
254
+
255
+ if (diagnostics.count() > 0) {
256
+ console.error('');
257
+ console.error(reporter.report({ format: 'text', includeSummary: true }));
258
+ }
259
+ }
260
+
261
+ // Write diagnostics.log in debug mode even on failure
262
+ if (options.debug) {
263
+ const writer = new DiagnosticWriter();
264
+ await writer.write(diagnostics, grafemaDir);
265
+ console.error(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
266
+ }
267
+
268
+ exitCode = 1;
269
+ } finally {
270
+ // Stop stats polling
271
+ if (statsInterval) {
272
+ clearInterval(statsInterval);
273
+ statsInterval = null;
274
+ }
275
+
276
+ if (backend.connected) {
277
+ await backend.close();
278
+ }
279
+
280
+ // Exit with appropriate code
281
+ // 0 = success, 1 = fatal, 2 = errors
282
+ exitWithCode(exitCode);
283
+ }
284
+ }
@@ -137,7 +137,7 @@ Examples:
137
137
 
138
138
  // Check graph freshness
139
139
  const freshnessChecker = new GraphFreshnessChecker();
140
- const freshness = await freshnessChecker.checkFreshness(backend);
140
+ const freshness = await freshnessChecker.checkFreshness(backend, projectPath);
141
141
 
142
142
  if (!freshness.isFresh) {
143
143
  if (options.failOnStale) {
@@ -292,7 +292,7 @@ async function runBuiltInValidator(
292
292
 
293
293
  // Check graph freshness
294
294
  const freshnessChecker = new GraphFreshnessChecker();
295
- const freshness = await freshnessChecker.checkFreshness(backend);
295
+ const freshness = await freshnessChecker.checkFreshness(backend, resolvedPath);
296
296
 
297
297
  if (!freshness.isFresh) {
298
298
  if (options.failOnStale) {
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Context command — Show deep context for a graph node
3
+ *
4
+ * Displays the full graph neighborhood: source code + all incoming/outgoing edges
5
+ * with code context at each connected node's location.
6
+ *
7
+ * Works for ANY node type: FUNCTION, VARIABLE, MODULE, http:route, CALL, etc.
8
+ *
9
+ * Output is grep-friendly with stable prefixes:
10
+ * -> outgoing edges
11
+ * <- incoming edges
12
+ * > highlighted source lines
13
+ */
14
+
15
+ import { Command } from 'commander';
16
+ import { resolve, join } from 'path';
17
+ import { existsSync } from 'fs';
18
+ import {
19
+ RFDBServerBackend,
20
+ buildNodeContext,
21
+ getNodeDisplayName,
22
+ formatEdgeMetadata,
23
+ STRUCTURAL_EDGE_TYPES,
24
+ } from '@grafema/core';
25
+ import type { NodeContext, EdgeGroup } from '@grafema/core';
26
+ import type { BaseNodeRecord } from '@grafema/types';
27
+ import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
28
+ import { formatLocation } from '../utils/formatNode.js';
29
+ import { exitWithError } from '../utils/errorFormatter.js';
30
+ import { Spinner } from '../utils/spinner.js';
31
+
32
+ interface ContextOptions {
33
+ project: string;
34
+ json?: boolean;
35
+ lines: string;
36
+ edgeType?: string;
37
+ }
38
+
39
+ /** Extended context with CLASS member expansion (REG-411) */
40
+ interface ContextWithMembers extends NodeContext {
41
+ memberContexts?: NodeContext[];
42
+ }
43
+
44
+ export const contextCommand = new Command('context')
45
+ .description('Show deep context for a graph node: source code + graph neighborhood')
46
+ .argument('<semanticId>', 'Semantic ID of the node (exact match)')
47
+ .option('-p, --project <path>', 'Project path', '.')
48
+ .option('-j, --json', 'Output as JSON (full dump, no filtering)')
49
+ .option('-l, --lines <n>', 'Context lines around each code reference', '3')
50
+ .option(
51
+ '-e, --edge-type <type>',
52
+ `Filter edges by type (e.g., CALLS, ASSIGNED_FROM, DEPENDS_ON)
53
+
54
+ Multiple types can be comma-separated: --edge-type CALLS,ASSIGNED_FROM
55
+
56
+ Examples:
57
+ grafema context <id> --edge-type CALLS
58
+ grafema context <id> -e DEPENDS_ON,IMPORTS_FROM`
59
+ )
60
+ .addHelpText('after', `
61
+ Output format (grep-friendly):
62
+ -> outgoing edge (this node points to)
63
+ <- incoming edge (points to this node)
64
+ > highlighted source line
65
+
66
+ Examples:
67
+ grafema context "src/app.js->global->FUNCTION->main"
68
+ grafema context "http:route#POST#/api/users" --edge-type ROUTES_TO,HANDLED_BY
69
+ grafema context <id> --json
70
+ grafema context <id> | grep "CALLS"
71
+ grafema context <id> | grep "<-"
72
+ `)
73
+ .action(async (semanticId: string, options: ContextOptions) => {
74
+ const projectPath = resolve(options.project);
75
+ const grafemaDir = join(projectPath, '.grafema');
76
+ const dbPath = join(grafemaDir, 'graph.rfdb');
77
+
78
+ if (!existsSync(dbPath)) {
79
+ exitWithError('No graph database found', ['Run: grafema analyze']);
80
+ }
81
+
82
+ const backend = new RFDBServerBackend({ dbPath });
83
+ await backend.connect();
84
+
85
+ const spinner = new Spinner('Loading context...');
86
+ spinner.start();
87
+
88
+ try {
89
+ const contextLines = parseInt(options.lines, 10);
90
+ const edgeTypeFilter = options.edgeType
91
+ ? new Set(options.edgeType.split(',').map(t => t.trim().toUpperCase()))
92
+ : null;
93
+
94
+ // 1. Look up node by exact semantic ID
95
+ const node = await backend.getNode(semanticId);
96
+ if (!node) {
97
+ spinner.stop();
98
+ exitWithError(`Node not found: "${semanticId}"`, [
99
+ 'Use: grafema query "<name>" to find the correct semantic ID',
100
+ ]);
101
+ }
102
+
103
+ // 2. Build context (with CLASS member expansion)
104
+ const ctx = await buildContextWithMembers(backend, node, { contextLines, edgeTypeFilter });
105
+
106
+ spinner.stop();
107
+
108
+ // 3. Output
109
+ if (options.json) {
110
+ console.log(JSON.stringify(ctx, null, 2));
111
+ } else {
112
+ printContext(ctx, projectPath, contextLines);
113
+ }
114
+ } finally {
115
+ spinner.stop();
116
+ await backend.close();
117
+ }
118
+ });
119
+
120
+ /**
121
+ * Build context with CLASS member expansion (REG-411)
122
+ */
123
+ async function buildContextWithMembers(
124
+ backend: RFDBServerBackend,
125
+ node: BaseNodeRecord,
126
+ options: { contextLines: number; edgeTypeFilter: Set<string> | null },
127
+ ): Promise<ContextWithMembers> {
128
+ const ctx = await buildNodeContext(backend, node, options);
129
+
130
+ let memberContexts: NodeContext[] | undefined;
131
+ if (node.type === 'CLASS') {
132
+ const outEdges = await backend.getOutgoingEdges(node.id);
133
+ const containsEdges = outEdges.filter(e => e.type === 'CONTAINS');
134
+ const members: NodeContext[] = [];
135
+ for (const edge of containsEdges) {
136
+ const memberNode = await backend.getNode(edge.dst);
137
+ if (memberNode && (memberNode.type === 'FUNCTION' || memberNode.type === 'METHOD')) {
138
+ const memberCtx = await buildNodeContext(backend, memberNode, options);
139
+ members.push(memberCtx);
140
+ }
141
+ }
142
+ members.sort((a, b) => {
143
+ const lineA = (a.node.line as number | undefined) ?? 0;
144
+ const lineB = (b.node.line as number | undefined) ?? 0;
145
+ return lineA - lineB;
146
+ });
147
+ if (members.length > 0) {
148
+ memberContexts = members;
149
+ }
150
+ }
151
+
152
+ return { ...ctx, memberContexts };
153
+ }
154
+
155
+ /**
156
+ * Print context to stdout in grep-friendly format
157
+ */
158
+ function printContext(ctx: ContextWithMembers, projectPath: string, contextLines: number): void {
159
+ const { node, source, outgoing, incoming } = ctx;
160
+
161
+ // Node header
162
+ const displayName = getNodeDisplayName(node);
163
+ console.log(`[${node.type}] ${displayName}`);
164
+ console.log(` ID: ${node.id}`);
165
+
166
+ const loc = formatLocation(node.file, node.line as number | undefined, projectPath);
167
+ if (loc) {
168
+ console.log(` Location: ${loc}`);
169
+ }
170
+
171
+ // Source code
172
+ if (source) {
173
+ console.log('');
174
+ console.log(` Source (lines ${source.startLine}-${source.endLine}):`);
175
+ const formatted = formatCodePreview(
176
+ { lines: source.lines, startLine: source.startLine, endLine: source.endLine },
177
+ node.line as number | undefined,
178
+ );
179
+ for (const line of formatted) {
180
+ console.log(` ${line}`);
181
+ }
182
+ }
183
+
184
+ // Outgoing edges
185
+ if (outgoing.length > 0) {
186
+ console.log('');
187
+ console.log(' Outgoing edges:');
188
+ for (const group of outgoing) {
189
+ printEdgeGroup(group, '->', projectPath, contextLines);
190
+ }
191
+ }
192
+
193
+ // Incoming edges
194
+ if (incoming.length > 0) {
195
+ console.log('');
196
+ console.log(' Incoming edges:');
197
+ for (const group of incoming) {
198
+ printEdgeGroup(group, '<-', projectPath, contextLines);
199
+ }
200
+ }
201
+
202
+ // Member methods (CLASS nodes)
203
+ if (ctx.memberContexts && ctx.memberContexts.length > 0) {
204
+ console.log('');
205
+ console.log(` Methods (${ctx.memberContexts.length}):`);
206
+ for (const memberCtx of ctx.memberContexts) {
207
+ console.log('');
208
+ console.log(` ── [${memberCtx.node.type}] ${getNodeDisplayName(memberCtx.node)}`);
209
+ const memberLoc = formatLocation(
210
+ memberCtx.node.file,
211
+ memberCtx.node.line as number | undefined,
212
+ projectPath,
213
+ );
214
+ if (memberLoc) {
215
+ console.log(` Location: ${memberLoc}`);
216
+ }
217
+
218
+ // Method source code
219
+ if (memberCtx.source) {
220
+ console.log('');
221
+ console.log(` Source (lines ${memberCtx.source.startLine}-${memberCtx.source.endLine}):`);
222
+ const formatted = formatCodePreview(
223
+ {
224
+ lines: memberCtx.source.lines,
225
+ startLine: memberCtx.source.startLine,
226
+ endLine: memberCtx.source.endLine,
227
+ },
228
+ memberCtx.node.line as number | undefined,
229
+ );
230
+ for (const line of formatted) {
231
+ console.log(` ${line}`);
232
+ }
233
+ }
234
+
235
+ // Method edges (non-structural only, to avoid clutter)
236
+ const methodOutgoing = memberCtx.outgoing.filter(g => !STRUCTURAL_EDGE_TYPES.has(g.edgeType));
237
+ const methodIncoming = memberCtx.incoming.filter(g => !STRUCTURAL_EDGE_TYPES.has(g.edgeType));
238
+
239
+ if (methodOutgoing.length > 0) {
240
+ console.log('');
241
+ console.log(' Outgoing edges:');
242
+ for (const group of methodOutgoing) {
243
+ printEdgeGroup(group, '->', projectPath, contextLines, ' ');
244
+ }
245
+ }
246
+ if (methodIncoming.length > 0) {
247
+ console.log('');
248
+ console.log(' Incoming edges:');
249
+ for (const group of methodIncoming) {
250
+ printEdgeGroup(group, '<-', projectPath, contextLines, ' ');
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ // Summary if no edges and no members
257
+ if (outgoing.length === 0 && incoming.length === 0 && !ctx.memberContexts?.length) {
258
+ console.log('');
259
+ console.log(' No edges found.');
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Print a group of edges with the same type
265
+ */
266
+ function printEdgeGroup(
267
+ group: EdgeGroup,
268
+ direction: '->' | '<-',
269
+ projectPath: string,
270
+ contextLines: number,
271
+ indent = ' ',
272
+ ): void {
273
+ const isStructural = STRUCTURAL_EDGE_TYPES.has(group.edgeType);
274
+
275
+ console.log(`${indent}${group.edgeType} (${group.edges.length}):`);
276
+
277
+ for (const { edge, node } of group.edges) {
278
+ if (!node) {
279
+ const danglingId = direction === '->' ? edge.dst : edge.src;
280
+ console.log(`${indent} ${direction} [dangling] ${danglingId}`);
281
+ continue;
282
+ }
283
+
284
+ const displayName = getNodeDisplayName(node);
285
+ const loc = formatLocation(node.file, node.line as number | undefined, projectPath);
286
+ const locStr = loc ? ` (${loc})` : '';
287
+
288
+ // Edge metadata inline (if present and useful)
289
+ const metaStr = formatEdgeMetadata(edge);
290
+
291
+ console.log(`${indent} ${direction} [${node.type}] ${displayName}${locStr}${metaStr}`);
292
+
293
+ // Code context for non-structural edges
294
+ if (!isStructural && node.file && node.line && contextLines > 0) {
295
+ const preview = getCodePreview({
296
+ file: node.file,
297
+ line: node.line as number,
298
+ contextBefore: Math.min(contextLines, 2),
299
+ contextAfter: Math.min(contextLines, 2),
300
+ });
301
+ if (preview) {
302
+ const formatted = formatCodePreview(preview, node.line as number);
303
+ for (const line of formatted) {
304
+ console.log(`${indent} ${line}`);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ }
@@ -527,7 +527,7 @@ export async function checkFreshness(
527
527
  try {
528
528
  await backend.connect();
529
529
  const freshnessChecker = new GraphFreshnessChecker();
530
- const result = await freshnessChecker.checkFreshness(backend);
530
+ const result = await freshnessChecker.checkFreshness(backend, projectPath);
531
531
  await backend.close();
532
532
 
533
533
  if (result.isFresh) {
@@ -15,6 +15,7 @@
15
15
  import { Command } from 'commander';
16
16
  import { resolve, join, relative, normalize } from 'path';
17
17
  import { existsSync, realpathSync } from 'fs';
18
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
18
19
  import { RFDBServerBackend, FileExplainer, type EnhancedNode } from '@grafema/core';
19
20
  import { exitWithError } from '../utils/errorFormatter.js';
20
21
 
@@ -85,8 +86,8 @@ If a file shows NOT_ANALYZED:
85
86
 
86
87
  try {
87
88
  const explainer = new FileExplainer(backend);
88
- // Query with absolute path since graph stores absolute paths
89
- const result = await explainer.explain(absoluteFilePath);
89
+ // Query with relative path since MODULE nodes store relative file paths
90
+ const result = await explainer.explain(relativeFilePath);
90
91
 
91
92
  // Override file in result for display purposes (show relative path)
92
93
  result.file = relativeFilePath;
@@ -166,7 +167,7 @@ function displayNode(node: EnhancedNode, type: string, projectPath: string): voi
166
167
 
167
168
  // Line 3: Location
168
169
  if (node.file) {
169
- const relPath = relative(projectPath, node.file);
170
+ const relPath = toRelativeDisplay(node.file, projectPath);
170
171
  const loc = node.line ? `${relPath}:${node.line}` : relPath;
171
172
  console.log(` Location: ${loc}`);
172
173
  }
@@ -9,7 +9,8 @@
9
9
  */
10
10
 
11
11
  import { Command } from 'commander';
12
- import { resolve, join, relative } from 'path';
12
+ import { resolve, join } from 'path';
13
+ import { toRelativeDisplay } from '../utils/pathUtils.js';
13
14
  import { existsSync } from 'fs';
14
15
  import { execSync } from 'child_process';
15
16
  import React, { useState, useEffect } from 'react';
@@ -234,6 +235,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
234
235
  const preview = getCodePreview({
235
236
  file: state.currentNode.file,
236
237
  line: state.currentNode.line,
238
+ projectPath,
237
239
  });
238
240
  if (preview) {
239
241
  const formatted = formatCodePreview(preview, state.currentNode.line);
@@ -421,7 +423,7 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
421
423
 
422
424
  const formatLoc = (node: NodeInfo) => {
423
425
  if (!node.file) return '';
424
- const rel = relative(projectPath, node.file);
426
+ const rel = toRelativeDisplay(node.file, projectPath);
425
427
  return node.line ? `${rel}:${node.line}` : rel;
426
428
  };
427
429
 
@@ -1040,7 +1042,7 @@ function outputResults(
1040
1042
  // Text format
1041
1043
  if (target) {
1042
1044
  console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
1043
- console.log(`File: ${relative(projectPath, target.file)}${target.line ? `:${target.line}` : ''}`);
1045
+ console.log(`File: ${toRelativeDisplay(target.file, projectPath)}${target.line ? `:${target.line}` : ''}`);
1044
1046
  console.log('');
1045
1047
  }
1046
1048
 
@@ -1048,7 +1050,7 @@ function outputResults(
1048
1050
  console.log(` (no ${mode} found)`);
1049
1051
  } else {
1050
1052
  for (const node of nodes) {
1051
- const loc = relative(projectPath, node.file);
1053
+ const loc = toRelativeDisplay(node.file, projectPath);
1052
1054
  console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
1053
1055
  }
1054
1056
  }
@@ -1063,7 +1065,7 @@ function formatNodeForJson(node: NodeInfo, projectPath: string): object {
1063
1065
  id: node.id,
1064
1066
  type: node.type,
1065
1067
  name: node.name,
1066
- file: relative(projectPath, node.file),
1068
+ file: toRelativeDisplay(node.file, projectPath),
1067
1069
  line: node.line,
1068
1070
  async: node.async,
1069
1071
  exported: node.exported,