@odavl/guardian 0.2.0 → 1.0.0

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 (84) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1345 -60
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +21 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +568 -7
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +24 -0
  21. package/src/guardian/baseline.js +12 -4
  22. package/src/guardian/breakage-intelligence.js +1 -0
  23. package/src/guardian/ci-cli.js +121 -0
  24. package/src/guardian/ci-output.js +4 -3
  25. package/src/guardian/cli-summary.js +79 -92
  26. package/src/guardian/config-loader.js +162 -0
  27. package/src/guardian/drift-detector.js +100 -0
  28. package/src/guardian/enhanced-html-reporter.js +221 -4
  29. package/src/guardian/env-guard.js +127 -0
  30. package/src/guardian/failure-intelligence.js +173 -0
  31. package/src/guardian/first-run-profile.js +89 -0
  32. package/src/guardian/first-run.js +6 -1
  33. package/src/guardian/flag-validator.js +17 -3
  34. package/src/guardian/html-reporter.js +2 -0
  35. package/src/guardian/human-reporter.js +431 -0
  36. package/src/guardian/index.js +22 -19
  37. package/src/guardian/init-command.js +9 -5
  38. package/src/guardian/intent-detector.js +146 -0
  39. package/src/guardian/journey-definitions.js +132 -0
  40. package/src/guardian/journey-scan-cli.js +145 -0
  41. package/src/guardian/journey-scanner.js +583 -0
  42. package/src/guardian/junit-reporter.js +18 -1
  43. package/src/guardian/live-cli.js +95 -0
  44. package/src/guardian/live-scheduler-runner.js +137 -0
  45. package/src/guardian/live-scheduler.js +146 -0
  46. package/src/guardian/market-reporter.js +341 -81
  47. package/src/guardian/pattern-analyzer.js +348 -0
  48. package/src/guardian/policy.js +80 -3
  49. package/src/guardian/preset-loader.js +9 -6
  50. package/src/guardian/reality.js +1278 -117
  51. package/src/guardian/reporter.js +27 -41
  52. package/src/guardian/run-artifacts.js +212 -0
  53. package/src/guardian/run-cleanup.js +207 -0
  54. package/src/guardian/run-latest.js +90 -0
  55. package/src/guardian/run-list.js +211 -0
  56. package/src/guardian/scan-presets.js +100 -11
  57. package/src/guardian/selector-fallbacks.js +394 -0
  58. package/src/guardian/semantic-contact-finder.js +2 -1
  59. package/src/guardian/site-introspection.js +257 -0
  60. package/src/guardian/smoke.js +2 -2
  61. package/src/guardian/snapshot-schema.js +25 -1
  62. package/src/guardian/snapshot.js +46 -2
  63. package/src/guardian/stability-scorer.js +169 -0
  64. package/src/guardian/template-command.js +184 -0
  65. package/src/guardian/text-formatters.js +426 -0
  66. package/src/guardian/verdict.js +320 -0
  67. package/src/guardian/verdicts.js +74 -0
  68. package/src/guardian/watch-runner.js +3 -7
  69. package/src/payments/stripe-checkout.js +169 -0
  70. package/src/plans/plan-definitions.js +148 -0
  71. package/src/plans/plan-manager.js +211 -0
  72. package/src/plans/usage-tracker.js +210 -0
  73. package/src/recipes/recipe-engine.js +188 -0
  74. package/src/recipes/recipe-failure-analysis.js +159 -0
  75. package/src/recipes/recipe-registry.js +134 -0
  76. package/src/recipes/recipe-runtime.js +507 -0
  77. package/src/recipes/recipe-store.js +410 -0
  78. package/guardian-contract-v1.md +0 -149
  79. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  80. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  81. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  82. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  83. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  84. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -46,14 +46,21 @@ class MarketReporter {
46
46
 
47
47
  generateHtmlReport(report) {
48
48
  const { summary, results, runId, baseUrl, attemptsRun, timestamp, manualResults = [], autoResults = [], flows = [], flowSummary = { total: 0, success: 0, failure: 0 }, intelligence = {} } = report;
49
- const verdictColor = summary.overallVerdict === 'SUCCESS' ? '#10b981'
50
- : summary.overallVerdict === 'FRICTION' ? '#f59e0b'
49
+ const { toCanonicalVerdict } = require('./verdicts');
50
+ const overallCanonical = toCanonicalVerdict(summary.overallVerdict);
51
+ const verdictColor = overallCanonical === 'READY' ? '#10b981'
52
+ : overallCanonical === 'FRICTION' ? '#f59e0b'
51
53
  : '#ef4444';
52
- const verdictEmoji = summary.overallVerdict === 'SUCCESS' ? '🟢' : summary.overallVerdict === 'FRICTION' ? '🟡' : '🔴';
54
+ const verdictEmoji = overallCanonical === 'READY' ? '🟢' : overallCanonical === 'FRICTION' ? '🟡' : '🔴';
55
+
56
+ // Phase 5: Generate Executive Summary
57
+ const executiveSummary = this._generateExecutiveSummary(intelligence, results, flows);
53
58
 
54
59
  const attemptsRows = results.map((result, idx) => {
55
- const color = result.outcome === 'SUCCESS' ? '#10b981' : result.outcome === 'FRICTION' ? '#f59e0b' : '#ef4444';
56
- const badge = result.outcome === 'SUCCESS' ? '✅ SUCCESS' : result.outcome === 'FRICTION' ? '⚠️ FRICTION' : '❌ FAILURE';
60
+ const { toCanonicalVerdict } = require('./verdicts');
61
+ const canon = toCanonicalVerdict(result.outcome);
62
+ const color = canon === 'READY' ? '#10b981' : canon === 'FRICTION' ? '#f59e0b' : '#ef4444';
63
+ const badge = canon === 'READY' ? '✅ READY' : canon === 'FRICTION' ? '⚠️ FRICTION' : '❌ DO_NOT_LAUNCH';
57
64
  const frictionSignals = result.friction && result.friction.signals ? result.friction.signals : [];
58
65
  const sourceLabel = result.source === 'auto-generated' ? ' 🤖' : '';
59
66
  return `
@@ -119,8 +126,10 @@ class MarketReporter {
119
126
  }).join('');
120
127
 
121
128
  const flowRows = flows.map((flow, idx) => {
122
- const color = flow.outcome === 'SUCCESS' ? '#10b981' : '#ef4444';
123
- const badge = flow.outcome === 'SUCCESS' ? '✅ SUCCESS' : '❌ FAILURE';
129
+ const { toCanonicalVerdict } = require('./verdicts');
130
+ const canon = toCanonicalVerdict(flow.outcome);
131
+ const color = canon === 'READY' ? '#10b981' : canon === 'FRICTION' ? '#f59e0b' : '#ef4444';
132
+ const badge = canon === 'READY' ? '✅ READY' : canon === 'FRICTION' ? '⚠️ FRICTION' : '❌ DO_NOT_LAUNCH';
124
133
  const evalSummary = flow.successEval ? (() => {
125
134
  const reasons = (flow.successEval.reasons || []).slice(0, 3).map(r => `• ${r}`).join('<br/>');
126
135
  const ev = flow.successEval.evidence || {};
@@ -171,7 +180,8 @@ class MarketReporter {
171
180
  .attempt-detail { margin-top: 10px; }
172
181
  .friction-block { margin: 10px 0; }
173
182
  .signal-card { background: #fefce8; border: 1px solid #fde047; border-left: 4px solid #f59e0b; border-radius: 6px; padding: 10px; margin-bottom: 10px; }
174
- .signal-card.severity-low { background: #f0f9ff; border-color: #7dd3fc; border-left-color: #0ea5e9; }
183
+ .signal-card.severity-low { background: #f0f9ff; border-color: #7dd3fc; border-left-color: #0ea5e9; display: none; }
184
+ .show-low-severity .signal-card.severity-low { display: block; }
175
185
  .signal-card.severity-medium { background: #fefce8; border-color: #fde047; border-left-color: #f59e0b; }
176
186
  .signal-card.severity-high { background: #fef2f2; border-color: #fca5a5; border-left-color: #ef4444; }
177
187
  .signal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
@@ -185,13 +195,47 @@ class MarketReporter {
185
195
  .steps-block ol { padding-left: 20px; }
186
196
  .no-friction { color: #16a34a; font-weight: 600; }
187
197
  .meta { display: flex; gap: 18px; margin-top: 10px; color: #e5e7eb; font-size: 0.95em; }
198
+ /* Phase 5: Executive Summary Styles */
199
+ .executive-summary { background: linear-gradient(135deg, #ffffff, #f8fafc); border: 3px solid #3b82f6; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
200
+ .executive-summary h2 { color: #1e40af; margin: 0 0 20px 0; font-size: 28px; border-bottom: 2px solid #3b82f6; padding-bottom: 12px; }
201
+ .exec-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 20px; }
202
+ .exec-card { background: white; border-radius: 8px; padding: 18px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-left: 4px solid #3b82f6; }
203
+ .exec-card h3 { color: #374151; margin: 0 0 12px 0; font-size: 16px; text-transform: uppercase; letter-spacing: 0.5px; }
204
+ .exec-verdict { font-size: 24px; font-weight: bold; margin: 8px 0; }
205
+ .exec-verdict.safe { color: #10b981; }
206
+ .exec-verdict.caution { color: #f59e0b; }
207
+ .exec-verdict.danger { color: #ef4444; }
208
+ .exec-score { font-size: 48px; font-weight: bold; color: #3b82f6; margin: 8px 0; }
209
+ .exec-score-label { color: #6b7280; font-size: 14px; }
210
+ .exec-risk-list { list-style: none; padding: 0; margin: 0; }
211
+ .exec-risk-item { background: #f9fafb; border-left: 3px solid #ef4444; padding: 10px 12px; margin-bottom: 10px; border-radius: 4px; }
212
+ .exec-risk-item .risk-title { font-weight: 600; color: #111827; margin-bottom: 4px; }
213
+ .exec-risk-item .risk-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; text-transform: uppercase; color: white; }
214
+ .exec-risk-item .risk-why-matters { color: #6b7280; font-size: 13px; margin-top: 6px; line-height: 1.4; font-style: italic; }
215
+ .exec-risk-item .risk-badge.CRITICAL { background: #dc2626; }
216
+ .exec-risk-item .risk-badge.WARNING { background: #f59e0b; }
217
+ .exec-risk-item .risk-badge.INFO { background: #3b82f6; }
218
+ .exec-action { background: #eff6ff; border: 2px solid #3b82f6; border-radius: 8px; padding: 16px; margin-top: 16px; }
219
+ .exec-action h3 { color: #1e40af; margin: 0 0 10px 0; font-size: 16px; }
220
+ .exec-action p { color: #1f2937; font-size: 15px; font-weight: 500; margin: 0; line-height: 1.5; }
221
+ /* Phase 5 (Part 3): Noise Reduction - Collapsible Sections */
222
+ .collapsible-section { background: white; border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); overflow: hidden; }
223
+ .collapsible-section[open] { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
224
+ .section-toggle { cursor: pointer; padding: 16px; font-weight: 600; color: #1f2937; display: flex; align-items: center; gap: 8px; user-select: none; font-size: 15px; }
225
+ .section-toggle:hover { background: #f9fafb; }
226
+ .collapsible-content { padding: 16px; border-top: 1px solid #e5e7eb; display: none; }
227
+ .collapsible-section[open] .collapsible-content { display: block; }
228
+ .collapsible-section details { margin-top: 12px; }
229
+ .collapsible-section details summary { cursor: pointer; padding: 8px; font-weight: 500; color: #374151; user-select: none; }
230
+ .collapsible-section details summary:hover { background: #f3f4f6; border-radius: 4px; }
231
+ .collapsible-section table { margin-bottom: 0; }
188
232
  </style>
189
233
  </head>
190
234
  <body>
191
235
  <div class="container">
192
236
  <div class="header">
193
237
  <h1>Market Reality Report</h1>
194
- <div class="verdict">${verdictEmoji} ${summary.overallVerdict}</div>
238
+ <div class="verdict">${verdictEmoji} ${overallCanonical}</div>
195
239
  <div class="meta">
196
240
  <div><strong>Run ID:</strong> ${runId}</div>
197
241
  <div><strong>Base URL:</strong> ${baseUrl}</div>
@@ -202,88 +246,139 @@ class MarketReporter {
202
246
  </div>
203
247
  </div>
204
248
 
205
- <table>
206
- <thead>
207
- <tr>
208
- <th>#</th>
209
- <th>Attempt ID</th>
210
- <th>Name</th>
211
- <th>Outcome</th>
212
- <th>Duration</th>
213
- <th>Friction Signals</th>
214
- <th>Reports</th>
215
- </tr>
216
- </thead>
217
- <tbody>
218
- ${attemptsRows}
219
- </tbody>
220
- </table>
249
+ <!-- Phase 5: Executive Summary -->
250
+ <div class="executive-summary">
251
+ <h2>🧠 Executive Summary</h2>
252
+ <div class="exec-grid">
253
+ <div class="exec-card">
254
+ <h3>Release Verdict</h3>
255
+ <div class="exec-verdict ${executiveSummary.verdictClass}">${executiveSummary.verdict}</div>
256
+ </div>
257
+ <div class="exec-card">
258
+ <h3>Overall Risk Score</h3>
259
+ <div class="exec-score">${executiveSummary.riskScore}</div>
260
+ <div class="exec-score-label">/ 100</div>
261
+ </div>
262
+ </div>
263
+ <div class="exec-card">
264
+ <h3>Top Reason</h3>
265
+ <p style="color: #1f2937; font-size: 15px; margin: 0;">${executiveSummary.topReason}</p>
266
+ </div>
267
+ ${executiveSummary.topRisks.length > 0 ? `
268
+ <div class="exec-card" style="margin-top: 20px;">
269
+ <h3>Top 3 Risks</h3>
270
+ <ul class="exec-risk-list">
271
+ ${executiveSummary.topRisks.map(risk => `
272
+ <li class="exec-risk-item">
273
+ <div class="risk-title">${risk.title}</div>
274
+ <span class="risk-badge ${risk.severity}">${risk.severity}</span>
275
+ <div class="risk-why-matters">Why this matters: ${risk.whyMatters}</div>
276
+ </li>
277
+ `).join('')}
278
+ </ul>
279
+ </div>
280
+ ` : ''}
281
+ <div class="exec-action">
282
+ <h3>📋 Recommended Next Action</h3>
283
+ <p>${executiveSummary.nextAction}</p>
284
+ </div>
285
+ </div>
286
+
287
+ <details class="collapsible-section">
288
+ <summary class="section-toggle">📋 Attempt Details (${results.length} runs)</summary>
289
+ <div class="collapsible-content">
290
+ <table>
291
+ <thead>
292
+ <tr>
293
+ <th>#</th>
294
+ <th>Attempt ID</th>
295
+ <th>Name</th>
296
+ <th>Outcome</th>
297
+ <th>Duration</th>
298
+ <th>Friction Signals</th>
299
+ <th>Reports</th>
300
+ </tr>
301
+ </thead>
302
+ <tbody>
303
+ ${attemptsRows}
304
+ </tbody>
305
+ </table>
221
306
 
222
- <div class="details">${attemptDetails}</div>
307
+ <div class="details">${attemptDetails}</div>
308
+ </div>
309
+ </details>
223
310
 
224
311
  ${flows.length ? `
225
- <h3 style="margin-top:24px;">Intent Flows</h3>
226
- <table>
227
- <thead>
228
- <tr>
229
- <th>#</th>
230
- <th>Flow ID</th>
231
- <th>Name</th>
232
- <th>Outcome</th>
233
- <th>Steps</th>
234
- <th>Error</th>
235
- </tr>
236
- </thead>
237
- <tbody>
238
- ${flowRows}
239
- </tbody>
240
- </table>
312
+ <details class="collapsible-section">
313
+ <summary class="section-toggle">🔄 Intent Flows (${flows.length})</summary>
314
+ <div class="collapsible-content">
315
+ <table>
316
+ <thead>
317
+ <tr>
318
+ <th>#</th>
319
+ <th>Flow ID</th>
320
+ <th>Name</th>
321
+ <th>Outcome</th>
322
+ <th>Steps</th>
323
+ <th>Error</th>
324
+ </tr>
325
+ </thead>
326
+ <tbody>
327
+ ${flowRows}
328
+ </tbody>
329
+ </table>
330
+ </div>
331
+ </details>
241
332
  ` : ''}
242
333
 
243
334
  ${intelligence && intelligence.totalFailures > 0 ? `
244
- <h3 style="margin-top:24px;">🔍 Breakage Intelligence</h3>
245
- <div style="background:#fff; padding:16px; border-radius:10px; margin-top:12px;">
246
- <p><strong>Critical Failures:</strong> ${intelligence.criticalCount} | <strong>Warnings:</strong> ${intelligence.warningCount} | <strong>Info:</strong> ${intelligence.infoCount}</p>
247
- ${intelligence.escalationSignals.length ? `
248
- <div style="background:#fef2f2; border:1px solid #fca5a5; padding:10px; border-radius:6px; margin-top:8px;">
249
- <strong>⚠️ Escalation Signals:</strong>
250
- <ul>
251
- ${intelligence.escalationSignals.map(s => `<li>${s}</li>`).join('')}
252
- </ul>
253
- </div>
254
- ` : ''}
255
- ${intelligence.failures && intelligence.failures.slice(0, 5).map(f => `
256
- <details style="margin-top:10px;">
257
- <summary><strong>${f.name}</strong> — ${f.breakType} (${f.severity})</summary>
258
- <div style="margin-top:8px; padding:8px; background:#f9fafb;">
259
- <p><strong>Primary Hint:</strong> ${f.primaryHint}</p>
260
- <p><strong>Why It Matters:</strong></p>
261
- <ul>${f.whyItMatters.map(w => `<li>${w}</li>`).join('')}</ul>
262
- <p><strong>Top Actions:</strong></p>
263
- <ol>${f.topActions.map(a => `<li>${a}</li>`).join('')}</ol>
264
- ${f.breakType === 'VISUAL' && f.visualDiff ? `
265
- <div style="margin-top:12px; padding:8px; background:#fef3c7; border:1px solid #fbbf24; border-radius:6px;">
266
- <p><strong>📊 Visual Regression Details:</strong></p>
335
+ <details class="collapsible-section" open>
336
+ <summary class="section-toggle">🔍 Breakage Intelligence</summary>
337
+ <div class="collapsible-content">
338
+ <div style="background:#fff; padding:16px; border-radius:10px; margin-top:12px;">
339
+ <p><strong>Critical Failures:</strong> ${intelligence.criticalCount} | <strong>Warnings:</strong> ${intelligence.warningCount} | <strong>Info:</strong> ${intelligence.infoCount}</p>
340
+ ${intelligence.escalationSignals.length ? `
341
+ <div style="background:#fef2f2; border:1px solid #fca5a5; padding:10px; border-radius:6px; margin-top:8px;">
342
+ <strong>⚠️ Escalation Signals:</strong>
267
343
  <ul>
268
- <li><strong>Change Detected:</strong> ${f.visualDiff.hasDiff ? 'YES ⚠️' : 'NO ✅'}</li>
269
- <li><strong>Diff Magnitude:</strong> ${(f.visualDiff.percentChange || 0).toFixed(1)}%</li>
270
- ${f.visualDiff.reason ? `<li><strong>Reason:</strong> ${f.visualDiff.reason}</li>` : ''}
271
- ${f.visualDiff.diffRegions && f.visualDiff.diffRegions.length > 0 ? `<li><strong>Changed Regions:</strong> ${f.visualDiff.diffRegions.join(', ')}</li>` : ''}
272
- </ul>
273
- </div>
274
- ` : ''}
275
- ${f.behavioralSignals ? `
276
- <div style="margin-top:12px; padding:8px; background:#e0f2fe; border:1px solid #06b6d4; border-radius:6px;">
277
- <p><strong>🎯 Behavioral Signals:</strong></p>
278
- <ul>
279
- ${f.behavioralSignals.map(sig => `<li><strong>${sig.type}:</strong> ${sig.status === 'VISIBLE' ? '✅' : '❌'} ${sig.description}</li>`).join('')}
344
+ ${intelligence.escalationSignals.map(s => `<li>${s}</li>`).join('')}
280
345
  </ul>
281
346
  </div>
282
347
  ` : ''}
348
+ ${intelligence.failures && intelligence.failures.slice(0, 5).map(f => `
349
+ <details style="margin-top:10px;">
350
+ <summary><strong>${f.name}</strong> — ${f.breakType} (${f.severity})</summary>
351
+ <div style="margin-top:8px; padding:8px; background:#f9fafb;">
352
+ <p><strong>Primary Hint:</strong> ${f.primaryHint}</p>
353
+ <p><strong>Why It Matters:</strong></p>
354
+ <ul>${f.whyItMatters.map(w => `<li>${w}</li>`).join('')}</ul>
355
+ <p><strong>Top Actions:</strong></p>
356
+ <ol>${f.topActions.map(a => `<li>${a}</li>`).join('')}</ol>
357
+ ${f.breakType === 'VISUAL' && f.visualDiff ? `
358
+ <div style="margin-top:12px; padding:8px; background:#fef3c7; border:1px solid #fbbf24; border-radius:6px;">
359
+ <p><strong>📊 Visual Regression Details:</strong></p>
360
+ <ul>
361
+ <li><strong>Change Detected:</strong> ${f.visualDiff.hasDiff ? 'YES ⚠️' : 'NO ✅'}</li>
362
+ <li><strong>Diff Magnitude:</strong> ${(f.visualDiff.percentChange || 0).toFixed(1)}%</li>
363
+ ${f.visualDiff.reason ? `<li><strong>Reason:</strong> ${f.visualDiff.reason}</li>` : ''}
364
+ ${f.visualDiff.diffRegions && f.visualDiff.diffRegions.length > 0 ? `<li><strong>Changed Regions:</strong> ${f.visualDiff.diffRegions.join(', ')}</li>` : ''}
365
+ </ul>
366
+ </div>
367
+ ` : ''}
368
+ ${f.behavioralSignals ? `
369
+ <div style="margin-top:12px; padding:8px; background:#e0f2fe; border:1px solid #06b6d4; border-radius:6px;">
370
+ <p><strong>🎯 Behavioral Signals:</strong></p>
371
+ <ul>
372
+ ${f.behavioralSignals.map(sig => `<li><strong>${sig.type}:</strong> ${sig.status === 'VISIBLE' ? '✅' : '❌'} ${sig.description}</li>`).join('')}
373
+ </ul>
374
+ </div>
375
+ ` : ''}
376
+ </div>
377
+ </details>
378
+ `).join('')}
283
379
  </div>
284
- </details>
285
- `).join('')}
286
- </div>
380
+ </div>
381
+ </details>
287
382
  ` : ''}
288
383
  </div>
289
384
  </body>
@@ -296,6 +391,169 @@ class MarketReporter {
296
391
  return htmlPath;
297
392
  }
298
393
 
394
+ /**
395
+ * Phase 5 (Part 2): Generate human-readable business impact explanation
396
+ * @private
397
+ */
398
+ _generateWhyMattersExplanation(breakType, domain) {
399
+ // Priority 1: Break type specific impacts
400
+ if (breakType === 'SUBMISSION') {
401
+ return 'Users cannot complete forms or checkout, causing direct revenue or lead loss.';
402
+ }
403
+ if (breakType === 'NAVIGATION') {
404
+ return 'Users may get stuck or lost, increasing abandonment rates.';
405
+ }
406
+ if (breakType === 'TIMEOUT') {
407
+ return 'Slow or unresponsive pages reduce trust and increase bounce rate.';
408
+ }
409
+ if (breakType === 'VISUAL') {
410
+ return 'Broken UI elements reduce credibility and user confidence.';
411
+ }
412
+ if (breakType === 'NETWORK') {
413
+ return 'Environment issues may break the site only in production.';
414
+ }
415
+ if (breakType === 'CONSOLE') {
416
+ return 'JavaScript errors may degrade functionality and user experience.';
417
+ }
418
+ if (breakType === 'VALIDATION') {
419
+ return 'Form validation failures prevent users from completing critical actions.';
420
+ }
421
+
422
+ // Priority 2: Domain-specific fallbacks
423
+ if (domain === 'REVENUE') {
424
+ return 'Critical checkout or payment issues directly impact business revenue.';
425
+ }
426
+ if (domain === 'LEAD') {
427
+ return 'Signup or contact form issues prevent capturing customer interest.';
428
+ }
429
+ if (domain === 'TRUST') {
430
+ return 'Authentication or security issues erode user trust and safety.';
431
+ }
432
+ if (domain === 'UX') {
433
+ return 'User experience degradation may increase frustration and churn.';
434
+ }
435
+
436
+ // Default fallback
437
+ return 'Critical functionality may be broken, impacting user satisfaction.';
438
+ }
439
+
440
+ /**
441
+ * Phase 5 (Part 3): Group similar issues by severity and type
442
+ * @private
443
+ */
444
+ _groupSimilarIssues(failures) {
445
+ const groups = {};
446
+
447
+ for (const failure of failures) {
448
+ const severity = failure.severity || 'INFO';
449
+ const breakType = failure.breakType || 'UNKNOWN';
450
+ const key = `${severity}_${breakType}`;
451
+
452
+ if (!groups[key]) {
453
+ groups[key] = [];
454
+ }
455
+ groups[key].push(failure);
456
+ }
457
+
458
+ return groups;
459
+ }
460
+
461
+ /**
462
+ * Phase 5: Generate Executive Summary data
463
+ * @private
464
+ */
465
+ _generateExecutiveSummary(intelligence, results, flows) {
466
+ const failures = intelligence && intelligence.failures ? intelligence.failures : [];
467
+
468
+ // Collect all failures with severity
469
+ const allFailures = [...failures];
470
+
471
+ // Count by severity
472
+ const criticalCount = allFailures.filter(f => f.severity === 'CRITICAL').length;
473
+ const warningCount = allFailures.filter(f => f.severity === 'WARNING').length;
474
+ const infoCount = allFailures.filter(f => f.severity === 'INFO').length;
475
+
476
+ // 1) Release Verdict
477
+ let verdict = '🟢 SAFE TO RELEASE';
478
+ let verdictClass = 'safe';
479
+
480
+ if (criticalCount > 0) {
481
+ verdict = '🔴 DO NOT RELEASE';
482
+ verdictClass = 'danger';
483
+ } else if (warningCount > 0) {
484
+ verdict = '🟡 RELEASE WITH CAUTION';
485
+ verdictClass = 'caution';
486
+ }
487
+
488
+ // 2) Risk Score (0-100)
489
+ // Use severity weights: CRITICAL=40, WARNING=15, INFO=5
490
+ const rawScore = (criticalCount * 40) + (warningCount * 15) + (infoCount * 5);
491
+ const riskScore = Math.min(100, rawScore);
492
+
493
+ // 3) Top Reason
494
+ let topReason = 'No blocking risks detected';
495
+ if (allFailures.length > 0) {
496
+ // Sort by severity priority: CRITICAL > WARNING > INFO
497
+ const sortedFailures = [...allFailures].sort((a, b) => {
498
+ const severityOrder = { CRITICAL: 3, WARNING: 2, INFO: 1 };
499
+ return (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
500
+ });
501
+ topReason = sortedFailures[0].name || sortedFailures[0].primaryHint || 'Failure detected';
502
+ }
503
+
504
+ // 4) Top 3 Risks
505
+ const topRisks = allFailures
506
+ .sort((a, b) => {
507
+ const severityOrder = { CRITICAL: 3, WARNING: 2, INFO: 1 };
508
+ return (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
509
+ })
510
+ .slice(0, 3)
511
+ .map(f => ({
512
+ title: f.name || f.primaryHint || 'Unknown issue',
513
+ severity: f.severity || 'INFO',
514
+ whyMatters: this._generateWhyMattersExplanation(f.breakType, f.domain)
515
+ }));
516
+
517
+ // 5) Recommended Next Action
518
+ let nextAction = 'All systems nominal. Ready to deploy.';
519
+
520
+ if (allFailures.length > 0) {
521
+ const topFailure = allFailures.sort((a, b) => {
522
+ const severityOrder = { CRITICAL: 3, WARNING: 2, INFO: 1 };
523
+ return (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
524
+ })[0];
525
+
526
+ // Generate action based on break type and domain
527
+ const breakType = topFailure.breakType || '';
528
+ const domain = topFailure.domain || '';
529
+
530
+ if (breakType === 'SUBMISSION' || domain === 'REVENUE') {
531
+ nextAction = 'Fix checkout/submission flow before release — revenue impact detected.';
532
+ } else if (breakType === 'VISUAL') {
533
+ nextAction = 'Review visual regression and save a new baseline if changes are intentional.';
534
+ } else if (breakType === 'NAVIGATION' || breakType === 'NETWORK') {
535
+ nextAction = 'Verify deployment environment configuration and API availability.';
536
+ } else if (breakType === 'TIMEOUT') {
537
+ nextAction = 'Investigate performance issues and optimize slow endpoints.';
538
+ } else if (domain === 'LEAD') {
539
+ nextAction = 'Fix signup/contact form before release — lead generation impacted.';
540
+ } else if (domain === 'TRUST') {
541
+ nextAction = 'Fix authentication/login flow before release — user trust at risk.';
542
+ } else {
543
+ nextAction = `Address ${topFailure.severity || 'high-priority'} issue: ${topFailure.primaryHint || topFailure.name || 'detected failure'}.`;
544
+ }
545
+ }
546
+
547
+ return {
548
+ verdict,
549
+ verdictClass,
550
+ riskScore,
551
+ topReason,
552
+ topRisks,
553
+ nextAction
554
+ };
555
+ }
556
+
299
557
  _buildSummary(results) {
300
558
  const successCount = results.filter(r => r.outcome === 'SUCCESS').length;
301
559
  const frictionCount = results.filter(r => r.outcome === 'FRICTION').length;
@@ -307,12 +565,14 @@ class MarketReporter {
307
565
  } else if (frictionCount > 0) {
308
566
  overallVerdict = 'FRICTION';
309
567
  }
568
+ const { toCanonicalVerdict } = require('./verdicts');
569
+ const overallCanonical = toCanonicalVerdict(overallVerdict);
310
570
 
311
571
  return {
312
572
  successCount,
313
573
  frictionCount,
314
574
  failureCount,
315
- overallVerdict
575
+ overallVerdict: overallCanonical
316
576
  };
317
577
  }
318
578
  }