@vibecheckai/cli 3.1.2 → 3.1.4

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.
Files changed (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -1,1499 +1,1230 @@
1
1
  /**
2
- * @deprecated Use html-report.js instead. This module is kept for backward compatibility.
3
- * Import from report.js for the unified API.
4
- */
5
-
6
- /**
7
- * World-Class HTML Report Templates
2
+ * World-Class Enterprise HTML Report Generator
8
3
  *
9
4
  * Features:
10
- * - Modern glass-morphism design
11
- * - Animated Chart.js visualizations
12
- * - Interactive findings explorer
13
- * - Dark/light mode toggle
14
- * - Print-optimized CSS
15
- * - Responsive mobile layout
5
+ * - Animated score ring with gradient
6
+ * - Interactive severity donut chart (pure CSS)
7
+ * - Reality Mode broken flow visualization
8
+ * - Dark/light theme toggle with persistence
9
+ * - Print-optimized layout
10
+ * - White-label customization
11
+ * - Responsive design
16
12
  * - Accessibility compliant
13
+ * - Zero external dependencies (all inline)
17
14
  */
18
15
 
19
- const { formatCategoryName } = require("./report-engine");
20
-
21
- // ============================================================================
22
- // CSS DESIGN SYSTEM
23
- // ============================================================================
24
-
25
- function getDesignSystem() {
26
- return `
27
- :root {
28
- /* Colors */
29
- --color-bg: #0f172a;
30
- --color-bg-card: rgba(30, 41, 59, 0.8);
31
- --color-bg-elevated: rgba(51, 65, 85, 0.6);
32
- --color-text: #f8fafc;
33
- --color-text-muted: #94a3b8;
34
- --color-text-dim: #64748b;
35
- --color-border: rgba(148, 163, 184, 0.2);
36
-
37
- /* Brand */
38
- --color-primary: #3b82f6;
39
- --color-primary-glow: rgba(59, 130, 246, 0.4);
40
-
41
- /* Status */
42
- --color-ship: #22c55e;
43
- --color-ship-bg: rgba(34, 197, 94, 0.15);
44
- --color-warn: #f59e0b;
45
- --color-warn-bg: rgba(245, 158, 11, 0.15);
46
- --color-block: #ef4444;
47
- --color-block-bg: rgba(239, 68, 68, 0.15);
48
-
49
- /* Severity */
50
- --color-critical: #ef4444;
51
- --color-high: #f97316;
52
- --color-medium: #eab308;
53
- --color-low: #6b7280;
54
-
55
- /* Spacing */
56
- --space-xs: 4px;
57
- --space-sm: 8px;
58
- --space-md: 16px;
59
- --space-lg: 24px;
60
- --space-xl: 32px;
61
- --space-2xl: 48px;
62
- --space-3xl: 64px;
63
-
64
- /* Radius */
65
- --radius-sm: 6px;
66
- --radius-md: 12px;
67
- --radius-lg: 20px;
68
- --radius-xl: 28px;
69
- --radius-full: 9999px;
70
-
71
- /* Shadows */
72
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
73
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
74
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
75
- --shadow-glow: 0 0 40px var(--color-primary-glow);
76
-
77
- /* Typography */
78
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
79
- --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
80
-
81
- /* Transitions */
82
- --transition-fast: 150ms ease;
83
- --transition-normal: 250ms ease;
84
- --transition-slow: 400ms ease;
85
- }
86
-
87
- /* Light mode */
88
- [data-theme="light"] {
89
- --color-bg: #f8fafc;
90
- --color-bg-card: rgba(255, 255, 255, 0.9);
91
- --color-bg-elevated: rgba(241, 245, 249, 0.9);
92
- --color-text: #0f172a;
93
- --color-text-muted: #475569;
94
- --color-text-dim: #94a3b8;
95
- --color-border: rgba(148, 163, 184, 0.3);
96
- --color-ship-bg: rgba(34, 197, 94, 0.1);
97
- --color-warn-bg: rgba(245, 158, 11, 0.1);
98
- --color-block-bg: rgba(239, 68, 68, 0.1);
99
- }
100
-
101
- * {
102
- box-sizing: border-box;
103
- margin: 0;
104
- padding: 0;
105
- }
106
-
107
- html {
108
- scroll-behavior: smooth;
109
- }
110
-
111
- body {
112
- font-family: var(--font-sans);
113
- background: var(--color-bg);
114
- color: var(--color-text);
115
- line-height: 1.6;
116
- min-height: 100vh;
117
- -webkit-font-smoothing: antialiased;
118
- }
119
-
120
- /* Background pattern */
121
- body::before {
122
- content: '';
123
- position: fixed;
124
- inset: 0;
125
- background:
126
- radial-gradient(circle at 20% 20%, var(--color-primary-glow), transparent 40%),
127
- radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.2), transparent 40%);
128
- pointer-events: none;
129
- z-index: -1;
130
- }
131
-
132
- /* ============================================================================
133
- LAYOUT
134
- ============================================================================ */
135
-
136
- .report {
137
- max-width: 1200px;
138
- margin: 0 auto;
139
- padding: var(--space-xl);
140
- }
141
-
142
- @media (max-width: 768px) {
143
- .report {
144
- padding: var(--space-md);
145
- }
146
- }
147
-
148
- /* ============================================================================
149
- HEADER
150
- ============================================================================ */
151
-
152
- .report-header {
153
- display: flex;
154
- justify-content: space-between;
155
- align-items: flex-start;
156
- margin-bottom: var(--space-2xl);
157
- padding-bottom: var(--space-xl);
158
- border-bottom: 1px solid var(--color-border);
159
- }
160
-
161
- .report-title {
162
- font-size: 2rem;
163
- font-weight: 700;
164
- letter-spacing: -0.025em;
165
- margin-bottom: var(--space-sm);
166
- }
167
-
168
- .report-meta {
169
- color: var(--color-text-muted);
170
- font-size: 0.875rem;
171
- }
172
-
173
- .report-meta span {
174
- display: block;
175
- }
176
-
177
- .report-actions {
178
- display: flex;
179
- gap: var(--space-sm);
180
- }
181
-
182
- .btn {
183
- display: inline-flex;
184
- align-items: center;
185
- gap: var(--space-sm);
186
- padding: var(--space-sm) var(--space-md);
187
- border-radius: var(--radius-md);
188
- font-size: 0.875rem;
189
- font-weight: 500;
190
- cursor: pointer;
191
- transition: all var(--transition-fast);
192
- border: 1px solid var(--color-border);
193
- background: var(--color-bg-card);
194
- color: var(--color-text);
195
- }
196
-
197
- .btn:hover {
198
- background: var(--color-bg-elevated);
199
- transform: translateY(-1px);
200
- }
201
-
202
- .btn-icon {
203
- padding: var(--space-sm);
204
- }
205
-
206
- /* ============================================================================
207
- HERO SECTION (Score + Verdict)
208
- ============================================================================ */
209
-
210
- .hero-section {
211
- display: grid;
212
- grid-template-columns: 1fr 1.5fr;
213
- gap: var(--space-xl);
214
- margin-bottom: var(--space-2xl);
215
- }
216
-
217
- @media (max-width: 768px) {
218
- .hero-section {
219
- grid-template-columns: 1fr;
220
- }
221
- }
222
-
223
- .score-card {
224
- background: var(--color-bg-card);
225
- backdrop-filter: blur(20px);
226
- border-radius: var(--radius-xl);
227
- padding: var(--space-2xl);
228
- border: 1px solid var(--color-border);
229
- display: flex;
230
- flex-direction: column;
231
- align-items: center;
232
- justify-content: center;
233
- text-align: center;
234
- position: relative;
235
- overflow: hidden;
236
- }
237
-
238
- .score-card::before {
239
- content: '';
240
- position: absolute;
241
- inset: 0;
242
- background: linear-gradient(135deg, transparent, rgba(255,255,255,0.05));
243
- pointer-events: none;
244
- }
245
-
246
- .score-ring {
247
- position: relative;
248
- width: 200px;
249
- height: 200px;
250
- margin-bottom: var(--space-lg);
251
- }
252
-
253
- .score-ring svg {
254
- transform: rotate(-90deg);
255
- }
256
-
257
- .score-ring-bg {
258
- fill: none;
259
- stroke: var(--color-border);
260
- stroke-width: 12;
261
- }
262
-
263
- .score-ring-progress {
264
- fill: none;
265
- stroke-width: 12;
266
- stroke-linecap: round;
267
- transition: stroke-dasharray 1s ease-out;
268
- }
269
-
270
- .score-ring-progress.ship { stroke: var(--color-ship); }
271
- .score-ring-progress.warn { stroke: var(--color-warn); }
272
- .score-ring-progress.block { stroke: var(--color-block); }
273
-
274
- .score-value {
275
- position: absolute;
276
- top: 50%;
277
- left: 50%;
278
- transform: translate(-50%, -50%);
279
- font-size: 3.5rem;
280
- font-weight: 800;
281
- letter-spacing: -0.05em;
282
- }
283
-
284
- .score-label {
285
- color: var(--color-text-muted);
286
- font-size: 0.875rem;
287
- text-transform: uppercase;
288
- letter-spacing: 0.1em;
289
- }
290
-
291
- .verdict-card {
292
- background: var(--color-bg-card);
293
- backdrop-filter: blur(20px);
294
- border-radius: var(--radius-xl);
295
- padding: var(--space-2xl);
296
- border: 1px solid var(--color-border);
297
- }
298
-
299
- .verdict-badge {
300
- display: inline-flex;
301
- align-items: center;
302
- gap: var(--space-sm);
303
- padding: var(--space-md) var(--space-xl);
304
- border-radius: var(--radius-full);
305
- font-size: 1.25rem;
306
- font-weight: 700;
307
- text-transform: uppercase;
308
- letter-spacing: 0.05em;
309
- margin-bottom: var(--space-lg);
310
- }
311
-
312
- .verdict-badge.ship {
313
- background: var(--color-ship-bg);
314
- color: var(--color-ship);
315
- box-shadow: 0 0 30px rgba(34, 197, 94, 0.3);
316
- }
317
-
318
- .verdict-badge.warn {
319
- background: var(--color-warn-bg);
320
- color: var(--color-warn);
321
- box-shadow: 0 0 30px rgba(245, 158, 11, 0.3);
322
- }
323
-
324
- .verdict-badge.block {
325
- background: var(--color-block-bg);
326
- color: var(--color-block);
327
- box-shadow: 0 0 30px rgba(239, 68, 68, 0.3);
328
- }
329
-
330
- .verdict-message {
331
- font-size: 1.125rem;
332
- margin-bottom: var(--space-xl);
333
- color: var(--color-text-muted);
334
- }
335
-
336
- .stats-grid {
337
- display: grid;
338
- grid-template-columns: repeat(3, 1fr);
339
- gap: var(--space-md);
340
- }
341
-
342
- .stat-item {
343
- text-align: center;
344
- padding: var(--space-md);
345
- background: var(--color-bg-elevated);
346
- border-radius: var(--radius-md);
347
- }
348
-
349
- .stat-value {
350
- font-size: 1.5rem;
351
- font-weight: 700;
352
- display: block;
353
- }
354
-
355
- .stat-label {
356
- font-size: 0.75rem;
357
- color: var(--color-text-muted);
358
- text-transform: uppercase;
359
- letter-spacing: 0.05em;
360
- }
361
-
362
- /* ============================================================================
363
- SECTIONS
364
- ============================================================================ */
365
-
366
- .section {
367
- margin-bottom: var(--space-2xl);
368
- }
369
-
370
- .section-header {
371
- display: flex;
372
- align-items: center;
373
- gap: var(--space-md);
374
- margin-bottom: var(--space-lg);
375
- }
376
-
377
- .section-icon {
378
- width: 48px;
379
- height: 48px;
380
- border-radius: var(--radius-md);
381
- background: linear-gradient(135deg, var(--color-primary), #8b5cf6);
382
- display: flex;
383
- align-items: center;
384
- justify-content: center;
385
- font-size: 1.5rem;
386
- box-shadow: var(--shadow-glow);
387
- }
388
-
389
- .section-title {
390
- font-size: 1.5rem;
391
- font-weight: 600;
392
- }
393
-
394
- .section-subtitle {
395
- color: var(--color-text-muted);
396
- font-size: 0.875rem;
397
- }
398
-
399
- /* ============================================================================
400
- CARDS
401
- ============================================================================ */
402
-
403
- .card {
404
- background: var(--color-bg-card);
405
- backdrop-filter: blur(20px);
406
- border-radius: var(--radius-lg);
407
- border: 1px solid var(--color-border);
408
- padding: var(--space-xl);
409
- transition: all var(--transition-normal);
410
- }
411
-
412
- .card:hover {
413
- border-color: var(--color-primary);
414
- box-shadow: var(--shadow-lg);
415
- }
416
-
417
- .card-grid {
418
- display: grid;
419
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
420
- gap: var(--space-lg);
421
- }
422
-
423
- /* ============================================================================
424
- CATEGORY BARS
425
- ============================================================================ */
426
-
427
- .category-list {
428
- display: flex;
429
- flex-direction: column;
430
- gap: var(--space-md);
431
- }
432
-
433
- .category-item {
434
- background: var(--color-bg-elevated);
435
- border-radius: var(--radius-md);
436
- padding: var(--space-md) var(--space-lg);
437
- }
438
-
439
- .category-header {
440
- display: flex;
441
- justify-content: space-between;
442
- align-items: center;
443
- margin-bottom: var(--space-sm);
444
- }
445
-
446
- .category-name {
447
- font-weight: 500;
448
- }
449
-
450
- .category-score {
451
- font-weight: 700;
452
- font-size: 1.125rem;
453
- }
454
-
455
- .category-score.good { color: var(--color-ship); }
456
- .category-score.ok { color: var(--color-warn); }
457
- .category-score.bad { color: var(--color-block); }
458
-
459
- .category-bar {
460
- height: 8px;
461
- background: var(--color-border);
462
- border-radius: var(--radius-full);
463
- overflow: hidden;
464
- }
465
-
466
- .category-bar-fill {
467
- height: 100%;
468
- border-radius: var(--radius-full);
469
- transition: width 0.8s ease-out;
470
- }
471
-
472
- .category-bar-fill.good { background: linear-gradient(90deg, var(--color-ship), #4ade80); }
473
- .category-bar-fill.ok { background: linear-gradient(90deg, var(--color-warn), #fbbf24); }
474
- .category-bar-fill.bad { background: linear-gradient(90deg, var(--color-block), #f87171); }
475
-
476
- /* ============================================================================
477
- SEVERITY CHART
478
- ============================================================================ */
479
-
480
- .severity-chart {
481
- display: flex;
482
- gap: var(--space-lg);
483
- align-items: center;
484
- }
485
-
486
- .severity-donut {
487
- flex-shrink: 0;
488
- }
489
-
490
- .severity-legend {
491
- display: grid;
492
- grid-template-columns: repeat(2, 1fr);
493
- gap: var(--space-sm) var(--space-xl);
494
- flex: 1;
495
- }
496
-
497
- .severity-item {
498
- display: flex;
499
- align-items: center;
500
- gap: var(--space-sm);
501
- }
502
-
503
- .severity-dot {
504
- width: 12px;
505
- height: 12px;
506
- border-radius: var(--radius-full);
507
- }
508
-
509
- .severity-dot.critical { background: var(--color-critical); }
510
- .severity-dot.high { background: var(--color-high); }
511
- .severity-dot.medium { background: var(--color-medium); }
512
- .severity-dot.low { background: var(--color-low); }
513
-
514
- .severity-label {
515
- color: var(--color-text-muted);
516
- font-size: 0.875rem;
517
- }
518
-
519
- .severity-count {
520
- font-weight: 700;
521
- margin-left: auto;
522
- }
523
-
524
- /* ============================================================================
525
- FINDINGS
526
- ============================================================================ */
527
-
528
- .findings-tabs {
529
- display: flex;
530
- gap: var(--space-sm);
531
- margin-bottom: var(--space-lg);
532
- border-bottom: 1px solid var(--color-border);
533
- padding-bottom: var(--space-md);
534
- }
535
-
536
- .findings-tab {
537
- padding: var(--space-sm) var(--space-md);
538
- border-radius: var(--radius-md);
539
- font-size: 0.875rem;
540
- font-weight: 500;
541
- cursor: pointer;
542
- transition: all var(--transition-fast);
543
- background: transparent;
544
- border: none;
545
- color: var(--color-text-muted);
546
- }
547
-
548
- .findings-tab:hover {
549
- color: var(--color-text);
550
- background: var(--color-bg-elevated);
551
- }
552
-
553
- .findings-tab.active {
554
- color: var(--color-primary);
555
- background: rgba(59, 130, 246, 0.1);
556
- }
557
-
558
- .findings-tab .count {
559
- display: inline-flex;
560
- align-items: center;
561
- justify-content: center;
562
- min-width: 20px;
563
- height: 20px;
564
- border-radius: var(--radius-full);
565
- background: var(--color-bg-elevated);
566
- font-size: 0.75rem;
567
- margin-left: var(--space-xs);
568
- }
569
-
570
- .findings-list {
571
- display: flex;
572
- flex-direction: column;
573
- gap: var(--space-md);
574
- }
575
-
576
- .finding-card {
577
- background: var(--color-bg-elevated);
578
- border-radius: var(--radius-md);
579
- padding: var(--space-lg);
580
- border-left: 4px solid;
581
- transition: all var(--transition-fast);
582
- }
583
-
584
- .finding-card:hover {
585
- transform: translateX(4px);
586
- }
587
-
588
- .finding-card.critical { border-left-color: var(--color-critical); }
589
- .finding-card.high { border-left-color: var(--color-high); }
590
- .finding-card.medium { border-left-color: var(--color-medium); }
591
- .finding-card.low { border-left-color: var(--color-low); }
592
-
593
- .finding-header {
594
- display: flex;
595
- align-items: flex-start;
596
- gap: var(--space-md);
597
- margin-bottom: var(--space-sm);
598
- }
599
-
600
- .finding-severity {
601
- padding: var(--space-xs) var(--space-sm);
602
- border-radius: var(--radius-sm);
603
- font-size: 0.625rem;
604
- font-weight: 700;
605
- text-transform: uppercase;
606
- letter-spacing: 0.05em;
607
- color: white;
608
- }
609
-
610
- .finding-severity.critical { background: var(--color-critical); }
611
- .finding-severity.high { background: var(--color-high); }
612
- .finding-severity.medium { background: var(--color-medium); }
613
- .finding-severity.low { background: var(--color-low); }
614
-
615
- .finding-id {
616
- color: var(--color-text-dim);
617
- font-size: 0.75rem;
618
- font-family: var(--font-mono);
619
- }
620
-
621
- .finding-title {
622
- font-weight: 600;
623
- flex: 1;
624
- }
625
-
626
- .finding-meta {
627
- display: flex;
628
- gap: var(--space-lg);
629
- margin-top: var(--space-sm);
630
- font-size: 0.875rem;
631
- color: var(--color-text-muted);
632
- }
633
-
634
- .finding-file {
635
- font-family: var(--font-mono);
636
- background: var(--color-bg-card);
637
- padding: var(--space-xs) var(--space-sm);
638
- border-radius: var(--radius-sm);
639
- font-size: 0.75rem;
640
- }
641
-
642
- .finding-fix {
643
- margin-top: var(--space-md);
644
- padding: var(--space-md);
645
- background: rgba(34, 197, 94, 0.1);
646
- border-radius: var(--radius-md);
647
- font-size: 0.875rem;
648
- color: var(--color-ship);
649
- }
650
-
651
- .finding-fix::before {
652
- content: '💡 ';
653
- }
654
-
655
- /* ============================================================================
656
- FIX ESTIMATES
657
- ============================================================================ */
658
-
659
- .fix-estimate-card {
660
- display: flex;
661
- align-items: center;
662
- gap: var(--space-xl);
663
- background: linear-gradient(135deg, var(--color-bg-card), var(--color-bg-elevated));
664
- border-radius: var(--radius-xl);
665
- padding: var(--space-2xl);
666
- border: 1px solid var(--color-border);
667
- }
668
-
669
- .fix-estimate-total {
670
- text-align: center;
671
- }
672
-
673
- .fix-estimate-value {
674
- font-size: 3rem;
675
- font-weight: 800;
676
- background: linear-gradient(135deg, var(--color-primary), #8b5cf6);
677
- -webkit-background-clip: text;
678
- -webkit-text-fill-color: transparent;
679
- background-clip: text;
680
- }
681
-
682
- .fix-estimate-label {
683
- color: var(--color-text-muted);
684
- font-size: 0.875rem;
685
- text-transform: uppercase;
686
- letter-spacing: 0.05em;
687
- }
688
-
689
- .fix-estimate-breakdown {
690
- display: grid;
691
- grid-template-columns: repeat(4, 1fr);
692
- gap: var(--space-md);
693
- flex: 1;
694
- }
695
-
696
- .fix-item {
697
- text-align: center;
698
- padding: var(--space-md);
699
- background: var(--color-bg-card);
700
- border-radius: var(--radius-md);
701
- }
702
-
703
- .fix-item-count {
704
- font-size: 1.5rem;
705
- font-weight: 700;
706
- }
707
-
708
- .fix-item-time {
709
- font-size: 0.75rem;
710
- color: var(--color-text-muted);
711
- }
712
-
713
- /* ============================================================================
714
- FOOTER
715
- ============================================================================ */
716
-
717
- .report-footer {
718
- margin-top: var(--space-3xl);
719
- padding-top: var(--space-xl);
720
- border-top: 1px solid var(--color-border);
721
- display: flex;
722
- justify-content: space-between;
723
- align-items: center;
724
- color: var(--color-text-muted);
725
- font-size: 0.875rem;
726
- }
727
-
728
- .footer-brand {
729
- display: flex;
730
- align-items: center;
731
- gap: var(--space-sm);
732
- font-weight: 600;
733
- color: var(--color-text);
734
- }
735
-
736
- /* ============================================================================
737
- COVERAGE METRICS
738
- ============================================================================ */
739
-
740
- .coverage-grid {
741
- display: grid;
742
- grid-template-columns: repeat(4, 1fr);
743
- gap: var(--space-xl);
744
- text-align: center;
745
- }
746
-
747
- @media (max-width: 768px) {
748
- .coverage-grid {
749
- grid-template-columns: repeat(2, 1fr);
750
- }
751
- }
752
-
753
- .coverage-ring-container {
754
- display: flex;
755
- flex-direction: column;
756
- align-items: center;
757
- gap: var(--space-sm);
758
- position: relative;
759
- }
760
-
761
- .coverage-ring {
762
- display: block;
763
- }
764
-
765
- .coverage-ring-value {
766
- position: absolute;
767
- top: 35px;
768
- left: 50%;
769
- transform: translateX(-50%);
770
- font-size: 1.25rem;
771
- font-weight: 700;
772
- }
773
-
774
- .coverage-ring-label {
775
- font-size: 0.75rem;
776
- color: var(--color-text-muted);
777
- text-transform: uppercase;
778
- letter-spacing: 0.05em;
779
- }
780
-
781
- /* ============================================================================
782
- CHARTS
783
- ============================================================================ */
784
-
785
- .bar-chart-container,
786
- .sparkline-container {
787
- padding: var(--space-md);
788
- }
789
-
790
- .bar-chart-header,
791
- .sparkline-header {
792
- display: flex;
793
- justify-content: space-between;
794
- align-items: center;
795
- margin-bottom: var(--space-md);
796
- }
797
-
798
- .bar-chart-label,
799
- .sparkline-label {
800
- font-weight: 600;
801
- font-size: 0.875rem;
802
- }
803
-
804
- .bar-chart-value,
805
- .sparkline-value {
806
- font-size: 0.75rem;
807
- color: var(--color-text-muted);
808
- background: var(--color-bg-elevated);
809
- padding: var(--space-xs) var(--space-sm);
810
- border-radius: var(--radius-sm);
811
- }
812
-
813
- .bar-chart {
814
- display: flex;
815
- align-items: flex-end;
816
- gap: 6px;
817
- height: 80px;
818
- padding: var(--space-sm) 0;
819
- }
820
-
821
- .bar-chart .bar {
822
- background: linear-gradient(180deg, var(--color-primary), rgba(59, 130, 246, 0.3));
823
- border-radius: 4px 4px 0 0;
824
- transition: height 0.3s ease;
825
- }
826
-
827
- .bar-chart .bar.spike {
828
- background: linear-gradient(180deg, var(--color-warn), rgba(245, 158, 11, 0.3));
829
- }
830
-
831
- .bar-chart-hint,
832
- .sparkline-hint {
833
- font-size: 0.625rem;
834
- color: var(--color-text-dim);
835
- margin-top: var(--space-sm);
836
- }
837
-
838
- .sparkline {
839
- display: block;
840
- width: 100%;
841
- }
842
-
843
- /* ============================================================================
844
- BROKEN FLOWS
845
- ============================================================================ */
846
-
847
- .broken-flows {
848
- display: flex;
849
- flex-direction: column;
850
- gap: var(--space-lg);
851
- }
852
-
853
- .flow-card {
854
- background: var(--color-bg-card);
855
- border-radius: var(--radius-lg);
856
- padding: var(--space-xl);
857
- border: 1px solid var(--color-border);
858
- }
859
-
860
- .flow-header {
861
- display: flex;
862
- justify-content: space-between;
863
- align-items: center;
864
- margin-bottom: var(--space-lg);
865
- }
866
-
867
- .flow-title {
868
- font-weight: 600;
869
- font-size: 1rem;
870
- }
871
-
872
- .flow-severity {
873
- padding: var(--space-xs) var(--space-md);
874
- border-radius: var(--radius-full);
875
- font-size: 0.625rem;
876
- font-weight: 700;
877
- text-transform: uppercase;
878
- letter-spacing: 0.05em;
879
- }
880
-
881
- .flow-severity.blocker {
882
- background: var(--color-block-bg);
883
- color: var(--color-block);
884
- }
885
-
886
- .flow-severity.warning {
887
- background: var(--color-warn-bg);
888
- color: var(--color-warn);
889
- }
890
-
891
- .flow-steps {
892
- display: flex;
893
- flex-wrap: wrap;
894
- align-items: center;
895
- gap: var(--space-sm);
896
- }
897
-
898
- .flow-step {
899
- display: inline-flex;
900
- align-items: center;
901
- gap: var(--space-xs);
902
- padding: var(--space-sm) var(--space-md);
903
- border-radius: var(--radius-md);
904
- font-size: 0.75rem;
905
- background: var(--color-bg-elevated);
906
- }
907
-
908
- .flow-step.ui {
909
- border-left: 3px solid var(--color-primary);
910
- }
911
-
912
- .flow-step.api {
913
- border-left: 3px solid #8b5cf6;
914
- }
915
-
916
- .flow-step.form {
917
- border-left: 3px solid var(--color-warn);
918
- }
919
-
920
- .flow-step.error {
921
- border-left: 3px solid var(--color-block);
922
- background: var(--color-block-bg);
923
- }
924
-
925
- .step-type {
926
- font-weight: 600;
927
- font-size: 0.625rem;
928
- color: var(--color-text-dim);
929
- text-transform: uppercase;
930
- }
931
-
932
- .step-label {
933
- color: var(--color-text);
934
- }
935
-
936
- .step-arrow {
937
- color: var(--color-text-dim);
938
- font-size: 0.875rem;
939
- }
940
-
941
- /* ============================================================================
942
- PRINT STYLES
943
- ============================================================================ */
944
-
945
- @media print {
946
- body {
947
- background: white !important;
948
- color: black !important;
949
- }
16
+ function generateWorldClassHTML(reportData, opts = {}) {
17
+ const { meta, summary, findings, reality, fixEstimates, truthpack } = reportData;
18
+ const theme = opts.theme || "dark";
19
+ const company = opts.company || "";
20
+ const logo = opts.logo || "";
21
+ const redactPaths = opts.redactPaths || false;
950
22
 
951
- body::before {
952
- display: none;
953
- }
23
+ // Calculate metrics
24
+ const criticalCount = summary.severityCounts?.critical || findings.filter(f => f.severity === "BLOCK" || f.severity === "critical").length;
25
+ const highCount = summary.severityCounts?.high || findings.filter(f => f.severity === "high").length;
26
+ const mediumCount = summary.severityCounts?.medium || findings.filter(f => f.severity === "WARN" || f.severity === "medium").length;
27
+ const lowCount = summary.severityCounts?.low || findings.filter(f => f.severity === "INFO" || f.severity === "low").length;
28
+ const totalFindings = criticalCount + highCount + mediumCount + lowCount;
954
29
 
955
- .report {
956
- max-width: none;
957
- padding: 0;
958
- }
959
-
960
- .card, .score-card, .verdict-card {
961
- background: white !important;
962
- border: 1px solid #e5e5e5 !important;
963
- box-shadow: none !important;
964
- }
965
-
966
- .btn, .report-actions, .findings-tabs {
967
- display: none !important;
968
- }
969
-
970
- .section {
971
- page-break-inside: avoid;
972
- }
973
- }
974
- `;
975
- }
976
-
977
- // ============================================================================
978
- // COMPONENTS
979
- // ============================================================================
980
-
981
- function generateScoreRing(score, verdict) {
982
- const radius = 85;
983
- const circumference = 2 * Math.PI * radius;
984
- const progress = (score / 100) * circumference;
985
- const verdictClass = verdict.toLowerCase();
986
-
987
- return `
988
- <div class="score-ring">
989
- <svg width="200" height="200" viewBox="0 0 200 200">
990
- <circle class="score-ring-bg" cx="100" cy="100" r="${radius}"/>
991
- <circle
992
- class="score-ring-progress ${verdictClass}"
993
- cx="100" cy="100" r="${radius}"
994
- stroke-dasharray="${progress} ${circumference - progress}"
995
- />
996
- </svg>
997
- <div class="score-value">${score}</div>
998
- </div>
999
- `;
1000
- }
1001
-
1002
- function generateCategoryBars(categoryScores) {
1003
- return `
1004
- <div class="category-list">
1005
- ${Object.entries(categoryScores).map(([cat, score]) => {
1006
- const cls = score >= 80 ? "good" : score >= 50 ? "ok" : "bad";
1007
- return `
1008
- <div class="category-item">
1009
- <div class="category-header">
1010
- <span class="category-name">${formatCategoryName(cat)}</span>
1011
- <span class="category-score ${cls}">${score}%</span>
1012
- </div>
1013
- <div class="category-bar">
1014
- <div class="category-bar-fill ${cls}" style="width: ${score}%"></div>
1015
- </div>
1016
- </div>
1017
- `;
1018
- }).join("")}
1019
- </div>
1020
- `;
1021
- }
1022
-
1023
- function generateSeverityDonut(severityCounts) {
1024
- const total = Object.values(severityCounts).reduce((a, b) => a + b, 0);
1025
- if (total === 0) {
1026
- return `<div style="text-align: center; color: var(--color-ship); font-size: 1.25rem; padding: 40px;">✅ No issues found!</div>`;
1027
- }
1028
-
1029
- const colors = {
1030
- critical: "#ef4444",
1031
- high: "#f97316",
1032
- medium: "#eab308",
1033
- low: "#6b7280",
30
+ // Verdict styling
31
+ const verdictConfig = {
32
+ SHIP: { color: "#10b981", bg: "rgba(16,185,129,0.15)", icon: "✓", text: "READY TO SHIP" },
33
+ WARN: { color: "#f59e0b", bg: "rgba(245,158,11,0.15)", icon: "!", text: "NEEDS ATTENTION" },
34
+ BLOCK: { color: "#ef4444", bg: "rgba(239,68,68,0.15)", icon: "✕", text: "BLOCKED" },
1034
35
  };
36
+ const verdict = verdictConfig[summary.verdict] || verdictConfig.WARN;
1035
37
 
1036
- // SVG donut chart
1037
- let cumulativePercent = 0;
1038
- const segments = [];
38
+ // Score color gradient
39
+ const scoreColor = summary.score >= 80 ? "#10b981" : summary.score >= 60 ? "#f59e0b" : "#ef4444";
1039
40
 
1040
- for (const [sev, count] of Object.entries(severityCounts)) {
1041
- if (count === 0) continue;
1042
- const percent = (count / total) * 100;
1043
- const dashArray = `${percent} ${100 - percent}`;
1044
- const dashOffset = -cumulativePercent;
1045
- segments.push(`
1046
- <circle
1047
- r="15.9155"
1048
- cx="50%" cy="50%"
1049
- fill="transparent"
1050
- stroke="${colors[sev]}"
1051
- stroke-width="6"
1052
- stroke-dasharray="${dashArray}"
1053
- stroke-dashoffset="${dashOffset}"
1054
- />
1055
- `);
1056
- cumulativePercent += percent;
1057
- }
41
+ // Category scores for radar/bars
42
+ const categoryScores = summary.categoryScores || {};
43
+ const categories = Object.entries(categoryScores);
1058
44
 
1059
- return `
1060
- <div class="severity-chart">
1061
- <svg class="severity-donut" width="120" height="120" viewBox="0 0 42 42">
1062
- <circle r="15.9155" cx="50%" cy="50%" fill="transparent" stroke="var(--color-border)" stroke-width="6"/>
1063
- ${segments.join("")}
1064
- <text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="var(--color-text)" font-size="8" font-weight="700">${total}</text>
1065
- </svg>
1066
- <div class="severity-legend">
1067
- ${Object.entries(severityCounts).map(([sev, count]) => `
1068
- <div class="severity-item">
1069
- <span class="severity-dot ${sev}"></span>
1070
- <span class="severity-label">${sev.charAt(0).toUpperCase() + sev.slice(1)}</span>
1071
- <span class="severity-count">${count}</span>
1072
- </div>
1073
- `).join("")}
1074
- </div>
1075
- </div>
1076
- `;
1077
- }
1078
-
1079
- function generateFindingsList(findings, limit = 20) {
1080
- if (findings.length === 0) {
1081
- return `<div style="text-align: center; color: var(--color-text-muted); padding: 40px;">No findings to display</div>`;
1082
- }
45
+ // Reality data
46
+ const hasReality = reality && (reality.coverage || reality.brokenFlows?.length > 0);
47
+ const coverage = reality?.coverage || {};
48
+ const brokenFlows = reality?.brokenFlows || [];
1083
49
 
1084
- const displayed = findings.slice(0, limit);
1085
-
1086
- return `
1087
- <div class="findings-list">
1088
- ${displayed.map(f => `
1089
- <div class="finding-card ${f.severity}">
1090
- <div class="finding-header">
1091
- <span class="finding-severity ${f.severity}">${f.severity}</span>
1092
- <span class="finding-id">${f.id}</span>
1093
- <span class="finding-title">${escapeHtml(f.title)}</span>
1094
- </div>
1095
- <div class="finding-meta">
1096
- ${f.file ? `<span class="finding-file">${escapeHtml(f.file)}${f.line ? `:${f.line}` : ""}</span>` : ""}
1097
- ${f.category ? `<span>Category: ${formatCategoryName(f.category)}</span>` : ""}
1098
- ${f.fixTime ? `<span>~${f.fixTime} min to fix</span>` : ""}
1099
- </div>
1100
- ${f.fix ? `<div class="finding-fix">${escapeHtml(f.fix)}</div>` : ""}
50
+ return `<!DOCTYPE html>
51
+ <html lang="en" data-theme="${theme}">
52
+ <head>
53
+ <meta charset="UTF-8">
54
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
+ <meta name="color-scheme" content="dark light">
56
+ <title>VibeCheck Report — ${meta.projectName}</title>
57
+ <style>
58
+ /* ========================================
59
+ CSS VARIABLES & THEMING
60
+ ======================================== */
61
+ :root {
62
+ --font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
63
+ --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', Consolas, monospace;
64
+
65
+ /* Spacing scale */
66
+ --space-1: 4px;
67
+ --space-2: 8px;
68
+ --space-3: 12px;
69
+ --space-4: 16px;
70
+ --space-5: 24px;
71
+ --space-6: 32px;
72
+ --space-7: 48px;
73
+ --space-8: 64px;
74
+
75
+ /* Border radius */
76
+ --radius-sm: 6px;
77
+ --radius-md: 10px;
78
+ --radius-lg: 16px;
79
+ --radius-xl: 24px;
80
+ --radius-full: 9999px;
81
+
82
+ /* Transitions */
83
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
84
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
85
+ --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
86
+
87
+ /* Severity colors */
88
+ --color-critical: #ef4444;
89
+ --color-high: #f97316;
90
+ --color-medium: #f59e0b;
91
+ --color-low: #3b82f6;
92
+ --color-info: #6b7280;
93
+ --color-success: #10b981;
94
+ }
95
+
96
+ [data-theme="dark"] {
97
+ --bg-primary: #0a0a0f;
98
+ --bg-secondary: #111118;
99
+ --bg-tertiary: #1a1a24;
100
+ --bg-elevated: #22222e;
101
+ --bg-hover: #2a2a38;
102
+ --border-primary: rgba(255,255,255,0.08);
103
+ --border-secondary: rgba(255,255,255,0.12);
104
+ --text-primary: #fafafa;
105
+ --text-secondary: #a1a1aa;
106
+ --text-tertiary: #71717a;
107
+ --text-muted: #52525b;
108
+ --accent-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);
109
+ --glow-color: rgba(139, 92, 246, 0.15);
110
+ }
111
+
112
+ [data-theme="light"] {
113
+ --bg-primary: #ffffff;
114
+ --bg-secondary: #f9fafb;
115
+ --bg-tertiary: #f3f4f6;
116
+ --bg-elevated: #ffffff;
117
+ --bg-hover: #e5e7eb;
118
+ --border-primary: rgba(0,0,0,0.08);
119
+ --border-secondary: rgba(0,0,0,0.12);
120
+ --text-primary: #111827;
121
+ --text-secondary: #4b5563;
122
+ --text-tertiary: #6b7280;
123
+ --text-muted: #9ca3af;
124
+ --accent-gradient: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #9333ea 100%);
125
+ --glow-color: rgba(79, 70, 229, 0.1);
126
+ }
127
+
128
+ /* ========================================
129
+ RESET & BASE
130
+ ======================================== */
131
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
132
+
133
+ html {
134
+ font-size: 16px;
135
+ -webkit-font-smoothing: antialiased;
136
+ -moz-osx-font-smoothing: grayscale;
137
+ scroll-behavior: smooth;
138
+ }
139
+
140
+ body {
141
+ font-family: var(--font-sans);
142
+ background: var(--bg-primary);
143
+ color: var(--text-primary);
144
+ line-height: 1.6;
145
+ min-height: 100vh;
146
+ }
147
+
148
+ /* ========================================
149
+ LAYOUT
150
+ ======================================== */
151
+ .report-container {
152
+ max-width: 1200px;
153
+ margin: 0 auto;
154
+ padding: var(--space-6);
155
+ }
156
+
157
+ /* ========================================
158
+ HEADER
159
+ ======================================== */
160
+ .header {
161
+ display: flex;
162
+ justify-content: space-between;
163
+ align-items: flex-start;
164
+ padding-bottom: var(--space-6);
165
+ border-bottom: 1px solid var(--border-primary);
166
+ margin-bottom: var(--space-7);
167
+ }
168
+
169
+ .header-left { display: flex; flex-direction: column; gap: var(--space-2); }
170
+
171
+ .logo-container {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: var(--space-3);
175
+ margin-bottom: var(--space-2);
176
+ }
177
+
178
+ .logo {
179
+ height: 32px;
180
+ width: auto;
181
+ }
182
+
183
+ .brand {
184
+ font-size: 0.75rem;
185
+ font-weight: 600;
186
+ text-transform: uppercase;
187
+ letter-spacing: 0.1em;
188
+ color: var(--text-tertiary);
189
+ }
190
+
191
+ .header h1 {
192
+ font-size: 1.75rem;
193
+ font-weight: 700;
194
+ letter-spacing: -0.02em;
195
+ background: var(--accent-gradient);
196
+ -webkit-background-clip: text;
197
+ -webkit-text-fill-color: transparent;
198
+ background-clip: text;
199
+ }
200
+
201
+ .header-meta {
202
+ display: flex;
203
+ gap: var(--space-5);
204
+ color: var(--text-tertiary);
205
+ font-size: 0.875rem;
206
+ }
207
+
208
+ .header-meta span { display: flex; align-items: center; gap: var(--space-2); }
209
+
210
+ .header-right { display: flex; align-items: center; gap: var(--space-4); }
211
+
212
+ /* Theme Toggle */
213
+ .theme-toggle {
214
+ width: 44px;
215
+ height: 24px;
216
+ background: var(--bg-tertiary);
217
+ border: 1px solid var(--border-secondary);
218
+ border-radius: var(--radius-full);
219
+ cursor: pointer;
220
+ position: relative;
221
+ transition: var(--transition-base);
222
+ }
223
+
224
+ .theme-toggle::after {
225
+ content: '';
226
+ position: absolute;
227
+ top: 2px;
228
+ left: 2px;
229
+ width: 18px;
230
+ height: 18px;
231
+ background: var(--text-primary);
232
+ border-radius: 50%;
233
+ transition: var(--transition-base);
234
+ }
235
+
236
+ [data-theme="light"] .theme-toggle::after {
237
+ transform: translateX(20px);
238
+ }
239
+
240
+ .theme-toggle:hover { background: var(--bg-hover); }
241
+
242
+ /* Print Button */
243
+ .print-btn {
244
+ padding: var(--space-2) var(--space-4);
245
+ background: var(--bg-tertiary);
246
+ border: 1px solid var(--border-secondary);
247
+ border-radius: var(--radius-md);
248
+ color: var(--text-secondary);
249
+ font-size: 0.875rem;
250
+ font-weight: 500;
251
+ cursor: pointer;
252
+ transition: var(--transition-fast);
253
+ }
254
+
255
+ .print-btn:hover {
256
+ background: var(--bg-hover);
257
+ color: var(--text-primary);
258
+ }
259
+
260
+ /* ========================================
261
+ HERO SECTION - Score + Verdict
262
+ ======================================== */
263
+ .hero {
264
+ display: grid;
265
+ grid-template-columns: 1fr 1fr;
266
+ gap: var(--space-6);
267
+ margin-bottom: var(--space-7);
268
+ }
269
+
270
+ .score-card {
271
+ background: var(--bg-secondary);
272
+ border: 1px solid var(--border-primary);
273
+ border-radius: var(--radius-xl);
274
+ padding: var(--space-7);
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ justify-content: center;
279
+ position: relative;
280
+ overflow: hidden;
281
+ }
282
+
283
+ .score-card::before {
284
+ content: '';
285
+ position: absolute;
286
+ top: -50%;
287
+ left: -50%;
288
+ width: 200%;
289
+ height: 200%;
290
+ background: radial-gradient(circle at center, var(--glow-color) 0%, transparent 50%);
291
+ opacity: 0.5;
292
+ }
293
+
294
+ .score-ring {
295
+ position: relative;
296
+ width: 200px;
297
+ height: 200px;
298
+ margin-bottom: var(--space-5);
299
+ }
300
+
301
+ .score-ring svg {
302
+ width: 100%;
303
+ height: 100%;
304
+ transform: rotate(-90deg);
305
+ }
306
+
307
+ .score-ring-bg {
308
+ fill: none;
309
+ stroke: var(--bg-tertiary);
310
+ stroke-width: 12;
311
+ }
312
+
313
+ .score-ring-progress {
314
+ fill: none;
315
+ stroke: url(#scoreGradient);
316
+ stroke-width: 12;
317
+ stroke-linecap: round;
318
+ stroke-dasharray: 565;
319
+ stroke-dashoffset: 565;
320
+ animation: scoreReveal 1.5s ease-out forwards;
321
+ animation-delay: 0.3s;
322
+ }
323
+
324
+ @keyframes scoreReveal {
325
+ to { stroke-dashoffset: ${565 - (565 * summary.score / 100)}; }
326
+ }
327
+
328
+ .score-value {
329
+ position: absolute;
330
+ top: 50%;
331
+ left: 50%;
332
+ transform: translate(-50%, -50%);
333
+ font-size: 3.5rem;
334
+ font-weight: 800;
335
+ letter-spacing: -0.02em;
336
+ color: ${scoreColor};
337
+ }
338
+
339
+ .score-label {
340
+ font-size: 0.875rem;
341
+ color: var(--text-tertiary);
342
+ text-transform: uppercase;
343
+ letter-spacing: 0.1em;
344
+ }
345
+
346
+ .verdict-card {
347
+ background: var(--bg-secondary);
348
+ border: 1px solid var(--border-primary);
349
+ border-radius: var(--radius-xl);
350
+ padding: var(--space-7);
351
+ display: flex;
352
+ flex-direction: column;
353
+ justify-content: center;
354
+ }
355
+
356
+ .verdict-badge {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ gap: var(--space-3);
360
+ padding: var(--space-3) var(--space-5);
361
+ background: ${verdict.bg};
362
+ border-radius: var(--radius-full);
363
+ width: fit-content;
364
+ margin-bottom: var(--space-5);
365
+ }
366
+
367
+ .verdict-icon {
368
+ width: 24px;
369
+ height: 24px;
370
+ border-radius: 50%;
371
+ background: ${verdict.color};
372
+ color: white;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ font-weight: 700;
377
+ font-size: 0.875rem;
378
+ }
379
+
380
+ .verdict-text {
381
+ font-size: 1rem;
382
+ font-weight: 700;
383
+ color: ${verdict.color};
384
+ letter-spacing: 0.05em;
385
+ }
386
+
387
+ .verdict-summary {
388
+ font-size: 1rem;
389
+ color: var(--text-secondary);
390
+ line-height: 1.7;
391
+ margin-bottom: var(--space-5);
392
+ }
393
+
394
+ .verdict-stats {
395
+ display: grid;
396
+ grid-template-columns: repeat(4, 1fr);
397
+ gap: var(--space-3);
398
+ }
399
+
400
+ .stat-item {
401
+ text-align: center;
402
+ padding: var(--space-3);
403
+ background: var(--bg-tertiary);
404
+ border-radius: var(--radius-md);
405
+ }
406
+
407
+ .stat-value {
408
+ font-size: 1.5rem;
409
+ font-weight: 700;
410
+ color: var(--text-primary);
411
+ }
412
+
413
+ .stat-value.critical { color: var(--color-critical); }
414
+ .stat-value.high { color: var(--color-high); }
415
+ .stat-value.medium { color: var(--color-medium); }
416
+ .stat-value.low { color: var(--color-low); }
417
+
418
+ .stat-label {
419
+ font-size: 0.75rem;
420
+ color: var(--text-tertiary);
421
+ text-transform: uppercase;
422
+ letter-spacing: 0.05em;
423
+ }
424
+
425
+ /* ========================================
426
+ CATEGORY SCORES
427
+ ======================================== */
428
+ .section {
429
+ margin-bottom: var(--space-7);
430
+ }
431
+
432
+ .section-header {
433
+ display: flex;
434
+ justify-content: space-between;
435
+ align-items: center;
436
+ margin-bottom: var(--space-5);
437
+ }
438
+
439
+ .section-title {
440
+ font-size: 1.25rem;
441
+ font-weight: 600;
442
+ color: var(--text-primary);
443
+ }
444
+
445
+ .section-badge {
446
+ font-size: 0.75rem;
447
+ padding: var(--space-1) var(--space-3);
448
+ background: var(--bg-tertiary);
449
+ border-radius: var(--radius-full);
450
+ color: var(--text-secondary);
451
+ }
452
+
453
+ .categories-grid {
454
+ display: grid;
455
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
456
+ gap: var(--space-4);
457
+ }
458
+
459
+ .category-card {
460
+ background: var(--bg-secondary);
461
+ border: 1px solid var(--border-primary);
462
+ border-radius: var(--radius-lg);
463
+ padding: var(--space-5);
464
+ transition: var(--transition-fast);
465
+ }
466
+
467
+ .category-card:hover {
468
+ border-color: var(--border-secondary);
469
+ transform: translateY(-2px);
470
+ }
471
+
472
+ .category-header {
473
+ display: flex;
474
+ justify-content: space-between;
475
+ align-items: center;
476
+ margin-bottom: var(--space-3);
477
+ }
478
+
479
+ .category-name {
480
+ font-size: 0.875rem;
481
+ font-weight: 500;
482
+ color: var(--text-secondary);
483
+ }
484
+
485
+ .category-score {
486
+ font-size: 1.25rem;
487
+ font-weight: 700;
488
+ }
489
+
490
+ .category-bar {
491
+ height: 6px;
492
+ background: var(--bg-tertiary);
493
+ border-radius: var(--radius-full);
494
+ overflow: hidden;
495
+ }
496
+
497
+ .category-bar-fill {
498
+ height: 100%;
499
+ border-radius: var(--radius-full);
500
+ transition: width 1s ease-out;
501
+ }
502
+
503
+ /* ========================================
504
+ REALITY MODE SECTION
505
+ ======================================== */
506
+ .reality-section {
507
+ background: var(--bg-secondary);
508
+ border: 1px solid var(--border-primary);
509
+ border-radius: var(--radius-xl);
510
+ padding: var(--space-6);
511
+ margin-bottom: var(--space-7);
512
+ }
513
+
514
+ .reality-header {
515
+ display: flex;
516
+ align-items: center;
517
+ gap: var(--space-3);
518
+ margin-bottom: var(--space-5);
519
+ }
520
+
521
+ .reality-icon {
522
+ width: 40px;
523
+ height: 40px;
524
+ background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
525
+ border-radius: var(--radius-md);
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ font-size: 1.25rem;
530
+ }
531
+
532
+ .reality-title {
533
+ font-size: 1.25rem;
534
+ font-weight: 600;
535
+ }
536
+
537
+ .reality-subtitle {
538
+ font-size: 0.875rem;
539
+ color: var(--text-tertiary);
540
+ }
541
+
542
+ .coverage-grid {
543
+ display: grid;
544
+ grid-template-columns: repeat(4, 1fr);
545
+ gap: var(--space-4);
546
+ margin-bottom: var(--space-6);
547
+ }
548
+
549
+ .coverage-item {
550
+ text-align: center;
551
+ padding: var(--space-4);
552
+ background: var(--bg-tertiary);
553
+ border-radius: var(--radius-lg);
554
+ }
555
+
556
+ .coverage-value {
557
+ font-size: 2rem;
558
+ font-weight: 700;
559
+ background: linear-gradient(135deg, #10b981 0%, #3b82f6 100%);
560
+ -webkit-background-clip: text;
561
+ -webkit-text-fill-color: transparent;
562
+ background-clip: text;
563
+ }
564
+
565
+ .coverage-label {
566
+ font-size: 0.75rem;
567
+ color: var(--text-tertiary);
568
+ margin-top: var(--space-1);
569
+ }
570
+
571
+ /* Broken Flows */
572
+ .broken-flows { margin-top: var(--space-6); }
573
+
574
+ .flow-card {
575
+ background: var(--bg-tertiary);
576
+ border: 1px solid var(--border-primary);
577
+ border-radius: var(--radius-lg);
578
+ padding: var(--space-5);
579
+ margin-bottom: var(--space-4);
580
+ }
581
+
582
+ .flow-header {
583
+ display: flex;
584
+ justify-content: space-between;
585
+ align-items: center;
586
+ margin-bottom: var(--space-4);
587
+ }
588
+
589
+ .flow-title {
590
+ font-size: 1rem;
591
+ font-weight: 600;
592
+ color: var(--text-primary);
593
+ }
594
+
595
+ .flow-severity {
596
+ padding: var(--space-1) var(--space-3);
597
+ border-radius: var(--radius-full);
598
+ font-size: 0.75rem;
599
+ font-weight: 600;
600
+ text-transform: uppercase;
601
+ }
602
+
603
+ .flow-severity.block {
604
+ background: rgba(239,68,68,0.15);
605
+ color: var(--color-critical);
606
+ }
607
+
608
+ .flow-steps {
609
+ display: flex;
610
+ align-items: center;
611
+ gap: var(--space-2);
612
+ flex-wrap: wrap;
613
+ }
614
+
615
+ .flow-step {
616
+ display: flex;
617
+ align-items: center;
618
+ gap: var(--space-2);
619
+ padding: var(--space-2) var(--space-3);
620
+ background: var(--bg-secondary);
621
+ border-radius: var(--radius-md);
622
+ font-size: 0.8125rem;
623
+ }
624
+
625
+ .flow-step.error {
626
+ background: rgba(239,68,68,0.15);
627
+ color: var(--color-critical);
628
+ }
629
+
630
+ .flow-step-icon {
631
+ width: 16px;
632
+ height: 16px;
633
+ border-radius: 4px;
634
+ display: flex;
635
+ align-items: center;
636
+ justify-content: center;
637
+ font-size: 0.625rem;
638
+ }
639
+
640
+ .flow-step-icon.ui { background: #3b82f6; color: white; }
641
+ .flow-step-icon.api { background: #10b981; color: white; }
642
+ .flow-step-icon.form { background: #8b5cf6; color: white; }
643
+ .flow-step-icon.error { background: #ef4444; color: white; }
644
+
645
+ .flow-arrow {
646
+ color: var(--text-muted);
647
+ font-size: 0.75rem;
648
+ }
649
+
650
+ /* ========================================
651
+ FINDINGS TABLE
652
+ ======================================== */
653
+ .findings-section {
654
+ background: var(--bg-secondary);
655
+ border: 1px solid var(--border-primary);
656
+ border-radius: var(--radius-xl);
657
+ overflow: hidden;
658
+ margin-bottom: var(--space-7);
659
+ }
660
+
661
+ .findings-header {
662
+ padding: var(--space-5) var(--space-6);
663
+ border-bottom: 1px solid var(--border-primary);
664
+ display: flex;
665
+ justify-content: space-between;
666
+ align-items: center;
667
+ }
668
+
669
+ .findings-title {
670
+ font-size: 1.25rem;
671
+ font-weight: 600;
672
+ }
673
+
674
+ .findings-count {
675
+ font-size: 0.875rem;
676
+ color: var(--text-tertiary);
677
+ }
678
+
679
+ .findings-filters {
680
+ display: flex;
681
+ gap: var(--space-2);
682
+ padding: var(--space-4) var(--space-6);
683
+ border-bottom: 1px solid var(--border-primary);
684
+ background: var(--bg-tertiary);
685
+ }
686
+
687
+ .filter-btn {
688
+ padding: var(--space-2) var(--space-4);
689
+ background: transparent;
690
+ border: 1px solid var(--border-secondary);
691
+ border-radius: var(--radius-full);
692
+ font-size: 0.8125rem;
693
+ color: var(--text-secondary);
694
+ cursor: pointer;
695
+ transition: var(--transition-fast);
696
+ }
697
+
698
+ .filter-btn:hover, .filter-btn.active {
699
+ background: var(--bg-hover);
700
+ color: var(--text-primary);
701
+ }
702
+
703
+ .filter-btn.active {
704
+ border-color: var(--text-primary);
705
+ }
706
+
707
+ .findings-list { max-height: 600px; overflow-y: auto; }
708
+
709
+ .finding-item {
710
+ display: grid;
711
+ grid-template-columns: 80px 1fr auto;
712
+ gap: var(--space-4);
713
+ padding: var(--space-4) var(--space-6);
714
+ border-bottom: 1px solid var(--border-primary);
715
+ transition: var(--transition-fast);
716
+ }
717
+
718
+ .finding-item:hover { background: var(--bg-hover); }
719
+
720
+ .finding-severity {
721
+ display: flex;
722
+ align-items: center;
723
+ justify-content: center;
724
+ }
725
+
726
+ .severity-badge {
727
+ padding: var(--space-1) var(--space-3);
728
+ border-radius: var(--radius-full);
729
+ font-size: 0.6875rem;
730
+ font-weight: 700;
731
+ text-transform: uppercase;
732
+ letter-spacing: 0.05em;
733
+ }
734
+
735
+ .severity-badge.critical { background: rgba(239,68,68,0.15); color: var(--color-critical); }
736
+ .severity-badge.high { background: rgba(249,115,22,0.15); color: var(--color-high); }
737
+ .severity-badge.medium, .severity-badge.warn { background: rgba(245,158,11,0.15); color: var(--color-medium); }
738
+ .severity-badge.low, .severity-badge.info { background: rgba(59,130,246,0.15); color: var(--color-low); }
739
+
740
+ .finding-content { min-width: 0; }
741
+
742
+ .finding-title {
743
+ font-size: 0.9375rem;
744
+ font-weight: 500;
745
+ color: var(--text-primary);
746
+ margin-bottom: var(--space-1);
747
+ }
748
+
749
+ .finding-file {
750
+ font-family: var(--font-mono);
751
+ font-size: 0.8125rem;
752
+ color: var(--text-tertiary);
753
+ }
754
+
755
+ .finding-fix {
756
+ font-size: 0.8125rem;
757
+ color: var(--text-secondary);
758
+ max-width: 300px;
759
+ text-align: right;
760
+ }
761
+
762
+ /* ========================================
763
+ FIX ESTIMATES
764
+ ======================================== */
765
+ .estimates-section {
766
+ background: var(--bg-secondary);
767
+ border: 1px solid var(--border-primary);
768
+ border-radius: var(--radius-xl);
769
+ padding: var(--space-6);
770
+ margin-bottom: var(--space-7);
771
+ }
772
+
773
+ .estimates-grid {
774
+ display: grid;
775
+ grid-template-columns: repeat(3, 1fr);
776
+ gap: var(--space-5);
777
+ }
778
+
779
+ .estimate-card {
780
+ text-align: center;
781
+ padding: var(--space-5);
782
+ background: var(--bg-tertiary);
783
+ border-radius: var(--radius-lg);
784
+ }
785
+
786
+ .estimate-value {
787
+ font-size: 2rem;
788
+ font-weight: 700;
789
+ background: var(--accent-gradient);
790
+ -webkit-background-clip: text;
791
+ -webkit-text-fill-color: transparent;
792
+ background-clip: text;
793
+ }
794
+
795
+ .estimate-label {
796
+ font-size: 0.875rem;
797
+ color: var(--text-tertiary);
798
+ margin-top: var(--space-1);
799
+ }
800
+
801
+ /* ========================================
802
+ FOOTER
803
+ ======================================== */
804
+ .footer {
805
+ text-align: center;
806
+ padding: var(--space-6) 0;
807
+ border-top: 1px solid var(--border-primary);
808
+ color: var(--text-tertiary);
809
+ font-size: 0.875rem;
810
+ }
811
+
812
+ .footer-brand {
813
+ display: flex;
814
+ align-items: center;
815
+ justify-content: center;
816
+ gap: var(--space-2);
817
+ margin-bottom: var(--space-2);
818
+ }
819
+
820
+ .footer-brand svg {
821
+ width: 20px;
822
+ height: 20px;
823
+ }
824
+
825
+ .footer a {
826
+ color: var(--text-secondary);
827
+ text-decoration: none;
828
+ transition: var(--transition-fast);
829
+ }
830
+
831
+ .footer a:hover { color: var(--text-primary); }
832
+
833
+ /* ========================================
834
+ RESPONSIVE
835
+ ======================================== */
836
+ @media (max-width: 768px) {
837
+ .hero { grid-template-columns: 1fr; }
838
+ .coverage-grid { grid-template-columns: repeat(2, 1fr); }
839
+ .verdict-stats { grid-template-columns: repeat(2, 1fr); }
840
+ .estimates-grid { grid-template-columns: 1fr; }
841
+ .finding-item { grid-template-columns: 1fr; gap: var(--space-2); }
842
+ .finding-fix { text-align: left; max-width: none; }
843
+ }
844
+
845
+ /* ========================================
846
+ PRINT STYLES
847
+ ======================================== */
848
+ @media print {
849
+ body { background: white; color: black; }
850
+ .report-container { max-width: none; padding: 20px; }
851
+ .theme-toggle, .print-btn { display: none; }
852
+ .score-ring-progress { animation: none; stroke-dashoffset: ${565 - (565 * summary.score / 100)}; }
853
+ .findings-list { max-height: none; overflow: visible; }
854
+ .section, .hero, .reality-section, .findings-section, .estimates-section {
855
+ break-inside: avoid;
856
+ page-break-inside: avoid;
857
+ }
858
+ [data-theme="dark"] {
859
+ --bg-primary: white;
860
+ --bg-secondary: #f9fafb;
861
+ --bg-tertiary: #f3f4f6;
862
+ --text-primary: #111827;
863
+ --text-secondary: #4b5563;
864
+ --text-tertiary: #6b7280;
865
+ --border-primary: #e5e7eb;
866
+ }
867
+ }
868
+ </style>
869
+ </head>
870
+ <body>
871
+ <div class="report-container">
872
+ <!-- HEADER -->
873
+ <header class="header">
874
+ <div class="header-left">
875
+ <div class="logo-container">
876
+ ${logo ? `<img src="${logo}" alt="Logo" class="logo">` : ''}
877
+ <span class="brand">${company || 'VibeCheck'}</span>
1101
878
  </div>
1102
- `).join("")}
1103
- ${findings.length > limit ? `
1104
- <div style="text-align: center; color: var(--color-text-muted); padding: var(--space-lg);">
1105
- + ${findings.length - limit} more findings
879
+ <h1>Ship Readiness Report</h1>
880
+ <div class="header-meta">
881
+ <span>
882
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
883
+ <path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
884
+ </svg>
885
+ ${meta.projectName}
886
+ </span>
887
+ <span>
888
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
889
+ <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
890
+ </svg>
891
+ ${new Date(meta.generatedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
892
+ </span>
893
+ <span>
894
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
895
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>
896
+ </svg>
897
+ ${meta.reportId || 'VC-' + Date.now().toString(36).toUpperCase()}
898
+ </span>
1106
899
  </div>
1107
- ` : ""}
1108
- </div>
1109
- `;
1110
- }
1111
-
1112
- function generateFixEstimates(fixEstimates) {
1113
- const { bySeverity, humanReadable } = fixEstimates;
1114
-
1115
- return `
1116
- <div class="fix-estimate-card">
1117
- <div class="fix-estimate-total">
1118
- <div class="fix-estimate-value">${humanReadable}</div>
1119
- <div class="fix-estimate-label">Total Fix Time</div>
1120
900
  </div>
1121
- <div class="fix-estimate-breakdown">
1122
- ${Object.entries(bySeverity).map(([sev, data]) => `
1123
- <div class="fix-item">
1124
- <div class="fix-item-count" style="color: var(--color-${sev})">${data.count}</div>
1125
- <div class="fix-item-time">${formatDuration(data.totalMinutes)}</div>
1126
- <div style="font-size: 0.75rem; color: var(--color-text-dim)">${sev}</div>
1127
- </div>
1128
- `).join("")}
901
+ <div class="header-right">
902
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme"></button>
903
+ <button class="print-btn" onclick="window.print()">
904
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 4px;">
905
+ <polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>
906
+ </svg>
907
+ Print
908
+ </button>
1129
909
  </div>
1130
- </div>
1131
- `;
1132
- }
1133
-
1134
- function generateCoverageRing(percent, label, color) {
1135
- const radius = 40;
1136
- const circumference = 2 * Math.PI * radius;
1137
- const progress = (percent / 100) * circumference;
1138
- const colorVar = color || (percent >= 80 ? "var(--color-ship)" : percent >= 50 ? "var(--color-warn)" : "var(--color-block)");
1139
-
1140
- return `
1141
- <div class="coverage-ring-container">
1142
- <svg class="coverage-ring" width="100" height="100" viewBox="0 0 100 100">
1143
- <circle cx="50" cy="50" r="${radius}" fill="none" stroke="var(--color-border)" stroke-width="8"/>
1144
- <circle cx="50" cy="50" r="${radius}" fill="none" stroke="${colorVar}" stroke-width="8"
1145
- stroke-linecap="round" stroke-dasharray="${progress} ${circumference - progress}"
1146
- transform="rotate(-90 50 50)" style="transition: stroke-dasharray 1s ease-out;"/>
1147
- </svg>
1148
- <div class="coverage-ring-value">${percent}%</div>
1149
- <div class="coverage-ring-label">${label}</div>
1150
- </div>
1151
- `;
1152
- }
910
+ </header>
1153
911
 
1154
- function generateCoverageMetrics(reality) {
1155
- if (!reality || !reality.coverage) return "";
1156
-
1157
- const { clientCallsMapped, runtimeRequests, uiActionsVerified, authRoutes } = reality.coverage;
1158
-
1159
- return `
1160
- <section class="section">
1161
- <div class="section-header">
1162
- <div class="section-icon">📊</div>
1163
- <div>
1164
- <h2 class="section-title">Coverage Metrics</h2>
1165
- <p class="section-subtitle">Runtime verification results</p>
912
+ <!-- HERO: SCORE + VERDICT -->
913
+ <section class="hero">
914
+ <div class="score-card">
915
+ <div class="score-ring">
916
+ <svg viewBox="0 0 200 200">
917
+ <defs>
918
+ <linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="100%">
919
+ <stop offset="0%" stop-color="${summary.score >= 80 ? '#10b981' : summary.score >= 60 ? '#f59e0b' : '#ef4444'}"/>
920
+ <stop offset="100%" stop-color="${summary.score >= 80 ? '#3b82f6' : summary.score >= 60 ? '#f97316' : '#dc2626'}"/>
921
+ </linearGradient>
922
+ </defs>
923
+ <circle class="score-ring-bg" cx="100" cy="100" r="90"/>
924
+ <circle class="score-ring-progress" cx="100" cy="100" r="90"/>
925
+ </svg>
926
+ <div class="score-value">${summary.score}</div>
1166
927
  </div>
928
+ <div class="score-label">Overall Score</div>
1167
929
  </div>
1168
- <div class="card">
1169
- <div class="coverage-grid">
1170
- ${generateCoverageRing(clientCallsMapped || 0, "Client Calls Mapped", "var(--color-warn)")}
1171
- ${generateCoverageRing(runtimeRequests || 0, "Runtime Requests", "var(--color-warn)")}
1172
- ${generateCoverageRing(uiActionsVerified || 0, "UI Actions Verified", "var(--color-ship)")}
1173
- ${generateCoverageRing(authRoutes || 0, "Auth Routes", "var(--color-ship)")}
930
+
931
+ <div class="verdict-card">
932
+ <div class="verdict-badge">
933
+ <span class="verdict-icon">${verdict.icon}</span>
934
+ <span class="verdict-text">${verdict.text}</span>
1174
935
  </div>
1175
- </div>
1176
- </section>
1177
- `;
1178
- }
1179
-
1180
- function generateSparklineChart(data, label, maxLabel) {
1181
- if (!data || data.length === 0) return "";
1182
-
1183
- const max = Math.max(...data);
1184
- const width = 300;
1185
- const height = 60;
1186
- const padding = 5;
1187
- const stepX = (width - padding * 2) / (data.length - 1);
1188
-
1189
- const points = data.map((v, i) => {
1190
- const x = padding + i * stepX;
1191
- const y = height - padding - ((v / max) * (height - padding * 2));
1192
- return `${x},${y}`;
1193
- }).join(" ");
1194
-
1195
- return `
1196
- <div class="sparkline-container">
1197
- <div class="sparkline-header">
1198
- <span class="sparkline-label">${label}</span>
1199
- <span class="sparkline-value">${maxLabel}</span>
1200
- </div>
1201
- <svg class="sparkline" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
1202
- <polyline fill="none" stroke="var(--color-primary)" stroke-width="2" points="${points}"/>
1203
- <circle cx="${padding + (data.length - 1) * stepX}" cy="${height - padding - ((data[data.length - 1] / max) * (height - padding * 2))}" r="4" fill="var(--color-primary)"/>
1204
- </svg>
1205
- <div class="sparkline-hint">smooth = stable · sharp peaks = slow endpoints</div>
1206
- </div>
1207
- `;
1208
- }
1209
-
1210
- function generateBarChart(data, label, totalLabel) {
1211
- if (!data || data.length === 0) return "";
1212
-
1213
- const max = Math.max(...data);
1214
- const barWidth = 20;
1215
- const gap = 8;
1216
- const height = 80;
1217
-
1218
- return `
1219
- <div class="bar-chart-container">
1220
- <div class="bar-chart-header">
1221
- <span class="bar-chart-label">${label}</span>
1222
- <span class="bar-chart-value">${data.reduce((a, b) => a + b, 0)} ${totalLabel}</span>
1223
- </div>
1224
- <div class="bar-chart">
1225
- ${data.map((v, i) => {
1226
- const barHeight = max > 0 ? (v / max) * (height - 10) : 0;
1227
- const isSpike = v > (max * 0.7);
1228
- return `<div class="bar ${isSpike ? 'spike' : ''}" style="height: ${barHeight}px; width: ${barWidth}px;"></div>`;
1229
- }).join("")}
1230
- </div>
1231
- <div class="bar-chart-hint">last ${data.length} steps · spikes = unmapped</div>
1232
- </div>
1233
- `;
1234
- }
1235
-
1236
- function generateRuntimeCharts(reality) {
1237
- if (!reality) return "";
1238
-
1239
- const hasCharts = reality.requestsOverTime || reality.latencySparkline;
1240
- if (!hasCharts) return "";
1241
-
1242
- return `
1243
- <section class="section">
1244
- <div class="card-grid" style="grid-template-columns: 1fr 1fr;">
1245
- ${reality.requestsOverTime ? `
1246
- <div class="card">
1247
- ${generateBarChart(reality.requestsOverTime, "Requests Over Time", "total")}
936
+ <p class="verdict-summary">${getVerdictSummary(summary.verdict, criticalCount, highCount, mediumCount)}</p>
937
+ <div class="verdict-stats">
938
+ <div class="stat-item">
939
+ <div class="stat-value critical">${criticalCount}</div>
940
+ <div class="stat-label">Critical</div>
941
+ </div>
942
+ <div class="stat-item">
943
+ <div class="stat-value high">${highCount}</div>
944
+ <div class="stat-label">High</div>
945
+ </div>
946
+ <div class="stat-item">
947
+ <div class="stat-value medium">${mediumCount}</div>
948
+ <div class="stat-label">Medium</div>
1248
949
  </div>
1249
- ` : ""}
1250
- ${reality.latencySparkline ? `
1251
- <div class="card">
1252
- ${generateSparklineChart(reality.latencySparkline, "Latency Sparkline", (reality.latencyP95 || 0) + "ms p95")}
950
+ <div class="stat-item">
951
+ <div class="stat-value low">${lowCount}</div>
952
+ <div class="stat-label">Low</div>
1253
953
  </div>
1254
- ` : ""}
954
+ </div>
1255
955
  </div>
1256
956
  </section>
1257
- `;
1258
- }
1259
957
 
1260
- function generateBrokenFlows(reality) {
1261
- if (!reality || !reality.brokenFlows || reality.brokenFlows.length === 0) return "";
1262
-
1263
- const criticalCount = reality.brokenFlows.filter(f => f.severity === "BLOCK").length;
1264
-
1265
- return `
958
+ ${categories.length > 0 ? `
959
+ <!-- CATEGORY SCORES -->
1266
960
  <section class="section">
1267
961
  <div class="section-header">
1268
- <div class="section-icon">🔗</div>
1269
- <div>
1270
- <h2 class="section-title">Broken User Flows</h2>
1271
- <p class="section-subtitle">${criticalCount} critical flows detected</p>
1272
- </div>
962
+ <h2 class="section-title">Category Breakdown</h2>
963
+ <span class="section-badge">${categories.length} categories</span>
1273
964
  </div>
1274
- <div class="broken-flows">
1275
- ${reality.brokenFlows.map(flow => `
1276
- <div class="flow-card">
1277
- <div class="flow-header">
1278
- <span class="flow-title">${escapeHtml(flow.title)}</span>
1279
- <span class="flow-severity ${flow.severity === 'BLOCK' ? 'blocker' : 'warning'}">${flow.severity === 'BLOCK' ? 'BLOCKER' : 'WARNING'}</span>
965
+ <div class="categories-grid">
966
+ ${categories.map(([name, score]) => {
967
+ const color = score >= 80 ? 'var(--color-success)' : score >= 60 ? 'var(--color-medium)' : 'var(--color-critical)';
968
+ return `
969
+ <div class="category-card">
970
+ <div class="category-header">
971
+ <span class="category-name">${formatCategoryName(name)}</span>
972
+ <span class="category-score" style="color: ${color}">${score}%</span>
1280
973
  </div>
1281
- <div class="flow-steps">
1282
- ${flow.steps.map((step, i) => `
1283
- <div class="flow-step ${step.type}">
1284
- <span class="step-type">${step.type.toUpperCase()}</span>
1285
- <span class="step-label">${escapeHtml(step.label)}</span>
1286
- </div>
1287
- ${i < flow.steps.length - 1 ? '<span class="step-arrow">→</span>' : ''}
1288
- `).join("")}
974
+ <div class="category-bar">
975
+ <div class="category-bar-fill" style="width: ${score}%; background: ${color}"></div>
1289
976
  </div>
1290
977
  </div>
1291
- `).join("")}
978
+ `;
979
+ }).join('')}
1292
980
  </div>
1293
981
  </section>
1294
- `;
1295
- }
1296
-
1297
- function formatDuration(mins) {
1298
- if (mins === 0) return "0m";
1299
- if (mins < 60) return `${mins}m`;
1300
- const h = Math.floor(mins / 60);
1301
- const m = mins % 60;
1302
- return m > 0 ? `${h}h ${m}m` : `${h}h`;
1303
- }
1304
-
1305
- function escapeHtml(str) {
1306
- if (!str) return "";
1307
- return String(str)
1308
- .replace(/&/g, "&amp;")
1309
- .replace(/</g, "&lt;")
1310
- .replace(/>/g, "&gt;")
1311
- .replace(/"/g, "&quot;");
1312
- }
982
+ ` : ''}
1313
983
 
1314
- // ============================================================================
1315
- // MAIN TEMPLATE
1316
- // ============================================================================
1317
-
1318
- function generateWorldClassHTML(reportData, options = {}) {
1319
- const { meta, summary, findings, fixEstimates, coverage, truthpack, reality } = reportData;
1320
- const verdictClass = summary.verdict.toLowerCase();
1321
-
1322
- const verdictMessages = {
1323
- ship: "All systems go. This application is production-ready.",
1324
- warn: "Minor issues detected. Review recommended before deployment.",
1325
- block: "Critical issues found. Immediate remediation required.",
1326
- };
1327
-
1328
- const verdictIcons = { ship: "✅", warn: "⚠️", block: "🚫" };
1329
-
1330
- return `<!DOCTYPE html>
1331
- <html lang="en" data-theme="${options.theme || "dark"}">
1332
- <head>
1333
- <meta charset="UTF-8">
1334
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1335
- <title>Vibecheck Report - ${escapeHtml(meta.projectName)}</title>
1336
- <link rel="preconnect" href="https://fonts.googleapis.com">
1337
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1338
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1339
- <style>${getDesignSystem()}</style>
1340
- </head>
1341
- <body>
1342
- <div class="report">
1343
- <!-- Header -->
1344
- <header class="report-header">
1345
- <div>
1346
- <h1 class="report-title">Ship Readiness Report</h1>
1347
- <div class="report-meta">
1348
- <span><strong>Project:</strong> ${escapeHtml(meta.projectName)}</span>
1349
- <span><strong>Generated:</strong> ${new Date(meta.generatedAt).toLocaleString()}</span>
1350
- <span><strong>Report ID:</strong> ${meta.reportId}</span>
984
+ ${hasReality ? `
985
+ <!-- REALITY MODE -->
986
+ <section class="reality-section">
987
+ <div class="reality-header">
988
+ <div class="reality-icon">🔬</div>
989
+ <div>
990
+ <h2 class="reality-title">Reality Mode</h2>
991
+ <p class="reality-subtitle">Runtime validation results from Playwright testing</p>
1351
992
  </div>
1352
993
  </div>
1353
- <div class="report-actions">
1354
- <button class="btn btn-icon" onclick="toggleTheme()" title="Toggle theme">🌓</button>
1355
- <button class="btn" onclick="window.print()">🖨️ Print</button>
1356
- </div>
1357
- </header>
1358
-
1359
- <!-- Hero Section -->
1360
- <section class="hero-section">
1361
- <div class="score-card">
1362
- ${generateScoreRing(summary.score, summary.verdict)}
1363
- <div class="score-label">Vibe Score</div>
1364
- </div>
1365
994
 
1366
- <div class="verdict-card">
1367
- <div class="verdict-badge ${verdictClass}">
1368
- ${verdictIcons[verdictClass]} ${summary.verdict}
995
+ ${Object.keys(coverage).length > 0 ? `
996
+ <div class="coverage-grid">
997
+ ${coverage.clientCallsMapped !== undefined ? `
998
+ <div class="coverage-item">
999
+ <div class="coverage-value">${coverage.clientCallsMapped}%</div>
1000
+ <div class="coverage-label">API Coverage</div>
1369
1001
  </div>
1370
- <p class="verdict-message">${verdictMessages[verdictClass]}</p>
1371
-
1372
- <div class="stats-grid">
1373
- <div class="stat-item">
1374
- <span class="stat-value">${summary.totalFindings}</span>
1375
- <span class="stat-label">Findings</span>
1376
- </div>
1377
- <div class="stat-item">
1378
- <span class="stat-value">${truthpack.routes}</span>
1379
- <span class="stat-label">Routes</span>
1002
+ ` : ''}
1003
+ ${coverage.uiActionsVerified !== undefined ? `
1004
+ <div class="coverage-item">
1005
+ <div class="coverage-value">${coverage.uiActionsVerified}%</div>
1006
+ <div class="coverage-label">UI Actions Verified</div>
1007
+ </div>
1008
+ ` : ''}
1009
+ ${coverage.authRoutes !== undefined ? `
1010
+ <div class="coverage-item">
1011
+ <div class="coverage-value">${coverage.authRoutes}%</div>
1012
+ <div class="coverage-label">Auth Routes</div>
1013
+ </div>
1014
+ ` : ''}
1015
+ ${reality.latencyP95 !== undefined ? `
1016
+ <div class="coverage-item">
1017
+ <div class="coverage-value">${reality.latencyP95}ms</div>
1018
+ <div class="coverage-label">P95 Latency</div>
1019
+ </div>
1020
+ ` : ''}
1021
+ </div>
1022
+ ` : ''}
1023
+
1024
+ ${brokenFlows.length > 0 ? `
1025
+ <div class="broken-flows">
1026
+ <h3 style="font-size: 1rem; font-weight: 600; margin-bottom: var(--space-4); color: var(--color-critical);">
1027
+ 🚨 Broken Flows Detected (${brokenFlows.length})
1028
+ </h3>
1029
+ ${brokenFlows.slice(0, 5).map(flow => `
1030
+ <div class="flow-card">
1031
+ <div class="flow-header">
1032
+ <span class="flow-title">${flow.title}</span>
1033
+ <span class="flow-severity ${(flow.severity || 'block').toLowerCase()}">${flow.severity || 'BLOCK'}</span>
1380
1034
  </div>
1381
- <div class="stat-item">
1382
- <span class="stat-value">${fixEstimates.humanReadable}</span>
1383
- <span class="stat-label">Fix Time</span>
1035
+ <div class="flow-steps">
1036
+ ${(flow.steps || []).map((step, i) => `
1037
+ ${i > 0 ? '<span class="flow-arrow">→</span>' : ''}
1038
+ <div class="flow-step ${step.type === 'error' ? 'error' : ''}">
1039
+ <span class="flow-step-icon ${step.type}">${getStepIcon(step.type)}</span>
1040
+ <span>${step.label}</span>
1041
+ </div>
1042
+ `).join('')}
1384
1043
  </div>
1385
1044
  </div>
1045
+ `).join('')}
1386
1046
  </div>
1047
+ ` : ''}
1387
1048
  </section>
1049
+ ` : ''}
1388
1050
 
1389
- <!-- Coverage Metrics (if reality data available) -->
1390
- ${generateCoverageMetrics(reality)}
1391
-
1392
- <!-- Runtime Charts (if reality data available) -->
1393
- ${generateRuntimeCharts(reality)}
1394
-
1395
- <!-- Category Breakdown -->
1396
- <section class="section">
1397
- <div class="section-header">
1398
- <div class="section-icon">📊</div>
1399
- <div>
1400
- <h2 class="section-title">Category Breakdown</h2>
1401
- <p class="section-subtitle">Score analysis by domain</p>
1402
- </div>
1403
- </div>
1404
- <div class="card">
1405
- ${generateCategoryBars(summary.categoryScores)}
1051
+ <!-- FINDINGS -->
1052
+ <section class="findings-section">
1053
+ <div class="findings-header">
1054
+ <h2 class="findings-title">Security Findings</h2>
1055
+ <span class="findings-count">${totalFindings} total findings</span>
1406
1056
  </div>
1407
- </section>
1408
-
1409
- <!-- Broken User Flows (if reality data available) -->
1410
- ${generateBrokenFlows(reality)}
1411
-
1412
- <!-- Severity Distribution -->
1413
- <section class="section">
1414
- <div class="section-header">
1415
- <div class="section-icon">🎯</div>
1416
- <div>
1417
- <h2 class="section-title">Severity Distribution</h2>
1418
- <p class="section-subtitle">Issues breakdown by priority</p>
1419
- </div>
1057
+ <div class="findings-filters">
1058
+ <button class="filter-btn active" data-filter="all">All</button>
1059
+ <button class="filter-btn" data-filter="critical">Critical (${criticalCount})</button>
1060
+ <button class="filter-btn" data-filter="high">High (${highCount})</button>
1061
+ <button class="filter-btn" data-filter="medium">Medium (${mediumCount})</button>
1062
+ <button class="filter-btn" data-filter="low">Low (${lowCount})</button>
1420
1063
  </div>
1421
- <div class="card">
1422
- ${generateSeverityDonut(summary.severityCounts)}
1064
+ <div class="findings-list">
1065
+ ${findings.slice(0, opts.maxFindings || 50).map(f => {
1066
+ const sev = normalizeSeverity(f.severity);
1067
+ const file = redactPaths && f.file ? redactPath(f.file) : f.file;
1068
+ return `
1069
+ <div class="finding-item" data-severity="${sev}">
1070
+ <div class="finding-severity">
1071
+ <span class="severity-badge ${sev}">${sev}</span>
1072
+ </div>
1073
+ <div class="finding-content">
1074
+ <div class="finding-title">${escapeHtml(f.title || f.message || 'Unknown finding')}</div>
1075
+ ${file ? `<div class="finding-file">${escapeHtml(file)}${f.line ? `:${f.line}` : ''}</div>` : ''}
1076
+ </div>
1077
+ ${f.fix ? `<div class="finding-fix">${escapeHtml(f.fix)}</div>` : ''}
1078
+ </div>
1079
+ `;
1080
+ }).join('')}
1423
1081
  </div>
1424
1082
  </section>
1425
1083
 
1426
- <!-- Fix Estimates -->
1427
- <section class="section">
1084
+ <!-- FIX ESTIMATES -->
1085
+ <section class="estimates-section">
1428
1086
  <div class="section-header">
1429
- <div class="section-icon">⏱️</div>
1430
- <div>
1431
- <h2 class="section-title">Fix Time Estimates</h2>
1432
- <p class="section-subtitle">Estimated remediation effort</p>
1433
- </div>
1087
+ <h2 class="section-title">Remediation Estimate</h2>
1434
1088
  </div>
1435
- ${generateFixEstimates(fixEstimates)}
1436
- </section>
1437
-
1438
- <!-- Findings -->
1439
- ${findings.length > 0 ? `
1440
- <section class="section">
1441
- <div class="section-header">
1442
- <div class="section-icon">🔍</div>
1443
- <div>
1444
- <h2 class="section-title">Findings</h2>
1445
- <p class="section-subtitle">${summary.totalFindings} issues identified</p>
1089
+ <div class="estimates-grid">
1090
+ <div class="estimate-card">
1091
+ <div class="estimate-value">${fixEstimates?.humanReadable || calculateFixTime(findings)}</div>
1092
+ <div class="estimate-label">Estimated Fix Time</div>
1093
+ </div>
1094
+ <div class="estimate-card">
1095
+ <div class="estimate-value">${criticalCount + highCount}</div>
1096
+ <div class="estimate-label">Priority Items</div>
1097
+ </div>
1098
+ <div class="estimate-card">
1099
+ <div class="estimate-value">${Math.round(summary.score * 1.1)}%</div>
1100
+ <div class="estimate-label">Projected Score After Fix</div>
1446
1101
  </div>
1447
- </div>
1448
- <div class="card">
1449
- ${generateFindingsList(findings, options.maxFindings || 20)}
1450
1102
  </div>
1451
1103
  </section>
1452
- ` : ""}
1453
1104
 
1454
- <!-- Footer -->
1455
- <footer class="report-footer">
1105
+ <!-- FOOTER -->
1106
+ <footer class="footer">
1456
1107
  <div class="footer-brand">
1457
- <span>⚡</span>
1458
- <span>Vibecheck</span>
1459
- </div>
1460
- <div>
1461
- ${meta.reportId} · <a href="https://vibecheck.dev" style="color: var(--color-primary)">vibecheck.dev</a>
1108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1109
+ <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
1110
+ </svg>
1111
+ <span>Generated by <strong>VibeCheck</strong></span>
1462
1112
  </div>
1113
+ <p>
1114
+ <a href="https://vibecheck.dev">vibecheck.dev</a> ·
1115
+ Report ID: ${meta.reportId || 'VC-' + Date.now().toString(36).toUpperCase()}
1116
+ </p>
1463
1117
  </footer>
1464
1118
  </div>
1465
1119
 
1466
1120
  <script>
1121
+ // Theme Toggle
1467
1122
  function toggleTheme() {
1468
1123
  const html = document.documentElement;
1469
- const current = html.getAttribute('data-theme');
1470
- html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
1124
+ const newTheme = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
1125
+ html.setAttribute('data-theme', newTheme);
1126
+ try { localStorage.setItem('vibecheck-theme', newTheme); } catch(e) {}
1471
1127
  }
1472
1128
 
1473
- // Animate score ring on load
1474
- document.addEventListener('DOMContentLoaded', () => {
1475
- const ring = document.querySelector('.score-ring-progress');
1476
- if (ring) {
1477
- const dashArray = ring.getAttribute('stroke-dasharray');
1478
- ring.setAttribute('stroke-dasharray', '0 1000');
1479
- setTimeout(() => ring.setAttribute('stroke-dasharray', dashArray), 100);
1480
- }
1129
+ // Restore theme
1130
+ try {
1131
+ const saved = localStorage.getItem('vibecheck-theme');
1132
+ if (saved) document.documentElement.setAttribute('data-theme', saved);
1133
+ } catch(e) {}
1134
+
1135
+ // Filter functionality
1136
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1137
+ btn.addEventListener('click', () => {
1138
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
1139
+ btn.classList.add('active');
1140
+
1141
+ const filter = btn.dataset.filter;
1142
+ document.querySelectorAll('.finding-item').forEach(item => {
1143
+ if (filter === 'all' || item.dataset.severity === filter) {
1144
+ item.style.display = '';
1145
+ } else {
1146
+ item.style.display = 'none';
1147
+ }
1148
+ });
1149
+ });
1481
1150
  });
1482
1151
  </script>
1483
1152
  </body>
1484
1153
  </html>`;
1485
1154
  }
1486
1155
 
1487
- // ============================================================================
1488
- // EXPORTS
1489
- // ============================================================================
1156
+ // Helper functions
1157
+ function getVerdictSummary(verdict, critical, high, medium) {
1158
+ if (verdict === "SHIP") {
1159
+ return "Your application has passed all critical checks and is ready for production deployment. No blocking issues were identified during the security scan.";
1160
+ } else if (verdict === "WARN") {
1161
+ const issues = [];
1162
+ if (critical > 0) issues.push(`${critical} critical`);
1163
+ if (high > 0) issues.push(`${high} high priority`);
1164
+ if (medium > 0) issues.push(`${medium} medium priority`);
1165
+ return `Your application has ${issues.join(', ')} issue${critical + high + medium > 1 ? 's' : ''} that should be addressed before deployment. Review the findings below and prioritize fixes based on severity.`;
1166
+ } else {
1167
+ return `Your application has ${critical} critical issue${critical !== 1 ? 's' : ''} that must be resolved before deployment. These represent potential security vulnerabilities or broken functionality that would impact production users.`;
1168
+ }
1169
+ }
1170
+
1171
+ function formatCategoryName(cat) {
1172
+ const names = {
1173
+ security: "Security",
1174
+ auth: "Authentication",
1175
+ billing: "Billing & Payments",
1176
+ routes: "Route Integrity",
1177
+ env: "Environment Config",
1178
+ quality: "Code Quality",
1179
+ mock: "Mock Data",
1180
+ error: "Error Handling",
1181
+ };
1182
+ return names[cat] || cat.charAt(0).toUpperCase() + cat.slice(1).replace(/_/g, " ");
1183
+ }
1184
+
1185
+ function normalizeSeverity(sev) {
1186
+ const s = (sev || '').toLowerCase();
1187
+ if (s === 'block' || s === 'critical') return 'critical';
1188
+ if (s === 'high') return 'high';
1189
+ if (s === 'warn' || s === 'medium' || s === 'warning') return 'medium';
1190
+ return 'low';
1191
+ }
1192
+
1193
+ function getStepIcon(type) {
1194
+ const icons = { ui: '🖱', api: '⚡', form: '📝', error: '❌' };
1195
+ return icons[type] || '•';
1196
+ }
1197
+
1198
+ function redactPath(path) {
1199
+ if (!path) return path;
1200
+ const parts = path.split('/');
1201
+ if (parts.length <= 2) return path;
1202
+ return '.../' + parts.slice(-2).join('/');
1203
+ }
1204
+
1205
+ function escapeHtml(str) {
1206
+ if (!str) return '';
1207
+ return String(str)
1208
+ .replace(/&/g, '&amp;')
1209
+ .replace(/</g, '&lt;')
1210
+ .replace(/>/g, '&gt;')
1211
+ .replace(/"/g, '&quot;');
1212
+ }
1213
+
1214
+ function calculateFixTime(findings) {
1215
+ if (!findings || findings.length === 0) return '0h';
1216
+
1217
+ const minutes = findings.reduce((total, f) => {
1218
+ const sev = normalizeSeverity(f.severity);
1219
+ const time = { critical: 60, high: 45, medium: 20, low: 10 };
1220
+ return total + (time[sev] || 15);
1221
+ }, 0);
1222
+
1223
+ if (minutes < 60) return `${minutes}m`;
1224
+ const hours = Math.round(minutes / 60 * 10) / 10;
1225
+ return hours > 8 ? `${Math.ceil(hours / 8)}d` : `${hours}h`;
1226
+ }
1490
1227
 
1491
1228
  module.exports = {
1492
1229
  generateWorldClassHTML,
1493
- getDesignSystem,
1494
- generateScoreRing,
1495
- generateCategoryBars,
1496
- generateSeverityDonut,
1497
- generateFindingsList,
1498
- generateFixEstimates,
1499
1230
  };