@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.
- package/CHANGELOG.md +146 -0
- package/README.md +155 -97
- package/bin/guardian.js +1544 -55
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +26 -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 +587 -12
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +85 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +50 -8
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +38 -0
- package/src/guardian/cli-summary.js +167 -67
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -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 +54 -0
- package/src/guardian/flag-validator.js +111 -0
- package/src/guardian/flow-executor.js +309 -44
- 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/language-detection.js +99 -0
- 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 +357 -82
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +27 -18
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +1612 -115
- 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/run-summary.js +20 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +201 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +69 -3
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +181 -0
- 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,25 @@ 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';
|
|
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} ${
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
307
|
+
<div class="details">${attemptDetails}</div>
|
|
308
|
+
</div>
|
|
309
|
+
</details>
|
|
208
310
|
|
|
209
311
|
${flows.length ? `
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
<
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
270
|
-
|
|
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
|
+
};
|