@odavl/guardian 0.1.0-rc1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -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}">${attempt.outcome || 'UNKNOWN'}</span>
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
+ };