@odavl/guardian 0.1.0-rc1

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/guardian.js +690 -0
  5. package/flows/example-login-flow.json +36 -0
  6. package/flows/example-signup-flow.json +44 -0
  7. package/guardian-contract-v1.md +149 -0
  8. package/guardian.config.json +54 -0
  9. package/guardian.policy.json +12 -0
  10. package/guardian.profile.docs.yaml +18 -0
  11. package/guardian.profile.ecommerce.yaml +17 -0
  12. package/guardian.profile.marketing.yaml +18 -0
  13. package/guardian.profile.saas.yaml +21 -0
  14. package/package.json +69 -0
  15. package/policies/enterprise.json +12 -0
  16. package/policies/saas.json +12 -0
  17. package/policies/startup.json +12 -0
  18. package/src/guardian/attempt-engine.js +454 -0
  19. package/src/guardian/attempt-registry.js +227 -0
  20. package/src/guardian/attempt-reporter.js +507 -0
  21. package/src/guardian/attempt.js +227 -0
  22. package/src/guardian/auto-attempt-builder.js +283 -0
  23. package/src/guardian/baseline-reporter.js +143 -0
  24. package/src/guardian/baseline-storage.js +285 -0
  25. package/src/guardian/baseline.js +492 -0
  26. package/src/guardian/behavioral-signals.js +261 -0
  27. package/src/guardian/breakage-intelligence.js +223 -0
  28. package/src/guardian/browser.js +92 -0
  29. package/src/guardian/cli-summary.js +141 -0
  30. package/src/guardian/crawler.js +142 -0
  31. package/src/guardian/discovery-engine.js +661 -0
  32. package/src/guardian/enhanced-html-reporter.js +305 -0
  33. package/src/guardian/failure-taxonomy.js +169 -0
  34. package/src/guardian/flow-executor.js +374 -0
  35. package/src/guardian/flow-registry.js +67 -0
  36. package/src/guardian/html-reporter.js +414 -0
  37. package/src/guardian/index.js +218 -0
  38. package/src/guardian/init-command.js +139 -0
  39. package/src/guardian/junit-reporter.js +264 -0
  40. package/src/guardian/market-criticality.js +335 -0
  41. package/src/guardian/market-reporter.js +305 -0
  42. package/src/guardian/network-trace.js +178 -0
  43. package/src/guardian/policy.js +357 -0
  44. package/src/guardian/preset-loader.js +148 -0
  45. package/src/guardian/reality.js +547 -0
  46. package/src/guardian/reporter.js +181 -0
  47. package/src/guardian/root-cause-analysis.js +171 -0
  48. package/src/guardian/safety.js +248 -0
  49. package/src/guardian/scan-presets.js +60 -0
  50. package/src/guardian/screenshot.js +152 -0
  51. package/src/guardian/sitemap.js +225 -0
  52. package/src/guardian/snapshot-schema.js +266 -0
  53. package/src/guardian/snapshot.js +327 -0
  54. package/src/guardian/validators.js +323 -0
  55. package/src/guardian/visual-diff.js +247 -0
  56. package/src/guardian/webhook.js +206 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Guardian HTML Report Generator
3
+ * Creates beautiful, self-contained HTML reports
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class GuardianHTMLReporter {
10
+ /**
11
+ * Generate HTML report from JSON report
12
+ * @param {object} jsonReport - JSON report object
13
+ * @param {string} artifactsDir - Directory containing artifacts
14
+ * @returns {string} HTML content
15
+ */
16
+ generate(jsonReport, artifactsDir) {
17
+ const html = `<!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="UTF-8">
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
+ <title>Guardian Report - ${jsonReport.baseUrl}</title>
23
+ <style>
24
+ * { margin: 0; padding: 0; box-sizing: border-box; }
25
+ body {
26
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
27
+ background: #f5f5f5;
28
+ color: #333;
29
+ line-height: 1.6;
30
+ }
31
+ .container {
32
+ max-width: 1200px;
33
+ margin: 0 auto;
34
+ padding: 20px;
35
+ }
36
+ .header {
37
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
38
+ color: white;
39
+ padding: 40px 20px;
40
+ border-radius: 10px;
41
+ margin-bottom: 30px;
42
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
43
+ }
44
+ .header h1 {
45
+ font-size: 2.5em;
46
+ margin-bottom: 10px;
47
+ }
48
+ .header .subtitle {
49
+ font-size: 1.2em;
50
+ opacity: 0.9;
51
+ }
52
+ .verdict {
53
+ background: white;
54
+ padding: 30px;
55
+ border-radius: 10px;
56
+ margin-bottom: 30px;
57
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
58
+ }
59
+ .verdict.ready {
60
+ border-left: 5px solid #10b981;
61
+ }
62
+ .verdict.do-not-launch {
63
+ border-left: 5px solid #ef4444;
64
+ }
65
+ .verdict.insufficient {
66
+ border-left: 5px solid #f59e0b;
67
+ }
68
+ .verdict-badge {
69
+ display: inline-block;
70
+ padding: 10px 20px;
71
+ border-radius: 20px;
72
+ font-weight: bold;
73
+ font-size: 1.2em;
74
+ margin-bottom: 15px;
75
+ }
76
+ .verdict-badge.ready {
77
+ background: #d1fae5;
78
+ color: #065f46;
79
+ }
80
+ .verdict-badge.do-not-launch {
81
+ background: #fee2e2;
82
+ color: #991b1b;
83
+ }
84
+ .verdict-badge.insufficient {
85
+ background: #fef3c7;
86
+ color: #92400e;
87
+ }
88
+ .metrics {
89
+ display: grid;
90
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
91
+ gap: 20px;
92
+ margin-bottom: 30px;
93
+ }
94
+ .metric-card {
95
+ background: white;
96
+ padding: 20px;
97
+ border-radius: 10px;
98
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
99
+ }
100
+ .metric-card .label {
101
+ font-size: 0.9em;
102
+ color: #666;
103
+ margin-bottom: 5px;
104
+ }
105
+ .metric-card .value {
106
+ font-size: 2em;
107
+ font-weight: bold;
108
+ color: #667eea;
109
+ }
110
+ .section {
111
+ background: white;
112
+ padding: 30px;
113
+ border-radius: 10px;
114
+ margin-bottom: 30px;
115
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
116
+ }
117
+ .section h2 {
118
+ margin-bottom: 20px;
119
+ color: #667eea;
120
+ border-bottom: 2px solid #f0f0f0;
121
+ padding-bottom: 10px;
122
+ }
123
+ .reason-list {
124
+ list-style: none;
125
+ }
126
+ .reason-list li {
127
+ padding: 10px;
128
+ margin-bottom: 10px;
129
+ background: #f9fafb;
130
+ border-left: 3px solid #667eea;
131
+ border-radius: 5px;
132
+ }
133
+ .screenshots {
134
+ display: grid;
135
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
136
+ gap: 20px;
137
+ }
138
+ .screenshot-card {
139
+ border: 1px solid #e5e7eb;
140
+ border-radius: 10px;
141
+ overflow: hidden;
142
+ transition: transform 0.2s;
143
+ }
144
+ .screenshot-card:hover {
145
+ transform: translateY(-5px);
146
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
147
+ }
148
+ .screenshot-card img {
149
+ width: 100%;
150
+ height: 200px;
151
+ object-fit: cover;
152
+ }
153
+ .screenshot-card .caption {
154
+ padding: 10px;
155
+ background: #f9fafb;
156
+ font-size: 0.9em;
157
+ color: #666;
158
+ }
159
+ .page-table {
160
+ width: 100%;
161
+ border-collapse: collapse;
162
+ }
163
+ .page-table th {
164
+ background: #f9fafb;
165
+ padding: 12px;
166
+ text-align: left;
167
+ font-weight: 600;
168
+ border-bottom: 2px solid #e5e7eb;
169
+ }
170
+ .page-table td {
171
+ padding: 12px;
172
+ border-bottom: 1px solid #e5e7eb;
173
+ }
174
+ .page-table tr:hover {
175
+ background: #f9fafb;
176
+ }
177
+ .status-badge {
178
+ display: inline-block;
179
+ padding: 4px 12px;
180
+ border-radius: 12px;
181
+ font-size: 0.85em;
182
+ font-weight: 600;
183
+ }
184
+ .status-success {
185
+ background: #d1fae5;
186
+ color: #065f46;
187
+ }
188
+ .status-error {
189
+ background: #fee2e2;
190
+ color: #991b1b;
191
+ }
192
+ .footer {
193
+ text-align: center;
194
+ padding: 20px;
195
+ color: #666;
196
+ font-size: 0.9em;
197
+ }
198
+ .confidence-high { color: #10b981; font-weight: bold; }
199
+ .confidence-medium { color: #f59e0b; font-weight: bold; }
200
+ .confidence-low { color: #ef4444; font-weight: bold; }
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <div class="container">
205
+ <div class="header">
206
+ <h1>šŸ›”ļø ODAVL Guardian</h1>
207
+ <div class="subtitle">Market Reality Testing Report</div>
208
+ </div>
209
+
210
+ ${this.generateVerdictSection(jsonReport)}
211
+ ${this.generateMetricsSection(jsonReport)}
212
+ ${this.generateReasonsSection(jsonReport)}
213
+ ${this.generatePagesSection(jsonReport)}
214
+ ${this.generateScreenshotsSection(jsonReport, artifactsDir)}
215
+
216
+ <div class="footer">
217
+ Generated by ODAVL Guardian • ${new Date(jsonReport.timestamp).toLocaleString()}
218
+ </div>
219
+ </div>
220
+ </body>
221
+ </html>`;
222
+
223
+ return html;
224
+ }
225
+
226
+ /**
227
+ * Generate verdict section
228
+ */
229
+ generateVerdictSection(report) {
230
+ const decision = report.finalJudgment.decision;
231
+ const decisionClass = decision.toLowerCase().replace(/_/g, '-');
232
+ const decisionIcon = decision === 'READY' ? '🟢' : decision === 'DO_NOT_LAUNCH' ? 'šŸ”“' : '🟔';
233
+ const decisionText = decision === 'READY' ? 'Safe to Launch' : decision === 'DO_NOT_LAUNCH' ? 'DO NOT LAUNCH' : 'Insufficient Confidence';
234
+
235
+ return `
236
+ <div class="verdict ${decisionClass}">
237
+ <div class="verdict-badge ${decisionClass}">
238
+ ${decisionIcon} ${decisionText}
239
+ </div>
240
+ <p style="margin-top: 15px; font-size: 1.1em;">
241
+ <strong>Target:</strong> ${report.baseUrl}
242
+ </p>
243
+ <p style="margin-top: 10px;">
244
+ <strong>Confidence:</strong>
245
+ <span class="confidence-${report.confidence.level.toLowerCase()}">${report.confidence.level}</span>
246
+ </p>
247
+ <p style="margin-top: 5px; color: #666;">
248
+ ${report.confidence.reasoning}
249
+ </p>
250
+ </div>
251
+ `;
252
+ }
253
+
254
+ /**
255
+ * Generate metrics section
256
+ */
257
+ generateMetricsSection(report) {
258
+ return `
259
+ <div class="metrics">
260
+ <div class="metric-card">
261
+ <div class="label">Coverage</div>
262
+ <div class="value">${report.summary.coverage}%</div>
263
+ </div>
264
+ <div class="metric-card">
265
+ <div class="label">Pages Visited</div>
266
+ <div class="value">${report.summary.visitedPages}</div>
267
+ </div>
268
+ <div class="metric-card">
269
+ <div class="label">Pages Discovered</div>
270
+ <div class="value">${report.summary.discoveredPages}</div>
271
+ </div>
272
+ <div class="metric-card">
273
+ <div class="label">Failed Pages</div>
274
+ <div class="value" style="color: ${report.summary.failedPages > 0 ? '#ef4444' : '#10b981'}">
275
+ ${report.summary.failedPages}
276
+ </div>
277
+ </div>
278
+ </div>
279
+ `;
280
+ }
281
+
282
+ /**
283
+ * Generate reasons section
284
+ */
285
+ generateReasonsSection(report) {
286
+ const reasons = report.finalJudgment.reasons || [];
287
+ if (reasons.length === 0) return '';
288
+
289
+ const reasonItems = reasons.map(r => `<li>${r}</li>`).join('');
290
+
291
+ return `
292
+ <div class="section">
293
+ <h2>šŸ“‹ Decision Reasons</h2>
294
+ <ul class="reason-list">
295
+ ${reasonItems}
296
+ </ul>
297
+ </div>
298
+ `;
299
+ }
300
+
301
+ /**
302
+ * Generate pages section
303
+ */
304
+ generatePagesSection(report) {
305
+ const pages = report.pages || [];
306
+ if (pages.length === 0) return '';
307
+
308
+ const rows = pages.map(page => {
309
+ const statusClass = page.status >= 200 && page.status < 400 ? 'status-success' : 'status-error';
310
+ const statusText = page.status || 'N/A';
311
+
312
+ return `
313
+ <tr>
314
+ <td>${page.index}</td>
315
+ <td style="word-break: break-all;">${page.url}</td>
316
+ <td><span class="status-badge ${statusClass}">${statusText}</span></td>
317
+ <td>${page.links || 0}</td>
318
+ </tr>
319
+ `;
320
+ }).join('');
321
+
322
+ return `
323
+ <div class="section">
324
+ <h2>šŸ“„ Pages Visited</h2>
325
+ <table class="page-table">
326
+ <thead>
327
+ <tr>
328
+ <th>#</th>
329
+ <th>URL</th>
330
+ <th>Status</th>
331
+ <th>Links</th>
332
+ </tr>
333
+ </thead>
334
+ <tbody>
335
+ ${rows}
336
+ </tbody>
337
+ </table>
338
+ </div>
339
+ `;
340
+ }
341
+
342
+ /**
343
+ * Generate screenshots section
344
+ */
345
+ generateScreenshotsSection(report, artifactsDir) {
346
+ const pagesDir = path.join(artifactsDir, 'pages');
347
+
348
+ // Check if pages directory exists
349
+ if (!fs.existsSync(pagesDir)) {
350
+ return '';
351
+ }
352
+
353
+ // Get all screenshot files
354
+ const files = fs.readdirSync(pagesDir).filter(f => f.endsWith('.jpeg') || f.endsWith('.jpg') || f.endsWith('.png'));
355
+
356
+ if (files.length === 0) {
357
+ return '';
358
+ }
359
+
360
+ const cards = files.map(file => {
361
+ const relativePath = `pages/${file}`;
362
+ return `
363
+ <div class="screenshot-card">
364
+ <img src="${relativePath}" alt="${file}" loading="lazy">
365
+ <div class="caption">${file}</div>
366
+ </div>
367
+ `;
368
+ }).join('');
369
+
370
+ return `
371
+ <div class="section">
372
+ <h2>šŸ“ø Screenshots</h2>
373
+ <div class="screenshots">
374
+ ${cards}
375
+ </div>
376
+ </div>
377
+ `;
378
+ }
379
+
380
+ /**
381
+ * Save HTML report to file
382
+ * @param {string} html - HTML content
383
+ * @param {string} outputPath - Where to save the HTML file
384
+ * @returns {boolean} Success status
385
+ */
386
+ save(html, outputPath) {
387
+ try {
388
+ fs.writeFileSync(outputPath, html, 'utf8');
389
+ return true;
390
+ } catch (error) {
391
+ console.error(`āŒ Failed to save HTML report: ${error.message}`);
392
+ return false;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Generate and save HTML report
398
+ * @param {object} jsonReport - JSON report object
399
+ * @param {string} artifactsDir - Directory containing artifacts
400
+ * @returns {boolean} Success status
401
+ */
402
+ generateAndSave(jsonReport, artifactsDir) {
403
+ try {
404
+ const html = this.generate(jsonReport, artifactsDir);
405
+ const outputPath = path.join(artifactsDir, 'report.html');
406
+ return this.save(html, outputPath);
407
+ } catch (error) {
408
+ console.error(`āŒ Failed to generate HTML report: ${error.message}`);
409
+ return false;
410
+ }
411
+ }
412
+ }
413
+
414
+ module.exports = GuardianHTMLReporter;
@@ -0,0 +1,218 @@
1
+ const { GuardianBrowser } = require('./browser');
2
+ const { GuardianCrawler } = require('./crawler');
3
+ const { GuardianReporter } = require('./reporter');
4
+ const GuardianScreenshot = require('./screenshot');
5
+ const GuardianNetworkTrace = require('./network-trace');
6
+ const GuardianSitemap = require('./sitemap');
7
+ const GuardianSafety = require('./safety');
8
+ const GuardianFlowExecutor = require('./flow-executor');
9
+ const GuardianHTMLReporter = require('./html-reporter');
10
+
11
+ async function runGuardian(config) {
12
+ const {
13
+ baseUrl,
14
+ maxPages = 25,
15
+ maxDepth = 3,
16
+ timeout = 20000,
17
+ artifactsDir = './artifacts',
18
+ // Phase 2 features
19
+ enableScreenshots = true,
20
+ enableTrace = true,
21
+ enableHAR = true,
22
+ enableSitemap = true,
23
+ enableSafety = true,
24
+ enableHTMLReport = true,
25
+ flowPath = null, // Path to flow JSON file
26
+ } = config;
27
+
28
+ // Validate baseUrl
29
+ try {
30
+ new URL(baseUrl);
31
+ } catch (e) {
32
+ console.error(`āŒ Invalid URL: ${baseUrl}`);
33
+ process.exit(2);
34
+ }
35
+
36
+ console.log(`\nšŸ›”ļø ODAVL Guardian — Market Reality Testing Engine`);
37
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
38
+ console.log(`šŸ“ Target: ${baseUrl}`);
39
+ console.log(`āš™ļø Config: max-pages=${maxPages}, max-depth=${maxDepth}, timeout=${timeout}ms`);
40
+
41
+ // Initialize modules
42
+ const screenshot = enableScreenshots ? new GuardianScreenshot() : null;
43
+ const networkTrace = (enableHAR || enableTrace) ? new GuardianNetworkTrace({ enableHAR, enableTrace }) : null;
44
+ const sitemap = enableSitemap ? new GuardianSitemap() : null;
45
+ const safety = enableSafety ? new GuardianSafety() : null;
46
+ const flowExecutor = flowPath ? new GuardianFlowExecutor({ safety, screenshotOnStep: enableScreenshots }) : null;
47
+ const htmlReporter = enableHTMLReport ? new GuardianHTMLReporter() : null;
48
+
49
+ const browser = new GuardianBrowser();
50
+
51
+ try {
52
+ // Discover URLs from sitemap (if enabled)
53
+ let sitemapUrls = [];
54
+ if (sitemap) {
55
+ const sitemapResult = await sitemap.discover(baseUrl);
56
+ if (sitemapResult.urls.length > 0) {
57
+ sitemapUrls = sitemap.filterSameOrigin(sitemapResult.urls, baseUrl);
58
+ console.log(`šŸ—ŗļø Sitemap: Discovered ${sitemapUrls.length} URLs`);
59
+ }
60
+ }
61
+
62
+ // Launch browser
63
+ console.log(`\nšŸš€ Launching browser...`);
64
+ const launchOptions = {};
65
+
66
+ // Enable HAR if requested
67
+ if (networkTrace && enableHAR) {
68
+ // HAR must be configured before context creation
69
+ launchOptions.recordHar = true;
70
+ }
71
+
72
+ await browser.launch(timeout, launchOptions);
73
+ console.log(`āœ… Browser launched`);
74
+
75
+ // Start trace recording if enabled
76
+ let tracePath = null;
77
+ if (networkTrace && enableTrace && browser.context) {
78
+ const reporter = new GuardianReporter();
79
+ const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
80
+ tracePath = await networkTrace.startTrace(browser.context, runDir);
81
+ if (tracePath) {
82
+ console.log(`šŸ“¹ Trace recording started`);
83
+ }
84
+ }
85
+
86
+ // Flow execution OR crawling
87
+ let crawlResult = null;
88
+ let flowResult = null;
89
+
90
+ if (flowExecutor && flowPath) {
91
+ // Execute flow instead of crawling
92
+ console.log(`\nšŸŽ¬ Flow execution mode`);
93
+ const flow = flowExecutor.loadFlow(flowPath);
94
+
95
+ if (!flow) {
96
+ throw new Error(`Failed to load flow from: ${flowPath}`);
97
+ }
98
+
99
+ const validation = flowExecutor.validateFlow(flow);
100
+ if (!validation.valid) {
101
+ throw new Error(`Invalid flow: ${validation.errors.join(', ')}`);
102
+ }
103
+
104
+ const reporter = new GuardianReporter();
105
+ const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
106
+
107
+ flowResult = await flowExecutor.executeFlow(browser.page, flow, runDir);
108
+
109
+ if (!flowResult.success) {
110
+ console.log(`āŒ Flow failed at step ${flowResult.failedStep}: ${flowResult.error}`);
111
+ }
112
+ } else {
113
+ // Normal crawling mode
114
+ console.log(`\nšŸ” Starting crawl...`);
115
+ const crawler = new GuardianCrawler(baseUrl, maxPages, maxDepth);
116
+
117
+ // Add sitemap URLs to crawler if available
118
+ if (sitemapUrls.length > 0) {
119
+ crawler.discovered = new Set([...crawler.discovered, ...sitemapUrls]);
120
+ }
121
+
122
+ // Add safety guard to crawler
123
+ if (safety) {
124
+ crawler.safety = safety;
125
+ }
126
+
127
+ // Add screenshot capability
128
+ if (screenshot) {
129
+ crawler.screenshot = screenshot;
130
+ }
131
+
132
+ // Prepare artifacts directory
133
+ const reporter = new GuardianReporter();
134
+ const { runDir } = reporter.prepareArtifactsDir(artifactsDir);
135
+
136
+ crawlResult = await crawler.crawl(browser, runDir);
137
+
138
+ console.log(`āœ… Crawl complete: visited ${crawlResult.totalVisited}/${crawlResult.totalDiscovered} pages`);
139
+
140
+ if (safety && crawlResult.safetyStats) {
141
+ const blocked = crawlResult.safetyStats.urlsBlocked || 0;
142
+ if (blocked > 0) {
143
+ console.log(`šŸ›”ļø Safety: Blocked ${blocked} dangerous URLs`);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Stop trace recording
149
+ if (networkTrace && enableTrace && tracePath && browser.context) {
150
+ await networkTrace.stopTrace(browser.context, tracePath);
151
+ console.log(`āœ… Trace saved: trace.zip`);
152
+ }
153
+
154
+ // Generate report
155
+ console.log(`\nšŸ“Š Generating report...`);
156
+ const reporter = new GuardianReporter();
157
+
158
+ let report;
159
+ if (flowResult) {
160
+ // Create report from flow execution
161
+ report = reporter.createFlowReport(flowResult, baseUrl);
162
+ } else {
163
+ // Create report from crawl
164
+ report = reporter.createReport(crawlResult, baseUrl);
165
+ }
166
+
167
+ // Save JSON report
168
+ const savedReport = reporter.saveReport(report, artifactsDir);
169
+ console.log(`āœ… Report saved to: ${savedReport.runDir}`);
170
+
171
+ // Generate HTML report if enabled
172
+ if (htmlReporter) {
173
+ const htmlSaved = htmlReporter.generateAndSave(report, savedReport.runDir);
174
+ if (htmlSaved) {
175
+ console.log(`āœ… HTML report: report.html`);
176
+ }
177
+ }
178
+
179
+ // Display verdict
180
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
181
+
182
+ const { decision } = report.finalJudgment;
183
+ const coverageStr = `${report.summary.coverage}%`;
184
+
185
+ if (decision === 'READY') {
186
+ console.log(`\n🟢 READY — Safe to launch`);
187
+ } else if (decision === 'DO_NOT_LAUNCH') {
188
+ console.log(`\nšŸ”“ DO_NOT_LAUNCH — Issues found`);
189
+ } else {
190
+ console.log(`\n🟔 INSUFFICIENT_CONFIDENCE — Needs more data`);
191
+ }
192
+
193
+ console.log(`\nšŸ“ˆ Coverage: ${coverageStr}`);
194
+ console.log(`šŸ“„ Pages visited: ${report.summary.visitedPages}`);
195
+ console.log(`āŒ Failed pages: ${report.summary.failedPages}`);
196
+ console.log(`šŸ’¬ Confidence: ${report.confidence.level}`);
197
+
198
+ console.log(`\nšŸ“‹ Reasons:`);
199
+ report.finalJudgment.reasons.forEach(reason => {
200
+ console.log(` • ${reason}`);
201
+ });
202
+
203
+ console.log(`\nšŸ’¾ Full report: ${savedReport.reportPath}`);
204
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
205
+
206
+ // Exit with appropriate code
207
+ const exitCode = (decision === 'READY') ? 0 : 1;
208
+ process.exit(exitCode);
209
+
210
+ } catch (err) {
211
+ console.error(`\nāŒ Error: ${err.message}`);
212
+ process.exit(2);
213
+ } finally {
214
+ await browser.close();
215
+ }
216
+ }
217
+
218
+ module.exports = { runGuardian };