codedna-analyzer 1.4.0__py3-none-any.whl
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.
- codedna/__init__.py +3 -0
- codedna/analyzers/__init__.py +1 -0
- codedna/analyzers/ai_analyzer.py +111 -0
- codedna/analyzers/architecture_detector.py +150 -0
- codedna/analyzers/cache_manager.py +70 -0
- codedna/analyzers/code_smell_detector.py +302 -0
- codedna/analyzers/complexity_analyzer.py +329 -0
- codedna/analyzers/dependency_mapper.py +171 -0
- codedna/analyzers/developer_analyzer.py +148 -0
- codedna/analyzers/diff_engine.py +395 -0
- codedna/analyzers/dna_generator.py +308 -0
- codedna/analyzers/evolution_engine.py +193 -0
- codedna/analyzers/github_analyzer.py +86 -0
- codedna/analyzers/language_detector.py +151 -0
- codedna/analyzers/repo_cloner.py +91 -0
- codedna/analyzers/security_detector.py +271 -0
- codedna/analyzers/structure_analyzer.py +91 -0
- codedna/ci/__init__.py +5 -0
- codedna/ci/gatekeeper.py +421 -0
- codedna/cli.py +839 -0
- codedna/compliance/__init__.py +5 -0
- codedna/compliance/sbom.py +410 -0
- codedna/config.py +167 -0
- codedna/mcp/__init__.py +5 -0
- codedna/mcp/protocol.py +65 -0
- codedna/mcp/server.py +393 -0
- codedna/remediation/__init__.py +5 -0
- codedna/remediation/planner.py +244 -0
- codedna/visualization/__init__.py +1 -0
- codedna/visualization/html_export.py +507 -0
- codedna/visualization/renderer.py +373 -0
- codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
- codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
- codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
- codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
- codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
- codedna_analyzer-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""HTML Export — generates interactive HTML dashboards for CodeDNA profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html as html_lib
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
HTML_TEMPLATE = """<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>CodeDNA Profile - {source}</title>
|
|
14
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
15
|
+
<script type="module">
|
|
16
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
17
|
+
mermaid.initialize({{ startOnLoad: true, theme: 'dark' }});
|
|
18
|
+
</script>
|
|
19
|
+
<style>
|
|
20
|
+
body {{ background-color: #0f172a; color: #f8fafc; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }}
|
|
21
|
+
.card {{ background-color: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; transition: border-color 0.2s ease; }}
|
|
22
|
+
.card:hover {{ border-color: #475569; }}
|
|
23
|
+
.header-bg {{ background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%); border-bottom: 1px solid #334155; }}
|
|
24
|
+
.radar-chart polygon {{ transition: all 0.3s ease; }}
|
|
25
|
+
.tab-btn.active {{ border-bottom: 2px solid #06b6d4; color: #38bdf8; }}
|
|
26
|
+
@media print {{
|
|
27
|
+
body {{ background: #ffffff !important; color: #000000 !important; }}
|
|
28
|
+
.card {{ background: #ffffff !important; border: 1px solid #e2e8f0 !important; color: #0f172a !important; box-shadow: none !important; }}
|
|
29
|
+
.header-bg {{ background: #f8fafc !important; color: #0f172a !important; border-bottom: 2px solid #cbd5e1 !important; }}
|
|
30
|
+
.no-print {{ display: none !important; }}
|
|
31
|
+
h1, h2, h3, p, span, li {{ color: #0f172a !important; text-shadow: none !important; }}
|
|
32
|
+
}}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body class="min-h-screen font-sans antialiased">
|
|
36
|
+
|
|
37
|
+
<!-- Header -->
|
|
38
|
+
<header class="header-bg py-8 px-6">
|
|
39
|
+
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
40
|
+
<div>
|
|
41
|
+
<div class="flex items-center gap-3">
|
|
42
|
+
<span class="text-3xl">🧬</span>
|
|
43
|
+
<h1 class="text-3xl md:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-sky-300 to-blue-500">
|
|
44
|
+
CodeDNA Profile
|
|
45
|
+
</h1>
|
|
46
|
+
<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-cyan-950/80 text-cyan-300 border border-cyan-800">{version_str}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<p class="text-slate-400 text-base flex items-center gap-2 mt-2">
|
|
49
|
+
<svg class="w-4 h-4 text-cyan-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
|
|
50
|
+
<span class="font-mono text-slate-300">{source}</span>
|
|
51
|
+
</p>
|
|
52
|
+
{github_stats}
|
|
53
|
+
</div>
|
|
54
|
+
<div class="flex flex-col items-start md:items-end">
|
|
55
|
+
<span class="inline-block px-4 py-2 rounded-lg bg-slate-800/90 border border-slate-700/80 font-mono text-cyan-400 font-semibold shadow-lg text-sm">
|
|
56
|
+
{signature}
|
|
57
|
+
</span>
|
|
58
|
+
<p class="text-xs text-slate-400 mt-2">Analyzed on {analyzed_at}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</header>
|
|
62
|
+
|
|
63
|
+
<main class="max-w-7xl mx-auto px-6 py-8 space-y-8">
|
|
64
|
+
|
|
65
|
+
<!-- KPI Metric Cards -->
|
|
66
|
+
<section class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
|
67
|
+
<div class="card p-5">
|
|
68
|
+
<h3 class="text-slate-400 text-xs uppercase tracking-wider font-semibold mb-1">System Type</h3>
|
|
69
|
+
<p class="text-2xl font-bold text-white tracking-tight">{system_type}</p>
|
|
70
|
+
<p class="text-xs text-slate-400 mt-1">Architecture pattern</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="card p-5">
|
|
73
|
+
<h3 class="text-slate-400 text-xs uppercase tracking-wider font-semibold mb-1">Overall Health</h3>
|
|
74
|
+
<p class="text-2xl font-bold {health_color} tracking-tight">{health_score}</p>
|
|
75
|
+
<p class="text-xs text-slate-400 mt-1">Quality & hygiene rating</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="card p-5">
|
|
78
|
+
<h3 class="text-slate-400 text-xs uppercase tracking-wider font-semibold mb-1">Total Files</h3>
|
|
79
|
+
<p class="text-2xl font-bold text-white tracking-tight">{total_files}</p>
|
|
80
|
+
<p class="text-xs text-slate-400 mt-1">Source codebase size</p>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="card p-5">
|
|
83
|
+
<h3 class="text-slate-400 text-xs uppercase tracking-wider font-semibold mb-1">Contributors</h3>
|
|
84
|
+
<p class="text-2xl font-bold text-white tracking-tight">{total_contributors}</p>
|
|
85
|
+
<p class="text-xs text-slate-400 mt-1">Bus Factor: <strong class="text-cyan-400">{bus_factor}</strong></p>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
<!-- Radar Chart & Executive Overview -->
|
|
90
|
+
<section class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
|
91
|
+
<div class="card p-6 lg:col-span-5 flex flex-col justify-between">
|
|
92
|
+
<div>
|
|
93
|
+
<h2 class="text-lg font-bold text-slate-200 mb-1 flex items-center gap-2">
|
|
94
|
+
🎯 Architecture & Quality Radar
|
|
95
|
+
</h2>
|
|
96
|
+
<p class="text-xs text-slate-400 mb-4">Five-dimensional assessment of software stability and design.</p>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="flex items-center justify-center py-2">
|
|
99
|
+
{radar_chart_svg}
|
|
100
|
+
</div>
|
|
101
|
+
<div class="text-center pt-2 border-t border-slate-800 text-xs text-slate-400">
|
|
102
|
+
Balanced polygons indicate resilient, well-modularized architectures.
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="card p-6 lg:col-span-7 flex flex-col justify-between">
|
|
107
|
+
<div>
|
|
108
|
+
<h2 class="text-lg font-bold text-slate-200 mb-1 flex items-center gap-2">
|
|
109
|
+
📊 Language Distribution
|
|
110
|
+
</h2>
|
|
111
|
+
<p class="text-xs text-slate-400 mb-4">Polyglot breakdown by percentage and source file count.</p>
|
|
112
|
+
<div class="space-y-3.5">
|
|
113
|
+
{language_bars}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="mt-6 pt-4 border-t border-slate-800 grid grid-cols-2 gap-4 text-xs text-slate-400">
|
|
117
|
+
<div>Infrastructure Traits: <strong class="text-slate-200">{traits_count}</strong></div>
|
|
118
|
+
<div>Evolution Commits: <strong class="text-slate-200">{total_commits}</strong></div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<!-- Deep Dive Grid -->
|
|
124
|
+
<section class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
125
|
+
|
|
126
|
+
<!-- Left Column: Dependency Diagram & Code Smells -->
|
|
127
|
+
<div class="lg:col-span-8 space-y-6">
|
|
128
|
+
|
|
129
|
+
<!-- Architecture Dependencies -->
|
|
130
|
+
<div class="card p-6">
|
|
131
|
+
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
|
|
132
|
+
<h2 class="text-lg font-bold text-slate-200 flex items-center gap-2">
|
|
133
|
+
🔗 Module Dependency Graph
|
|
134
|
+
</h2>
|
|
135
|
+
<span class="text-xs px-2.5 py-1 rounded bg-slate-800 text-slate-400 border border-slate-700">
|
|
136
|
+
Interactive Topology
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="bg-slate-900/90 rounded-lg p-4 w-full overflow-x-auto min-h-[300px] flex items-center justify-center border border-slate-800">
|
|
140
|
+
<div class="mermaid">
|
|
141
|
+
{mermaid_graph}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Risk Signals with Interactive Filter -->
|
|
147
|
+
<div class="card p-6 border-l-4 border-l-red-500">
|
|
148
|
+
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4">
|
|
149
|
+
<div>
|
|
150
|
+
<h2 class="text-lg font-bold text-slate-200 flex items-center gap-2">
|
|
151
|
+
⚠️ Risk Signals & Structural Smells
|
|
152
|
+
</h2>
|
|
153
|
+
<p class="text-xs text-slate-400">Identified anti-patterns, secrets, and high-complexity modules.</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="no-print">
|
|
156
|
+
<input type="text" id="riskSearch" onkeyup="filterRisks()" placeholder="Filter risks..."
|
|
157
|
+
class="bg-slate-900 border border-slate-700 rounded-md px-3 py-1 text-xs text-slate-200 focus:outline-none focus:border-cyan-500">
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<ul id="riskList" class="space-y-2.5 text-sm">
|
|
161
|
+
{risk_signals}
|
|
162
|
+
</ul>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Right Column: Sidebar Specs -->
|
|
168
|
+
<div class="lg:col-span-4 space-y-6">
|
|
169
|
+
|
|
170
|
+
<!-- Architecture Traits -->
|
|
171
|
+
<div class="card p-6">
|
|
172
|
+
<h2 class="text-lg font-bold text-slate-200 mb-3 flex items-center gap-2">
|
|
173
|
+
🏗️ Architecture Traits
|
|
174
|
+
</h2>
|
|
175
|
+
<ul class="space-y-2 text-sm text-slate-300">
|
|
176
|
+
{architecture_traits}
|
|
177
|
+
</ul>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Developer Genome -->
|
|
181
|
+
<div class="card p-6">
|
|
182
|
+
<div class="flex justify-between items-center mb-3">
|
|
183
|
+
<h2 class="text-lg font-bold text-slate-200 flex items-center gap-2">
|
|
184
|
+
👥 Developer Genome
|
|
185
|
+
</h2>
|
|
186
|
+
<span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400">Bus Factor: {bus_factor}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="space-y-2.5">
|
|
189
|
+
{top_contributors}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- Evolution Timeline -->
|
|
194
|
+
<div class="card p-6">
|
|
195
|
+
<h2 class="text-lg font-bold text-slate-200 mb-3 flex items-center gap-2">
|
|
196
|
+
📈 Evolution
|
|
197
|
+
</h2>
|
|
198
|
+
<ul class="space-y-2 text-sm text-slate-300">
|
|
199
|
+
<li class="flex justify-between"><span>Total Commits</span> <strong class="text-white">{total_commits}</strong></li>
|
|
200
|
+
<li class="flex justify-between"><span>First Commit</span> <strong class="text-white">{first_commit}</strong></li>
|
|
201
|
+
</ul>
|
|
202
|
+
<div class="mt-4 pt-4 border-t border-slate-700">
|
|
203
|
+
<p class="text-xs text-slate-400 uppercase font-semibold mb-2">Detected Patterns</p>
|
|
204
|
+
<div class="flex flex-wrap gap-1.5">
|
|
205
|
+
{evolution_patterns}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
</section>
|
|
213
|
+
|
|
214
|
+
</main>
|
|
215
|
+
|
|
216
|
+
<footer class="border-t border-slate-800 py-6 text-center text-slate-500 text-sm mt-12">
|
|
217
|
+
Generated by <a href="https://github.com/shenald-dev/codedna" class="text-cyan-400 hover:underline">CodeDNA</a> — A genetic analyzer for software.
|
|
218
|
+
</footer>
|
|
219
|
+
|
|
220
|
+
<script>
|
|
221
|
+
function filterRisks() {{
|
|
222
|
+
const query = document.getElementById('riskSearch').value.toLowerCase();
|
|
223
|
+
const items = document.querySelectorAll('#riskList li');
|
|
224
|
+
items.forEach(el => {{
|
|
225
|
+
const text = el.innerText.toLowerCase();
|
|
226
|
+
el.style.display = text.includes(query) ? '' : 'none';
|
|
227
|
+
}});
|
|
228
|
+
}}
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
</body>
|
|
232
|
+
</html>
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class HTMLExporter:
|
|
237
|
+
"""Exports a DNA profile to an interactive, executive-ready HTML dashboard."""
|
|
238
|
+
|
|
239
|
+
def export(self, profile: dict, mermaid_graph: str = "") -> str:
|
|
240
|
+
"""Generate HTML string for the profile."""
|
|
241
|
+
|
|
242
|
+
# Format GitHub Stats if present
|
|
243
|
+
github_stats = ""
|
|
244
|
+
if profile.get("github"):
|
|
245
|
+
gh = profile["github"]
|
|
246
|
+
if gh.get("is_github"):
|
|
247
|
+
def _fmt_num(val):
|
|
248
|
+
if isinstance(val, str):
|
|
249
|
+
try:
|
|
250
|
+
val = float(val)
|
|
251
|
+
if val.is_integer():
|
|
252
|
+
val = int(val)
|
|
253
|
+
except ValueError:
|
|
254
|
+
pass
|
|
255
|
+
return f"{val:,}" if isinstance(val, (int, float)) else html_lib.escape(str(val))
|
|
256
|
+
|
|
257
|
+
stars_str = _fmt_num(gh.get('stars', 0))
|
|
258
|
+
forks_str = _fmt_num(gh.get('forks', 0))
|
|
259
|
+
issues_str = _fmt_num(gh.get('issues', 0))
|
|
260
|
+
|
|
261
|
+
github_stats = f"""
|
|
262
|
+
<div class="flex items-center gap-4 mt-3 text-sm font-medium text-slate-400">
|
|
263
|
+
<span class="flex items-center gap-1"><span class="text-yellow-400">★</span> {stars_str}</span>
|
|
264
|
+
<span class="flex items-center gap-1"><span class="text-slate-300">⑂</span> {forks_str}</span>
|
|
265
|
+
<span class="flex items-center gap-1"><span class="text-green-400">⊙</span> {issues_str} issues</span>
|
|
266
|
+
</div>
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Format Health
|
|
270
|
+
health_score = profile["health"]["overall"]
|
|
271
|
+
health_color = (
|
|
272
|
+
"text-green-400" if health_score == "Healthy"
|
|
273
|
+
else "text-yellow-400" if health_score == "Fair"
|
|
274
|
+
else "text-orange-400" if health_score == "Needs Attention"
|
|
275
|
+
else "text-red-500"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Format Languages
|
|
279
|
+
lang_bars = []
|
|
280
|
+
for lang, data in profile.get("languages", {}).get("languages", {}).items():
|
|
281
|
+
pct = data['percentage']
|
|
282
|
+
color = self._lang_tw_color(lang)
|
|
283
|
+
files = data.get('files', 0)
|
|
284
|
+
lines_str = ""
|
|
285
|
+
if "lines" in data and data["lines"] is not None:
|
|
286
|
+
lines_str = f", {html_lib.escape(str(data['lines']))} lines"
|
|
287
|
+
lang_bars.append(f"""
|
|
288
|
+
<div>
|
|
289
|
+
<div class="flex justify-between text-sm mb-1">
|
|
290
|
+
<span class="font-medium text-slate-200">{html_lib.escape(str(lang))}</span>
|
|
291
|
+
<span class="text-slate-400">{html_lib.escape(str(pct))}% <span class="text-xs ml-1">({html_lib.escape(str(files))} files{lines_str})</span></span>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="w-full bg-slate-800 rounded-full h-2">
|
|
294
|
+
<div class="{color} h-2 rounded-full" style="width: {pct}%"></div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
""")
|
|
298
|
+
|
|
299
|
+
# Format Risks
|
|
300
|
+
risks = profile.get("risks", [])
|
|
301
|
+
risk_html = ""
|
|
302
|
+
if not risks:
|
|
303
|
+
risk_html = '<li class="text-green-400">✓ No critical risks detected.</li>'
|
|
304
|
+
else:
|
|
305
|
+
for risk in risks:
|
|
306
|
+
icon = "🔴" if "🔴" in risk else "🟡"
|
|
307
|
+
clean_risk = risk.replace("🔴 ", "").replace("🟡 ", "")
|
|
308
|
+
risk_html += f'<li class="flex items-start gap-2 text-slate-300 risk-item"><span class="shrink-0">{icon}</span><span>{html_lib.escape(str(clean_risk))}</span></li>'
|
|
309
|
+
|
|
310
|
+
# Architecture Traits
|
|
311
|
+
traits = profile.get("architecture", {}).get("traits", [])
|
|
312
|
+
traits_html = "".join(f'<li class="flex items-center gap-2"><span class="text-green-400">✓</span> {html_lib.escape(str(t))}</li>' for t in traits)
|
|
313
|
+
if not traits_html:
|
|
314
|
+
traits_html = '<li class="text-slate-500 italic">No standard traits detected</li>'
|
|
315
|
+
|
|
316
|
+
# Developer Genome
|
|
317
|
+
devs = profile.get("developer_genome", {}).get("top_contributors", [])
|
|
318
|
+
devs_html = ""
|
|
319
|
+
for d in devs:
|
|
320
|
+
devs_html += f"""
|
|
321
|
+
<div class="flex justify-between items-center bg-slate-800/60 p-2.5 rounded border border-slate-700/50">
|
|
322
|
+
<div>
|
|
323
|
+
<p class="text-sm font-medium text-slate-200">{html_lib.escape(str(d['name']))}</p>
|
|
324
|
+
<p class="text-xs text-cyan-400">{html_lib.escape(str(d['role']))}</p>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="text-right">
|
|
327
|
+
<p class="text-sm font-bold text-white">{html_lib.escape(str(d['commits']))}</p>
|
|
328
|
+
<p class="text-xs text-slate-500">commits</p>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
# Evolution Patterns
|
|
334
|
+
evo_patterns = profile.get("evolution", {}).get("patterns", [])
|
|
335
|
+
evo_html = "".join(f'<span class="px-2 py-1 bg-blue-500/10 text-blue-400 border border-blue-500/20 rounded text-xs">{html_lib.escape(str(p))}</span>' for p in evo_patterns)
|
|
336
|
+
|
|
337
|
+
# Mermaid Fallback
|
|
338
|
+
if not mermaid_graph:
|
|
339
|
+
mermaid_graph = "graph LR\n A[No connection data found]"
|
|
340
|
+
|
|
341
|
+
# Generate Pure SVG Radar Chart
|
|
342
|
+
radar_chart_svg = self._generate_radar_svg(profile)
|
|
343
|
+
|
|
344
|
+
# First commit date formatting
|
|
345
|
+
first_commit_raw = profile.get("evolution", {}).get("first_commit")
|
|
346
|
+
first_commit = html_lib.escape(str(first_commit_raw[:10])) if first_commit_raw else "Unknown"
|
|
347
|
+
|
|
348
|
+
version_val = profile.get("metadata", {}).get("version", "1.3.0")
|
|
349
|
+
version_str = f"v{version_val}" if not str(version_val).startswith("v") else str(version_val)
|
|
350
|
+
|
|
351
|
+
html = HTML_TEMPLATE.format(
|
|
352
|
+
version_str=html_lib.escape(version_str),
|
|
353
|
+
source=html_lib.escape(str(profile["metadata"]["source"])),
|
|
354
|
+
analyzed_at=html_lib.escape(str(profile["metadata"]["analyzed_at"][:10])),
|
|
355
|
+
github_stats=github_stats,
|
|
356
|
+
signature=html_lib.escape(str(profile["signature"])),
|
|
357
|
+
system_type=html_lib.escape(str(profile["system_type"])),
|
|
358
|
+
health_score=html_lib.escape(str(health_score)),
|
|
359
|
+
health_color=health_color,
|
|
360
|
+
total_files=html_lib.escape(str(profile["structure_stats"]["total_files"])),
|
|
361
|
+
total_contributors=html_lib.escape(str(profile["developer_genome"]["total_contributors"])),
|
|
362
|
+
mermaid_graph=mermaid_graph,
|
|
363
|
+
language_bars="\n".join(lang_bars),
|
|
364
|
+
risk_signals=risk_html,
|
|
365
|
+
architecture_traits=traits_html,
|
|
366
|
+
traits_count=len(traits),
|
|
367
|
+
bus_factor=html_lib.escape(str(profile["developer_genome"]["bus_factor"])),
|
|
368
|
+
top_contributors=devs_html,
|
|
369
|
+
total_commits=html_lib.escape(str(profile["evolution"]["total_commits"])),
|
|
370
|
+
first_commit=first_commit,
|
|
371
|
+
evolution_patterns=evo_html,
|
|
372
|
+
radar_chart_svg=radar_chart_svg,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return html
|
|
376
|
+
|
|
377
|
+
def _generate_radar_svg(self, profile: dict) -> str:
|
|
378
|
+
"""Generate a responsive, standalone 5-axis SVG radar chart."""
|
|
379
|
+
cx, cy, r = 160, 140, 95
|
|
380
|
+
axes = [
|
|
381
|
+
("Health", self._compute_health_metric(profile)),
|
|
382
|
+
("Security", self._compute_security_metric(profile)),
|
|
383
|
+
("Modularity", self._compute_modularity_metric(profile)),
|
|
384
|
+
("Cohesion", self._compute_cohesion_metric(profile)),
|
|
385
|
+
("Resilience", self._compute_resilience_metric(profile)),
|
|
386
|
+
]
|
|
387
|
+
n = len(axes)
|
|
388
|
+
angles = [-math.pi / 2 + (2 * math.pi * i / n) for i in range(n)]
|
|
389
|
+
|
|
390
|
+
# Background grid polygons (20%, 40%, 60%, 80%, 100%)
|
|
391
|
+
grid_polys = []
|
|
392
|
+
for level in [0.2, 0.4, 0.6, 0.8, 1.0]:
|
|
393
|
+
pts = []
|
|
394
|
+
for angle in angles:
|
|
395
|
+
x = cx + r * level * math.cos(angle)
|
|
396
|
+
y = cy + r * level * math.sin(angle)
|
|
397
|
+
pts.append(f"{x:.1f},{y:.1f}")
|
|
398
|
+
grid_polys.append(
|
|
399
|
+
f'<polygon points="{" ".join(pts)}" fill="none" stroke="#334155" stroke-width="1" stroke-dasharray="2 2"/>'
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Spoke lines & labels
|
|
403
|
+
spokes = []
|
|
404
|
+
labels = []
|
|
405
|
+
for (name, val), angle in zip(axes, angles):
|
|
406
|
+
x2 = cx + r * math.cos(angle)
|
|
407
|
+
y2 = cy + r * math.sin(angle)
|
|
408
|
+
spokes.append(f'<line x1="{cx}" y1="{cy}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="#334155" stroke-width="1"/>')
|
|
409
|
+
|
|
410
|
+
# Label positioned slightly outside the radius
|
|
411
|
+
lx = cx + (r + 24) * math.cos(angle)
|
|
412
|
+
ly = cy + (r + 14) * math.sin(angle)
|
|
413
|
+
anchor = "middle" if abs(math.cos(angle)) < 0.3 else ("start" if math.cos(angle) > 0 else "end")
|
|
414
|
+
labels.append(
|
|
415
|
+
f'<text x="{lx:.1f}" y="{ly:.1f}" fill="#94a3b8" font-size="11" font-weight="600" text-anchor="{anchor}">{name}</text>'
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Data polygon
|
|
419
|
+
data_pts = []
|
|
420
|
+
data_circles = []
|
|
421
|
+
for (name, val), angle in zip(axes, angles):
|
|
422
|
+
clamped = max(0.1, min(1.0, val / 100.0))
|
|
423
|
+
dx = cx + r * clamped * math.cos(angle)
|
|
424
|
+
dy = cy + r * clamped * math.sin(angle)
|
|
425
|
+
data_pts.append(f"{dx:.1f},{dy:.1f}")
|
|
426
|
+
data_circles.append(f'<circle cx="{dx:.1f}" cy="{dy:.1f}" r="4" fill="#38bdf8" stroke="#0f172a" stroke-width="2"/>')
|
|
427
|
+
|
|
428
|
+
data_polygon = (
|
|
429
|
+
f'<polygon points="{" ".join(data_pts)}" '
|
|
430
|
+
f'fill="rgba(6, 182, 212, 0.28)" stroke="#06b6d4" stroke-width="2.5"/>'
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
svg = f"""
|
|
434
|
+
<svg viewBox="0 0 320 290" class="radar-chart w-full max-w-[320px]">
|
|
435
|
+
<defs>
|
|
436
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
437
|
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
438
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
439
|
+
</filter>
|
|
440
|
+
</defs>
|
|
441
|
+
{' '.join(grid_polys)}
|
|
442
|
+
{' '.join(spokes)}
|
|
443
|
+
{data_polygon}
|
|
444
|
+
{' '.join(data_circles)}
|
|
445
|
+
{' '.join(labels)}
|
|
446
|
+
</svg>
|
|
447
|
+
"""
|
|
448
|
+
return svg
|
|
449
|
+
|
|
450
|
+
def _compute_health_metric(self, profile: dict) -> float:
|
|
451
|
+
overall = profile.get("health", {}).get("overall", "Unknown")
|
|
452
|
+
mapping = {"Healthy": 95, "Fair": 75, "Needs Attention": 50, "Critical": 25}
|
|
453
|
+
return float(mapping.get(overall, 60))
|
|
454
|
+
|
|
455
|
+
def _compute_security_metric(self, profile: dict) -> float:
|
|
456
|
+
sec = profile.get("security", {})
|
|
457
|
+
if sec.get("has_secrets", False):
|
|
458
|
+
return 25.0
|
|
459
|
+
crit = sec.get("total_critical", 0)
|
|
460
|
+
return max(30.0, 100.0 - (crit * 20.0))
|
|
461
|
+
|
|
462
|
+
def _compute_modularity_metric(self, profile: dict) -> float:
|
|
463
|
+
deps = profile.get("dependencies", {})
|
|
464
|
+
score = 85.0
|
|
465
|
+
if deps.get("has_circular_deps"):
|
|
466
|
+
score -= 30.0
|
|
467
|
+
density = deps.get("density", 0.0)
|
|
468
|
+
if density > 0.5:
|
|
469
|
+
score -= 20.0
|
|
470
|
+
return max(20.0, score)
|
|
471
|
+
|
|
472
|
+
def _compute_cohesion_metric(self, profile: dict) -> float:
|
|
473
|
+
arch = profile.get("architecture", {})
|
|
474
|
+
patterns = arch.get("detected_patterns", [])
|
|
475
|
+
if patterns:
|
|
476
|
+
best_conf = patterns[0].get("confidence", 0.7)
|
|
477
|
+
return round(best_conf * 100.0, 1)
|
|
478
|
+
return 70.0
|
|
479
|
+
|
|
480
|
+
def _compute_resilience_metric(self, profile: dict) -> float:
|
|
481
|
+
dev = profile.get("developer_genome", {})
|
|
482
|
+
bus = dev.get("bus_factor", 1)
|
|
483
|
+
contribs = dev.get("total_contributors", 1)
|
|
484
|
+
if bus >= 3 or contribs > 10:
|
|
485
|
+
return 95.0
|
|
486
|
+
elif bus == 2 or contribs > 3:
|
|
487
|
+
return 75.0
|
|
488
|
+
elif contribs > 1:
|
|
489
|
+
return 55.0
|
|
490
|
+
return 35.0
|
|
491
|
+
|
|
492
|
+
def _lang_tw_color(self, lang: str) -> str:
|
|
493
|
+
colors = {
|
|
494
|
+
"Python": "bg-yellow-400",
|
|
495
|
+
"JavaScript": "bg-yellow-300",
|
|
496
|
+
"TypeScript": "bg-blue-400",
|
|
497
|
+
"Go": "bg-cyan-400",
|
|
498
|
+
"Rust": "bg-orange-500",
|
|
499
|
+
"Java": "bg-red-500",
|
|
500
|
+
"C#": "bg-purple-500",
|
|
501
|
+
"Ruby": "bg-red-400",
|
|
502
|
+
"PHP": "bg-indigo-400",
|
|
503
|
+
"HTML": "bg-orange-400",
|
|
504
|
+
"CSS": "bg-blue-500",
|
|
505
|
+
"Shell": "bg-green-400",
|
|
506
|
+
}
|
|
507
|
+
return colors.get(lang, "bg-slate-400")
|