@grafema/cli 0.2.12-beta → 0.3.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 (131) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/analyze.d.ts.map +1 -1
  4. package/dist/commands/analyze.js +2 -4
  5. package/dist/commands/analyze.js.map +1 -1
  6. package/dist/commands/analyzeAction.d.ts +5 -3
  7. package/dist/commands/analyzeAction.d.ts.map +1 -1
  8. package/dist/commands/analyzeAction.js +109 -151
  9. package/dist/commands/analyzeAction.js.map +1 -1
  10. package/dist/commands/check.d.ts +1 -1
  11. package/dist/commands/check.js +4 -4
  12. package/dist/commands/check.js.map +1 -1
  13. package/dist/commands/context.js +2 -2
  14. package/dist/commands/context.js.map +1 -1
  15. package/dist/commands/coverage.js +2 -2
  16. package/dist/commands/coverage.js.map +1 -1
  17. package/dist/commands/describe.d.ts +13 -0
  18. package/dist/commands/describe.d.ts.map +1 -0
  19. package/dist/commands/describe.js +131 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/doctor/checks.d.ts +6 -1
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +128 -13
  24. package/dist/commands/doctor/checks.js.map +1 -1
  25. package/dist/commands/doctor.d.ts +10 -9
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +12 -10
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/explain.js +2 -2
  30. package/dist/commands/explain.js.map +1 -1
  31. package/dist/commands/file.js +2 -2
  32. package/dist/commands/file.js.map +1 -1
  33. package/dist/commands/get.js +2 -2
  34. package/dist/commands/get.js.map +1 -1
  35. package/dist/commands/git-ingest.d.ts +6 -0
  36. package/dist/commands/git-ingest.d.ts.map +1 -0
  37. package/dist/commands/git-ingest.js +46 -0
  38. package/dist/commands/git-ingest.js.map +1 -0
  39. package/dist/commands/impact.d.ts.map +1 -1
  40. package/dist/commands/impact.js +276 -50
  41. package/dist/commands/impact.js.map +1 -1
  42. package/dist/commands/init.d.ts.map +1 -1
  43. package/dist/commands/init.js +20 -22
  44. package/dist/commands/init.js.map +1 -1
  45. package/dist/commands/ls.js +2 -2
  46. package/dist/commands/ls.js.map +1 -1
  47. package/dist/commands/overview.js +2 -2
  48. package/dist/commands/overview.js.map +1 -1
  49. package/dist/commands/query.d.ts +1 -1
  50. package/dist/commands/query.d.ts.map +1 -1
  51. package/dist/commands/query.js +169 -7
  52. package/dist/commands/query.js.map +1 -1
  53. package/dist/commands/schema.js +2 -2
  54. package/dist/commands/schema.js.map +1 -1
  55. package/dist/commands/server.js +11 -6
  56. package/dist/commands/server.js.map +1 -1
  57. package/dist/commands/stats.js +2 -2
  58. package/dist/commands/stats.js.map +1 -1
  59. package/dist/commands/tldr.d.ts +12 -0
  60. package/dist/commands/tldr.d.ts.map +1 -0
  61. package/dist/commands/tldr.js +81 -0
  62. package/dist/commands/tldr.js.map +1 -0
  63. package/dist/commands/trace.d.ts +1 -1
  64. package/dist/commands/trace.d.ts.map +1 -1
  65. package/dist/commands/trace.js +17 -133
  66. package/dist/commands/trace.js.map +1 -1
  67. package/dist/commands/types.js +2 -2
  68. package/dist/commands/types.js.map +1 -1
  69. package/dist/commands/who.d.ts +12 -0
  70. package/dist/commands/who.d.ts.map +1 -0
  71. package/dist/commands/who.js +184 -0
  72. package/dist/commands/who.js.map +1 -0
  73. package/dist/commands/why.d.ts +12 -0
  74. package/dist/commands/why.d.ts.map +1 -0
  75. package/dist/commands/why.js +118 -0
  76. package/dist/commands/why.js.map +1 -0
  77. package/dist/commands/wtf.d.ts +12 -0
  78. package/dist/commands/wtf.d.ts.map +1 -0
  79. package/dist/commands/wtf.js +117 -0
  80. package/dist/commands/wtf.js.map +1 -0
  81. package/dist/plugins/builtinPlugins.d.ts +1 -9
  82. package/dist/plugins/builtinPlugins.d.ts.map +1 -1
  83. package/dist/plugins/builtinPlugins.js +2 -67
  84. package/dist/plugins/builtinPlugins.js.map +1 -1
  85. package/dist/plugins/pluginLoader.d.ts +1 -15
  86. package/dist/plugins/pluginLoader.d.ts.map +1 -1
  87. package/dist/plugins/pluginLoader.js +2 -100
  88. package/dist/plugins/pluginLoader.js.map +1 -1
  89. package/dist/plugins/pluginResolver.js +3 -3
  90. package/dist/utils/progressRenderer.d.ts +15 -1
  91. package/dist/utils/progressRenderer.d.ts.map +1 -1
  92. package/dist/utils/progressRenderer.js.map +1 -1
  93. package/dist/utils/queryHints.d.ts +6 -0
  94. package/dist/utils/queryHints.d.ts.map +1 -0
  95. package/dist/utils/queryHints.js +36 -0
  96. package/dist/utils/queryHints.js.map +1 -0
  97. package/package.json +4 -4
  98. package/skills/grafema-codebase-analysis/SKILL.md +1 -1
  99. package/src/cli.ts +14 -0
  100. package/src/commands/analyze.ts +2 -4
  101. package/src/commands/analyzeAction.ts +122 -168
  102. package/src/commands/check.ts +5 -5
  103. package/src/commands/context.ts +3 -3
  104. package/src/commands/coverage.ts +2 -2
  105. package/src/commands/describe.ts +160 -0
  106. package/src/commands/doctor/checks.ts +153 -10
  107. package/src/commands/doctor.ts +13 -9
  108. package/src/commands/explain.ts +2 -2
  109. package/src/commands/explore.tsx +2 -2
  110. package/src/commands/file.ts +3 -3
  111. package/src/commands/get.ts +2 -2
  112. package/src/commands/git-ingest.ts +49 -0
  113. package/src/commands/impact.ts +318 -55
  114. package/src/commands/init.ts +20 -22
  115. package/src/commands/ls.ts +2 -2
  116. package/src/commands/overview.ts +2 -2
  117. package/src/commands/query.ts +197 -7
  118. package/src/commands/schema.ts +2 -2
  119. package/src/commands/server.ts +12 -6
  120. package/src/commands/stats.ts +2 -2
  121. package/src/commands/tldr.ts +103 -0
  122. package/src/commands/trace.ts +19 -161
  123. package/src/commands/types.ts +2 -2
  124. package/src/commands/who.ts +215 -0
  125. package/src/commands/why.ts +134 -0
  126. package/src/commands/wtf.ts +140 -0
  127. package/src/plugins/builtinPlugins.ts +1 -108
  128. package/src/plugins/pluginLoader.ts +1 -123
  129. package/src/plugins/pluginResolver.js +3 -3
  130. package/src/utils/progressRenderer.ts +15 -1
  131. package/src/utils/queryHints.ts +46 -0
@@ -1,24 +1,24 @@
1
1
  /**
2
- * Analyze command action — connects to RFDB, loads plugins, runs Orchestrator.
2
+ * Analyze command action — spawns grafema-orchestrator for project analysis.
3
3
  *
4
- * Extracted from analyze.ts (REG-435) to keep command definition separate
5
- * from execution logic.
4
+ * The Rust grafema-orchestrator binary handles the full analysis pipeline:
5
+ * discovery, parsing (OXC), analysis (grafema-analyzer), resolution,
6
+ * and RFDB ingestion. This action finds the binary, spawns it with
7
+ * the correct args, streams output, and prints a summary.
6
8
  */
7
9
 
8
10
  import { resolve, join } from 'path';
9
11
  import { existsSync, mkdirSync } from 'fs';
12
+ import { spawn } from 'child_process';
10
13
  import {
11
- Orchestrator,
12
14
  RFDBServerBackend,
13
- DiagnosticReporter,
14
- DiagnosticWriter,
15
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';
16
+ findOrchestratorBinary,
17
+ getBinaryNotFoundMessage,
18
+ findAnalyzerBinary,
19
+ ensureBinary,
20
+ } from '@grafema/util';
21
+ import type { LogLevel } from '@grafema/util';
22
22
 
23
23
  export interface NodeEdgeCountBackend {
24
24
  nodeCount: () => Promise<number>;
@@ -53,6 +53,28 @@ function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: s
53
53
  return 'silent'; // Default: silent logs, clean progress UI
54
54
  }
55
55
 
56
+ /**
57
+ * Find the grafema.config.yaml config file for the orchestrator.
58
+ *
59
+ * Search order:
60
+ * 1. <projectPath>/grafema.config.yaml
61
+ * 2. <projectPath>/.grafema/config.yaml (legacy location)
62
+ */
63
+ function findConfigFile(projectPath: string): string | null {
64
+ const candidates = [
65
+ join(projectPath, 'grafema.config.yaml'),
66
+ join(projectPath, '.grafema', 'config.yaml'),
67
+ ];
68
+
69
+ for (const candidate of candidates) {
70
+ if (existsSync(candidate)) {
71
+ return candidate;
72
+ }
73
+ }
74
+
75
+ return null;
76
+ }
77
+
56
78
  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
79
  const projectPath = resolve(path);
58
80
  const grafemaDir = join(projectPath, '.grafema');
@@ -71,35 +93,74 @@ export async function analyzeAction(path: string, options: { service?: string; e
71
93
  // Create logger based on CLI flags
72
94
  const logLevel = getLogLevel(options);
73
95
  const logFile = options.logFile ? resolve(options.logFile) : undefined;
74
- const logger = createLogger(logLevel, logFile ? { logFile } : undefined);
96
+ const _logger = createLogger(logLevel, logFile ? { logFile } : undefined);
75
97
 
76
98
  if (logFile) {
77
99
  debug(`Log file: ${logFile}`);
78
100
  }
79
101
  debug(`Analyzing project: ${projectPath}`);
80
102
 
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
103
+ // Find grafema-orchestrator binary
104
+ const orchestratorBinary = findOrchestratorBinary();
105
+ if (!orchestratorBinary) {
106
+ console.error('');
107
+ console.error(getBinaryNotFoundMessage('grafema-orchestrator'));
108
+ process.exit(1);
109
+ }
110
+
111
+ debug(`Using orchestrator: ${orchestratorBinary}`);
112
+
113
+ // Ensure JS/TS analyzer binaries exist (lazy download if missing)
114
+ for (const binName of ['grafema-analyzer', 'grafema-resolve']) {
115
+ const existing = findAnalyzerBinary(binName);
116
+ if (!existing) {
117
+ const downloaded = await ensureBinary(binName, null, info);
118
+ if (downloaded) {
119
+ debug(`Downloaded ${binName} → ${downloaded}`);
120
+ }
121
+ }
122
+ }
123
+
124
+ // Find config file for the orchestrator
125
+ const configPath = findConfigFile(projectPath);
126
+ if (!configPath) {
127
+ console.error('');
128
+ console.error('No grafema config file found.');
129
+ console.error('');
130
+ console.error('Expected one of:');
131
+ console.error(` ${join(projectPath, 'grafema.config.yaml')}`);
132
+ console.error(` ${join(projectPath, '.grafema', 'config.yaml')}`);
133
+ console.error('');
134
+ console.error('Create a config file with at least:');
135
+ console.error(' root: "."');
136
+ console.error(' include:');
137
+ console.error(' - "src/**/*.js"');
138
+ console.error('');
139
+ process.exit(1);
140
+ }
141
+
142
+ debug(`Using config: ${configPath}`);
143
+
144
+ // Connect to RFDB server — auto-start by default (zero-config UX)
85
145
  const backend = new RFDBServerBackend({
86
146
  dbPath,
87
- autoStart: options.autoStart ?? false,
88
- silent: !options.verbose // Silent in normal mode (show progress), verbose shows logs
147
+ autoStart: options.autoStart ?? true,
148
+ silent: !options.verbose,
149
+ clientName: 'cli'
89
150
  });
90
151
 
91
152
  try {
92
153
  await backend.connect();
93
154
  } catch (err) {
94
- if (!options.autoStart && err instanceof Error && err.message.includes('not running')) {
155
+ if (err instanceof Error && err.message.includes('not running')) {
95
156
  console.error('');
96
- console.error('RFDB server is not running.');
157
+ console.error('RFDB server failed to start.');
97
158
  console.error('');
98
- console.error('Start the server first:');
159
+ console.error('Try starting manually:');
99
160
  console.error(' grafema server start');
100
161
  console.error('');
101
- console.error('Or use --auto-start flag:');
102
- console.error(' grafema analyze --auto-start');
162
+ console.error('Or run diagnostics:');
163
+ console.error(' grafema doctor');
103
164
  console.error('');
104
165
  process.exit(1);
105
166
  }
@@ -111,174 +172,67 @@ export async function analyzeAction(path: string, options: { service?: string; e
111
172
  await backend.clear();
112
173
  }
113
174
 
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
175
  const startTime = Date.now();
138
176
 
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
- });
177
+ // Build orchestrator args
178
+ const args: string[] = ['analyze', '--config', configPath, '--socket', backend.socketPath];
148
179
 
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?.();
180
+ if (options.clear) {
181
+ args.push('--force');
161
182
  }
162
183
 
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
- });
184
+ debug(`Spawning: ${orchestratorBinary} ${args.join(' ')}`);
176
185
 
177
186
  let exitCode = 0;
178
187
 
179
188
  try {
180
- await orchestrator.run(projectPath);
181
- await backend.flush();
189
+ // Spawn grafema-orchestrator
190
+ exitCode = await new Promise<number>((resolvePromise, reject) => {
191
+ const child = spawn(orchestratorBinary, args, {
192
+ stdio: [
193
+ 'ignore',
194
+ options.quiet ? 'ignore' : 'inherit',
195
+ options.quiet ? 'ignore' : 'inherit',
196
+ ],
197
+ env: {
198
+ ...process.env,
199
+ // Pass RUST_LOG for tracing verbosity
200
+ RUST_LOG: options.verbose ? 'info' : (options.debug ? 'debug' : 'warn'),
201
+ },
202
+ });
182
203
 
183
- const elapsedSeconds = (Date.now() - startTime) / 1000;
184
- const stats = await fetchNodeEdgeCounts(backend);
204
+ child.on('error', (err) => {
205
+ reject(new Error(`Failed to spawn grafema-orchestrator: ${err.message}`));
206
+ });
185
207
 
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}`);
208
+ child.on('close', (code) => {
209
+ resolvePromise(code ?? 1);
210
+ });
211
+ });
194
212
 
195
- // Get diagnostics and report summary
196
- const diagnostics = orchestrator.getDiagnostics();
197
- const reporter = new DiagnosticReporter(diagnostics);
213
+ if (exitCode === 0) {
214
+ const elapsedSeconds = (Date.now() - startTime) / 1000;
215
+ const stats = await fetchNodeEdgeCounts(backend);
198
216
 
199
- // Print summary if there are any issues
200
- if (diagnostics.count() > 0) {
201
217
  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
218
+ info(`Analysis complete in ${elapsedSeconds.toFixed(2)}s`);
219
+ info(` Nodes: ${stats.nodeCount}`);
220
+ info(` Edges: ${stats.edgeCount}`);
223
221
  } 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
222
  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)}`);
223
+ console.error(`Analysis failed with exit code ${exitCode}`);
266
224
  }
267
-
225
+ } catch (e) {
226
+ const error = e instanceof Error ? e : new Error(String(e));
227
+ console.error('');
228
+ console.error(`Analysis failed: ${error.message}`);
268
229
  exitCode = 1;
269
230
  } finally {
270
- // Stop stats polling
271
- if (statsInterval) {
272
- clearInterval(statsInterval);
273
- statsInterval = null;
274
- }
275
-
276
231
  if (backend.connected) {
277
232
  await backend.close();
278
233
  }
279
234
 
280
235
  // Exit with appropriate code
281
- // 0 = success, 1 = fatal, 2 = errors
282
236
  exitWithCode(exitCode);
283
237
  }
284
238
  }
@@ -15,8 +15,8 @@ import {
15
15
  GraphFreshnessChecker,
16
16
  IncrementalReanalyzer,
17
17
  DIAGNOSTIC_CATEGORIES,
18
- } from '@grafema/core';
19
- import type { GuaranteeGraph, DiagnosticCategoryKey } from '@grafema/core';
18
+ } from '@grafema/util';
19
+ import type { GuaranteeGraph, DiagnosticCategoryKey } from '@grafema/util';
20
20
  import { exitWithError } from '../utils/errorFormatter.js';
21
21
 
22
22
 
@@ -31,7 +31,7 @@ const BUILT_IN_VALIDATORS: Record<string, { name: string; description: string; c
31
31
  // }
32
32
  };
33
33
 
34
- // Re-export for backward compatibility (deprecated - import from @grafema/core instead)
34
+ // Re-export for backward compatibility (deprecated - import from @grafema/util instead)
35
35
  export { DIAGNOSTIC_CATEGORIES as CHECK_CATEGORIES };
36
36
 
37
37
  export const checkCommand = new Command('check')
@@ -132,7 +132,7 @@ Examples:
132
132
  exitWithError('No graph database found', ['Run: grafema analyze']);
133
133
  }
134
134
 
135
- const backend = new RFDBServerBackend({ dbPath });
135
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
136
136
  await backend.connect();
137
137
 
138
138
  // Check graph freshness
@@ -287,7 +287,7 @@ async function runBuiltInValidator(
287
287
  exitWithError('No graph database found', ['Run: grafema analyze']);
288
288
  }
289
289
 
290
- const backend = new RFDBServerBackend({ dbPath });
290
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
291
291
  await backend.connect();
292
292
 
293
293
  // Check graph freshness
@@ -21,8 +21,8 @@ import {
21
21
  getNodeDisplayName,
22
22
  formatEdgeMetadata,
23
23
  STRUCTURAL_EDGE_TYPES,
24
- } from '@grafema/core';
25
- import type { NodeContext, EdgeGroup } from '@grafema/core';
24
+ } from '@grafema/util';
25
+ import type { NodeContext, EdgeGroup } from '@grafema/util';
26
26
  import type { BaseNodeRecord } from '@grafema/types';
27
27
  import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
28
28
  import { formatLocation } from '../utils/formatNode.js';
@@ -79,7 +79,7 @@ Examples:
79
79
  exitWithError('No graph database found', ['Run: grafema analyze']);
80
80
  }
81
81
 
82
- const backend = new RFDBServerBackend({ dbPath });
82
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
83
83
  await backend.connect();
84
84
 
85
85
  const spinner = new Spinner('Loading context...');
@@ -10,7 +10,7 @@
10
10
  import { Command } from 'commander';
11
11
  import { resolve, join } from 'path';
12
12
  import { existsSync } from 'fs';
13
- import { RFDBServerBackend, CoverageAnalyzer, type CoverageResult } from '@grafema/core';
13
+ import { RFDBServerBackend, CoverageAnalyzer, type CoverageResult } from '@grafema/util';
14
14
  import { exitWithError } from '../utils/errorFormatter.js';
15
15
 
16
16
  export const coverageCommand = new Command('coverage')
@@ -34,7 +34,7 @@ Examples:
34
34
  exitWithError('No graph database found', ['Run: grafema analyze']);
35
35
  }
36
36
 
37
- const backend = new RFDBServerBackend({ dbPath });
37
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
38
38
  await backend.connect();
39
39
 
40
40
  try {
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Describe command — Render compact DSL notation for a graph node
3
+ *
4
+ * Resolves target by: semantic ID → file path (MODULE) → node name.
5
+ * Calls extractSubgraph() + renderNotation() from @grafema/util.
6
+ *
7
+ * Output is DSL notation using archetype-grouped operators:
8
+ * o- dependency, > call/flow, < read/input, => write,
9
+ * >x exception, ~>> event, ?| guard, |= governance
10
+ */
11
+
12
+ import { Command } from 'commander';
13
+ import { resolve, join } from 'path';
14
+ import { existsSync } from 'fs';
15
+ import {
16
+ RFDBServerBackend,
17
+ renderNotation,
18
+ extractSubgraph,
19
+ PERSPECTIVES,
20
+ } from '@grafema/util';
21
+ import type { DescribeOptions } from '@grafema/util';
22
+ import { exitWithError } from '../utils/errorFormatter.js';
23
+ import { Spinner } from '../utils/spinner.js';
24
+
25
+ interface DescribeCommandOptions {
26
+ project: string;
27
+ depth: string;
28
+ perspective?: string;
29
+ budget?: string;
30
+ json?: boolean;
31
+ locations?: boolean;
32
+ }
33
+
34
+ export const describeCommand = new Command('describe')
35
+ .description('Render compact DSL notation for a graph node')
36
+ .argument('<target>', 'Semantic ID, file path, or node name')
37
+ .option('-p, --project <path>', 'Project path', '.')
38
+ .option('-d, --depth <level>', 'LOD: 0=names, 1=edges, 2=nested+fold, 3=nested (exact)', '1')
39
+ .option(
40
+ '--perspective <name>',
41
+ `Perspective preset: ${Object.keys(PERSPECTIVES).join(', ')}`,
42
+ )
43
+ .option('-b, --budget <n>', 'Max items before summarization (default 7)')
44
+ .option('-j, --json', 'Output as JSON for scripting')
45
+ .option('-l, --locations', 'Include file:line locations')
46
+ .addHelpText('after', `
47
+ Perspectives:
48
+ security writes + exceptions
49
+ data flow out/in + writes
50
+ errors exceptions only
51
+ api flow out + publishes + depends
52
+ events publishes only
53
+
54
+ LOD levels:
55
+ 0 Node names only (minimal)
56
+ 1 Node + edges (default)
57
+ 2 Node + edges + nested children (folded)
58
+ 3 Node + edges + nested children (exact, no folding)
59
+
60
+ Examples:
61
+ grafema describe "src/app.ts->FUNCTION->main"
62
+ grafema describe src/app.ts
63
+ grafema describe handleRequest
64
+ grafema describe handleRequest --perspective security
65
+ grafema describe MyClass -d 2 --locations
66
+ grafema describe handleRequest --json
67
+ `)
68
+ .action(async (target: string, options: DescribeCommandOptions) => {
69
+ const projectPath = resolve(options.project);
70
+ const grafemaDir = join(projectPath, '.grafema');
71
+ const dbPath = join(grafemaDir, 'graph.rfdb');
72
+
73
+ if (!existsSync(dbPath)) {
74
+ exitWithError('No graph database found', ['Run: grafema analyze']);
75
+ }
76
+
77
+ const depth = parseInt(options.depth, 10);
78
+ if (isNaN(depth) || depth < 0 || depth > 3) {
79
+ exitWithError('Invalid depth', ['Use 0, 1, 2, or 3']);
80
+ }
81
+
82
+ if (options.perspective && !PERSPECTIVES[options.perspective]) {
83
+ exitWithError(`Unknown perspective: "${options.perspective}"`, [
84
+ `Available: ${Object.keys(PERSPECTIVES).join(', ')}`,
85
+ ]);
86
+ }
87
+
88
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
89
+ await backend.connect();
90
+
91
+ const spinner = new Spinner('Resolving target...');
92
+ spinner.start();
93
+
94
+ try {
95
+ // Step 1: Resolve target → node
96
+ let node = await backend.getNode(target);
97
+
98
+ if (!node) {
99
+ for await (const n of backend.queryNodes({ file: target, type: 'MODULE' })) {
100
+ node = n;
101
+ break;
102
+ }
103
+ }
104
+ if (!node) {
105
+ for await (const n of backend.queryNodes({ name: target })) {
106
+ node = n;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (!node) {
112
+ spinner.stop();
113
+ exitWithError(`Target not found: "${target}"`, [
114
+ 'Use: grafema query "<name>" to find available nodes',
115
+ 'Try: semantic ID, file path, or node name',
116
+ ]);
117
+ return;
118
+ }
119
+
120
+ // Step 2: Extract subgraph
121
+ const subgraph = await extractSubgraph(backend, node.id, depth);
122
+
123
+ // Step 3: Build options
124
+ const describeOptions: DescribeOptions = {
125
+ depth,
126
+ includeLocations: options.locations ?? depth >= 2,
127
+ };
128
+ if (options.perspective && PERSPECTIVES[options.perspective]) {
129
+ describeOptions.archetypeFilter = PERSPECTIVES[options.perspective];
130
+ }
131
+ if (options.budget) {
132
+ describeOptions.budget = parseInt(options.budget, 10);
133
+ }
134
+
135
+ // Step 4: Render
136
+ const notation = renderNotation(subgraph, describeOptions);
137
+
138
+ spinner.stop();
139
+
140
+ if (options.json) {
141
+ console.log(JSON.stringify({
142
+ target: node.id,
143
+ dsl: notation || `[${node.type}] ${node.name ?? node.id}\nNo relationships found at depth=${depth}.`,
144
+ subgraph: {
145
+ rootNodes: subgraph.rootNodes.length,
146
+ edges: subgraph.edges.length,
147
+ nodes: subgraph.nodeMap.size,
148
+ },
149
+ }, null, 2));
150
+ } else if (notation.trim()) {
151
+ console.log(notation);
152
+ } else {
153
+ console.log(`[${node.type}] ${node.name ?? node.id}`);
154
+ console.log(`No relationships found at depth=${depth}.`);
155
+ }
156
+ } finally {
157
+ spinner.stop();
158
+ await backend.close();
159
+ }
160
+ });