@grafema/cli 0.1.1-alpha → 0.2.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.
- package/dist/cli.js +10 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +69 -11
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +177 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +7 -0
- package/dist/commands/doctor/checks.d.ts +55 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -0
- package/dist/commands/doctor/checks.js +534 -0
- package/dist/commands/doctor/output.d.ts +20 -0
- package/dist/commands/doctor/output.d.ts.map +1 -0
- package/dist/commands/doctor/output.js +94 -0
- package/dist/commands/doctor/types.d.ts +42 -0
- package/dist/commands/doctor/types.d.ts.map +1 -0
- package/dist/commands/doctor/types.js +4 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +80 -0
- package/dist/commands/explain.d.ts +16 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +145 -0
- package/dist/commands/explore.d.ts +7 -1
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +204 -85
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +16 -4
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +48 -50
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +93 -15
- package/dist/commands/ls.d.ts +14 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +132 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +15 -2
- package/dist/commands/query.d.ts +98 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +549 -136
- package/dist/commands/schema.d.ts +13 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +279 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -6
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +7 -0
- package/dist/commands/trace.d.ts +73 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +500 -5
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +79 -0
- package/dist/utils/formatNode.d.ts +13 -0
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +35 -2
- package/package.json +3 -3
- package/src/cli.ts +10 -0
- package/src/commands/analyze.ts +84 -9
- package/src/commands/check.ts +201 -0
- package/src/commands/coverage.ts +7 -0
- package/src/commands/doctor/checks.ts +612 -0
- package/src/commands/doctor/output.ts +115 -0
- package/src/commands/doctor/types.ts +45 -0
- package/src/commands/doctor.ts +106 -0
- package/src/commands/explain.ts +173 -0
- package/src/commands/explore.tsx +247 -97
- package/src/commands/get.ts +20 -6
- package/src/commands/impact.ts +55 -61
- package/src/commands/init.ts +101 -14
- package/src/commands/ls.ts +166 -0
- package/src/commands/overview.ts +15 -2
- package/src/commands/query.ts +643 -149
- package/src/commands/schema.ts +345 -0
- package/src/commands/server.ts +13 -6
- package/src/commands/stats.ts +7 -0
- package/src/commands/trace.ts +647 -6
- package/src/commands/types.ts +94 -0
- package/src/utils/formatNode.ts +42 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatNode.d.ts","sourceRoot":"","sources":["../../src/utils/formatNode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"formatNode.d.ts","sourceRoot":"","sources":["../../src/utils/formatNode.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAyBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAoB3F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,EAAE,MAAM,GAClB,MAAM,CAIR"}
|
package/dist/utils/formatNode.js
CHANGED
|
@@ -5,6 +5,38 @@
|
|
|
5
5
|
* Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
|
|
6
6
|
*/
|
|
7
7
|
import { relative } from 'path';
|
|
8
|
+
/**
|
|
9
|
+
* Get the display name for a node based on its type.
|
|
10
|
+
*
|
|
11
|
+
* HTTP nodes use method + path/url instead of name.
|
|
12
|
+
* Other nodes use their name field.
|
|
13
|
+
*/
|
|
14
|
+
export function getNodeDisplayName(node) {
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case 'http:route':
|
|
17
|
+
// Express routes: "GET /users"
|
|
18
|
+
if (node.method && node.path) {
|
|
19
|
+
return `${node.method} ${node.path}`;
|
|
20
|
+
}
|
|
21
|
+
break;
|
|
22
|
+
case 'http:request':
|
|
23
|
+
// Fetch/axios requests: "POST /api/data"
|
|
24
|
+
if (node.method && node.url) {
|
|
25
|
+
return `${node.method} ${node.url}`;
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
// Default: use name, but guard against JSON metadata corruption
|
|
30
|
+
if (node.name && !node.name.startsWith('{')) {
|
|
31
|
+
return node.name;
|
|
32
|
+
}
|
|
33
|
+
// Fallback: extract name from semantic ID if possible
|
|
34
|
+
const parts = node.id.split('#');
|
|
35
|
+
if (parts.length > 1) {
|
|
36
|
+
return parts[1]; // Usually contains the key identifier
|
|
37
|
+
}
|
|
38
|
+
return node.id;
|
|
39
|
+
}
|
|
8
40
|
/**
|
|
9
41
|
* Format a node for primary display (multi-line)
|
|
10
42
|
*
|
|
@@ -16,8 +48,9 @@ import { relative } from 'path';
|
|
|
16
48
|
export function formatNodeDisplay(node, options) {
|
|
17
49
|
const { projectPath, showLocation = true, indent = '' } = options;
|
|
18
50
|
const lines = [];
|
|
19
|
-
// Line 1: [TYPE] name
|
|
20
|
-
|
|
51
|
+
// Line 1: [TYPE] display name (type-specific)
|
|
52
|
+
const displayName = getNodeDisplayName(node);
|
|
53
|
+
lines.push(`${indent}[${node.type}] ${displayName}`);
|
|
21
54
|
// Line 2: ID (semantic ID)
|
|
22
55
|
lines.push(`${indent} ID: ${node.id}`);
|
|
23
56
|
// Line 3: Location (optional)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafema/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1-beta",
|
|
4
4
|
"description": "CLI for Grafema code analysis toolkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"ink-text-input": "^6.0.0",
|
|
38
38
|
"react": "^19.2.3",
|
|
39
39
|
"yaml": "^2.8.2",
|
|
40
|
-
"@grafema/core": "0.
|
|
41
|
-
"@grafema/types": "0.
|
|
40
|
+
"@grafema/core": "0.2.0-beta",
|
|
41
|
+
"@grafema/types": "0.2.0-beta"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^25.0.8",
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { initCommand } from './commands/init.js';
|
|
|
8
8
|
import { analyzeCommand } from './commands/analyze.js';
|
|
9
9
|
import { overviewCommand } from './commands/overview.js';
|
|
10
10
|
import { queryCommand } from './commands/query.js';
|
|
11
|
+
import { typesCommand } from './commands/types.js';
|
|
12
|
+
import { lsCommand } from './commands/ls.js';
|
|
11
13
|
import { getCommand } from './commands/get.js';
|
|
12
14
|
import { traceCommand } from './commands/trace.js';
|
|
13
15
|
import { impactCommand } from './commands/impact.js';
|
|
@@ -16,6 +18,9 @@ import { statsCommand } from './commands/stats.js';
|
|
|
16
18
|
import { checkCommand } from './commands/check.js';
|
|
17
19
|
import { serverCommand } from './commands/server.js';
|
|
18
20
|
import { coverageCommand } from './commands/coverage.js';
|
|
21
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
22
|
+
import { schemaCommand } from './commands/schema.js';
|
|
23
|
+
import { explainCommand } from './commands/explain.js';
|
|
19
24
|
|
|
20
25
|
const program = new Command();
|
|
21
26
|
|
|
@@ -29,6 +34,8 @@ program.addCommand(initCommand);
|
|
|
29
34
|
program.addCommand(analyzeCommand);
|
|
30
35
|
program.addCommand(overviewCommand);
|
|
31
36
|
program.addCommand(queryCommand);
|
|
37
|
+
program.addCommand(typesCommand);
|
|
38
|
+
program.addCommand(lsCommand);
|
|
32
39
|
program.addCommand(getCommand);
|
|
33
40
|
program.addCommand(traceCommand);
|
|
34
41
|
program.addCommand(impactCommand);
|
|
@@ -37,5 +44,8 @@ program.addCommand(statsCommand); // Keep for backwards compat
|
|
|
37
44
|
program.addCommand(coverageCommand);
|
|
38
45
|
program.addCommand(checkCommand);
|
|
39
46
|
program.addCommand(serverCommand);
|
|
47
|
+
program.addCommand(doctorCommand);
|
|
48
|
+
program.addCommand(schemaCommand);
|
|
49
|
+
program.addCommand(explainCommand);
|
|
40
50
|
|
|
41
51
|
program.parse();
|
package/src/commands/analyze.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { resolve, join } from 'path';
|
|
7
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync } from 'fs';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
8
9
|
import {
|
|
9
10
|
Orchestrator,
|
|
10
11
|
RFDBServerBackend,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
// Analysis
|
|
25
26
|
JSASTAnalyzer,
|
|
26
27
|
ExpressRouteAnalyzer,
|
|
28
|
+
ExpressResponseAnalyzer,
|
|
27
29
|
SocketIOAnalyzer,
|
|
28
30
|
DatabaseAnalyzer,
|
|
29
31
|
FetchAnalyzer,
|
|
@@ -32,12 +34,14 @@ import {
|
|
|
32
34
|
RustAnalyzer,
|
|
33
35
|
// Enrichment
|
|
34
36
|
MethodCallResolver,
|
|
37
|
+
ArgumentParameterLinker,
|
|
35
38
|
AliasTracker,
|
|
36
39
|
ValueDomainAnalyzer,
|
|
37
40
|
MountPointResolver,
|
|
38
41
|
PrefixEvaluator,
|
|
39
42
|
InstanceOfResolver,
|
|
40
43
|
ImportExportLinker,
|
|
44
|
+
FunctionCallResolver,
|
|
41
45
|
HTTPConnectionEnricher,
|
|
42
46
|
RustFFIEnricher,
|
|
43
47
|
// Validation
|
|
@@ -48,6 +52,7 @@ import {
|
|
|
48
52
|
GraphConnectivityValidator,
|
|
49
53
|
DataFlowValidator,
|
|
50
54
|
TypeScriptDeadCodeValidator,
|
|
55
|
+
BrokenImportValidator,
|
|
51
56
|
} from '@grafema/core';
|
|
52
57
|
import type { LogLevel } from '@grafema/types';
|
|
53
58
|
|
|
@@ -62,6 +67,7 @@ const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
|
|
|
62
67
|
// Analysis
|
|
63
68
|
JSASTAnalyzer: () => new JSASTAnalyzer() as Plugin,
|
|
64
69
|
ExpressRouteAnalyzer: () => new ExpressRouteAnalyzer() as Plugin,
|
|
70
|
+
ExpressResponseAnalyzer: () => new ExpressResponseAnalyzer() as Plugin,
|
|
65
71
|
SocketIOAnalyzer: () => new SocketIOAnalyzer() as Plugin,
|
|
66
72
|
DatabaseAnalyzer: () => new DatabaseAnalyzer() as Plugin,
|
|
67
73
|
FetchAnalyzer: () => new FetchAnalyzer() as Plugin,
|
|
@@ -70,12 +76,14 @@ const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
|
|
|
70
76
|
RustAnalyzer: () => new RustAnalyzer() as Plugin,
|
|
71
77
|
// Enrichment
|
|
72
78
|
MethodCallResolver: () => new MethodCallResolver() as Plugin,
|
|
79
|
+
ArgumentParameterLinker: () => new ArgumentParameterLinker() as Plugin,
|
|
73
80
|
AliasTracker: () => new AliasTracker() as Plugin,
|
|
74
81
|
ValueDomainAnalyzer: () => new ValueDomainAnalyzer() as Plugin,
|
|
75
82
|
MountPointResolver: () => new MountPointResolver() as Plugin,
|
|
76
83
|
PrefixEvaluator: () => new PrefixEvaluator() as Plugin,
|
|
77
84
|
InstanceOfResolver: () => new InstanceOfResolver() as Plugin,
|
|
78
85
|
ImportExportLinker: () => new ImportExportLinker() as Plugin,
|
|
86
|
+
FunctionCallResolver: () => new FunctionCallResolver() as Plugin,
|
|
79
87
|
HTTPConnectionEnricher: () => new HTTPConnectionEnricher() as Plugin,
|
|
80
88
|
RustFFIEnricher: () => new RustFFIEnricher() as Plugin,
|
|
81
89
|
// Validation
|
|
@@ -86,16 +94,63 @@ const BUILTIN_PLUGINS: Record<string, () => Plugin> = {
|
|
|
86
94
|
GraphConnectivityValidator: () => new GraphConnectivityValidator() as Plugin,
|
|
87
95
|
DataFlowValidator: () => new DataFlowValidator() as Plugin,
|
|
88
96
|
TypeScriptDeadCodeValidator: () => new TypeScriptDeadCodeValidator() as Plugin,
|
|
97
|
+
BrokenImportValidator: () => new BrokenImportValidator() as Plugin,
|
|
89
98
|
};
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Load custom plugins from .grafema/plugins/ directory
|
|
102
|
+
*/
|
|
103
|
+
async function loadCustomPlugins(
|
|
104
|
+
projectPath: string,
|
|
105
|
+
log: (msg: string) => void
|
|
106
|
+
): Promise<Record<string, () => Plugin>> {
|
|
107
|
+
const pluginsDir = join(projectPath, '.grafema', 'plugins');
|
|
108
|
+
if (!existsSync(pluginsDir)) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const customPlugins: Record<string, () => Plugin> = {};
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const files = readdirSync(pluginsDir).filter(
|
|
116
|
+
(f) => f.endsWith('.js') || f.endsWith('.mjs')
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
try {
|
|
121
|
+
const pluginPath = join(pluginsDir, file);
|
|
122
|
+
const pluginUrl = pathToFileURL(pluginPath).href;
|
|
123
|
+
const module = await import(pluginUrl);
|
|
124
|
+
|
|
125
|
+
const PluginClass = module.default || module[file.replace(/\.(m?js)$/, '')];
|
|
126
|
+
if (PluginClass && typeof PluginClass === 'function') {
|
|
127
|
+
const pluginName = PluginClass.name || file.replace(/\.(m?js)$/, '');
|
|
128
|
+
customPlugins[pluginName] = () => new PluginClass() as Plugin;
|
|
129
|
+
log(`Loaded custom plugin: ${pluginName}`);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn(`Failed to load plugin ${file}: ${(err as Error).message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn(`Error loading custom plugins: ${(err as Error).message}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return customPlugins;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createPlugins(
|
|
143
|
+
config: GrafemaConfig['plugins'],
|
|
144
|
+
customPlugins: Record<string, () => Plugin> = {}
|
|
145
|
+
): Plugin[] {
|
|
92
146
|
const plugins: Plugin[] = [];
|
|
93
147
|
const phases: (keyof GrafemaConfig['plugins'])[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
|
|
94
148
|
|
|
95
149
|
for (const phase of phases) {
|
|
96
150
|
const names = config[phase] || [];
|
|
97
151
|
for (const name of names) {
|
|
98
|
-
|
|
152
|
+
// Check built-in first, then custom
|
|
153
|
+
const factory = BUILTIN_PLUGINS[name] || customPlugins[name];
|
|
99
154
|
if (factory) {
|
|
100
155
|
plugins.push(factory());
|
|
101
156
|
} else {
|
|
@@ -133,7 +188,18 @@ export const analyzeCommand = new Command('analyze')
|
|
|
133
188
|
.option('-v, --verbose', 'Show verbose logging')
|
|
134
189
|
.option('--debug', 'Enable debug mode (writes diagnostics.log)')
|
|
135
190
|
.option('--log-level <level>', 'Set log level (silent, errors, warnings, info, debug)')
|
|
136
|
-
.
|
|
191
|
+
.option('--strict', 'Enable strict mode (fail on unresolved references)')
|
|
192
|
+
.addHelpText('after', `
|
|
193
|
+
Examples:
|
|
194
|
+
grafema analyze Analyze current project
|
|
195
|
+
grafema analyze ./my-project Analyze specific directory
|
|
196
|
+
grafema analyze --clear Clear database and rebuild from scratch
|
|
197
|
+
grafema analyze -s api Analyze only "api" service (monorepo)
|
|
198
|
+
grafema analyze -v Verbose output with progress details
|
|
199
|
+
grafema analyze --debug Write diagnostics.log for debugging
|
|
200
|
+
grafema analyze --strict Fail on unresolved references (debugging)
|
|
201
|
+
`)
|
|
202
|
+
.action(async (path: string, options: { service?: string; entrypoint?: string; clear?: boolean; quiet?: boolean; verbose?: boolean; debug?: boolean; logLevel?: string; strict?: boolean }) => {
|
|
137
203
|
const projectPath = resolve(path);
|
|
138
204
|
const grafemaDir = join(projectPath, '.grafema');
|
|
139
205
|
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
@@ -169,10 +235,18 @@ export const analyzeCommand = new Command('analyze')
|
|
|
169
235
|
}
|
|
170
236
|
}
|
|
171
237
|
|
|
172
|
-
|
|
238
|
+
// Load custom plugins from .grafema/plugins/
|
|
239
|
+
const customPlugins = await loadCustomPlugins(projectPath, log);
|
|
240
|
+
const plugins = createPlugins(config.plugins, customPlugins);
|
|
173
241
|
|
|
174
242
|
log(`Loaded ${plugins.length} plugins`);
|
|
175
243
|
|
|
244
|
+
// Resolve strict mode: CLI flag overrides config
|
|
245
|
+
const strictMode = options.strict ?? config.strict ?? false;
|
|
246
|
+
if (strictMode) {
|
|
247
|
+
log('Strict mode enabled - analysis will fail on unresolved references');
|
|
248
|
+
}
|
|
249
|
+
|
|
176
250
|
const startTime = Date.now();
|
|
177
251
|
|
|
178
252
|
const orchestrator = new Orchestrator({
|
|
@@ -183,6 +257,7 @@ export const analyzeCommand = new Command('analyze')
|
|
|
183
257
|
forceAnalysis: options.clear || false,
|
|
184
258
|
logger,
|
|
185
259
|
services: config.services.length > 0 ? config.services : undefined, // Pass config services (REG-174)
|
|
260
|
+
strictMode, // REG-330: Pass strict mode flag
|
|
186
261
|
onProgress: (progress) => {
|
|
187
262
|
if (options.verbose) {
|
|
188
263
|
log(`[${progress.phase}] ${progress.message}`);
|
|
@@ -211,7 +286,7 @@ export const analyzeCommand = new Command('analyze')
|
|
|
211
286
|
// Print summary if there are any issues
|
|
212
287
|
if (diagnostics.count() > 0) {
|
|
213
288
|
log('');
|
|
214
|
-
log(reporter.
|
|
289
|
+
log(reporter.categorizedSummary());
|
|
215
290
|
|
|
216
291
|
// In verbose mode, print full report
|
|
217
292
|
if (options.verbose) {
|
|
@@ -220,10 +295,10 @@ export const analyzeCommand = new Command('analyze')
|
|
|
220
295
|
}
|
|
221
296
|
}
|
|
222
297
|
|
|
223
|
-
//
|
|
298
|
+
// Always write diagnostics.log (required for `grafema check` command)
|
|
299
|
+
const writer = new DiagnosticWriter();
|
|
300
|
+
await writer.write(diagnostics, grafemaDir);
|
|
224
301
|
if (options.debug) {
|
|
225
|
-
const writer = new DiagnosticWriter();
|
|
226
|
-
await writer.write(diagnostics, grafemaDir);
|
|
227
302
|
log(`Diagnostics written to ${writer.getLogPath(grafemaDir)}`);
|
|
228
303
|
}
|
|
229
304
|
|
package/src/commands/check.ts
CHANGED
|
@@ -38,6 +38,37 @@ const BUILT_IN_VALIDATORS: Record<string, { name: string; description: string }>
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
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
|
+
};
|
|
71
|
+
|
|
41
72
|
export const checkCommand = new Command('check')
|
|
42
73
|
.description('Check invariants/guarantees')
|
|
43
74
|
.argument('[rule]', 'Specific rule ID to check (or "all" for all rules)')
|
|
@@ -47,8 +78,22 @@ export const checkCommand = new Command('check')
|
|
|
47
78
|
.option('-j, --json', 'Output results as JSON')
|
|
48
79
|
.option('-q, --quiet', 'Only output failures')
|
|
49
80
|
.option('--list-guarantees', 'List available built-in guarantees')
|
|
81
|
+
.option('--list-categories', 'List available diagnostic categories')
|
|
50
82
|
.option('--skip-reanalysis', 'Skip automatic reanalysis of stale modules')
|
|
51
83
|
.option('--fail-on-stale', 'Exit with error if stale modules found (CI mode)')
|
|
84
|
+
.addHelpText('after', `
|
|
85
|
+
Examples:
|
|
86
|
+
grafema check Run all guarantee checks
|
|
87
|
+
grafema check connectivity Check graph connectivity
|
|
88
|
+
grafema check calls Check call resolution
|
|
89
|
+
grafema check dataflow Check data flow integrity
|
|
90
|
+
grafema check all Run all diagnostic categories
|
|
91
|
+
grafema check --guarantee node-creation Run built-in validator
|
|
92
|
+
grafema check --list-categories List available categories
|
|
93
|
+
grafema check --list-guarantees List built-in guarantees
|
|
94
|
+
grafema check --fail-on-stale CI mode: fail if graph is stale
|
|
95
|
+
grafema check -q Only show failures (quiet mode)
|
|
96
|
+
`)
|
|
52
97
|
.action(
|
|
53
98
|
async (
|
|
54
99
|
rule: string | undefined,
|
|
@@ -59,10 +104,25 @@ export const checkCommand = new Command('check')
|
|
|
59
104
|
json?: boolean;
|
|
60
105
|
quiet?: boolean;
|
|
61
106
|
listGuarantees?: boolean;
|
|
107
|
+
listCategories?: boolean;
|
|
62
108
|
skipReanalysis?: boolean;
|
|
63
109
|
failOnStale?: boolean;
|
|
64
110
|
}
|
|
65
111
|
) => {
|
|
112
|
+
// List available categories
|
|
113
|
+
if (options.listCategories) {
|
|
114
|
+
console.log('Available diagnostic categories:');
|
|
115
|
+
console.log('');
|
|
116
|
+
for (const [key, category] of Object.entries(CHECK_CATEGORIES)) {
|
|
117
|
+
console.log(` ${key}`);
|
|
118
|
+
console.log(` ${category.name}`);
|
|
119
|
+
console.log(` ${category.description}`);
|
|
120
|
+
console.log(` Usage: grafema check ${key}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
66
126
|
// List available guarantees
|
|
67
127
|
if (options.listGuarantees) {
|
|
68
128
|
console.log('Available built-in guarantees:');
|
|
@@ -75,6 +135,12 @@ export const checkCommand = new Command('check')
|
|
|
75
135
|
return;
|
|
76
136
|
}
|
|
77
137
|
|
|
138
|
+
// Check if rule argument is a category name
|
|
139
|
+
if (rule && (rule in CHECK_CATEGORIES || rule === 'all')) {
|
|
140
|
+
await runCategoryCheck(rule, options);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
78
144
|
// Run built-in guarantee validator
|
|
79
145
|
if (options.guarantee) {
|
|
80
146
|
const validatorInfo = BUILT_IN_VALIDATORS[options.guarantee];
|
|
@@ -377,3 +443,138 @@ async function runBuiltInValidator(
|
|
|
377
443
|
await backend.close();
|
|
378
444
|
}
|
|
379
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Run category-based diagnostic check
|
|
449
|
+
*/
|
|
450
|
+
async function runCategoryCheck(
|
|
451
|
+
category: string,
|
|
452
|
+
options: { project: string; json?: boolean; quiet?: boolean }
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
const resolvedPath = resolve(options.project);
|
|
455
|
+
const grafemaDir = join(resolvedPath, '.grafema');
|
|
456
|
+
const diagnosticsLogPath = join(grafemaDir, 'diagnostics.log');
|
|
457
|
+
|
|
458
|
+
if (!existsSync(diagnosticsLogPath)) {
|
|
459
|
+
exitWithError('No diagnostics found', [
|
|
460
|
+
'Run: grafema analyze',
|
|
461
|
+
'Diagnostics are collected during analysis'
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Read diagnostics from log file (JSON lines format)
|
|
466
|
+
const diagnosticsContent = readFileSync(diagnosticsLogPath, 'utf-8');
|
|
467
|
+
const allDiagnostics = diagnosticsContent
|
|
468
|
+
.split('\n')
|
|
469
|
+
.filter(line => line.trim())
|
|
470
|
+
.map(line => {
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(line);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
.filter(Boolean);
|
|
478
|
+
|
|
479
|
+
// Filter diagnostics by category codes
|
|
480
|
+
let filteredDiagnostics = allDiagnostics;
|
|
481
|
+
if (category !== 'all') {
|
|
482
|
+
const categoryInfo = CHECK_CATEGORIES[category];
|
|
483
|
+
if (!categoryInfo) {
|
|
484
|
+
exitWithError(`Unknown category: ${category}`, [
|
|
485
|
+
'Use --list-categories to see available options'
|
|
486
|
+
]);
|
|
487
|
+
}
|
|
488
|
+
filteredDiagnostics = allDiagnostics.filter((d: any) =>
|
|
489
|
+
categoryInfo.codes.includes(d.code)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (options.json) {
|
|
494
|
+
console.log(JSON.stringify({
|
|
495
|
+
category: category,
|
|
496
|
+
total: filteredDiagnostics.length,
|
|
497
|
+
diagnostics: filteredDiagnostics
|
|
498
|
+
}, null, 2));
|
|
499
|
+
} else {
|
|
500
|
+
const categoryName = category === 'all'
|
|
501
|
+
? 'All Categories'
|
|
502
|
+
: CHECK_CATEGORIES[category].name;
|
|
503
|
+
|
|
504
|
+
if (!options.quiet) {
|
|
505
|
+
console.log(`Checking ${categoryName}...`);
|
|
506
|
+
console.log('');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (filteredDiagnostics.length === 0) {
|
|
510
|
+
console.log('\x1b[32m✓\x1b[0m No issues found');
|
|
511
|
+
} else {
|
|
512
|
+
console.log(`\x1b[33m⚠\x1b[0m Found ${filteredDiagnostics.length} diagnostic(s):`);
|
|
513
|
+
console.log('');
|
|
514
|
+
|
|
515
|
+
// Group by severity
|
|
516
|
+
const errors = filteredDiagnostics.filter((d: any) => d.severity === 'error' || d.severity === 'fatal');
|
|
517
|
+
const warnings = filteredDiagnostics.filter((d: any) => d.severity === 'warning');
|
|
518
|
+
const infos = filteredDiagnostics.filter((d: any) => d.severity === 'info');
|
|
519
|
+
|
|
520
|
+
// Display errors first
|
|
521
|
+
if (errors.length > 0) {
|
|
522
|
+
console.log(`\x1b[31mErrors (${errors.length}):\x1b[0m`);
|
|
523
|
+
for (const diag of errors.slice(0, 10)) {
|
|
524
|
+
const location = diag.file ? `${diag.file}${diag.line ? `:${diag.line}` : ''}` : '';
|
|
525
|
+
console.log(` \x1b[31m•\x1b[0m [${diag.code}] ${diag.message}`);
|
|
526
|
+
if (location) {
|
|
527
|
+
console.log(` ${location}`);
|
|
528
|
+
}
|
|
529
|
+
if (diag.suggestion && !options.quiet) {
|
|
530
|
+
console.log(` Suggestion: ${diag.suggestion}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (errors.length > 10) {
|
|
534
|
+
console.log(` ... and ${errors.length - 10} more errors`);
|
|
535
|
+
}
|
|
536
|
+
console.log('');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Display warnings
|
|
540
|
+
if (warnings.length > 0) {
|
|
541
|
+
console.log(`\x1b[33mWarnings (${warnings.length}):\x1b[0m`);
|
|
542
|
+
for (const diag of warnings.slice(0, 10)) {
|
|
543
|
+
const location = diag.file ? `${diag.file}${diag.line ? `:${diag.line}` : ''}` : '';
|
|
544
|
+
console.log(` \x1b[33m•\x1b[0m [${diag.code}] ${diag.message}`);
|
|
545
|
+
if (location) {
|
|
546
|
+
console.log(` ${location}`);
|
|
547
|
+
}
|
|
548
|
+
if (diag.suggestion && !options.quiet) {
|
|
549
|
+
console.log(` Suggestion: ${diag.suggestion}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (warnings.length > 10) {
|
|
553
|
+
console.log(` ... and ${warnings.length - 10} more warnings`);
|
|
554
|
+
}
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Display infos
|
|
559
|
+
if (infos.length > 0 && !options.quiet) {
|
|
560
|
+
console.log(`\x1b[36mInfo (${infos.length}):\x1b[0m`);
|
|
561
|
+
for (const diag of infos.slice(0, 5)) {
|
|
562
|
+
const location = diag.file ? `${diag.file}${diag.line ? `:${diag.line}` : ''}` : '';
|
|
563
|
+
console.log(` \x1b[36m•\x1b[0m [${diag.code}] ${diag.message}`);
|
|
564
|
+
if (location) {
|
|
565
|
+
console.log(` ${location}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (infos.length > 5) {
|
|
569
|
+
console.log(` ... and ${infos.length - 5} more info messages`);
|
|
570
|
+
}
|
|
571
|
+
console.log('');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
console.log('');
|
|
576
|
+
if (filteredDiagnostics.some((d: any) => d.severity === 'error' || d.severity === 'fatal')) {
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
package/src/commands/coverage.ts
CHANGED
|
@@ -18,6 +18,13 @@ export const coverageCommand = new Command('coverage')
|
|
|
18
18
|
.option('-p, --project <path>', 'Project path', '.')
|
|
19
19
|
.option('-j, --json', 'Output as JSON')
|
|
20
20
|
.option('-v, --verbose', 'Show detailed file lists')
|
|
21
|
+
.addHelpText('after', `
|
|
22
|
+
Examples:
|
|
23
|
+
grafema coverage Show coverage summary
|
|
24
|
+
grafema coverage --verbose Show detailed file lists
|
|
25
|
+
grafema coverage --json Output coverage as JSON
|
|
26
|
+
grafema coverage -p ./app Coverage for specific project
|
|
27
|
+
`)
|
|
21
28
|
.action(async (options: { project: string; json?: boolean; verbose?: boolean }) => {
|
|
22
29
|
const projectPath = resolve(options.project);
|
|
23
30
|
const grafemaDir = join(projectPath, '.grafema');
|