@vibecheckai/cli 3.5.0 → 3.5.2

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 (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. package/mcp-server/tools.js +0 -495
@@ -1,100 +1,114 @@
1
1
  /**
2
2
  * HTML Proof Report Generator
3
3
  *
4
- * Generates beautiful, shareable HTML proof reports with embedded evidence.
5
- * Designed for CI/CD integration and stakeholder presentations.
4
+ * Creates a beautiful, self-contained HTML report that proves your app works.
5
+ * Includes embedded video, network evidence, and clear verdicts.
6
6
  *
7
- * Features:
8
- * - Embedded video/screenshot evidence
9
- * - Interactive findings browser
10
- * - Vibe score visualization
11
- * - Export to PDF capability
12
- * - Dark/light mode support
7
+ * Designed to be shareable: drop into a PR, CI artifact, or send to stakeholders.
13
8
  */
14
9
 
15
10
  "use strict";
16
11
 
17
- /**
18
- * Generate HTML proof report
19
- * @param {object} data - Report data
20
- * @returns {string} HTML content
21
- */
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, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;')
26
+ .replace(/'/g, '&#39;');
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
+
22
38
  function generateHtmlProofReport(data) {
23
39
  const {
24
- verdict = "UNKNOWN",
25
- projectName = "Project",
26
- url = null,
27
- startedAt = new Date().toISOString(),
28
- finishedAt = new Date().toISOString(),
29
- durationMs = 0,
40
+ verdict,
41
+ projectName,
42
+ url,
43
+ startedAt,
44
+ finishedAt,
45
+ durationMs,
30
46
  findings = [],
31
47
  coverage = null,
32
48
  passes = {},
33
- artifacts = null,
49
+ artifacts = {},
34
50
  meta = {},
35
- stabilityRuns = 1,
51
+ stabilityRuns = 1
36
52
  } = data;
37
53
 
38
- const blockers = findings.filter(f => f.severity === "BLOCK");
39
- const warnings = findings.filter(f => f.severity === "WARN");
40
-
41
- // Calculate duration string
42
- const durationSec = Math.round(durationMs / 1000);
43
- const durationStr = durationSec < 60 ? `${durationSec}s` : `${Math.floor(durationSec / 60)}m ${durationSec % 60}s`;
54
+ const blocks = findings.filter(f => f.severity === 'BLOCK').length;
55
+ const warns = findings.filter(f => f.severity === 'WARN').length;
44
56
 
45
- // Verdict colors
46
- const verdictColors = {
47
- SHIP: { bg: "#10b981", text: "#ffffff", border: "#059669" },
48
- WARN: { bg: "#f59e0b", text: "#000000", border: "#d97706" },
49
- BLOCK: { bg: "#ef4444", text: "#ffffff", border: "#dc2626" },
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' }
50
62
  };
51
- const vc = verdictColors[verdict] || verdictColors.BLOCK;
52
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
53
75
  return `<!DOCTYPE html>
54
76
  <html lang="en">
55
77
  <head>
56
78
  <meta charset="UTF-8">
57
79
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
- <title>vibecheck Proof Report - ${projectName}</title>
80
+ <title>Reality Proof: ${escapeHtml(projectName)}</title>
59
81
  <style>
60
82
  :root {
61
- --bg-primary: #0f172a;
62
- --bg-secondary: #1e293b;
63
- --bg-tertiary: #334155;
64
- --text-primary: #f1f5f9;
65
- --text-secondary: #94a3b8;
66
- --text-muted: #64748b;
67
- --accent: #8b5cf6;
83
+ --verdict-color: ${v.color};
84
+ --verdict-bg: ${v.bg};
85
+ --primary: #6366f1;
68
86
  --success: #10b981;
69
87
  --warning: #f59e0b;
70
- --error: #ef4444;
71
- --border: #334155;
72
- }
73
-
74
- @media (prefers-color-scheme: light) {
75
- :root {
76
- --bg-primary: #ffffff;
77
- --bg-secondary: #f8fafc;
78
- --bg-tertiary: #f1f5f9;
79
- --text-primary: #0f172a;
80
- --text-secondary: #475569;
81
- --text-muted: #94a3b8;
82
- --border: #e2e8f0;
83
- }
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;
84
99
  }
85
100
 
86
101
  * {
87
- box-sizing: border-box;
88
102
  margin: 0;
89
103
  padding: 0;
104
+ box-sizing: border-box;
90
105
  }
91
106
 
92
107
  body {
93
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
94
- background: var(--bg-primary);
95
- color: var(--text-primary);
108
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
109
+ background: var(--gray-50);
110
+ color: var(--gray-800);
96
111
  line-height: 1.6;
97
- min-height: 100vh;
98
112
  }
99
113
 
100
114
  .container {
@@ -107,252 +121,394 @@ function generateHtmlProofReport(data) {
107
121
  .header {
108
122
  text-align: center;
109
123
  margin-bottom: 3rem;
110
- padding-bottom: 2rem;
111
- border-bottom: 1px solid var(--border);
112
124
  }
113
125
 
114
126
  .logo {
115
127
  font-size: 2rem;
116
- font-weight: 800;
117
- background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 50%, #f59e0b 100%);
118
- -webkit-background-clip: text;
119
- -webkit-text-fill-color: transparent;
120
- background-clip: text;
128
+ font-weight: 700;
129
+ color: var(--primary);
121
130
  margin-bottom: 0.5rem;
122
131
  }
123
132
 
124
- .subtitle {
125
- color: var(--text-secondary);
126
- font-size: 1.1rem;
133
+ .logo span {
134
+ color: var(--gray-400);
135
+ font-weight: 400;
127
136
  }
128
137
 
129
- .project-name {
130
- font-size: 1.5rem;
131
- font-weight: 600;
132
- margin-top: 1rem;
133
- color: var(--text-primary);
138
+ .subtitle {
139
+ color: var(--gray-500);
140
+ font-size: 1rem;
134
141
  }
135
142
 
136
143
  /* Verdict Card */
137
144
  .verdict-card {
138
- background: ${vc.bg};
139
- color: ${vc.text};
140
- border: 2px solid ${vc.border};
145
+ background: white;
141
146
  border-radius: 16px;
142
- padding: 2rem;
147
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
148
+ padding: 2.5rem;
143
149
  text-align: center;
144
150
  margin-bottom: 2rem;
145
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
151
+ border: 2px solid var(--verdict-color);
146
152
  }
147
153
 
148
- .verdict-icon {
154
+ .verdict-emoji {
149
155
  font-size: 4rem;
150
156
  margin-bottom: 1rem;
151
157
  }
152
158
 
153
- .verdict-text {
154
- font-size: 2.5rem;
155
- font-weight: 800;
156
- letter-spacing: 2px;
159
+ .verdict-label {
160
+ font-size: 1.75rem;
161
+ font-weight: 700;
162
+ color: var(--verdict-color);
157
163
  margin-bottom: 0.5rem;
158
164
  }
159
165
 
160
- .verdict-tagline {
161
- font-size: 1.2rem;
162
- opacity: 0.9;
166
+ .verdict-desc {
167
+ color: var(--gray-500);
168
+ margin-bottom: 1.5rem;
163
169
  }
164
170
 
165
- /* Stats Grid */
166
- .stats-grid {
167
- display: grid;
168
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
169
- gap: 1.5rem;
170
- margin-bottom: 3rem;
171
+ .verdict-stats {
172
+ display: flex;
173
+ justify-content: center;
174
+ gap: 3rem;
175
+ flex-wrap: wrap;
171
176
  }
172
177
 
173
- .stat-card {
174
- background: var(--bg-secondary);
175
- border: 1px solid var(--border);
176
- border-radius: 12px;
177
- padding: 1.5rem;
178
+ .stat {
178
179
  text-align: center;
179
180
  }
180
181
 
181
182
  .stat-value {
182
- font-size: 2.5rem;
183
+ font-size: 2rem;
183
184
  font-weight: 700;
184
- color: var(--accent);
185
+ color: var(--gray-800);
185
186
  }
186
187
 
187
- .stat-value.success { color: var(--success); }
188
+ .stat-value.danger { color: var(--danger); }
188
189
  .stat-value.warning { color: var(--warning); }
189
- .stat-value.error { color: var(--error); }
190
+ .stat-value.success { color: var(--success); }
190
191
 
191
192
  .stat-label {
192
- color: var(--text-secondary);
193
- font-size: 0.9rem;
194
- margin-top: 0.5rem;
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;
195
226
  }
196
227
 
197
228
  /* Sections */
198
229
  .section {
199
- background: var(--bg-secondary);
200
- border: 1px solid var(--border);
230
+ background: white;
201
231
  border-radius: 12px;
202
- padding: 1.5rem;
232
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
203
233
  margin-bottom: 2rem;
234
+ overflow: hidden;
204
235
  }
205
236
 
206
237
  .section-header {
238
+ background: var(--gray-50);
239
+ padding: 1rem 1.5rem;
240
+ border-bottom: 1px solid var(--gray-200);
207
241
  display: flex;
208
242
  align-items: center;
209
243
  gap: 0.75rem;
244
+ }
245
+
246
+ .section-icon {
210
247
  font-size: 1.25rem;
248
+ }
249
+
250
+ .section-title {
211
251
  font-weight: 600;
212
- margin-bottom: 1.5rem;
213
- padding-bottom: 1rem;
214
- border-bottom: 1px solid var(--border);
252
+ color: var(--gray-800);
215
253
  }
216
254
 
217
- .section-icon {
218
- font-size: 1.5rem;
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;
219
263
  }
220
264
 
221
- /* Findings List */
222
- .findings-list {
223
- display: flex;
224
- flex-direction: column;
265
+ .section-body {
266
+ padding: 1.5rem;
267
+ }
268
+
269
+ /* Evidence Section */
270
+ .evidence-grid {
271
+ display: grid;
225
272
  gap: 1rem;
226
273
  }
227
274
 
228
- .finding-item {
229
- background: var(--bg-tertiary);
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);
230
290
  border-radius: 8px;
231
- padding: 1rem;
232
- border-left: 4px solid var(--border);
291
+ overflow: hidden;
292
+ aspect-ratio: 16/9;
233
293
  }
234
294
 
235
- .finding-item.blocker { border-left-color: var(--error); }
236
- .finding-item.warning { border-left-color: var(--warning); }
295
+ .video-container video {
296
+ width: 100%;
297
+ height: 100%;
298
+ object-fit: contain;
299
+ }
237
300
 
238
- .finding-header {
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 {
239
311
  display: flex;
240
- justify-content: space-between;
241
312
  align-items: center;
242
- margin-bottom: 0.5rem;
313
+ justify-content: center;
314
+ height: 200px;
315
+ background: var(--gray-100);
316
+ color: var(--gray-400);
317
+ font-size: 0.875rem;
243
318
  }
244
319
 
245
- .finding-title {
246
- font-weight: 600;
320
+ /* Network Evidence */
321
+ .network-table {
322
+ width: 100%;
323
+ border-collapse: collapse;
247
324
  }
248
325
 
249
- .finding-badge {
250
- padding: 0.25rem 0.75rem;
251
- border-radius: 4px;
252
- font-size: 0.75rem;
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);
253
335
  font-weight: 600;
336
+ font-size: 0.75rem;
254
337
  text-transform: uppercase;
338
+ letter-spacing: 0.05em;
339
+ color: var(--gray-500);
255
340
  }
256
341
 
257
- .finding-badge.blocker {
258
- background: var(--error);
259
- color: white;
342
+ .network-table td {
343
+ font-size: 0.875rem;
260
344
  }
261
345
 
262
- .finding-badge.warning {
263
- background: var(--warning);
264
- color: black;
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;
265
354
  }
266
355
 
267
- .finding-details {
268
- color: var(--text-secondary);
269
- font-size: 0.9rem;
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;
270
366
  }
271
367
 
272
- .finding-location {
273
- color: var(--text-muted);
274
- font-size: 0.85rem;
275
- font-family: monospace;
276
- margin-top: 0.5rem;
368
+ .finding-card.block {
369
+ border-left: 4px solid var(--danger);
277
370
  }
278
371
 
279
- /* Coverage Bar */
280
- .coverage-bar {
281
- height: 24px;
282
- background: var(--bg-tertiary);
283
- border-radius: 12px;
284
- overflow: hidden;
285
- margin: 1rem 0;
372
+ .finding-card.warn {
373
+ border-left: 4px solid var(--warning);
286
374
  }
287
375
 
288
- .coverage-fill {
289
- height: 100%;
290
- background: linear-gradient(90deg, var(--success), var(--accent));
291
- border-radius: 12px;
292
- transition: width 0.5s ease;
376
+ .finding-header {
377
+ padding: 1rem;
378
+ background: var(--gray-50);
379
+ display: flex;
380
+ align-items: flex-start;
381
+ gap: 0.75rem;
293
382
  }
294
383
 
295
- /* Artifacts */
296
- .artifacts-grid {
297
- display: grid;
298
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
299
- gap: 1rem;
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;
300
391
  }
301
392
 
302
- .artifact-card {
303
- background: var(--bg-tertiary);
304
- border-radius: 8px;
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 {
305
403
  padding: 1rem;
404
+ }
405
+
406
+ .finding-meta {
306
407
  display: flex;
307
- align-items: center;
408
+ flex-wrap: wrap;
308
409
  gap: 1rem;
410
+ font-size: 0.875rem;
411
+ color: var(--gray-500);
412
+ margin-bottom: 0.75rem;
309
413
  }
310
414
 
311
- .artifact-icon {
312
- font-size: 2rem;
415
+ .finding-reason {
416
+ color: var(--gray-600);
313
417
  }
314
418
 
315
- .artifact-info {
316
- flex: 1;
419
+ .finding-screenshot {
420
+ margin-top: 1rem;
421
+ border-radius: 8px;
422
+ overflow: hidden;
423
+ border: 1px solid var(--gray-200);
317
424
  }
318
425
 
319
- .artifact-name {
320
- font-weight: 600;
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);
321
453
  }
322
454
 
323
- .artifact-path {
324
- color: var(--text-muted);
325
- font-size: 0.85rem;
326
- font-family: monospace;
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);
327
471
  }
328
472
 
329
473
  /* Footer */
330
474
  .footer {
331
475
  text-align: center;
332
- padding-top: 2rem;
333
- border-top: 1px solid var(--border);
334
- color: var(--text-muted);
476
+ padding: 2rem;
477
+ color: var(--gray-400);
478
+ font-size: 0.875rem;
335
479
  }
336
480
 
337
481
  .footer a {
338
- color: var(--accent);
482
+ color: var(--primary);
339
483
  text-decoration: none;
340
484
  }
341
485
 
342
- .footer a:hover {
343
- text-decoration: underline;
344
- }
345
-
346
486
  /* Print styles */
347
487
  @media print {
348
- body {
349
- background: white;
350
- color: black;
351
- }
352
-
353
- .section {
354
- break-inside: avoid;
355
- }
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;
356
512
  }
357
513
  </style>
358
514
  </head>
@@ -360,204 +516,398 @@ function generateHtmlProofReport(data) {
360
516
  <div class="container">
361
517
  <!-- Header -->
362
518
  <header class="header">
363
- <div class="logo">vibecheck</div>
364
- <div class="subtitle">Proof Report</div>
365
- <div class="project-name">${escapeHtml(projectName)}</div>
366
- ${url ? `<div style="color: var(--text-muted); margin-top: 0.5rem;">${escapeHtml(url)}</div>` : ''}
519
+ <div class="logo">vibecheck <span>prove</span></div>
520
+ <p class="subtitle">Reality Proof Report</p>
367
521
  </header>
368
522
 
369
523
  <!-- Verdict Card -->
370
524
  <div class="verdict-card">
371
- <div class="verdict-icon">${verdict === 'SHIP' ? '🚀' : verdict === 'WARN' ? '⚠️' : '🛑'}</div>
372
- <div class="verdict-text">${verdict === 'SHIP' ? 'PROVED REAL' : verdict === 'WARN' ? 'PARTIALLY PROVED' : 'NOT PROVED'}</div>
373
- <div class="verdict-tagline">${verdict === 'SHIP' ? 'Your app is verified and ready to ship!' : verdict === 'WARN' ? 'Some warnings remain - review before shipping' : 'Blockers remain - cannot verify readiness'}</div>
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>
374
552
  </div>
375
553
 
376
- <!-- Stats Grid -->
377
- <div class="stats-grid">
378
- <div class="stat-card">
379
- <div class="stat-value ${blockers.length > 0 ? 'error' : 'success'}">${blockers.length}</div>
380
- <div class="stat-label">Blockers</div>
381
- </div>
382
- <div class="stat-card">
383
- <div class="stat-value ${warnings.length > 0 ? 'warning' : 'success'}">${warnings.length}</div>
384
- <div class="stat-label">Warnings</div>
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>
385
559
  </div>
386
- <div class="stat-card">
387
- <div class="stat-value">${coverage?.percent || 0}%</div>
388
- <div class="stat-label">Coverage</div>
560
+ <div class="meta-item">
561
+ <div class="meta-label">URL Tested</div>
562
+ <div class="meta-value">${escapeHtml(url || 'N/A')}</div>
389
563
  </div>
390
- <div class="stat-card">
391
- <div class="stat-value">${durationStr}</div>
392
- <div class="stat-label">Duration</div>
564
+ <div class="meta-item">
565
+ <div class="meta-label">Tested At</div>
566
+ <div class="meta-value">${new Date(startedAt).toLocaleString()}</div>
393
567
  </div>
394
- <div class="stat-card">
395
- <div class="stat-value">${stabilityRuns}</div>
396
- <div class="stat-label">Stability Runs</div>
568
+ <div class="meta-item">
569
+ <div class="meta-label">Stability Runs</div>
570
+ <div class="meta-value">${stabilityRuns}</div>
397
571
  </div>
398
572
  </div>
399
573
 
400
- ${coverage ? `
401
- <!-- Coverage Section -->
402
- <div class="section">
403
- <div class="section-header">
404
- <span class="section-icon">📊</span>
405
- <span>UI Path Coverage</span>
406
- </div>
407
- <div class="coverage-bar">
408
- <div class="coverage-fill" style="width: ${coverage.percent}%"></div>
409
- </div>
410
- <p style="text-align: center; color: var(--text-secondary);">
411
- ${coverage.hit || 0} of ${coverage.total || 0} paths visited (${coverage.percent}%)
412
- </p>
413
- ${coverage.missed?.length > 0 ? `
414
- <div style="margin-top: 1rem;">
415
- <p style="font-weight: 600; margin-bottom: 0.5rem;">Missed paths:</p>
416
- <ul style="color: var(--text-secondary); padding-left: 1.5rem;">
417
- ${coverage.missed.slice(0, 5).map(p => `<li style="font-family: monospace;">${escapeHtml(p)}</li>`).join('')}
418
- ${coverage.missed.length > 5 ? `<li style="color: var(--text-muted);">... and ${coverage.missed.length - 5} more</li>` : ''}
419
- </ul>
420
- </div>
421
- ` : ''}
422
- </div>
423
- ` : ''}
574
+ ${generateVideoSection(artifacts, passes)}
575
+
576
+ ${generateNetworkSection(passes)}
577
+
578
+ ${generateCoverageSection(coverage)}
424
579
 
425
- ${findings.length > 0 ? `
426
- <!-- Findings Section -->
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> &middot;
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 -->
427
616
  <div class="section">
428
617
  <div class="section-header">
429
- <span class="section-icon">🎯</span>
430
- <span>Findings (${findings.length})</span>
618
+ <span class="section-icon">🎬</span>
619
+ <span class="section-title">Video Evidence</span>
620
+ <span class="section-badge">Runtime Proof</span>
431
621
  </div>
432
- <div class="findings-list">
433
- ${findings.slice(0, 20).map(f => `
434
- <div class="finding-item ${f.severity === 'BLOCK' ? 'blocker' : 'warning'}">
435
- <div class="finding-header">
436
- <span class="finding-title">${escapeHtml(f.title || f.message || f.type || 'Finding')}</span>
437
- <span class="finding-badge ${f.severity === 'BLOCK' ? 'blocker' : 'warning'}">${f.severity}</span>
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>
438
648
  </div>
439
- <div class="finding-details">${escapeHtml(f.reason || f.message || '')}</div>
440
- ${f.page || f.file ? `<div class="finding-location">${escapeHtml(f.page || f.file)}${f.line ? `:${f.line}` : ''}</div>` : ''}
649
+ ` : ''}
441
650
  </div>
442
- `).join('')}
443
- ${findings.length > 20 ? `<p style="text-align: center; color: var(--text-muted);">... and ${findings.length - 20} more findings</p>` : ''}
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
+ ` : ''}
444
674
  </div>
445
675
  </div>
446
- ` : `
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 -->
447
689
  <div class="section">
448
690
  <div class="section-header">
449
- <span class="section-icon">✨</span>
450
- <span>No Issues Found</span>
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
+ ` : ''}
451
741
  </div>
452
- <p style="text-align: center; color: var(--success); font-size: 1.2rem;">
453
- All UI elements are responsive and functional!
454
- </p>
455
742
  </div>
456
- `}
457
-
458
- ${artifacts ? `
459
- <!-- Artifacts Section -->
743
+ `;
744
+ }
745
+
746
+ function generateCoverageSection(coverage) {
747
+ if (!coverage) return '';
748
+
749
+ return `
750
+ <!-- Coverage -->
460
751
  <div class="section">
461
752
  <div class="section-header">
462
- <span class="section-icon">📁</span>
463
- <span>Evidence Artifacts</span>
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>
464
756
  </div>
465
- <div class="artifacts-grid">
466
- ${artifacts.screenshots ? `
467
- <div class="artifact-card">
468
- <div class="artifact-icon">📸</div>
469
- <div class="artifact-info">
470
- <div class="artifact-name">Screenshots</div>
471
- <div class="artifact-path">${artifacts.screenshots}</div>
472
- </div>
757
+ <div class="section-body">
758
+ <div class="coverage-bar">
759
+ <div class="coverage-fill" style="width: ${coverage.percent || 0}%"></div>
473
760
  </div>
474
- ` : ''}
475
- ${artifacts.videos?.directory ? `
476
- <div class="artifact-card">
477
- <div class="artifact-icon">🎬</div>
478
- <div class="artifact-info">
479
- <div class="artifact-name">Video Recordings</div>
480
- <div class="artifact-path">${artifacts.videos.directory}</div>
481
- </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>
482
764
  </div>
483
- ` : ''}
484
- ${artifacts.traces?.directory ? `
485
- <div class="artifact-card">
486
- <div class="artifact-icon">📊</div>
487
- <div class="artifact-info">
488
- <div class="artifact-name">Playwright Traces</div>
489
- <div class="artifact-path">${artifacts.traces.directory}</div>
490
- </div>
491
- </div>
492
- ` : ''}
493
- ${artifacts.har?.directory ? `
494
- <div class="artifact-card">
495
- <div class="artifact-icon">📡</div>
496
- <div class="artifact-info">
497
- <div class="artifact-name">HAR Network Logs</div>
498
- <div class="artifact-path">${artifacts.har.directory}</div>
499
- </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>
500
775
  </div>
501
776
  ` : ''}
502
777
  </div>
503
778
  </div>
504
- ` : ''}
505
-
506
- <!-- Meta Section -->
779
+ `;
780
+ }
781
+
782
+ function generateFindingsSection(findings, findingsByCategory) {
783
+ if (findings.length === 0) {
784
+ return `
785
+ <!-- Findings -->
507
786
  <div class="section">
508
787
  <div class="section-header">
509
- <span class="section-icon">ℹ️</span>
510
- <span>Report Details</span>
788
+ <span class="section-icon">✅</span>
789
+ <span class="section-title">Findings</span>
790
+ <span class="section-badge">0 issues</span>
511
791
  </div>
512
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
513
- <div>
514
- <div style="color: var(--text-muted); font-size: 0.85rem;">Started</div>
515
- <div>${new Date(startedAt).toLocaleString()}</div>
516
- </div>
517
- <div>
518
- <div style="color: var(--text-muted); font-size: 0.85rem;">Completed</div>
519
- <div>${new Date(finishedAt).toLocaleString()}</div>
520
- </div>
521
- <div>
522
- <div style="color: var(--text-muted); font-size: 0.85rem;">Duration</div>
523
- <div>${durationStr}</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.
524
795
  </div>
525
- ${meta.recordingEnabled ? `
526
- <div>
527
- <div style="color: var(--text-muted); font-size: 0.85rem;">Recording</div>
528
- <div>
529
- ${meta.recordingEnabled.video ? '🎬 Video ' : ''}
530
- ${meta.recordingEnabled.trace ? '📊 Trace ' : ''}
531
- ${meta.recordingEnabled.har ? '📡 HAR' : ''}
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
+ ` : ''}
532
845
  </div>
533
846
  </div>
534
- ` : ''}
847
+ `).join('')}
535
848
  </div>
536
849
  </div>
537
-
538
- <!-- Footer -->
539
- <footer class="footer">
540
- <p>Generated by <a href="https://vibecheckai.dev">vibecheck</a></p>
541
- <p style="margin-top: 0.5rem; font-size: 0.85rem;">
542
- The vibe coder's reality check • Prove your code actually works
543
- </p>
544
- </footer>
545
- </div>
546
- </body>
547
- </html>`;
850
+ `;
548
851
  }
549
852
 
550
- /**
551
- * Escape HTML special characters
552
- */
553
- function escapeHtml(str) {
554
- if (!str) return '';
555
- return String(str)
556
- .replace(/&/g, '&amp;')
557
- .replace(/</g, '&lt;')
558
- .replace(/>/g, '&gt;')
559
- .replace(/"/g, '&quot;')
560
- .replace(/'/g, '&#039;');
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
+ }
561
905
  }
562
906
 
563
- module.exports = { generateHtmlProofReport, escapeHtml };
907
+ // ═══════════════════════════════════════════════════════════════════════════════
908
+ // EXPORT
909
+ // ═══════════════════════════════════════════════════════════════════════════════
910
+
911
+ module.exports = {
912
+ generateHtmlProofReport
913
+ };