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