@girardelli/architect 1.2.1 → 2.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.
- package/README.md +111 -112
- package/dist/agent-generator.d.ts +95 -0
- package/dist/agent-generator.d.ts.map +1 -0
- package/dist/agent-generator.js +1295 -0
- package/dist/agent-generator.js.map +1 -0
- package/dist/cli.js +76 -2
- package/dist/cli.js.map +1 -1
- package/dist/html-reporter.d.ts +26 -4
- package/dist/html-reporter.d.ts.map +1 -1
- package/dist/html-reporter.js +832 -33
- package/dist/html-reporter.js.map +1 -1
- package/dist/index.d.ts +26 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/dist/refactor-engine.d.ts +18 -0
- package/dist/refactor-engine.d.ts.map +1 -0
- package/dist/refactor-engine.js +86 -0
- package/dist/refactor-engine.js.map +1 -0
- package/dist/refactor-reporter.d.ts +20 -0
- package/dist/refactor-reporter.d.ts.map +1 -0
- package/dist/refactor-reporter.js +389 -0
- package/dist/refactor-reporter.js.map +1 -0
- package/dist/rules/barrel-optimizer.d.ts +13 -0
- package/dist/rules/barrel-optimizer.d.ts.map +1 -0
- package/dist/rules/barrel-optimizer.js +77 -0
- package/dist/rules/barrel-optimizer.js.map +1 -0
- package/dist/rules/dead-code-detector.d.ts +21 -0
- package/dist/rules/dead-code-detector.d.ts.map +1 -0
- package/dist/rules/dead-code-detector.js +117 -0
- package/dist/rules/dead-code-detector.js.map +1 -0
- package/dist/rules/hub-splitter.d.ts +13 -0
- package/dist/rules/hub-splitter.d.ts.map +1 -0
- package/dist/rules/hub-splitter.js +110 -0
- package/dist/rules/hub-splitter.js.map +1 -0
- package/dist/rules/import-organizer.d.ts +13 -0
- package/dist/rules/import-organizer.d.ts.map +1 -0
- package/dist/rules/import-organizer.js +85 -0
- package/dist/rules/import-organizer.js.map +1 -0
- package/dist/rules/module-grouper.d.ts +13 -0
- package/dist/rules/module-grouper.d.ts.map +1 -0
- package/dist/rules/module-grouper.js +110 -0
- package/dist/rules/module-grouper.js.map +1 -0
- package/dist/scorer.d.ts +12 -0
- package/dist/scorer.d.ts.map +1 -1
- package/dist/scorer.js +61 -17
- package/dist/scorer.js.map +1 -1
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-generator.ts +1401 -0
- package/src/cli.ts +83 -2
- package/src/html-reporter.ts +872 -35
- package/src/index.ts +108 -9
- package/src/refactor-engine.ts +117 -0
- package/src/refactor-reporter.ts +408 -0
- package/src/rules/barrel-optimizer.ts +97 -0
- package/src/rules/dead-code-detector.ts +132 -0
- package/src/rules/hub-splitter.ts +123 -0
- package/src/rules/import-organizer.ts +98 -0
- package/src/rules/module-grouper.ts +124 -0
- package/src/scorer.ts +63 -17
- package/src/types.ts +52 -0
package/dist/html-reporter.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Generates premium visual HTML reports from AnalysisReport.
|
|
3
|
+
* Features: D3.js force graph, bubble charts, radar chart, animated counters.
|
|
3
4
|
*/
|
|
4
5
|
export class HtmlReportGenerator {
|
|
5
|
-
generateHtml(report) {
|
|
6
|
+
generateHtml(report, plan, agentSuggestion) {
|
|
6
7
|
const grouped = this.groupAntiPatterns(report.antiPatterns);
|
|
7
8
|
const sugGrouped = this.groupSuggestions(report.suggestions);
|
|
8
9
|
return `<!DOCTYPE html>
|
|
@@ -12,22 +13,24 @@ export class HtmlReportGenerator {
|
|
|
12
13
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
14
|
<title>Architect Report — ${this.escapeHtml(report.projectInfo.name)}</title>
|
|
14
15
|
${this.getStyles()}
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7"><\/script>
|
|
16
17
|
</head>
|
|
17
18
|
<body>
|
|
18
19
|
${this.renderHeader(report)}
|
|
19
20
|
<div class="container">
|
|
20
21
|
${this.renderScoreHero(report)}
|
|
22
|
+
${this.renderRadarChart(report)}
|
|
21
23
|
${this.renderStats(report)}
|
|
22
24
|
${this.renderLayers(report)}
|
|
25
|
+
${this.renderDependencyGraph(report)}
|
|
26
|
+
${this.renderAntiPatternBubbles(report, grouped)}
|
|
23
27
|
${this.renderAntiPatterns(report, grouped)}
|
|
24
|
-
${this.renderDiagram(report)}
|
|
25
28
|
${this.renderSuggestions(sugGrouped)}
|
|
29
|
+
${plan ? this.renderRefactoringPlan(plan) : ''}
|
|
30
|
+
${agentSuggestion ? this.renderAgentSuggestions(agentSuggestion) : ''}
|
|
26
31
|
</div>
|
|
27
32
|
${this.renderFooter()}
|
|
28
|
-
|
|
29
|
-
mermaid.initialize({ theme: 'default', startOnLoad: true });
|
|
30
|
-
<\/script>
|
|
33
|
+
${this.getScripts(report)}
|
|
31
34
|
</body>
|
|
32
35
|
</html>`;
|
|
33
36
|
}
|
|
@@ -132,7 +135,7 @@ ${this.renderFooter()}
|
|
|
132
135
|
stroke-dashoffset="${offset}" />
|
|
133
136
|
</svg>
|
|
134
137
|
<div class="score-value">
|
|
135
|
-
<div class="number" style="color: ${this.scoreColor(overall)}"
|
|
138
|
+
<div class="number score-counter" data-target="${overall}" style="color: ${this.scoreColor(overall)}">0</div>
|
|
136
139
|
<div class="label">/ 100</div>
|
|
137
140
|
<div class="grade">${this.scoreLabel(overall)}</div>
|
|
138
141
|
</div>
|
|
@@ -140,25 +143,36 @@ ${this.renderFooter()}
|
|
|
140
143
|
<div class="score-breakdown">
|
|
141
144
|
${breakdownItems}
|
|
142
145
|
</div>
|
|
146
|
+
</div>`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Radar chart for the 4 score components
|
|
150
|
+
*/
|
|
151
|
+
renderRadarChart(report) {
|
|
152
|
+
const entries = Object.entries(report.score.breakdown);
|
|
153
|
+
return `
|
|
154
|
+
<h2 class="section-title">🎯 Health Radar</h2>
|
|
155
|
+
<div class="card" style="display: flex; justify-content: center;">
|
|
156
|
+
<svg id="radar-chart" width="350" height="350" viewBox="0 0 350 350"></svg>
|
|
143
157
|
</div>`;
|
|
144
158
|
}
|
|
145
159
|
renderStats(report) {
|
|
146
160
|
return `
|
|
147
161
|
<div class="stats-grid">
|
|
148
162
|
<div class="stat-card">
|
|
149
|
-
<div class="value"
|
|
163
|
+
<div class="value stat-counter" data-target="${report.projectInfo.totalFiles}">0</div>
|
|
150
164
|
<div class="label">Files Scanned</div>
|
|
151
165
|
</div>
|
|
152
166
|
<div class="stat-card">
|
|
153
|
-
<div class="value"
|
|
167
|
+
<div class="value stat-counter" data-target="${report.projectInfo.totalLines}">0</div>
|
|
154
168
|
<div class="label">Lines of Code</div>
|
|
155
169
|
</div>
|
|
156
170
|
<div class="stat-card">
|
|
157
|
-
<div class="value"
|
|
171
|
+
<div class="value stat-counter" data-target="${report.antiPatterns.length}">0</div>
|
|
158
172
|
<div class="label">Anti-Patterns</div>
|
|
159
173
|
</div>
|
|
160
174
|
<div class="stat-card">
|
|
161
|
-
<div class="value"
|
|
175
|
+
<div class="value stat-counter" data-target="${report.dependencyGraph.edges.length}">0</div>
|
|
162
176
|
<div class="label">Dependencies</div>
|
|
163
177
|
</div>
|
|
164
178
|
</div>`;
|
|
@@ -188,7 +202,55 @@ ${this.renderFooter()}
|
|
|
188
202
|
<h2 class="section-title">📐 Architectural Layers</h2>
|
|
189
203
|
<div class="layers-grid">${cards}</div>`;
|
|
190
204
|
}
|
|
191
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Interactive D3.js force-directed dependency graph
|
|
207
|
+
*/
|
|
208
|
+
renderDependencyGraph(report) {
|
|
209
|
+
if (report.dependencyGraph.edges.length === 0)
|
|
210
|
+
return '';
|
|
211
|
+
// Build node data with connection counts
|
|
212
|
+
const connectionCount = {};
|
|
213
|
+
for (const edge of report.dependencyGraph.edges) {
|
|
214
|
+
connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
|
|
215
|
+
connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
|
|
216
|
+
}
|
|
217
|
+
const layerMap = {};
|
|
218
|
+
for (const layer of report.layers) {
|
|
219
|
+
for (const file of layer.files) {
|
|
220
|
+
layerMap[file] = layer.name;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const nodes = report.dependencyGraph.nodes.map(n => ({
|
|
224
|
+
id: n,
|
|
225
|
+
name: n.split('/').pop() || n,
|
|
226
|
+
connections: connectionCount[n] || 0,
|
|
227
|
+
layer: layerMap[n] || 'Other',
|
|
228
|
+
}));
|
|
229
|
+
const links = report.dependencyGraph.edges.map(e => ({
|
|
230
|
+
source: e.from,
|
|
231
|
+
target: e.to,
|
|
232
|
+
}));
|
|
233
|
+
return `
|
|
234
|
+
<h2 class="section-title">🔗 Dependency Graph</h2>
|
|
235
|
+
<div class="card graph-card">
|
|
236
|
+
<div class="graph-legend">
|
|
237
|
+
<span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
|
|
238
|
+
<span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
|
|
239
|
+
<span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
|
|
240
|
+
<span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
|
|
241
|
+
<span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
|
|
242
|
+
<span class="legend-item"><span class="legend-dot" style="background: #64748b"></span> Other</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div id="dep-graph" style="width:100%; min-height:400px;"></div>
|
|
245
|
+
<div class="graph-hint">🖱️ Drag nodes to explore • Node size = number of connections</div>
|
|
246
|
+
</div>
|
|
247
|
+
<script type="application/json" id="graph-nodes">${JSON.stringify(nodes)}<\/script>
|
|
248
|
+
<script type="application/json" id="graph-links">${JSON.stringify(links)}<\/script>`;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Bubble chart for anti-patterns — bigger = more severe
|
|
252
|
+
*/
|
|
253
|
+
renderAntiPatternBubbles(report, grouped) {
|
|
192
254
|
if (report.antiPatterns.length === 0) {
|
|
193
255
|
return `
|
|
194
256
|
<h2 class="section-title">✅ Anti-Patterns</h2>
|
|
@@ -196,6 +258,29 @@ ${this.renderFooter()}
|
|
|
196
258
|
<p>No significant anti-patterns detected. Excellent architecture!</p>
|
|
197
259
|
</div>`;
|
|
198
260
|
}
|
|
261
|
+
const severityWeight = {
|
|
262
|
+
CRITICAL: 80, HIGH: 60, MEDIUM: 40, LOW: 25,
|
|
263
|
+
};
|
|
264
|
+
const severityColor = {
|
|
265
|
+
CRITICAL: '#ef4444', HIGH: '#f59e0b', MEDIUM: '#60a5fa', LOW: '#22c55e',
|
|
266
|
+
};
|
|
267
|
+
const bubbles = Object.entries(grouped).map(([name, data]) => ({
|
|
268
|
+
name,
|
|
269
|
+
count: data.count,
|
|
270
|
+
severity: data.severity,
|
|
271
|
+
radius: (severityWeight[data.severity] || 30) + data.count * 8,
|
|
272
|
+
color: severityColor[data.severity] || '#64748b',
|
|
273
|
+
}));
|
|
274
|
+
return `
|
|
275
|
+
<h2 class="section-title">🫧 Anti-Pattern Impact Map</h2>
|
|
276
|
+
<div class="card" style="display:flex; justify-content:center;">
|
|
277
|
+
<div id="bubble-chart" style="width:100%; min-height:300px;"></div>
|
|
278
|
+
</div>
|
|
279
|
+
<script type="application/json" id="bubble-data">${JSON.stringify(bubbles)}<\/script>`;
|
|
280
|
+
}
|
|
281
|
+
renderAntiPatterns(report, grouped) {
|
|
282
|
+
if (report.antiPatterns.length === 0)
|
|
283
|
+
return '';
|
|
199
284
|
const rows = Object.entries(grouped)
|
|
200
285
|
.sort((a, b) => b[1].count - a[1].count)
|
|
201
286
|
.map(([name, data]) => `
|
|
@@ -211,7 +296,7 @@ ${this.renderFooter()}
|
|
|
211
296
|
</tr>`)
|
|
212
297
|
.join('');
|
|
213
298
|
return `
|
|
214
|
-
<h2 class="section-title">⚠️ Anti-
|
|
299
|
+
<h2 class="section-title">⚠️ Anti-Pattern Details (${report.antiPatterns.length})</h2>
|
|
215
300
|
<div class="card">
|
|
216
301
|
<table>
|
|
217
302
|
<thead>
|
|
@@ -225,17 +310,6 @@ ${this.renderFooter()}
|
|
|
225
310
|
</thead>
|
|
226
311
|
<tbody>${rows}</tbody>
|
|
227
312
|
</table>
|
|
228
|
-
</div>`;
|
|
229
|
-
}
|
|
230
|
-
renderDiagram(report) {
|
|
231
|
-
if (!report.diagram.mermaid)
|
|
232
|
-
return '';
|
|
233
|
-
return `
|
|
234
|
-
<h2 class="section-title">📊 Architecture Diagram</h2>
|
|
235
|
-
<div class="card">
|
|
236
|
-
<div class="mermaid-container">
|
|
237
|
-
<pre class="mermaid">${this.escapeHtml(report.diagram.mermaid)}</pre>
|
|
238
|
-
</div>
|
|
239
313
|
</div>`;
|
|
240
314
|
}
|
|
241
315
|
renderSuggestions(suggestions) {
|
|
@@ -273,9 +347,636 @@ ${this.renderFooter()}
|
|
|
273
347
|
renderFooter() {
|
|
274
348
|
return `
|
|
275
349
|
<div class="footer">
|
|
276
|
-
<p>Generated by <a href="https://github.com/
|
|
277
|
-
<p>By <strong>Camilo Girardelli</strong> · <a href="https://
|
|
350
|
+
<p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect v2.0</a> — AI-powered architecture analysis + refactoring engine</p>
|
|
351
|
+
<p>By <strong>Camilo Girardelli</strong> · <a href="https://www.girardellitecnologia.com">Girardelli Tecnologia</a></p>
|
|
352
|
+
</div>`;
|
|
353
|
+
}
|
|
354
|
+
// ── Refactoring Plan Section ──
|
|
355
|
+
opColor(type) {
|
|
356
|
+
switch (type) {
|
|
357
|
+
case 'CREATE': return '#22c55e';
|
|
358
|
+
case 'MOVE': return '#3b82f6';
|
|
359
|
+
case 'MODIFY': return '#f59e0b';
|
|
360
|
+
case 'DELETE': return '#ef4444';
|
|
361
|
+
default: return '#64748b';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
opIcon(type) {
|
|
365
|
+
switch (type) {
|
|
366
|
+
case 'CREATE': return '➕';
|
|
367
|
+
case 'MOVE': return '📦';
|
|
368
|
+
case 'MODIFY': return '✏️';
|
|
369
|
+
case 'DELETE': return '🗑️';
|
|
370
|
+
default: return '📄';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
renderRefactoringPlan(plan) {
|
|
374
|
+
if (plan.steps.length === 0) {
|
|
375
|
+
return `
|
|
376
|
+
<h2 class="section-title">✅ Refactoring Plan</h2>
|
|
377
|
+
<div class="card success-card">
|
|
378
|
+
<p>No refactoring needed! Your architecture is already in great shape.</p>
|
|
379
|
+
</div>`;
|
|
380
|
+
}
|
|
381
|
+
const improvement = plan.estimatedScoreAfter.overall - plan.currentScore.overall;
|
|
382
|
+
const metrics = Object.keys(plan.currentScore.breakdown);
|
|
383
|
+
const bars = metrics.map(metric => {
|
|
384
|
+
const before = plan.currentScore.breakdown[metric];
|
|
385
|
+
const after = plan.estimatedScoreAfter.breakdown[metric] ?? before;
|
|
386
|
+
const diff = after - before;
|
|
387
|
+
return `
|
|
388
|
+
<div class="comparison-row">
|
|
389
|
+
<div class="refactor-metric-name">${metric}</div>
|
|
390
|
+
<div class="refactor-metric-bars">
|
|
391
|
+
<div class="rbar-before" style="width: ${before}%; background: ${this.scoreColor(before)}40"><span>${before}</span></div>
|
|
392
|
+
<div class="rbar-after" style="width: ${after}%; background: ${this.scoreColor(after)}"><span>${after}</span></div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="refactor-metric-diff" style="color: ${diff > 0 ? '#22c55e' : '#64748b'}">
|
|
395
|
+
${diff > 0 ? `+${diff}` : diff === 0 ? '—' : String(diff)}
|
|
396
|
+
</div>
|
|
397
|
+
</div>`;
|
|
398
|
+
}).join('');
|
|
399
|
+
const stepsHtml = plan.steps.map(step => this.renderRefactorStep(step)).join('');
|
|
400
|
+
const criticalCount = plan.steps.filter(s => s.priority === 'CRITICAL').length;
|
|
401
|
+
const highCount = plan.steps.filter(s => s.priority === 'HIGH').length;
|
|
402
|
+
const mediumCount = plan.steps.filter(s => s.priority === 'MEDIUM').length;
|
|
403
|
+
const lowCount = plan.steps.filter(s => s.priority === 'LOW').length;
|
|
404
|
+
return `
|
|
405
|
+
<h2 class="section-title">🔧 Refactoring Plan</h2>
|
|
406
|
+
|
|
407
|
+
<div class="card refactor-score">
|
|
408
|
+
<div class="refactor-score-pair">
|
|
409
|
+
<div class="rscore-box">
|
|
410
|
+
<div class="rscore-num" style="color: ${this.scoreColor(plan.currentScore.overall)}">${plan.currentScore.overall}</div>
|
|
411
|
+
<div class="rscore-label">Current</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="rscore-arrow">
|
|
414
|
+
<svg width="60" height="30" viewBox="0 0 60 30">
|
|
415
|
+
<path d="M5 15 L45 15 M40 8 L48 15 L40 22" stroke="#818cf8" stroke-width="2.5" fill="none"/>
|
|
416
|
+
</svg>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="rscore-box">
|
|
419
|
+
<div class="rscore-num" style="color: ${this.scoreColor(plan.estimatedScoreAfter.overall)}">${plan.estimatedScoreAfter.overall}</div>
|
|
420
|
+
<div class="rscore-label">Estimated</div>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="rscore-improvement" style="color: #22c55e">+${improvement} pts</div>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="refactor-bars-section">
|
|
425
|
+
<div class="refactor-legend">
|
|
426
|
+
<span class="rlegend-tag rbefore">Before</span>
|
|
427
|
+
<span class="rlegend-tag rafter">After</span>
|
|
428
|
+
</div>
|
|
429
|
+
${bars}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div class="refactor-stats-row">
|
|
434
|
+
<div class="rstat">${plan.steps.length} steps</div>
|
|
435
|
+
<div class="rstat">${plan.totalOperations} operations</div>
|
|
436
|
+
<div class="rstat">Tier 1: ${plan.tier1Steps}</div>
|
|
437
|
+
<div class="rstat">Tier 2: ${plan.tier2Steps}</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div class="priority-bar">
|
|
441
|
+
${criticalCount ? `<div class="prio-seg prio-critical" style="flex: ${criticalCount}">🔴 ${criticalCount}</div>` : ''}
|
|
442
|
+
${highCount ? `<div class="prio-seg prio-high" style="flex: ${highCount}">🟠 ${highCount}</div>` : ''}
|
|
443
|
+
${mediumCount ? `<div class="prio-seg prio-medium" style="flex: ${mediumCount}">🔵 ${mediumCount}</div>` : ''}
|
|
444
|
+
${lowCount ? `<div class="prio-seg prio-low" style="flex: ${lowCount}">🟢 ${lowCount}</div>` : ''}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div class="refactor-roadmap">
|
|
448
|
+
${stepsHtml}
|
|
449
|
+
</div>`;
|
|
450
|
+
}
|
|
451
|
+
renderRefactorStep(step) {
|
|
452
|
+
const operationsHtml = step.operations.map(op => `
|
|
453
|
+
<div class="rop">
|
|
454
|
+
<span class="rop-icon">${this.opIcon(op.type)}</span>
|
|
455
|
+
<span class="rop-badge" style="background: ${this.opColor(op.type)}20; color: ${this.opColor(op.type)}; border: 1px solid ${this.opColor(op.type)}40">${op.type}</span>
|
|
456
|
+
<code class="rop-path">${this.escapeHtml(op.path)}</code>
|
|
457
|
+
${op.newPath ? `<span class="rop-arrow">→</span> <code class="rop-path">${this.escapeHtml(op.newPath)}</code>` : ''}
|
|
458
|
+
<div class="rop-desc">${this.escapeHtml(op.description)}</div>
|
|
459
|
+
</div>
|
|
460
|
+
`).join('');
|
|
461
|
+
const impactHtml = step.scoreImpact.map(i => `<span class="rimpact-tag">${i.metric}: ${i.before}→${i.after} <strong>+${i.after - i.before}</strong></span>`).join('');
|
|
462
|
+
return `
|
|
463
|
+
<div class="rstep-card">
|
|
464
|
+
<div class="rstep-header">
|
|
465
|
+
<div class="rstep-number">${step.id}</div>
|
|
466
|
+
<div class="rstep-info">
|
|
467
|
+
<div class="rstep-title-row">
|
|
468
|
+
<h3>${this.escapeHtml(step.title)}</h3>
|
|
469
|
+
<span class="severity-badge severity-${step.priority}">${step.priority}</span>
|
|
470
|
+
<span class="tier-badge">Tier ${step.tier}</span>
|
|
471
|
+
</div>
|
|
472
|
+
<p class="rstep-desc">${this.escapeHtml(step.description)}</p>
|
|
473
|
+
<details class="rstep-details">
|
|
474
|
+
<summary>📖 Why?</summary>
|
|
475
|
+
<p class="rstep-rationale">${this.escapeHtml(step.rationale)}</p>
|
|
476
|
+
</details>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="rstep-ops">
|
|
480
|
+
<h4>📋 Operations (${step.operations.length})</h4>
|
|
481
|
+
${operationsHtml}
|
|
482
|
+
</div>
|
|
483
|
+
<div class="rstep-impact">
|
|
484
|
+
<h4>📈 Score Impact</h4>
|
|
485
|
+
<div class="rimpact-tags">${impactHtml}</div>
|
|
486
|
+
</div>
|
|
278
487
|
</div>`;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* All JavaScript for D3.js visualizations, animated counters, and radar chart
|
|
491
|
+
*/
|
|
492
|
+
getScripts(report) {
|
|
493
|
+
const breakdown = report.score.breakdown;
|
|
494
|
+
return `<script>
|
|
495
|
+
// ── Animated Counters ──
|
|
496
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
497
|
+
const counters = document.querySelectorAll('.score-counter, .stat-counter');
|
|
498
|
+
const observer = new IntersectionObserver((entries) => {
|
|
499
|
+
entries.forEach(entry => {
|
|
500
|
+
if (entry.isIntersecting) {
|
|
501
|
+
const el = entry.target;
|
|
502
|
+
const target = parseInt(el.dataset.target || '0');
|
|
503
|
+
animateCounter(el, target);
|
|
504
|
+
observer.unobserve(el);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}, { threshold: 0.5 });
|
|
508
|
+
|
|
509
|
+
counters.forEach(c => observer.observe(c));
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
function animateCounter(el, target) {
|
|
513
|
+
const duration = 1500;
|
|
514
|
+
const start = performance.now();
|
|
515
|
+
const update = (now) => {
|
|
516
|
+
const elapsed = now - start;
|
|
517
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
518
|
+
const ease = 1 - Math.pow(1 - progress, 3);
|
|
519
|
+
el.textContent = Math.round(target * ease).toLocaleString();
|
|
520
|
+
if (progress < 1) requestAnimationFrame(update);
|
|
521
|
+
};
|
|
522
|
+
requestAnimationFrame(update);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── Radar Chart ──
|
|
526
|
+
(function() {
|
|
527
|
+
const data = [
|
|
528
|
+
{ axis: 'Modularity', value: ${breakdown.modularity} },
|
|
529
|
+
{ axis: 'Coupling', value: ${breakdown.coupling} },
|
|
530
|
+
{ axis: 'Cohesion', value: ${breakdown.cohesion} },
|
|
531
|
+
{ axis: 'Layering', value: ${breakdown.layering} },
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
const svg = d3.select('#radar-chart');
|
|
535
|
+
const w = 350, h = 350, cx = w/2, cy = h/2, maxR = 120;
|
|
536
|
+
const levels = 5;
|
|
537
|
+
const total = data.length;
|
|
538
|
+
const angleSlice = (Math.PI * 2) / total;
|
|
539
|
+
|
|
540
|
+
// Grid circles
|
|
541
|
+
for (let i = 1; i <= levels; i++) {
|
|
542
|
+
const r = (maxR / levels) * i;
|
|
543
|
+
svg.append('circle')
|
|
544
|
+
.attr('cx', cx).attr('cy', cy).attr('r', r)
|
|
545
|
+
.attr('fill', 'none').attr('stroke', '#334155').attr('stroke-width', 0.5)
|
|
546
|
+
.attr('stroke-dasharray', '4,4');
|
|
547
|
+
|
|
548
|
+
svg.append('text')
|
|
549
|
+
.attr('x', cx + 4).attr('y', cy - r + 4)
|
|
550
|
+
.text(Math.round(100 / levels * i))
|
|
551
|
+
.attr('fill', '#475569').attr('font-size', '10px');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Axis lines
|
|
555
|
+
data.forEach((d, i) => {
|
|
556
|
+
const angle = angleSlice * i - Math.PI/2;
|
|
557
|
+
const x = cx + Math.cos(angle) * (maxR + 20);
|
|
558
|
+
const y = cy + Math.sin(angle) * (maxR + 20);
|
|
559
|
+
|
|
560
|
+
svg.append('line')
|
|
561
|
+
.attr('x1', cx).attr('y1', cy).attr('x2', cx + Math.cos(angle) * maxR).attr('y2', cy + Math.sin(angle) * maxR)
|
|
562
|
+
.attr('stroke', '#334155').attr('stroke-width', 1);
|
|
563
|
+
|
|
564
|
+
svg.append('text')
|
|
565
|
+
.attr('x', x).attr('y', y)
|
|
566
|
+
.attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
|
|
567
|
+
.attr('fill', '#94a3b8').attr('font-size', '12px').attr('font-weight', '600')
|
|
568
|
+
.text(d.axis);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Data polygon
|
|
572
|
+
const points = data.map((d, i) => {
|
|
573
|
+
const angle = angleSlice * i - Math.PI/2;
|
|
574
|
+
const r = (d.value / 100) * maxR;
|
|
575
|
+
return [cx + Math.cos(angle) * r, cy + Math.sin(angle) * r];
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const pointsStr = points.map(p => p.join(',')).join(' ');
|
|
579
|
+
|
|
580
|
+
svg.append('polygon')
|
|
581
|
+
.attr('points', pointsStr)
|
|
582
|
+
.attr('fill', 'rgba(129, 140, 248, 0.15)')
|
|
583
|
+
.attr('stroke', '#818cf8').attr('stroke-width', 2);
|
|
584
|
+
|
|
585
|
+
// Data dots
|
|
586
|
+
points.forEach((p, i) => {
|
|
587
|
+
const color = data[i].value >= 70 ? '#22c55e' : data[i].value >= 50 ? '#f59e0b' : '#ef4444';
|
|
588
|
+
svg.append('circle')
|
|
589
|
+
.attr('cx', p[0]).attr('cy', p[1]).attr('r', 5)
|
|
590
|
+
.attr('fill', color).attr('stroke', '#0f172a').attr('stroke-width', 2);
|
|
591
|
+
|
|
592
|
+
svg.append('text')
|
|
593
|
+
.attr('x', p[0]).attr('y', p[1] - 12)
|
|
594
|
+
.attr('text-anchor', 'middle')
|
|
595
|
+
.attr('fill', color).attr('font-size', '12px').attr('font-weight', '700')
|
|
596
|
+
.text(data[i].value);
|
|
597
|
+
});
|
|
598
|
+
})();
|
|
599
|
+
|
|
600
|
+
// ── D3 Force Dependency Graph ──
|
|
601
|
+
(function() {
|
|
602
|
+
const nodesEl = document.getElementById('graph-nodes');
|
|
603
|
+
const linksEl = document.getElementById('graph-links');
|
|
604
|
+
if (!nodesEl || !linksEl) return;
|
|
605
|
+
|
|
606
|
+
const nodes = JSON.parse(nodesEl.textContent || '[]');
|
|
607
|
+
const links = JSON.parse(linksEl.textContent || '[]');
|
|
608
|
+
if (nodes.length === 0) return;
|
|
609
|
+
|
|
610
|
+
const container = document.getElementById('dep-graph');
|
|
611
|
+
const width = container.clientWidth || 800;
|
|
612
|
+
const height = 450;
|
|
613
|
+
container.style.height = height + 'px';
|
|
614
|
+
|
|
615
|
+
const layerColors = {
|
|
616
|
+
API: '#ec4899', Service: '#3b82f6', Data: '#10b981',
|
|
617
|
+
UI: '#f59e0b', Infrastructure: '#8b5cf6', Other: '#64748b',
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const svg = d3.select('#dep-graph').append('svg')
|
|
621
|
+
.attr('width', width).attr('height', height)
|
|
622
|
+
.attr('viewBox', [0, 0, width, height]);
|
|
623
|
+
|
|
624
|
+
// Arrow marker
|
|
625
|
+
svg.append('defs').append('marker')
|
|
626
|
+
.attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
|
|
627
|
+
.attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
|
|
628
|
+
.attr('markerWidth', 6).attr('markerHeight', 6)
|
|
629
|
+
.append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
|
|
630
|
+
.attr('fill', '#475569');
|
|
631
|
+
|
|
632
|
+
const simulation = d3.forceSimulation(nodes)
|
|
633
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(60))
|
|
634
|
+
.force('charge', d3.forceManyBody().strength(-150))
|
|
635
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
636
|
+
.force('x', d3.forceX(width / 2).strength(0.1))
|
|
637
|
+
.force('y', d3.forceY(height / 2).strength(0.1))
|
|
638
|
+
.force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 3 + 12, 15)));
|
|
639
|
+
|
|
640
|
+
const link = svg.append('g')
|
|
641
|
+
.selectAll('line').data(links).join('line')
|
|
642
|
+
.attr('stroke', '#334155').attr('stroke-width', 1.5)
|
|
643
|
+
.attr('stroke-opacity', 0.6).attr('marker-end', 'url(#arrowhead)');
|
|
644
|
+
|
|
645
|
+
const node = svg.append('g')
|
|
646
|
+
.selectAll('g').data(nodes).join('g')
|
|
647
|
+
.call(d3.drag()
|
|
648
|
+
.on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
649
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
650
|
+
.on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// Node circles — size based on connections
|
|
654
|
+
node.append('circle')
|
|
655
|
+
.attr('r', d => Math.max(d.connections * 3 + 6, 8))
|
|
656
|
+
.attr('fill', d => layerColors[d.layer] || '#64748b')
|
|
657
|
+
.attr('stroke', '#0f172a').attr('stroke-width', 2)
|
|
658
|
+
.attr('opacity', 0.85);
|
|
659
|
+
|
|
660
|
+
// Node labels
|
|
661
|
+
node.append('text')
|
|
662
|
+
.text(d => d.name.replace(/\\.[^.]+$/, ''))
|
|
663
|
+
.attr('x', 0).attr('y', d => -(Math.max(d.connections * 3 + 6, 8) + 6))
|
|
664
|
+
.attr('text-anchor', 'middle')
|
|
665
|
+
.attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
|
|
666
|
+
|
|
667
|
+
// Tooltip on hover
|
|
668
|
+
node.append('title')
|
|
669
|
+
.text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
|
|
670
|
+
|
|
671
|
+
simulation.on('tick', () => {
|
|
672
|
+
// Clamp nodes to stay within SVG bounds
|
|
673
|
+
nodes.forEach(d => {
|
|
674
|
+
const r = Math.max(d.connections * 3 + 6, 8) + 10;
|
|
675
|
+
d.x = Math.max(r, Math.min(width - r, d.x));
|
|
676
|
+
d.y = Math.max(r, Math.min(height - r, d.y));
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
link
|
|
680
|
+
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
681
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
682
|
+
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
683
|
+
});
|
|
684
|
+
})();
|
|
685
|
+
|
|
686
|
+
// ── Bubble Chart ──
|
|
687
|
+
(function() {
|
|
688
|
+
const dataEl = document.getElementById('bubble-data');
|
|
689
|
+
if (!dataEl) return;
|
|
690
|
+
|
|
691
|
+
const bubbles = JSON.parse(dataEl.textContent || '[]');
|
|
692
|
+
if (bubbles.length === 0) return;
|
|
693
|
+
|
|
694
|
+
const container = document.getElementById('bubble-chart');
|
|
695
|
+
const width = container.clientWidth || 600;
|
|
696
|
+
const height = 300;
|
|
697
|
+
|
|
698
|
+
const svg = d3.select('#bubble-chart').append('svg')
|
|
699
|
+
.attr('width', width).attr('height', height)
|
|
700
|
+
.attr('viewBox', [0, 0, width, height]);
|
|
701
|
+
|
|
702
|
+
const simulation = d3.forceSimulation(bubbles)
|
|
703
|
+
.force('charge', d3.forceManyBody().strength(5))
|
|
704
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
705
|
+
.force('collision', d3.forceCollide().radius(d => d.radius + 4))
|
|
706
|
+
.stop();
|
|
707
|
+
|
|
708
|
+
for (let i = 0; i < 120; i++) simulation.tick();
|
|
709
|
+
|
|
710
|
+
const g = svg.selectAll('g').data(bubbles).join('g')
|
|
711
|
+
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
712
|
+
|
|
713
|
+
// Glow effect
|
|
714
|
+
g.append('circle')
|
|
715
|
+
.attr('r', d => d.radius)
|
|
716
|
+
.attr('fill', d => d.color + '20')
|
|
717
|
+
.attr('stroke', d => d.color).attr('stroke-width', 2)
|
|
718
|
+
.attr('opacity', 0)
|
|
719
|
+
.transition().duration(800).delay((d, i) => i * 200)
|
|
720
|
+
.attr('opacity', 1);
|
|
721
|
+
|
|
722
|
+
// Inner circle
|
|
723
|
+
g.append('circle')
|
|
724
|
+
.attr('r', d => d.radius * 0.7)
|
|
725
|
+
.attr('fill', d => d.color + '30')
|
|
726
|
+
.attr('opacity', 0)
|
|
727
|
+
.transition().duration(800).delay((d, i) => i * 200)
|
|
728
|
+
.attr('opacity', 1);
|
|
729
|
+
|
|
730
|
+
// Name
|
|
731
|
+
g.append('text')
|
|
732
|
+
.text(d => d.name)
|
|
733
|
+
.attr('text-anchor', 'middle').attr('dy', '-0.3em')
|
|
734
|
+
.attr('fill', '#e2e8f0').attr('font-size', d => Math.max(d.radius / 4, 10) + 'px')
|
|
735
|
+
.attr('font-weight', '700');
|
|
736
|
+
|
|
737
|
+
// Count badge
|
|
738
|
+
g.append('text')
|
|
739
|
+
.text(d => '×' + d.count)
|
|
740
|
+
.attr('text-anchor', 'middle').attr('dy', '1.2em')
|
|
741
|
+
.attr('fill', d => d.color).attr('font-size', d => Math.max(d.radius / 5, 9) + 'px')
|
|
742
|
+
.attr('font-weight', '600');
|
|
743
|
+
|
|
744
|
+
// Severity label
|
|
745
|
+
g.append('text')
|
|
746
|
+
.text(d => d.severity)
|
|
747
|
+
.attr('text-anchor', 'middle').attr('dy', '2.5em')
|
|
748
|
+
.attr('fill', '#64748b').attr('font-size', '9px').attr('text-transform', 'uppercase');
|
|
749
|
+
})();
|
|
750
|
+
<\/script>`;
|
|
751
|
+
}
|
|
752
|
+
renderAgentSuggestions(s) {
|
|
753
|
+
const roleIcon = (name) => {
|
|
754
|
+
if (name.includes('ORCHESTRATOR'))
|
|
755
|
+
return '\u{1F3AD}';
|
|
756
|
+
if (name.includes('BACKEND') || name.includes('FRONTEND') || name.includes('DATABASE') || name.includes('FLUTTER'))
|
|
757
|
+
return '\u{1F4BB}';
|
|
758
|
+
if (name.includes('SECURITY'))
|
|
759
|
+
return '\u{1F6E1}\uFE0F';
|
|
760
|
+
if (name.includes('QA'))
|
|
761
|
+
return '\u{1F9EA}';
|
|
762
|
+
if (name.includes('TECH-DEBT'))
|
|
763
|
+
return '\u{1F4CA}';
|
|
764
|
+
return '\u{1F916}';
|
|
765
|
+
};
|
|
766
|
+
const roleLabel = (name) => {
|
|
767
|
+
if (name.includes('ORCHESTRATOR'))
|
|
768
|
+
return 'coordination';
|
|
769
|
+
if (name.includes('SECURITY'))
|
|
770
|
+
return 'protection';
|
|
771
|
+
if (name.includes('QA'))
|
|
772
|
+
return 'quality';
|
|
773
|
+
if (name.includes('TECH-DEBT'))
|
|
774
|
+
return 'governance';
|
|
775
|
+
return 'development';
|
|
776
|
+
};
|
|
777
|
+
const roleColor = (name) => {
|
|
778
|
+
if (name.includes('ORCHESTRATOR'))
|
|
779
|
+
return '#c084fc';
|
|
780
|
+
if (name.includes('SECURITY'))
|
|
781
|
+
return '#f87171';
|
|
782
|
+
if (name.includes('QA'))
|
|
783
|
+
return '#34d399';
|
|
784
|
+
if (name.includes('TECH-DEBT'))
|
|
785
|
+
return '#fbbf24';
|
|
786
|
+
return '#60a5fa';
|
|
787
|
+
};
|
|
788
|
+
const agentCards = s.suggestedAgents.map(a => `<label class="agent-toggle-card" data-category="agents" data-name="${a}">
|
|
789
|
+
<input type="checkbox" class="agent-check" checked data-type="agents" data-item="${a}">
|
|
790
|
+
<div class="agent-toggle-inner">
|
|
791
|
+
<div class="agent-toggle-icon">${roleIcon(a)}</div>
|
|
792
|
+
<div class="agent-toggle-info">
|
|
793
|
+
<span class="agent-toggle-name">${a}</span>
|
|
794
|
+
<span class="agent-toggle-role" style="color:${roleColor(a)}">${roleLabel(a)}</span>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="agent-toggle-check">\u2713</div>
|
|
797
|
+
</div>
|
|
798
|
+
</label>`).join('\n');
|
|
799
|
+
const ruleCards = s.suggestedRules.map(r => `<label class="agent-toggle-card mini" data-category="rules">
|
|
800
|
+
<input type="checkbox" class="agent-check" checked data-type="rules" data-item="${r}">
|
|
801
|
+
<div class="agent-toggle-inner">
|
|
802
|
+
<span class="agent-toggle-icon">\u{1F4CF}</span>
|
|
803
|
+
<span class="agent-toggle-name">${r}.md</span>
|
|
804
|
+
<div class="agent-toggle-check">\u2713</div>
|
|
805
|
+
</div>
|
|
806
|
+
</label>`).join('\n');
|
|
807
|
+
const guardCards = s.suggestedGuards.map(g => `<label class="agent-toggle-card mini" data-category="guards">
|
|
808
|
+
<input type="checkbox" class="agent-check" checked data-type="guards" data-item="${g}">
|
|
809
|
+
<div class="agent-toggle-inner">
|
|
810
|
+
<span class="agent-toggle-icon">\u{1F6E1}\uFE0F</span>
|
|
811
|
+
<span class="agent-toggle-name">${g}.md</span>
|
|
812
|
+
<div class="agent-toggle-check">\u2713</div>
|
|
813
|
+
</div>
|
|
814
|
+
</label>`).join('\n');
|
|
815
|
+
const workflowCards = s.suggestedWorkflows.map(w => `<label class="agent-toggle-card mini" data-category="workflows">
|
|
816
|
+
<input type="checkbox" class="agent-check" checked data-type="workflows" data-item="${w}">
|
|
817
|
+
<div class="agent-toggle-inner">
|
|
818
|
+
<span class="agent-toggle-icon">\u26A1</span>
|
|
819
|
+
<span class="agent-toggle-name">${w}.md</span>
|
|
820
|
+
<div class="agent-toggle-check">\u2713</div>
|
|
821
|
+
</div>
|
|
822
|
+
</label>`).join('\n');
|
|
823
|
+
const skillCards = s.suggestedSkills.map(sk => `<label class="agent-toggle-card" data-category="skills">
|
|
824
|
+
<input type="checkbox" class="agent-check" checked data-type="skills" data-item="${sk.source}">
|
|
825
|
+
<div class="agent-toggle-inner">
|
|
826
|
+
<span class="agent-toggle-icon">\u{1F9E0}</span>
|
|
827
|
+
<div class="agent-toggle-info">
|
|
828
|
+
<span class="agent-toggle-name">${sk.name}</span>
|
|
829
|
+
<span class="agent-toggle-role" style="color:#34d399">${sk.description}</span>
|
|
830
|
+
</div>
|
|
831
|
+
<div class="agent-toggle-check">\u2713</div>
|
|
832
|
+
</div>
|
|
833
|
+
</label>`).join('\n');
|
|
834
|
+
const auditSection = s.audit.filter(f => f.type !== 'OK').length > 0 ? `
|
|
835
|
+
<div class="agent-audit-section">
|
|
836
|
+
<h3 class="agent-section-subtitle">\u{1F50D} Audit Findings</h3>
|
|
837
|
+
<div class="agent-audit-grid">
|
|
838
|
+
${s.audit.filter(f => f.type !== 'OK').map(f => {
|
|
839
|
+
const icon = f.type === 'MISSING' ? '\u274C' : f.type === 'IMPROVEMENT' ? '\u{1F4A1}' : '\u26A0\uFE0F';
|
|
840
|
+
const cls = f.type === 'MISSING' ? 'audit-missing' : 'audit-improvement';
|
|
841
|
+
return `<div class="agent-audit-item ${cls}">
|
|
842
|
+
<span class="audit-icon">${icon}</span>
|
|
843
|
+
<div class="audit-content">
|
|
844
|
+
<span class="audit-desc">${f.description}</span>
|
|
845
|
+
${f.suggestion ? `<span class="audit-suggestion">\u2192 ${f.suggestion}</span>` : ''}
|
|
846
|
+
</div>
|
|
847
|
+
</div>`;
|
|
848
|
+
}).join('\n')}
|
|
849
|
+
</div>
|
|
850
|
+
</div>` : '';
|
|
851
|
+
const stackPills = [
|
|
852
|
+
`\u{1F527} ${s.stack.primary}`,
|
|
853
|
+
`\u{1F4E6} ${s.stack.frameworks.length > 0 ? s.stack.frameworks.join(', ') : 'No framework'}`,
|
|
854
|
+
s.hasExistingAgents ? '\u{1F4C1} Existing .agent/' : '\u{1F4C1} New .agent/',
|
|
855
|
+
[s.stack.hasBackend ? '\u{1F519} Backend' : '', s.stack.hasFrontend ? '\u{1F5A5}\uFE0F Frontend' : '', s.stack.hasMobile ? '\u{1F4F1} Mobile' : '', s.stack.hasDatabase ? '\u{1F5C4}\uFE0F DB' : ''].filter(Boolean).join('\n ')
|
|
856
|
+
];
|
|
857
|
+
const totalItems = s.suggestedAgents.length + s.suggestedRules.length + s.suggestedGuards.length + s.suggestedWorkflows.length + s.suggestedSkills.length;
|
|
858
|
+
return `
|
|
859
|
+
<h2 class="section-title">\u{1F916} Agent System (Suggested)</h2>
|
|
860
|
+
|
|
861
|
+
<div class="card agent-system-card">
|
|
862
|
+
<div class="agent-stack-banner">
|
|
863
|
+
${stackPills.map(p => `<div class="stack-pill">${p}</div>`).join('\n ')}
|
|
864
|
+
</div>
|
|
865
|
+
|
|
866
|
+
<div class="agent-controls">
|
|
867
|
+
<button class="agent-ctrl-btn" onclick="toggleAll(true)">\u2705 Select All</button>
|
|
868
|
+
<button class="agent-ctrl-btn" onclick="toggleAll(false)">\u2B1C Select None</button>
|
|
869
|
+
<span class="agent-count-label"><span id="agentSelectedCount">${totalItems}</span> selected</span>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
872
|
+
<h3 class="agent-section-subtitle">\u{1F916} Agents</h3>
|
|
873
|
+
<div class="agent-toggle-grid">
|
|
874
|
+
${agentCards}
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<div class="agent-extras-grid">
|
|
878
|
+
<div>
|
|
879
|
+
<h3 class="agent-section-subtitle">\u{1F4CF} Rules</h3>
|
|
880
|
+
<div class="agent-toggle-list">${ruleCards}</div>
|
|
881
|
+
</div>
|
|
882
|
+
<div>
|
|
883
|
+
<h3 class="agent-section-subtitle">\u{1F6E1}\uFE0F Guards</h3>
|
|
884
|
+
<div class="agent-toggle-list">${guardCards}</div>
|
|
885
|
+
</div>
|
|
886
|
+
<div>
|
|
887
|
+
<h3 class="agent-section-subtitle">\u26A1 Workflows</h3>
|
|
888
|
+
<div class="agent-toggle-list">${workflowCards}</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
|
|
892
|
+
<h3 class="agent-section-subtitle">\u{1F9E0} Skills <span style="font-size:0.7rem;color:#94a3b8;font-weight:400">from skills.sh</span></h3>
|
|
893
|
+
<div class="agent-toggle-grid">
|
|
894
|
+
${skillCards}
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
${auditSection}
|
|
898
|
+
|
|
899
|
+
<div class="agent-command-box">
|
|
900
|
+
<div class="agent-command-header">
|
|
901
|
+
<span>\u{1F4A1} Command to generate selected items:</span>
|
|
902
|
+
<button class="agent-copy-btn" onclick="copyAgentCommand()">
|
|
903
|
+
<span id="copyIcon">\u{1F4CB}</span> Copy
|
|
904
|
+
</button>
|
|
905
|
+
</div>
|
|
906
|
+
<code id="agentCommandOutput" class="agent-command-code">${s.command}</code>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
<style>
|
|
911
|
+
.agent-system-card { padding: 1.5rem; }
|
|
912
|
+
.agent-stack-banner { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
|
913
|
+
.stack-pill { background: #1e293b; border: 1px solid #334155; border-radius: 99px; padding: 0.4rem 1rem; font-size: 0.8rem; color: #94a3b8; white-space: pre-line; }
|
|
914
|
+
.agent-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
|
|
915
|
+
.agent-ctrl-btn { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.4rem 1rem; border-radius: 8px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s; }
|
|
916
|
+
.agent-ctrl-btn:hover { background: #334155; }
|
|
917
|
+
.agent-count-label { color: #94a3b8; font-size: 0.85rem; margin-left: auto; }
|
|
918
|
+
#agentSelectedCount { color: #c084fc; font-weight: 700; }
|
|
919
|
+
.agent-section-subtitle { color: #e2e8f0; font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 0.75rem; }
|
|
920
|
+
.agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; }
|
|
921
|
+
.agent-toggle-card { cursor: pointer; transition: all 0.3s; }
|
|
922
|
+
.agent-toggle-card input { display: none; }
|
|
923
|
+
.agent-toggle-inner { display: flex; align-items: center; gap: 0.75rem; background: #1e293b; border: 2px solid #334155; border-radius: 12px; padding: 0.75rem 1rem; transition: all 0.3s; }
|
|
924
|
+
.agent-toggle-card input:checked + .agent-toggle-inner { border-color: #818cf8; background: #1e1b4b; }
|
|
925
|
+
.agent-toggle-icon { font-size: 1.3rem; flex-shrink: 0; }
|
|
926
|
+
.agent-toggle-info { flex: 1; min-width: 0; }
|
|
927
|
+
.agent-toggle-name { display: block; color: #e2e8f0; font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
928
|
+
.agent-toggle-role { display: block; font-size: 0.7rem; margin-top: 0.15rem; }
|
|
929
|
+
.agent-toggle-check { color: #334155; font-size: 1rem; flex-shrink: 0; transition: color 0.3s; }
|
|
930
|
+
.agent-toggle-card input:checked + .agent-toggle-inner .agent-toggle-check { color: #818cf8; }
|
|
931
|
+
.agent-toggle-card.mini .agent-toggle-inner { padding: 0.5rem 0.75rem; border-radius: 8px; }
|
|
932
|
+
.agent-extras-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 0.5rem; }
|
|
933
|
+
@media (max-width: 768px) { .agent-extras-grid { grid-template-columns: 1fr; } }
|
|
934
|
+
.agent-toggle-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
935
|
+
.agent-audit-section { margin-top: 1.5rem; }
|
|
936
|
+
.agent-audit-grid { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
937
|
+
.agent-audit-item { display: flex; gap: 0.75rem; align-items: flex-start; background: #1e293b; padding: 0.75rem 1rem; border-radius: 8px; }
|
|
938
|
+
.agent-audit-item.audit-missing { border-left: 3px solid #ef4444; }
|
|
939
|
+
.agent-audit-item.audit-improvement { border-left: 3px solid #fbbf24; }
|
|
940
|
+
.audit-icon { font-size: 1rem; flex-shrink: 0; margin-top: 2px; }
|
|
941
|
+
.audit-content { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
942
|
+
.audit-desc { color: #e2e8f0; font-size: 0.85rem; }
|
|
943
|
+
.audit-suggestion { color: #94a3b8; font-size: 0.8rem; font-style: italic; }
|
|
944
|
+
.agent-command-box { margin-top: 1.5rem; background: #0f172a; border-radius: 12px; border: 1px solid #334155; overflow: hidden; }
|
|
945
|
+
.agent-command-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: #1e293b; font-size: 0.8rem; color: #94a3b8; }
|
|
946
|
+
.agent-copy-btn { background: #c084fc; color: #0f172a; border: none; border-radius: 6px; padding: 0.4rem 0.8rem; cursor: pointer; font-size: 0.75rem; font-weight: 600; transition: all 0.2s; }
|
|
947
|
+
.agent-copy-btn:hover { background: #a855f7; transform: scale(1.05); }
|
|
948
|
+
.agent-command-code { display: block; padding: 1rem; color: #c084fc; font-size: 0.85rem; word-break: break-all; font-family: 'Fira Code', monospace; }
|
|
949
|
+
</style>
|
|
950
|
+
|
|
951
|
+
<script>
|
|
952
|
+
(function() {
|
|
953
|
+
var basePath = ${JSON.stringify(s.command.replace('architect agents ', ''))};
|
|
954
|
+
var totalItems = ${totalItems};
|
|
955
|
+
function updateCommand() {
|
|
956
|
+
var checks = document.querySelectorAll('.agent-check');
|
|
957
|
+
var selected = { agents: [], rules: [], guards: [], workflows: [], skills: [] };
|
|
958
|
+
var count = 0;
|
|
959
|
+
checks.forEach(function(cb) { if (cb.checked) { selected[cb.dataset.type].push(cb.dataset.item); count++; } });
|
|
960
|
+
document.getElementById('agentSelectedCount').textContent = count;
|
|
961
|
+
var cmd;
|
|
962
|
+
if (count === totalItems) { cmd = 'architect agents ' + basePath; }
|
|
963
|
+
else if (count === 0) { cmd = '# No items selected'; }
|
|
964
|
+
else {
|
|
965
|
+
var parts = ['architect agents ' + basePath];
|
|
966
|
+
if (selected.agents.length > 0) parts.push('--agents ' + selected.agents.join(','));
|
|
967
|
+
if (selected.rules.length > 0) parts.push('--rules ' + selected.rules.join(','));
|
|
968
|
+
if (selected.guards.length > 0) parts.push('--guards ' + selected.guards.join(','));
|
|
969
|
+
if (selected.workflows.length > 0) parts.push('--workflows ' + selected.workflows.join(','));
|
|
970
|
+
if (selected.skills.length > 0) parts.push('&& ' + selected.skills.map(function(sk){ return 'npx skills add ' + sk; }).join(' && '));
|
|
971
|
+
cmd = parts.join(' ');
|
|
972
|
+
}
|
|
973
|
+
document.getElementById('agentCommandOutput').textContent = cmd;
|
|
974
|
+
}
|
|
975
|
+
document.querySelectorAll('.agent-check').forEach(function(cb) { cb.addEventListener('change', updateCommand); });
|
|
976
|
+
window.toggleAll = function(state) { document.querySelectorAll('.agent-check').forEach(function(cb) { cb.checked = state; }); updateCommand(); };
|
|
977
|
+
window.copyAgentCommand = function() { var cmd = document.getElementById('agentCommandOutput').textContent; navigator.clipboard.writeText(cmd).then(function() { var btn = document.getElementById('copyIcon'); btn.textContent = '\u2705'; setTimeout(function() { btn.textContent = '\ud83d\udccb'; }, 2000); }); };
|
|
978
|
+
})();
|
|
979
|
+
<\/script>`;
|
|
279
980
|
}
|
|
280
981
|
getStyles() {
|
|
281
982
|
return `<style>
|
|
@@ -377,6 +1078,20 @@ ${this.renderFooter()}
|
|
|
377
1078
|
}
|
|
378
1079
|
.success-card { border-color: #22c55e40; color: #22c55e; text-align: center; padding: 2rem; font-size: 1.1rem; }
|
|
379
1080
|
|
|
1081
|
+
/* ── Graph ── */
|
|
1082
|
+
.graph-card { padding: 1rem; }
|
|
1083
|
+
.graph-legend {
|
|
1084
|
+
display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
|
|
1085
|
+
justify-content: center;
|
|
1086
|
+
}
|
|
1087
|
+
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: #94a3b8; }
|
|
1088
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
1089
|
+
.graph-hint {
|
|
1090
|
+
text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
|
|
1091
|
+
font-style: italic;
|
|
1092
|
+
}
|
|
1093
|
+
#dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; }
|
|
1094
|
+
|
|
380
1095
|
/* ── Layers Grid ── */
|
|
381
1096
|
.layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
|
|
382
1097
|
.layer-card {
|
|
@@ -417,11 +1132,6 @@ ${this.renderFooter()}
|
|
|
417
1132
|
.locations { font-size: 0.75rem; color: #64748b; }
|
|
418
1133
|
.locations code { background: #0f172a; padding: 1px 4px; border-radius: 3px; font-size: 0.7rem; }
|
|
419
1134
|
|
|
420
|
-
/* ── Mermaid ── */
|
|
421
|
-
.mermaid-container {
|
|
422
|
-
background: #f8fafc; border-radius: 12px; padding: 2rem; text-align: center; color: #0f172a;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
1135
|
/* ── Footer ── */
|
|
426
1136
|
.footer {
|
|
427
1137
|
text-align: center; padding: 2rem; color: #475569; font-size: 0.85rem;
|
|
@@ -430,12 +1140,102 @@ ${this.renderFooter()}
|
|
|
430
1140
|
.footer a { color: #818cf8; text-decoration: none; }
|
|
431
1141
|
.footer a:hover { text-decoration: underline; }
|
|
432
1142
|
|
|
1143
|
+
/* ── Refactoring Plan ── */
|
|
1144
|
+
.refactor-score { padding: 2rem; }
|
|
1145
|
+
.refactor-score-pair {
|
|
1146
|
+
display: flex; align-items: center; justify-content: center; gap: 1.5rem;
|
|
1147
|
+
margin-bottom: 2rem; flex-wrap: wrap;
|
|
1148
|
+
}
|
|
1149
|
+
.rscore-box { text-align: center; }
|
|
1150
|
+
.rscore-num { font-size: 3rem; font-weight: 900; line-height: 1; }
|
|
1151
|
+
.rscore-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
|
|
1152
|
+
.rscore-improvement { font-size: 1.3rem; font-weight: 700; }
|
|
1153
|
+
|
|
1154
|
+
.refactor-legend { display: flex; gap: 1rem; margin-bottom: 0.5rem; }
|
|
1155
|
+
.rlegend-tag { font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 6px; }
|
|
1156
|
+
.rlegend-tag.rbefore { background: rgba(255,255,255,0.05); color: #94a3b8; }
|
|
1157
|
+
.rlegend-tag.rafter { background: rgba(129,140,248,0.2); color: #818cf8; }
|
|
1158
|
+
|
|
1159
|
+
.refactor-metric-name { width: 100px; font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; font-weight: 600; }
|
|
1160
|
+
.refactor-metric-bars { flex: 1; position: relative; height: 30px; }
|
|
1161
|
+
.rbar-before, .rbar-after {
|
|
1162
|
+
position: absolute; left: 0; height: 14px; border-radius: 4px;
|
|
1163
|
+
display: flex; align-items: center; padding-left: 6px;
|
|
1164
|
+
font-size: 0.7rem; font-weight: 600;
|
|
1165
|
+
}
|
|
1166
|
+
.rbar-before { top: 0; }
|
|
1167
|
+
.rbar-after { top: 15px; }
|
|
1168
|
+
.refactor-metric-diff { width: 50px; text-align: right; font-weight: 700; font-size: 0.85rem; }
|
|
1169
|
+
|
|
1170
|
+
.refactor-stats-row {
|
|
1171
|
+
display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;
|
|
1172
|
+
}
|
|
1173
|
+
.rstat {
|
|
1174
|
+
background: #1e293b; border: 1px solid #334155; border-radius: 99px;
|
|
1175
|
+
padding: 0.4rem 1rem; font-size: 0.85rem; color: #94a3b8; font-weight: 500;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.priority-bar {
|
|
1179
|
+
display: flex; border-radius: 12px; overflow: hidden; height: 32px; margin-bottom: 2rem;
|
|
1180
|
+
}
|
|
1181
|
+
.prio-seg {
|
|
1182
|
+
display: flex; align-items: center; justify-content: center;
|
|
1183
|
+
font-size: 0.75rem; font-weight: 600;
|
|
1184
|
+
}
|
|
1185
|
+
.prio-critical { background: #ef444430; color: #ef4444; }
|
|
1186
|
+
.prio-high { background: #f59e0b30; color: #f59e0b; }
|
|
1187
|
+
.prio-medium { background: #3b82f630; color: #60a5fa; }
|
|
1188
|
+
.prio-low { background: #22c55e30; color: #22c55e; }
|
|
1189
|
+
|
|
1190
|
+
.refactor-roadmap { display: flex; flex-direction: column; gap: 1rem; }
|
|
1191
|
+
.rstep-card {
|
|
1192
|
+
background: #1e293b; border-radius: 16px; border: 1px solid #334155;
|
|
1193
|
+
padding: 1.5rem; transition: border-color 0.2s;
|
|
1194
|
+
}
|
|
1195
|
+
.rstep-card:hover { border-color: #818cf8; }
|
|
1196
|
+
.rstep-header { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
|
1197
|
+
.rstep-number {
|
|
1198
|
+
width: 40px; height: 40px; border-radius: 50%;
|
|
1199
|
+
background: linear-gradient(135deg, #818cf8, #c084fc);
|
|
1200
|
+
display: flex; align-items: center; justify-content: center;
|
|
1201
|
+
font-weight: 800; font-size: 1rem; color: white; flex-shrink: 0;
|
|
1202
|
+
}
|
|
1203
|
+
.rstep-info { flex: 1; }
|
|
1204
|
+
.rstep-title-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
|
1205
|
+
.rstep-title-row h3 { font-size: 1.1rem; font-weight: 700; }
|
|
1206
|
+
.rstep-desc { color: #94a3b8; font-size: 0.9rem; margin-top: 0.3rem; }
|
|
1207
|
+
.tier-badge {
|
|
1208
|
+
background: #818cf815; color: #818cf8; border: 1px solid #818cf830;
|
|
1209
|
+
padding: 0.15rem 0.5rem; border-radius: 99px; font-size: 0.65rem; font-weight: 600;
|
|
1210
|
+
}
|
|
1211
|
+
.rstep-details { margin-top: 0.5rem; }
|
|
1212
|
+
.rstep-details summary { cursor: pointer; color: #818cf8; font-size: 0.85rem; font-weight: 500; }
|
|
1213
|
+
.rstep-rationale { color: #64748b; font-size: 0.85rem; margin-top: 0.3rem; font-style: italic; }
|
|
1214
|
+
|
|
1215
|
+
.rstep-ops { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
|
|
1216
|
+
.rstep-ops h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
|
|
1217
|
+
.rop { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
|
|
1218
|
+
.rop-icon { font-size: 0.9rem; }
|
|
1219
|
+
.rop-badge { padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.65rem; font-weight: 700; }
|
|
1220
|
+
.rop-path { background: #0f172a; padding: 1px 6px; border-radius: 4px; font-size: 0.8rem; color: #c084fc; }
|
|
1221
|
+
.rop-arrow { color: #818cf8; font-weight: 700; }
|
|
1222
|
+
.rop-desc { width: 100%; color: #64748b; font-size: 0.8rem; padding-left: 1.8rem; }
|
|
1223
|
+
|
|
1224
|
+
.rstep-impact { margin-top: 0.5rem; }
|
|
1225
|
+
.rstep-impact h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.3rem; }
|
|
1226
|
+
.rimpact-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
1227
|
+
.rimpact-tag {
|
|
1228
|
+
background: #22c55e10; color: #22c55e; border: 1px solid #22c55e30;
|
|
1229
|
+
padding: 0.2rem 0.6rem; border-radius: 8px; font-size: 0.75rem; font-weight: 500;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
433
1232
|
/* ── Responsive ── */
|
|
434
1233
|
@media (max-width: 768px) {
|
|
435
1234
|
.score-hero { flex-direction: column; gap: 1.5rem; }
|
|
436
1235
|
.score-breakdown { grid-template-columns: 1fr; }
|
|
437
1236
|
.header h1 { font-size: 1.8rem; }
|
|
438
1237
|
.container { padding: 1rem; }
|
|
1238
|
+
.refactor-score-pair { flex-direction: column; }
|
|
439
1239
|
}
|
|
440
1240
|
|
|
441
1241
|
/* ── Print ── */
|
|
@@ -446,7 +1246,6 @@ ${this.renderFooter()}
|
|
|
446
1246
|
.card, .stat-card, .score-hero, .layer-card, .score-item {
|
|
447
1247
|
background: white; border-color: #e2e8f0;
|
|
448
1248
|
}
|
|
449
|
-
.mermaid-container { border: 1px solid #e2e8f0; }
|
|
450
1249
|
}
|
|
451
1250
|
</style>`;
|
|
452
1251
|
}
|