@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,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
+ };