@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
|
@@ -12,11 +12,38 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const { analyzePatterns, loadRecentRunsForSite } = require('./pattern-analyzer');
|
|
16
|
+
const {
|
|
17
|
+
formatVerdictStatus,
|
|
18
|
+
formatConfidence,
|
|
19
|
+
formatVerdictWhy,
|
|
20
|
+
formatKeyFindings,
|
|
21
|
+
formatLimits,
|
|
22
|
+
formatConfidenceMicroLine,
|
|
23
|
+
formatFirstRunNote,
|
|
24
|
+
formatJourneyMessage,
|
|
25
|
+
formatNextRunHint,
|
|
26
|
+
formatPatternSummary,
|
|
27
|
+
formatPatternWhy,
|
|
28
|
+
formatPatternFocus,
|
|
29
|
+
formatPatternLimits,
|
|
30
|
+
formatConfidenceDrivers,
|
|
31
|
+
formatFocusSummary,
|
|
32
|
+
formatDeltaInsight,
|
|
33
|
+
// Stage V / Step 5.2: Silence Discipline helpers
|
|
34
|
+
shouldRenderFocusSummary,
|
|
35
|
+
shouldRenderDeltaInsight,
|
|
36
|
+
shouldRenderPatterns,
|
|
37
|
+
shouldRenderConfidenceDrivers,
|
|
38
|
+
shouldRenderJourneyMessage,
|
|
39
|
+
shouldRenderNextRunHint,
|
|
40
|
+
shouldRenderFirstRunNote
|
|
41
|
+
} = require('./text-formatters');
|
|
15
42
|
|
|
16
43
|
/**
|
|
17
44
|
* Generate enhanced HTML report
|
|
18
45
|
*/
|
|
19
|
-
function generateEnhancedHtml(snapshot, outputDir) {
|
|
46
|
+
function generateEnhancedHtml(snapshot, outputDir, options = {}) {
|
|
20
47
|
if (!snapshot) {
|
|
21
48
|
return '<html><body><h1>No snapshot data</h1></body></html>';
|
|
22
49
|
}
|
|
@@ -51,12 +78,24 @@ function generateEnhancedHtml(snapshot, outputDir) {
|
|
|
51
78
|
.meta { color: #7f8c8d; margin-bottom: 30px; }
|
|
52
79
|
.meta span { display: inline-block; margin-right: 20px; }
|
|
53
80
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
81
|
+
.verdict-card { background: #ffffff; border: 2px solid #3498db; padding: 16px; border-radius: 8px; margin: 20px 0; }
|
|
82
|
+
.verdict-title { font-weight: 600; font-size: 18px; color: #2c3e50; margin-bottom: 8px; }
|
|
83
|
+
.verdict-item { margin: 4px 0; }
|
|
84
|
+
.bullets { margin-top: 8px; padding-left: 18px; }
|
|
54
85
|
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; }
|
|
55
86
|
.stat-card.critical { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
56
87
|
.stat-card.warning { background: linear-gradient(135deg, #fad961 0%, #f76b1c 100%); }
|
|
57
88
|
.stat-card.info { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333; }
|
|
58
89
|
.stat-number { font-size: 48px; font-weight: bold; margin-bottom: 10px; }
|
|
59
90
|
.stat-label { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; opacity: 0.9; }
|
|
91
|
+
.pattern-item { background: #f9f9f9; border-left: 4px solid #9b59b6; padding: 15px; margin-bottom: 15px; border-radius: 4px; }
|
|
92
|
+
.pattern-item.high { border-left-color: #e74c3c; }
|
|
93
|
+
.pattern-item.medium { border-left-color: #f39c12; }
|
|
94
|
+
.pattern-item.low { border-left-color: #95a5a6; }
|
|
95
|
+
.pattern-summary { font-weight: 600; margin-bottom: 8px; }
|
|
96
|
+
.pattern-why { color: #7f8c8d; font-size: 14px; margin-bottom: 8px; }
|
|
97
|
+
.pattern-limits { color: #95a5a6; font-size: 13px; font-style: italic; }
|
|
98
|
+
.pattern-focus { color: #2c3e50; font-size: 14px; margin-bottom: 6px; }
|
|
60
99
|
.risk-item { background: #fff; border-left: 4px solid #e74c3c; padding: 15px; margin-bottom: 15px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
61
100
|
.risk-item.warning { border-left-color: #f39c12; }
|
|
62
101
|
.risk-item.info { border-left-color: #3498db; }
|
|
@@ -99,6 +138,179 @@ function generateEnhancedHtml(snapshot, outputDir) {
|
|
|
99
138
|
<span><strong>Date:</strong> ${meta.createdAt || 'Unknown'}</span>
|
|
100
139
|
</div>
|
|
101
140
|
|
|
141
|
+
<!-- Verdict & Confidence -->
|
|
142
|
+
<div class="verdict-card">
|
|
143
|
+
<div class="verdict-title">Verdict & Confidence</div>
|
|
144
|
+
${(() => {
|
|
145
|
+
const v = snapshot.verdict || meta.verdict || null;
|
|
146
|
+
if (!v) return '<div class="verdict-item">No verdict available</div>';
|
|
147
|
+
// First-run context detection
|
|
148
|
+
let firstRunLine = '';
|
|
149
|
+
var journeyLineHtml = '';
|
|
150
|
+
let priorRuns = 0; // Declare in outer scope for use in drivers logic
|
|
151
|
+
try {
|
|
152
|
+
const artifactsDir = options.artifactsDir;
|
|
153
|
+
const siteSlug = options.siteSlug || (meta.siteSlug);
|
|
154
|
+
if (artifactsDir && siteSlug) {
|
|
155
|
+
const runs = loadRecentRunsForSite(artifactsDir, siteSlug, 10);
|
|
156
|
+
priorRuns = runs.length;
|
|
157
|
+
}
|
|
158
|
+
const runIndex = priorRuns;
|
|
159
|
+
if (shouldRenderFirstRunNote(runIndex)) {
|
|
160
|
+
firstRunLine = `<div class=\"verdict-item\"><em>${formatFirstRunNote()}</em></div>`;
|
|
161
|
+
}
|
|
162
|
+
// Confidence interpretation micro-line (Stage IV)
|
|
163
|
+
const cfLevel = (v.confidence || {}).level;
|
|
164
|
+
const showMicro = ((cfLevel && cfLevel !== 'high') || runIndex < 2);
|
|
165
|
+
var confidenceLineHtml = showMicro ? `<div class=\"verdict-item\">${formatConfidenceMicroLine()}</div>` : '';
|
|
166
|
+
|
|
167
|
+
// Three-Runs Journey Messaging (Stage IV)
|
|
168
|
+
try {
|
|
169
|
+
if (artifactsDir && siteSlug) {
|
|
170
|
+
const patterns = analyzePatterns(artifactsDir, siteSlug, 10) || [];
|
|
171
|
+
const patternsPresent = patterns.length > 0;
|
|
172
|
+
if (!patternsPresent) {
|
|
173
|
+
if (shouldRenderJourneyMessage(runIndex)) {
|
|
174
|
+
const journeyMsg = formatJourneyMessage(runIndex);
|
|
175
|
+
if (journeyMsg) {
|
|
176
|
+
journeyLineHtml = `<div class=\"verdict-item\">${journeyMsg}</div>`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (_) {}
|
|
182
|
+
} catch (_) {}
|
|
183
|
+
const vStatus = formatVerdictStatus(v);
|
|
184
|
+
const vConf = formatConfidence(v);
|
|
185
|
+
const vWhy = formatVerdictWhy(v);
|
|
186
|
+
const vFindings = formatKeyFindings(v);
|
|
187
|
+
const vLimits = formatLimits(v);
|
|
188
|
+
const vNextHint = formatNextRunHint(v);
|
|
189
|
+
|
|
190
|
+
// Confidence Drivers Card (Layer 4 / Step 4.2)
|
|
191
|
+
// Stage V / Step 5.2: Use centralized suppression helper
|
|
192
|
+
let vDrivers = [];
|
|
193
|
+
if (shouldRenderConfidenceDrivers(v, priorRuns)) {
|
|
194
|
+
vDrivers = formatConfidenceDrivers(v);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Focus Summary (Layer 5 - Advisor Mode)
|
|
198
|
+
// Stage V / Step 5.2: Use centralized suppression helper
|
|
199
|
+
let vFocus = [];
|
|
200
|
+
try {
|
|
201
|
+
const patterns = options.artifactsDir && options.siteSlug
|
|
202
|
+
? analyzePatterns(options.artifactsDir, options.siteSlug, 10)
|
|
203
|
+
: [];
|
|
204
|
+
if (shouldRenderFocusSummary(v, patterns)) {
|
|
205
|
+
vFocus = formatFocusSummary(v, patterns);
|
|
206
|
+
}
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
|
|
209
|
+
// Delta Insight (Stage V / Step 5.1)
|
|
210
|
+
let deltaImproved = [];
|
|
211
|
+
let deltaRegressed = [];
|
|
212
|
+
try {
|
|
213
|
+
if (options.artifactsDir && options.siteSlug) {
|
|
214
|
+
const runs = loadRecentRunsForSite(options.artifactsDir, options.siteSlug, 10);
|
|
215
|
+
if (runs.length >= 2) {
|
|
216
|
+
const previousRun = runs[1];
|
|
217
|
+
let previousVerdict = null;
|
|
218
|
+
let previousPatterns = [];
|
|
219
|
+
|
|
220
|
+
if (previousRun.snapshotPath) {
|
|
221
|
+
try {
|
|
222
|
+
const prevSnap = JSON.parse(fs.readFileSync(previousRun.snapshotPath, 'utf8'));
|
|
223
|
+
previousVerdict = prevSnap.verdict || prevSnap.meta?.verdict || null;
|
|
224
|
+
} catch (_) {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
previousPatterns = analyzePatterns(options.artifactsDir, options.siteSlug, 10, previousRun.runId) || [];
|
|
229
|
+
} catch (_) {}
|
|
230
|
+
|
|
231
|
+
const detectedPatterns = options.artifactsDir && options.siteSlug
|
|
232
|
+
? analyzePatterns(options.artifactsDir, options.siteSlug, 10)
|
|
233
|
+
: [];
|
|
234
|
+
|
|
235
|
+
const delta = formatDeltaInsight(v, previousVerdict, detectedPatterns, previousPatterns);
|
|
236
|
+
|
|
237
|
+
// Stage V / Step 5.2: Use centralized suppression helper
|
|
238
|
+
if (shouldRenderDeltaInsight(delta)) {
|
|
239
|
+
deltaImproved = delta.improved || [];
|
|
240
|
+
deltaRegressed = delta.regressed || [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
|
|
246
|
+
return `
|
|
247
|
+
<div class="verdict-item"><strong>Verdict:</strong> ${vStatus}</div>
|
|
248
|
+
<div class="verdict-item"><strong>Confidence:</strong> ${vConf}</div>
|
|
249
|
+
${confidenceLineHtml || ''}
|
|
250
|
+
${vWhy ? `<div class="verdict-item"><strong>Why:</strong> ${vWhy}</div>` : ''}
|
|
251
|
+
${vDrivers.length ? `<div class="verdict-item"><strong>Confidence Drivers:</strong>
|
|
252
|
+
<ul class="bullets">${vDrivers.map(d => `<li>${d}</li>`).join('')}</ul>
|
|
253
|
+
</div>` : ''}
|
|
254
|
+
${vFocus.length ? `<div class="verdict-item"><strong>Focus Summary:</strong>
|
|
255
|
+
<ul class="bullets">${vFocus.map(f => `<li>${f}</li>`).join('')}</ul>
|
|
256
|
+
</div>` : ''}
|
|
257
|
+
${(deltaImproved.length || deltaRegressed.length) ? `<div class="verdict-item"><strong>Delta Insight:</strong>
|
|
258
|
+
<ul class="bullets">
|
|
259
|
+
${deltaImproved.map(line => `<li>✅ ${line}</li>`).join('')}
|
|
260
|
+
${deltaRegressed.map(line => `<li>⚠️ ${line}</li>`).join('')}
|
|
261
|
+
</ul>
|
|
262
|
+
</div>` : ''}
|
|
263
|
+
${firstRunLine}
|
|
264
|
+
${journeyLineHtml}
|
|
265
|
+
${vFindings.length ? `<div class="verdict-item"><strong>Key Findings:</strong>
|
|
266
|
+
<ul class="bullets">${vFindings.map(f => `<li>${f}</li>`).join('')}</ul>
|
|
267
|
+
</div>` : ''}
|
|
268
|
+
${vLimits.length ? `<div class="verdict-item"><strong>Limits:</strong>
|
|
269
|
+
<ul class="bullets">${vLimits.map(l => `<li>${l}</li>`).join('')}</ul>
|
|
270
|
+
</div>` : ''}
|
|
271
|
+
${(() => {
|
|
272
|
+
if (shouldRenderNextRunHint(v)) {
|
|
273
|
+
const hint = formatNextRunHint(v);
|
|
274
|
+
return hint ? `<div class="verdict-item"><strong>Next Run Hint:</strong> ${hint}</div>` : '';
|
|
275
|
+
}
|
|
276
|
+
return '';
|
|
277
|
+
})()}
|
|
278
|
+
`;
|
|
279
|
+
})()}
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<!-- Observed Patterns -->
|
|
283
|
+
${(() => {
|
|
284
|
+
if (!options.artifactsDir || !options.siteSlug) return '';
|
|
285
|
+
try {
|
|
286
|
+
const patterns = analyzePatterns(options.artifactsDir, options.siteSlug, 10);
|
|
287
|
+
// Stage V / Step 5.2: Use centralized suppression helper
|
|
288
|
+
if (!shouldRenderPatterns(patterns)) return '';
|
|
289
|
+
return `
|
|
290
|
+
<div style="margin-top: 30px;">
|
|
291
|
+
<h2>🔍 Observed Patterns (Cross-Run Analysis)</h2>
|
|
292
|
+
${patterns.slice(0, 5).map((pattern, idx) => {
|
|
293
|
+
const summary = formatPatternSummary(pattern);
|
|
294
|
+
const why = formatPatternWhy(pattern);
|
|
295
|
+
const focus = formatPatternFocus(pattern);
|
|
296
|
+
const limits = formatPatternLimits(pattern);
|
|
297
|
+
return `
|
|
298
|
+
<div class="pattern-item ${pattern.confidence}">
|
|
299
|
+
<div class="pattern-summary">${idx + 1}. ${summary}</div>
|
|
300
|
+
<div class="pattern-why">${why}</div>
|
|
301
|
+
${focus ? `<div class="pattern-focus">${focus}</div>` : ''}
|
|
302
|
+
${limits ? `<div class="pattern-limits">Limits: ${limits}</div>` : ''}
|
|
303
|
+
</div>
|
|
304
|
+
`;
|
|
305
|
+
}).join('')}
|
|
306
|
+
${patterns.length > 5 ? `<p style="color: #7f8c8d; margin-top: 10px;">... ${patterns.length - 5} more pattern(s) detected.</p>` : ''}
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
})()}
|
|
313
|
+
|
|
102
314
|
<!-- Summary Cards -->
|
|
103
315
|
<div class="summary">
|
|
104
316
|
<div class="stat-card critical">
|
|
@@ -155,12 +367,17 @@ function generateEnhancedHtml(snapshot, outputDir) {
|
|
|
155
367
|
`;
|
|
156
368
|
attempts.forEach(attempt => {
|
|
157
369
|
const outcomeClass = attempt.outcome === 'SUCCESS' ? 'success' : 'failure';
|
|
370
|
+
const outcomeLabel = attempt.outcome === 'SKIPPED' ? 'Not Executed' : (attempt.outcome || 'UNKNOWN');
|
|
371
|
+
const reasonLine = attempt.outcome === 'SKIPPED' && attempt.skipReason ? `<div class="risk-details" style="font-size:12px;color:#7f8c8d;">Reason: ${attempt.skipReason}</div>` : '';
|
|
158
372
|
html += `
|
|
159
373
|
<li class="attempt-item">
|
|
160
374
|
<span class="attempt-name">${attempt.attemptName || attempt.attemptId}</span>
|
|
161
|
-
<span class="attempt-outcome ${outcomeClass}">${
|
|
375
|
+
<span class="attempt-outcome ${outcomeClass}">${outcomeLabel}</span>
|
|
162
376
|
</li>
|
|
163
377
|
`;
|
|
378
|
+
if (reasonLine) {
|
|
379
|
+
html += reasonLine;
|
|
380
|
+
}
|
|
164
381
|
});
|
|
165
382
|
html += `
|
|
166
383
|
</ul>
|
|
@@ -286,8 +503,8 @@ function generateEnhancedHtml(snapshot, outputDir) {
|
|
|
286
503
|
/**
|
|
287
504
|
* Write enhanced HTML report to file
|
|
288
505
|
*/
|
|
289
|
-
function writeEnhancedHtml(snapshot, outputDir) {
|
|
290
|
-
const html = generateEnhancedHtml(snapshot, outputDir);
|
|
506
|
+
function writeEnhancedHtml(snapshot, outputDir, options = {}) {
|
|
507
|
+
const html = generateEnhancedHtml(snapshot, outputDir, options);
|
|
291
508
|
const reportPath = path.join(outputDir, 'report.html');
|
|
292
509
|
|
|
293
510
|
const dir = path.dirname(reportPath);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Readiness Guard
|
|
3
|
+
*
|
|
4
|
+
* Detects missing critical dependencies and fails early with actionable errors.
|
|
5
|
+
* NO stack traces. NO noise. Just the fix.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if Playwright browsers are installed
|
|
14
|
+
*/
|
|
15
|
+
function checkPlaywrightBrowsers() {
|
|
16
|
+
try {
|
|
17
|
+
// Playwright caches browsers under ~/.cache/ms-playwright or similar
|
|
18
|
+
// Quick check: can we require the Playwright package?
|
|
19
|
+
require('playwright');
|
|
20
|
+
// If that works, browsers should be available
|
|
21
|
+
return { ok: true };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: 'Playwright not properly installed or browsers missing',
|
|
26
|
+
fix: 'npx playwright install'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check Node version compatibility
|
|
33
|
+
*/
|
|
34
|
+
function checkNodeVersion() {
|
|
35
|
+
const version = process.version; // e.g., "v18.0.0"
|
|
36
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
37
|
+
if (major < 18) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: `Node.js ${version} is too old (minimum: 18.0.0)`,
|
|
41
|
+
fix: 'Upgrade Node.js to version 18 or later'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check disk space (rough heuristic)
|
|
49
|
+
*/
|
|
50
|
+
function checkDiskSpace() {
|
|
51
|
+
try {
|
|
52
|
+
// Try to write a small temp file
|
|
53
|
+
const tempFile = path.join(require('os').tmpdir(), `.guardian-space-check-${Date.now()}`);
|
|
54
|
+
fs.writeFileSync(tempFile, 'test', 'utf8');
|
|
55
|
+
fs.unlinkSync(tempFile);
|
|
56
|
+
return { ok: true };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: 'Insufficient disk space or write permission denied',
|
|
61
|
+
fix: 'Ensure /tmp (or temp directory) has at least 100MB free'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run all environment checks
|
|
68
|
+
* @returns {object} - { allOk: boolean, issues: array }
|
|
69
|
+
*/
|
|
70
|
+
function checkEnvironment() {
|
|
71
|
+
const checks = [
|
|
72
|
+
{ name: 'Node.js version', check: checkNodeVersion },
|
|
73
|
+
{ name: 'Playwright browsers', check: checkPlaywrightBrowsers },
|
|
74
|
+
{ name: 'Disk space', check: checkDiskSpace }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const issues = [];
|
|
78
|
+
|
|
79
|
+
for (const { name, check } of checks) {
|
|
80
|
+
try {
|
|
81
|
+
const result = check();
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
issues.push({
|
|
84
|
+
name,
|
|
85
|
+
error: result.error,
|
|
86
|
+
fix: result.fix
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
issues.push({
|
|
91
|
+
name,
|
|
92
|
+
error: e.message,
|
|
93
|
+
fix: 'See documentation or contact support'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
allOk: issues.length === 0,
|
|
100
|
+
issues
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Print environment guard error and exit
|
|
106
|
+
*/
|
|
107
|
+
function failWithEnvironmentError(issues) {
|
|
108
|
+
console.error('\n❌ Environment Check Failed\n');
|
|
109
|
+
console.error('Guardian cannot run due to missing or incompatible dependencies:\n');
|
|
110
|
+
|
|
111
|
+
issues.forEach((issue, idx) => {
|
|
112
|
+
console.error(`${idx + 1}. ${issue.name}`);
|
|
113
|
+
console.error(` Error: ${issue.error}`);
|
|
114
|
+
console.error(` Fix: ${issue.fix}`);
|
|
115
|
+
console.error('');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.error('After fixing the above, run Guardian again:\n');
|
|
119
|
+
console.error(' guardian reality --url <your-site-url>\n');
|
|
120
|
+
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
checkEnvironment,
|
|
126
|
+
failWithEnvironmentError
|
|
127
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 12.3: Human Failure Intelligence
|
|
3
|
+
* Deterministic heuristics to explain failures like a human.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const STORE_DIR = path.join(os.homedir(), '.odavl-guardian', 'failures');
|
|
11
|
+
const SIGNATURE_FILE = path.join(STORE_DIR, 'signatures.json');
|
|
12
|
+
|
|
13
|
+
function ensureStore() {
|
|
14
|
+
if (!fs.existsSync(STORE_DIR)) {
|
|
15
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadSignatures() {
|
|
20
|
+
ensureStore();
|
|
21
|
+
if (!fs.existsSync(SIGNATURE_FILE)) {
|
|
22
|
+
return { sites: {} };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(SIGNATURE_FILE, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return { sites: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveSignatures(data) {
|
|
32
|
+
ensureStore();
|
|
33
|
+
fs.writeFileSync(SIGNATURE_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDomain(url) {
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(url);
|
|
40
|
+
return u.hostname;
|
|
41
|
+
} catch {
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function classifyFailureStage(stepIndex, totalSteps, goalIndex, success) {
|
|
47
|
+
if (success) return 'AFTER_GOAL';
|
|
48
|
+
if (typeof goalIndex !== 'number') goalIndex = totalSteps - 1;
|
|
49
|
+
if (stepIndex < goalIndex) return 'BEFORE_GOAL';
|
|
50
|
+
if (stepIndex === goalIndex) return 'AT_GOAL';
|
|
51
|
+
return 'AFTER_GOAL';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function determineCause(step) {
|
|
55
|
+
// Deterministic priority
|
|
56
|
+
// 1) CTA_NOT_FOUND
|
|
57
|
+
// 2) ELEMENT_NOT_FOUND on submit
|
|
58
|
+
// 3) TIMEOUT near navigation
|
|
59
|
+
// 4) INTENT_DRIFT
|
|
60
|
+
const code = step?.errorCode || step?.result?.errorCode || step?.status;
|
|
61
|
+
const tags = step?.tags || step?.result?.tags || [];
|
|
62
|
+
const action = (step?.action || step?.name || '').toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (code === 'CTA_NOT_FOUND' || tags.includes('cta')) {
|
|
65
|
+
return { cause: 'Primary action not visible', hint: 'Make the main signup button visible without scrolling.' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ((code === 'ELEMENT_NOT_FOUND' || code === 'MISSING_ELEMENT') && (tags.includes('submit') || action.includes('submit'))) {
|
|
69
|
+
return { cause: 'Form submission blocked', hint: 'Ensure the submit button exists and is enabled.' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (code === 'TIMEOUT' || (tags.includes('nav') && (code === 'SLOW' || code === 'BLOCKED'))) {
|
|
73
|
+
return { cause: 'Slow or blocked navigation', hint: 'Speed up routing or ensure target page loads reliably.' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (code === 'INTENT_DRIFT' || tags.includes('drift')) {
|
|
77
|
+
return { cause: 'Page no longer matches visitor intent', hint: 'Restore intent-aligned content and CTA on the target page.' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function analyzeFailure(journeyResult) {
|
|
84
|
+
const totalSteps = (journeyResult.executedSteps?.length || 0) + (journeyResult.failedSteps?.length || 0);
|
|
85
|
+
const steps = journeyResult.executedSteps || [];
|
|
86
|
+
const failed = journeyResult.failedSteps || [];
|
|
87
|
+
|
|
88
|
+
let failureStepId = null;
|
|
89
|
+
let failureStepIdx = -1;
|
|
90
|
+
let failureStep = null;
|
|
91
|
+
|
|
92
|
+
if (failed.length > 0) {
|
|
93
|
+
// failedSteps may be an array of IDs; locate the first failing step
|
|
94
|
+
const firstFailId = typeof failed[0] === 'string' ? failed[0] : failed[0]?.id || failed[0];
|
|
95
|
+
failureStepId = firstFailId;
|
|
96
|
+
failureStepIdx = steps.findIndex(s => s.id === firstFailId);
|
|
97
|
+
if (failureStepIdx === -1 && typeof firstFailId === 'number') {
|
|
98
|
+
failureStepIdx = firstFailId;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Otherwise, look for error status in executed steps
|
|
102
|
+
failureStepIdx = steps.findIndex(s => s.status === 'error' || s.status === 'timeout' || s.result?.status === 'error');
|
|
103
|
+
if (failureStepIdx !== -1) failureStepId = steps[failureStepIdx].id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (failureStepIdx === -1) {
|
|
107
|
+
// No explicit failure found; classify after goal if success, else before goal as default
|
|
108
|
+
const stage = classifyFailureStage(totalSteps - 1, totalSteps, totalSteps - 1, journeyResult.goal?.goalReached === true || journeyResult.success === true);
|
|
109
|
+
const causeInfo = { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
|
|
110
|
+
return {
|
|
111
|
+
failureStepId: failureStepId,
|
|
112
|
+
failureStage: stage,
|
|
113
|
+
cause: causeInfo.cause,
|
|
114
|
+
hint: causeInfo.hint,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
failureStep = steps[failureStepIdx] || null;
|
|
119
|
+
const goalReached = journeyResult.goal?.goalReached === true || journeyResult.success === true;
|
|
120
|
+
let stage = 'AFTER_GOAL';
|
|
121
|
+
if (!goalReached) {
|
|
122
|
+
// Default to BEFORE_GOAL when goal not reached unless explicitly marked as goal step
|
|
123
|
+
const goalStepId = journeyResult.goal?.goalStepId;
|
|
124
|
+
if (goalStepId && (failureStep?.id === goalStepId || failureStepIdx === goalStepId)) {
|
|
125
|
+
stage = 'AT_GOAL';
|
|
126
|
+
} else {
|
|
127
|
+
stage = 'BEFORE_GOAL';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const causeInfo = determineCause(failureStep);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
failureStepId: failureStep?.id || failureStepIdx,
|
|
134
|
+
failureStage: stage,
|
|
135
|
+
cause: causeInfo.cause,
|
|
136
|
+
hint: causeInfo.hint,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildSignature(info) {
|
|
141
|
+
return `${info.failureStage}|${info.cause}|${info.failureStepId ?? 'unknown'}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function recordSignature(siteUrl, info) {
|
|
145
|
+
const data = loadSignatures();
|
|
146
|
+
const domain = getDomain(siteUrl);
|
|
147
|
+
if (!data.sites[domain]) data.sites[domain] = { signatures: {} };
|
|
148
|
+
const sig = buildSignature(info);
|
|
149
|
+
const entry = data.sites[domain].signatures[sig] || { count: 0, lastSeen: null };
|
|
150
|
+
entry.count += 1;
|
|
151
|
+
entry.lastSeen = new Date().toISOString();
|
|
152
|
+
data.sites[domain].signatures[sig] = entry;
|
|
153
|
+
saveSignatures(data);
|
|
154
|
+
return { signature: sig, count: entry.count };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getSignatureCount(siteUrl, info) {
|
|
158
|
+
const data = loadSignatures();
|
|
159
|
+
const domain = getDomain(siteUrl);
|
|
160
|
+
const sig = buildSignature(info);
|
|
161
|
+
return data.sites[domain]?.signatures?.[sig]?.count || 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
analyzeFailure,
|
|
166
|
+
determineCause,
|
|
167
|
+
classifyFailureStage,
|
|
168
|
+
recordSignature,
|
|
169
|
+
getSignatureCount,
|
|
170
|
+
buildSignature,
|
|
171
|
+
loadSignatures,
|
|
172
|
+
saveSignatures,
|
|
173
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Run State Tracking
|
|
3
|
+
*
|
|
4
|
+
* Detects if this is the user's first invocation of Guardian
|
|
5
|
+
* Applies conservative "golden path" profile on first run
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const FIRST_RUN_STATE_DIR = path.join(os.homedir(), '.odavl-guardian');
|
|
13
|
+
const FIRST_RUN_MARKER = path.join(FIRST_RUN_STATE_DIR, 'first-run-complete.json');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if this is the user's first run
|
|
17
|
+
*/
|
|
18
|
+
function isFirstRun() {
|
|
19
|
+
return !fs.existsSync(FIRST_RUN_MARKER);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mark first run as complete
|
|
24
|
+
*/
|
|
25
|
+
function markFirstRunComplete() {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(FIRST_RUN_STATE_DIR)) {
|
|
28
|
+
fs.mkdirSync(FIRST_RUN_STATE_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(FIRST_RUN_MARKER, JSON.stringify({
|
|
31
|
+
completedAt: new Date().toISOString(),
|
|
32
|
+
version: require('../../package.json').version
|
|
33
|
+
}), 'utf8');
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Silent fail if we can't write state
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get first-run execution profile
|
|
41
|
+
* Conservative settings for initial scan
|
|
42
|
+
*/
|
|
43
|
+
function getFirstRunProfile() {
|
|
44
|
+
return {
|
|
45
|
+
// Timeouts: more generous for first run
|
|
46
|
+
timeout: 25000, // 25s (vs default 20s)
|
|
47
|
+
// Disable resource-intensive options
|
|
48
|
+
parallel: 1, // Single-threaded
|
|
49
|
+
failFast: false,
|
|
50
|
+
fast: false, // Don't skip for speed
|
|
51
|
+
// Minimal discovery
|
|
52
|
+
enableDiscovery: false,
|
|
53
|
+
enableCrawl: true, // Light crawl only
|
|
54
|
+
maxPages: 10, // Fewer pages
|
|
55
|
+
maxDepth: 2, // Shallower
|
|
56
|
+
// Evidence capture
|
|
57
|
+
enableScreenshots: true,
|
|
58
|
+
enableTrace: false, // Traces can slow things down
|
|
59
|
+
headful: false, // Headless is faster
|
|
60
|
+
// Safety
|
|
61
|
+
includeUniversal: false,
|
|
62
|
+
// CI mode off for first run (more readable output)
|
|
63
|
+
ciMode: false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply first-run profile to config
|
|
69
|
+
* Merges conservative defaults without overwriting user intent on --url
|
|
70
|
+
*/
|
|
71
|
+
function applyFirstRunProfile(userConfig) {
|
|
72
|
+
if (!isFirstRun()) {
|
|
73
|
+
return userConfig; // Not first run; use as-is
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const profile = getFirstRunProfile();
|
|
77
|
+
return {
|
|
78
|
+
...profile,
|
|
79
|
+
...userConfig, // User overrides profile
|
|
80
|
+
baseUrl: userConfig.baseUrl // Preserve required --url
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
isFirstRun,
|
|
86
|
+
markFirstRunComplete,
|
|
87
|
+
getFirstRunProfile,
|
|
88
|
+
applyFirstRunProfile
|
|
89
|
+
};
|