@eduardbar/drift 0.9.0 → 1.0.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/.github/workflows/publish-vscode.yml +76 -0
- package/AGENTS.md +30 -12
- package/CHANGELOG.md +9 -0
- package/README.md +273 -168
- package/ROADMAP.md +130 -98
- package/dist/analyzer.d.ts +4 -38
- package/dist/analyzer.js +85 -1510
- package/dist/cli.js +47 -4
- package/dist/config.js +1 -1
- package/dist/fix.d.ts +13 -0
- package/dist/fix.js +120 -0
- package/dist/git/blame.d.ts +22 -0
- package/dist/git/blame.js +227 -0
- package/dist/git/helpers.d.ts +36 -0
- package/dist/git/helpers.js +152 -0
- package/dist/git/trend.d.ts +21 -0
- package/dist/git/trend.js +80 -0
- package/dist/git.d.ts +0 -4
- package/dist/git.js +2 -2
- package/dist/report.js +620 -293
- package/dist/rules/phase0-basic.d.ts +11 -0
- package/dist/rules/phase0-basic.js +176 -0
- package/dist/rules/phase1-complexity.d.ts +31 -0
- package/dist/rules/phase1-complexity.js +277 -0
- package/dist/rules/phase2-crossfile.d.ts +27 -0
- package/dist/rules/phase2-crossfile.js +122 -0
- package/dist/rules/phase3-arch.d.ts +31 -0
- package/dist/rules/phase3-arch.js +148 -0
- package/dist/rules/phase5-ai.d.ts +8 -0
- package/dist/rules/phase5-ai.js +262 -0
- package/dist/rules/phase8-semantic.d.ts +22 -0
- package/dist/rules/phase8-semantic.js +109 -0
- package/dist/rules/shared.d.ts +7 -0
- package/dist/rules/shared.js +27 -0
- package/package.json +8 -3
- package/packages/vscode-drift/.vscodeignore +9 -0
- package/packages/vscode-drift/LICENSE +21 -0
- package/packages/vscode-drift/README.md +64 -0
- package/packages/vscode-drift/images/icon.png +0 -0
- package/packages/vscode-drift/images/icon.svg +30 -0
- package/packages/vscode-drift/package-lock.json +485 -0
- package/packages/vscode-drift/package.json +119 -0
- package/packages/vscode-drift/src/analyzer.ts +38 -0
- package/packages/vscode-drift/src/diagnostics.ts +55 -0
- package/packages/vscode-drift/src/extension.ts +111 -0
- package/packages/vscode-drift/src/statusbar.ts +47 -0
- package/packages/vscode-drift/src/treeview.ts +108 -0
- package/packages/vscode-drift/tsconfig.json +18 -0
- package/packages/vscode-drift/vscode-drift-0.1.0.vsix +0 -0
- package/packages/vscode-drift/vscode-drift-0.1.1.vsix +0 -0
- package/src/analyzer.ts +124 -1726
- package/src/cli.ts +53 -4
- package/src/config.ts +1 -1
- package/src/fix.ts +154 -0
- package/src/git/blame.ts +279 -0
- package/src/git/helpers.ts +198 -0
- package/src/git/trend.ts +116 -0
- package/src/git.ts +2 -2
- package/src/report.ts +631 -296
- package/src/rules/phase0-basic.ts +187 -0
- package/src/rules/phase1-complexity.ts +302 -0
- package/src/rules/phase2-crossfile.ts +149 -0
- package/src/rules/phase3-arch.ts +179 -0
- package/src/rules/phase5-ai.ts +292 -0
- package/src/rules/phase8-semantic.ts +132 -0
- package/src/rules/shared.ts +39 -0
- package/tests/helpers.ts +45 -0
- package/tests/rules.test.ts +1269 -0
- package/vitest.config.ts +15 -0
package/dist/report.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
// drift-ignore-file
|
|
1
2
|
import { basename } from 'node:path';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
const require = createRequire(import.meta.url);
|
|
4
5
|
const { version: VERSION } = require('../package.json');
|
|
6
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
5
7
|
function severityColor(severity) {
|
|
6
8
|
switch (severity) {
|
|
7
9
|
case 'error': return '#ef4444';
|
|
8
|
-
case 'warning': return '#
|
|
9
|
-
case 'info': return '#
|
|
10
|
+
case 'warning': return '#f59e0b';
|
|
11
|
+
case 'info': return '#3b82f6';
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
function severityIcon(severity) {
|
|
@@ -34,6 +36,17 @@ function scoreLabel(score) {
|
|
|
34
36
|
return 'HIGH';
|
|
35
37
|
return 'CRITICAL';
|
|
36
38
|
}
|
|
39
|
+
function scoreGrade(score) {
|
|
40
|
+
if (score < 20)
|
|
41
|
+
return 'A';
|
|
42
|
+
if (score < 40)
|
|
43
|
+
return 'B';
|
|
44
|
+
if (score < 60)
|
|
45
|
+
return 'C';
|
|
46
|
+
if (score < 80)
|
|
47
|
+
return 'D';
|
|
48
|
+
return 'F';
|
|
49
|
+
}
|
|
37
50
|
function escapeHtml(str) {
|
|
38
51
|
return str
|
|
39
52
|
.replace(/&/g, '&')
|
|
@@ -42,114 +55,25 @@ function escapeHtml(str) {
|
|
|
42
55
|
.replace(/"/g, '"')
|
|
43
56
|
.replace(/'/g, ''');
|
|
44
57
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
year: 'numeric', month: 'short', day: 'numeric',
|
|
49
|
-
hour: '2-digit', minute: '2-digit',
|
|
50
|
-
});
|
|
51
|
-
const projColor = scoreColor(report.totalScore);
|
|
52
|
-
const projLabel = scoreLabel(report.totalScore);
|
|
53
|
-
// count totals
|
|
54
|
-
let totalErrors = 0;
|
|
55
|
-
let totalWarnings = 0;
|
|
56
|
-
let totalInfos = 0;
|
|
57
|
-
for (const f of report.files) {
|
|
58
|
-
for (const issue of f.issues) {
|
|
59
|
-
if (issue.severity === 'error')
|
|
60
|
-
totalErrors++;
|
|
61
|
-
else if (issue.severity === 'warning')
|
|
62
|
-
totalWarnings++;
|
|
63
|
-
else
|
|
64
|
-
totalInfos++;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const filesWithIssues = report.files.filter(f => f.issues.length > 0).length;
|
|
68
|
-
// top issues by rule
|
|
69
|
-
const byRule = {};
|
|
70
|
-
for (const f of report.files) {
|
|
71
|
-
for (const issue of f.issues) {
|
|
72
|
-
if (!byRule[issue.rule]) {
|
|
73
|
-
byRule[issue.rule] = { count: 0, severity: issue.severity };
|
|
74
|
-
}
|
|
75
|
-
byRule[issue.rule].count++;
|
|
76
|
-
// escalate severity if needed
|
|
77
|
-
const cur = byRule[issue.rule].severity;
|
|
78
|
-
if (issue.severity === 'error')
|
|
79
|
-
byRule[issue.rule].severity = 'error';
|
|
80
|
-
else if (issue.severity === 'warning' && cur !== 'error')
|
|
81
|
-
byRule[issue.rule].severity = 'warning';
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const topRules = Object.entries(byRule)
|
|
85
|
-
.sort((a, b) => b[1].count - a[1].count)
|
|
86
|
-
.slice(0, 15);
|
|
87
|
-
const topRulesRows = topRules.map(([rule, { count, severity }]) => {
|
|
88
|
-
const icon = severityIcon(severity);
|
|
89
|
-
const color = severityColor(severity);
|
|
90
|
-
return `
|
|
91
|
-
<tr>
|
|
92
|
-
<td><span class="sev-icon" style="color:${color}">${icon}</span> <span class="rule-name">${escapeHtml(rule)}</span></td>
|
|
93
|
-
<td class="count-cell">${count}</td>
|
|
94
|
-
</tr>`;
|
|
95
|
-
}).join('');
|
|
96
|
-
// files sections — already sorted by score desc from buildReport
|
|
97
|
-
const fileSections = report.files
|
|
98
|
-
.filter(f => f.issues.length > 0)
|
|
99
|
-
.map(f => {
|
|
100
|
-
const hasError = f.issues.some(i => i.severity === 'error');
|
|
101
|
-
const openAttr = hasError ? ' open' : '';
|
|
102
|
-
const fColor = scoreColor(f.score);
|
|
103
|
-
const fLabel = scoreLabel(f.score);
|
|
104
|
-
const issueItems = f.issues.map(issue => {
|
|
105
|
-
const ic = severityColor(issue.severity);
|
|
106
|
-
const ii = severityIcon(issue.severity);
|
|
107
|
-
const snippet = issue.snippet
|
|
108
|
-
? `<pre class="snippet"><code>${escapeHtml(issue.snippet)}</code></pre>`
|
|
109
|
-
: '';
|
|
110
|
-
return `
|
|
111
|
-
<li class="issue-item">
|
|
112
|
-
<div class="issue-header">
|
|
113
|
-
<span class="sev-icon" style="color:${ic}">${ii}</span>
|
|
114
|
-
<span class="issue-location">Line ${issue.line}${issue.column > 0 ? `:${issue.column}` : ''}</span>
|
|
115
|
-
<span class="issue-rule">${escapeHtml(issue.rule)}</span>
|
|
116
|
-
<span class="issue-message">${escapeHtml(issue.message)}</span>
|
|
117
|
-
</div>
|
|
118
|
-
${snippet}
|
|
119
|
-
</li>`;
|
|
120
|
-
}).join('');
|
|
121
|
-
return `
|
|
122
|
-
<details${openAttr} class="file-section">
|
|
123
|
-
<summary class="file-summary">
|
|
124
|
-
<span class="file-path">${escapeHtml(f.path)}</span>
|
|
125
|
-
<span class="file-score" style="color:${fColor}">${f.score} <span class="file-label">${fLabel}</span></span>
|
|
126
|
-
<span class="file-count">${f.issues.length} issue${f.issues.length !== 1 ? 's' : ''}</span>
|
|
127
|
-
</summary>
|
|
128
|
-
<ul class="issue-list">${issueItems}
|
|
129
|
-
</ul>
|
|
130
|
-
</details>`;
|
|
131
|
-
}).join('\n');
|
|
132
|
-
return `<!DOCTYPE html>
|
|
133
|
-
<html lang="en">
|
|
134
|
-
<head>
|
|
135
|
-
<meta charset="UTF-8" />
|
|
136
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
137
|
-
<title>drift report — ${escapeHtml(projectName)}</title>
|
|
138
|
-
<style>
|
|
58
|
+
// ─── CSS ────────────────────────────────────────────────────────────────────
|
|
59
|
+
function buildCss() {
|
|
60
|
+
return `
|
|
139
61
|
:root {
|
|
140
|
-
--bg:
|
|
141
|
-
--bg-card:
|
|
142
|
-
--bg-
|
|
143
|
-
--
|
|
144
|
-
--
|
|
145
|
-
--
|
|
146
|
-
--
|
|
147
|
-
--
|
|
148
|
-
--
|
|
149
|
-
--
|
|
150
|
-
--
|
|
151
|
-
--
|
|
152
|
-
--
|
|
62
|
+
--bg: #0a0a0f;
|
|
63
|
+
--bg-card: #12121a;
|
|
64
|
+
--bg-hover: #1a1a2e;
|
|
65
|
+
--bg-code: #0d0d17;
|
|
66
|
+
--border: #2a2a3a;
|
|
67
|
+
--text: #ffffff;
|
|
68
|
+
--muted: #94a3b8;
|
|
69
|
+
--accent: #6366f1;
|
|
70
|
+
--accent-2: #8b5cf6;
|
|
71
|
+
--error: #ef4444;
|
|
72
|
+
--warning: #f59e0b;
|
|
73
|
+
--info: #3b82f6;
|
|
74
|
+
--success: #22c55e;
|
|
75
|
+
--font-mono: ui-monospace, "Cascadia Code", "Fira Code", Consolas, monospace;
|
|
76
|
+
--radius: 6px;
|
|
153
77
|
}
|
|
154
78
|
|
|
155
79
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -158,338 +82,741 @@ export function generateHtmlReport(report) {
|
|
|
158
82
|
background: var(--bg);
|
|
159
83
|
color: var(--text);
|
|
160
84
|
font-family: var(--font-mono);
|
|
161
|
-
font-size:
|
|
85
|
+
font-size: 13px;
|
|
162
86
|
line-height: 1.6;
|
|
163
|
-
|
|
87
|
+
min-height: 100vh;
|
|
164
88
|
}
|
|
165
89
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
90
|
+
/* ── Layout ── */
|
|
91
|
+
#app {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
min-height: 100vh;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
header.top-header {
|
|
98
|
+
background: var(--bg-card);
|
|
99
|
+
border-bottom: 1px solid var(--border);
|
|
100
|
+
padding: 1rem 1.5rem;
|
|
169
101
|
}
|
|
170
102
|
|
|
171
|
-
|
|
172
|
-
.header {
|
|
103
|
+
.header-row {
|
|
173
104
|
display: flex;
|
|
174
|
-
flex-wrap: wrap;
|
|
175
105
|
align-items: center;
|
|
176
106
|
justify-content: space-between;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
border-bottom: 1px solid var(--border);
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
gap: 1rem;
|
|
109
|
+
margin-bottom: 1rem;
|
|
181
110
|
}
|
|
182
111
|
|
|
183
|
-
.
|
|
184
|
-
font-size: 1.
|
|
112
|
+
.project-title {
|
|
113
|
+
font-size: 1.2rem;
|
|
185
114
|
font-weight: 700;
|
|
186
115
|
letter-spacing: -0.02em;
|
|
187
116
|
}
|
|
188
117
|
|
|
189
|
-
.
|
|
118
|
+
.scan-meta {
|
|
190
119
|
color: var(--muted);
|
|
191
|
-
font-size: 0.
|
|
120
|
+
font-size: 0.75rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.stats-cards {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
gap: 0.75rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.stat-card {
|
|
130
|
+
background: var(--bg);
|
|
131
|
+
border: 1px solid var(--border);
|
|
132
|
+
border-radius: var(--radius);
|
|
133
|
+
padding: 0.65rem 1rem;
|
|
134
|
+
min-width: 130px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.stat-card .stat-value {
|
|
138
|
+
font-size: 1.5rem;
|
|
139
|
+
font-weight: 700;
|
|
140
|
+
line-height: 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.stat-card .stat-label {
|
|
144
|
+
color: var(--muted);
|
|
145
|
+
font-size: 0.7rem;
|
|
192
146
|
margin-top: 0.25rem;
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
letter-spacing: 0.06em;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.layout {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex: 1;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── Sidebar ── */
|
|
158
|
+
#sidebar {
|
|
159
|
+
width: 280px;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
background: var(--bg-card);
|
|
162
|
+
border-right: 1px solid var(--border);
|
|
163
|
+
overflow-y: auto;
|
|
164
|
+
padding: 1rem;
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
gap: 1.25rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.sidebar-block {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
gap: 0.5rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.sidebar-label {
|
|
177
|
+
font-size: 0.65rem;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
letter-spacing: 0.12em;
|
|
180
|
+
text-transform: uppercase;
|
|
181
|
+
color: var(--muted);
|
|
193
182
|
}
|
|
194
183
|
|
|
184
|
+
/* Score block in sidebar */
|
|
195
185
|
.score-block {
|
|
196
|
-
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: baseline;
|
|
188
|
+
gap: 0.6rem;
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
.score-number {
|
|
200
|
-
font-size:
|
|
192
|
+
font-size: 3rem;
|
|
201
193
|
font-weight: 800;
|
|
202
194
|
line-height: 1;
|
|
203
195
|
letter-spacing: -0.04em;
|
|
204
196
|
}
|
|
205
197
|
|
|
198
|
+
.score-right {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
gap: 0.2rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
206
204
|
.score-label {
|
|
207
|
-
font-size: 0.
|
|
205
|
+
font-size: 0.65rem;
|
|
208
206
|
font-weight: 600;
|
|
209
207
|
letter-spacing: 0.1em;
|
|
210
208
|
text-transform: uppercase;
|
|
211
|
-
margin-top: 0.2rem;
|
|
212
209
|
}
|
|
213
210
|
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
.grade-badge {
|
|
212
|
+
display: inline-block;
|
|
213
|
+
font-size: 1.1rem;
|
|
214
|
+
font-weight: 800;
|
|
215
|
+
width: 2rem;
|
|
216
|
+
height: 2rem;
|
|
217
|
+
line-height: 2rem;
|
|
218
|
+
text-align: center;
|
|
219
|
+
border-radius: 4px;
|
|
220
|
+
background: var(--bg-hover);
|
|
221
|
+
border: 1px solid var(--border);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Severity checkboxes */
|
|
225
|
+
.sev-check-list {
|
|
216
226
|
display: flex;
|
|
217
|
-
flex-
|
|
218
|
-
gap:
|
|
219
|
-
margin-bottom: 2rem;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
gap: 0.4rem;
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
.
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
.sev-check-item {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 0.5rem;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
font-size: 0.82rem;
|
|
237
|
+
padding: 0.25rem 0.4rem;
|
|
238
|
+
border-radius: 4px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.sev-check-item:hover { background: var(--bg-hover); }
|
|
242
|
+
|
|
243
|
+
.sev-check-item input[type="checkbox"] {
|
|
244
|
+
accent-color: var(--accent);
|
|
245
|
+
width: 14px;
|
|
246
|
+
height: 14px;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.sev-dot {
|
|
251
|
+
width: 8px;
|
|
252
|
+
height: 8px;
|
|
253
|
+
border-radius: 50%;
|
|
254
|
+
flex-shrink: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* File search */
|
|
258
|
+
#file-search {
|
|
259
|
+
width: 100%;
|
|
260
|
+
background: var(--bg);
|
|
225
261
|
border: 1px solid var(--border);
|
|
226
262
|
border-radius: var(--radius);
|
|
227
|
-
|
|
263
|
+
color: var(--text);
|
|
264
|
+
font-family: var(--font-mono);
|
|
265
|
+
font-size: 0.8rem;
|
|
266
|
+
padding: 0.45rem 0.7rem;
|
|
267
|
+
outline: none;
|
|
268
|
+
transition: border-color 0.15s;
|
|
228
269
|
}
|
|
229
270
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
#file-search:focus { border-color: var(--accent); }
|
|
272
|
+
#file-search::placeholder { color: var(--muted); }
|
|
273
|
+
|
|
274
|
+
/* Rules list */
|
|
275
|
+
.rules-list {
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
gap: 2px;
|
|
279
|
+
max-height: 280px;
|
|
280
|
+
overflow-y: auto;
|
|
234
281
|
}
|
|
235
282
|
|
|
236
|
-
.
|
|
237
|
-
|
|
283
|
+
.rule-filter-btn {
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
justify-content: space-between;
|
|
287
|
+
background: transparent;
|
|
288
|
+
border: none;
|
|
289
|
+
color: var(--text);
|
|
290
|
+
font-family: var(--font-mono);
|
|
238
291
|
font-size: 0.75rem;
|
|
239
|
-
|
|
292
|
+
padding: 0.35rem 0.5rem;
|
|
293
|
+
border-radius: 4px;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
text-align: left;
|
|
296
|
+
border-left: 3px solid transparent;
|
|
297
|
+
transition: background 0.1s, border-color 0.1s;
|
|
240
298
|
}
|
|
241
299
|
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
300
|
+
.rule-filter-btn:hover { background: var(--bg-hover); }
|
|
301
|
+
|
|
302
|
+
.rule-filter-btn.active {
|
|
303
|
+
border-left-color: var(--accent);
|
|
304
|
+
background: var(--bg-hover);
|
|
305
|
+
color: var(--accent);
|
|
247
306
|
}
|
|
248
307
|
|
|
249
|
-
|
|
250
|
-
.section-title {
|
|
251
|
-
font-size: 0.7rem;
|
|
252
|
-
font-weight: 600;
|
|
253
|
-
letter-spacing: 0.12em;
|
|
254
|
-
text-transform: uppercase;
|
|
308
|
+
.rule-filter-btn .rule-count {
|
|
255
309
|
color: var(--muted);
|
|
256
|
-
|
|
310
|
+
font-size: 0.7rem;
|
|
311
|
+
background: var(--bg);
|
|
312
|
+
border: 1px solid var(--border);
|
|
313
|
+
border-radius: 99px;
|
|
314
|
+
padding: 0 0.4rem;
|
|
315
|
+
min-width: 1.6rem;
|
|
316
|
+
text-align: center;
|
|
317
|
+
flex-shrink: 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.rule-filter-btn.active .rule-count {
|
|
321
|
+
background: var(--bg-hover);
|
|
322
|
+
border-color: var(--accent);
|
|
257
323
|
}
|
|
258
324
|
|
|
259
|
-
|
|
325
|
+
/* Reset button */
|
|
326
|
+
#reset-filters {
|
|
260
327
|
width: 100%;
|
|
261
|
-
|
|
262
|
-
margin-bottom: 2rem;
|
|
263
|
-
background: var(--bg-card);
|
|
328
|
+
background: transparent;
|
|
264
329
|
border: 1px solid var(--border);
|
|
265
330
|
border-radius: var(--radius);
|
|
266
|
-
|
|
331
|
+
color: var(--muted);
|
|
332
|
+
font-family: var(--font-mono);
|
|
333
|
+
font-size: 0.75rem;
|
|
334
|
+
padding: 0.4rem;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
transition: border-color 0.15s, color 0.15s;
|
|
267
337
|
}
|
|
268
338
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
letter-spacing: 0.08em;
|
|
273
|
-
text-transform: uppercase;
|
|
274
|
-
color: var(--muted);
|
|
275
|
-
padding: 0.6rem 1rem;
|
|
276
|
-
border-bottom: 1px solid var(--border);
|
|
339
|
+
#reset-filters:hover {
|
|
340
|
+
border-color: var(--accent);
|
|
341
|
+
color: var(--text);
|
|
277
342
|
}
|
|
278
343
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
344
|
+
/* ── Main panel ── */
|
|
345
|
+
#main {
|
|
346
|
+
flex: 1;
|
|
347
|
+
overflow-y: auto;
|
|
348
|
+
padding: 1rem 1.25rem;
|
|
283
349
|
}
|
|
284
350
|
|
|
285
|
-
.
|
|
351
|
+
.main-header {
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
margin-bottom: 1rem;
|
|
356
|
+
padding-bottom: 0.75rem;
|
|
357
|
+
border-bottom: 1px solid var(--border);
|
|
358
|
+
}
|
|
286
359
|
|
|
287
|
-
|
|
288
|
-
|
|
360
|
+
#issue-counter {
|
|
361
|
+
font-size: 0.75rem;
|
|
289
362
|
color: var(--muted);
|
|
290
|
-
font-size: 0.85rem;
|
|
291
|
-
width: 60px;
|
|
292
363
|
}
|
|
293
364
|
|
|
294
|
-
.rule-name { color: var(--text); }
|
|
295
|
-
.sev-icon { margin-right: 0.4rem; }
|
|
296
|
-
|
|
297
365
|
/* ── File sections ── */
|
|
298
|
-
.files-section { margin-top: 2rem; }
|
|
299
|
-
|
|
300
366
|
.file-section {
|
|
301
367
|
background: var(--bg-card);
|
|
302
368
|
border: 1px solid var(--border);
|
|
303
369
|
border-radius: var(--radius);
|
|
304
|
-
margin-bottom: 0.
|
|
370
|
+
margin-bottom: 0.6rem;
|
|
305
371
|
overflow: hidden;
|
|
306
372
|
}
|
|
307
373
|
|
|
308
|
-
.file-summary {
|
|
374
|
+
.file-section > summary {
|
|
309
375
|
display: flex;
|
|
310
376
|
flex-wrap: wrap;
|
|
311
377
|
align-items: center;
|
|
312
|
-
gap: 0.
|
|
313
|
-
padding: 0.
|
|
378
|
+
gap: 0.6rem;
|
|
379
|
+
padding: 0.65rem 0.9rem;
|
|
314
380
|
cursor: pointer;
|
|
315
381
|
user-select: none;
|
|
316
382
|
list-style: none;
|
|
317
383
|
}
|
|
318
384
|
|
|
319
|
-
.file-summary::-webkit-details-marker { display: none; }
|
|
385
|
+
.file-section > summary::-webkit-details-marker { display: none; }
|
|
320
386
|
|
|
321
|
-
.file-summary::before {
|
|
387
|
+
.file-section > summary::before {
|
|
322
388
|
content: '▶';
|
|
323
|
-
font-size: 0.
|
|
389
|
+
font-size: 0.6rem;
|
|
324
390
|
color: var(--muted);
|
|
325
391
|
transition: transform 0.15s;
|
|
326
392
|
flex-shrink: 0;
|
|
327
393
|
}
|
|
328
394
|
|
|
329
|
-
details[open] >
|
|
395
|
+
details[open] > summary::before { transform: rotate(90deg); }
|
|
396
|
+
|
|
397
|
+
.file-section > summary:hover { background: var(--bg-hover); }
|
|
330
398
|
|
|
331
|
-
.file-
|
|
399
|
+
.file-name {
|
|
400
|
+
font-size: 0.82rem;
|
|
401
|
+
font-weight: 600;
|
|
332
402
|
flex: 1;
|
|
333
|
-
font-size: 0.85rem;
|
|
334
403
|
word-break: break-all;
|
|
335
404
|
}
|
|
336
405
|
|
|
406
|
+
.file-path-full {
|
|
407
|
+
font-size: 0.7rem;
|
|
408
|
+
color: var(--muted);
|
|
409
|
+
word-break: break-all;
|
|
410
|
+
flex-basis: 100%;
|
|
411
|
+
padding-left: 1.2rem;
|
|
412
|
+
}
|
|
413
|
+
|
|
337
414
|
.file-score {
|
|
338
|
-
font-size: 0.
|
|
415
|
+
font-size: 0.85rem;
|
|
339
416
|
font-weight: 700;
|
|
417
|
+
white-space: nowrap;
|
|
340
418
|
}
|
|
341
419
|
|
|
342
|
-
.
|
|
343
|
-
font-size: 0.
|
|
420
|
+
.issue-badge {
|
|
421
|
+
font-size: 0.68rem;
|
|
344
422
|
font-weight: 600;
|
|
345
|
-
letter-spacing: 0.08em;
|
|
346
|
-
text-transform: uppercase;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.file-count {
|
|
350
|
-
font-size: 0.75rem;
|
|
351
|
-
color: var(--muted);
|
|
352
|
-
background: var(--bg);
|
|
353
|
-
border: 1px solid var(--border);
|
|
354
423
|
border-radius: 99px;
|
|
355
|
-
padding: 0.
|
|
424
|
+
padding: 0.08rem 0.5rem;
|
|
425
|
+
border: 1px solid;
|
|
356
426
|
white-space: nowrap;
|
|
357
427
|
}
|
|
358
428
|
|
|
359
|
-
|
|
360
|
-
.issue-
|
|
361
|
-
|
|
362
|
-
|
|
429
|
+
.issue-badge.error { color: var(--error); border-color: var(--error); background: #ef444418; }
|
|
430
|
+
.issue-badge.warning { color: var(--warning); border-color: var(--warning); background: #f59e0b18; }
|
|
431
|
+
.issue-badge.info { color: var(--info); border-color: var(--info); background: #3b82f618; }
|
|
432
|
+
|
|
433
|
+
/* ── Issue rows ── */
|
|
434
|
+
.issues-list {
|
|
435
|
+
padding: 0 0.9rem 0.75rem;
|
|
363
436
|
}
|
|
364
437
|
|
|
365
|
-
.issue-
|
|
366
|
-
|
|
438
|
+
.issue-row {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: 52px 20px 1fr;
|
|
441
|
+
grid-template-rows: auto auto;
|
|
442
|
+
column-gap: 0.5rem;
|
|
443
|
+
row-gap: 0.2rem;
|
|
444
|
+
padding: 0.55rem 0;
|
|
367
445
|
border-top: 1px solid var(--border);
|
|
446
|
+
font-size: 0.8rem;
|
|
368
447
|
}
|
|
369
448
|
|
|
370
|
-
.issue-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
font-size: 0.82rem;
|
|
449
|
+
.issue-line {
|
|
450
|
+
color: var(--muted);
|
|
451
|
+
font-size: 0.72rem;
|
|
452
|
+
white-space: nowrap;
|
|
453
|
+
align-self: center;
|
|
376
454
|
}
|
|
377
455
|
|
|
378
|
-
.issue-
|
|
379
|
-
|
|
456
|
+
.issue-sev {
|
|
457
|
+
align-self: center;
|
|
380
458
|
font-size: 0.75rem;
|
|
381
|
-
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.issue-rule-msg {
|
|
462
|
+
display: flex;
|
|
463
|
+
flex-wrap: wrap;
|
|
464
|
+
align-items: baseline;
|
|
465
|
+
gap: 0.4rem;
|
|
466
|
+
grid-column: 3;
|
|
382
467
|
}
|
|
383
468
|
|
|
384
469
|
.issue-rule {
|
|
385
470
|
background: var(--bg);
|
|
386
471
|
border: 1px solid var(--border);
|
|
387
472
|
border-radius: 3px;
|
|
388
|
-
padding: 0 0.
|
|
389
|
-
font-size: 0.
|
|
473
|
+
padding: 0 0.3rem;
|
|
474
|
+
font-size: 0.7rem;
|
|
390
475
|
color: var(--muted);
|
|
391
476
|
white-space: nowrap;
|
|
392
477
|
}
|
|
393
478
|
|
|
394
|
-
.issue-
|
|
479
|
+
.issue-msg {
|
|
395
480
|
color: var(--text);
|
|
396
|
-
|
|
481
|
+
font-size: 0.8rem;
|
|
397
482
|
}
|
|
398
483
|
|
|
399
|
-
|
|
400
|
-
|
|
484
|
+
.issue-snippet {
|
|
485
|
+
grid-column: 1 / -1;
|
|
401
486
|
background: var(--bg-code);
|
|
402
487
|
border: 1px solid var(--border);
|
|
403
|
-
border-radius:
|
|
404
|
-
padding: 0.
|
|
405
|
-
margin-top: 0.
|
|
488
|
+
border-radius: 4px;
|
|
489
|
+
padding: 0.5rem 0.75rem;
|
|
490
|
+
margin-top: 0.35rem;
|
|
491
|
+
font-size: 12px;
|
|
492
|
+
font-family: var(--font-mono);
|
|
406
493
|
overflow-x: auto;
|
|
407
|
-
|
|
494
|
+
white-space: pre;
|
|
408
495
|
line-height: 1.5;
|
|
409
496
|
}
|
|
410
497
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
498
|
+
/* ── Empty state ── */
|
|
499
|
+
.empty-state {
|
|
500
|
+
text-align: center;
|
|
501
|
+
color: var(--muted);
|
|
502
|
+
padding: 3rem 1rem;
|
|
503
|
+
font-size: 0.85rem;
|
|
414
504
|
}
|
|
415
505
|
|
|
416
506
|
/* ── Footer ── */
|
|
417
507
|
.footer {
|
|
418
|
-
|
|
419
|
-
padding-top: 1rem;
|
|
508
|
+
background: var(--bg-card);
|
|
420
509
|
border-top: 1px solid var(--border);
|
|
510
|
+
padding: 0.6rem 1.5rem;
|
|
421
511
|
color: var(--muted);
|
|
422
|
-
font-size: 0.
|
|
512
|
+
font-size: 0.7rem;
|
|
423
513
|
text-align: center;
|
|
424
514
|
}
|
|
425
515
|
|
|
516
|
+
/* ── Scrollbars ── */
|
|
517
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
518
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
519
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
|
|
520
|
+
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
521
|
+
|
|
426
522
|
/* ── Responsive ── */
|
|
427
|
-
@media (max-width:
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
523
|
+
@media (max-width: 700px) {
|
|
524
|
+
.layout { flex-direction: column; }
|
|
525
|
+
#sidebar {
|
|
526
|
+
width: 100%;
|
|
527
|
+
border-right: none;
|
|
528
|
+
border-bottom: 1px solid var(--border);
|
|
529
|
+
max-height: 50vh;
|
|
530
|
+
}
|
|
531
|
+
.score-number { font-size: 2.2rem; }
|
|
532
|
+
}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
// ─── JS ─────────────────────────────────────────────────────────────────────
|
|
536
|
+
function buildJs() {
|
|
537
|
+
return `
|
|
538
|
+
const state = {
|
|
539
|
+
severities: new Set(['error', 'warning', 'info']),
|
|
540
|
+
rule: null,
|
|
541
|
+
fileSearch: '',
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
function applyFilters() {
|
|
545
|
+
let visibleCount = 0;
|
|
546
|
+
let totalCount = 0;
|
|
547
|
+
|
|
548
|
+
document.querySelectorAll('.file-section').forEach(function(section) {
|
|
549
|
+
const issues = section.querySelectorAll('.issue-row');
|
|
550
|
+
let fileVisible = 0;
|
|
551
|
+
|
|
552
|
+
issues.forEach(function(issue) {
|
|
553
|
+
const sev = issue.dataset.severity;
|
|
554
|
+
const rule = issue.dataset.rule;
|
|
555
|
+
const visible =
|
|
556
|
+
state.severities.has(sev) &&
|
|
557
|
+
(state.rule === null || state.rule === rule);
|
|
558
|
+
|
|
559
|
+
issue.style.display = visible ? '' : 'none';
|
|
560
|
+
if (visible) { visibleCount++; fileVisible++; }
|
|
561
|
+
totalCount++;
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const filePath = (section.dataset.path || '').toLowerCase();
|
|
565
|
+
const searchMatch = state.fileSearch === '' || filePath.includes(state.fileSearch);
|
|
566
|
+
section.style.display = (fileVisible > 0 && searchMatch) ? '' : 'none';
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const counter = document.getElementById('issue-counter');
|
|
570
|
+
if (counter) {
|
|
571
|
+
counter.textContent = 'Showing ' + visibleCount + ' of ' + totalCount + ' issues';
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
document.querySelectorAll('.severity-filter').forEach(function(cb) {
|
|
576
|
+
cb.addEventListener('change', function() {
|
|
577
|
+
if (cb.checked) {
|
|
578
|
+
state.severities.add(cb.value);
|
|
579
|
+
} else {
|
|
580
|
+
state.severities.delete(cb.value);
|
|
581
|
+
}
|
|
582
|
+
applyFilters();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
var fileSearch = document.getElementById('file-search');
|
|
587
|
+
if (fileSearch) {
|
|
588
|
+
fileSearch.addEventListener('input', function(e) {
|
|
589
|
+
state.fileSearch = e.target.value.toLowerCase();
|
|
590
|
+
applyFilters();
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
document.querySelectorAll('.rule-filter-btn').forEach(function(btn) {
|
|
595
|
+
btn.addEventListener('click', function() {
|
|
596
|
+
if (state.rule === btn.dataset.rule) {
|
|
597
|
+
state.rule = null;
|
|
598
|
+
btn.classList.remove('active');
|
|
599
|
+
} else {
|
|
600
|
+
document.querySelectorAll('.rule-filter-btn').forEach(function(b) {
|
|
601
|
+
b.classList.remove('active');
|
|
602
|
+
});
|
|
603
|
+
state.rule = btn.dataset.rule;
|
|
604
|
+
btn.classList.add('active');
|
|
605
|
+
}
|
|
606
|
+
applyFilters();
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
var resetBtn = document.getElementById('reset-filters');
|
|
611
|
+
if (resetBtn) {
|
|
612
|
+
resetBtn.addEventListener('click', function() {
|
|
613
|
+
state.severities = new Set(['error', 'warning', 'info']);
|
|
614
|
+
state.rule = null;
|
|
615
|
+
state.fileSearch = '';
|
|
616
|
+
document.querySelectorAll('.severity-filter').forEach(function(cb) {
|
|
617
|
+
cb.checked = true;
|
|
618
|
+
});
|
|
619
|
+
document.querySelectorAll('.rule-filter-btn').forEach(function(b) {
|
|
620
|
+
b.classList.remove('active');
|
|
621
|
+
});
|
|
622
|
+
var fs = document.getElementById('file-search');
|
|
623
|
+
if (fs) fs.value = '';
|
|
624
|
+
applyFilters();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
applyFilters();
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
// ─── Main export ────────────────────────────────────────────────────────────
|
|
632
|
+
export function generateHtmlReport(report) {
|
|
633
|
+
const projectName = basename(report.targetPath);
|
|
634
|
+
const scanDate = new Date(report.scannedAt).toLocaleString('en-US', {
|
|
635
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
636
|
+
hour: '2-digit', minute: '2-digit',
|
|
637
|
+
});
|
|
638
|
+
const projColor = scoreColor(report.totalScore);
|
|
639
|
+
const projLabel = scoreLabel(report.totalScore);
|
|
640
|
+
const projGrade = scoreGrade(report.totalScore);
|
|
641
|
+
const filesWithIssues = report.files.filter(f => f.issues.length > 0).length;
|
|
642
|
+
// ── Top rules for sidebar ──────────────────────────────────────────────
|
|
643
|
+
const topRules = Object.entries(report.summary.byRule)
|
|
644
|
+
.sort(([, a], [, b]) => b - a)
|
|
645
|
+
.slice(0, 20);
|
|
646
|
+
const ruleItems = topRules.map(([rule, count]) => `
|
|
647
|
+
<button class="rule-filter-btn" data-rule="${escapeHtml(rule)}">
|
|
648
|
+
<span class="rule-name">${escapeHtml(rule)}</span>
|
|
649
|
+
<span class="rule-count">${count}</span>
|
|
650
|
+
</button>`).join('');
|
|
651
|
+
// ── File sections ──────────────────────────────────────────────────────
|
|
652
|
+
const fileSections = report.files
|
|
653
|
+
.filter(f => f.issues.length > 0)
|
|
654
|
+
.sort((a, b) => {
|
|
655
|
+
const aErr = a.issues.filter(i => i.severity === 'error').length;
|
|
656
|
+
const bErr = b.issues.filter(i => i.severity === 'error').length;
|
|
657
|
+
return bErr - aErr || b.score - a.score;
|
|
658
|
+
})
|
|
659
|
+
.map(f => {
|
|
660
|
+
const hasError = f.issues.some(i => i.severity === 'error');
|
|
661
|
+
const fColor = scoreColor(f.score);
|
|
662
|
+
const errCount = f.issues.filter(i => i.severity === 'error').length;
|
|
663
|
+
const warnCount = f.issues.filter(i => i.severity === 'warning').length;
|
|
664
|
+
const infoCount = f.issues.filter(i => i.severity === 'info').length;
|
|
665
|
+
const issueRows = f.issues.map(issue => {
|
|
666
|
+
const sev = issue.severity;
|
|
667
|
+
const ic = severityColor(sev);
|
|
668
|
+
const ii = severityIcon(sev);
|
|
669
|
+
const snippet = issue.snippet
|
|
670
|
+
? `<pre class="issue-snippet">${escapeHtml(issue.snippet)}</pre>`
|
|
671
|
+
: '';
|
|
672
|
+
const col = issue.column > 0 ? `:${issue.column}` : '';
|
|
673
|
+
return `
|
|
674
|
+
<div class="issue-row" data-severity="${sev}" data-rule="${escapeHtml(issue.rule)}">
|
|
675
|
+
<span class="issue-line">L${issue.line}${escapeHtml(col)}</span>
|
|
676
|
+
<span class="issue-sev" style="color:${ic}">${ii}</span>
|
|
677
|
+
<div class="issue-rule-msg">
|
|
678
|
+
<span class="issue-rule">${escapeHtml(issue.rule)}</span>
|
|
679
|
+
<span class="issue-msg">${escapeHtml(issue.message)}</span>
|
|
680
|
+
</div>
|
|
681
|
+
${snippet}
|
|
682
|
+
</div>`;
|
|
683
|
+
}).join('');
|
|
684
|
+
const badgesHtml = [
|
|
685
|
+
errCount > 0 ? `<span class="issue-badge error">${errCount} err</span>` : '',
|
|
686
|
+
warnCount > 0 ? `<span class="issue-badge warning">${warnCount} warn</span>` : '',
|
|
687
|
+
infoCount > 0 ? `<span class="issue-badge info">${infoCount} info</span>` : '',
|
|
688
|
+
].join('');
|
|
689
|
+
return `
|
|
690
|
+
<details class="file-section" data-path="${escapeHtml(f.path)}"${hasError ? ' open' : ''}>
|
|
691
|
+
<summary>
|
|
692
|
+
<span class="file-name">${escapeHtml(basename(f.path))}</span>
|
|
693
|
+
<span class="file-path-full">${escapeHtml(f.path)}</span>
|
|
694
|
+
<span class="file-score" style="color:${fColor}">${f.score}/100</span>
|
|
695
|
+
${badgesHtml}
|
|
696
|
+
</summary>
|
|
697
|
+
<div class="issues-list">${issueRows}
|
|
698
|
+
</div>
|
|
699
|
+
</details>`;
|
|
700
|
+
}).join('\n');
|
|
701
|
+
const noIssues = `<div class="empty-state">No issues found. Clean codebase.</div>`;
|
|
702
|
+
return `<!DOCTYPE html>
|
|
703
|
+
<html lang="en">
|
|
704
|
+
<head>
|
|
705
|
+
<meta charset="UTF-8" />
|
|
706
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
707
|
+
<title>drift report — ${escapeHtml(projectName)}</title>
|
|
708
|
+
<style>${buildCss()}</style>
|
|
433
709
|
</head>
|
|
434
710
|
<body>
|
|
435
|
-
<div
|
|
711
|
+
<div id="app">
|
|
436
712
|
|
|
437
713
|
<!-- Header -->
|
|
438
|
-
<header class="header">
|
|
439
|
-
<div class="header-
|
|
440
|
-
<
|
|
441
|
-
|
|
714
|
+
<header class="top-header">
|
|
715
|
+
<div class="header-row">
|
|
716
|
+
<div>
|
|
717
|
+
<div class="project-title">${escapeHtml(projectName)}</div>
|
|
718
|
+
<div class="scan-meta">Scanned ${escapeHtml(scanDate)} · drift v${VERSION}</div>
|
|
719
|
+
</div>
|
|
442
720
|
</div>
|
|
443
|
-
<div class="
|
|
444
|
-
<div class="
|
|
445
|
-
|
|
721
|
+
<div class="stats-cards">
|
|
722
|
+
<div class="stat-card">
|
|
723
|
+
<div class="stat-value">${report.totalFiles}</div>
|
|
724
|
+
<div class="stat-label">Total files</div>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="stat-card">
|
|
727
|
+
<div class="stat-value">${filesWithIssues}</div>
|
|
728
|
+
<div class="stat-label">Files with issues</div>
|
|
729
|
+
</div>
|
|
730
|
+
<div class="stat-card">
|
|
731
|
+
<div class="stat-value">${report.totalIssues}</div>
|
|
732
|
+
<div class="stat-label">Total issues</div>
|
|
733
|
+
</div>
|
|
734
|
+
<div class="stat-card">
|
|
735
|
+
<div class="stat-value" style="color:${projColor}">${report.totalScore}</div>
|
|
736
|
+
<div class="stat-label">Score (drift)</div>
|
|
737
|
+
</div>
|
|
446
738
|
</div>
|
|
447
739
|
</header>
|
|
448
740
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
<span style="color:var(--warning)">▲ ${totalWarnings}</span>
|
|
465
|
-
<span style="color:var(--info)">◦ ${totalInfos}</span>
|
|
741
|
+
<div class="layout">
|
|
742
|
+
|
|
743
|
+
<!-- Sidebar -->
|
|
744
|
+
<aside id="sidebar">
|
|
745
|
+
|
|
746
|
+
<!-- Score + Grade -->
|
|
747
|
+
<div class="sidebar-block">
|
|
748
|
+
<div class="sidebar-label">Drift Score</div>
|
|
749
|
+
<div class="score-block">
|
|
750
|
+
<div class="score-number" style="color:${projColor}">${report.totalScore}</div>
|
|
751
|
+
<div class="score-right">
|
|
752
|
+
<div class="score-label" style="color:${projColor}">${projLabel}</div>
|
|
753
|
+
<div class="grade-badge" style="color:${projColor}">${projGrade}</div>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
466
756
|
</div>
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
757
|
+
|
|
758
|
+
<!-- Severity filters -->
|
|
759
|
+
<div class="sidebar-block">
|
|
760
|
+
<div class="sidebar-label">Severity</div>
|
|
761
|
+
<div class="sev-check-list">
|
|
762
|
+
<label class="sev-check-item">
|
|
763
|
+
<input type="checkbox" class="severity-filter" value="error" checked />
|
|
764
|
+
<span class="sev-dot" style="background:var(--error)"></span>
|
|
765
|
+
<span style="color:var(--error)">✖ Error</span>
|
|
766
|
+
<span style="color:var(--muted);margin-left:auto;font-size:0.7rem">${report.summary.errors}</span>
|
|
767
|
+
</label>
|
|
768
|
+
<label class="sev-check-item">
|
|
769
|
+
<input type="checkbox" class="severity-filter" value="warning" checked />
|
|
770
|
+
<span class="sev-dot" style="background:var(--warning)"></span>
|
|
771
|
+
<span style="color:var(--warning)">▲ Warning</span>
|
|
772
|
+
<span style="color:var(--muted);margin-left:auto;font-size:0.7rem">${report.summary.warnings}</span>
|
|
773
|
+
</label>
|
|
774
|
+
<label class="sev-check-item">
|
|
775
|
+
<input type="checkbox" class="severity-filter" value="info" checked />
|
|
776
|
+
<span class="sev-dot" style="background:var(--info)"></span>
|
|
777
|
+
<span style="color:var(--info)">◦ Info</span>
|
|
778
|
+
<span style="color:var(--muted);margin-left:auto;font-size:0.7rem">${report.summary.infos}</span>
|
|
779
|
+
</label>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
<!-- File search -->
|
|
784
|
+
<div class="sidebar-block">
|
|
785
|
+
<div class="sidebar-label">Search files</div>
|
|
786
|
+
<input id="file-search" type="text" placeholder="Filter by filename…" autocomplete="off" />
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<!-- Rules list -->
|
|
790
|
+
${topRules.length > 0 ? `
|
|
791
|
+
<div class="sidebar-block">
|
|
792
|
+
<div class="sidebar-label">Rules (click to filter)</div>
|
|
793
|
+
<div class="rules-list">${ruleItems}
|
|
794
|
+
</div>
|
|
795
|
+
</div>` : ''}
|
|
796
|
+
|
|
797
|
+
<!-- Reset -->
|
|
798
|
+
<button id="reset-filters">Reset filters</button>
|
|
799
|
+
|
|
800
|
+
</aside>
|
|
801
|
+
|
|
802
|
+
<!-- Main -->
|
|
803
|
+
<main id="main">
|
|
804
|
+
<div class="main-header">
|
|
805
|
+
<span id="issue-counter" style="color:var(--muted);font-size:0.75rem">Loading…</span>
|
|
806
|
+
</div>
|
|
807
|
+
${fileSections || noIssues}
|
|
808
|
+
</main>
|
|
809
|
+
|
|
810
|
+
</div><!-- .layout -->
|
|
811
|
+
|
|
490
812
|
<footer class="footer">Generated by drift v${VERSION}</footer>
|
|
491
813
|
|
|
492
|
-
</div
|
|
814
|
+
</div><!-- #app -->
|
|
815
|
+
|
|
816
|
+
<script>
|
|
817
|
+
const DRIFT_DATA = ${JSON.stringify(report)};
|
|
818
|
+
${buildJs()}
|
|
819
|
+
</script>
|
|
493
820
|
</body>
|
|
494
821
|
</html>`;
|
|
495
822
|
}
|