@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.
- package/README.md +73 -0
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +136 -52
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/check.d.ts +2 -6
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +32 -46
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/coverage.js +1 -0
- package/dist/commands/coverage.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +9 -5
- package/dist/commands/doctor/checks.js.map +1 -0
- package/dist/commands/doctor/output.js +1 -0
- package/dist/commands/doctor/output.js.map +1 -0
- package/dist/commands/doctor/types.js +1 -0
- package/dist/commands/doctor/types.js.map +1 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.js +1 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +9 -4
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +7 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/impact.js +1 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -1
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +7 -0
- package/dist/commands/ls.js.map +1 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +1 -0
- package/dist/commands/overview.js.map +1 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +68 -1
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/schema.js +1 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/server.d.ts +2 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +50 -3
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/stats.js +1 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/trace.js +1 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/utils/codePreview.js +1 -0
- package/dist/utils/codePreview.js.map +1 -0
- package/dist/utils/errorFormatter.js +1 -0
- package/dist/utils/errorFormatter.js.map +1 -0
- package/dist/utils/formatNode.js +1 -0
- package/dist/utils/formatNode.js.map +1 -0
- package/dist/utils/progressRenderer.d.ts +119 -0
- package/dist/utils/progressRenderer.d.ts.map +1 -0
- package/dist/utils/progressRenderer.js +245 -0
- package/dist/utils/progressRenderer.js.map +1 -0
- package/dist/utils/spinner.d.ts +39 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +84 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +5 -4
- package/src/commands/analyze.ts +150 -55
- package/src/commands/check.ts +36 -68
- package/src/commands/doctor/checks.ts +8 -5
- package/src/commands/explore.tsx +8 -4
- package/src/commands/get.ts +8 -0
- package/src/commands/impact.ts +1 -1
- package/src/commands/init.ts +6 -2
- package/src/commands/ls.ts +8 -0
- package/src/commands/overview.ts +0 -4
- package/src/commands/query.ts +77 -1
- package/src/commands/server.ts +57 -3
- package/src/utils/progressRenderer.ts +288 -0
- package/src/utils/spinner.ts +94 -0
package/src/commands/analyze.ts
CHANGED
|
@@ -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(/\.
|
|
146
|
+
const PluginClass = module.default || module[file.replace(/\.[cm]?js$/, '')];
|
|
126
147
|
if (PluginClass && typeof PluginClass === 'function') {
|
|
127
|
-
const pluginName = PluginClass.name || file.replace(/\.
|
|
128
|
-
customPlugins[pluginName] = () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ('
|
|
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 '
|
|
178
|
-
return '
|
|
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
|
-
|
|
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
|
|
257
|
+
const logFile = options.logFile ? resolve(options.logFile) : undefined;
|
|
258
|
+
const logger = createLogger(logLevel, logFile ? { logFile } : undefined);
|
|
220
259
|
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
302
|
-
const stats = await backend
|
|
367
|
+
const elapsedSeconds = (Date.now() - startTime) / 1000;
|
|
368
|
+
const stats = await fetchNodeEdgeCounts(backend);
|
|
303
369
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
316
|
-
|
|
385
|
+
info('');
|
|
386
|
+
info(reporter.categorizedSummary());
|
|
317
387
|
|
|
318
388
|
// In verbose mode, print full report
|
|
319
389
|
if (options.verbose) {
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
460
|
+
if (backend.connected) {
|
|
461
|
+
await backend.close();
|
|
462
|
+
}
|
|
367
463
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
process.exit(exitCode);
|
|
464
|
+
// Exit with appropriate code
|
|
465
|
+
// 0 = success, 1 = fatal, 2 = errors
|
|
466
|
+
exitWithCode(exitCode);
|
|
372
467
|
}
|
|
373
468
|
});
|
package/src/commands/check.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
42
|
-
export
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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: `
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
556
|
+
message: `Could not check freshness: ${message}`,
|
|
554
557
|
};
|
|
555
558
|
}
|
|
556
559
|
}
|