@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.
- package/CHANGELOG.md +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +6 -1
- package/src/guardian/flag-validator.js +17 -3
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /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
|
|
50
|
-
|
|
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 =
|
|
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
|
|
56
|
-
const
|
|
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
|
|
123
|
-
const
|
|
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} ${
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
307
|
+
<div class="details">${attemptDetails}</div>
|
|
308
|
+
</div>
|
|
309
|
+
</details>
|
|
223
310
|
|
|
224
311
|
${flows.length ? `
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
<
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
285
|
-
|
|
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
|
}
|