@vibecheckai/cli 3.1.8 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/registry.js +106 -116
- package/bin/runners/context/generators/mcp.js +18 -0
- package/bin/runners/context/index.js +72 -4
- package/bin/runners/context/proof-context.js +293 -1
- package/bin/runners/context/security-scanner.js +311 -73
- package/bin/runners/lib/analyzers.js +607 -20
- package/bin/runners/lib/detectors-v2.js +172 -15
- package/bin/runners/lib/entitlements-v2.js +48 -1
- package/bin/runners/lib/evidence-pack.js +678 -0
- package/bin/runners/lib/html-proof-report.js +913 -0
- package/bin/runners/lib/missions/plan.js +231 -41
- package/bin/runners/lib/missions/templates.js +125 -0
- package/bin/runners/lib/scan-output.js +492 -253
- package/bin/runners/lib/ship-output.js +901 -641
- package/bin/runners/runCheckpoint.js +44 -3
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runDoctor.js +10 -2
- package/bin/runners/runFix.js +51 -341
- package/bin/runners/runInit.js +11 -0
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +608 -29
- package/bin/runners/runProve.js +210 -25
- package/bin/runners/runReality.js +846 -101
- package/bin/runners/runScan.js +238 -4
- package/bin/runners/runShip.js +19 -3
- package/bin/runners/runWatch.js +14 -1
- package/bin/vibecheck.js +32 -2
- package/mcp-server/consolidated-tools.js +408 -42
- package/mcp-server/index.js +152 -15
- package/mcp-server/proof-tools.js +571 -0
- package/mcp-server/tier-auth.js +22 -19
- package/mcp-server/tools-v3.js +744 -0
- package/mcp-server/truth-firewall-tools.js +190 -4
- package/package.json +3 -1
- package/bin/runners/runInstall.js +0 -281
- package/bin/runners/runLabs.js +0 -341
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Proof Report Generator
|
|
3
|
+
*
|
|
4
|
+
* Creates a beautiful, self-contained HTML report that proves your app works.
|
|
5
|
+
* Includes embedded video, network evidence, and clear verdicts.
|
|
6
|
+
*
|
|
7
|
+
* Designed to be shareable: drop into a PR, CI artifact, or send to stakeholders.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// HTML TEMPLATE
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
function escapeHtml(str) {
|
|
20
|
+
if (!str) return '';
|
|
21
|
+
return String(str)
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDuration(ms) {
|
|
30
|
+
if (!ms) return '0s';
|
|
31
|
+
if (ms < 1000) return `${ms}ms`;
|
|
32
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
33
|
+
const mins = Math.floor(ms / 60000);
|
|
34
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
35
|
+
return `${mins}m ${secs}s`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function generateHtmlProofReport(data) {
|
|
39
|
+
const {
|
|
40
|
+
verdict,
|
|
41
|
+
projectName,
|
|
42
|
+
url,
|
|
43
|
+
startedAt,
|
|
44
|
+
finishedAt,
|
|
45
|
+
durationMs,
|
|
46
|
+
findings = [],
|
|
47
|
+
coverage = null,
|
|
48
|
+
passes = {},
|
|
49
|
+
artifacts = {},
|
|
50
|
+
meta = {},
|
|
51
|
+
stabilityRuns = 1
|
|
52
|
+
} = data;
|
|
53
|
+
|
|
54
|
+
const blocks = findings.filter(f => f.severity === 'BLOCK').length;
|
|
55
|
+
const warns = findings.filter(f => f.severity === 'WARN').length;
|
|
56
|
+
|
|
57
|
+
const verdictConfig = {
|
|
58
|
+
SHIP: { emoji: '✅', color: '#10b981', bg: '#ecfdf5', label: 'PROVED REAL' },
|
|
59
|
+
CLEAN: { emoji: '✅', color: '#10b981', bg: '#ecfdf5', label: 'PROVED REAL' },
|
|
60
|
+
WARN: { emoji: '⚠️', color: '#f59e0b', bg: '#fffbeb', label: 'WARNINGS FOUND' },
|
|
61
|
+
BLOCK: { emoji: '🛑', color: '#ef4444', bg: '#fef2f2', label: 'NOT PROVED' }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const v = verdictConfig[verdict] || verdictConfig.BLOCK;
|
|
65
|
+
|
|
66
|
+
// Group findings by category
|
|
67
|
+
const findingsByCategory = {};
|
|
68
|
+
for (const f of findings) {
|
|
69
|
+
const cat = f.category || 'Other';
|
|
70
|
+
if (!findingsByCategory[cat]) findingsByCategory[cat] = [];
|
|
71
|
+
findingsByCategory[cat].push(f);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build HTML
|
|
75
|
+
return `<!DOCTYPE html>
|
|
76
|
+
<html lang="en">
|
|
77
|
+
<head>
|
|
78
|
+
<meta charset="UTF-8">
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
80
|
+
<title>Reality Proof: ${escapeHtml(projectName)}</title>
|
|
81
|
+
<style>
|
|
82
|
+
:root {
|
|
83
|
+
--verdict-color: ${v.color};
|
|
84
|
+
--verdict-bg: ${v.bg};
|
|
85
|
+
--primary: #6366f1;
|
|
86
|
+
--success: #10b981;
|
|
87
|
+
--warning: #f59e0b;
|
|
88
|
+
--danger: #ef4444;
|
|
89
|
+
--gray-50: #f9fafb;
|
|
90
|
+
--gray-100: #f3f4f6;
|
|
91
|
+
--gray-200: #e5e7eb;
|
|
92
|
+
--gray-300: #d1d5db;
|
|
93
|
+
--gray-400: #9ca3af;
|
|
94
|
+
--gray-500: #6b7280;
|
|
95
|
+
--gray-600: #4b5563;
|
|
96
|
+
--gray-700: #374151;
|
|
97
|
+
--gray-800: #1f2937;
|
|
98
|
+
--gray-900: #111827;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
* {
|
|
102
|
+
margin: 0;
|
|
103
|
+
padding: 0;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
body {
|
|
108
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
109
|
+
background: var(--gray-50);
|
|
110
|
+
color: var(--gray-800);
|
|
111
|
+
line-height: 1.6;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.container {
|
|
115
|
+
max-width: 1200px;
|
|
116
|
+
margin: 0 auto;
|
|
117
|
+
padding: 2rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Header */
|
|
121
|
+
.header {
|
|
122
|
+
text-align: center;
|
|
123
|
+
margin-bottom: 3rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.logo {
|
|
127
|
+
font-size: 2rem;
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
color: var(--primary);
|
|
130
|
+
margin-bottom: 0.5rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.logo span {
|
|
134
|
+
color: var(--gray-400);
|
|
135
|
+
font-weight: 400;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.subtitle {
|
|
139
|
+
color: var(--gray-500);
|
|
140
|
+
font-size: 1rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Verdict Card */
|
|
144
|
+
.verdict-card {
|
|
145
|
+
background: white;
|
|
146
|
+
border-radius: 16px;
|
|
147
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
148
|
+
padding: 2.5rem;
|
|
149
|
+
text-align: center;
|
|
150
|
+
margin-bottom: 2rem;
|
|
151
|
+
border: 2px solid var(--verdict-color);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.verdict-emoji {
|
|
155
|
+
font-size: 4rem;
|
|
156
|
+
margin-bottom: 1rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.verdict-label {
|
|
160
|
+
font-size: 1.75rem;
|
|
161
|
+
font-weight: 700;
|
|
162
|
+
color: var(--verdict-color);
|
|
163
|
+
margin-bottom: 0.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.verdict-desc {
|
|
167
|
+
color: var(--gray-500);
|
|
168
|
+
margin-bottom: 1.5rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.verdict-stats {
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
gap: 3rem;
|
|
175
|
+
flex-wrap: wrap;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.stat {
|
|
179
|
+
text-align: center;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.stat-value {
|
|
183
|
+
font-size: 2rem;
|
|
184
|
+
font-weight: 700;
|
|
185
|
+
color: var(--gray-800);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.stat-value.danger { color: var(--danger); }
|
|
189
|
+
.stat-value.warning { color: var(--warning); }
|
|
190
|
+
.stat-value.success { color: var(--success); }
|
|
191
|
+
|
|
192
|
+
.stat-label {
|
|
193
|
+
font-size: 0.875rem;
|
|
194
|
+
color: var(--gray-500);
|
|
195
|
+
text-transform: uppercase;
|
|
196
|
+
letter-spacing: 0.05em;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Meta Info */
|
|
200
|
+
.meta-grid {
|
|
201
|
+
display: grid;
|
|
202
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
203
|
+
gap: 1rem;
|
|
204
|
+
margin-bottom: 2rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.meta-item {
|
|
208
|
+
background: white;
|
|
209
|
+
border-radius: 8px;
|
|
210
|
+
padding: 1rem;
|
|
211
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.meta-label {
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
color: var(--gray-500);
|
|
217
|
+
text-transform: uppercase;
|
|
218
|
+
letter-spacing: 0.05em;
|
|
219
|
+
margin-bottom: 0.25rem;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.meta-value {
|
|
223
|
+
font-weight: 600;
|
|
224
|
+
color: var(--gray-800);
|
|
225
|
+
word-break: break-all;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Sections */
|
|
229
|
+
.section {
|
|
230
|
+
background: white;
|
|
231
|
+
border-radius: 12px;
|
|
232
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
233
|
+
margin-bottom: 2rem;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.section-header {
|
|
238
|
+
background: var(--gray-50);
|
|
239
|
+
padding: 1rem 1.5rem;
|
|
240
|
+
border-bottom: 1px solid var(--gray-200);
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
gap: 0.75rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.section-icon {
|
|
247
|
+
font-size: 1.25rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.section-title {
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
color: var(--gray-800);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.section-badge {
|
|
256
|
+
margin-left: auto;
|
|
257
|
+
background: var(--gray-200);
|
|
258
|
+
color: var(--gray-600);
|
|
259
|
+
padding: 0.25rem 0.75rem;
|
|
260
|
+
border-radius: 999px;
|
|
261
|
+
font-size: 0.75rem;
|
|
262
|
+
font-weight: 500;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.section-body {
|
|
266
|
+
padding: 1.5rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Evidence Section */
|
|
270
|
+
.evidence-grid {
|
|
271
|
+
display: grid;
|
|
272
|
+
gap: 1rem;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.evidence-row {
|
|
276
|
+
display: grid;
|
|
277
|
+
grid-template-columns: 1fr 1fr;
|
|
278
|
+
gap: 1rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@media (max-width: 768px) {
|
|
282
|
+
.evidence-row {
|
|
283
|
+
grid-template-columns: 1fr;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Video Player */
|
|
288
|
+
.video-container {
|
|
289
|
+
background: var(--gray-900);
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
aspect-ratio: 16/9;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.video-container video {
|
|
296
|
+
width: 100%;
|
|
297
|
+
height: 100%;
|
|
298
|
+
object-fit: contain;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.video-label {
|
|
302
|
+
text-align: center;
|
|
303
|
+
padding: 0.75rem;
|
|
304
|
+
background: var(--gray-100);
|
|
305
|
+
font-size: 0.875rem;
|
|
306
|
+
color: var(--gray-600);
|
|
307
|
+
font-weight: 500;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.video-placeholder {
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
height: 200px;
|
|
315
|
+
background: var(--gray-100);
|
|
316
|
+
color: var(--gray-400);
|
|
317
|
+
font-size: 0.875rem;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Network Evidence */
|
|
321
|
+
.network-table {
|
|
322
|
+
width: 100%;
|
|
323
|
+
border-collapse: collapse;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.network-table th,
|
|
327
|
+
.network-table td {
|
|
328
|
+
padding: 0.75rem;
|
|
329
|
+
text-align: left;
|
|
330
|
+
border-bottom: 1px solid var(--gray-200);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.network-table th {
|
|
334
|
+
background: var(--gray-50);
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
font-size: 0.75rem;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.05em;
|
|
339
|
+
color: var(--gray-500);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.network-table td {
|
|
343
|
+
font-size: 0.875rem;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.status-badge {
|
|
347
|
+
display: inline-flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 0.25rem;
|
|
350
|
+
padding: 0.125rem 0.5rem;
|
|
351
|
+
border-radius: 4px;
|
|
352
|
+
font-size: 0.75rem;
|
|
353
|
+
font-weight: 500;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.status-badge.success { background: #dcfce7; color: #166534; }
|
|
357
|
+
.status-badge.warning { background: #fef3c7; color: #92400e; }
|
|
358
|
+
.status-badge.error { background: #fee2e2; color: #991b1b; }
|
|
359
|
+
|
|
360
|
+
/* Findings */
|
|
361
|
+
.finding-card {
|
|
362
|
+
border: 1px solid var(--gray-200);
|
|
363
|
+
border-radius: 8px;
|
|
364
|
+
margin-bottom: 1rem;
|
|
365
|
+
overflow: hidden;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.finding-card.block {
|
|
369
|
+
border-left: 4px solid var(--danger);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.finding-card.warn {
|
|
373
|
+
border-left: 4px solid var(--warning);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.finding-header {
|
|
377
|
+
padding: 1rem;
|
|
378
|
+
background: var(--gray-50);
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: flex-start;
|
|
381
|
+
gap: 0.75rem;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.finding-severity {
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
padding: 0.25rem 0.5rem;
|
|
387
|
+
border-radius: 4px;
|
|
388
|
+
font-size: 0.75rem;
|
|
389
|
+
font-weight: 700;
|
|
390
|
+
text-transform: uppercase;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.finding-severity.block { background: #fee2e2; color: #991b1b; }
|
|
394
|
+
.finding-severity.warn { background: #fef3c7; color: #92400e; }
|
|
395
|
+
|
|
396
|
+
.finding-title {
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
color: var(--gray-800);
|
|
399
|
+
flex: 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.finding-body {
|
|
403
|
+
padding: 1rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.finding-meta {
|
|
407
|
+
display: flex;
|
|
408
|
+
flex-wrap: wrap;
|
|
409
|
+
gap: 1rem;
|
|
410
|
+
font-size: 0.875rem;
|
|
411
|
+
color: var(--gray-500);
|
|
412
|
+
margin-bottom: 0.75rem;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.finding-reason {
|
|
416
|
+
color: var(--gray-600);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.finding-screenshot {
|
|
420
|
+
margin-top: 1rem;
|
|
421
|
+
border-radius: 8px;
|
|
422
|
+
overflow: hidden;
|
|
423
|
+
border: 1px solid var(--gray-200);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.finding-screenshot img {
|
|
427
|
+
width: 100%;
|
|
428
|
+
height: auto;
|
|
429
|
+
display: block;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Coverage */
|
|
433
|
+
.coverage-bar {
|
|
434
|
+
height: 12px;
|
|
435
|
+
background: var(--gray-200);
|
|
436
|
+
border-radius: 6px;
|
|
437
|
+
overflow: hidden;
|
|
438
|
+
margin-bottom: 0.5rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.coverage-fill {
|
|
442
|
+
height: 100%;
|
|
443
|
+
background: linear-gradient(90deg, var(--primary), #818cf8);
|
|
444
|
+
border-radius: 6px;
|
|
445
|
+
transition: width 0.3s ease;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.coverage-label {
|
|
449
|
+
display: flex;
|
|
450
|
+
justify-content: space-between;
|
|
451
|
+
font-size: 0.875rem;
|
|
452
|
+
color: var(--gray-600);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Trace Link */
|
|
456
|
+
.trace-link {
|
|
457
|
+
display: inline-flex;
|
|
458
|
+
align-items: center;
|
|
459
|
+
gap: 0.5rem;
|
|
460
|
+
padding: 0.75rem 1rem;
|
|
461
|
+
background: var(--gray-100);
|
|
462
|
+
border-radius: 8px;
|
|
463
|
+
color: var(--primary);
|
|
464
|
+
text-decoration: none;
|
|
465
|
+
font-weight: 500;
|
|
466
|
+
transition: background 0.2s;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.trace-link:hover {
|
|
470
|
+
background: var(--gray-200);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Footer */
|
|
474
|
+
.footer {
|
|
475
|
+
text-align: center;
|
|
476
|
+
padding: 2rem;
|
|
477
|
+
color: var(--gray-400);
|
|
478
|
+
font-size: 0.875rem;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.footer a {
|
|
482
|
+
color: var(--primary);
|
|
483
|
+
text-decoration: none;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Print styles */
|
|
487
|
+
@media print {
|
|
488
|
+
.video-container { display: none; }
|
|
489
|
+
.container { max-width: 100%; padding: 1rem; }
|
|
490
|
+
.section { break-inside: avoid; }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* Collapsible */
|
|
494
|
+
.collapsible-header {
|
|
495
|
+
cursor: pointer;
|
|
496
|
+
user-select: none;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.collapsible-header::after {
|
|
500
|
+
content: '▼';
|
|
501
|
+
font-size: 0.75rem;
|
|
502
|
+
margin-left: 0.5rem;
|
|
503
|
+
transition: transform 0.2s;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.collapsible.collapsed .collapsible-header::after {
|
|
507
|
+
transform: rotate(-90deg);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.collapsible.collapsed .collapsible-body {
|
|
511
|
+
display: none;
|
|
512
|
+
}
|
|
513
|
+
</style>
|
|
514
|
+
</head>
|
|
515
|
+
<body>
|
|
516
|
+
<div class="container">
|
|
517
|
+
<!-- Header -->
|
|
518
|
+
<header class="header">
|
|
519
|
+
<div class="logo">vibecheck <span>prove</span></div>
|
|
520
|
+
<p class="subtitle">Reality Proof Report</p>
|
|
521
|
+
</header>
|
|
522
|
+
|
|
523
|
+
<!-- Verdict Card -->
|
|
524
|
+
<div class="verdict-card">
|
|
525
|
+
<div class="verdict-emoji">${v.emoji}</div>
|
|
526
|
+
<div class="verdict-label">${v.label}</div>
|
|
527
|
+
<p class="verdict-desc">
|
|
528
|
+
${verdict === 'SHIP' || verdict === 'CLEAN'
|
|
529
|
+
? 'Your app has been verified with real user flows hitting real APIs.'
|
|
530
|
+
: verdict === 'WARN'
|
|
531
|
+
? 'Verification complete with warnings. Review before shipping.'
|
|
532
|
+
: 'Verification failed. Critical issues must be fixed.'}
|
|
533
|
+
</p>
|
|
534
|
+
<div class="verdict-stats">
|
|
535
|
+
<div class="stat">
|
|
536
|
+
<div class="stat-value ${blocks > 0 ? 'danger' : 'success'}">${blocks}</div>
|
|
537
|
+
<div class="stat-label">Blockers</div>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="stat">
|
|
540
|
+
<div class="stat-value ${warns > 0 ? 'warning' : 'success'}">${warns}</div>
|
|
541
|
+
<div class="stat-label">Warnings</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="stat">
|
|
544
|
+
<div class="stat-value">${coverage?.percent || 0}%</div>
|
|
545
|
+
<div class="stat-label">Coverage</div>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="stat">
|
|
548
|
+
<div class="stat-value">${formatDuration(durationMs)}</div>
|
|
549
|
+
<div class="stat-label">Duration</div>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<!-- Meta Info -->
|
|
555
|
+
<div class="meta-grid">
|
|
556
|
+
<div class="meta-item">
|
|
557
|
+
<div class="meta-label">Project</div>
|
|
558
|
+
<div class="meta-value">${escapeHtml(projectName)}</div>
|
|
559
|
+
</div>
|
|
560
|
+
<div class="meta-item">
|
|
561
|
+
<div class="meta-label">URL Tested</div>
|
|
562
|
+
<div class="meta-value">${escapeHtml(url || 'N/A')}</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="meta-item">
|
|
565
|
+
<div class="meta-label">Tested At</div>
|
|
566
|
+
<div class="meta-value">${new Date(startedAt).toLocaleString()}</div>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="meta-item">
|
|
569
|
+
<div class="meta-label">Stability Runs</div>
|
|
570
|
+
<div class="meta-value">${stabilityRuns}</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
${generateVideoSection(artifacts, passes)}
|
|
575
|
+
|
|
576
|
+
${generateNetworkSection(passes)}
|
|
577
|
+
|
|
578
|
+
${generateCoverageSection(coverage)}
|
|
579
|
+
|
|
580
|
+
${generateFindingsSection(findings, findingsByCategory)}
|
|
581
|
+
|
|
582
|
+
${generateArtifactsSection(artifacts)}
|
|
583
|
+
|
|
584
|
+
<!-- Footer -->
|
|
585
|
+
<footer class="footer">
|
|
586
|
+
Generated by <a href="https://vibecheck.ai">vibecheck</a> ·
|
|
587
|
+
${new Date().toISOString()}
|
|
588
|
+
</footer>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<script>
|
|
592
|
+
// Collapsible sections
|
|
593
|
+
document.querySelectorAll('.collapsible').forEach(el => {
|
|
594
|
+
const header = el.querySelector('.collapsible-header');
|
|
595
|
+
if (header) {
|
|
596
|
+
header.addEventListener('click', () => {
|
|
597
|
+
el.classList.toggle('collapsed');
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
</script>
|
|
602
|
+
</body>
|
|
603
|
+
</html>`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function generateVideoSection(artifacts, passes) {
|
|
607
|
+
const hasVideos = artifacts?.videos?.anon || artifacts?.videos?.auth;
|
|
608
|
+
const hasTraces = artifacts?.traces?.anon || artifacts?.traces?.auth;
|
|
609
|
+
|
|
610
|
+
if (!hasVideos && !hasTraces) {
|
|
611
|
+
return '';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return `
|
|
615
|
+
<!-- Video Evidence -->
|
|
616
|
+
<div class="section">
|
|
617
|
+
<div class="section-header">
|
|
618
|
+
<span class="section-icon">🎬</span>
|
|
619
|
+
<span class="section-title">Video Evidence</span>
|
|
620
|
+
<span class="section-badge">Runtime Proof</span>
|
|
621
|
+
</div>
|
|
622
|
+
<div class="section-body">
|
|
623
|
+
<p style="color: var(--gray-500); margin-bottom: 1rem;">
|
|
624
|
+
These recordings show real browser sessions testing your application.
|
|
625
|
+
</p>
|
|
626
|
+
<div class="evidence-row">
|
|
627
|
+
${artifacts?.videos?.anon ? `
|
|
628
|
+
<div>
|
|
629
|
+
<div class="video-container">
|
|
630
|
+
<video controls preload="metadata">
|
|
631
|
+
<source src="${escapeHtml(artifacts.videos.anon)}" type="video/webm">
|
|
632
|
+
Your browser does not support video playback.
|
|
633
|
+
</video>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="video-label">👤 Anonymous Session</div>
|
|
636
|
+
</div>
|
|
637
|
+
` : '<div class="video-placeholder">No anonymous session video</div>'}
|
|
638
|
+
|
|
639
|
+
${artifacts?.videos?.auth ? `
|
|
640
|
+
<div>
|
|
641
|
+
<div class="video-container">
|
|
642
|
+
<video controls preload="metadata">
|
|
643
|
+
<source src="${escapeHtml(artifacts.videos.auth)}" type="video/webm">
|
|
644
|
+
Your browser does not support video playback.
|
|
645
|
+
</video>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="video-label">🔑 Authenticated Session</div>
|
|
648
|
+
</div>
|
|
649
|
+
` : ''}
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
${hasTraces ? `
|
|
653
|
+
<div style="margin-top: 1.5rem;">
|
|
654
|
+
<p style="color: var(--gray-600); font-weight: 500; margin-bottom: 0.75rem;">
|
|
655
|
+
📊 Full Traces (for detailed debugging)
|
|
656
|
+
</p>
|
|
657
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
658
|
+
${artifacts?.traces?.anon ? `
|
|
659
|
+
<a href="https://trace.playwright.dev" class="trace-link" target="_blank">
|
|
660
|
+
View Anonymous Trace ↗
|
|
661
|
+
</a>
|
|
662
|
+
` : ''}
|
|
663
|
+
${artifacts?.traces?.auth ? `
|
|
664
|
+
<a href="https://trace.playwright.dev" class="trace-link" target="_blank">
|
|
665
|
+
View Authenticated Trace ↗
|
|
666
|
+
</a>
|
|
667
|
+
` : ''}
|
|
668
|
+
</div>
|
|
669
|
+
<p style="color: var(--gray-400); font-size: 0.75rem; margin-top: 0.5rem;">
|
|
670
|
+
Download trace files and upload to trace.playwright.dev
|
|
671
|
+
</p>
|
|
672
|
+
</div>
|
|
673
|
+
` : ''}
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function generateNetworkSection(passes) {
|
|
680
|
+
const anonErrors = passes?.anon?.networkErrors || [];
|
|
681
|
+
const authErrors = passes?.auth?.networkErrors || [];
|
|
682
|
+
const allErrors = [...anonErrors, ...authErrors].slice(0, 10);
|
|
683
|
+
|
|
684
|
+
const anonResponses = passes?.anon?.fakeDataDetections || [];
|
|
685
|
+
const fakeDetections = anonResponses.filter(d => d.confidence >= 0.7).slice(0, 5);
|
|
686
|
+
|
|
687
|
+
return `
|
|
688
|
+
<!-- Network Evidence -->
|
|
689
|
+
<div class="section">
|
|
690
|
+
<div class="section-header">
|
|
691
|
+
<span class="section-icon">📡</span>
|
|
692
|
+
<span class="section-title">Network Evidence</span>
|
|
693
|
+
<span class="section-badge">${allErrors.length} errors, ${fakeDetections.length} fakes detected</span>
|
|
694
|
+
</div>
|
|
695
|
+
<div class="section-body">
|
|
696
|
+
${allErrors.length > 0 ? `
|
|
697
|
+
<h4 style="margin-bottom: 0.75rem; color: var(--gray-700);">Network Errors</h4>
|
|
698
|
+
<table class="network-table">
|
|
699
|
+
<thead>
|
|
700
|
+
<tr>
|
|
701
|
+
<th>URL</th>
|
|
702
|
+
<th>Error</th>
|
|
703
|
+
</tr>
|
|
704
|
+
</thead>
|
|
705
|
+
<tbody>
|
|
706
|
+
${allErrors.map(e => `
|
|
707
|
+
<tr>
|
|
708
|
+
<td style="word-break: break-all; max-width: 400px;">${escapeHtml(e.url)}</td>
|
|
709
|
+
<td><span class="status-badge error">${escapeHtml(e.failure || 'Failed')}</span></td>
|
|
710
|
+
</tr>
|
|
711
|
+
`).join('')}
|
|
712
|
+
</tbody>
|
|
713
|
+
</table>
|
|
714
|
+
` : `
|
|
715
|
+
<div style="text-align: center; padding: 2rem; color: var(--gray-400);">
|
|
716
|
+
✅ No network errors detected
|
|
717
|
+
</div>
|
|
718
|
+
`}
|
|
719
|
+
|
|
720
|
+
${fakeDetections.length > 0 ? `
|
|
721
|
+
<h4 style="margin: 1.5rem 0 0.75rem; color: var(--gray-700);">Fake Data Detections</h4>
|
|
722
|
+
<table class="network-table">
|
|
723
|
+
<thead>
|
|
724
|
+
<tr>
|
|
725
|
+
<th>Type</th>
|
|
726
|
+
<th>Evidence</th>
|
|
727
|
+
<th>Confidence</th>
|
|
728
|
+
</tr>
|
|
729
|
+
</thead>
|
|
730
|
+
<tbody>
|
|
731
|
+
${fakeDetections.map(d => `
|
|
732
|
+
<tr>
|
|
733
|
+
<td>${escapeHtml(d.type)}</td>
|
|
734
|
+
<td>${escapeHtml(d.evidence)}</td>
|
|
735
|
+
<td><span class="status-badge ${d.confidence >= 0.9 ? 'error' : 'warning'}">${Math.round(d.confidence * 100)}%</span></td>
|
|
736
|
+
</tr>
|
|
737
|
+
`).join('')}
|
|
738
|
+
</tbody>
|
|
739
|
+
</table>
|
|
740
|
+
` : ''}
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function generateCoverageSection(coverage) {
|
|
747
|
+
if (!coverage) return '';
|
|
748
|
+
|
|
749
|
+
return `
|
|
750
|
+
<!-- Coverage -->
|
|
751
|
+
<div class="section">
|
|
752
|
+
<div class="section-header">
|
|
753
|
+
<span class="section-icon">📊</span>
|
|
754
|
+
<span class="section-title">UI Path Coverage</span>
|
|
755
|
+
<span class="section-badge">${coverage.hit || 0}/${coverage.total || 0} paths</span>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="section-body">
|
|
758
|
+
<div class="coverage-bar">
|
|
759
|
+
<div class="coverage-fill" style="width: ${coverage.percent || 0}%"></div>
|
|
760
|
+
</div>
|
|
761
|
+
<div class="coverage-label">
|
|
762
|
+
<span>${coverage.percent || 0}% covered</span>
|
|
763
|
+
<span>${coverage.hit || 0} of ${coverage.total || 0} paths visited</span>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
${coverage.missed && coverage.missed.length > 0 ? `
|
|
767
|
+
<div style="margin-top: 1rem;">
|
|
768
|
+
<p style="color: var(--gray-600); font-weight: 500; margin-bottom: 0.5rem;">
|
|
769
|
+
Missed Paths (${coverage.missed.length})
|
|
770
|
+
</p>
|
|
771
|
+
<ul style="color: var(--gray-500); font-size: 0.875rem; padding-left: 1.5rem;">
|
|
772
|
+
${coverage.missed.slice(0, 5).map(p => `<li>${escapeHtml(p)}</li>`).join('')}
|
|
773
|
+
${coverage.missed.length > 5 ? `<li>...and ${coverage.missed.length - 5} more</li>` : ''}
|
|
774
|
+
</ul>
|
|
775
|
+
</div>
|
|
776
|
+
` : ''}
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function generateFindingsSection(findings, findingsByCategory) {
|
|
783
|
+
if (findings.length === 0) {
|
|
784
|
+
return `
|
|
785
|
+
<!-- Findings -->
|
|
786
|
+
<div class="section">
|
|
787
|
+
<div class="section-header">
|
|
788
|
+
<span class="section-icon">✅</span>
|
|
789
|
+
<span class="section-title">Findings</span>
|
|
790
|
+
<span class="section-badge">0 issues</span>
|
|
791
|
+
</div>
|
|
792
|
+
<div class="section-body">
|
|
793
|
+
<div style="text-align: center; padding: 2rem; color: var(--gray-400);">
|
|
794
|
+
✨ No issues found! Your UI is responsive and real.
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const blocks = findings.filter(f => f.severity === 'BLOCK');
|
|
802
|
+
const warns = findings.filter(f => f.severity === 'WARN');
|
|
803
|
+
|
|
804
|
+
return `
|
|
805
|
+
<!-- Findings -->
|
|
806
|
+
<div class="section">
|
|
807
|
+
<div class="section-header">
|
|
808
|
+
<span class="section-icon">🔍</span>
|
|
809
|
+
<span class="section-title">Findings</span>
|
|
810
|
+
<span class="section-badge">${blocks.length} blockers, ${warns.length} warnings</span>
|
|
811
|
+
</div>
|
|
812
|
+
<div class="section-body">
|
|
813
|
+
${Object.entries(findingsByCategory).map(([category, catFindings]) => `
|
|
814
|
+
<div class="collapsible" style="margin-bottom: 1.5rem;">
|
|
815
|
+
<h4 class="collapsible-header" style="color: var(--gray-700); margin-bottom: 0.75rem; cursor: pointer;">
|
|
816
|
+
${getCategoryEmoji(category)} ${escapeHtml(category)}
|
|
817
|
+
<span style="color: var(--gray-400); font-weight: normal;">(${catFindings.length})</span>
|
|
818
|
+
</h4>
|
|
819
|
+
<div class="collapsible-body">
|
|
820
|
+
${catFindings.slice(0, 10).map(f => `
|
|
821
|
+
<div class="finding-card ${f.severity.toLowerCase()}">
|
|
822
|
+
<div class="finding-header">
|
|
823
|
+
<span class="finding-severity ${f.severity.toLowerCase()}">${f.severity}</span>
|
|
824
|
+
<span class="finding-title">${escapeHtml(f.title)}</span>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="finding-body">
|
|
827
|
+
<div class="finding-meta">
|
|
828
|
+
${f.page ? `<span>📍 ${escapeHtml(truncateUrl(f.page))}</span>` : ''}
|
|
829
|
+
${f.confidence ? `<span>🎯 ${Math.round(f.confidence * 100)}% confidence</span>` : ''}
|
|
830
|
+
</div>
|
|
831
|
+
${f.reason ? `<p class="finding-reason">${escapeHtml(f.reason)}</p>` : ''}
|
|
832
|
+
${f.screenshot ? `
|
|
833
|
+
<div class="finding-screenshot">
|
|
834
|
+
<img src="${escapeHtml(f.screenshot)}" alt="Screenshot" loading="lazy">
|
|
835
|
+
</div>
|
|
836
|
+
` : ''}
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
`).join('')}
|
|
840
|
+
${catFindings.length > 10 ? `
|
|
841
|
+
<p style="color: var(--gray-400); font-size: 0.875rem; text-align: center;">
|
|
842
|
+
...and ${catFindings.length - 10} more in this category
|
|
843
|
+
</p>
|
|
844
|
+
` : ''}
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
`).join('')}
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function generateArtifactsSection(artifacts) {
|
|
854
|
+
const hasArtifacts = artifacts?.screenshots || artifacts?.videos?.directory ||
|
|
855
|
+
artifacts?.traces?.directory || artifacts?.har?.directory;
|
|
856
|
+
|
|
857
|
+
if (!hasArtifacts) return '';
|
|
858
|
+
|
|
859
|
+
return `
|
|
860
|
+
<!-- Artifacts -->
|
|
861
|
+
<div class="section">
|
|
862
|
+
<div class="section-header">
|
|
863
|
+
<span class="section-icon">📁</span>
|
|
864
|
+
<span class="section-title">Artifacts</span>
|
|
865
|
+
</div>
|
|
866
|
+
<div class="section-body">
|
|
867
|
+
<p style="color: var(--gray-500); margin-bottom: 1rem;">
|
|
868
|
+
Full evidence files for CI/CD integration and audit trails.
|
|
869
|
+
</p>
|
|
870
|
+
<ul style="list-style: none; color: var(--gray-600);">
|
|
871
|
+
${artifacts?.screenshots ? `<li style="margin-bottom: 0.5rem;">📸 Screenshots: <code style="background: var(--gray-100); padding: 0.25rem 0.5rem; border-radius: 4px;">${escapeHtml(artifacts.screenshots)}</code></li>` : ''}
|
|
872
|
+
${artifacts?.videos?.directory ? `<li style="margin-bottom: 0.5rem;">🎬 Videos: <code style="background: var(--gray-100); padding: 0.25rem 0.5rem; border-radius: 4px;">${escapeHtml(artifacts.videos.directory)}</code></li>` : ''}
|
|
873
|
+
${artifacts?.traces?.directory ? `<li style="margin-bottom: 0.5rem;">📊 Traces: <code style="background: var(--gray-100); padding: 0.25rem 0.5rem; border-radius: 4px;">${escapeHtml(artifacts.traces.directory)}</code></li>` : ''}
|
|
874
|
+
${artifacts?.har?.directory ? `<li style="margin-bottom: 0.5rem;">📡 HAR Files: <code style="background: var(--gray-100); padding: 0.25rem 0.5rem; border-radius: 4px;">${escapeHtml(artifacts.har.directory)}</code></li>` : ''}
|
|
875
|
+
</ul>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function getCategoryEmoji(category) {
|
|
882
|
+
const emojis = {
|
|
883
|
+
'DeadUI': '💀',
|
|
884
|
+
'AuthCoverage': '🔒',
|
|
885
|
+
'HTTPError': '📡',
|
|
886
|
+
'FakeDomain': '🔗',
|
|
887
|
+
'FakeResponse': '🎭',
|
|
888
|
+
'MockStatus': '📡',
|
|
889
|
+
'StubCode': '📝',
|
|
890
|
+
'TODOCode': '📋',
|
|
891
|
+
'PlaceholderSecret': '🔐',
|
|
892
|
+
'Other': '📌'
|
|
893
|
+
};
|
|
894
|
+
return emojis[category] || '📌';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function truncateUrl(url) {
|
|
898
|
+
if (!url) return '';
|
|
899
|
+
try {
|
|
900
|
+
const parsed = new URL(url);
|
|
901
|
+
return parsed.pathname + (parsed.search ? '?' + parsed.search.slice(1, 20) + '...' : '');
|
|
902
|
+
} catch {
|
|
903
|
+
return url.length > 60 ? url.slice(0, 60) + '...' : url;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
908
|
+
// EXPORT
|
|
909
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
910
|
+
|
|
911
|
+
module.exports = {
|
|
912
|
+
generateHtmlProofReport
|
|
913
|
+
};
|