@girardelli/architect 1.3.0 → 2.2.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 (58) hide show
  1. package/README.md +111 -112
  2. package/dist/agent-generator.d.ts +106 -0
  3. package/dist/agent-generator.d.ts.map +1 -0
  4. package/dist/agent-generator.js +1398 -0
  5. package/dist/agent-generator.js.map +1 -0
  6. package/dist/cli.js +132 -15
  7. package/dist/cli.js.map +1 -1
  8. package/dist/html-reporter.d.ts +8 -2
  9. package/dist/html-reporter.d.ts.map +1 -1
  10. package/dist/html-reporter.js +773 -50
  11. package/dist/html-reporter.js.map +1 -1
  12. package/dist/index.d.ts +26 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +25 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/refactor-engine.d.ts +18 -0
  17. package/dist/refactor-engine.d.ts.map +1 -0
  18. package/dist/refactor-engine.js +86 -0
  19. package/dist/refactor-engine.js.map +1 -0
  20. package/dist/refactor-reporter.d.ts +20 -0
  21. package/dist/refactor-reporter.d.ts.map +1 -0
  22. package/dist/refactor-reporter.js +389 -0
  23. package/dist/refactor-reporter.js.map +1 -0
  24. package/dist/rules/barrel-optimizer.d.ts +13 -0
  25. package/dist/rules/barrel-optimizer.d.ts.map +1 -0
  26. package/dist/rules/barrel-optimizer.js +77 -0
  27. package/dist/rules/barrel-optimizer.js.map +1 -0
  28. package/dist/rules/dead-code-detector.d.ts +21 -0
  29. package/dist/rules/dead-code-detector.d.ts.map +1 -0
  30. package/dist/rules/dead-code-detector.js +117 -0
  31. package/dist/rules/dead-code-detector.js.map +1 -0
  32. package/dist/rules/hub-splitter.d.ts +13 -0
  33. package/dist/rules/hub-splitter.d.ts.map +1 -0
  34. package/dist/rules/hub-splitter.js +110 -0
  35. package/dist/rules/hub-splitter.js.map +1 -0
  36. package/dist/rules/import-organizer.d.ts +13 -0
  37. package/dist/rules/import-organizer.d.ts.map +1 -0
  38. package/dist/rules/import-organizer.js +85 -0
  39. package/dist/rules/import-organizer.js.map +1 -0
  40. package/dist/rules/module-grouper.d.ts +13 -0
  41. package/dist/rules/module-grouper.d.ts.map +1 -0
  42. package/dist/rules/module-grouper.js +110 -0
  43. package/dist/rules/module-grouper.js.map +1 -0
  44. package/dist/types.d.ts +51 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/agent-generator.ts +1526 -0
  48. package/src/cli.ts +150 -15
  49. package/src/html-reporter.ts +799 -51
  50. package/src/index.ts +39 -1
  51. package/src/refactor-engine.ts +117 -0
  52. package/src/refactor-reporter.ts +408 -0
  53. package/src/rules/barrel-optimizer.ts +97 -0
  54. package/src/rules/dead-code-detector.ts +132 -0
  55. package/src/rules/hub-splitter.ts +123 -0
  56. package/src/rules/import-organizer.ts +98 -0
  57. package/src/rules/module-grouper.ts +124 -0
  58. package/src/types.ts +52 -0
package/src/index.ts CHANGED
@@ -5,12 +5,15 @@ import { ArchitectureScorer } from './scorer.js';
5
5
  import { DiagramGenerator } from './diagram.js';
6
6
  import { ReportGenerator } from './reporter.js';
7
7
  import { HtmlReportGenerator } from './html-reporter.js';
8
+ import { RefactorEngine } from './refactor-engine.js';
9
+ import { AgentGenerator, AgentSuggestion } from './agent-generator.js';
8
10
  import { ConfigLoader } from './config.js';
9
- import { AnalysisReport } from './types.js';
11
+ import { AnalysisReport, RefactoringPlan } from './types.js';
10
12
  import { relative } from 'path';
11
13
 
12
14
  export interface ArchitectCommand {
13
15
  analyze: (path: string) => Promise<AnalysisReport>;
16
+ refactor: (report: AnalysisReport, projectPath: string) => RefactoringPlan;
14
17
  diagram: (path: string) => Promise<string>;
15
18
  score: (path: string) => Promise<{ overall: number; breakdown: Record<string, number> }>;
16
19
  antiPatterns: (path: string) => Promise<Array<{ name: string; severity: string; description: string }>>;
@@ -82,6 +85,40 @@ class Architect implements ArchitectCommand {
82
85
  return this.relativizePaths(report, projectPath);
83
86
  }
84
87
 
88
+ /**
89
+ * Generate a refactoring plan from an analysis report.
90
+ * Uses Tier 1 (rule engine) and Tier 2 (AST) transforms.
91
+ */
92
+ refactor(report: AnalysisReport, projectPath: string): RefactoringPlan {
93
+ const engine = new RefactorEngine();
94
+ return engine.analyze(report, projectPath);
95
+ }
96
+
97
+ /**
98
+ * Generate or audit .agent/ directory for a project.
99
+ */
100
+ agents(
101
+ report: AnalysisReport,
102
+ plan: RefactoringPlan,
103
+ projectPath: string,
104
+ outputDir?: string
105
+ ): { generated: string[]; audit: Array<{ type: string; category: string; file: string; description: string; suggestion?: string }> } {
106
+ const generator = new AgentGenerator();
107
+ return generator.generate(report, plan, projectPath, outputDir);
108
+ }
109
+
110
+ /**
111
+ * Suggest agents without writing files — dry-run for unified report.
112
+ */
113
+ suggestAgents(
114
+ report: AnalysisReport,
115
+ plan: RefactoringPlan,
116
+ projectPath: string,
117
+ ): AgentSuggestion {
118
+ const generator = new AgentGenerator();
119
+ return generator.suggest(report, plan, projectPath);
120
+ }
121
+
85
122
  private relativizePaths(report: AnalysisReport, basePath: string): AnalysisReport {
86
123
  const rel = (p: string): string => {
87
124
  if (p.startsWith('/') || p.startsWith('\\')) {
@@ -268,6 +305,7 @@ export {
268
305
  DiagramGenerator,
269
306
  ReportGenerator,
270
307
  HtmlReportGenerator,
308
+ AgentGenerator,
271
309
  ConfigLoader,
272
310
  };
273
311
 
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename, dirname, join, relative } from 'path';
3
+ import {
4
+ AnalysisReport,
5
+ RefactoringPlan,
6
+ RefactorStep,
7
+ RefactorRule,
8
+ FileOperation,
9
+ } from './types.js';
10
+
11
+ // ── Tier 1 Rules ──
12
+ import { HubSplitterRule } from './rules/hub-splitter.js';
13
+ import { BarrelOptimizerRule } from './rules/barrel-optimizer.js';
14
+ import { ImportOrganizerRule } from './rules/import-organizer.js';
15
+ import { ModuleGrouperRule } from './rules/module-grouper.js';
16
+ import { DeadCodeDetectorRule } from './rules/dead-code-detector.js';
17
+
18
+ /**
19
+ * Refactoring Engine v2.0
20
+ * Orchestrates Tier 1 (rule-based) and Tier 2 (AST) refactoring rules.
21
+ */
22
+ export class RefactorEngine {
23
+ private rules: RefactorRule[];
24
+
25
+ constructor() {
26
+ this.rules = [
27
+ // Tier 1: Rule Engine (pattern matching)
28
+ new HubSplitterRule(),
29
+ new BarrelOptimizerRule(),
30
+ new ImportOrganizerRule(),
31
+ new ModuleGrouperRule(),
32
+ new DeadCodeDetectorRule(),
33
+ ];
34
+ }
35
+
36
+ /**
37
+ * Analyze a project and generate a refactoring plan.
38
+ */
39
+ analyze(report: AnalysisReport, projectPath: string): RefactoringPlan {
40
+ const allSteps: RefactorStep[] = [];
41
+ let stepId = 1;
42
+
43
+ // Run each rule
44
+ for (const rule of this.rules) {
45
+ const steps = rule.analyze(report, projectPath);
46
+ for (const step of steps) {
47
+ step.id = stepId++;
48
+ allSteps.push(step);
49
+ }
50
+ }
51
+
52
+ // Sort by priority
53
+ const priorityOrder: Record<string, number> = {
54
+ CRITICAL: 0,
55
+ HIGH: 1,
56
+ MEDIUM: 2,
57
+ LOW: 3,
58
+ };
59
+ allSteps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
60
+
61
+ // Re-number after sorting
62
+ allSteps.forEach((s, i) => (s.id = i + 1));
63
+
64
+ // Calculate total operations
65
+ const totalOperations = allSteps.reduce(
66
+ (sum, s) => sum + s.operations.length,
67
+ 0
68
+ );
69
+
70
+ // Estimate score after refactoring
71
+ const estimatedScoreAfter = this.estimateScoreAfter(report, allSteps);
72
+
73
+ return {
74
+ timestamp: new Date().toISOString(),
75
+ projectPath,
76
+ currentScore: report.score,
77
+ estimatedScoreAfter,
78
+ steps: allSteps,
79
+ totalOperations,
80
+ tier1Steps: allSteps.filter((s) => s.tier === 1).length,
81
+ tier2Steps: allSteps.filter((s) => s.tier === 2).length,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Estimates the architecture score after applying all refactoring steps.
87
+ */
88
+ private estimateScoreAfter(
89
+ report: AnalysisReport,
90
+ steps: RefactorStep[]
91
+ ): { overall: number; breakdown: Record<string, number> } {
92
+ const breakdown = { ...report.score.breakdown };
93
+
94
+ for (const step of steps) {
95
+ for (const impact of step.scoreImpact) {
96
+ if (impact.metric in breakdown) {
97
+ // Use estimated after value, capped at 95
98
+ const key = impact.metric as keyof typeof breakdown;
99
+ breakdown[key] = Math.min(95, Math.max(breakdown[key], impact.after));
100
+ }
101
+ }
102
+ }
103
+
104
+ // Recalculate overall with same weights
105
+ const overall = Math.round(
106
+ breakdown.modularity * 0.4 +
107
+ breakdown.coupling * 0.25 +
108
+ breakdown.cohesion * 0.2 +
109
+ breakdown.layering * 0.15
110
+ );
111
+
112
+ return {
113
+ overall: Math.min(100, overall),
114
+ breakdown,
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,408 @@
1
+ import { RefactoringPlan, RefactorStep, FileOperation } from './types.js';
2
+
3
+ /**
4
+ * Generates interactive HTML report for refactoring plans.
5
+ * Includes step-by-step roadmap, score predictions, and file operation previews.
6
+ */
7
+ export class RefactorReportGenerator {
8
+ generateHtml(plan: RefactoringPlan): string {
9
+ return `<!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>Architect Refactoring Plan</title>
15
+ ${this.getStyles()}
16
+ </head>
17
+ <body>
18
+ ${this.renderHeader(plan)}
19
+ <div class="container">
20
+ ${this.renderScoreComparison(plan)}
21
+ ${this.renderSummaryStats(plan)}
22
+ ${this.renderRoadmap(plan)}
23
+ </div>
24
+ ${this.renderFooter()}
25
+ </body>
26
+ </html>`;
27
+ }
28
+
29
+ private scoreColor(score: number): string {
30
+ if (score >= 70) return '#22c55e';
31
+ if (score >= 50) return '#f59e0b';
32
+ return '#ef4444';
33
+ }
34
+
35
+ private escapeHtml(text: string): string {
36
+ return text
37
+ .replace(/&/g, '&amp;')
38
+ .replace(/</g, '&lt;')
39
+ .replace(/>/g, '&gt;')
40
+ .replace(/"/g, '&quot;');
41
+ }
42
+
43
+ private opColor(type: string): string {
44
+ switch (type) {
45
+ case 'CREATE': return '#22c55e';
46
+ case 'MOVE': return '#3b82f6';
47
+ case 'MODIFY': return '#f59e0b';
48
+ case 'DELETE': return '#ef4444';
49
+ default: return '#64748b';
50
+ }
51
+ }
52
+
53
+ private opIcon(type: string): string {
54
+ switch (type) {
55
+ case 'CREATE': return '➕';
56
+ case 'MOVE': return '📦';
57
+ case 'MODIFY': return '✏️';
58
+ case 'DELETE': return '🗑️';
59
+ default: return '📄';
60
+ }
61
+ }
62
+
63
+ private renderHeader(plan: RefactoringPlan): string {
64
+ const date = new Date(plan.timestamp).toLocaleDateString('en-US', {
65
+ year: 'numeric', month: 'long', day: 'numeric',
66
+ });
67
+ return `
68
+ <div class="header">
69
+ <h1>🔧 Architect — Refactoring Plan</h1>
70
+ <p class="subtitle">Actionable roadmap to improve your architecture</p>
71
+ <div class="meta">
72
+ <span>📂 <strong>${this.escapeHtml(plan.projectPath)}</strong></span>
73
+ <span>📋 <strong>${plan.steps.length}</strong> steps</span>
74
+ <span>🔄 <strong>${plan.totalOperations}</strong> operations</span>
75
+ <span>📅 <strong>${date}</strong></span>
76
+ </div>
77
+ </div>`;
78
+ }
79
+
80
+ private renderScoreComparison(plan: RefactoringPlan): string {
81
+ const current = plan.currentScore;
82
+ const estimated = plan.estimatedScoreAfter;
83
+ const improvement = estimated.overall - current.overall;
84
+
85
+ const metrics = Object.keys(current.breakdown) as Array<keyof typeof current.breakdown>;
86
+ const bars = metrics.map(metric => {
87
+ const before = current.breakdown[metric];
88
+ const after = estimated.breakdown[metric];
89
+ const diff = after - before;
90
+ return `
91
+ <div class="comparison-row">
92
+ <div class="metric-name">${metric}</div>
93
+ <div class="metric-bars">
94
+ <div class="bar-before" style="width: ${before}%; background: ${this.scoreColor(before)}40">
95
+ <span>${before}</span>
96
+ </div>
97
+ <div class="bar-after" style="width: ${after}%; background: ${this.scoreColor(after)}">
98
+ <span>${after}</span>
99
+ </div>
100
+ </div>
101
+ <div class="metric-diff" style="color: ${diff > 0 ? '#22c55e' : '#64748b'}">
102
+ ${diff > 0 ? `+${diff}` : diff === 0 ? '—' : diff}
103
+ </div>
104
+ </div>`;
105
+ }).join('');
106
+
107
+ return `
108
+ <h2 class="section-title">📊 Score Prediction</h2>
109
+ <div class="card score-comparison">
110
+ <div class="score-pair">
111
+ <div class="score-box">
112
+ <div class="score-num" style="color: ${this.scoreColor(current.overall)}">${current.overall}</div>
113
+ <div class="score-label">Current</div>
114
+ </div>
115
+ <div class="score-arrow">
116
+ <svg width="60" height="30" viewBox="0 0 60 30">
117
+ <path d="M5 15 L45 15 M40 8 L48 15 L40 22" stroke="#818cf8" stroke-width="2.5" fill="none"/>
118
+ </svg>
119
+ </div>
120
+ <div class="score-box">
121
+ <div class="score-num" style="color: ${this.scoreColor(estimated.overall)}">${estimated.overall}</div>
122
+ <div class="score-label">Estimated</div>
123
+ </div>
124
+ <div class="score-diff" style="color: #22c55e">+${improvement} pts</div>
125
+ </div>
126
+ <div class="score-bars-section">
127
+ <div class="bars-legend">
128
+ <span class="legend-tag before">Before</span>
129
+ <span class="legend-tag after">After</span>
130
+ </div>
131
+ ${bars}
132
+ </div>
133
+ </div>`;
134
+ }
135
+
136
+ private renderSummaryStats(plan: RefactoringPlan): string {
137
+ const criticalCount = plan.steps.filter(s => s.priority === 'CRITICAL').length;
138
+ const highCount = plan.steps.filter(s => s.priority === 'HIGH').length;
139
+ const mediumCount = plan.steps.filter(s => s.priority === 'MEDIUM').length;
140
+ const lowCount = plan.steps.filter(s => s.priority === 'LOW').length;
141
+
142
+ return `
143
+ <div class="stats-grid">
144
+ <div class="stat-card">
145
+ <div class="value">${plan.steps.length}</div>
146
+ <div class="label">Total Steps</div>
147
+ </div>
148
+ <div class="stat-card">
149
+ <div class="value">${plan.tier1Steps}</div>
150
+ <div class="label">Tier 1 (Rules)</div>
151
+ </div>
152
+ <div class="stat-card">
153
+ <div class="value">${plan.tier2Steps}</div>
154
+ <div class="label">Tier 2 (AST)</div>
155
+ </div>
156
+ <div class="stat-card">
157
+ <div class="value">${plan.totalOperations}</div>
158
+ <div class="label">File Operations</div>
159
+ </div>
160
+ </div>
161
+ <div class="priority-bar">
162
+ ${criticalCount ? `<div class="prio-seg prio-critical" style="flex: ${criticalCount}">🔴 ${criticalCount}</div>` : ''}
163
+ ${highCount ? `<div class="prio-seg prio-high" style="flex: ${highCount}">🟠 ${highCount}</div>` : ''}
164
+ ${mediumCount ? `<div class="prio-seg prio-medium" style="flex: ${mediumCount}">🔵 ${mediumCount}</div>` : ''}
165
+ ${lowCount ? `<div class="prio-seg prio-low" style="flex: ${lowCount}">🟢 ${lowCount}</div>` : ''}
166
+ </div>`;
167
+ }
168
+
169
+ private renderRoadmap(plan: RefactoringPlan): string {
170
+ if (plan.steps.length === 0) {
171
+ return `
172
+ <h2 class="section-title">✅ No Refactoring Needed</h2>
173
+ <div class="card success-card">
174
+ <p>Your architecture is clean! No refactoring suggestions at this time.</p>
175
+ </div>`;
176
+ }
177
+
178
+ const stepsHtml = plan.steps.map(step => this.renderStep(step)).join('');
179
+
180
+ return `
181
+ <h2 class="section-title">🗺️ Refactoring Roadmap</h2>
182
+ <div class="roadmap">
183
+ ${stepsHtml}
184
+ </div>`;
185
+ }
186
+
187
+ private renderStep(step: RefactorStep): string {
188
+ const operationsHtml = step.operations.map(op => `
189
+ <div class="operation">
190
+ <span class="op-icon">${this.opIcon(op.type)}</span>
191
+ <span class="op-badge" style="background: ${this.opColor(op.type)}20; color: ${this.opColor(op.type)}; border: 1px solid ${this.opColor(op.type)}40">${op.type}</span>
192
+ <code class="op-path">${this.escapeHtml(op.path)}</code>
193
+ ${op.newPath ? `<span class="op-arrow">→</span> <code class="op-path">${this.escapeHtml(op.newPath)}</code>` : ''}
194
+ <div class="op-desc">${this.escapeHtml(op.description)}</div>
195
+ </div>
196
+ `).join('');
197
+
198
+ const impactHtml = step.scoreImpact.map(i =>
199
+ `<span class="impact-tag">${i.metric}: ${i.before}→${i.after} <span class="impact-diff">+${i.after - i.before}</span></span>`
200
+ ).join('');
201
+
202
+ return `
203
+ <div class="step-card" data-priority="${step.priority}">
204
+ <div class="step-header">
205
+ <div class="step-number">${step.id}</div>
206
+ <div class="step-info">
207
+ <div class="step-title-row">
208
+ <h3>${this.escapeHtml(step.title)}</h3>
209
+ <span class="severity-badge severity-${step.priority}">${step.priority}</span>
210
+ <span class="tier-badge">Tier ${step.tier}</span>
211
+ </div>
212
+ <p class="step-desc">${this.escapeHtml(step.description)}</p>
213
+ <details class="step-details">
214
+ <summary>📖 Why?</summary>
215
+ <p class="rationale">${this.escapeHtml(step.rationale)}</p>
216
+ </details>
217
+ </div>
218
+ </div>
219
+ <div class="step-operations">
220
+ <h4>📋 Operations (${step.operations.length})</h4>
221
+ ${operationsHtml}
222
+ </div>
223
+ <div class="step-impact">
224
+ <h4>📈 Score Impact</h4>
225
+ <div class="impact-tags">${impactHtml}</div>
226
+ </div>
227
+ </div>`;
228
+ }
229
+
230
+ private renderFooter(): string {
231
+ return `
232
+ <div class="footer">
233
+ <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect v2.0</a> — Refactoring Engine</p>
234
+ <p>By <strong>Camilo Girardelli</strong> · <a href="https://www.girardellitecnologia.com">Girardelli Tecnologia</a></p>
235
+ </div>`;
236
+ }
237
+
238
+ private getStyles(): string {
239
+ return `<style>
240
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
241
+ * { margin: 0; padding: 0; box-sizing: border-box; }
242
+ body {
243
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
244
+ background: #0f172a; color: #e2e8f0; line-height: 1.6; min-height: 100vh;
245
+ }
246
+ .container { max-width: 1100px; margin: 0 auto; padding: 2rem; }
247
+
248
+ .header {
249
+ text-align: center; padding: 3rem 2rem;
250
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 50%, #1e1b4b 100%);
251
+ border-bottom: 1px solid #334155; margin-bottom: 2rem;
252
+ }
253
+ .header h1 {
254
+ font-size: 2.5rem; font-weight: 900;
255
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
256
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
257
+ margin-bottom: 0.5rem;
258
+ }
259
+ .header .subtitle { color: #94a3b8; font-size: 1.1rem; font-weight: 300; }
260
+ .header .meta {
261
+ margin-top: 1rem; display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;
262
+ }
263
+ .header .meta span {
264
+ background: #1e293b; padding: 0.4rem 1rem; border-radius: 99px;
265
+ font-size: 0.85rem; color: #94a3b8; border: 1px solid #334155;
266
+ }
267
+ .header .meta span strong { color: #e2e8f0; }
268
+
269
+ .section-title {
270
+ font-size: 1.4rem; font-weight: 700; margin: 2.5rem 0 1rem;
271
+ display: flex; align-items: center; gap: 0.5rem;
272
+ }
273
+
274
+ .card {
275
+ background: #1e293b; border-radius: 16px; border: 1px solid #334155;
276
+ padding: 1.5rem; margin-bottom: 1rem;
277
+ }
278
+ .success-card { border-color: #22c55e40; color: #22c55e; text-align: center; padding: 2rem; }
279
+
280
+ /* Score Comparison */
281
+ .score-comparison { padding: 2rem; }
282
+ .score-pair {
283
+ display: flex; align-items: center; justify-content: center; gap: 1.5rem;
284
+ margin-bottom: 2rem; flex-wrap: wrap;
285
+ }
286
+ .score-box { text-align: center; }
287
+ .score-num { font-size: 3.5rem; font-weight: 900; line-height: 1; }
288
+ .score-label { font-size: 0.85rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
289
+ .score-diff { font-size: 1.5rem; font-weight: 700; }
290
+
291
+ .bars-legend { display: flex; gap: 1rem; margin-bottom: 0.5rem; }
292
+ .legend-tag { font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 6px; }
293
+ .legend-tag.before { background: rgba(255,255,255,0.05); color: #94a3b8; }
294
+ .legend-tag.after { background: rgba(129,140,248,0.2); color: #818cf8; }
295
+
296
+ .comparison-row {
297
+ display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;
298
+ }
299
+ .metric-name { width: 100px; font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; font-weight: 600; }
300
+ .metric-bars { flex: 1; position: relative; height: 30px; }
301
+ .bar-before, .bar-after {
302
+ position: absolute; top: 0; left: 0; height: 15px; border-radius: 4px;
303
+ display: flex; align-items: center; padding-left: 6px;
304
+ font-size: 0.7rem; font-weight: 600; transition: width 1s ease;
305
+ }
306
+ .bar-before { top: 0; }
307
+ .bar-after { top: 16px; }
308
+ .metric-diff { width: 50px; text-align: right; font-weight: 700; font-size: 0.85rem; }
309
+
310
+ /* Stats */
311
+ .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1rem; }
312
+ .stat-card {
313
+ background: linear-gradient(135deg, #1e293b, #0f172a);
314
+ border: 1px solid #334155; border-radius: 16px; padding: 1.5rem; text-align: center;
315
+ }
316
+ .stat-card .value {
317
+ font-size: 2rem; font-weight: 800;
318
+ background: linear-gradient(135deg, #818cf8, #c084fc);
319
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
320
+ }
321
+ .stat-card .label { font-size: 0.85rem; color: #94a3b8; margin-top: 0.3rem; }
322
+
323
+ .priority-bar {
324
+ display: flex; border-radius: 12px; overflow: hidden; height: 32px; margin-bottom: 2rem;
325
+ }
326
+ .prio-seg {
327
+ display: flex; align-items: center; justify-content: center;
328
+ font-size: 0.75rem; font-weight: 600;
329
+ }
330
+ .prio-critical { background: #ef444430; color: #ef4444; }
331
+ .prio-high { background: #f59e0b30; color: #f59e0b; }
332
+ .prio-medium { background: #3b82f630; color: #60a5fa; }
333
+ .prio-low { background: #22c55e30; color: #22c55e; }
334
+
335
+ /* Steps */
336
+ .roadmap { display: flex; flex-direction: column; gap: 1rem; }
337
+ .step-card {
338
+ background: #1e293b; border-radius: 16px; border: 1px solid #334155;
339
+ padding: 1.5rem; transition: border-color 0.2s;
340
+ }
341
+ .step-card:hover { border-color: #818cf8; }
342
+ .step-header { display: flex; gap: 1rem; margin-bottom: 1rem; }
343
+ .step-number {
344
+ width: 40px; height: 40px; border-radius: 50%;
345
+ background: linear-gradient(135deg, #818cf8, #c084fc);
346
+ display: flex; align-items: center; justify-content: center;
347
+ font-weight: 800; font-size: 1rem; color: white; flex-shrink: 0;
348
+ }
349
+ .step-info { flex: 1; }
350
+ .step-title-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
351
+ .step-title-row h3 { font-size: 1.1rem; font-weight: 700; }
352
+ .step-desc { color: #94a3b8; font-size: 0.9rem; margin-top: 0.3rem; }
353
+
354
+ .severity-badge {
355
+ display: inline-block; padding: 0.15rem 0.5rem; border-radius: 99px;
356
+ font-size: 0.65rem; font-weight: 600; letter-spacing: 0.5px;
357
+ }
358
+ .severity-CRITICAL { background: #dc262620; color: #ef4444; border: 1px solid #ef444440; }
359
+ .severity-HIGH { background: #f59e0b20; color: #f59e0b; border: 1px solid #f59e0b40; }
360
+ .severity-MEDIUM { background: #3b82f620; color: #60a5fa; border: 1px solid #60a5fa40; }
361
+ .severity-LOW { background: #22c55e20; color: #22c55e; border: 1px solid #22c55e40; }
362
+
363
+ .tier-badge {
364
+ background: #818cf815; color: #818cf8; border: 1px solid #818cf830;
365
+ padding: 0.15rem 0.5rem; border-radius: 99px; font-size: 0.65rem; font-weight: 600;
366
+ }
367
+
368
+ .step-details { margin-top: 0.5rem; }
369
+ .step-details summary { cursor: pointer; color: #818cf8; font-size: 0.85rem; font-weight: 500; }
370
+ .rationale { color: #64748b; font-size: 0.85rem; margin-top: 0.3rem; font-style: italic; }
371
+
372
+ .step-operations { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
373
+ .step-operations h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
374
+ .operation { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
375
+ .op-icon { font-size: 0.9rem; }
376
+ .op-badge { padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.65rem; font-weight: 700; }
377
+ .op-path { background: #0f172a; padding: 1px 6px; border-radius: 4px; font-size: 0.8rem; color: #c084fc; }
378
+ .op-arrow { color: #818cf8; font-weight: 700; }
379
+ .op-desc { width: 100%; color: #64748b; font-size: 0.8rem; padding-left: 1.8rem; }
380
+
381
+ .step-impact { margin-top: 0.5rem; }
382
+ .step-impact h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.3rem; }
383
+ .impact-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
384
+ .impact-tag {
385
+ background: #22c55e10; color: #22c55e; border: 1px solid #22c55e30;
386
+ padding: 0.2rem 0.6rem; border-radius: 8px; font-size: 0.75rem; font-weight: 500;
387
+ }
388
+ .impact-diff { font-weight: 700; }
389
+
390
+ .footer {
391
+ text-align: center; padding: 2rem; color: #475569; font-size: 0.85rem;
392
+ border-top: 1px solid #1e293b; margin-top: 3rem;
393
+ }
394
+ .footer a { color: #818cf8; text-decoration: none; }
395
+
396
+ @media (max-width: 768px) {
397
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
398
+ .score-pair { flex-direction: column; }
399
+ .container { padding: 1rem; }
400
+ }
401
+
402
+ @media print {
403
+ body { background: white; color: #1e293b; }
404
+ .card, .step-card, .stat-card { background: white; border-color: #e2e8f0; }
405
+ }
406
+ </style>`;
407
+ }
408
+ }
@@ -0,0 +1,97 @@
1
+ import { basename, dirname } from 'path';
2
+ import { AnalysisReport, RefactorRule, RefactorStep, FileOperation } from '../types.js';
3
+
4
+ /**
5
+ * Barrel Optimizer Rule (Tier 1)
6
+ * Analyzes barrel files (__init__.py, index.ts) and suggests optimization.
7
+ * Barrel files that re-export everything create unnecessary coupling.
8
+ */
9
+ export class BarrelOptimizerRule implements RefactorRule {
10
+ name = 'barrel-optimizer';
11
+ tier = 1 as const;
12
+
13
+ private static readonly BARREL_FILES = new Set([
14
+ '__init__.py', 'index.ts', 'index.js', 'index.tsx', 'index.jsx',
15
+ ]);
16
+
17
+ analyze(report: AnalysisReport, projectPath: string): RefactorStep[] {
18
+ const steps: RefactorStep[] = [];
19
+
20
+ // Find barrel files in the dependency graph
21
+ const barrelNodes = report.dependencyGraph.nodes.filter((n) =>
22
+ BarrelOptimizerRule.BARREL_FILES.has(basename(n))
23
+ );
24
+
25
+ for (const barrel of barrelNodes) {
26
+ // Count how many things this barrel re-exports (outgoing edges)
27
+ const outgoing = report.dependencyGraph.edges.filter(
28
+ (e) => e.from === barrel
29
+ );
30
+ const incoming = report.dependencyGraph.edges.filter(
31
+ (e) => e.to === barrel
32
+ );
33
+
34
+ if (outgoing.length < 3) continue;
35
+
36
+ // Check for "pass-through" pattern: files import from barrel
37
+ // but barrel just re-exports from siblings
38
+ const siblingDir = dirname(barrel);
39
+ const siblingExports = outgoing.filter(
40
+ (e) => dirname(e.to) === siblingDir
41
+ );
42
+
43
+ const operations: FileOperation[] = [];
44
+
45
+ // Suggest direct imports instead of barrel
46
+ for (const consumer of incoming) {
47
+ const consumedModules = outgoing
48
+ .filter((e) => {
49
+ // Check if consumer actually needs this module
50
+ return report.dependencyGraph.edges.some(
51
+ (edge) => edge.from === consumer.from && edge.to === e.to
52
+ );
53
+ })
54
+ .map((e) => e.to);
55
+
56
+ if (consumedModules.length > 0) {
57
+ operations.push({
58
+ type: 'MODIFY',
59
+ path: consumer.from,
60
+ description: `Replace barrel import from \`${basename(barrel)}\` with direct imports: ${consumedModules.map((m) => basename(m)).join(', ')}`,
61
+ });
62
+ }
63
+ }
64
+
65
+ // Suggest simplifying the barrel
66
+ if (siblingExports.length > 5) {
67
+ operations.push({
68
+ type: 'MODIFY',
69
+ path: barrel,
70
+ description: `Simplify ${basename(barrel)}: only re-export public API (${siblingExports.length} re-exports detected, consider reducing)`,
71
+ });
72
+ }
73
+
74
+ if (operations.length > 0) {
75
+ steps.push({
76
+ id: 0,
77
+ tier: 1,
78
+ rule: this.name,
79
+ priority: outgoing.length >= 8 ? 'HIGH' : 'MEDIUM',
80
+ title: `Optimize barrel: ${barrel}`,
81
+ description: `\`${barrel}\` re-exports ${outgoing.length} modules. ` +
82
+ `This creates a "Shotgun Surgery" risk — any change propagates widely.`,
83
+ rationale: `Barrel files that re-export everything make it hard to tree-shake unused code ` +
84
+ `and create implicit dependencies. Direct imports make dependency relationships explicit ` +
85
+ `and reduce the blast radius of changes.`,
86
+ operations,
87
+ scoreImpact: [
88
+ { metric: 'coupling', before: report.score.breakdown.coupling, after: Math.min(95, report.score.breakdown.coupling + 10) },
89
+ { metric: 'layering', before: report.score.breakdown.layering, after: Math.min(95, report.score.breakdown.layering + 5) },
90
+ ],
91
+ });
92
+ }
93
+ }
94
+
95
+ return steps;
96
+ }
97
+ }