@girardelli/architect 1.1.0

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/CONTRIBUTING.md +140 -0
  2. package/LICENSE +21 -0
  3. package/PROJECT_STRUCTURE.txt +168 -0
  4. package/README.md +269 -0
  5. package/dist/analyzer.d.ts +17 -0
  6. package/dist/analyzer.d.ts.map +1 -0
  7. package/dist/analyzer.js +254 -0
  8. package/dist/analyzer.js.map +1 -0
  9. package/dist/anti-patterns.d.ts +17 -0
  10. package/dist/anti-patterns.d.ts.map +1 -0
  11. package/dist/anti-patterns.js +211 -0
  12. package/dist/anti-patterns.js.map +1 -0
  13. package/dist/cli.d.ts +15 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +164 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +6 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +73 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/diagram.d.ts +9 -0
  22. package/dist/diagram.d.ts.map +1 -0
  23. package/dist/diagram.js +116 -0
  24. package/dist/diagram.js.map +1 -0
  25. package/dist/html-reporter.d.ts +23 -0
  26. package/dist/html-reporter.d.ts.map +1 -0
  27. package/dist/html-reporter.js +454 -0
  28. package/dist/html-reporter.js.map +1 -0
  29. package/dist/index.d.ts +48 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +151 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/reporter.d.ts +13 -0
  34. package/dist/reporter.d.ts.map +1 -0
  35. package/dist/reporter.js +135 -0
  36. package/dist/reporter.js.map +1 -0
  37. package/dist/scanner.d.ts +25 -0
  38. package/dist/scanner.d.ts.map +1 -0
  39. package/dist/scanner.js +288 -0
  40. package/dist/scanner.js.map +1 -0
  41. package/dist/scorer.d.ts +15 -0
  42. package/dist/scorer.d.ts.map +1 -0
  43. package/dist/scorer.js +172 -0
  44. package/dist/scorer.js.map +1 -0
  45. package/dist/types.d.ts +106 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +2 -0
  48. package/dist/types.js.map +1 -0
  49. package/examples/sample-report.md +207 -0
  50. package/jest.config.js +18 -0
  51. package/package.json +70 -0
  52. package/src/analyzer.ts +310 -0
  53. package/src/anti-patterns.ts +264 -0
  54. package/src/cli.ts +183 -0
  55. package/src/config.ts +82 -0
  56. package/src/diagram.ts +144 -0
  57. package/src/html-reporter.ts +485 -0
  58. package/src/index.ts +212 -0
  59. package/src/reporter.ts +166 -0
  60. package/src/scanner.ts +298 -0
  61. package/src/scorer.ts +193 -0
  62. package/src/types.ts +114 -0
  63. package/tests/anti-patterns.test.ts +94 -0
  64. package/tests/scanner.test.ts +55 -0
  65. package/tests/scorer.test.ts +80 -0
  66. package/tsconfig.json +24 -0
package/src/cli.ts ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Architect CLI
5
+ * Executa análise arquitetural e gera relatórios em múltiplos formatos
6
+ *
7
+ * Uso:
8
+ * npx architect analyze ./src
9
+ * npx architect analyze ./src --format html --output report.html
10
+ * npx architect diagram ./src
11
+ * npx architect score ./src
12
+ * npx architect anti-patterns ./src
13
+ * npx architect layers ./src
14
+ */
15
+
16
+ import { architect } from './index.js';
17
+ import { ReportGenerator } from './reporter.js';
18
+ import { HtmlReportGenerator } from './html-reporter.js';
19
+ import { writeFileSync } from 'fs';
20
+ import { resolve, basename } from 'path';
21
+
22
+ type OutputFormat = 'json' | 'markdown' | 'html';
23
+
24
+ interface CliOptions {
25
+ command: string;
26
+ path: string;
27
+ format: OutputFormat;
28
+ output?: string;
29
+ }
30
+
31
+ function parseArgs(args: string[]): CliOptions {
32
+ const command = args[0] || 'analyze';
33
+ const pathArg = args.find((a) => !a.startsWith('--') && a !== command) || '.';
34
+ const formatIdx = args.indexOf('--format');
35
+ const format = (formatIdx >= 0 ? args[formatIdx + 1] : 'html') as OutputFormat;
36
+ const outputIdx = args.indexOf('--output');
37
+ const output = outputIdx >= 0 ? args[outputIdx + 1] : undefined;
38
+
39
+ return { command, path: resolve(pathArg), format, output };
40
+ }
41
+
42
+ function printUsage(): void {
43
+ console.log(`
44
+ 🏗️ Architect — AI-powered architecture analysis
45
+
46
+ Usage:
47
+ architect <command> [path] [options]
48
+
49
+ Commands:
50
+ analyze Full architecture analysis (default)
51
+ diagram Generate architecture diagram only
52
+ score Calculate quality score only
53
+ anti-patterns Detect anti-patterns only
54
+ layers Analyze layer structure only
55
+
56
+ Options:
57
+ --format <type> Output format: html, json, markdown (default: html)
58
+ --output <file> Output file path (default: architect-report.<ext>)
59
+ --help Show this help message
60
+
61
+ Examples:
62
+ architect analyze ./src
63
+ architect analyze ./src --format html --output report.html
64
+ architect score ./src --format json
65
+ architect anti-patterns ./backend/src
66
+ `);
67
+ }
68
+
69
+ async function main(): Promise<void> {
70
+ const args = process.argv.slice(2);
71
+
72
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
73
+ printUsage();
74
+ process.exit(0);
75
+ }
76
+
77
+ const options = parseArgs(args);
78
+
79
+ console.log('🏗️ Architect — Architecture Analysis');
80
+ console.log(`📂 Path: ${options.path}`);
81
+ console.log(`📋 Command: ${options.command}`);
82
+ console.log(`📄 Format: ${options.format}\n`);
83
+
84
+ try {
85
+ switch (options.command) {
86
+ case 'analyze': {
87
+ const report = await architect.analyze(options.path);
88
+ const projectName = report.projectInfo.name || basename(options.path);
89
+
90
+ if (options.format === 'html') {
91
+ const htmlGenerator = new HtmlReportGenerator();
92
+ const html = htmlGenerator.generateHtml(report);
93
+ const outputPath = options.output || `architect-report-${projectName}.html`;
94
+ writeFileSync(outputPath, html);
95
+ console.log(`✅ HTML report saved to: ${outputPath}`);
96
+ console.log(`📊 Score: ${report.score.overall}/100`);
97
+ console.log(`⚠️ Anti-patterns: ${report.antiPatterns.length}`);
98
+ } else if (options.format === 'markdown') {
99
+ const mdGenerator = new ReportGenerator();
100
+ const markdown = mdGenerator.generateMarkdownReport(report);
101
+ const outputPath = options.output || `architect-report-${projectName}.md`;
102
+ writeFileSync(outputPath, markdown);
103
+ console.log(`✅ Markdown report saved to: ${outputPath}`);
104
+ } else {
105
+ const outputPath = options.output || `architect-report-${projectName}.json`;
106
+ writeFileSync(outputPath, JSON.stringify(report, null, 2));
107
+ console.log(`✅ JSON report saved to: ${outputPath}`);
108
+ }
109
+
110
+ // Print summary to console
111
+ console.log(`\n═══════════════════════════════════════`);
112
+ console.log(` SCORE: ${report.score.overall}/100`);
113
+ console.log(`═══════════════════════════════════════`);
114
+ console.log(`├─ Modularity: ${report.score.breakdown.modularity}`);
115
+ console.log(`├─ Coupling: ${report.score.breakdown.coupling}`);
116
+ console.log(`├─ Cohesion: ${report.score.breakdown.cohesion}`);
117
+ console.log(`└─ Layering: ${report.score.breakdown.layering}`);
118
+ console.log(`\n📁 Files: ${report.projectInfo.totalFiles} | 📝 Lines: ${report.projectInfo.totalLines.toLocaleString()}`);
119
+ console.log(`⚠️ Anti-patterns: ${report.antiPatterns.length}`);
120
+ break;
121
+ }
122
+
123
+ case 'diagram': {
124
+ const diagram = await architect.diagram(options.path);
125
+ if (options.output) {
126
+ writeFileSync(options.output, diagram);
127
+ console.log(`✅ Diagram saved to: ${options.output}`);
128
+ } else {
129
+ console.log(diagram);
130
+ }
131
+ break;
132
+ }
133
+
134
+ case 'score': {
135
+ const score = await architect.score(options.path);
136
+ if (options.format === 'json') {
137
+ console.log(JSON.stringify(score, null, 2));
138
+ } else {
139
+ console.log(`Score: ${score.overall}/100`);
140
+ for (const [name, value] of Object.entries(score.breakdown)) {
141
+ console.log(` ${name}: ${value}/100`);
142
+ }
143
+ }
144
+ break;
145
+ }
146
+
147
+ case 'anti-patterns': {
148
+ const patterns = await architect.antiPatterns(options.path);
149
+ if (options.format === 'json') {
150
+ console.log(JSON.stringify(patterns, null, 2));
151
+ } else {
152
+ console.log(`Found ${patterns.length} anti-pattern(s):\n`);
153
+ for (const p of patterns) {
154
+ console.log(` [${p.severity}] ${p.name}: ${p.description}`);
155
+ }
156
+ }
157
+ break;
158
+ }
159
+
160
+ case 'layers': {
161
+ const layers = await architect.layers(options.path);
162
+ if (options.format === 'json') {
163
+ console.log(JSON.stringify(layers, null, 2));
164
+ } else {
165
+ for (const l of layers) {
166
+ console.log(`${l.name}: ${l.files.length} files`);
167
+ }
168
+ }
169
+ break;
170
+ }
171
+
172
+ default:
173
+ console.error(`❌ Unknown command: ${options.command}`);
174
+ printUsage();
175
+ process.exit(1);
176
+ }
177
+ } catch (error) {
178
+ console.error('❌ Error:', error instanceof Error ? error.message : error);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ main();
package/src/config.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { ArchitectConfig } from './types.js';
4
+
5
+ const DEFAULT_CONFIG: ArchitectConfig = {
6
+ ignore: [
7
+ 'node_modules',
8
+ 'dist',
9
+ 'build',
10
+ 'coverage',
11
+ '.git',
12
+ '.next',
13
+ 'venv',
14
+ '__pycache__',
15
+ 'target',
16
+ ],
17
+ frameworks: {
18
+ detect: true,
19
+ },
20
+ antiPatterns: {
21
+ godClass: {
22
+ linesThreshold: 500,
23
+ methodsThreshold: 10,
24
+ },
25
+ shotgunSurgery: {
26
+ changePropagationThreshold: 5,
27
+ },
28
+ },
29
+ score: {
30
+ modularity: 0.4,
31
+ coupling: 0.25,
32
+ cohesion: 0.2,
33
+ layering: 0.15,
34
+ },
35
+ };
36
+
37
+ export class ConfigLoader {
38
+ static loadConfig(projectPath: string): ArchitectConfig {
39
+ const configPath = join(projectPath, '.architect.json');
40
+
41
+ try {
42
+ const content = readFileSync(configPath, 'utf-8');
43
+ const userConfig = JSON.parse(content) as ArchitectConfig;
44
+ return this.mergeConfigs(DEFAULT_CONFIG, userConfig);
45
+ } catch {
46
+ return DEFAULT_CONFIG;
47
+ }
48
+ }
49
+
50
+ private static mergeConfigs(
51
+ defaults: ArchitectConfig,
52
+ user: ArchitectConfig
53
+ ): ArchitectConfig {
54
+ return {
55
+ ignore: user.ignore ?? defaults.ignore,
56
+ frameworks: {
57
+ detect: user.frameworks?.detect ?? defaults.frameworks?.detect,
58
+ },
59
+ antiPatterns: {
60
+ godClass: {
61
+ linesThreshold:
62
+ user.antiPatterns?.godClass?.linesThreshold ??
63
+ defaults.antiPatterns?.godClass?.linesThreshold,
64
+ methodsThreshold:
65
+ user.antiPatterns?.godClass?.methodsThreshold ??
66
+ defaults.antiPatterns?.godClass?.methodsThreshold,
67
+ },
68
+ shotgunSurgery: {
69
+ changePropagationThreshold:
70
+ user.antiPatterns?.shotgunSurgery?.changePropagationThreshold ??
71
+ defaults.antiPatterns?.shotgunSurgery?.changePropagationThreshold,
72
+ },
73
+ },
74
+ score: {
75
+ modularity: user.score?.modularity ?? defaults.score?.modularity,
76
+ coupling: user.score?.coupling ?? defaults.score?.coupling,
77
+ cohesion: user.score?.cohesion ?? defaults.score?.cohesion,
78
+ layering: user.score?.layering ?? defaults.score?.layering,
79
+ },
80
+ };
81
+ }
82
+ }
package/src/diagram.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { DependencyEdge, Layer } from './types.js';
2
+
3
+ export class DiagramGenerator {
4
+ generateComponentDiagram(
5
+ edges: DependencyEdge[],
6
+ layers: Layer[]
7
+ ): string {
8
+ const nodes = new Set<string>();
9
+ edges.forEach((e) => {
10
+ nodes.add(e.from);
11
+ nodes.add(e.to);
12
+ });
13
+
14
+ const nodeColors: Record<string, string> = {};
15
+ for (const layer of layers) {
16
+ for (const file of layer.files) {
17
+ const moduleName = file.split('/').pop() || file;
18
+ const colorMap: Record<string, string> = {
19
+ API: '#FFB6C1',
20
+ Service: '#87CEEB',
21
+ Data: '#90EE90',
22
+ UI: '#FFD700',
23
+ Infrastructure: '#D3D3D3',
24
+ };
25
+ nodeColors[moduleName] = colorMap[layer.name] || '#FFFFFF';
26
+ }
27
+ }
28
+
29
+ let mermaid = 'graph TB\n';
30
+
31
+ for (const node of Array.from(nodes).slice(0, 20)) {
32
+ const moduleName = node.split('/').pop() || node;
33
+ const color = nodeColors[moduleName] || '#FFFFFF';
34
+ mermaid += ` ${this.sanitizeNodeName(node)}["${moduleName}"]:::${this.getStyleClass(color)}\n`;
35
+ }
36
+
37
+ mermaid += '\n';
38
+
39
+ for (const edge of edges.slice(0, 30)) {
40
+ mermaid += ` ${this.sanitizeNodeName(edge.from)} --> ${this.sanitizeNodeName(edge.to)}\n`;
41
+ }
42
+
43
+ mermaid += '\n';
44
+ mermaid += ' classDef apiStyle fill:#FFB6C1,stroke:#333,color:#000\n';
45
+ mermaid += ' classDef serviceStyle fill:#87CEEB,stroke:#333,color:#000\n';
46
+ mermaid += ' classDef dataStyle fill:#90EE90,stroke:#333,color:#000\n';
47
+ mermaid += ' classDef uiStyle fill:#FFD700,stroke:#333,color:#000\n';
48
+ mermaid += ' classDef infraStyle fill:#D3D3D3,stroke:#333,color:#000\n';
49
+
50
+ return mermaid;
51
+ }
52
+
53
+ generateLayerDiagram(layers: Layer[]): string {
54
+ let mermaid = 'graph LR\n';
55
+
56
+ const layerOrder = ['UI', 'API', 'Service', 'Data', 'Infrastructure'];
57
+ const colorMap: Record<string, string> = {
58
+ UI: 'uiStyle',
59
+ API: 'apiStyle',
60
+ Service: 'serviceStyle',
61
+ Data: 'dataStyle',
62
+ Infrastructure: 'infraStyle',
63
+ };
64
+
65
+ for (const layerName of layerOrder) {
66
+ const layer = layers.find((l) => l.name === layerName);
67
+ if (layer && layer.files.length > 0) {
68
+ const nodeId = layerName.replace(/\s+/g, '_');
69
+ const fileCount = layer.files.length;
70
+ mermaid += ` ${nodeId}["${layerName}<br/>(${fileCount} files)"]:::${colorMap[layerName]}\n`;
71
+ }
72
+ }
73
+
74
+ for (let i = 0; i < layerOrder.length - 1; i++) {
75
+ const from = layerOrder[i].replace(/\s+/g, '_');
76
+ const to = layerOrder[i + 1].replace(/\s+/g, '_');
77
+ mermaid += ` ${from} --> ${to}\n`;
78
+ }
79
+
80
+ mermaid += '\n';
81
+ mermaid += ' classDef apiStyle fill:#FFB6C1,stroke:#333,color:#000\n';
82
+ mermaid += ' classDef serviceStyle fill:#87CEEB,stroke:#333,color:#000\n';
83
+ mermaid += ' classDef dataStyle fill:#90EE90,stroke:#333,color:#000\n';
84
+ mermaid += ' classDef uiStyle fill:#FFD700,stroke:#333,color:#000\n';
85
+ mermaid += ' classDef infraStyle fill:#D3D3D3,stroke:#333,color:#000\n';
86
+
87
+ return mermaid;
88
+ }
89
+
90
+ generateDependencyFlowDiagram(edges: DependencyEdge[]): string {
91
+ const flowMap: Record<string, number> = {};
92
+
93
+ for (const edge of edges) {
94
+ const flowKey = `${edge.from} -> ${edge.to}`;
95
+ flowMap[flowKey] = (flowMap[flowKey] || 0) + edge.weight;
96
+ }
97
+
98
+ const topFlows = Object.entries(flowMap)
99
+ .sort((a, b) => b[1] - a[1])
100
+ .slice(0, 15);
101
+
102
+ let mermaid = 'graph LR\n';
103
+
104
+ const nodes = new Set<string>();
105
+ for (const [flow] of topFlows) {
106
+ const [from, _, to] = flow.split(' ');
107
+ nodes.add(from);
108
+ nodes.add(to);
109
+ }
110
+
111
+ for (const node of nodes) {
112
+ const moduleName = node.split('/').pop() || node;
113
+ mermaid += ` ${this.sanitizeNodeName(node)}["${moduleName}"]\n`;
114
+ }
115
+
116
+ mermaid += '\n';
117
+
118
+ for (const [flow, weight] of topFlows) {
119
+ const [from, _, to] = flow.split(' ');
120
+ const label = weight > 1 ? ` | ${weight}` : '';
121
+ mermaid += ` ${this.sanitizeNodeName(from)} -->|${label}| ${this.sanitizeNodeName(to)}\n`;
122
+ }
123
+
124
+ return mermaid;
125
+ }
126
+
127
+ private sanitizeNodeName(path: string): string {
128
+ return path
129
+ .replace(/[^a-zA-Z0-9]/g, '_')
130
+ .replace(/_+/g, '_')
131
+ .toLowerCase();
132
+ }
133
+
134
+ private getStyleClass(color: string): string {
135
+ const classMap: Record<string, string> = {
136
+ '#FFB6C1': 'apiStyle',
137
+ '#87CEEB': 'serviceStyle',
138
+ '#90EE90': 'dataStyle',
139
+ '#FFD700': 'uiStyle',
140
+ '#D3D3D3': 'infraStyle',
141
+ };
142
+ return classMap[color] || 'defaultStyle';
143
+ }
144
+ }