@grafema/cli 0.1.1-alpha

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 (66) hide show
  1. package/LICENSE +190 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.d.ts.map +1 -0
  7. package/dist/commands/analyze.js +209 -0
  8. package/dist/commands/check.d.ts +10 -0
  9. package/dist/commands/check.d.ts.map +1 -0
  10. package/dist/commands/check.js +295 -0
  11. package/dist/commands/coverage.d.ts +11 -0
  12. package/dist/commands/coverage.d.ts.map +1 -0
  13. package/dist/commands/coverage.js +96 -0
  14. package/dist/commands/explore.d.ts +6 -0
  15. package/dist/commands/explore.d.ts.map +1 -0
  16. package/dist/commands/explore.js +633 -0
  17. package/dist/commands/get.d.ts +10 -0
  18. package/dist/commands/get.d.ts.map +1 -0
  19. package/dist/commands/get.js +189 -0
  20. package/dist/commands/impact.d.ts +10 -0
  21. package/dist/commands/impact.d.ts.map +1 -0
  22. package/dist/commands/impact.js +313 -0
  23. package/dist/commands/init.d.ts +6 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +94 -0
  26. package/dist/commands/overview.d.ts +6 -0
  27. package/dist/commands/overview.d.ts.map +1 -0
  28. package/dist/commands/overview.js +91 -0
  29. package/dist/commands/query.d.ts +13 -0
  30. package/dist/commands/query.d.ts.map +1 -0
  31. package/dist/commands/query.js +340 -0
  32. package/dist/commands/server.d.ts +11 -0
  33. package/dist/commands/server.d.ts.map +1 -0
  34. package/dist/commands/server.js +300 -0
  35. package/dist/commands/stats.d.ts +6 -0
  36. package/dist/commands/stats.d.ts.map +1 -0
  37. package/dist/commands/stats.js +52 -0
  38. package/dist/commands/trace.d.ts +10 -0
  39. package/dist/commands/trace.d.ts.map +1 -0
  40. package/dist/commands/trace.js +270 -0
  41. package/dist/utils/codePreview.d.ts +28 -0
  42. package/dist/utils/codePreview.d.ts.map +1 -0
  43. package/dist/utils/codePreview.js +51 -0
  44. package/dist/utils/errorFormatter.d.ts +24 -0
  45. package/dist/utils/errorFormatter.d.ts.map +1 -0
  46. package/dist/utils/errorFormatter.js +32 -0
  47. package/dist/utils/formatNode.d.ts +53 -0
  48. package/dist/utils/formatNode.d.ts.map +1 -0
  49. package/dist/utils/formatNode.js +49 -0
  50. package/package.json +54 -0
  51. package/src/cli.ts +41 -0
  52. package/src/commands/analyze.ts +271 -0
  53. package/src/commands/check.ts +379 -0
  54. package/src/commands/coverage.ts +108 -0
  55. package/src/commands/explore.tsx +1056 -0
  56. package/src/commands/get.ts +265 -0
  57. package/src/commands/impact.ts +400 -0
  58. package/src/commands/init.ts +112 -0
  59. package/src/commands/overview.ts +108 -0
  60. package/src/commands/query.ts +425 -0
  61. package/src/commands/server.ts +335 -0
  62. package/src/commands/stats.ts +58 -0
  63. package/src/commands/trace.ts +341 -0
  64. package/src/utils/codePreview.ts +77 -0
  65. package/src/utils/errorFormatter.ts +35 -0
  66. package/src/utils/formatNode.ts +88 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Check command - Check invariants/guarantees
3
+ *
4
+ * Supports two modes:
5
+ * 1. Rule-based: Check YAML-defined guarantees (default)
6
+ * 2. Built-in validators: --guarantee=<name> (e.g., --guarantee=node-creation)
7
+ */
8
+
9
+ import { Command } from 'commander';
10
+ import { resolve, join } from 'path';
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import {
13
+ RFDBServerBackend,
14
+ GuaranteeManager,
15
+ NodeCreationValidator,
16
+ GraphFreshnessChecker,
17
+ IncrementalReanalyzer
18
+ } from '@grafema/core';
19
+ import type { GuaranteeGraph } from '@grafema/core';
20
+ import type { GraphBackend } from '@grafema/types';
21
+ import { exitWithError } from '../utils/errorFormatter.js';
22
+
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
+
33
+ // 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
+ }
39
+ };
40
+
41
+ export const checkCommand = new Command('check')
42
+ .description('Check invariants/guarantees')
43
+ .argument('[rule]', 'Specific rule ID to check (or "all" for all rules)')
44
+ .option('-p, --project <path>', 'Project path', '.')
45
+ .option('-f, --file <path>', 'Path to guarantees YAML file')
46
+ .option('-g, --guarantee <name>', 'Run a built-in guarantee validator (e.g., node-creation)')
47
+ .option('-j, --json', 'Output results as JSON')
48
+ .option('-q, --quiet', 'Only output failures')
49
+ .option('--list-guarantees', 'List available built-in guarantees')
50
+ .option('--skip-reanalysis', 'Skip automatic reanalysis of stale modules')
51
+ .option('--fail-on-stale', 'Exit with error if stale modules found (CI mode)')
52
+ .action(
53
+ async (
54
+ rule: string | undefined,
55
+ options: {
56
+ project: string;
57
+ file?: string;
58
+ guarantee?: string;
59
+ json?: boolean;
60
+ quiet?: boolean;
61
+ listGuarantees?: boolean;
62
+ skipReanalysis?: boolean;
63
+ failOnStale?: boolean;
64
+ }
65
+ ) => {
66
+ // List available guarantees
67
+ if (options.listGuarantees) {
68
+ console.log('Available built-in guarantees:');
69
+ console.log('');
70
+ for (const [key, info] of Object.entries(BUILT_IN_VALIDATORS)) {
71
+ console.log(` ${key}`);
72
+ console.log(` ${info.description}`);
73
+ console.log('');
74
+ }
75
+ return;
76
+ }
77
+
78
+ // Run built-in guarantee validator
79
+ if (options.guarantee) {
80
+ const validatorInfo = BUILT_IN_VALIDATORS[options.guarantee];
81
+ if (!validatorInfo) {
82
+ const available = Object.keys(BUILT_IN_VALIDATORS).join(', ');
83
+ exitWithError(`Unknown guarantee: ${options.guarantee}`, [
84
+ `Available: ${available}`
85
+ ]);
86
+ }
87
+
88
+ await runBuiltInValidator(options.guarantee, options.project, {
89
+ json: options.json,
90
+ quiet: options.quiet,
91
+ skipReanalysis: options.skipReanalysis,
92
+ failOnStale: options.failOnStale
93
+ });
94
+ return;
95
+ }
96
+ const projectPath = resolve(options.project);
97
+ const grafemaDir = join(projectPath, '.grafema');
98
+ const dbPath = join(grafemaDir, 'graph.rfdb');
99
+
100
+ if (!existsSync(dbPath)) {
101
+ exitWithError('No graph database found', ['Run: grafema analyze']);
102
+ }
103
+
104
+ const backend = new RFDBServerBackend({ dbPath });
105
+ await backend.connect();
106
+
107
+ // Check graph freshness
108
+ const freshnessChecker = new GraphFreshnessChecker();
109
+ const freshness = await freshnessChecker.checkFreshness(backend);
110
+
111
+ if (!freshness.isFresh) {
112
+ if (options.failOnStale) {
113
+ console.error(`✗ Graph is stale: ${freshness.staleCount} module(s) changed`);
114
+ for (const stale of freshness.staleModules.slice(0, 5)) {
115
+ console.error(` ${stale.file} (${stale.reason})`);
116
+ }
117
+ if (freshness.staleModules.length > 5) {
118
+ console.error(` ... and ${freshness.staleModules.length - 5} more`);
119
+ }
120
+ console.error('');
121
+ console.error('→ Run: grafema analyze');
122
+ await backend.close();
123
+ process.exit(1);
124
+ }
125
+
126
+ if (!options.skipReanalysis) {
127
+ console.log(`Reanalyzing ${freshness.staleCount} stale module(s)...`);
128
+ const reanalyzer = new IncrementalReanalyzer(backend, projectPath);
129
+ const result = await reanalyzer.reanalyze(freshness.staleModules);
130
+ console.log(`Reanalyzed ${result.modulesReanalyzed} module(s) in ${result.durationMs}ms`);
131
+ console.log('');
132
+ } else {
133
+ console.warn(`Warning: ${freshness.staleCount} stale module(s) detected. Use --skip-reanalysis to suppress.`);
134
+ for (const stale of freshness.staleModules.slice(0, 5)) {
135
+ console.warn(` - ${stale.file} (${stale.reason})`);
136
+ }
137
+ if (freshness.staleModules.length > 5) {
138
+ console.warn(` ... and ${freshness.staleModules.length - 5} more`);
139
+ }
140
+ console.log('');
141
+ }
142
+ } else if (!options.quiet) {
143
+ console.log('Graph is fresh');
144
+ console.log('');
145
+ }
146
+
147
+ try {
148
+ const guaranteeGraph = backend as unknown as GuaranteeGraph;
149
+ const manager = new GuaranteeManager(guaranteeGraph, projectPath);
150
+
151
+ // Load guarantees from file if specified
152
+ const guaranteesFile = options.file || join(grafemaDir, 'guarantees.yaml');
153
+ if (existsSync(guaranteesFile)) {
154
+ await manager.import(guaranteesFile);
155
+ }
156
+
157
+ // Get all guarantees
158
+ const guarantees = await manager.list();
159
+
160
+ if (guarantees.length === 0) {
161
+ console.log('No guarantees found.');
162
+ console.log('');
163
+ console.log('Create guarantees in .grafema/guarantees.yaml or use --file option.');
164
+ return;
165
+ }
166
+
167
+ // Filter to specific rule if requested
168
+ const toCheck =
169
+ rule && rule !== 'all'
170
+ ? guarantees.filter((g) => g.id === rule || g.name === rule)
171
+ : guarantees;
172
+
173
+ if (toCheck.length === 0 && rule) {
174
+ const available = guarantees.map((g) => g.id).join(', ');
175
+ exitWithError(`Guarantee not found: ${rule}`, [
176
+ `Available: ${available}`
177
+ ]);
178
+ }
179
+
180
+ // Check all matching guarantees
181
+ const results = await manager.checkAll();
182
+
183
+ // Filter results to only requested rules
184
+ const filteredResults = rule && rule !== 'all'
185
+ ? {
186
+ ...results,
187
+ results: results.results.filter(
188
+ (r) => toCheck.some((g) => g.id === r.guaranteeId)
189
+ ),
190
+ }
191
+ : results;
192
+
193
+ if (options.json) {
194
+ console.log(JSON.stringify(filteredResults, null, 2));
195
+ } else {
196
+ if (!options.quiet) {
197
+ console.log(`Checking ${filteredResults.results.length} guarantee(s)...`);
198
+ console.log('');
199
+ }
200
+
201
+ for (const result of filteredResults.results) {
202
+ if (options.quiet && result.passed) continue;
203
+
204
+ const status = result.passed ? '✓' : '✗';
205
+ const color = result.passed ? '\x1b[32m' : '\x1b[31m';
206
+ const reset = '\x1b[0m';
207
+
208
+ console.log(`${color}${status}${reset} ${result.guaranteeId}: ${result.name}`);
209
+
210
+ if (!result.passed && result.violations.length > 0) {
211
+ console.log(` Violations (${result.violationCount}):`);
212
+ for (const v of result.violations.slice(0, 10)) {
213
+ // Prefer nodeId (semantic ID) for queryability
214
+ const identifier = v.nodeId || (v.file ? `${v.file}${v.line ? `:${v.line}` : ''}` : '(unknown)');
215
+ console.log(` - ${identifier}`);
216
+ if (v.name || v.type) {
217
+ console.log(` ${v.name || ''} (${v.type || 'unknown'})`);
218
+ }
219
+ }
220
+ if (result.violations.length > 10) {
221
+ console.log(` ... and ${result.violations.length - 10} more`);
222
+ }
223
+ }
224
+
225
+ if (result.error) {
226
+ console.log(` Error: ${result.error}`);
227
+ }
228
+ }
229
+
230
+ console.log('');
231
+ console.log(`Summary: ${filteredResults.passed}/${filteredResults.total} passed`);
232
+
233
+ if (filteredResults.failed > 0) {
234
+ process.exit(1);
235
+ }
236
+ }
237
+ } finally {
238
+ await backend.close();
239
+ }
240
+ }
241
+ );
242
+
243
+ /**
244
+ * Run a built-in validator
245
+ */
246
+ async function runBuiltInValidator(
247
+ guaranteeName: string,
248
+ projectPath: string,
249
+ options: { json?: boolean; quiet?: boolean; skipReanalysis?: boolean; failOnStale?: boolean }
250
+ ): Promise<void> {
251
+ const resolvedPath = resolve(projectPath);
252
+ const grafemaDir = join(resolvedPath, '.grafema');
253
+ const dbPath = join(grafemaDir, 'graph.rfdb');
254
+
255
+ if (!existsSync(dbPath)) {
256
+ exitWithError('No graph database found', ['Run: grafema analyze']);
257
+ }
258
+
259
+ const backend = new RFDBServerBackend({ dbPath });
260
+ await backend.connect();
261
+
262
+ // Check graph freshness
263
+ const freshnessChecker = new GraphFreshnessChecker();
264
+ const freshness = await freshnessChecker.checkFreshness(backend);
265
+
266
+ if (!freshness.isFresh) {
267
+ if (options.failOnStale) {
268
+ console.error(`✗ Graph is stale: ${freshness.staleCount} module(s) changed`);
269
+ for (const stale of freshness.staleModules.slice(0, 5)) {
270
+ console.error(` ${stale.file} (${stale.reason})`);
271
+ }
272
+ if (freshness.staleModules.length > 5) {
273
+ console.error(` ... and ${freshness.staleModules.length - 5} more`);
274
+ }
275
+ console.error('');
276
+ console.error('→ Run: grafema analyze');
277
+ await backend.close();
278
+ process.exit(1);
279
+ }
280
+
281
+ if (!options.skipReanalysis) {
282
+ console.log(`Reanalyzing ${freshness.staleCount} stale module(s)...`);
283
+ const reanalyzer = new IncrementalReanalyzer(backend, resolvedPath);
284
+ const result = await reanalyzer.reanalyze(freshness.staleModules);
285
+ console.log(`Reanalyzed ${result.modulesReanalyzed} module(s) in ${result.durationMs}ms`);
286
+ console.log('');
287
+ } else {
288
+ console.warn(`Warning: ${freshness.staleCount} stale module(s) detected. Use --skip-reanalysis to suppress.`);
289
+ for (const stale of freshness.staleModules.slice(0, 5)) {
290
+ console.warn(` - ${stale.file} (${stale.reason})`);
291
+ }
292
+ if (freshness.staleModules.length > 5) {
293
+ console.warn(` ... and ${freshness.staleModules.length - 5} more`);
294
+ }
295
+ console.log('');
296
+ }
297
+ } else if (!options.quiet) {
298
+ console.log('Graph is fresh');
299
+ console.log('');
300
+ }
301
+
302
+ try {
303
+ let validator;
304
+ let validatorName: string;
305
+
306
+ switch (guaranteeName) {
307
+ case 'node-creation':
308
+ validator = new NodeCreationValidator();
309
+ validatorName = 'NodeCreationValidator';
310
+ break;
311
+ default:
312
+ exitWithError(`Unknown guarantee: ${guaranteeName}`, [
313
+ 'Use --list-guarantees to see available options'
314
+ ]);
315
+ }
316
+
317
+ if (!options.quiet) {
318
+ console.log(`Running ${validatorName}...`);
319
+ console.log('');
320
+ }
321
+
322
+ const result = await validator.execute({
323
+ graph: backend as unknown as GraphBackend,
324
+ projectPath: resolvedPath
325
+ });
326
+
327
+ const metadata = result.metadata as {
328
+ summary?: {
329
+ totalViolations: number;
330
+ [key: string]: unknown;
331
+ };
332
+ issues?: Array<{
333
+ type: string;
334
+ severity: string;
335
+ message: string;
336
+ file?: string;
337
+ line?: number;
338
+ suggestion?: string;
339
+ }>;
340
+ };
341
+
342
+ if (options.json) {
343
+ console.log(JSON.stringify({
344
+ guarantee: guaranteeName,
345
+ passed: (metadata.summary?.totalViolations ?? 0) === 0,
346
+ ...metadata
347
+ }, null, 2));
348
+ } else {
349
+ const violations = metadata.summary?.totalViolations ?? 0;
350
+ const issues = metadata.issues ?? [];
351
+
352
+ if (violations === 0) {
353
+ console.log('\x1b[32m✓\x1b[0m All checks passed');
354
+ } else {
355
+ console.log(`\x1b[31m✗\x1b[0m Found ${violations} violation(s):`);
356
+ console.log('');
357
+
358
+ for (const issue of issues.slice(0, 10)) {
359
+ const location = issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ''}` : '';
360
+ console.log(` \x1b[31m•\x1b[0m [${issue.type}] ${issue.message}`);
361
+ if (issue.suggestion && !options.quiet) {
362
+ console.log(` Suggestion: ${issue.suggestion}`);
363
+ }
364
+ }
365
+
366
+ if (issues.length > 10) {
367
+ console.log(` ... and ${issues.length - 10} more violations`);
368
+ }
369
+ }
370
+
371
+ console.log('');
372
+ if (violations > 0) {
373
+ process.exit(1);
374
+ }
375
+ }
376
+ } finally {
377
+ await backend.close();
378
+ }
379
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Coverage command - Show analysis coverage statistics
3
+ *
4
+ * Shows what percentage of the codebase has been analyzed:
5
+ * - Analyzed: Files in the graph as MODULE nodes
6
+ * - Unsupported: Files with extensions that no indexer handles
7
+ * - Unreachable: Files with supported extensions but not imported from entrypoints
8
+ */
9
+
10
+ import { Command } from 'commander';
11
+ import { resolve, join } from 'path';
12
+ import { existsSync } from 'fs';
13
+ import { RFDBServerBackend, CoverageAnalyzer, type CoverageResult } from '@grafema/core';
14
+ import { exitWithError } from '../utils/errorFormatter.js';
15
+
16
+ export const coverageCommand = new Command('coverage')
17
+ .description('Show analysis coverage statistics')
18
+ .option('-p, --project <path>', 'Project path', '.')
19
+ .option('-j, --json', 'Output as JSON')
20
+ .option('-v, --verbose', 'Show detailed file lists')
21
+ .action(async (options: { project: string; json?: boolean; verbose?: boolean }) => {
22
+ const projectPath = resolve(options.project);
23
+ const grafemaDir = join(projectPath, '.grafema');
24
+ const dbPath = join(grafemaDir, 'graph.rfdb');
25
+
26
+ if (!existsSync(dbPath)) {
27
+ exitWithError('No graph database found', ['Run: grafema analyze']);
28
+ }
29
+
30
+ const backend = new RFDBServerBackend({ dbPath });
31
+ await backend.connect();
32
+
33
+ try {
34
+ const analyzer = new CoverageAnalyzer(backend, projectPath);
35
+ const result = await analyzer.analyze();
36
+
37
+ if (options.json) {
38
+ console.log(JSON.stringify(result, null, 2));
39
+ } else {
40
+ printCoverageReport(result, options.verbose ?? false);
41
+ }
42
+ } finally {
43
+ await backend.close();
44
+ }
45
+ });
46
+
47
+ /**
48
+ * Print human-readable coverage report
49
+ */
50
+ function printCoverageReport(result: CoverageResult, verbose: boolean): void {
51
+ console.log('');
52
+ console.log('Analysis Coverage');
53
+ console.log('=================');
54
+ console.log('');
55
+
56
+ // Summary
57
+ console.log(`Project: ${result.projectPath}`);
58
+ console.log('');
59
+
60
+ // Main statistics
61
+ console.log('File breakdown:');
62
+ console.log(` Total files: ${result.total}`);
63
+ console.log(` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph`);
64
+ console.log(` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available`);
65
+ console.log(` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints`);
66
+
67
+ // Unsupported files breakdown
68
+ if (result.unsupported.count > 0) {
69
+ console.log('');
70
+ console.log('Unsupported files by extension:');
71
+ const sortedExtensions = Object.entries(result.unsupported.byExtension)
72
+ .sort((a, b) => b[1].length - a[1].length);
73
+
74
+ for (const [ext, files] of sortedExtensions) {
75
+ console.log(` ${ext}: ${files.length} files`);
76
+ if (verbose) {
77
+ for (const file of files.slice(0, 10)) {
78
+ console.log(` - ${file}`);
79
+ }
80
+ if (files.length > 10) {
81
+ console.log(` ... and ${files.length - 10} more`);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ // Unreachable files breakdown
88
+ if (result.unreachable.count > 0) {
89
+ console.log('');
90
+ console.log('Unreachable source files:');
91
+ const sortedExtensions = Object.entries(result.unreachable.byExtension)
92
+ .sort((a, b) => b[1].length - a[1].length);
93
+
94
+ for (const [ext, files] of sortedExtensions) {
95
+ console.log(` ${ext}: ${files.length} files - not imported from entrypoints`);
96
+ if (verbose) {
97
+ for (const file of files.slice(0, 10)) {
98
+ console.log(` - ${file}`);
99
+ }
100
+ if (files.length > 10) {
101
+ console.log(` ... and ${files.length - 10} more`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ console.log('');
108
+ }