@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- package/src/guardian/webhook.js +206 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced HTML Reporter
|
|
3
|
+
*
|
|
4
|
+
* Generate interactive HTML reports with:
|
|
5
|
+
* - Top Risks section
|
|
6
|
+
* - Discovery results
|
|
7
|
+
* - Diff view
|
|
8
|
+
* - Evidence gallery
|
|
9
|
+
*
|
|
10
|
+
* Vanilla HTML + CSS + minimal JS. Works offline.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate enhanced HTML report
|
|
18
|
+
*/
|
|
19
|
+
function generateEnhancedHtml(snapshot, outputDir) {
|
|
20
|
+
if (!snapshot) {
|
|
21
|
+
return '<html><body><h1>No snapshot data</h1></body></html>';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const meta = snapshot.meta || {};
|
|
25
|
+
const marketImpact = snapshot.marketImpactSummary || {};
|
|
26
|
+
const counts = marketImpact.countsBySeverity || { CRITICAL: 0, WARNING: 0, INFO: 0 };
|
|
27
|
+
const topRisks = marketImpact.topRisks || [];
|
|
28
|
+
const attempts = snapshot.attempts || [];
|
|
29
|
+
const discovery = snapshot.discovery || {};
|
|
30
|
+
const baseline = snapshot.baseline || {};
|
|
31
|
+
|
|
32
|
+
let html = `<!DOCTYPE html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="UTF-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
37
|
+
<title>Guardian Report - ${meta.url || 'Unknown'}</title>
|
|
38
|
+
<style>
|
|
39
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
40
|
+
body {
|
|
41
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
42
|
+
line-height: 1.6;
|
|
43
|
+
color: #333;
|
|
44
|
+
background: #f5f5f5;
|
|
45
|
+
padding: 20px;
|
|
46
|
+
}
|
|
47
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
48
|
+
h1 { color: #2c3e50; margin-bottom: 10px; font-size: 32px; }
|
|
49
|
+
h2 { color: #34495e; margin-top: 30px; margin-bottom: 15px; font-size: 24px; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
|
50
|
+
h3 { color: #7f8c8d; margin-top: 20px; margin-bottom: 10px; font-size: 18px; }
|
|
51
|
+
.meta { color: #7f8c8d; margin-bottom: 30px; }
|
|
52
|
+
.meta span { display: inline-block; margin-right: 20px; }
|
|
53
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
54
|
+
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; }
|
|
55
|
+
.stat-card.critical { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
56
|
+
.stat-card.warning { background: linear-gradient(135deg, #fad961 0%, #f76b1c 100%); }
|
|
57
|
+
.stat-card.info { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333; }
|
|
58
|
+
.stat-number { font-size: 48px; font-weight: bold; margin-bottom: 10px; }
|
|
59
|
+
.stat-label { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; opacity: 0.9; }
|
|
60
|
+
.risk-item { background: #fff; border-left: 4px solid #e74c3c; padding: 15px; margin-bottom: 15px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
61
|
+
.risk-item.warning { border-left-color: #f39c12; }
|
|
62
|
+
.risk-item.info { border-left-color: #3498db; }
|
|
63
|
+
.risk-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
64
|
+
.risk-title { font-weight: bold; font-size: 16px; }
|
|
65
|
+
.risk-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: bold; text-transform: uppercase; }
|
|
66
|
+
.risk-badge.critical { background: #e74c3c; color: white; }
|
|
67
|
+
.risk-badge.warning { background: #f39c12; color: white; }
|
|
68
|
+
.risk-badge.info { background: #3498db; color: white; }
|
|
69
|
+
.risk-details { color: #7f8c8d; font-size: 14px; margin-top: 5px; }
|
|
70
|
+
.attempt-list { list-style: none; }
|
|
71
|
+
.attempt-item { background: #ecf0f1; padding: 10px 15px; margin-bottom: 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
|
|
72
|
+
.attempt-name { font-weight: 500; }
|
|
73
|
+
.attempt-outcome { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
|
|
74
|
+
.attempt-outcome.success { background: #2ecc71; color: white; }
|
|
75
|
+
.attempt-outcome.failure { background: #e74c3c; color: white; }
|
|
76
|
+
.discovery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 15px; }
|
|
77
|
+
.discovery-card { background: #ecf0f1; padding: 15px; border-radius: 4px; }
|
|
78
|
+
.discovery-stat { font-size: 24px; font-weight: bold; color: #3498db; margin-bottom: 5px; }
|
|
79
|
+
.discovery-label { font-size: 14px; color: #7f8c8d; }
|
|
80
|
+
.diff-section { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 4px; margin-bottom: 15px; }
|
|
81
|
+
.diff-item { margin: 10px 0; padding-left: 20px; }
|
|
82
|
+
.diff-item.added { border-left: 3px solid #2ecc71; color: #27ae60; }
|
|
83
|
+
.diff-item.removed { border-left: 3px solid #e74c3c; color: #c0392b; }
|
|
84
|
+
.diff-item.changed { border-left: 3px solid #f39c12; color: #d68910; }
|
|
85
|
+
.evidence-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
|
|
86
|
+
.evidence-card { background: white; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
|
|
87
|
+
.evidence-img { width: 100%; height: 200px; object-fit: cover; background: #f8f9fa; cursor: pointer; }
|
|
88
|
+
.evidence-caption { padding: 10px; font-size: 14px; color: #7f8c8d; }
|
|
89
|
+
.no-data { text-align: center; color: #95a5a6; padding: 40px; font-style: italic; }
|
|
90
|
+
footer { margin-top: 40px; text-align: center; color: #95a5a6; font-size: 14px; border-top: 1px solid #ecf0f1; padding-top: 20px; }
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div class="container">
|
|
95
|
+
<h1>🛡️ Guardian Reality Report</h1>
|
|
96
|
+
<div class="meta">
|
|
97
|
+
<span><strong>URL:</strong> ${meta.url || 'Unknown'}</span>
|
|
98
|
+
<span><strong>Run ID:</strong> ${meta.runId || 'Unknown'}</span>
|
|
99
|
+
<span><strong>Date:</strong> ${meta.createdAt || 'Unknown'}</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Summary Cards -->
|
|
103
|
+
<div class="summary">
|
|
104
|
+
<div class="stat-card critical">
|
|
105
|
+
<div class="stat-number">${counts.CRITICAL || 0}</div>
|
|
106
|
+
<div class="stat-label">Critical</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="stat-card warning">
|
|
109
|
+
<div class="stat-number">${counts.WARNING || 0}</div>
|
|
110
|
+
<div class="stat-label">Warnings</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="stat-card info">
|
|
113
|
+
<div class="stat-number">${counts.INFO || 0}</div>
|
|
114
|
+
<div class="stat-label">Info</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="stat-card">
|
|
117
|
+
<div class="stat-number">${attempts.length}</div>
|
|
118
|
+
<div class="stat-label">Attempts</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
// Top Risks Section
|
|
124
|
+
if (topRisks.length > 0) {
|
|
125
|
+
html += `
|
|
126
|
+
<h2>🔥 Top Risks</h2>
|
|
127
|
+
<div class="risks-section">
|
|
128
|
+
`;
|
|
129
|
+
topRisks.slice(0, 5).forEach(risk => {
|
|
130
|
+
const severityClass = risk.severity ? risk.severity.toLowerCase() : 'info';
|
|
131
|
+
html += `
|
|
132
|
+
<div class="risk-item ${severityClass}">
|
|
133
|
+
<div class="risk-header">
|
|
134
|
+
<div class="risk-title">${risk.humanReadableReason || 'Unknown risk'}</div>
|
|
135
|
+
<span class="risk-badge ${severityClass}">${risk.severity || 'INFO'}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="risk-details">
|
|
138
|
+
Category: ${risk.category || 'Unknown'} |
|
|
139
|
+
Impact Score: ${risk.impactScore || 0} |
|
|
140
|
+
Attempt: ${risk.attemptId || 'N/A'}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
`;
|
|
144
|
+
});
|
|
145
|
+
html += `
|
|
146
|
+
</div>
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Attempts Section
|
|
151
|
+
if (attempts.length > 0) {
|
|
152
|
+
html += `
|
|
153
|
+
<h2>🎯 Attempts</h2>
|
|
154
|
+
<ul class="attempt-list">
|
|
155
|
+
`;
|
|
156
|
+
attempts.forEach(attempt => {
|
|
157
|
+
const outcomeClass = attempt.outcome === 'SUCCESS' ? 'success' : 'failure';
|
|
158
|
+
html += `
|
|
159
|
+
<li class="attempt-item">
|
|
160
|
+
<span class="attempt-name">${attempt.attemptName || attempt.attemptId}</span>
|
|
161
|
+
<span class="attempt-outcome ${outcomeClass}">${attempt.outcome || 'UNKNOWN'}</span>
|
|
162
|
+
</li>
|
|
163
|
+
`;
|
|
164
|
+
});
|
|
165
|
+
html += `
|
|
166
|
+
</ul>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Discovery Section
|
|
171
|
+
if (discovery.pagesVisitedCount > 0) {
|
|
172
|
+
html += `
|
|
173
|
+
<h2>🔍 Discovery</h2>
|
|
174
|
+
<div class="discovery-grid">
|
|
175
|
+
<div class="discovery-card">
|
|
176
|
+
<div class="discovery-stat">${discovery.pagesVisitedCount || 0}</div>
|
|
177
|
+
<div class="discovery-label">Pages Visited</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="discovery-card">
|
|
180
|
+
<div class="discovery-stat">${discovery.interactionsDiscovered || 0}</div>
|
|
181
|
+
<div class="discovery-label">Interactions Discovered</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="discovery-card">
|
|
184
|
+
<div class="discovery-stat">${discovery.interactionsExecuted || 0}</div>
|
|
185
|
+
<div class="discovery-label">Interactions Executed</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
// Discovery Results
|
|
191
|
+
if (discovery.results && discovery.results.length > 0) {
|
|
192
|
+
html += `
|
|
193
|
+
<h3>Interaction Results</h3>
|
|
194
|
+
<ul class="attempt-list">
|
|
195
|
+
`;
|
|
196
|
+
discovery.results.slice(0, 10).forEach(result => {
|
|
197
|
+
const outcomeClass = result.outcome === 'SUCCESS' ? 'success' : 'failure';
|
|
198
|
+
html += `
|
|
199
|
+
<li class="attempt-item">
|
|
200
|
+
<span class="attempt-name">${result.interactionId || 'unknown'}</span>
|
|
201
|
+
<span class="attempt-outcome ${outcomeClass}">${result.outcome || 'UNKNOWN'}</span>
|
|
202
|
+
</li>
|
|
203
|
+
`;
|
|
204
|
+
});
|
|
205
|
+
html += `
|
|
206
|
+
</ul>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Baseline Diff Section
|
|
212
|
+
if (baseline.diff && (baseline.diff.regressions || baseline.diff.improvements)) {
|
|
213
|
+
html += `
|
|
214
|
+
<h2>📊 Changes Since Last Run</h2>
|
|
215
|
+
<div class="diff-section">
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
if (baseline.diff.regressions && Object.keys(baseline.diff.regressions).length > 0) {
|
|
219
|
+
html += `
|
|
220
|
+
<h3>⚠️ Regressions</h3>
|
|
221
|
+
`;
|
|
222
|
+
Object.entries(baseline.diff.regressions).forEach(([attemptId, details]) => {
|
|
223
|
+
html += `
|
|
224
|
+
<div class="diff-item removed">
|
|
225
|
+
<strong>${attemptId}:</strong> ${details.reason || 'Regressed'}
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (baseline.diff.improvements && Object.keys(baseline.diff.improvements).length > 0) {
|
|
232
|
+
html += `
|
|
233
|
+
<h3>✅ Improvements</h3>
|
|
234
|
+
`;
|
|
235
|
+
Object.entries(baseline.diff.improvements).forEach(([attemptId, details]) => {
|
|
236
|
+
html += `
|
|
237
|
+
<div class="diff-item added">
|
|
238
|
+
<strong>${attemptId}:</strong> ${details.reason || 'Improved'}
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
html += `
|
|
245
|
+
</div>
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Evidence Gallery
|
|
250
|
+
const attemptsWithScreenshots = attempts.filter(a => a.evidence && a.evidence.screenshotPath);
|
|
251
|
+
if (attemptsWithScreenshots.length > 0) {
|
|
252
|
+
html += `
|
|
253
|
+
<h2>📸 Evidence Gallery</h2>
|
|
254
|
+
<div class="evidence-gallery">
|
|
255
|
+
`;
|
|
256
|
+
attemptsWithScreenshots.forEach(attempt => {
|
|
257
|
+
const screenshotPath = attempt.evidence.screenshotPath || '';
|
|
258
|
+
const relativePath = screenshotPath.replace(outputDir, '').replace(/\\/g, '/');
|
|
259
|
+
html += `
|
|
260
|
+
<div class="evidence-card">
|
|
261
|
+
<img src=".${relativePath}" alt="${attempt.attemptName || attempt.attemptId}" class="evidence-img" onclick="window.open(this.src)">
|
|
262
|
+
<div class="evidence-caption">
|
|
263
|
+
<strong>${attempt.attemptName || attempt.attemptId}</strong><br>
|
|
264
|
+
${attempt.outcome || 'UNKNOWN'}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
`;
|
|
268
|
+
});
|
|
269
|
+
html += `
|
|
270
|
+
</div>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
html += `
|
|
275
|
+
<footer>
|
|
276
|
+
Generated by ODAVL Guardian | ${new Date().toISOString()}
|
|
277
|
+
</footer>
|
|
278
|
+
</div>
|
|
279
|
+
</body>
|
|
280
|
+
</html>
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
return html;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Write enhanced HTML report to file
|
|
288
|
+
*/
|
|
289
|
+
function writeEnhancedHtml(snapshot, outputDir) {
|
|
290
|
+
const html = generateEnhancedHtml(snapshot, outputDir);
|
|
291
|
+
const reportPath = path.join(outputDir, 'report.html');
|
|
292
|
+
|
|
293
|
+
const dir = path.dirname(reportPath);
|
|
294
|
+
if (!fs.existsSync(dir)) {
|
|
295
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fs.writeFileSync(reportPath, html, 'utf-8');
|
|
299
|
+
return reportPath;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
generateEnhancedHtml,
|
|
304
|
+
writeEnhancedHtml
|
|
305
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — Failure Taxonomy
|
|
3
|
+
* Deterministic categorization of failures by type, domain, and severity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const BREAK_TYPES = {
|
|
7
|
+
NAVIGATION: 'NAVIGATION', // Failed to navigate or redirect
|
|
8
|
+
SUBMISSION: 'SUBMISSION', // Form/checkout submission failed
|
|
9
|
+
VALIDATION: 'VALIDATION', // Validator detected issue
|
|
10
|
+
TIMEOUT: 'TIMEOUT', // Step or interaction timed out
|
|
11
|
+
VISUAL: 'VISUAL', // Expected element not visible
|
|
12
|
+
CONSOLE: 'CONSOLE', // Console error detected
|
|
13
|
+
NETWORK: 'NETWORK' // Network/HTTP error
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const IMPACT_DOMAINS = {
|
|
17
|
+
REVENUE: 'REVENUE',
|
|
18
|
+
LEAD: 'LEAD',
|
|
19
|
+
TRUST: 'TRUST',
|
|
20
|
+
UX: 'UX'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SEVERITY_LEVELS = {
|
|
24
|
+
INFO: 'INFO',
|
|
25
|
+
WARNING: 'WARNING',
|
|
26
|
+
CRITICAL: 'CRITICAL'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Map attempt/flow ID to primary impact domain
|
|
31
|
+
*/
|
|
32
|
+
function getImpactDomain(attemptId) {
|
|
33
|
+
const domainMap = {
|
|
34
|
+
// Attempts
|
|
35
|
+
contact_form: IMPACT_DOMAINS.LEAD,
|
|
36
|
+
newsletter_signup: IMPACT_DOMAINS.LEAD,
|
|
37
|
+
signup: IMPACT_DOMAINS.LEAD,
|
|
38
|
+
language_switch: IMPACT_DOMAINS.TRUST,
|
|
39
|
+
login: IMPACT_DOMAINS.TRUST,
|
|
40
|
+
checkout: IMPACT_DOMAINS.REVENUE,
|
|
41
|
+
|
|
42
|
+
// Flows
|
|
43
|
+
signup_flow: IMPACT_DOMAINS.LEAD,
|
|
44
|
+
login_flow: IMPACT_DOMAINS.TRUST,
|
|
45
|
+
checkout_flow: IMPACT_DOMAINS.REVENUE
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return domainMap[attemptId] || IMPACT_DOMAINS.UX;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Classify a failure by type
|
|
53
|
+
* @param {Object} failure - { error, outcome, friction, validators, failed_step, visualDiff, behavioralSignals }
|
|
54
|
+
* @returns {string} BREAK_TYPE
|
|
55
|
+
*/
|
|
56
|
+
function classifyBreakType(failure) {
|
|
57
|
+
const { error, outcome, friction, validators, failedStep, lastStep, visualDiff, behavioralSignals } = failure;
|
|
58
|
+
const errorMsg = (error || '').toLowerCase();
|
|
59
|
+
|
|
60
|
+
// Phase 5: Visual diff detected (regression)
|
|
61
|
+
if (visualDiff && visualDiff.hasDiff) {
|
|
62
|
+
return BREAK_TYPES.VISUAL;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Phase 5: Behavioral signals (missing/disabled/hidden elements)
|
|
66
|
+
if (behavioralSignals) {
|
|
67
|
+
const signals = Array.isArray(behavioralSignals) ? behavioralSignals : [behavioralSignals];
|
|
68
|
+
// Check for visual signals (hidden/missing elements) - from both signal and type properties
|
|
69
|
+
if (signals.some(s =>
|
|
70
|
+
(s.signal === 'ELEMENT_MISSING' || s.signal === 'OFFSCREEN_ELEMENT' || s.signal === 'CTA_HIDDEN') ||
|
|
71
|
+
(s.type === 'ELEMENT_VISIBILITY' && (s.status === 'HIDDEN' || s.status === 'OFFSCREEN')) ||
|
|
72
|
+
(s.type === 'LAYOUT_SHIFT' && s.status === 'DETECTED') ||
|
|
73
|
+
(s.type === 'STYLE_CHANGE' && s.status === 'CHANGED')
|
|
74
|
+
)) {
|
|
75
|
+
return BREAK_TYPES.VISUAL;
|
|
76
|
+
}
|
|
77
|
+
// Check for accessibility signals (disabled elements) - from both signal and type properties
|
|
78
|
+
if (signals.some(s =>
|
|
79
|
+
(s.signal === 'DISABLED_ELEMENT' || s.signal === 'CTA_DISABLED') ||
|
|
80
|
+
(s.type === 'CTA_ACCESSIBILITY' && (s.status === 'DISABLED' || s.status === 'HIDDEN'))
|
|
81
|
+
)) {
|
|
82
|
+
return BREAK_TYPES.VALIDATION;
|
|
83
|
+
}
|
|
84
|
+
// Any behavioral signal triggers at least a visual concern
|
|
85
|
+
if (signals.length > 0) {
|
|
86
|
+
return BREAK_TYPES.VISUAL;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Timeout (check before network since timeout can contain "timeout")
|
|
91
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('waitfor')) {
|
|
92
|
+
return BREAK_TYPES.TIMEOUT;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Navigation failures
|
|
96
|
+
if (errorMsg.includes('navigation') || errorMsg.includes('goto') || errorMsg.includes('redirect')) {
|
|
97
|
+
return BREAK_TYPES.NAVIGATION;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Submission failures (check before form, focus on submit action)
|
|
101
|
+
if (errorMsg.includes('submit') || (lastStep && lastStep.action === 'click' && lastStep.selector && lastStep.selector.includes('submit'))) {
|
|
102
|
+
return BREAK_TYPES.SUBMISSION;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Visual/element failures (check before generic validator)
|
|
106
|
+
if (errorMsg.includes('screenshot') || errorMsg.includes('visual') || errorMsg.includes('selector') || errorMsg.includes('visible') || errorMsg.includes('element not found')) {
|
|
107
|
+
return BREAK_TYPES.VISUAL;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validator failures
|
|
111
|
+
if (validators && Array.isArray(validators) && validators.some(v => v.status === 'FAIL')) {
|
|
112
|
+
return BREAK_TYPES.VALIDATION;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Form-related failures
|
|
116
|
+
if (errorMsg.includes('form')) {
|
|
117
|
+
return BREAK_TYPES.SUBMISSION;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Console errors
|
|
121
|
+
if (errorMsg.includes('console') || errorMsg.includes('error logged')) {
|
|
122
|
+
return BREAK_TYPES.CONSOLE;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Network errors (default catch-all for connection issues)
|
|
126
|
+
if (errorMsg.includes('network') || errorMsg.includes('refused') || errorMsg.includes('connection')) {
|
|
127
|
+
return BREAK_TYPES.NETWORK;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Default
|
|
131
|
+
return BREAK_TYPES.VALIDATION;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Determine severity based on domain and break type
|
|
136
|
+
* @param {string} domain - IMPACT_DOMAIN
|
|
137
|
+
* @param {string} breakType - BREAK_TYPE
|
|
138
|
+
* @param {boolean} isFlow - true if this is a flow (higher severity)
|
|
139
|
+
* @returns {string} SEVERITY_LEVEL
|
|
140
|
+
*/
|
|
141
|
+
function determineSeverity(domain, breakType, isFlow = false) {
|
|
142
|
+
const baseScore = {
|
|
143
|
+
[IMPACT_DOMAINS.REVENUE]: 80,
|
|
144
|
+
[IMPACT_DOMAINS.LEAD]: 60,
|
|
145
|
+
[IMPACT_DOMAINS.TRUST]: 55,
|
|
146
|
+
[IMPACT_DOMAINS.UX]: 30
|
|
147
|
+
}[domain] || 30;
|
|
148
|
+
|
|
149
|
+
// Flows are inherently more critical than attempts
|
|
150
|
+
const flowBonus = isFlow ? 20 : 0;
|
|
151
|
+
|
|
152
|
+
// TIMEOUT and NETWORK are critical
|
|
153
|
+
const typeBonus = (breakType === BREAK_TYPES.TIMEOUT || breakType === BREAK_TYPES.NETWORK) ? 15 : 0;
|
|
154
|
+
|
|
155
|
+
const score = baseScore + flowBonus + typeBonus;
|
|
156
|
+
|
|
157
|
+
if (score >= 75) return SEVERITY_LEVELS.CRITICAL;
|
|
158
|
+
if (score >= 45) return SEVERITY_LEVELS.WARNING;
|
|
159
|
+
return SEVERITY_LEVELS.INFO;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
BREAK_TYPES,
|
|
164
|
+
IMPACT_DOMAINS,
|
|
165
|
+
SEVERITY_LEVELS,
|
|
166
|
+
getImpactDomain,
|
|
167
|
+
classifyBreakType,
|
|
168
|
+
determineSeverity
|
|
169
|
+
};
|