@grafema/cli 0.2.3-beta → 0.2.5-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 (84) hide show
  1. package/README.md +73 -0
  2. package/dist/cli.js +1 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/analyze.d.ts +9 -0
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +136 -52
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/check.d.ts +2 -6
  9. package/dist/commands/check.d.ts.map +1 -1
  10. package/dist/commands/check.js +32 -46
  11. package/dist/commands/check.js.map +1 -0
  12. package/dist/commands/coverage.js +1 -0
  13. package/dist/commands/coverage.js.map +1 -0
  14. package/dist/commands/doctor/checks.d.ts.map +1 -1
  15. package/dist/commands/doctor/checks.js +9 -5
  16. package/dist/commands/doctor/checks.js.map +1 -0
  17. package/dist/commands/doctor/output.js +1 -0
  18. package/dist/commands/doctor/output.js.map +1 -0
  19. package/dist/commands/doctor/types.js +1 -0
  20. package/dist/commands/doctor/types.js.map +1 -0
  21. package/dist/commands/doctor.js +1 -0
  22. package/dist/commands/doctor.js.map +1 -0
  23. package/dist/commands/explain.js +1 -0
  24. package/dist/commands/explain.js.map +1 -0
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +9 -4
  27. package/dist/commands/explore.js.map +1 -0
  28. package/dist/commands/get.d.ts.map +1 -1
  29. package/dist/commands/get.js +7 -0
  30. package/dist/commands/get.js.map +1 -0
  31. package/dist/commands/impact.js +1 -0
  32. package/dist/commands/impact.js.map +1 -0
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +7 -1
  35. package/dist/commands/init.js.map +1 -0
  36. package/dist/commands/ls.d.ts.map +1 -1
  37. package/dist/commands/ls.js +7 -0
  38. package/dist/commands/ls.js.map +1 -0
  39. package/dist/commands/overview.d.ts.map +1 -1
  40. package/dist/commands/overview.js +1 -0
  41. package/dist/commands/overview.js.map +1 -0
  42. package/dist/commands/query.d.ts.map +1 -1
  43. package/dist/commands/query.js +68 -1
  44. package/dist/commands/query.js.map +1 -0
  45. package/dist/commands/schema.js +1 -0
  46. package/dist/commands/schema.js.map +1 -0
  47. package/dist/commands/server.d.ts +2 -1
  48. package/dist/commands/server.d.ts.map +1 -1
  49. package/dist/commands/server.js +50 -3
  50. package/dist/commands/server.js.map +1 -0
  51. package/dist/commands/stats.js +1 -0
  52. package/dist/commands/stats.js.map +1 -0
  53. package/dist/commands/trace.js +1 -0
  54. package/dist/commands/trace.js.map +1 -0
  55. package/dist/commands/types.js +1 -0
  56. package/dist/commands/types.js.map +1 -0
  57. package/dist/utils/codePreview.js +1 -0
  58. package/dist/utils/codePreview.js.map +1 -0
  59. package/dist/utils/errorFormatter.js +1 -0
  60. package/dist/utils/errorFormatter.js.map +1 -0
  61. package/dist/utils/formatNode.js +1 -0
  62. package/dist/utils/formatNode.js.map +1 -0
  63. package/dist/utils/progressRenderer.d.ts +119 -0
  64. package/dist/utils/progressRenderer.d.ts.map +1 -0
  65. package/dist/utils/progressRenderer.js +245 -0
  66. package/dist/utils/progressRenderer.js.map +1 -0
  67. package/dist/utils/spinner.d.ts +39 -0
  68. package/dist/utils/spinner.d.ts.map +1 -0
  69. package/dist/utils/spinner.js +84 -0
  70. package/dist/utils/spinner.js.map +1 -0
  71. package/package.json +5 -4
  72. package/src/commands/analyze.ts +150 -55
  73. package/src/commands/check.ts +36 -68
  74. package/src/commands/doctor/checks.ts +8 -5
  75. package/src/commands/explore.tsx +8 -4
  76. package/src/commands/get.ts +8 -0
  77. package/src/commands/impact.ts +1 -1
  78. package/src/commands/init.ts +6 -2
  79. package/src/commands/ls.ts +8 -0
  80. package/src/commands/overview.ts +0 -4
  81. package/src/commands/query.ts +77 -1
  82. package/src/commands/server.ts +57 -3
  83. package/src/utils/progressRenderer.ts +288 -0
  84. package/src/utils/spinner.ts +94 -0
@@ -6,14 +6,16 @@ import { Command } from 'commander';
6
6
  import { resolve, join } from 'path';
7
7
  import { existsSync, mkdirSync, readdirSync } from 'fs';
8
8
  import { pathToFileURL } from 'url';
9
+ import type {
10
+ Plugin} from '@grafema/core';
9
11
  import {
10
12
  Orchestrator,
11
13
  RFDBServerBackend,
12
- Plugin,
13
14
  DiagnosticReporter,
14
15
  DiagnosticWriter,
15
16
  createLogger,
16
17
  loadConfig,
18
+ StrictModeFailure,
17
19
  type GrafemaConfig,
18
20
  // Discovery
19
21
  SimpleProjectDiscovery,
@@ -38,12 +40,14 @@ import {
38
40
  AliasTracker,
39
41
  ValueDomainAnalyzer,
40
42
  MountPointResolver,
43
+ ExpressHandlerLinker,
41
44
  PrefixEvaluator,
42
45
  InstanceOfResolver,
43
46
  ImportExportLinker,
44
47
  FunctionCallResolver,
45
48
  HTTPConnectionEnricher,
46
49
  RustFFIEnricher,
50
+ RejectionPropagationEnricher,
47
51
  // Validation
48
52
  CallResolverValidator,
49
53
  EvalBanValidator,
@@ -54,7 +58,22 @@ import {
54
58
  TypeScriptDeadCodeValidator,
55
59
  BrokenImportValidator,
56
60
  } from '@grafema/core';
57
- import type { LogLevel } from '@grafema/types';
61
+ import type { LogLevel, GraphBackend } from '@grafema/types';
62
+ import { ProgressRenderer } from '../utils/progressRenderer.js';
63
+
64
+ export interface NodeEdgeCountBackend {
65
+ nodeCount: () => Promise<number>;
66
+ edgeCount: () => Promise<number>;
67
+ }
68
+
69
+ export async function fetchNodeEdgeCounts(backend: NodeEdgeCountBackend): Promise<{ nodeCount: number; edgeCount: number }> {
70
+ const [nodeCount, edgeCount] = await Promise.all([backend.nodeCount(), backend.edgeCount()]);
71
+ return { nodeCount, edgeCount };
72
+ }
73
+
74
+ export function exitWithCode(code: number, exitFn: (code: number) => void = process.exit): void {
75
+ exitFn(code);
76
+ }
58
77
 
59
78
  const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
60
79
  // Discovery
@@ -80,12 +99,14 @@ const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
80
99
  AliasTracker: () => new AliasTracker() as Plugin,
81
100
  ValueDomainAnalyzer: () => new ValueDomainAnalyzer() as Plugin,
82
101
  MountPointResolver: () => new MountPointResolver() as Plugin,
102
+ ExpressHandlerLinker: () => new ExpressHandlerLinker() as Plugin,
83
103
  PrefixEvaluator: () => new PrefixEvaluator() as Plugin,
84
104
  InstanceOfResolver: () => new InstanceOfResolver() as Plugin,
85
105
  ImportExportLinker: () => new ImportExportLinker() as Plugin,
86
106
  FunctionCallResolver: () => new FunctionCallResolver() as Plugin,
87
107
  HTTPConnectionEnricher: () => new HTTPConnectionEnricher() as Plugin,
88
108
  RustFFIEnricher: () => new RustFFIEnricher() as Plugin,
109
+ RejectionPropagationEnricher: () => new RejectionPropagationEnricher() as Plugin,
89
110
  // Validation
90
111
  CallResolverValidator: () => new CallResolverValidator() as Plugin,
91
112
  EvalBanValidator: () => new EvalBanValidator() as Plugin,
@@ -113,7 +134,7 @@ async function loadCustomPlugins(
113
134
 
114
135
  try {
115
136
  const files = readdirSync(pluginsDir).filter(
116
- (f) => f.endsWith('.js') || f.endsWith('.mjs')
137
+ (f) => f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.cjs')
117
138
  );
118
139
 
119
140
  for (const file of files) {
@@ -122,18 +143,24 @@ async function loadCustomPlugins(
122
143
  const pluginUrl = pathToFileURL(pluginPath).href;
123
144
  const module = await import(pluginUrl);
124
145
 
125
- const PluginClass = module.default || module[file.replace(/\.(m?js)$/, '')];
146
+ const PluginClass = module.default || module[file.replace(/\.[cm]?js$/, '')];
126
147
  if (PluginClass && typeof PluginClass === 'function') {
127
- const pluginName = PluginClass.name || file.replace(/\.(m?js)$/, '');
128
- customPlugins[pluginName] = () => new PluginClass() as Plugin;
148
+ const pluginName = PluginClass.name || file.replace(/\.[cm]?js$/, '');
149
+ customPlugins[pluginName] = () => {
150
+ const instance = new PluginClass() as Plugin;
151
+ instance.config.sourceFile = pluginPath;
152
+ return instance;
153
+ };
129
154
  log(`Loaded custom plugin: ${pluginName}`);
130
155
  }
131
156
  } catch (err) {
132
- console.warn(`Failed to load plugin ${file}: ${(err as Error).message}`);
157
+ const message = err instanceof Error ? err.message : String(err);
158
+ console.warn(`Failed to load plugin ${file}: ${message}`);
133
159
  }
134
160
  }
135
161
  } catch (err) {
136
- console.warn(`Error loading custom plugins: ${(err as Error).message}`);
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ console.warn(`Error loading custom plugins: ${message}`);
137
164
  }
138
165
 
139
166
  return customPlugins;
@@ -141,7 +168,8 @@ async function loadCustomPlugins(
141
168
 
142
169
  function createPlugins(
143
170
  config: GrafemaConfig['plugins'],
144
- customPlugins: Record<string, () => Plugin> = {}
171
+ customPlugins: Record<string, () => Plugin> = {},
172
+ verbose: boolean = false
145
173
  ): Plugin[] {
146
174
  const plugins: Plugin[] = [];
147
175
  const phases: (keyof GrafemaConfig['plugins'])[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
@@ -153,8 +181,9 @@ function createPlugins(
153
181
  const factory = BUILTIN_PLUGINS[name] || customPlugins[name];
154
182
  if (factory) {
155
183
  plugins.push(factory());
156
- } else {
157
- console.warn(`Unknown plugin: ${name}`);
184
+ } else if (verbose) {
185
+ // Only show plugin warning in verbose mode
186
+ console.warn(`Plugin not found: ${name} (skipping). Check .grafema/config.yaml or add to .grafema/plugins/`);
158
187
  }
159
188
  }
160
189
  }
@@ -164,7 +193,10 @@ function createPlugins(
164
193
 
165
194
  /**
166
195
  * Determine log level from CLI options.
167
- * Priority: --log-level > --quiet > --verbose > default ('info')
196
+ * Priority: --log-level > --quiet > --verbose > default ('silent')
197
+ *
198
+ * By default, logs are silent to allow clean progress UI.
199
+ * Use --verbose to see detailed logs (disables interactive progress).
168
200
  */
169
201
  function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: string }): LogLevel {
170
202
  if (options.logLevel) {
@@ -174,8 +206,8 @@ function getLogLevel(options: { quiet?: boolean; verbose?: boolean; logLevel?: s
174
206
  }
175
207
  }
176
208
  if (options.quiet) return 'silent';
177
- if (options.verbose) return 'debug';
178
- return 'info';
209
+ if (options.verbose) return 'info'; // --verbose shows logs instead of progress UI
210
+ return 'silent'; // Default: silent logs, clean progress UI
179
211
  }
180
212
 
181
213
  export const analyzeCommand = new Command('analyze')
@@ -188,6 +220,7 @@ export const analyzeCommand = new Command('analyze')
188
220
  .option('-v, --verbose', 'Show verbose logging')
189
221
  .option('--debug', 'Enable debug mode (writes diagnostics.log)')
190
222
  .option('--log-level <level>', 'Set log level (silent, errors, warnings, info, debug)')
223
+ .option('--log-file <path>', 'Write all log output to a file')
191
224
  .option('--strict', 'Enable strict mode (fail on unresolved references)')
192
225
  .option('--auto-start', 'Auto-start RFDB server if not running')
193
226
  .addHelpText('after', `
@@ -198,12 +231,13 @@ Examples:
198
231
  grafema analyze -s api Analyze only "api" service (monorepo)
199
232
  grafema analyze -v Verbose output with progress details
200
233
  grafema analyze --debug Write diagnostics.log for debugging
234
+ grafema analyze --log-file out.log Write all logs to a file
201
235
  grafema analyze --strict Fail on unresolved references (debugging)
202
236
  grafema analyze --auto-start Auto-start server (useful for CI)
203
237
 
204
238
  Note: Start the server first with: grafema server start
205
239
  `)
206
- .action(async (path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; strict?: boolean; autoStart?: boolean }) => {
240
+ .action(async (path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; logFile?: string; strict?: boolean; autoStart?: boolean }) => {
207
241
  const projectPath = resolve(path);
208
242
  const grafemaDir = join(projectPath, '.grafema');
209
243
  const dbPath = join(grafemaDir, 'graph.rfdb');
@@ -212,20 +246,30 @@ Note: Start the server first with: grafema server start
212
246
  mkdirSync(grafemaDir, { recursive: true });
213
247
  }
214
248
 
215
- const log = options.quiet ? () => {} : console.log;
249
+ // Two log levels for CLI output:
250
+ // - info: important results (shows unless --quiet)
251
+ // - debug: verbose details (shows only with --verbose)
252
+ const info = options.quiet ? () => {} : console.log;
253
+ const debug = options.verbose ? console.log : () => {};
216
254
 
217
255
  // Create logger based on CLI flags
218
256
  const logLevel = getLogLevel(options);
219
- const logger = createLogger(logLevel);
257
+ const logFile = options.logFile ? resolve(options.logFile) : undefined;
258
+ const logger = createLogger(logLevel, logFile ? { logFile } : undefined);
220
259
 
221
- log(`Analyzing project: ${projectPath}`);
260
+ if (logFile) {
261
+ debug(`Log file: ${logFile}`);
262
+ }
263
+ debug(`Analyzing project: ${projectPath}`);
222
264
 
223
265
  // Connect to RFDB server
224
266
  // Default: require explicit `grafema server start`
225
267
  // Use --auto-start for CI or backwards compatibility
268
+ // In normal mode (not verbose), suppress backend logs for clean progress UI
226
269
  const backend = new RFDBServerBackend({
227
270
  dbPath,
228
- autoStart: options.autoStart ?? false
271
+ autoStart: options.autoStart ?? false,
272
+ silent: !options.verbose // Silent in normal mode (show progress), verbose shows logs
229
273
  });
230
274
 
231
275
  try {
@@ -247,7 +291,7 @@ Note: Start the server first with: grafema server start
247
291
  }
248
292
 
249
293
  if (options.clear) {
250
- log('Clearing existing database...');
294
+ debug('Clearing existing database...');
251
295
  await backend.clear();
252
296
  }
253
297
 
@@ -255,29 +299,53 @@ Note: Start the server first with: grafema server start
255
299
 
256
300
  // Extract services from config (REG-174)
257
301
  if (config.services.length > 0) {
258
- log(`Loaded ${config.services.length} service(s) from config`);
302
+ debug(`Loaded ${config.services.length} service(s) from config`);
259
303
  for (const svc of config.services) {
260
304
  const entry = svc.entryPoint ? ` (entry: ${svc.entryPoint})` : '';
261
- log(` - ${svc.name}: ${svc.path}${entry}`);
305
+ debug(` - ${svc.name}: ${svc.path}${entry}`);
262
306
  }
263
307
  }
264
308
 
265
309
  // Load custom plugins from .grafema/plugins/
266
- const customPlugins = await loadCustomPlugins(projectPath, log);
267
- const plugins = createPlugins(config.plugins, customPlugins);
310
+ const customPlugins = await loadCustomPlugins(projectPath, debug);
311
+ const plugins = createPlugins(config.plugins, customPlugins, options.verbose);
268
312
 
269
- log(`Loaded ${plugins.length} plugins`);
313
+ debug(`Loaded ${plugins.length} plugins`);
270
314
 
271
315
  // Resolve strict mode: CLI flag overrides config
272
316
  const strictMode = options.strict ?? config.strict ?? false;
273
317
  if (strictMode) {
274
- log('Strict mode enabled - analysis will fail on unresolved references');
318
+ debug('Strict mode enabled - analysis will fail on unresolved references');
275
319
  }
276
320
 
277
321
  const startTime = Date.now();
278
322
 
323
+ // Create progress renderer for CLI output
324
+ // In quiet mode, use a no-op renderer (skip rendering)
325
+ // In verbose mode, use non-interactive (newlines per update)
326
+ // In normal mode, use interactive (spinner with line overwrite)
327
+ const renderer = options.quiet
328
+ ? null
329
+ : new ProgressRenderer({
330
+ isInteractive: !options.verbose && process.stdout.isTTY,
331
+ });
332
+
333
+ // Poll graph stats periodically to show node/edge counts in progress
334
+ let statsInterval: NodeJS.Timeout | null = null;
335
+ if (renderer && !options.quiet) {
336
+ statsInterval = setInterval(async () => {
337
+ try {
338
+ const stats = await fetchNodeEdgeCounts(backend);
339
+ renderer.setStats(stats.nodeCount, stats.edgeCount);
340
+ } catch {
341
+ // Ignore stats errors during analysis
342
+ }
343
+ }, 500); // Poll every 500ms
344
+ statsInterval.unref?.();
345
+ }
346
+
279
347
  const orchestrator = new Orchestrator({
280
- graph: backend as unknown as import('@grafema/types').GraphBackend,
348
+ graph: backend as unknown as GraphBackend,
281
349
  plugins,
282
350
  serviceFilter: options.service || null,
283
351
  entrypoint: options.entrypoint,
@@ -286,9 +354,7 @@ Note: Start the server first with: grafema server start
286
354
  services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
287
355
  strictMode, // REG-330: Pass strict mode flag
288
356
  onProgress: (progress) => {
289
- if (options.verbose) {
290
- log(`[${progress.phase}] ${progress.message}`);
291
- }
357
+ renderer?.update(progress);
292
358
  },
293
359
  });
294
360
 
@@ -298,13 +364,17 @@ Note: Start the server first with: grafema server start
298
364
  await orchestrator.run(projectPath);
299
365
  await backend.flush();
300
366
 
301
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
302
- const stats = await backend.getStats();
367
+ const elapsedSeconds = (Date.now() - startTime) / 1000;
368
+ const stats = await fetchNodeEdgeCounts(backend);
303
369
 
304
- log('');
305
- log(`Analysis complete in ${elapsed}s`);
306
- log(` Nodes: ${stats.nodeCount}`);
307
- log(` Edges: ${stats.edgeCount}`);
370
+ // Clear progress line in interactive mode, then show results
371
+ if (renderer && process.stdout.isTTY) {
372
+ process.stdout.write('\r\x1b[K'); // Clear line
373
+ }
374
+ info('');
375
+ info(renderer ? renderer.finish(elapsedSeconds) : `Analysis complete in ${elapsedSeconds.toFixed(2)}s`);
376
+ info(` Nodes: ${stats.nodeCount}`);
377
+ info(` Edges: ${stats.edgeCount}`);
308
378
 
309
379
  // Get diagnostics and report summary
310
380
  const diagnostics = orchestrator.getDiagnostics();
@@ -312,13 +382,13 @@ Note: Start the server first with: grafema server start
312
382
 
313
383
  // Print summary if there are any issues
314
384
  if (diagnostics.count() > 0) {
315
- log('');
316
- log(reporter.categorizedSummary());
385
+ info('');
386
+ info(reporter.categorizedSummary());
317
387
 
318
388
  // In verbose mode, print full report
319
389
  if (options.verbose) {
320
- log('');
321
- log(reporter.report({ format: 'text', includeSummary: false }));
390
+ debug('');
391
+ debug(reporter.report({ format: 'text', includeSummary: false }));
322
392
  }
323
393
  }
324
394
 
@@ -326,7 +396,7 @@ Note: Start the server first with: grafema server start
326
396
  const writer = new DiagnosticWriter();
327
397
  await writer.write(diagnostics, grafemaDir);
328
398
  if (options.debug) {
329
- log(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
399
+ debug(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
330
400
  }
331
401
 
332
402
  // Determine exit code based on severity
@@ -338,19 +408,38 @@ Note: Start the server first with: grafema server start
338
408
  exitCode = 0; // Success (maybe warnings)
339
409
  }
340
410
  } catch (e) {
341
- // Orchestrator threw (fatal error stopped analysis)
342
- const error = e instanceof Error ? e : new Error(String(e));
343
411
  const diagnostics = orchestrator.getDiagnostics();
344
412
  const reporter = new DiagnosticReporter(diagnostics);
345
413
 
346
- console.error('');
347
- console.error(`✗ Analysis failed: ${error.message}`);
348
- console.error('');
349
- console.error('→ Run with --debug for detailed diagnostics');
414
+ // Clear progress line in interactive mode
415
+ if (renderer && process.stdout.isTTY) {
416
+ process.stdout.write('\r\x1b[K');
417
+ }
350
418
 
351
- if (diagnostics.count() > 0) {
419
+ // Check if this is a strict mode failure (REG-332: structured output)
420
+ if (e instanceof StrictModeFailure) {
421
+ // Format ONLY from diagnostics, not from error.message
422
+ console.error('');
423
+ console.error(`✗ Strict mode: ${e.count} unresolved reference(s) found during ENRICHMENT.`);
424
+ console.error('');
425
+ console.error(reporter.formatStrict(e.diagnostics, {
426
+ verbose: options.verbose,
427
+ suppressedCount: e.suppressedCount, // REG-332
428
+ }));
429
+ console.error('');
430
+ console.error('Run without --strict for graceful degradation, or fix the underlying issues.');
431
+ } else {
432
+ // Generic error handling (non-strict)
433
+ const error = e instanceof Error ? e : new Error(String(e));
352
434
  console.error('');
353
- console.error(reporter.report({ format: 'text', includeSummary: true }));
435
+ console.error(`✗ Analysis failed: ${error.message}`);
436
+ console.error('');
437
+ console.error('→ Run with --debug for detailed diagnostics');
438
+
439
+ if (diagnostics.count() > 0) {
440
+ console.error('');
441
+ console.error(reporter.report({ format: 'text', includeSummary: true }));
442
+ }
354
443
  }
355
444
 
356
445
  // Write diagnostics.log in debug mode even on failure
@@ -361,13 +450,19 @@ Note: Start the server first with: grafema server start
361
450
  }
362
451
 
363
452
  exitCode = 1;
364
- }
453
+ } finally {
454
+ // Stop stats polling
455
+ if (statsInterval) {
456
+ clearInterval(statsInterval);
457
+ statsInterval = null;
458
+ }
365
459
 
366
- await backend.close();
460
+ if (backend.connected) {
461
+ await backend.close();
462
+ }
367
463
 
368
- // Exit with appropriate code
369
- // 0 = success, 1 = fatal, 2 = errors
370
- if (exitCode !== 0) {
371
- process.exit(exitCode);
464
+ // Exit with appropriate code
465
+ // 0 = success, 1 = fatal, 2 = errors
466
+ exitWithCode(exitCode);
372
467
  }
373
468
  });
@@ -12,69 +12,34 @@ import { existsSync, readFileSync } from 'fs';
12
12
  import {
13
13
  RFDBServerBackend,
14
14
  GuaranteeManager,
15
- NodeCreationValidator,
16
15
  GraphFreshnessChecker,
17
- IncrementalReanalyzer
16
+ IncrementalReanalyzer,
17
+ DIAGNOSTIC_CATEGORIES,
18
18
  } from '@grafema/core';
19
- import type { GuaranteeGraph } from '@grafema/core';
20
- import type { GraphBackend } from '@grafema/types';
19
+ import type { GuaranteeGraph, DiagnosticCategoryKey } from '@grafema/core';
21
20
  import { exitWithError } from '../utils/errorFormatter.js';
22
21
 
23
- interface GuaranteeFile {
24
- guarantees: Array<{
25
- id: string;
26
- name: string;
27
- rule: string;
28
- severity?: 'error' | 'warning' | 'info';
29
- governs?: string[];
30
- }>;
31
- }
32
22
 
33
23
  // Available built-in validators
34
- const BUILT_IN_VALIDATORS: Record<string, { name: string; description: string }> = {
35
- 'node-creation': {
36
- name: 'NodeCreationValidator',
37
- description: 'Validates that all nodes are created through NodeFactory'
38
- }
24
+ // Add new validators here as they are implemented
25
+ const BUILT_IN_VALIDATORS: Record<string, { name: string; description: string; create: () => unknown }> = {
26
+ // Example:
27
+ // 'my-validator': {
28
+ // name: 'MyValidator',
29
+ // description: 'Validates something important',
30
+ // create: () => new MyValidator()
31
+ // }
39
32
  };
40
33
 
41
- // Category definition for diagnostic filtering
42
- export interface DiagnosticCheckCategory {
43
- name: string;
44
- description: string;
45
- codes: string[];
46
- }
47
-
48
- // Available diagnostic categories
49
- export const CHECK_CATEGORIES: Record<string, DiagnosticCheckCategory> = {
50
- 'connectivity': {
51
- name: 'Graph Connectivity',
52
- description: 'Check for disconnected nodes in the graph',
53
- codes: ['ERR_DISCONNECTED_NODES', 'ERR_DISCONNECTED_NODE'],
54
- },
55
- 'calls': {
56
- name: 'Call Resolution',
57
- description: 'Check for unresolved function calls',
58
- codes: ['ERR_UNRESOLVED_CALL'],
59
- },
60
- 'dataflow': {
61
- name: 'Data Flow',
62
- description: 'Check for missing assignments and broken references',
63
- codes: ['ERR_MISSING_ASSIGNMENT', 'ERR_BROKEN_REFERENCE', 'ERR_NO_LEAF_NODE'],
64
- },
65
- 'imports': {
66
- name: 'Import Validation',
67
- description: 'Check for broken imports and undefined symbols',
68
- codes: ['ERR_BROKEN_IMPORT', 'ERR_UNDEFINED_SYMBOL'],
69
- },
70
- };
34
+ // Re-export for backward compatibility (deprecated - import from @grafema/core instead)
35
+ export { DIAGNOSTIC_CATEGORIES as CHECK_CATEGORIES };
71
36
 
72
37
  export const checkCommand = new Command('check')
73
38
  .description('Check invariants/guarantees')
74
39
  .argument('[rule]', 'Specific rule ID to check (or "all" for all rules)')
75
40
  .option('-p, --project <path>', 'Project path', '.')
76
41
  .option('-f, --file <path>', 'Path to guarantees YAML file')
77
- .option('-g, --guarantee <name>', 'Run a built-in guarantee validator (e.g., node-creation)')
42
+ .option('-g, --guarantee <name>', 'Run a built-in guarantee validator')
78
43
  .option('-j, --json', 'Output results as JSON')
79
44
  .option('-q, --quiet', 'Only output failures')
80
45
  .option('--list-guarantees', 'List available built-in guarantees')
@@ -88,7 +53,7 @@ Examples:
88
53
  grafema check calls Check call resolution
89
54
  grafema check dataflow Check data flow integrity
90
55
  grafema check all Run all diagnostic categories
91
- grafema check --guarantee node-creation Run built-in validator
56
+ grafema check --guarantee <name> Run built-in validator
92
57
  grafema check --list-categories List available categories
93
58
  grafema check --list-guarantees List built-in guarantees
94
59
  grafema check --fail-on-stale CI mode: fail if graph is stale
@@ -113,7 +78,7 @@ Examples:
113
78
  if (options.listCategories) {
114
79
  console.log('Available diagnostic categories:');
115
80
  console.log('');
116
- for (const [key, category] of Object.entries(CHECK_CATEGORIES)) {
81
+ for (const [key, category] of Object.entries(DIAGNOSTIC_CATEGORIES)) {
117
82
  console.log(` ${key}`);
118
83
  console.log(` ${category.name}`);
119
84
  console.log(` ${category.description}`);
@@ -136,7 +101,7 @@ Examples:
136
101
  }
137
102
 
138
103
  // Check if rule argument is a category name
139
- if (rule && (rule in CHECK_CATEGORIES || rule === 'all')) {
104
+ if (rule && (rule in DIAGNOSTIC_CATEGORIES || rule === 'all')) {
140
105
  await runCategoryCheck(rule, options);
141
106
  return;
142
107
  }
@@ -366,27 +331,29 @@ async function runBuiltInValidator(
366
331
  }
367
332
 
368
333
  try {
369
- let validator;
370
- let validatorName: string;
371
-
372
- switch (guaranteeName) {
373
- case 'node-creation':
374
- validator = new NodeCreationValidator();
375
- validatorName = 'NodeCreationValidator';
376
- break;
377
- default:
334
+ const validatorInfo = BUILT_IN_VALIDATORS[guaranteeName];
335
+ if (!validatorInfo) {
336
+ const available = Object.keys(BUILT_IN_VALIDATORS);
337
+ if (available.length === 0) {
378
338
  exitWithError(`Unknown guarantee: ${guaranteeName}`, [
379
- 'Use --list-guarantees to see available options'
339
+ 'No built-in guarantees are currently available'
380
340
  ]);
341
+ }
342
+ exitWithError(`Unknown guarantee: ${guaranteeName}`, [
343
+ `Available: ${available.join(', ')}`
344
+ ]);
381
345
  }
382
346
 
347
+ const validator = validatorInfo.create() as { execute: (ctx: { graph: unknown; projectPath: string }) => Promise<{ metadata?: unknown }> };
348
+ const validatorName = validatorInfo.name;
349
+
383
350
  if (!options.quiet) {
384
351
  console.log(`Running ${validatorName}...`);
385
352
  console.log('');
386
353
  }
387
354
 
388
355
  const result = await validator.execute({
389
- graph: backend as unknown as GraphBackend,
356
+ graph: backend,
390
357
  projectPath: resolvedPath
391
358
  });
392
359
 
@@ -422,7 +389,7 @@ async function runBuiltInValidator(
422
389
  console.log('');
423
390
 
424
391
  for (const issue of issues.slice(0, 10)) {
425
- const location = issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ''}` : '';
392
+ const _location = issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ''}` : '';
426
393
  console.log(` \x1b[31m•\x1b[0m [${issue.type}] ${issue.message}`);
427
394
  if (issue.suggestion && !options.quiet) {
428
395
  console.log(` Suggestion: ${issue.suggestion}`);
@@ -470,7 +437,7 @@ async function runCategoryCheck(
470
437
  .map(line => {
471
438
  try {
472
439
  return JSON.parse(line);
473
- } catch (e) {
440
+ } catch {
474
441
  return null;
475
442
  }
476
443
  })
@@ -479,12 +446,13 @@ async function runCategoryCheck(
479
446
  // Filter diagnostics by category codes
480
447
  let filteredDiagnostics = allDiagnostics;
481
448
  if (category !== 'all') {
482
- const categoryInfo = CHECK_CATEGORIES[category];
483
- if (!categoryInfo) {
449
+ if (!(category in DIAGNOSTIC_CATEGORIES)) {
484
450
  exitWithError(`Unknown category: ${category}`, [
485
451
  'Use --list-categories to see available options'
486
452
  ]);
487
453
  }
454
+ const categoryKey = category as DiagnosticCategoryKey;
455
+ const categoryInfo = DIAGNOSTIC_CATEGORIES[categoryKey];
488
456
  filteredDiagnostics = allDiagnostics.filter((d: any) =>
489
457
  categoryInfo.codes.includes(d.code)
490
458
  );
@@ -499,7 +467,7 @@ async function runCategoryCheck(
499
467
  } else {
500
468
  const categoryName = category === 'all'
501
469
  ? 'All Categories'
502
- : CHECK_CATEGORIES[category].name;
470
+ : DIAGNOSTIC_CATEGORIES[category as DiagnosticCategoryKey].name;
503
471
 
504
472
  if (!options.quiet) {
505
473
  console.log(`Checking ${categoryName}...`);
@@ -159,8 +159,8 @@ export async function checkConfigValidity(
159
159
  return {
160
160
  name: 'config',
161
161
  status: 'warn',
162
- message: `Unknown plugin(s): ${unknownPlugins.join(', ')}`,
163
- recommendation: 'Check plugin names for typos. Run: grafema doctor --verbose for available plugins',
162
+ message: `Plugin(s) not found: ${unknownPlugins.join(', ')} (will be skipped during analysis)`,
163
+ recommendation: 'Check plugin names for typos or add custom plugins to .grafema/plugins/. Run: grafema doctor --verbose for available plugins',
164
164
  details: { unknownPlugins },
165
165
  };
166
166
  }
@@ -357,10 +357,11 @@ export async function checkGraphStats(
357
357
  },
358
358
  };
359
359
  } catch (err) {
360
+ const message = err instanceof Error ? err.message : String(err);
360
361
  return {
361
362
  name: 'graph_stats',
362
363
  status: 'warn',
363
- message: `Could not read graph stats: ${(err as Error).message}`,
364
+ message: `Could not read graph stats: ${message}`,
364
365
  };
365
366
  }
366
367
  }
@@ -496,10 +497,11 @@ export async function checkConnectivity(
496
497
  details: { unreachableCount, percentage, byType },
497
498
  };
498
499
  } catch (err) {
500
+ const message = err instanceof Error ? err.message : String(err);
499
501
  return {
500
502
  name: 'connectivity',
501
503
  status: 'warn',
502
- message: `Could not check connectivity: ${(err as Error).message}`,
504
+ message: `Could not check connectivity: ${message}`,
503
505
  };
504
506
  }
505
507
  }
@@ -547,10 +549,11 @@ export async function checkFreshness(
547
549
  },
548
550
  };
549
551
  } catch (err) {
552
+ const message = err instanceof Error ? err.message : String(err);
550
553
  return {
551
554
  name: 'freshness',
552
555
  status: 'warn',
553
- message: `Could not check freshness: ${(err as Error).message}`,
556
+ message: `Could not check freshness: ${message}`,
554
557
  };
555
558
  }
556
559
  }