@odavl/guardian 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,416 +1,416 @@
|
|
|
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
|
-
const { getFounderBadgeHTML } = require('../founder/founder-tracker');
|
|
9
|
-
|
|
10
|
-
class GuardianHTMLReporter {
|
|
11
|
-
/**
|
|
12
|
-
* Generate HTML report from JSON report
|
|
13
|
-
* @param {object} jsonReport - JSON report object
|
|
14
|
-
* @param {string} artifactsDir - Directory containing artifacts
|
|
15
|
-
* @returns {string} HTML content
|
|
16
|
-
*/
|
|
17
|
-
generate(jsonReport, artifactsDir) {
|
|
18
|
-
const html = `<!DOCTYPE html>
|
|
19
|
-
<html lang="en">
|
|
20
|
-
<head>
|
|
21
|
-
<meta charset="UTF-8">
|
|
22
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
23
|
-
<title>Guardian Report - ${jsonReport.baseUrl}</title>
|
|
24
|
-
<style>
|
|
25
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
-
body {
|
|
27
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
28
|
-
background: #f5f5f5;
|
|
29
|
-
color: #333;
|
|
30
|
-
line-height: 1.6;
|
|
31
|
-
}
|
|
32
|
-
.container {
|
|
33
|
-
max-width: 1200px;
|
|
34
|
-
margin: 0 auto;
|
|
35
|
-
padding: 20px;
|
|
36
|
-
}
|
|
37
|
-
.header {
|
|
38
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
39
|
-
color: white;
|
|
40
|
-
padding: 40px 20px;
|
|
41
|
-
border-radius: 10px;
|
|
42
|
-
margin-bottom: 30px;
|
|
43
|
-
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
44
|
-
}
|
|
45
|
-
.header h1 {
|
|
46
|
-
font-size: 2.5em;
|
|
47
|
-
margin-bottom: 10px;
|
|
48
|
-
}
|
|
49
|
-
.header .subtitle {
|
|
50
|
-
font-size: 1.2em;
|
|
51
|
-
opacity: 0.9;
|
|
52
|
-
}
|
|
53
|
-
.verdict {
|
|
54
|
-
background: white;
|
|
55
|
-
padding: 30px;
|
|
56
|
-
border-radius: 10px;
|
|
57
|
-
margin-bottom: 30px;
|
|
58
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
59
|
-
}
|
|
60
|
-
.verdict.ready {
|
|
61
|
-
border-left: 5px solid #10b981;
|
|
62
|
-
}
|
|
63
|
-
.verdict.do-not-launch {
|
|
64
|
-
border-left: 5px solid #ef4444;
|
|
65
|
-
}
|
|
66
|
-
.verdict.insufficient {
|
|
67
|
-
border-left: 5px solid #f59e0b;
|
|
68
|
-
}
|
|
69
|
-
.verdict-badge {
|
|
70
|
-
display: inline-block;
|
|
71
|
-
padding: 10px 20px;
|
|
72
|
-
border-radius: 20px;
|
|
73
|
-
font-weight: bold;
|
|
74
|
-
font-size: 1.2em;
|
|
75
|
-
margin-bottom: 15px;
|
|
76
|
-
}
|
|
77
|
-
.verdict-badge.ready {
|
|
78
|
-
background: #d1fae5;
|
|
79
|
-
color: #065f46;
|
|
80
|
-
}
|
|
81
|
-
.verdict-badge.do-not-launch {
|
|
82
|
-
background: #fee2e2;
|
|
83
|
-
color: #991b1b;
|
|
84
|
-
}
|
|
85
|
-
.verdict-badge.insufficient {
|
|
86
|
-
background: #fef3c7;
|
|
87
|
-
color: #92400e;
|
|
88
|
-
}
|
|
89
|
-
.metrics {
|
|
90
|
-
display: grid;
|
|
91
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
92
|
-
gap: 20px;
|
|
93
|
-
margin-bottom: 30px;
|
|
94
|
-
}
|
|
95
|
-
.metric-card {
|
|
96
|
-
background: white;
|
|
97
|
-
padding: 20px;
|
|
98
|
-
border-radius: 10px;
|
|
99
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
100
|
-
}
|
|
101
|
-
.metric-card .label {
|
|
102
|
-
font-size: 0.9em;
|
|
103
|
-
color: #666;
|
|
104
|
-
margin-bottom: 5px;
|
|
105
|
-
}
|
|
106
|
-
.metric-card .value {
|
|
107
|
-
font-size: 2em;
|
|
108
|
-
font-weight: bold;
|
|
109
|
-
color: #667eea;
|
|
110
|
-
}
|
|
111
|
-
.section {
|
|
112
|
-
background: white;
|
|
113
|
-
padding: 30px;
|
|
114
|
-
border-radius: 10px;
|
|
115
|
-
margin-bottom: 30px;
|
|
116
|
-
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
117
|
-
}
|
|
118
|
-
.section h2 {
|
|
119
|
-
margin-bottom: 20px;
|
|
120
|
-
color: #667eea;
|
|
121
|
-
border-bottom: 2px solid #f0f0f0;
|
|
122
|
-
padding-bottom: 10px;
|
|
123
|
-
}
|
|
124
|
-
.reason-list {
|
|
125
|
-
list-style: none;
|
|
126
|
-
}
|
|
127
|
-
.reason-list li {
|
|
128
|
-
padding: 10px;
|
|
129
|
-
margin-bottom: 10px;
|
|
130
|
-
background: #f9fafb;
|
|
131
|
-
border-left: 3px solid #667eea;
|
|
132
|
-
border-radius: 5px;
|
|
133
|
-
}
|
|
134
|
-
.screenshots {
|
|
135
|
-
display: grid;
|
|
136
|
-
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
137
|
-
gap: 20px;
|
|
138
|
-
}
|
|
139
|
-
.screenshot-card {
|
|
140
|
-
border: 1px solid #e5e7eb;
|
|
141
|
-
border-radius: 10px;
|
|
142
|
-
overflow: hidden;
|
|
143
|
-
transition: transform 0.2s;
|
|
144
|
-
}
|
|
145
|
-
.screenshot-card:hover {
|
|
146
|
-
transform: translateY(-5px);
|
|
147
|
-
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
|
148
|
-
}
|
|
149
|
-
.screenshot-card img {
|
|
150
|
-
width: 100%;
|
|
151
|
-
height: 200px;
|
|
152
|
-
object-fit: cover;
|
|
153
|
-
}
|
|
154
|
-
.screenshot-card .caption {
|
|
155
|
-
padding: 10px;
|
|
156
|
-
background: #f9fafb;
|
|
157
|
-
font-size: 0.9em;
|
|
158
|
-
color: #666;
|
|
159
|
-
}
|
|
160
|
-
.page-table {
|
|
161
|
-
width: 100%;
|
|
162
|
-
border-collapse: collapse;
|
|
163
|
-
}
|
|
164
|
-
.page-table th {
|
|
165
|
-
background: #f9fafb;
|
|
166
|
-
padding: 12px;
|
|
167
|
-
text-align: left;
|
|
168
|
-
font-weight: 600;
|
|
169
|
-
border-bottom: 2px solid #e5e7eb;
|
|
170
|
-
}
|
|
171
|
-
.page-table td {
|
|
172
|
-
padding: 12px;
|
|
173
|
-
border-bottom: 1px solid #e5e7eb;
|
|
174
|
-
}
|
|
175
|
-
.page-table tr:hover {
|
|
176
|
-
background: #f9fafb;
|
|
177
|
-
}
|
|
178
|
-
.status-badge {
|
|
179
|
-
display: inline-block;
|
|
180
|
-
padding: 4px 12px;
|
|
181
|
-
border-radius: 12px;
|
|
182
|
-
font-size: 0.85em;
|
|
183
|
-
font-weight: 600;
|
|
184
|
-
}
|
|
185
|
-
.status-success {
|
|
186
|
-
background: #d1fae5;
|
|
187
|
-
color: #065f46;
|
|
188
|
-
}
|
|
189
|
-
.status-error {
|
|
190
|
-
background: #fee2e2;
|
|
191
|
-
color: #991b1b;
|
|
192
|
-
}
|
|
193
|
-
.footer {
|
|
194
|
-
text-align: center;
|
|
195
|
-
padding: 20px;
|
|
196
|
-
color: #666;
|
|
197
|
-
font-size: 0.9em;
|
|
198
|
-
}
|
|
199
|
-
.confidence-high { color: #10b981; font-weight: bold; }
|
|
200
|
-
.confidence-medium { color: #f59e0b; font-weight: bold; }
|
|
201
|
-
.confidence-low { color: #ef4444; font-weight: bold; }
|
|
202
|
-
</style>
|
|
203
|
-
</head>
|
|
204
|
-
<body>
|
|
205
|
-
<div class="container">
|
|
206
|
-
<div class="header">
|
|
207
|
-
<h1>🛡️ ODAVL Guardian</h1>
|
|
208
|
-
<div class="subtitle">Market Reality Testing Report</div>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
${getFounderBadgeHTML()}
|
|
212
|
-
${this.generateVerdictSection(jsonReport)}
|
|
213
|
-
${this.generateMetricsSection(jsonReport)}
|
|
214
|
-
${this.generateReasonsSection(jsonReport)}
|
|
215
|
-
${this.generatePagesSection(jsonReport)}
|
|
216
|
-
${this.generateScreenshotsSection(jsonReport, artifactsDir)}
|
|
217
|
-
|
|
218
|
-
<div class="footer">
|
|
219
|
-
Generated by ODAVL Guardian • ${new Date(jsonReport.timestamp).toLocaleString()}
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
</body>
|
|
223
|
-
</html>`;
|
|
224
|
-
|
|
225
|
-
return html;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Generate verdict section
|
|
230
|
-
*/
|
|
231
|
-
generateVerdictSection(report) {
|
|
232
|
-
const decision = report.finalJudgment.decision;
|
|
233
|
-
const decisionClass = decision.toLowerCase().replace(/_/g, '-');
|
|
234
|
-
const decisionIcon = decision === 'READY' ? '🟢' : decision === 'DO_NOT_LAUNCH' ? '🔴' : '🟡';
|
|
235
|
-
const decisionText = decision === 'READY' ? 'Safe to Launch' : decision === 'DO_NOT_LAUNCH' ? 'DO NOT LAUNCH' : 'Insufficient Confidence';
|
|
236
|
-
|
|
237
|
-
return `
|
|
238
|
-
<div class="verdict ${decisionClass}">
|
|
239
|
-
<div class="verdict-badge ${decisionClass}">
|
|
240
|
-
${decisionIcon} ${decisionText}
|
|
241
|
-
</div>
|
|
242
|
-
<p style="margin-top: 15px; font-size: 1.1em;">
|
|
243
|
-
<strong>Target:</strong> ${report.baseUrl}
|
|
244
|
-
</p>
|
|
245
|
-
<p style="margin-top: 10px;">
|
|
246
|
-
<strong>Confidence:</strong>
|
|
247
|
-
<span class="confidence-${report.confidence.level.toLowerCase()}">${report.confidence.level}</span>
|
|
248
|
-
</p>
|
|
249
|
-
<p style="margin-top: 5px; color: #666;">
|
|
250
|
-
${report.confidence.reasoning}
|
|
251
|
-
</p>
|
|
252
|
-
</div>
|
|
253
|
-
`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Generate metrics section
|
|
258
|
-
*/
|
|
259
|
-
generateMetricsSection(report) {
|
|
260
|
-
return `
|
|
261
|
-
<div class="metrics">
|
|
262
|
-
<div class="metric-card">
|
|
263
|
-
<div class="label">Coverage</div>
|
|
264
|
-
<div class="value">${report.summary.coverage}%</div>
|
|
265
|
-
</div>
|
|
266
|
-
<div class="metric-card">
|
|
267
|
-
<div class="label">Pages Visited</div>
|
|
268
|
-
<div class="value">${report.summary.visitedPages}</div>
|
|
269
|
-
</div>
|
|
270
|
-
<div class="metric-card">
|
|
271
|
-
<div class="label">Pages Discovered</div>
|
|
272
|
-
<div class="value">${report.summary.discoveredPages}</div>
|
|
273
|
-
</div>
|
|
274
|
-
<div class="metric-card">
|
|
275
|
-
<div class="label">Failed Pages</div>
|
|
276
|
-
<div class="value" style="color: ${report.summary.failedPages > 0 ? '#ef4444' : '#10b981'}">
|
|
277
|
-
${report.summary.failedPages}
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Generate reasons section
|
|
286
|
-
*/
|
|
287
|
-
generateReasonsSection(report) {
|
|
288
|
-
const reasons = report.finalJudgment.reasons || [];
|
|
289
|
-
if (reasons.length === 0) return '';
|
|
290
|
-
|
|
291
|
-
const reasonItems = reasons.map(r => `<li>${r}</li>`).join('');
|
|
292
|
-
|
|
293
|
-
return `
|
|
294
|
-
<div class="section">
|
|
295
|
-
<h2>📋 Decision Reasons</h2>
|
|
296
|
-
<ul class="reason-list">
|
|
297
|
-
${reasonItems}
|
|
298
|
-
</ul>
|
|
299
|
-
</div>
|
|
300
|
-
`;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Generate pages section
|
|
305
|
-
*/
|
|
306
|
-
generatePagesSection(report) {
|
|
307
|
-
const pages = report.pages || [];
|
|
308
|
-
if (pages.length === 0) return '';
|
|
309
|
-
|
|
310
|
-
const rows = pages.map(page => {
|
|
311
|
-
const statusClass = page.status >= 200 && page.status < 400 ? 'status-success' : 'status-error';
|
|
312
|
-
const statusText = page.status || 'N/A';
|
|
313
|
-
|
|
314
|
-
return `
|
|
315
|
-
<tr>
|
|
316
|
-
<td>${page.index}</td>
|
|
317
|
-
<td style="word-break: break-all;">${page.url}</td>
|
|
318
|
-
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
|
319
|
-
<td>${page.links || 0}</td>
|
|
320
|
-
</tr>
|
|
321
|
-
`;
|
|
322
|
-
}).join('');
|
|
323
|
-
|
|
324
|
-
return `
|
|
325
|
-
<div class="section">
|
|
326
|
-
<h2>📄 Pages Visited</h2>
|
|
327
|
-
<table class="page-table">
|
|
328
|
-
<thead>
|
|
329
|
-
<tr>
|
|
330
|
-
<th>#</th>
|
|
331
|
-
<th>URL</th>
|
|
332
|
-
<th>Status</th>
|
|
333
|
-
<th>Links</th>
|
|
334
|
-
</tr>
|
|
335
|
-
</thead>
|
|
336
|
-
<tbody>
|
|
337
|
-
${rows}
|
|
338
|
-
</tbody>
|
|
339
|
-
</table>
|
|
340
|
-
</div>
|
|
341
|
-
`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Generate screenshots section
|
|
346
|
-
*/
|
|
347
|
-
generateScreenshotsSection(report, artifactsDir) {
|
|
348
|
-
const pagesDir = path.join(artifactsDir, 'pages');
|
|
349
|
-
|
|
350
|
-
// Check if pages directory exists
|
|
351
|
-
if (!fs.existsSync(pagesDir)) {
|
|
352
|
-
return '';
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Get all screenshot files
|
|
356
|
-
const files = fs.readdirSync(pagesDir).filter(f => f.endsWith('.jpeg') || f.endsWith('.jpg') || f.endsWith('.png'));
|
|
357
|
-
|
|
358
|
-
if (files.length === 0) {
|
|
359
|
-
return '';
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const cards = files.map(file => {
|
|
363
|
-
const relativePath = `pages/${file}`;
|
|
364
|
-
return `
|
|
365
|
-
<div class="screenshot-card">
|
|
366
|
-
<img src="${relativePath}" alt="${file}" loading="lazy">
|
|
367
|
-
<div class="caption">${file}</div>
|
|
368
|
-
</div>
|
|
369
|
-
`;
|
|
370
|
-
}).join('');
|
|
371
|
-
|
|
372
|
-
return `
|
|
373
|
-
<div class="section">
|
|
374
|
-
<h2>📸 Screenshots</h2>
|
|
375
|
-
<div class="screenshots">
|
|
376
|
-
${cards}
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Save HTML report to file
|
|
384
|
-
* @param {string} html - HTML content
|
|
385
|
-
* @param {string} outputPath - Where to save the HTML file
|
|
386
|
-
* @returns {boolean} Success status
|
|
387
|
-
*/
|
|
388
|
-
save(html, outputPath) {
|
|
389
|
-
try {
|
|
390
|
-
fs.writeFileSync(outputPath, html, 'utf8');
|
|
391
|
-
return true;
|
|
392
|
-
} catch (error) {
|
|
393
|
-
console.error(`❌ Failed to save HTML report: ${error.message}`);
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Generate and save HTML report
|
|
400
|
-
* @param {object} jsonReport - JSON report object
|
|
401
|
-
* @param {string} artifactsDir - Directory containing artifacts
|
|
402
|
-
* @returns {boolean} Success status
|
|
403
|
-
*/
|
|
404
|
-
generateAndSave(jsonReport, artifactsDir) {
|
|
405
|
-
try {
|
|
406
|
-
const html = this.generate(jsonReport, artifactsDir);
|
|
407
|
-
const outputPath = path.join(artifactsDir, 'report.html');
|
|
408
|
-
return this.save(html, outputPath);
|
|
409
|
-
} catch (error) {
|
|
410
|
-
console.error(`❌ Failed to generate HTML report: ${error.message}`);
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
module.exports = GuardianHTMLReporter;
|
|
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
|
+
const { getFounderBadgeHTML } = require('../founder/founder-tracker');
|
|
9
|
+
|
|
10
|
+
class GuardianHTMLReporter {
|
|
11
|
+
/**
|
|
12
|
+
* Generate HTML report from JSON report
|
|
13
|
+
* @param {object} jsonReport - JSON report object
|
|
14
|
+
* @param {string} artifactsDir - Directory containing artifacts
|
|
15
|
+
* @returns {string} HTML content
|
|
16
|
+
*/
|
|
17
|
+
generate(jsonReport, artifactsDir) {
|
|
18
|
+
const html = `<!DOCTYPE html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="UTF-8">
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
23
|
+
<title>Guardian Report - ${jsonReport.baseUrl}</title>
|
|
24
|
+
<style>
|
|
25
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
+
body {
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
28
|
+
background: #f5f5f5;
|
|
29
|
+
color: #333;
|
|
30
|
+
line-height: 1.6;
|
|
31
|
+
}
|
|
32
|
+
.container {
|
|
33
|
+
max-width: 1200px;
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
padding: 20px;
|
|
36
|
+
}
|
|
37
|
+
.header {
|
|
38
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
39
|
+
color: white;
|
|
40
|
+
padding: 40px 20px;
|
|
41
|
+
border-radius: 10px;
|
|
42
|
+
margin-bottom: 30px;
|
|
43
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
44
|
+
}
|
|
45
|
+
.header h1 {
|
|
46
|
+
font-size: 2.5em;
|
|
47
|
+
margin-bottom: 10px;
|
|
48
|
+
}
|
|
49
|
+
.header .subtitle {
|
|
50
|
+
font-size: 1.2em;
|
|
51
|
+
opacity: 0.9;
|
|
52
|
+
}
|
|
53
|
+
.verdict {
|
|
54
|
+
background: white;
|
|
55
|
+
padding: 30px;
|
|
56
|
+
border-radius: 10px;
|
|
57
|
+
margin-bottom: 30px;
|
|
58
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
59
|
+
}
|
|
60
|
+
.verdict.ready {
|
|
61
|
+
border-left: 5px solid #10b981;
|
|
62
|
+
}
|
|
63
|
+
.verdict.do-not-launch {
|
|
64
|
+
border-left: 5px solid #ef4444;
|
|
65
|
+
}
|
|
66
|
+
.verdict.insufficient {
|
|
67
|
+
border-left: 5px solid #f59e0b;
|
|
68
|
+
}
|
|
69
|
+
.verdict-badge {
|
|
70
|
+
display: inline-block;
|
|
71
|
+
padding: 10px 20px;
|
|
72
|
+
border-radius: 20px;
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
font-size: 1.2em;
|
|
75
|
+
margin-bottom: 15px;
|
|
76
|
+
}
|
|
77
|
+
.verdict-badge.ready {
|
|
78
|
+
background: #d1fae5;
|
|
79
|
+
color: #065f46;
|
|
80
|
+
}
|
|
81
|
+
.verdict-badge.do-not-launch {
|
|
82
|
+
background: #fee2e2;
|
|
83
|
+
color: #991b1b;
|
|
84
|
+
}
|
|
85
|
+
.verdict-badge.insufficient {
|
|
86
|
+
background: #fef3c7;
|
|
87
|
+
color: #92400e;
|
|
88
|
+
}
|
|
89
|
+
.metrics {
|
|
90
|
+
display: grid;
|
|
91
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
92
|
+
gap: 20px;
|
|
93
|
+
margin-bottom: 30px;
|
|
94
|
+
}
|
|
95
|
+
.metric-card {
|
|
96
|
+
background: white;
|
|
97
|
+
padding: 20px;
|
|
98
|
+
border-radius: 10px;
|
|
99
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
100
|
+
}
|
|
101
|
+
.metric-card .label {
|
|
102
|
+
font-size: 0.9em;
|
|
103
|
+
color: #666;
|
|
104
|
+
margin-bottom: 5px;
|
|
105
|
+
}
|
|
106
|
+
.metric-card .value {
|
|
107
|
+
font-size: 2em;
|
|
108
|
+
font-weight: bold;
|
|
109
|
+
color: #667eea;
|
|
110
|
+
}
|
|
111
|
+
.section {
|
|
112
|
+
background: white;
|
|
113
|
+
padding: 30px;
|
|
114
|
+
border-radius: 10px;
|
|
115
|
+
margin-bottom: 30px;
|
|
116
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
117
|
+
}
|
|
118
|
+
.section h2 {
|
|
119
|
+
margin-bottom: 20px;
|
|
120
|
+
color: #667eea;
|
|
121
|
+
border-bottom: 2px solid #f0f0f0;
|
|
122
|
+
padding-bottom: 10px;
|
|
123
|
+
}
|
|
124
|
+
.reason-list {
|
|
125
|
+
list-style: none;
|
|
126
|
+
}
|
|
127
|
+
.reason-list li {
|
|
128
|
+
padding: 10px;
|
|
129
|
+
margin-bottom: 10px;
|
|
130
|
+
background: #f9fafb;
|
|
131
|
+
border-left: 3px solid #667eea;
|
|
132
|
+
border-radius: 5px;
|
|
133
|
+
}
|
|
134
|
+
.screenshots {
|
|
135
|
+
display: grid;
|
|
136
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
137
|
+
gap: 20px;
|
|
138
|
+
}
|
|
139
|
+
.screenshot-card {
|
|
140
|
+
border: 1px solid #e5e7eb;
|
|
141
|
+
border-radius: 10px;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
transition: transform 0.2s;
|
|
144
|
+
}
|
|
145
|
+
.screenshot-card:hover {
|
|
146
|
+
transform: translateY(-5px);
|
|
147
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
|
148
|
+
}
|
|
149
|
+
.screenshot-card img {
|
|
150
|
+
width: 100%;
|
|
151
|
+
height: 200px;
|
|
152
|
+
object-fit: cover;
|
|
153
|
+
}
|
|
154
|
+
.screenshot-card .caption {
|
|
155
|
+
padding: 10px;
|
|
156
|
+
background: #f9fafb;
|
|
157
|
+
font-size: 0.9em;
|
|
158
|
+
color: #666;
|
|
159
|
+
}
|
|
160
|
+
.page-table {
|
|
161
|
+
width: 100%;
|
|
162
|
+
border-collapse: collapse;
|
|
163
|
+
}
|
|
164
|
+
.page-table th {
|
|
165
|
+
background: #f9fafb;
|
|
166
|
+
padding: 12px;
|
|
167
|
+
text-align: left;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
border-bottom: 2px solid #e5e7eb;
|
|
170
|
+
}
|
|
171
|
+
.page-table td {
|
|
172
|
+
padding: 12px;
|
|
173
|
+
border-bottom: 1px solid #e5e7eb;
|
|
174
|
+
}
|
|
175
|
+
.page-table tr:hover {
|
|
176
|
+
background: #f9fafb;
|
|
177
|
+
}
|
|
178
|
+
.status-badge {
|
|
179
|
+
display: inline-block;
|
|
180
|
+
padding: 4px 12px;
|
|
181
|
+
border-radius: 12px;
|
|
182
|
+
font-size: 0.85em;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
}
|
|
185
|
+
.status-success {
|
|
186
|
+
background: #d1fae5;
|
|
187
|
+
color: #065f46;
|
|
188
|
+
}
|
|
189
|
+
.status-error {
|
|
190
|
+
background: #fee2e2;
|
|
191
|
+
color: #991b1b;
|
|
192
|
+
}
|
|
193
|
+
.footer {
|
|
194
|
+
text-align: center;
|
|
195
|
+
padding: 20px;
|
|
196
|
+
color: #666;
|
|
197
|
+
font-size: 0.9em;
|
|
198
|
+
}
|
|
199
|
+
.confidence-high { color: #10b981; font-weight: bold; }
|
|
200
|
+
.confidence-medium { color: #f59e0b; font-weight: bold; }
|
|
201
|
+
.confidence-low { color: #ef4444; font-weight: bold; }
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
<body>
|
|
205
|
+
<div class="container">
|
|
206
|
+
<div class="header">
|
|
207
|
+
<h1>🛡️ ODAVL Guardian</h1>
|
|
208
|
+
<div class="subtitle">Market Reality Testing Report</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
${getFounderBadgeHTML()}
|
|
212
|
+
${this.generateVerdictSection(jsonReport)}
|
|
213
|
+
${this.generateMetricsSection(jsonReport)}
|
|
214
|
+
${this.generateReasonsSection(jsonReport)}
|
|
215
|
+
${this.generatePagesSection(jsonReport)}
|
|
216
|
+
${this.generateScreenshotsSection(jsonReport, artifactsDir)}
|
|
217
|
+
|
|
218
|
+
<div class="footer">
|
|
219
|
+
Generated by ODAVL Guardian • ${new Date(jsonReport.timestamp).toLocaleString()}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
|
|
225
|
+
return html;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generate verdict section
|
|
230
|
+
*/
|
|
231
|
+
generateVerdictSection(report) {
|
|
232
|
+
const decision = report.finalJudgment.decision;
|
|
233
|
+
const decisionClass = decision.toLowerCase().replace(/_/g, '-');
|
|
234
|
+
const decisionIcon = decision === 'READY' ? '🟢' : decision === 'DO_NOT_LAUNCH' ? '🔴' : '🟡';
|
|
235
|
+
const decisionText = decision === 'READY' ? 'Safe to Launch' : decision === 'DO_NOT_LAUNCH' ? 'DO NOT LAUNCH' : 'Insufficient Confidence';
|
|
236
|
+
|
|
237
|
+
return `
|
|
238
|
+
<div class="verdict ${decisionClass}">
|
|
239
|
+
<div class="verdict-badge ${decisionClass}">
|
|
240
|
+
${decisionIcon} ${decisionText}
|
|
241
|
+
</div>
|
|
242
|
+
<p style="margin-top: 15px; font-size: 1.1em;">
|
|
243
|
+
<strong>Target:</strong> ${report.baseUrl}
|
|
244
|
+
</p>
|
|
245
|
+
<p style="margin-top: 10px;">
|
|
246
|
+
<strong>Confidence:</strong>
|
|
247
|
+
<span class="confidence-${report.confidence.level.toLowerCase()}">${report.confidence.level}</span>
|
|
248
|
+
</p>
|
|
249
|
+
<p style="margin-top: 5px; color: #666;">
|
|
250
|
+
${report.confidence.reasoning}
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate metrics section
|
|
258
|
+
*/
|
|
259
|
+
generateMetricsSection(report) {
|
|
260
|
+
return `
|
|
261
|
+
<div class="metrics">
|
|
262
|
+
<div class="metric-card">
|
|
263
|
+
<div class="label">Coverage</div>
|
|
264
|
+
<div class="value">${report.summary.coverage}%</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="metric-card">
|
|
267
|
+
<div class="label">Pages Visited</div>
|
|
268
|
+
<div class="value">${report.summary.visitedPages}</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="metric-card">
|
|
271
|
+
<div class="label">Pages Discovered</div>
|
|
272
|
+
<div class="value">${report.summary.discoveredPages}</div>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="metric-card">
|
|
275
|
+
<div class="label">Failed Pages</div>
|
|
276
|
+
<div class="value" style="color: ${report.summary.failedPages > 0 ? '#ef4444' : '#10b981'}">
|
|
277
|
+
${report.summary.failedPages}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate reasons section
|
|
286
|
+
*/
|
|
287
|
+
generateReasonsSection(report) {
|
|
288
|
+
const reasons = report.finalJudgment.reasons || [];
|
|
289
|
+
if (reasons.length === 0) return '';
|
|
290
|
+
|
|
291
|
+
const reasonItems = reasons.map(r => `<li>${r}</li>`).join('');
|
|
292
|
+
|
|
293
|
+
return `
|
|
294
|
+
<div class="section">
|
|
295
|
+
<h2>📋 Decision Reasons</h2>
|
|
296
|
+
<ul class="reason-list">
|
|
297
|
+
${reasonItems}
|
|
298
|
+
</ul>
|
|
299
|
+
</div>
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate pages section
|
|
305
|
+
*/
|
|
306
|
+
generatePagesSection(report) {
|
|
307
|
+
const pages = report.pages || [];
|
|
308
|
+
if (pages.length === 0) return '';
|
|
309
|
+
|
|
310
|
+
const rows = pages.map(page => {
|
|
311
|
+
const statusClass = page.status >= 200 && page.status < 400 ? 'status-success' : 'status-error';
|
|
312
|
+
const statusText = page.status || 'N/A';
|
|
313
|
+
|
|
314
|
+
return `
|
|
315
|
+
<tr>
|
|
316
|
+
<td>${page.index}</td>
|
|
317
|
+
<td style="word-break: break-all;">${page.url}</td>
|
|
318
|
+
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
|
319
|
+
<td>${page.links || 0}</td>
|
|
320
|
+
</tr>
|
|
321
|
+
`;
|
|
322
|
+
}).join('');
|
|
323
|
+
|
|
324
|
+
return `
|
|
325
|
+
<div class="section">
|
|
326
|
+
<h2>📄 Pages Visited</h2>
|
|
327
|
+
<table class="page-table">
|
|
328
|
+
<thead>
|
|
329
|
+
<tr>
|
|
330
|
+
<th>#</th>
|
|
331
|
+
<th>URL</th>
|
|
332
|
+
<th>Status</th>
|
|
333
|
+
<th>Links</th>
|
|
334
|
+
</tr>
|
|
335
|
+
</thead>
|
|
336
|
+
<tbody>
|
|
337
|
+
${rows}
|
|
338
|
+
</tbody>
|
|
339
|
+
</table>
|
|
340
|
+
</div>
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generate screenshots section
|
|
346
|
+
*/
|
|
347
|
+
generateScreenshotsSection(report, artifactsDir) {
|
|
348
|
+
const pagesDir = path.join(artifactsDir, 'pages');
|
|
349
|
+
|
|
350
|
+
// Check if pages directory exists
|
|
351
|
+
if (!fs.existsSync(pagesDir)) {
|
|
352
|
+
return '';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Get all screenshot files
|
|
356
|
+
const files = fs.readdirSync(pagesDir).filter(f => f.endsWith('.jpeg') || f.endsWith('.jpg') || f.endsWith('.png'));
|
|
357
|
+
|
|
358
|
+
if (files.length === 0) {
|
|
359
|
+
return '';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const cards = files.map(file => {
|
|
363
|
+
const relativePath = `pages/${file}`;
|
|
364
|
+
return `
|
|
365
|
+
<div class="screenshot-card">
|
|
366
|
+
<img src="${relativePath}" alt="${file}" loading="lazy">
|
|
367
|
+
<div class="caption">${file}</div>
|
|
368
|
+
</div>
|
|
369
|
+
`;
|
|
370
|
+
}).join('');
|
|
371
|
+
|
|
372
|
+
return `
|
|
373
|
+
<div class="section">
|
|
374
|
+
<h2>📸 Screenshots</h2>
|
|
375
|
+
<div class="screenshots">
|
|
376
|
+
${cards}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Save HTML report to file
|
|
384
|
+
* @param {string} html - HTML content
|
|
385
|
+
* @param {string} outputPath - Where to save the HTML file
|
|
386
|
+
* @returns {boolean} Success status
|
|
387
|
+
*/
|
|
388
|
+
save(html, outputPath) {
|
|
389
|
+
try {
|
|
390
|
+
fs.writeFileSync(outputPath, html, 'utf8');
|
|
391
|
+
return true;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error(`❌ Failed to save HTML report: ${error.message}`);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Generate and save HTML report
|
|
400
|
+
* @param {object} jsonReport - JSON report object
|
|
401
|
+
* @param {string} artifactsDir - Directory containing artifacts
|
|
402
|
+
* @returns {boolean} Success status
|
|
403
|
+
*/
|
|
404
|
+
generateAndSave(jsonReport, artifactsDir) {
|
|
405
|
+
try {
|
|
406
|
+
const html = this.generate(jsonReport, artifactsDir);
|
|
407
|
+
const outputPath = path.join(artifactsDir, 'report.html');
|
|
408
|
+
return this.save(html, outputPath);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(`❌ Failed to generate HTML report: ${error.message}`);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = GuardianHTMLReporter;
|