@odavl/guardian 0.1.0-rc1 → 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 (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -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 +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /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,25 @@ 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';
133
+ const evalSummary = flow.successEval ? (() => {
134
+ const reasons = (flow.successEval.reasons || []).slice(0, 3).map(r => `• ${r}`).join('<br/>');
135
+ const ev = flow.successEval.evidence || {};
136
+ const net = Array.isArray(ev.network) ? ev.network : [];
137
+ const primary = net.find(n => (n.method === 'POST' || n.method === 'PUT') && n.status != null) || net[0];
138
+ const reqLine = primary ? (() => { try { const p = new URL(primary.url); return `${primary.method} ${p.pathname} → ${primary.status}`; } catch { return `${primary.method} ${primary.url} → ${primary.status}`; } })() : null;
139
+ const navLine = ev.urlChanged ? 'navigation: changed' : null;
140
+ const formStates = [];
141
+ if (ev.formCleared) formStates.push('cleared');
142
+ if (ev.formDisabled) formStates.push('disabled');
143
+ if (ev.formDisappeared) formStates.push('disappeared');
144
+ const formLine = formStates.length ? `form: ${formStates.join(', ')}` : null;
145
+ const evidences = [reqLine, navLine, formLine].filter(Boolean).map(e => `• ${e}`).join('<br/>' );
146
+ return `<div><strong>Reasons:</strong><br/>${reasons || '—'}<br/><strong>Evidence:</strong><br/>${evidences || '—'}</div>`;
147
+ })() : '';
124
148
  return `
125
149
  <tr>
126
150
  <td>${idx + 1}</td>
@@ -128,7 +152,7 @@ class MarketReporter {
128
152
  <td>${flow.flowName || ''}</td>
129
153
  <td><span class="badge" style="background:${color}">${badge}</span></td>
130
154
  <td>${flow.stepsExecuted || 0}/${flow.stepsTotal || 0}</td>
131
- <td>${flow.error || ''}</td>
155
+ <td>${flow.error || evalSummary || ''}</td>
132
156
  </tr>
133
157
  `;
134
158
  }).join('');
@@ -156,7 +180,8 @@ class MarketReporter {
156
180
  .attempt-detail { margin-top: 10px; }
157
181
  .friction-block { margin: 10px 0; }
158
182
  .signal-card { background: #fefce8; border: 1px solid #fde047; border-left: 4px solid #f59e0b; border-radius: 6px; padding: 10px; margin-bottom: 10px; }
159
- .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; }
160
185
  .signal-card.severity-medium { background: #fefce8; border-color: #fde047; border-left-color: #f59e0b; }
161
186
  .signal-card.severity-high { background: #fef2f2; border-color: #fca5a5; border-left-color: #ef4444; }
162
187
  .signal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
@@ -170,13 +195,47 @@ class MarketReporter {
170
195
  .steps-block ol { padding-left: 20px; }
171
196
  .no-friction { color: #16a34a; font-weight: 600; }
172
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; }
173
232
  </style>
174
233
  </head>
175
234
  <body>
176
235
  <div class="container">
177
236
  <div class="header">
178
237
  <h1>Market Reality Report</h1>
179
- <div class="verdict">${verdictEmoji} ${summary.overallVerdict}</div>
238
+ <div class="verdict">${verdictEmoji} ${overallCanonical}</div>
180
239
  <div class="meta">
181
240
  <div><strong>Run ID:</strong> ${runId}</div>
182
241
  <div><strong>Base URL:</strong> ${baseUrl}</div>
@@ -187,88 +246,139 @@ class MarketReporter {
187
246
  </div>
188
247
  </div>
189
248
 
190
- <table>
191
- <thead>
192
- <tr>
193
- <th>#</th>
194
- <th>Attempt ID</th>
195
- <th>Name</th>
196
- <th>Outcome</th>
197
- <th>Duration</th>
198
- <th>Friction Signals</th>
199
- <th>Reports</th>
200
- </tr>
201
- </thead>
202
- <tbody>
203
- ${attemptsRows}
204
- </tbody>
205
- </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>
206
306
 
207
- <div class="details">${attemptDetails}</div>
307
+ <div class="details">${attemptDetails}</div>
308
+ </div>
309
+ </details>
208
310
 
209
311
  ${flows.length ? `
210
- <h3 style="margin-top:24px;">Intent Flows</h3>
211
- <table>
212
- <thead>
213
- <tr>
214
- <th>#</th>
215
- <th>Flow ID</th>
216
- <th>Name</th>
217
- <th>Outcome</th>
218
- <th>Steps</th>
219
- <th>Error</th>
220
- </tr>
221
- </thead>
222
- <tbody>
223
- ${flowRows}
224
- </tbody>
225
- </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>
226
332
  ` : ''}
227
333
 
228
334
  ${intelligence && intelligence.totalFailures > 0 ? `
229
- <h3 style="margin-top:24px;">🔍 Breakage Intelligence</h3>
230
- <div style="background:#fff; padding:16px; border-radius:10px; margin-top:12px;">
231
- <p><strong>Critical Failures:</strong> ${intelligence.criticalCount} | <strong>Warnings:</strong> ${intelligence.warningCount} | <strong>Info:</strong> ${intelligence.infoCount}</p>
232
- ${intelligence.escalationSignals.length ? `
233
- <div style="background:#fef2f2; border:1px solid #fca5a5; padding:10px; border-radius:6px; margin-top:8px;">
234
- <strong>⚠️ Escalation Signals:</strong>
235
- <ul>
236
- ${intelligence.escalationSignals.map(s => `<li>${s}</li>`).join('')}
237
- </ul>
238
- </div>
239
- ` : ''}
240
- ${intelligence.failures && intelligence.failures.slice(0, 5).map(f => `
241
- <details style="margin-top:10px;">
242
- <summary><strong>${f.name}</strong> — ${f.breakType} (${f.severity})</summary>
243
- <div style="margin-top:8px; padding:8px; background:#f9fafb;">
244
- <p><strong>Primary Hint:</strong> ${f.primaryHint}</p>
245
- <p><strong>Why It Matters:</strong></p>
246
- <ul>${f.whyItMatters.map(w => `<li>${w}</li>`).join('')}</ul>
247
- <p><strong>Top Actions:</strong></p>
248
- <ol>${f.topActions.map(a => `<li>${a}</li>`).join('')}</ol>
249
- ${f.breakType === 'VISUAL' && f.visualDiff ? `
250
- <div style="margin-top:12px; padding:8px; background:#fef3c7; border:1px solid #fbbf24; border-radius:6px;">
251
- <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>
252
343
  <ul>
253
- <li><strong>Change Detected:</strong> ${f.visualDiff.hasDiff ? 'YES ⚠️' : 'NO ✅'}</li>
254
- <li><strong>Diff Magnitude:</strong> ${(f.visualDiff.percentChange || 0).toFixed(1)}%</li>
255
- ${f.visualDiff.reason ? `<li><strong>Reason:</strong> ${f.visualDiff.reason}</li>` : ''}
256
- ${f.visualDiff.diffRegions && f.visualDiff.diffRegions.length > 0 ? `<li><strong>Changed Regions:</strong> ${f.visualDiff.diffRegions.join(', ')}</li>` : ''}
257
- </ul>
258
- </div>
259
- ` : ''}
260
- ${f.behavioralSignals ? `
261
- <div style="margin-top:12px; padding:8px; background:#e0f2fe; border:1px solid #06b6d4; border-radius:6px;">
262
- <p><strong>🎯 Behavioral Signals:</strong></p>
263
- <ul>
264
- ${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('')}
265
345
  </ul>
266
346
  </div>
267
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('')}
268
379
  </div>
269
- </details>
270
- `).join('')}
271
- </div>
380
+ </div>
381
+ </details>
272
382
  ` : ''}
273
383
  </div>
274
384
  </body>
@@ -281,6 +391,169 @@ class MarketReporter {
281
391
  return htmlPath;
282
392
  }
283
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
+
284
557
  _buildSummary(results) {
285
558
  const successCount = results.filter(r => r.outcome === 'SUCCESS').length;
286
559
  const frictionCount = results.filter(r => r.outcome === 'FRICTION').length;
@@ -292,12 +565,14 @@ class MarketReporter {
292
565
  } else if (frictionCount > 0) {
293
566
  overallVerdict = 'FRICTION';
294
567
  }
568
+ const { toCanonicalVerdict } = require('./verdicts');
569
+ const overallCanonical = toCanonicalVerdict(overallVerdict);
295
570
 
296
571
  return {
297
572
  successCount,
298
573
  frictionCount,
299
574
  failureCount,
300
- overallVerdict
575
+ overallVerdict: overallCanonical
301
576
  };
302
577
  }
303
578
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Phase 7.2 - Parallel Executor
3
+ * Controlled parallel execution of attempts with bounded concurrency
4
+ */
5
+
6
+ /**
7
+ * Execute attempts with bounded parallelism
8
+ * @param {Array} attempts - Array of attempts to execute
9
+ * @param {Function} executeAttemptFn - Async function(attempt) => result
10
+ * @param {number} maxConcurrency - Max concurrent attempts (must be >= 1)
11
+ * @param {Object} options - { shouldStop?: Function }
12
+ * @returns {Promise<Array>} Results in original input order
13
+ */
14
+ async function executeParallel(attempts, executeAttemptFn, maxConcurrency = 1, options = {}) {
15
+ if (maxConcurrency < 1) {
16
+ throw new Error('maxConcurrency must be >= 1');
17
+ }
18
+
19
+ // Handle empty queue immediately
20
+ if (!attempts || attempts.length === 0) {
21
+ return [];
22
+ }
23
+
24
+ const results = new Array(attempts.length);
25
+ const queue = attempts.map((attempt, index) => ({ attempt, index }));
26
+ let executing = 0;
27
+ let queueIndex = 0;
28
+ let shouldStop = false;
29
+
30
+ return new Promise((resolve, reject) => {
31
+ const processNext = async () => {
32
+ // Check if we should stop (e.g., fail-fast triggered)
33
+ if (options.shouldStop && options.shouldStop()) {
34
+ shouldStop = true;
35
+ }
36
+
37
+ // If no more work and nothing executing, we're done
38
+ if (queueIndex >= queue.length && executing === 0) {
39
+ resolve(results);
40
+ return;
41
+ }
42
+
43
+ // If we've hit the concurrency limit or have no more items, wait
44
+ if (executing >= maxConcurrency || queueIndex >= queue.length) {
45
+ return;
46
+ }
47
+
48
+ // Get next item from queue
49
+ const { attempt, index } = queue[queueIndex];
50
+ queueIndex++;
51
+ executing++;
52
+
53
+ try {
54
+ // Only execute if we haven't stopped
55
+ if (!shouldStop) {
56
+ results[index] = await executeAttemptFn(attempt);
57
+ } else {
58
+ // Mark as skipped if we stopped
59
+ results[index] = { skipped: true };
60
+ }
61
+ } catch (err) {
62
+ results[index] = { error: err, skipped: false };
63
+ } finally {
64
+ executing--;
65
+ // Process next item(s)
66
+ processNext();
67
+ if (executing < maxConcurrency && queueIndex < queue.length && !shouldStop) {
68
+ processNext();
69
+ }
70
+ }
71
+ };
72
+
73
+ // Start workers
74
+ for (let i = 0; i < Math.min(maxConcurrency, queue.length); i++) {
75
+ processNext();
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Validate parallel concurrency value
82
+ * @param {number|string} value - Value to validate
83
+ * @returns {{ valid: boolean, parallel?: number, error?: string, hint?: string }}
84
+ */
85
+ function validateParallel(value) {
86
+ if (value === undefined || value === null) {
87
+ return { valid: true, parallel: 1 };
88
+ }
89
+
90
+ const num = parseInt(value, 10);
91
+
92
+ // Check for NaN
93
+ if (isNaN(num)) {
94
+ return {
95
+ valid: false,
96
+ error: `Invalid --parallel value: '${value}' (expected integer >= 1)`,
97
+ hint: 'Example: --parallel 2'
98
+ };
99
+ }
100
+
101
+ // Check if less than 1
102
+ if (num < 1) {
103
+ return {
104
+ valid: false,
105
+ error: `Invalid --parallel value: ${num} (must be >= 1)`,
106
+ hint: 'Example: --parallel 2'
107
+ };
108
+ }
109
+
110
+ return { valid: true, parallel: num };
111
+ }
112
+
113
+ module.exports = {
114
+ executeParallel,
115
+ validateParallel
116
+ };