@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
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { executeAttempt } = require('./attempt');
4
4
  const { MarketReporter } = require('./market-reporter');
5
5
  const { getDefaultAttemptIds, getAttemptDefinition, registerAttempt } = require('./attempt-registry');
6
- const { GuardianFlowExecutor } = require('./flow-executor');
6
+ const { GuardianFlowExecutor, validateFlowDefinition } = require('./flow-executor');
7
7
  const { getDefaultFlowIds, getFlowDefinition } = require('./flow-registry');
8
8
  const { GuardianBrowser } = require('./browser');
9
9
  const { GuardianCrawler } = require('./crawler');
@@ -13,23 +13,91 @@ const { buildAutoAttempts } = require('./auto-attempt-builder');
13
13
  const { baselineExists, loadBaseline, saveBaselineAtomic, createBaselineFromSnapshot, compareSnapshots } = require('./baseline-storage');
14
14
  const { analyzeMarketImpact, determineExitCodeFromEscalation } = require('./market-criticality');
15
15
  const { parsePolicyOption } = require('./preset-loader');
16
- const { evaluatePolicy } = require('./policy');
16
+ const { evaluatePolicy, loadPolicy } = require('./policy');
17
17
  const { aggregateIntelligence } = require('./breakage-intelligence');
18
18
  const { writeEnhancedHtml } = require('./enhanced-html-reporter');
19
- const { printCliSummary } = require('./cli-summary');
19
+ const { analyzePatterns, loadRecentRunsForSite } = require('./pattern-analyzer');
20
+ const crypto = require('crypto');
21
+ const {
22
+ formatVerdictStatus,
23
+ formatConfidence,
24
+ formatVerdictWhy,
25
+ formatKeyFindings,
26
+ formatLimits,
27
+ formatPatternSummary,
28
+ formatPatternFocus,
29
+ formatConfidenceDrivers,
30
+ formatFocusSummary,
31
+ formatDeltaInsight,
32
+ // Stage V / Step 5.2: Silence Discipline helpers
33
+ shouldRenderFocusSummary,
34
+ shouldRenderDeltaInsight,
35
+ shouldRenderPatterns,
36
+ shouldRenderConfidenceDrivers
37
+ } = require('./text-formatters');
20
38
  const { sendWebhooks, getWebhookUrl, buildWebhookPayload } = require('./webhook');
39
+ const { findContactOnPage, formatDetectionForReport } = require('./semantic-contact-finder');
40
+ const { formatRunSummary } = require('./run-summary');
41
+ const { isCiMode } = require('./ci-mode');
42
+ const { formatCiSummary, deriveBaselineVerdict } = require('./ci-output');
43
+ const { toCanonicalVerdict, mapExitCodeFromCanonical } = require('./verdicts');
44
+ // Phase 7.1: Performance modes
45
+ const { getTimeoutProfile } = require('./timeout-profiles');
46
+ const { validateAttemptFilter, filterAttempts, filterFlows } = require('./attempts-filter');
47
+ // Phase 7.2: Parallel execution
48
+ const { executeParallel, validateParallel } = require('./parallel-executor');
49
+ // Phase 7.3: Browser reuse
50
+ const { BrowserPool } = require('./browser-pool');
51
+ // Phase 7.4: Smart skips
52
+ const { checkPrerequisites } = require('./prerequisite-checker');
53
+ // Wave 1: Run artifacts naming and metadata
54
+ const { makeRunDirName, makeSiteSlug, writeMetaJson, readMetaJson } = require('./run-artifacts');
55
+ // Wave 2: Latest pointers
56
+ const { updateLatestGlobal, updateLatestBySite } = require('./run-latest');
57
+ // Wave 3: Smart attempt selection
58
+ const { inspectSite, detectProfile } = require('./site-introspection');
59
+ const { filterAttempts: filterAttemptsByRelevance, summarizeIntrospection } = require('./attempt-relevance');
60
+ // Stage II: Golden path
61
+ const { isFirstRun, markFirstRunComplete, applyFirstRunProfile } = require('./first-run-profile');
62
+ const { applyLocalConfig } = require('./config-loader');
21
63
 
22
- function generateRunId(prefix = 'market-run') {
23
- const now = new Date();
24
- const dateStr = now.toISOString().replace(/[:\-]/g, '').substring(0, 15).replace('T', '-');
25
- return `${prefix}-${dateStr}`;
64
+ function applySafeDefaults(config, warn) {
65
+ const updated = { ...config };
66
+ if (!Array.isArray(updated.attempts) || updated.attempts.length === 0) {
67
+ if (warn) warn('No attempts provided; using curated defaults.');
68
+ updated.attempts = getDefaultAttemptIds();
69
+ }
70
+ if (!Array.isArray(updated.flows) || updated.flows.length === 0) {
71
+ if (warn) warn('No flows provided; using curated defaults.');
72
+ updated.flows = getDefaultFlowIds();
73
+ }
74
+ return updated;
26
75
  }
27
76
 
77
+ const EXECUTED_OUTCOMES = new Set(['SUCCESS', 'FAILURE', 'FRICTION', 'DISCOVERY_FAILED']);
78
+ const isExecutedAttempt = (attemptResult) => attemptResult && EXECUTED_OUTCOMES.has(attemptResult.outcome);
79
+ const SKIP_CODES = {
80
+ DISABLED_BY_PRESET: 'DISABLED_BY_PRESET',
81
+ NOT_APPLICABLE: 'NOT_APPLICABLE',
82
+ ENGINE_MISSING: 'ENGINE_MISSING',
83
+ USER_FILTERED: 'USER_FILTERED',
84
+ PREREQ: 'PREREQUISITE_FAILED'
85
+ };
86
+
87
+ // Removed compact decision packet writer for Level 1 singleton decision schema
88
+
28
89
  async function executeReality(config) {
90
+ const baseWarn = (...args) => console.warn(...args);
91
+ const firstRunMode = isFirstRun();
92
+ // Apply first-run profile if needed (conservative defaults)
93
+ const profiledConfig = firstRunMode ? applyFirstRunProfile(config) : config;
94
+ const safeConfig = applySafeDefaults(profiledConfig, baseWarn);
95
+ const runSignals = [];
96
+
29
97
  const {
30
98
  baseUrl,
31
99
  attempts = getDefaultAttemptIds(),
32
- artifactsDir = './artifacts',
100
+ artifactsDir = './.odavlguardian',
33
101
  headful = false,
34
102
  enableTrace = true,
35
103
  enableScreenshots = true,
@@ -47,8 +115,62 @@ async function executeReality(config) {
47
115
  autoAttemptOptions = {},
48
116
  enableFlows = true,
49
117
  flows = getDefaultFlowIds(),
50
- flowOptions = {}
51
- } = config;
118
+ flowOptions = {},
119
+ // Phase 7.1: Performance modes
120
+ timeoutProfile = 'default',
121
+ failFast = false,
122
+ fast = false,
123
+ attemptsFilter = null,
124
+ // Phase 7.2: Parallel execution
125
+ parallel = 1
126
+ } = safeConfig;
127
+
128
+ // Phase 7.1: Validate and apply attempts filter
129
+ let validation = null;
130
+ if (attemptsFilter) {
131
+ validation = validateAttemptFilter(attemptsFilter);
132
+ if (!validation.valid) {
133
+ console.error(`Error: ${validation.error}`);
134
+ if (validation.hint) console.error(`Hint: ${validation.hint}`);
135
+ process.exit(2);
136
+ }
137
+ }
138
+
139
+ // Phase 7.2: Validate parallel value
140
+ const parallelValidation = validateParallel(parallel);
141
+ if (!parallelValidation.valid) {
142
+ console.error(`Error: ${parallelValidation.error}`);
143
+ if (parallelValidation.hint) console.error(`Hint: ${parallelValidation.hint}`);
144
+ process.exit(2);
145
+ }
146
+ const validatedParallel = parallelValidation.parallel || 1;
147
+
148
+ // Phase 7.1: Filter attempts and flows
149
+ let filteredAttempts = attempts;
150
+ let filteredFlows = flows;
151
+ if (attemptsFilter && validation && validation.valid && validation.ids.length > 0) {
152
+ const beforeFilter = Array.isArray(filteredAttempts) ? filteredAttempts.slice() : [];
153
+ filteredAttempts = filterAttempts(attempts, validation.ids);
154
+ filteredFlows = filterFlows(flows, validation.ids);
155
+ const removed = beforeFilter.filter(id => !filteredAttempts.includes(id));
156
+ userFilteredAttempts.push(...removed.map(id => ({ attemptId: id, reason: 'Filtered by --attempts' })));
157
+ if (filteredAttempts.length === 0 && filteredFlows.length === 0) {
158
+ console.error('Error: No matching attempts or flows found after filtering');
159
+ console.error(`Hint: Check your --attempts filter: ${attemptsFilter}`);
160
+ process.exit(2);
161
+ }
162
+ }
163
+
164
+ const requestedAttempts = Array.isArray(filteredAttempts) ? filteredAttempts.slice() : [];
165
+ const disabledByPreset = new Set((config.disabledAttempts || []).map(id => String(id)));
166
+ const enabledRequestedAttempts = requestedAttempts.filter(id => !disabledByPreset.has(String(id)));
167
+ const presetDisabledAttempts = requestedAttempts.filter(id => disabledByPreset.has(String(id)));
168
+ const userFilteredAttempts = [];
169
+ const missingAttempts = [];
170
+
171
+ // Phase 7.1: Resolve timeout profile
172
+ const timeoutProfileConfig = getTimeoutProfile(timeoutProfile);
173
+ const resolvedTimeout = timeout || timeoutProfileConfig.default;
52
174
 
53
175
  // Validate baseUrl
54
176
  try {
@@ -57,15 +179,75 @@ async function executeReality(config) {
57
179
  throw new Error(`Invalid URL: ${baseUrl}`);
58
180
  }
59
181
 
60
- const runId = generateRunId();
61
- const runDir = path.join(artifactsDir, runId);
182
+ // Wave 1: Generate human-readable run directory name
183
+ const startTime = new Date();
184
+ const siteSlug = makeSiteSlug(baseUrl);
185
+ // Use 'default' if no policy specified, otherwise extract preset name
186
+ let policyName = (() => {
187
+ if (!policy) return 'default';
188
+ if (typeof policy === 'string') {
189
+ return policy.startsWith('preset:') ? policy.replace('preset:', '') : policy;
190
+ }
191
+ if (typeof policy === 'object' && policy.id) return policy.id;
192
+ return 'custom';
193
+ })();
194
+ // Result will be determined at the end; use placeholder for now
195
+ let runDirName = makeRunDirName({
196
+ timestamp: startTime,
197
+ url: baseUrl,
198
+ policy: policyName,
199
+ result: 'PENDING'
200
+ });
201
+ let runDir = path.join(artifactsDir, runDirName);
62
202
  fs.mkdirSync(runDir, { recursive: true });
203
+ const runId = runDirName;
204
+ const ciMode = isCiMode();
205
+
206
+ // Print positioning message based on policy
207
+ const isPolicyProtect = policy && ((typeof policy === 'string' && (policy === 'preset:startup' || policy.includes('startup'))) || (typeof policy === 'object' && policy.id === 'startup'));
208
+ if (!ciMode) {
209
+ if (isPolicyProtect) {
210
+ console.log('\nPROTECT MODE: Full market reality test (slower, deeper)');
211
+ } else {
212
+ console.log('\nREALITY MODE: Full market reality snapshot');
213
+ }
214
+ } else {
215
+ if (isPolicyProtect) {
216
+ console.log('PROTECT MODE: Full market reality test');
217
+ } else {
218
+ console.log('REALITY MODE: Full market reality snapshot');
219
+ }
220
+ }
221
+
222
+ // Phase 7.1: Print mode info
223
+ if (!ciMode) {
224
+ const modeLines = [];
225
+ if (fast) modeLines.push('MODE: fast');
226
+ if (failFast) modeLines.push('FAIL-FAST: enabled');
227
+ if (timeoutProfile !== 'default') modeLines.push(`TIMEOUT: ${timeoutProfile}`);
228
+ if (attemptsFilter) modeLines.push(`ATTEMPTS: ${attemptsFilter}`);
229
+ if (modeLines.length > 0) {
230
+ console.log(`\n⚡ ${modeLines.join(' | ')}`);
231
+ }
232
+ }
63
233
 
64
- console.log(`\n🧪 Market Reality Snapshot v1`);
65
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
66
- console.log(`📍 Base URL: ${baseUrl}`);
67
- console.log(`🎯 Attempts: ${attempts.join(', ')}`);
68
- console.log(`📁 Run Dir: ${runDir}`);
234
+ if (ciMode) {
235
+ console.log(`\nCI RUN: Market Reality Snapshot`);
236
+ console.log(`Base URL: ${baseUrl}`);
237
+ console.log(`Attempts: ${filteredAttempts.join(', ')}`);
238
+ console.log(`Run Dir: ${runDir}`);
239
+ } else if (firstRunMode) {
240
+ // Simplified output for first run
241
+ console.log(`\n🚀 Guardian First Run`);
242
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
243
+ console.log(`🧪 Scanning: ${baseUrl}`);
244
+ } else {
245
+ console.log(`\n🧪 Market Reality Snapshot v1`);
246
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
247
+ console.log(`📍 Base URL: ${baseUrl}`);
248
+ console.log(`🎯 Attempts: ${filteredAttempts.join(', ')}`);
249
+ console.log(`📁 Run Dir: ${runDir}`);
250
+ }
69
251
 
70
252
  // Initialize snapshot builder
71
253
  const snapshotBuilder = new SnapshotBuilder(baseUrl, runId, toolVersion);
@@ -73,13 +255,51 @@ async function executeReality(config) {
73
255
 
74
256
  let crawlResult = null;
75
257
  let discoveryResult = null;
258
+ let pageLanguage = 'unknown';
259
+ let contactDetectionResult = null;
260
+ let siteIntrospection = null;
261
+ let siteProfile = 'unknown';
76
262
 
77
263
  // Optional: Crawl to discover URLs (lightweight, first N pages)
78
264
  if (enableCrawl) {
79
265
  console.log(`\n🔍 Crawling for discovered URLs...`);
80
266
  const browser = new GuardianBrowser();
81
267
  try {
82
- await browser.launch(timeout);
268
+ await browser.launch(resolvedTimeout);
269
+ await browser.page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: resolvedTimeout });
270
+
271
+ // Wave 3: Site introspection for smart attempt selection
272
+ try {
273
+ console.log(`\n🔬 Inspecting site capabilities...`);
274
+ siteIntrospection = await inspectSite(browser.page);
275
+ siteProfile = detectProfile(siteIntrospection);
276
+ const summary = summarizeIntrospection(siteIntrospection);
277
+ console.log(`✅ Site profile: ${siteProfile}`);
278
+ console.log(` ${summary}`);
279
+ } catch (introspectionErr) {
280
+ console.warn(`⚠️ Site introspection failed (non-critical): ${introspectionErr.message}`);
281
+ // Default to empty introspection if it fails
282
+ siteIntrospection = {
283
+ hasLogin: false,
284
+ hasSignup: false,
285
+ hasCheckout: false,
286
+ hasNewsletter: false,
287
+ hasContactForm: false,
288
+ hasLanguageSwitch: false
289
+ };
290
+ siteProfile = 'unknown';
291
+ }
292
+
293
+ // Wave 1.1: Detect page language and contact
294
+ try {
295
+ contactDetectionResult = await findContactOnPage(browser.page, baseUrl);
296
+ pageLanguage = contactDetectionResult.language;
297
+ console.log(`\n${formatDetectionForReport(contactDetectionResult)}\n`);
298
+ } catch (detectionErr) {
299
+ // Language detection non-critical
300
+ console.warn(`⚠️ Language/contact detection failed: ${detectionErr.message}`);
301
+ }
302
+
83
303
  const crawler = new GuardianCrawler(baseUrl, maxPages, maxDepth);
84
304
  crawlResult = await crawler.crawl(browser);
85
305
  console.log(`✅ Crawl complete: discovered ${crawlResult.totalDiscovered}, visited ${crawlResult.totalVisited}`);
@@ -87,20 +307,50 @@ async function executeReality(config) {
87
307
  await browser.close();
88
308
  } catch (crawlErr) {
89
309
  console.log(`⚠️ Crawl failed (non-critical): ${crawlErr.message}`);
310
+ runSignals.push({ id: 'crawl_failed', severity: 'high', type: 'coverage', description: `Crawl failed: ${crawlErr.message}` });
90
311
  // Continue anyway - crawl is optional
91
312
  }
92
313
  }
93
314
 
315
+ // Wave 3: If crawl was disabled but introspection wasn't done, do it now
316
+ if (!enableCrawl && !siteIntrospection) {
317
+ console.log(`\n🔬 Inspecting site capabilities...`);
318
+ const browser = new GuardianBrowser();
319
+ try {
320
+ await browser.launch(resolvedTimeout);
321
+ await browser.page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: resolvedTimeout });
322
+
323
+ siteIntrospection = await inspectSite(browser.page);
324
+ siteProfile = detectProfile(siteIntrospection);
325
+ const summary = summarizeIntrospection(siteIntrospection);
326
+ console.log(`✅ Site profile: ${siteProfile}`);
327
+ console.log(` ${summary}`);
328
+
329
+ await browser.close();
330
+ } catch (introspectionErr) {
331
+ console.warn(`⚠️ Site introspection failed (non-critical): ${introspectionErr.message}`);
332
+ siteIntrospection = {
333
+ hasLogin: false,
334
+ hasSignup: false,
335
+ hasCheckout: false,
336
+ hasNewsletter: false,
337
+ hasContactForm: false,
338
+ hasLanguageSwitch: false
339
+ };
340
+ siteProfile = 'unknown';
341
+ }
342
+ }
343
+
94
344
  // Optional: Discovery Engine (Phase 4) — deterministic safe exploration
95
345
  if (enableDiscovery) {
96
346
  console.log(`\n🔎 Running discovery engine...`);
97
347
  const browser = new GuardianBrowser();
98
348
  try {
99
- await browser.launch(timeout);
349
+ await browser.launch(resolvedTimeout);
100
350
  const engine = new DiscoveryEngine({
101
351
  baseUrl,
102
352
  maxPages,
103
- timeout,
353
+ timeout: resolvedTimeout,
104
354
  executeInteractions: false,
105
355
  browser,
106
356
  });
@@ -110,6 +360,7 @@ async function executeReality(config) {
110
360
  await browser.close();
111
361
  } catch (discErr) {
112
362
  console.log(`⚠️ Discovery failed (non-critical): ${discErr.message}`);
363
+ runSignals.push({ id: 'discovery_failed', severity: 'high', type: 'discovery', description: `Discovery failed: ${discErr.message}` });
113
364
  }
114
365
  }
115
366
 
@@ -146,7 +397,7 @@ async function executeReality(config) {
146
397
  const flowResults = [];
147
398
 
148
399
  // Determine attempts to run (manual + auto-generated)
149
- let attemptsToRun = Array.isArray(attempts) ? attempts.slice() : getDefaultAttemptIds();
400
+ let attemptsToRun = enabledRequestedAttempts.slice();
150
401
 
151
402
  // Phase 2: Add auto-generated attempts
152
403
  if (enableAutoAttempts && autoAttempts.length > 0) {
@@ -155,11 +406,11 @@ async function executeReality(config) {
155
406
  console.log(`➕ Added ${autoAttemptIds.length} auto-generated attempts`);
156
407
  }
157
408
 
158
- if (includeUniversal && !attemptsToRun.includes('universal_reality')) {
409
+ if (includeUniversal && !attemptsToRun.includes('universal_reality') && !disabledByPreset.has('universal_reality')) {
159
410
  attemptsToRun.push('universal_reality');
160
411
  }
161
412
  // If discovery enabled and site is simple (few interactions), add universal pack
162
- if (enableDiscovery && discoveryResult && !attemptsToRun.includes('universal_reality')) {
413
+ if (enableDiscovery && discoveryResult && !attemptsToRun.includes('universal_reality') && !disabledByPreset.has('universal_reality')) {
163
414
  const simpleSite = (discoveryResult.interactionsDiscovered || 0) === 0 || (discoveryResult.pagesVisitedCount || 0) <= 1;
164
415
  if (simpleSite) {
165
416
  attemptsToRun.push('universal_reality');
@@ -167,74 +418,383 @@ async function executeReality(config) {
167
418
  }
168
419
  }
169
420
 
170
- // Execute all registered attempts
421
+ // Phase 7.1: Apply attempts filter
422
+ if (attemptsFilter && validation && validation.valid && validation.ids.length > 0) {
423
+ attemptsToRun = filterAttempts(attemptsToRun, validation.ids);
424
+ }
425
+
426
+ // Wave 3: Apply smart attempt selection based on introspection
427
+ let attemptsSkipped = [];
428
+ if (siteIntrospection) {
429
+ const attemptObjects = attemptsToRun.map(id => ({ id }));
430
+ const relevanceResult = filterAttemptsByRelevance(attemptObjects, siteIntrospection);
431
+ attemptsToRun = relevanceResult.toRun.map(a => a.id);
432
+ attemptsSkipped = relevanceResult.toSkip;
433
+
434
+ if (attemptsSkipped.length > 0 && !ciMode) {
435
+ console.log(`\n⊘ Skipping ${attemptsSkipped.length} irrelevant attempt(s):`);
436
+ for (const skip of attemptsSkipped) {
437
+ console.log(` • ${skip.attempt}: ${skip.reason}`);
438
+ }
439
+ }
440
+ }
441
+
442
+ // Phase 7.2: Print parallel mode if enabled
443
+ if (!ciMode && validatedParallel > 1) {
444
+ console.log(`\n⚡ PARALLEL: ${validatedParallel} concurrent attempts`);
445
+ }
446
+
447
+ // Phase 7.3: Initialize browser pool (single browser per run)
448
+ const browserPool = new BrowserPool();
449
+ const browserOptions = {
450
+ headless: !headful,
451
+ args: !headful ? [] : [],
452
+ timeout: resolvedTimeout
453
+ };
454
+
455
+ try {
456
+ await browserPool.launch(browserOptions);
457
+ if (!ciMode) {
458
+ console.log(`🌐 Browser pool ready (reuse enabled)`);
459
+ }
460
+ } catch (err) {
461
+ throw new Error(`Failed to launch browser pool: ${err.message}`);
462
+ }
463
+
464
+ // Execute all registered attempts (with optional parallelism)
171
465
  console.log(`\n🎬 Executing attempts...`);
172
- for (const attemptId of attemptsToRun) {
173
- const attemptDef = getAttemptDefinition(attemptId);
174
- if (!attemptDef) {
175
- throw new Error(`Attempt ${attemptId} not found in registry`);
466
+
467
+ // Shared state for fail-fast coordination
468
+ let shouldStopScheduling = false;
469
+
470
+ // Phase 7.2: Execute attempts with bounded parallelism
471
+ // Phase 7.3: Pass browser pool to attempts
472
+ // Phase 7.4: Check applicability before executing
473
+ const attemptResults_parallel = await executeParallel(
474
+ attemptsToRun,
475
+ async (attemptId) => {
476
+ const attemptDef = getAttemptDefinition(attemptId);
477
+ if (!attemptDef) {
478
+ missingAttempts.push(attemptId);
479
+ return {
480
+ attemptId,
481
+ attemptName: attemptId,
482
+ goal: 'Unknown',
483
+ riskCategory: 'UNKNOWN',
484
+ source: 'manual',
485
+ outcome: 'SKIPPED',
486
+ skipReason: 'Attempt not registered',
487
+ skipReasonCode: SKIP_CODES.ENGINE_MISSING,
488
+ exitCode: 0,
489
+ steps: [],
490
+ friction: null,
491
+ error: null
492
+ };
493
+ }
494
+
495
+ if (!ciMode) {
496
+ console.log(` • ${attemptDef.name}...`);
497
+ }
498
+
499
+ const attemptArtifactsDir = path.join(runDir, attemptId);
500
+
501
+ // Phase 7.3: Create isolated context for this attempt
502
+ const { context, page } = await browserPool.createContext({
503
+ timeout: resolvedTimeout
504
+ });
505
+
506
+ let result;
507
+ try {
508
+ // Navigate to site to check prerequisites and applicability
509
+ await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: resolvedTimeout });
510
+
511
+ // Phase 7.4: Check prerequisites before executing attempt
512
+ const prereqCheck = await checkPrerequisites(page, attemptId, 2000);
513
+
514
+ if (!prereqCheck.canProceed) {
515
+ // Skip attempt - prerequisites not met
516
+ if (!ciMode) {
517
+ console.log(` ⊘ Skipped: ${prereqCheck.reason}`);
518
+ }
519
+
520
+ result = {
521
+ outcome: 'SKIPPED',
522
+ skipReason: prereqCheck.reason,
523
+ skipReasonCode: SKIP_CODES.PREREQ,
524
+ exitCode: 0,
525
+ steps: [],
526
+ friction: null,
527
+ error: null
528
+ };
529
+ } else {
530
+ // Check if this attempt is applicable to the site
531
+ const { AttemptEngine } = require('./attempt-engine');
532
+ const engine = new AttemptEngine({ attemptId, timeout: resolvedTimeout });
533
+ const applicabilityCheck = await engine.checkAttemptApplicability(page, attemptId);
534
+
535
+ if (!applicabilityCheck.applicable && applicabilityCheck.confidence < 0.3) {
536
+ // Attempt not applicable - features not present
537
+ if (!ciMode) {
538
+ console.log(` ⊘ Not applicable: ${applicabilityCheck.reason}`);
539
+ }
540
+
541
+ result = {
542
+ outcome: 'NOT_APPLICABLE',
543
+ skipReason: applicabilityCheck.reason,
544
+ skipReasonCode: SKIP_CODES.NOT_APPLICABLE,
545
+ discoverySignals: applicabilityCheck.discoverySignals,
546
+ exitCode: 0,
547
+ steps: [],
548
+ friction: null,
549
+ error: null
550
+ };
551
+ } else if (!applicabilityCheck.applicable && applicabilityCheck.confidence >= 0.3 && applicabilityCheck.confidence < 0.6) {
552
+ // Features possibly present but discovery uncertain - try to execute anyway
553
+ // Mark as DISCOVERY_FAILED if it fails
554
+ result = await executeAttempt({
555
+ baseUrl,
556
+ attemptId,
557
+ artifactsDir: attemptArtifactsDir,
558
+ headful,
559
+ enableTrace,
560
+ enableScreenshots,
561
+ quiet: ciMode,
562
+ timeout: resolvedTimeout,
563
+ browserContext: context,
564
+ browserPage: page
565
+ });
566
+
567
+ // If execution failed due to element not found, reclassify as DISCOVERY_FAILED
568
+ if (result.outcome === 'FAILURE' && result.error && /element|selector|locator/i.test(result.error)) {
569
+ result.outcome = 'DISCOVERY_FAILED';
570
+ result.skipReason = `Element discovery failed: ${result.error}`;
571
+ result.discoverySignals = applicabilityCheck.discoverySignals;
572
+ }
573
+ } else {
574
+ // Applicable or high confidence - execute normally
575
+ result = await executeAttempt({
576
+ baseUrl,
577
+ attemptId,
578
+ artifactsDir: attemptArtifactsDir,
579
+ headful,
580
+ enableTrace,
581
+ enableScreenshots,
582
+ quiet: ciMode,
583
+ timeout: resolvedTimeout,
584
+ // Phase 7.3: Pass context from pool
585
+ browserContext: context,
586
+ browserPage: page
587
+ });
588
+ }
589
+ }
590
+ } finally {
591
+ // Phase 7.3: Cleanup context after attempt
592
+ await browserPool.closeContext(context);
593
+ }
594
+
595
+ const attemptResult = {
596
+ attemptId,
597
+ attemptName: attemptDef.name,
598
+ goal: attemptDef.goal,
599
+ riskCategory: attemptDef.riskCategory || 'UNKNOWN',
600
+ source: attemptDef.source || 'manual',
601
+ ...result
602
+ };
603
+
604
+ // Phase 7.1: Fail-fast logic (stop on FAILURE, not FRICTION or SKIPPED)
605
+ if (failFast && attemptResult.outcome === 'FAILURE') {
606
+ shouldStopScheduling = true;
607
+ if (!ciMode) {
608
+ console.log(`\n⚡ FAIL-FAST: stopping after failure: ${attemptDef.name}`);
609
+ }
610
+ }
611
+
612
+ return attemptResult;
613
+ },
614
+ validatedParallel,
615
+ { shouldStop: () => shouldStopScheduling }
616
+ );
617
+
618
+ // Collect results in order
619
+ for (const result of attemptResults_parallel) {
620
+ if (result) {
621
+ attemptResults.push(result);
176
622
  }
623
+ }
177
624
 
178
- console.log(` ${attemptDef.name}...`);
179
- const attemptArtifactsDir = path.join(runDir, attemptId);
180
- const result = await executeAttempt({
181
- baseUrl,
182
- attemptId,
183
- artifactsDir: attemptArtifactsDir,
184
- headful,
185
- enableTrace,
186
- enableScreenshots
625
+ // Add explicit SKIPPED entries for attempts filtered out before execution
626
+ for (const skip of attemptsSkipped) {
627
+ const def = getAttemptDefinition(skip.attempt) || {};
628
+ attemptResults.push({
629
+ attemptId: skip.attempt,
630
+ attemptName: def.name || skip.attempt,
631
+ goal: def.goal,
632
+ riskCategory: def.riskCategory || 'UNKNOWN',
633
+ source: def.source || 'manual',
634
+ outcome: 'SKIPPED',
635
+ skipReason: skip.reason || 'Skipped before execution',
636
+ skipReasonCode: skip.reasonCode || SKIP_CODES.USER_FILTERED,
637
+ exitCode: 0,
638
+ steps: [],
639
+ friction: null,
640
+ error: null
641
+ });
642
+ }
643
+
644
+ // Add explicit SKIPPED entries for attempts disabled by preset (kept for auditability)
645
+ for (const disabledId of presetDisabledAttempts) {
646
+ const def = getAttemptDefinition(disabledId) || {};
647
+ attemptResults.push({
648
+ attemptId: disabledId,
649
+ attemptName: def.name || disabledId,
650
+ goal: def.goal,
651
+ riskCategory: def.riskCategory || 'UNKNOWN',
652
+ source: def.source || 'manual',
653
+ outcome: 'SKIPPED',
654
+ skipReason: 'Disabled by preset',
655
+ skipReasonCode: SKIP_CODES.DISABLED_BY_PRESET,
656
+ exitCode: 0,
657
+ steps: [],
658
+ friction: null,
659
+ error: null,
660
+ disabledByPreset: true
187
661
  });
662
+ }
188
663
 
664
+ // Add explicit SKIPPED for user-filtered attempts that were removed before scheduling
665
+ for (const uf of userFilteredAttempts) {
666
+ const def = getAttemptDefinition(uf.attemptId) || {};
189
667
  attemptResults.push({
190
- attemptId,
191
- attemptName: attemptDef.name,
192
- goal: attemptDef.goal,
193
- riskCategory: attemptDef.riskCategory || 'UNKNOWN',
194
- source: attemptDef.source || 'manual',
195
- ...result
668
+ attemptId: uf.attemptId,
669
+ attemptName: def.name || uf.attemptId,
670
+ goal: def.goal,
671
+ riskCategory: def.riskCategory || 'UNKNOWN',
672
+ source: def.source || 'manual',
673
+ outcome: 'SKIPPED',
674
+ skipReason: uf.reason,
675
+ skipReasonCode: SKIP_CODES.USER_FILTERED,
676
+ exitCode: 0,
677
+ steps: [],
678
+ friction: null,
679
+ error: null,
680
+ userFiltered: true
196
681
  });
682
+ }
683
+
684
+ // Preserve requested ordering for downstream artifacts
685
+ const attemptOrder = new Map(requestedAttempts.map((id, idx) => [id, idx]));
686
+ attemptResults.sort((a, b) => (attemptOrder.get(a.attemptId) ?? 999) - (attemptOrder.get(b.attemptId) ?? 999));
197
687
 
688
+ // Normalize execution metadata
689
+ for (const result of attemptResults) {
690
+ result.executed = isExecutedAttempt(result);
198
691
  }
199
692
 
200
693
  // Phase 3: Execute intent flows (deterministic, curated)
201
694
  if (enableFlows) {
202
695
  console.log(`\n🎯 Executing intent flows...`);
203
696
  const flowExecutor = new GuardianFlowExecutor({
204
- timeout,
697
+ timeout: resolvedTimeout,
205
698
  screenshotOnStep: enableScreenshots,
699
+ baseUrl,
700
+ quiet: ciMode,
206
701
  ...flowOptions
207
702
  });
208
703
  const browser = new GuardianBrowser();
209
704
 
210
705
  try {
211
- await browser.launch(timeout);
212
- for (const flowId of (Array.isArray(flows) && flows.length ? flows : getDefaultFlowIds())) {
706
+ await browser.launch(resolvedTimeout);
707
+ // Phase 7.1: Apply flows filter
708
+ let flowsToRun = Array.isArray(filteredFlows) && filteredFlows.length ? filteredFlows : getDefaultFlowIds();
709
+
710
+ for (const flowId of flowsToRun) {
213
711
  const flowDef = getFlowDefinition(flowId);
214
712
  if (!flowDef) {
215
713
  console.warn(`⚠️ Flow ${flowId} not found, skipping`);
216
714
  continue;
217
715
  }
218
716
 
717
+ const validation = validateFlowDefinition(flowDef);
718
+ if (!validation.ok) {
719
+ const reason = validation.reason || 'Flow misconfigured';
720
+ const flowResult = {
721
+ flowId,
722
+ flowName: flowDef.name,
723
+ riskCategory: flowDef.riskCategory || 'TRUST/UX',
724
+ description: flowDef.description,
725
+ outcome: 'FAILURE',
726
+ stepsExecuted: 0,
727
+ stepsTotal: Array.isArray(flowDef.steps) ? flowDef.steps.length : 0,
728
+ failedStep: 0,
729
+ error: reason,
730
+ screenshots: [],
731
+ failureReasons: [reason],
732
+ source: 'flow'
733
+ };
734
+ flowResults.push(flowResult);
735
+
736
+ // Phase 7.1: Fail-fast on flow failure
737
+ if (failFast && flowResult.outcome === 'FAILURE') {
738
+ console.log(`\n⚡ FAIL-FAST: stopping after first failure: ${flowDef.name}`);
739
+ break;
740
+ }
741
+ continue;
742
+ }
743
+
219
744
  console.log(` • ${flowDef.name}...`);
220
745
  const flowArtifactsDir = path.join(runDir, 'flows', flowId);
221
746
  fs.mkdirSync(flowArtifactsDir, { recursive: true });
222
747
 
223
- const flowResult = await flowExecutor.executeFlow(browser.page, flowDef, flowArtifactsDir, baseUrl);
224
- flowResults.push({
748
+ let flowResult;
749
+ try {
750
+ flowResult = await flowExecutor.executeFlow(browser.page, flowDef, flowArtifactsDir, baseUrl);
751
+ } catch (flowErr) {
752
+ console.warn(`⚠️ Flow ${flowDef.name} crashed: ${flowErr.message}`);
753
+ flowResult = {
754
+ flowId,
755
+ flowName: flowDef.name,
756
+ riskCategory: flowDef.riskCategory || 'TRUST/UX',
757
+ description: flowDef.description,
758
+ outcome: 'FAILURE',
759
+ stepsExecuted: 0,
760
+ stepsTotal: flowDef.steps.length,
761
+ durationMs: 0,
762
+ failedStep: null,
763
+ error: flowErr.message,
764
+ screenshots: [],
765
+ failureReasons: [`flow crashed: ${flowErr.message}`]
766
+ };
767
+ }
768
+
769
+ const resultWithMetadata = {
225
770
  flowId,
226
771
  flowName: flowDef.name,
227
772
  riskCategory: flowDef.riskCategory || 'TRUST/UX',
228
773
  description: flowDef.description,
229
- outcome: flowResult.success ? 'SUCCESS' : 'FAILURE',
774
+ outcome: flowResult.outcome || (flowResult.success ? 'SUCCESS' : 'FAILURE'),
230
775
  stepsExecuted: flowResult.stepsExecuted,
231
776
  stepsTotal: flowResult.stepsTotal,
232
777
  durationMs: flowResult.durationMs,
233
778
  failedStep: flowResult.failedStep,
234
779
  error: flowResult.error,
235
780
  screenshots: flowResult.screenshots,
236
- source: 'flow'
237
- });
781
+ failureReasons: flowResult.failureReasons || [],
782
+ source: 'flow',
783
+ successEval: flowResult.successEval ? {
784
+ status: flowResult.successEval.status,
785
+ confidence: flowResult.successEval.confidence,
786
+ reasons: (flowResult.successEval.reasons || []).slice(0, 3),
787
+ evidence: flowResult.successEval.evidence || {}
788
+ } : null
789
+ };
790
+
791
+ flowResults.push(resultWithMetadata);
792
+
793
+ // Phase 7.1: Fail-fast logic for flows (stop on FAILURE, not FRICTION)
794
+ if (failFast && resultWithMetadata.outcome === 'FAILURE') {
795
+ console.log(`\n⚡ FAIL-FAST: stopping after first failure: ${flowDef.name}`);
796
+ break;
797
+ }
238
798
  }
239
799
  } catch (flowErr) {
240
800
  console.warn(`⚠️ Flow execution failed (non-critical): ${flowErr.message}`);
@@ -243,12 +803,48 @@ async function executeReality(config) {
243
803
  }
244
804
  }
245
805
 
806
+ // Flow summary logging
807
+ if (flowResults.length > 0 && !ciMode) {
808
+ const successCount = flowResults.filter(f => (f.outcome || f.success === true ? f.outcome === 'SUCCESS' || f.success === true : false)).length;
809
+ const frictionCount = flowResults.filter(f => f.outcome === 'FRICTION').length;
810
+ const failureCount = flowResults.filter(f => f.outcome === 'FAILURE' || f.success === false).length;
811
+ console.log(`\nRun completed: ${flowResults.length} flows (${successCount} successes, ${frictionCount} frictions, ${failureCount} failures)`);
812
+ const troubled = flowResults.filter(f => f.outcome === 'FRICTION' || f.outcome === 'FAILURE');
813
+ troubled.forEach(f => {
814
+ const reason = (f.failureReasons && f.failureReasons[0]) || (f.error) || (f.successEval && f.successEval.reasons && f.successEval.reasons[0]) || 'no reason captured';
815
+ console.log(` - ${f.flowName}: ${reason}`);
816
+ });
817
+ }
818
+
819
+ // Ensure artifacts exist for skipped attempts so totals remain canonical and inspectable
820
+ for (const result of attemptResults) {
821
+ if (!isExecutedAttempt(result)) {
822
+ const attemptDir = path.join(runDir, result.attemptId);
823
+ const attemptRunDir = path.join(attemptDir, 'attempt-skipped');
824
+ fs.mkdirSync(path.join(attemptRunDir, 'attempt-screenshots'), { recursive: true });
825
+
826
+ const skipSummary = {
827
+ attemptId: result.attemptId,
828
+ outcome: 'SKIPPED',
829
+ reason: result.skipReason || 'Not executed'
830
+ };
831
+
832
+ const jsonPath = path.join(attemptRunDir, 'attempt-report.json');
833
+ const htmlPath = path.join(attemptRunDir, 'attempt-report.html');
834
+ fs.writeFileSync(jsonPath, JSON.stringify(skipSummary, null, 2));
835
+ fs.writeFileSync(htmlPath, `<html><body><h1>${result.attemptName || result.attemptId}</h1><p>Skipped: ${skipSummary.reason}</p></body></html>`);
836
+
837
+ result.reportJsonPath = result.reportJsonPath || jsonPath;
838
+ result.reportHtmlPath = result.reportHtmlPath || htmlPath;
839
+ }
840
+ }
841
+
246
842
  // Generate market report (existing flow)
247
843
  const reporter = new MarketReporter();
248
844
  const report = reporter.createReport({
249
845
  runId,
250
846
  baseUrl,
251
- attemptsRun: attemptsToRun,
847
+ attemptsRun: requestedAttempts,
252
848
  results: attemptResults.map(r => ({
253
849
  attemptId: r.attemptId,
254
850
  attemptName: r.attemptName,
@@ -367,22 +963,381 @@ async function executeReality(config) {
367
963
  console.warn(`⚠️ Enhanced HTML report failed (non-critical): ${htmlErr.message}`);
368
964
  }
369
965
 
370
- // Phase 5/6: Evaluate policy
966
+ // Phase 7.3: Cleanup browser pool
967
+ await browserPool.close();
968
+
969
+ // Wave 4: Honest results & near-success
970
+ // Calculate attempt statistics and coverage
971
+ const disabledAttemptResults = attemptResults.filter(a => a.disabledByPreset);
972
+ const eligibleAttempts = attemptResults.filter(a => !a.disabledByPreset);
973
+ const executedAttempts = eligibleAttempts.filter(isExecutedAttempt);
974
+ const skippedAttempts = eligibleAttempts.filter(a => !isExecutedAttempt(a));
975
+ const skippedNotApplicable = eligibleAttempts.filter(a => a.skipReasonCode === SKIP_CODES.NOT_APPLICABLE);
976
+ const skippedMissing = eligibleAttempts.filter(a => a.skipReasonCode === SKIP_CODES.ENGINE_MISSING);
977
+ const skippedUserFiltered = eligibleAttempts.filter(a => a.skipReasonCode === SKIP_CODES.USER_FILTERED || a.userFiltered);
978
+ const skippedDisabledByPreset = disabledAttemptResults;
979
+ const attemptStats = {
980
+ total: eligibleAttempts.length,
981
+ executed: executedAttempts.length,
982
+ executedCount: executedAttempts.length,
983
+ enabledPlannedCount: enabledRequestedAttempts.length,
984
+ disabledPlannedCount: disabledAttemptResults.length,
985
+ successful: executedAttempts.filter(a => a.outcome === 'SUCCESS').length,
986
+ failed: executedAttempts.filter(a => a.outcome === 'FAILURE').length,
987
+ skipped: skippedAttempts.length,
988
+ skippedDetails: skippedAttempts.map(a => ({ attempt: a.attemptId, reason: a.skipReason || 'Not executed', code: a.skipReasonCode })),
989
+ disabled: disabledAttemptResults.length,
990
+ disabledDetails: disabledAttemptResults.map(a => ({ attempt: a.attemptId, reason: a.skipReason || 'Disabled by preset', code: SKIP_CODES.DISABLED_BY_PRESET })),
991
+ userFiltered: skippedUserFiltered.length,
992
+ skippedDisabledByPreset: skippedDisabledByPreset.length,
993
+ skippedNotApplicable: skippedNotApplicable.length,
994
+ skippedMissing: skippedMissing.length,
995
+ skippedUserFiltered: skippedUserFiltered.length
996
+ };
997
+
998
+ const nearSuccessDetails = [];
999
+ for (const a of attemptResults) {
1000
+ if (a.outcome === 'FAILURE') {
1001
+ const errMsg = typeof a.error === 'string' ? a.error : '';
1002
+ const noFailedSteps = Array.isArray(a.steps) ? a.steps.every(s => s.status !== 'failed') : true;
1003
+ if (noFailedSteps && errMsg.includes('Success conditions not met')) {
1004
+ nearSuccessDetails.push({ attempt: a.attemptId, reason: 'Submit succeeded but no confirmation text detected' });
1005
+ }
1006
+ }
1007
+ }
1008
+ attemptStats.nearSuccess = nearSuccessDetails.length;
1009
+ attemptStats.nearSuccessDetails = nearSuccessDetails;
1010
+
1011
+ // Coverage and evidence signals
1012
+ const coverageDenominator = attemptStats.total;
1013
+ const coverageNumerator = attemptStats.executed + skippedNotApplicable.length;
1014
+ const coverageGaps = Math.max(coverageDenominator - coverageNumerator, 0);
1015
+ const coverageSignal = {
1016
+ gaps: coverageGaps,
1017
+ executed: attemptStats.executed,
1018
+ total: coverageDenominator,
1019
+ details: attemptStats.skippedDetails,
1020
+ disabled: attemptStats.disabledDetails,
1021
+ skippedDisabledByPreset: skippedDisabledByPreset.map(a => ({ attempt: a.attemptId, reason: a.skipReason || 'Disabled by preset', code: SKIP_CODES.DISABLED_BY_PRESET })),
1022
+ skippedNotApplicable: skippedNotApplicable.map(a => ({ attempt: a.attemptId, reason: a.skipReason })),
1023
+ skippedMissing: skippedMissing.map(a => ({ attempt: a.attemptId, reason: a.skipReason })),
1024
+ skippedUserFiltered: skippedUserFiltered.map(a => ({ attempt: a.attemptId, reason: a.skipReason })),
1025
+ counts: {
1026
+ executedCount: attemptStats.executed,
1027
+ enabledPlannedCount: attemptStats.enabledPlannedCount,
1028
+ disabledPlannedCount: attemptStats.disabledPlannedCount,
1029
+ skippedDisabledByPreset: skippedDisabledByPreset.length,
1030
+ skippedUserFiltered: skippedUserFiltered.length,
1031
+ skippedNotApplicable: skippedNotApplicable.length,
1032
+ skippedMissing: skippedMissing.length
1033
+ }
1034
+ };
1035
+
1036
+ const evidenceMetrics = {
1037
+ completeness: coverageDenominator > 0 ? coverageNumerator / coverageDenominator : 0,
1038
+ integrity: 0,
1039
+ hashedFiles: 0,
1040
+ totalFiles: 0,
1041
+ screenshotsEnabled: enableScreenshots,
1042
+ tracesEnabled: enableTrace
1043
+ };
1044
+
1045
+ // Build signals used by policy (single source of verdict truth)
1046
+ const policySignals = {
1047
+ coverage: coverageSignal,
1048
+ marketImpact,
1049
+ flows: flowResults,
1050
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1051
+ attempts: attemptResults,
1052
+ crawlIssues: runSignals.filter(s => s.type === 'coverage'),
1053
+ discoveryIssues: runSignals.filter(s => s.type === 'discovery'),
1054
+ evidence: {
1055
+ metrics: evidenceMetrics,
1056
+ missingScreenshots: !enableScreenshots,
1057
+ missingTraces: !enableTrace
1058
+ },
1059
+ runtimeSignals: runSignals
1060
+ };
1061
+
1062
+ // Resolve policy (strict failure on invalid)
371
1063
  let policyEval = null;
372
- if (policy) {
373
- try {
374
- const policyObj = parsePolicyOption(policy);
375
- if (policyObj) {
376
- console.log(`\n🛡️ Evaluating policy...`);
377
- policyEval = evaluatePolicy(snapshotBuilder.getSnapshot(), policyObj);
378
- console.log(`Policy: ${policyEval.passed ? '✅ PASSED' : '❌ FAILED'}`);
379
- if (!policyEval.passed && policyEval.reasons) {
380
- policyEval.reasons.slice(0, 3).forEach(r => console.log(` ${r}`));
1064
+ let policyObj = null;
1065
+ let presetId = policyName; // Preserve preset ID for naming (e.g., 'startup')
1066
+ let policyHash = null;
1067
+ let policySource = 'default';
1068
+ try {
1069
+ if (policy && typeof policy === 'string' && fs.existsSync(policy)) {
1070
+ policySource = path.resolve(policy);
1071
+ } else if (policy) {
1072
+ policySource = typeof policy === 'string' ? `inline:${policy}` : `preset:${config.preset || policyName}`;
1073
+ } else {
1074
+ policySource = 'default';
1075
+ }
1076
+ policyObj = policy
1077
+ ? (typeof policy === 'object' ? policy : parsePolicyOption(policy))
1078
+ : loadPolicy();
1079
+ policyName = policyObj?.name || policyObj?.id || policyName || (policy ? policy : 'default');
1080
+ policyHash = crypto.createHash('sha256').update(JSON.stringify(policyObj)).digest('hex');
1081
+ } catch (policyLoadErr) {
1082
+ console.error(`Error: ${policyLoadErr.message}`);
1083
+ process.exit(2);
1084
+ }
1085
+
1086
+ // First pass policy evaluation (before manifest integrity)
1087
+ policyEval = evaluatePolicy(snapshotBuilder.getSnapshot(), policyObj, policySignals);
1088
+ console.log(`\n🛡️ Evaluating policy... (${policyName})`);
1089
+ console.log(`Policy evaluation result: exitCode=${policyEval.exitCode}, passed=${policyEval.passed}`);
1090
+ if (policyEval.reasons && policyEval.reasons.length > 0) {
1091
+ policyEval.reasons.slice(0, 3).forEach(r => console.log(` • ${r}`));
1092
+ }
1093
+
1094
+ const resolvedConfig = {
1095
+ presetId: config.preset || presetId,
1096
+ policySource,
1097
+ policyId: policyObj?.id || policyObj?.name || policyName,
1098
+ policyHash,
1099
+ mediaRequirements: {
1100
+ requireScreenshots: !!(policyObj?.evidence?.requireScreenshots),
1101
+ requireTraces: !!(policyObj?.evidence?.requireTraces),
1102
+ minCompleteness: policyObj?.evidence?.minCompleteness,
1103
+ minIntegrity: policyObj?.evidence?.minIntegrity
1104
+ },
1105
+ attemptPlan: {
1106
+ enabled: enabledRequestedAttempts,
1107
+ disabled: Array.from(disabledByPreset),
1108
+ userFiltered: userFilteredAttempts.map(u => u.attemptId),
1109
+ missing: missingAttempts
1110
+ },
1111
+ coverage: {
1112
+ total: coverageSignal.total,
1113
+ executed: coverageSignal.executed,
1114
+ executedCount: attemptStats.executed,
1115
+ enabledPlannedCount: attemptStats.enabledPlannedCount,
1116
+ disabledPlannedCount: attemptStats.disabledPlannedCount,
1117
+ skippedNotApplicable: coverageSignal.skippedNotApplicable?.length || 0,
1118
+ skippedMissing: coverageSignal.skippedMissing?.length || 0,
1119
+ skippedUserFiltered: coverageSignal.skippedUserFiltered?.length || 0,
1120
+ skippedDisabledByPreset: coverageSignal.skippedDisabledByPreset?.length || 0,
1121
+ disabled: coverageSignal.disabled?.length || 0
1122
+ },
1123
+ evidenceMetrics
1124
+ };
1125
+
1126
+ // Calculate actual duration from start time
1127
+ const endTime = new Date();
1128
+ const actualDurationMs = endTime.getTime() - startTime.getTime();
1129
+
1130
+ // Rename run directory to status placeholder for auditability
1131
+ let exitCode = policyEval.exitCode;
1132
+ const runResultPreManifest = 'PENDING';
1133
+ const priorRunDir = runDir;
1134
+ const finalRunDirName = makeRunDirName({ timestamp: startTime, url: baseUrl, policy: presetId, result: runResultPreManifest });
1135
+ const finalRunDir = path.join(artifactsDir, finalRunDirName);
1136
+ if (finalRunDir !== runDir) {
1137
+ fs.renameSync(runDir, finalRunDir);
1138
+ runDir = finalRunDir;
1139
+ runDirName = finalRunDirName;
1140
+ }
1141
+ // Rebase attempt artifact paths after rename
1142
+ for (const attempt of attemptResults) {
1143
+ if (attempt.attemptJsonPath) {
1144
+ attempt.attemptJsonPath = attempt.attemptJsonPath.replace(priorRunDir, runDir);
1145
+ }
1146
+ if (attempt.stepsLogPath) {
1147
+ attempt.stepsLogPath = attempt.stepsLogPath.replace(priorRunDir, runDir);
1148
+ }
1149
+ if (attempt.reportJsonPath) {
1150
+ attempt.reportJsonPath = attempt.reportJsonPath.replace(priorRunDir, runDir);
1151
+ }
1152
+ if (attempt.reportHtmlPath) {
1153
+ attempt.reportHtmlPath = attempt.reportHtmlPath.replace(priorRunDir, runDir);
1154
+ }
1155
+ }
1156
+ const snapshotPathFinal = path.join(runDir, 'snapshot.json');
1157
+ const marketJsonPathFinal = path.join(runDir, path.basename(jsonPath));
1158
+ const marketHtmlPathFinal = path.join(runDir, path.basename(htmlPath));
1159
+
1160
+ // Build decision artifact and summary (first pass)
1161
+ const initialDecision = computeFinalVerdict({
1162
+ marketImpact,
1163
+ policyEval,
1164
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1165
+ flows: flowResults,
1166
+ attempts: attemptResults
1167
+ });
1168
+ const initialExplanation = buildRealityExplanation({
1169
+ finalDecision: initialDecision,
1170
+ attemptStats,
1171
+ marketImpact,
1172
+ policyEval,
1173
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1174
+ flows: flowResults,
1175
+ attempts: attemptResults,
1176
+ coverage: coverageSignal
1177
+ });
1178
+
1179
+ const decisionPath = writeDecisionArtifact({
1180
+ runDir,
1181
+ runId,
1182
+ baseUrl,
1183
+ policyName,
1184
+ preset: config.preset || policyName,
1185
+ finalDecision: initialDecision,
1186
+ attemptStats,
1187
+ marketImpact,
1188
+ policyEval,
1189
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1190
+ flows: flowResults,
1191
+ resolved: resolvedConfig,
1192
+ attempts: attemptResults,
1193
+ coverage: coverageSignal,
1194
+ explanation: initialExplanation
1195
+ });
1196
+
1197
+ const summaryPath = writeRunSummary(runDir, initialDecision, attemptStats, marketImpact, policyEval, initialExplanation);
1198
+
1199
+ // Build integrity manifest over all artifacts and update evidence metrics
1200
+ try {
1201
+ const manifestInfo = writeIntegrityManifest(runDir);
1202
+ evidenceMetrics.hashedFiles = manifestInfo.hashedFiles;
1203
+ evidenceMetrics.totalFiles = manifestInfo.totalFiles;
1204
+ evidenceMetrics.integrity = manifestInfo.totalFiles > 0 ? manifestInfo.hashedFiles / manifestInfo.totalFiles : 0;
1205
+ } catch (manifestErr) {
1206
+ console.warn(`⚠️ Failed to write integrity manifest: ${manifestErr.message}`);
1207
+ evidenceMetrics.integrity = 0;
1208
+ runSignals.push({ id: 'manifest_failed', severity: 'medium', type: 'evidence', description: `Manifest generation failed: ${manifestErr.message}` });
1209
+ }
1210
+
1211
+ // Re-run policy evaluation with final evidence metrics
1212
+ policyEval = evaluatePolicy(snapshotBuilder.getSnapshot(), policyObj, policySignals);
1213
+ const finalDecision = computeFinalVerdict({
1214
+ marketImpact,
1215
+ policyEval,
1216
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1217
+ flows: flowResults,
1218
+ attempts: attemptResults
1219
+ });
1220
+ const finalExplanation = buildRealityExplanation({
1221
+ finalDecision,
1222
+ attemptStats,
1223
+ marketImpact,
1224
+ policyEval,
1225
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1226
+ flows: flowResults,
1227
+ attempts: attemptResults,
1228
+ coverage: coverageSignal
1229
+ });
1230
+ exitCode = finalDecision.exitCode;
1231
+
1232
+ // Rewrite decision + summary with final verdict
1233
+ const decisionPathFinal = writeDecisionArtifact({
1234
+ runDir,
1235
+ runId,
1236
+ baseUrl,
1237
+ policyName,
1238
+ preset: config.preset || policyName,
1239
+ finalDecision,
1240
+ attemptStats,
1241
+ marketImpact,
1242
+ policyEval,
1243
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1244
+ flows: flowResults,
1245
+ resolved: resolvedConfig,
1246
+ attempts: attemptResults,
1247
+ coverage: coverageSignal,
1248
+ explanation: finalExplanation
1249
+ });
1250
+ const summaryPathFinal = writeRunSummary(runDir, finalDecision, attemptStats, marketImpact, policyEval, finalExplanation);
1251
+
1252
+ const runResult = finalDecision.finalVerdict;
1253
+
1254
+ // Persist policy evaluation and meta into snapshot
1255
+ try {
1256
+ const snap = snapshotBuilder.getSnapshot();
1257
+ snap.policyEvaluation = policyEval;
1258
+ snap.policyName = policyName;
1259
+ snap.meta.policyHash = policyHash;
1260
+ snap.meta.preset = config.preset || presetId;
1261
+ snap.meta.evidenceMetrics = evidenceMetrics;
1262
+ snap.meta.coverage = coverageSignal;
1263
+ snap.meta.resolved = resolvedConfig;
1264
+ snap.resolved = resolvedConfig;
1265
+ snap.meta.result = finalDecision.finalVerdict;
1266
+ snap.meta.attemptsSummary = {
1267
+ executed: attemptStats.executed,
1268
+ successful: attemptStats.successful,
1269
+ failed: attemptStats.failed,
1270
+ skipped: attemptStats.skipped,
1271
+ disabled: attemptStats.disabled,
1272
+ nearSuccess: attemptStats.nearSuccess,
1273
+ nearSuccessDetails: attemptStats.nearSuccessDetails
1274
+ };
1275
+ snap.evidenceMetrics = { ...evidenceMetrics, coverage: coverageSignal };
1276
+ snap.coverage = coverageSignal;
1277
+ await saveSnapshot(snap, snapshotPathFinal);
1278
+ // Minimal attestation: sha256(policyHash + snapshotHash + manifestHash + runId)
1279
+ const snapshotHash = hashFile(snapshotPathFinal);
1280
+ let manifestHash = null;
1281
+ try { manifestHash = hashFile(path.join(runDir, 'manifest.json')); } catch {}
1282
+ const attestationHash = crypto.createHash('sha256').update(`${policyHash}|${snapshotHash}|${manifestHash || 'none'}|${runId}`).digest('hex');
1283
+ snap.meta.attestation = { hash: attestationHash, policyHash, snapshotHash, manifestHash, runId };
1284
+ await saveSnapshot(snap, snapshotPathFinal);
1285
+
1286
+ // Rewrite decision to include attestation and auditor-grade summary
1287
+ writeDecisionArtifact({
1288
+ runDir,
1289
+ runId,
1290
+ baseUrl,
1291
+ policyName,
1292
+ preset: config.preset || policyName,
1293
+ finalDecision,
1294
+ attemptStats,
1295
+ marketImpact,
1296
+ policyEval,
1297
+ baseline: { baselineCreated, baselineSnapshot, diffResult },
1298
+ flows: flowResults,
1299
+ resolved: resolvedConfig,
1300
+ attestation: snap.meta.attestation,
1301
+ audit: {
1302
+ executedAttempts: (snapshotBuilder.getSnapshot()?.attempts || []).filter(a => a.executed).map(a => a.attemptId),
1303
+ notTested: {
1304
+ disabledByPreset: (attemptStats.disabledDetails || []).map(d => d.attempt),
1305
+ userFiltered: (snap.coverage?.skippedUserFiltered || []).map(s => s.attempt),
1306
+ notApplicable: (snap.coverage?.skippedNotApplicable || []).map(s => s.attempt),
1307
+ missing: (snap.coverage?.skippedMissing || []).map(s => s.attempt)
381
1308
  }
382
- }
383
- } catch (policyErr) {
384
- console.warn(`⚠️ Policy evaluation failed (non-critical): ${policyErr.message}`);
1309
+ },
1310
+ attempts: attemptResults,
1311
+ coverage: coverageSignal,
1312
+ explanation: finalExplanation
1313
+ });
1314
+ } catch (_) {}
1315
+
1316
+ // Persist META.json
1317
+ let metaData;
1318
+ try {
1319
+ metaData = {
1320
+ runDir,
1321
+ url: baseUrl,
1322
+ siteSlug,
1323
+ policy: policyName,
1324
+ policyHash,
1325
+ preset: config.preset || presetId,
1326
+ result: runResult,
1327
+ durationMs: actualDurationMs,
1328
+ profile: siteProfile,
1329
+ attempts: attemptStats,
1330
+ evidence: evidenceMetrics,
1331
+ decisionPath: decisionPathFinal || decisionPath,
1332
+ summaryPath: summaryPathFinal || summaryPath
1333
+ };
1334
+ writeMetaJson(metaData);
1335
+ if (process.env.GUARDIAN_DEBUG) {
1336
+ console.log(`\n💾 META.json written successfully`);
385
1337
  }
1338
+ } catch (metaErr) {
1339
+ console.warn(`⚠️ Failed to write META.json: ${metaErr.message}`);
1340
+ exitCode = 1;
386
1341
  }
387
1342
 
388
1343
  // Phase 5/6: Send webhook notifications
@@ -405,77 +1360,188 @@ async function executeReality(config) {
405
1360
  }
406
1361
  }
407
1362
 
408
- // Determine exit code (including market criticality escalation + policy)
409
- let exitCode = 0;
410
- const finalSnapshot = snapshotBuilder.getSnapshot();
411
-
412
- if (baselineCreated) {
413
- // First run: check market criticality
414
- exitCode = 0;
415
- if (marketImpact.highestSeverity === 'CRITICAL') {
416
- console.log(`🚨 First run with CRITICAL market risks`);
417
- exitCode = 1;
418
- } else if (marketImpact.highestSeverity === 'WARNING') {
419
- console.log(`⚠️ First run with WARNING market risks`);
420
- exitCode = 2;
421
- }
422
- console.log(`✅ Baseline created`);
423
- } else if (baselineSnapshot) {
424
- // Subsequent runs: check for regressions + severity escalation
425
- const baselineSeverity = baselineSnapshot.marketImpactSummary?.highestSeverity || 'INFO';
426
- const currentSeverity = marketImpact.highestSeverity;
427
- const escalation = determineExitCodeFromEscalation(baselineSeverity, currentSeverity);
428
-
429
- if (escalation.escalated) {
430
- // Severity escalation is a FAILURE
431
- exitCode = 1;
432
- console.log(`🚨 Severity escalated: ${baselineSeverity} → ${currentSeverity}`);
433
- } else if (diffResult && Object.keys(diffResult.regressions).length > 0) {
434
- exitCode = 1;
435
- console.log(`❌ Regressions detected`);
436
- } else if (currentSeverity !== 'INFO') {
437
- // Still have market risks but didn't escalate
438
- exitCode = 2;
439
- console.log(`⚠️ ${currentSeverity} market risks present`);
440
- } else {
441
- exitCode = 0;
442
- console.log(`✅ No critical changes`);
1363
+ // Wave 2: Update latest pointers with finalized metadata
1364
+ try {
1365
+ const metaContent = readMetaJson(runDir);
1366
+ updateLatestGlobal(runDir, runDirName, metaContent, artifactsDir);
1367
+ updateLatestBySite(runDir, runDirName, metaContent, artifactsDir);
1368
+ if (process.env.GUARDIAN_DEBUG) {
1369
+ console.log(`✅ Latest pointers updated`);
443
1370
  }
444
- }
445
-
446
- // Override exit code if policy failed
447
- if (policyEval && !policyEval.passed) {
448
- exitCode = policyEval.exitCode || 1;
449
- console.log(`🛡️ Policy override: exit code ${exitCode}`);
1371
+ } catch (latestErr) {
1372
+ console.warn(`⚠️ Failed to update latest pointers: ${latestErr.message}`);
450
1373
  }
451
1374
 
452
1375
  return {
453
1376
  exitCode,
454
1377
  report,
455
1378
  runDir,
456
- snapshotPath,
457
- marketJsonPath: jsonPath,
458
- marketHtmlPath: htmlPath,
1379
+ snapshotPath: snapshotPathFinal,
1380
+ marketJsonPath: marketJsonPathFinal,
1381
+ marketHtmlPath: marketHtmlPathFinal,
459
1382
  attemptResults,
460
1383
  flowResults,
461
1384
  baselineCreated,
462
1385
  diffResult,
463
- snapshot: finalSnapshot,
464
- policyEval
1386
+ snapshot: snapshotBuilder.getSnapshot(),
1387
+ policyEval,
1388
+ resolved: resolvedConfig,
1389
+ finalDecision,
1390
+ explanation: finalExplanation,
1391
+ coverage: coverageSignal
465
1392
  };
466
1393
  }
467
1394
 
468
1395
  async function runRealityCLI(config) {
469
1396
  try {
470
- const result = await executeReality(config);
1397
+ // Stage II: Environment guard (before any other work)
1398
+ const { checkEnvironment, failWithEnvironmentError } = require('./env-guard');
1399
+ const envCheck = checkEnvironment();
1400
+ if (!envCheck.allOk) {
1401
+ failWithEnvironmentError(envCheck.issues);
1402
+ return; // Exit path, but just in case
1403
+ }
1404
+
1405
+ const { config: effectiveConfig, report: configReport } = applyLocalConfig(config);
1406
+
1407
+ console.log('\nConfig:');
1408
+ console.log(`- source: ${configReport.source}`);
1409
+ if (configReport.path) {
1410
+ console.log(`- path: ${configReport.path}`);
1411
+ }
1412
+ console.log('- effective:');
1413
+ console.log(` - crawl.maxPages: ${configReport.effective.crawl.maxPages}`);
1414
+ console.log(` - crawl.maxDepth: ${configReport.effective.crawl.maxDepth}`);
1415
+ console.log(` - timeouts.navigationMs: ${configReport.effective.timeouts.navigationMs}`);
1416
+ console.log(` - output.dir: ${configReport.effective.output.dir}`);
1417
+
1418
+ const cfg = effectiveConfig;
1419
+
1420
+ if (cfg.watch) {
1421
+ const { startWatchMode } = require('./watch-runner');
1422
+ const watchResult = await startWatchMode(cfg);
1423
+ if (watchResult && watchResult.watchStarted === false && typeof watchResult.exitCode === 'number') {
1424
+ process.exit(watchResult.exitCode);
1425
+ }
1426
+ // When watch is active, do not exit; watcher owns lifecycle
1427
+ return;
1428
+ }
1429
+
1430
+ const result = await executeReality(cfg);
1431
+
1432
+ // Mark first run as complete
1433
+ if (isFirstRun()) {
1434
+ markFirstRunComplete();
1435
+ }
471
1436
 
472
1437
  // Phase 6: Print enhanced CLI summary
473
- printCliSummary(result.snapshot, result.policyEval);
1438
+ const ciMode = isCiMode();
1439
+ if (!ciMode) {
1440
+ const resolved = result.resolved || {};
1441
+ console.log(`\nResolved configuration:`);
1442
+ console.log(` Preset: ${resolved.presetId || 'unknown'}`);
1443
+ console.log(` Policy: ${resolved.policyId || 'unknown'} (source: ${resolved.policySource || 'n/a'}, hash: ${resolved.policyHash || 'n/a'})`);
1444
+ if (resolved.mediaRequirements) {
1445
+ console.log(` Media requirements: screenshots=${resolved.mediaRequirements.requireScreenshots}, traces=${resolved.mediaRequirements.requireTraces}`);
1446
+ }
1447
+ if (resolved.attemptPlan) {
1448
+ console.log(` Attempt plan: enabled=${(resolved.attemptPlan.enabled || []).length}, disabled=${(resolved.attemptPlan.disabled || []).length}, userFiltered=${(resolved.attemptPlan.userFiltered || []).length}, missing=${(resolved.attemptPlan.missing || []).length}`);
1449
+ }
1450
+ if (resolved.coverage) {
1451
+ console.log(` Attempt outcomes: executed=${resolved.coverage.executedCount ?? resolved.coverage.executed}, disabled=${resolved.coverage.disabledPlannedCount ?? 0}, skippedDisabledByPreset=${resolved.coverage.skippedDisabledByPreset ?? 0}, skippedUserFiltered=${resolved.coverage.skippedUserFiltered ?? 0}, skippedNotApplicable=${resolved.coverage.skippedNotApplicable ?? 0}, skippedMissing=${resolved.coverage.skippedMissing ?? 0}`);
1452
+ }
1453
+ }
1454
+ if (ciMode) {
1455
+ const ciSummary = formatCiSummary({
1456
+ flowResults: result.flowResults || [],
1457
+ diffResult: result.diffResult || null,
1458
+ baselineCreated: result.baselineCreated || false,
1459
+ exitCode: result.exitCode,
1460
+ maxReasons: 5
1461
+ });
1462
+ console.log(ciSummary);
1463
+ } else {
1464
+ // Strict CLI summary: traceable, factual only
1465
+ const snap = result.snapshot || {};
1466
+ const meta = snap.meta || {};
1467
+ const coverage = snap.coverage || result.coverage || {};
1468
+ const counts = coverage.counts || {};
1469
+ const evidence = snap.evidenceMetrics || {};
1470
+ const resolved = snap.resolved || {};
1471
+ const finalDecision = result.finalDecision || {};
1472
+ const explanation = result.explanation || buildRealityExplanation({
1473
+ finalDecision,
1474
+ attemptStats: snap.meta?.attemptsSummary || {},
1475
+ marketImpact: snap.marketImpact || {},
1476
+ policyEval: result.policyEval,
1477
+ baseline: { diffResult: result.diffResult },
1478
+ flows: result.flowResults,
1479
+ attempts: result.attemptResults,
1480
+ coverage
1481
+ });
1482
+ const sections = explanation.sections || {};
1483
+ const verdictSection = sections['Final Verdict'] || {};
1484
+ console.log('\n' + '━'.repeat(70));
1485
+ console.log('🛡️ Guardian Reality Summary');
1486
+ console.log('━'.repeat(70) + '\n');
1487
+ console.log(`Target: ${meta.url || 'unknown'}`);
1488
+ console.log(`Run ID: ${meta.runId || 'unknown'}\n`);
1489
+ console.log(`Verdict: ${meta.result || finalDecision.finalVerdict || 'unknown'}`);
1490
+ console.log(`Exit Code: ${result.exitCode}`);
1491
+ if (verdictSection.explanation) console.log(`Reason: ${verdictSection.explanation}`);
1492
+ if (verdictSection.whyNot && verdictSection.whyNot.length > 0) {
1493
+ console.log(`Why not alternatives: ${verdictSection.whyNot.join(' ')}`);
1494
+ }
1495
+ const planned = coverage.total ?? (resolved.coverage?.total);
1496
+ const executed = counts.executedCount ?? (resolved.coverage?.executedCount) ?? coverage.executed;
1497
+ console.log(`Executed / Planned: ${executed} / ${planned}`);
1498
+ const completeness = evidence.completeness ?? resolved.evidenceMetrics?.completeness;
1499
+ const integrity = evidence.integrity ?? resolved.evidenceMetrics?.integrity;
1500
+ console.log(`Coverage Completeness: ${typeof completeness === 'number' ? completeness.toFixed(4) : completeness}`);
1501
+ console.log(`Evidence Integrity: ${typeof integrity === 'number' ? integrity.toFixed(4) : integrity}`);
1502
+ if (meta.attestation?.hash) console.log(`Attestation: ${meta.attestation.hash}`);
1503
+ const executedAttempts = (snap.attempts || []).filter(a => a.executed).map(a => a.attemptId);
1504
+ console.log('\nAudit Summary:');
1505
+ console.log(` Tested (${executedAttempts.length}): ${executedAttempts.join(', ') || 'none'}`);
1506
+ const skippedDisabled = (coverage.skippedDisabledByPreset || []).map(s => s.attempt);
1507
+ const skippedUserFiltered = (coverage.skippedUserFiltered || []).map(s => s.attempt);
1508
+ const skippedNotApplicable = (coverage.skippedNotApplicable || []).map(s => s.attempt);
1509
+ const skippedMissing = (coverage.skippedMissing || []).map(s => s.attempt);
1510
+ console.log(` Not Tested — DisabledByPreset (${skippedDisabled.length}): ${skippedDisabled.join(', ') || 'none'}`);
1511
+ console.log(` Not Tested — UserFiltered (${skippedUserFiltered.length}): ${skippedUserFiltered.join(', ') || 'none'}`);
1512
+ console.log(` Not Tested — NotApplicable (${skippedNotApplicable.length}): ${skippedNotApplicable.join(', ') || 'none'}`);
1513
+ console.log(` Not Tested — Missing (${skippedMissing.length}): ${skippedMissing.join(', ') || 'none'}`);
1514
+ if (sections['What Guardian Observed']) {
1515
+ console.log('\nWhat Guardian Observed:');
1516
+ sections['What Guardian Observed'].details.forEach(d => console.log(` • ${d}`));
1517
+ }
1518
+ if (sections['What Guardian Could Not Confirm']) {
1519
+ console.log('\nWhat Guardian Could Not Confirm:');
1520
+ sections['What Guardian Could Not Confirm'].details.forEach(d => console.log(` • ${d}`));
1521
+ }
1522
+ if (sections['Evidence Summary']) {
1523
+ console.log('\nEvidence Summary:');
1524
+ sections['Evidence Summary'].details.forEach(d => console.log(` • ${d}`));
1525
+ }
1526
+ if (sections['Limits of This Run']) {
1527
+ console.log('\nLimits of This Run:');
1528
+ sections['Limits of This Run'].details.forEach(d => console.log(` • ${d}`));
1529
+ }
1530
+ const reportBase = result.runDir || (configReport?.effective?.output?.dir ? path.join(configReport.effective.output.dir, meta.runId || '') : `artifacts/${meta.runId || ''}`);
1531
+ console.log(`\n📁 Full report: ${reportBase}\n`);
1532
+ console.log('━'.repeat(70));
1533
+ }
474
1534
 
475
1535
  process.exit(result.exitCode);
476
1536
  } catch (err) {
477
1537
  console.error(`\n❌ Error: ${err.message}`);
478
- if (err.stack) console.error(err.stack);
1538
+ if (process.env.GUARDIAN_DEBUG) {
1539
+ if (err.stack) console.error(err.stack);
1540
+ } else if (err.stack) {
1541
+ const stackLine = (err.stack.split('\n')[1] || '').trim();
1542
+ if (stackLine) console.error(` at ${stackLine}`);
1543
+ console.error(' (Set GUARDIAN_DEBUG=1 for full stack)');
1544
+ }
479
1545
  process.exit(1);
480
1546
  }
481
1547
  }
@@ -544,4 +1610,435 @@ function computeMarketRiskSummary(attemptResults) {
544
1610
  return summary;
545
1611
  }
546
1612
 
547
- module.exports = { executeReality, runRealityCLI };
1613
+ function computeFlowExitCode(flowResults) {
1614
+ if (!Array.isArray(flowResults) || flowResults.length === 0) return 0;
1615
+ const hasFailure = flowResults.some(f => f.outcome === 'FAILURE' || f.success === false);
1616
+ if (hasFailure) return 2;
1617
+ const hasFriction = flowResults.some(f => f.outcome === 'FRICTION');
1618
+ if (hasFriction) return 1;
1619
+ return 0;
1620
+ }
1621
+
1622
+ function computeFinalVerdict({ marketImpact, policyEval, baseline, flows, attempts }) {
1623
+ const reasons = [];
1624
+
1625
+ const executedAttempts = Array.isArray(attempts)
1626
+ ? attempts.filter(a => a.executed)
1627
+ : [];
1628
+ const successfulAttempts = executedAttempts.filter(a => a.outcome === 'SUCCESS');
1629
+ const failedAttempts = executedAttempts.filter(a => a.outcome === 'FAILURE');
1630
+ const frictionAttempts = executedAttempts.filter(a => a.outcome === 'FRICTION');
1631
+
1632
+ const flowList = Array.isArray(flows) ? flows : [];
1633
+ const failedFlows = flowList.filter(f => f.outcome === 'FAILURE' || f.success === false);
1634
+ const frictionFlows = flowList.filter(f => f.outcome === 'FRICTION');
1635
+ const observedFlows = successfulAttempts.map(a => a.attemptId || a.id || 'unknown');
1636
+
1637
+ // Observation summary always first
1638
+ if (executedAttempts.length > 0) {
1639
+ reasons.push({ code: 'OBSERVED', message: `Observed ${executedAttempts.length} attempted flow(s); successful=${successfulAttempts.length}, failed=${failedAttempts.length}, friction=${frictionAttempts.length}.` });
1640
+ }
1641
+
1642
+ // Baseline regressions
1643
+ const diff = baseline?.diffResult || baseline?.diff;
1644
+ if (diff && diff.regressions && Object.keys(diff.regressions).length > 0) {
1645
+ const regressionAttempts = Object.keys(diff.regressions);
1646
+ reasons.push({ code: 'BASELINE_REGRESSION', message: `Baseline regressions detected for: ${regressionAttempts.join(', ')}.` });
1647
+ }
1648
+
1649
+ // Policy evaluation evidence
1650
+ if (policyEval) {
1651
+ if (!policyEval.passed && policyEval.summary) {
1652
+ reasons.push({ code: 'POLICY', message: policyEval.summary });
1653
+ } else if (!policyEval.passed) {
1654
+ reasons.push({ code: 'POLICY', message: 'Policy conditions not satisfied; evidence insufficient.' });
1655
+ }
1656
+ }
1657
+
1658
+ // Market impact evidence
1659
+ if (marketImpact && marketImpact.highestSeverity) {
1660
+ reasons.push({ code: 'MARKET_IMPACT', message: `Market impact severity observed: ${marketImpact.highestSeverity}.` });
1661
+ }
1662
+
1663
+ // Flow/attempt failures
1664
+ if (failedAttempts.length > 0) {
1665
+ reasons.push({ code: 'CRITICAL_FLOW_FAILURE', message: `Critical flows failed: ${failedAttempts.map(a => a.attemptId || a.id || 'unknown').join(', ')}.` });
1666
+ }
1667
+ if (failedFlows.length > 0) {
1668
+ reasons.push({ code: 'FLOW_FAILURE', message: `Flow executions failed: ${failedFlows.map(f => f.flowId || f.flowName || 'unknown').join(', ')}.` });
1669
+ }
1670
+ if (frictionAttempts.length > 0 || frictionFlows.length > 0) {
1671
+ const frictionIds = [
1672
+ ...frictionAttempts.map(a => a.attemptId || a.id || 'unknown'),
1673
+ ...frictionFlows.map(f => f.flowId || f.flowName || 'unknown')
1674
+ ];
1675
+ reasons.push({ code: 'FLOW_FRICTION', message: `Flows with friction: ${frictionIds.join(', ')}.` });
1676
+ }
1677
+
1678
+ // Determine verdict
1679
+ let internalVerdict;
1680
+ let finalVerdict;
1681
+ let exitCode;
1682
+
1683
+ if (executedAttempts.length === 0 && flowList.length === 0) {
1684
+ internalVerdict = 'INSUFFICIENT_DATA';
1685
+ reasons.unshift({ code: 'NO_OBSERVATIONS', message: 'No meaningful flows executed; only static or configuration checks available.' });
1686
+ } else if (failedAttempts.length === 0 && failedFlows.length === 0 && frictionAttempts.length === 0 && frictionFlows.length === 0 && (!policyEval || policyEval.passed)) {
1687
+ internalVerdict = 'OBSERVED';
1688
+ if (observedFlows.length > 0) {
1689
+ reasons.push({ code: 'OBSERVED_FLOWS', message: `Observed end-to-end flows: ${observedFlows.join(', ')}.` });
1690
+ }
1691
+ reasons.push({ code: 'SCOPE', message: 'Verdict based solely on executed flows; no claim about business readiness beyond observed actions.' });
1692
+ } else {
1693
+ internalVerdict = 'PARTIAL';
1694
+ if (observedFlows.length > 0) {
1695
+ reasons.push({ code: 'PARTIAL_SCOPE', message: `Some flows observed (${observedFlows.join(', ')}), but at least one flow failed or could not be confirmed.` });
1696
+ } else {
1697
+ reasons.push({ code: 'PARTIAL_SCOPE', message: 'Flows were attempted but outcomes include failures or friction; observations are incomplete.' });
1698
+ }
1699
+ }
1700
+
1701
+ // Ensure deterministic ordering: sort by code then message
1702
+ const orderedReasons = reasons
1703
+ .filter(r => r && r.code && r.message)
1704
+ .sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
1705
+
1706
+ finalVerdict = toCanonicalVerdict(internalVerdict);
1707
+ exitCode = mapExitCodeFromCanonical(finalVerdict);
1708
+ return { finalVerdict, exitCode, reasons: orderedReasons };
1709
+ }
1710
+
1711
+ function hashFile(filePath) {
1712
+ const data = fs.readFileSync(filePath);
1713
+ return crypto.createHash('sha256').update(data).digest('hex');
1714
+ }
1715
+
1716
+ function writeIntegrityManifest(runDir) {
1717
+ const manifest = {
1718
+ generatedAt: new Date().toISOString(),
1719
+ files: []
1720
+ };
1721
+
1722
+ const allFiles = [];
1723
+
1724
+ const walk = (dir) => {
1725
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1726
+ for (const entry of entries) {
1727
+ const full = path.join(dir, entry.name);
1728
+ if (entry.isDirectory()) {
1729
+ walk(full);
1730
+ } else {
1731
+ allFiles.push(full);
1732
+ }
1733
+ }
1734
+ };
1735
+
1736
+ walk(runDir);
1737
+
1738
+ for (const target of allFiles) {
1739
+ if (fs.existsSync(target)) {
1740
+ manifest.files.push({
1741
+ path: path.relative(runDir, target),
1742
+ sha256: hashFile(target)
1743
+ });
1744
+ }
1745
+ }
1746
+
1747
+ const manifestPath = path.join(runDir, 'manifest.json');
1748
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1749
+ return {
1750
+ manifestPath,
1751
+ hashedFiles: manifest.files.length,
1752
+ totalFiles: allFiles.length
1753
+ };
1754
+ }
1755
+
1756
+ function buildRealityExplanation({ finalDecision = {}, attemptStats = {}, marketImpact = {}, policyEval = {}, baseline = {}, flows = [], attempts = [], coverage = {} }) {
1757
+ const verdict = finalDecision.finalVerdict || 'UNKNOWN';
1758
+ const exitCode = typeof finalDecision.exitCode === 'number' ? finalDecision.exitCode : 1;
1759
+
1760
+ const executedAttempts = (attempts || []).filter(isExecutedAttempt);
1761
+ const successes = executedAttempts.filter(a => a.outcome === 'SUCCESS');
1762
+ const failures = executedAttempts.filter(a => a.outcome === 'FAILURE');
1763
+ const frictions = executedAttempts.filter(a => a.outcome === 'FRICTION');
1764
+
1765
+ const flowList = Array.isArray(flows) ? flows : [];
1766
+ const flowFailures = flowList.filter(f => f.outcome === 'FAILURE' || f.success === false);
1767
+ const flowFrictions = flowList.filter(f => f.outcome === 'FRICTION');
1768
+ const flowSuccesses = flowList.filter(f => f.outcome === 'SUCCESS' || f.success === true);
1769
+
1770
+ const observedDetails = [];
1771
+ if (executedAttempts.length > 0) {
1772
+ const attemptSummary = executedAttempts
1773
+ .map(a => `${a.attemptId || a.id || 'unknown'} (${a.outcome || 'unknown'})`)
1774
+ .sort();
1775
+ observedDetails.push(`Executed ${executedAttempts.length} attempt(s): ${attemptSummary.join(', ')}.`);
1776
+ } else {
1777
+ observedDetails.push('No user journeys executed; evidence limited to crawl/policy signals.');
1778
+ }
1779
+ if (flowSuccesses.length > 0) {
1780
+ const successFlows = flowSuccesses
1781
+ .map(f => f.flowId || f.flowName || 'unknown')
1782
+ .sort();
1783
+ observedDetails.push(`Successful flows: ${successFlows.join(', ')}.`);
1784
+ }
1785
+
1786
+ const couldNotConfirm = [];
1787
+ if (failures.length > 0) {
1788
+ const failureAttempts = failures
1789
+ .map(a => `${a.attemptId || a.id || 'unknown'} (${a.error ? String(a.error).split('\n')[0] : a.outcome})`)
1790
+ .sort();
1791
+ couldNotConfirm.push(`Failures detected in: ${failureAttempts.join(', ')}.`);
1792
+ }
1793
+ if (frictions.length > 0) {
1794
+ const frictionAttempts = frictions.map(a => a.attemptId || a.id || 'unknown').sort();
1795
+ couldNotConfirm.push(`Friction observed in: ${frictionAttempts.join(', ')}.`);
1796
+ }
1797
+ if (flowFailures.length > 0) {
1798
+ const ff = flowFailures.map(f => f.flowId || f.flowName || 'unknown').sort();
1799
+ couldNotConfirm.push(`Flow failures: ${ff.join(', ')}.`);
1800
+ }
1801
+ if (flowFrictions.length > 0) {
1802
+ const ff = flowFrictions.map(f => f.flowId || f.flowName || 'unknown').sort();
1803
+ couldNotConfirm.push(`Flow friction: ${ff.join(', ')}.`);
1804
+ }
1805
+ if (policyEval && policyEval.passed === false) {
1806
+ const reason = policyEval.summary || 'Policy conditions not satisfied.';
1807
+ couldNotConfirm.push(`Policy check failed: ${reason}`);
1808
+ }
1809
+ const diff = baseline?.diffResult || baseline?.diff;
1810
+ if (diff && diff.regressions && Object.keys(diff.regressions).length > 0) {
1811
+ const regressionAttempts = Object.keys(diff.regressions).sort();
1812
+ couldNotConfirm.push(`Baseline regressions: ${regressionAttempts.join(', ')}.`);
1813
+ }
1814
+ if (coverage && typeof coverage.gaps === 'number' && coverage.gaps > 0) {
1815
+ couldNotConfirm.push(`Coverage gaps: ${coverage.gaps} planned attempt(s) not observed.`);
1816
+ }
1817
+ if (couldNotConfirm.length === 0) {
1818
+ couldNotConfirm.push('No outstanding gaps; all planned evidence confirmed for observed scope.');
1819
+ }
1820
+
1821
+ const evidenceSummary = [];
1822
+ const executedCount = attemptStats.executed ?? executedAttempts.length;
1823
+ const totalPlanned = attemptStats.total ?? attemptStats.enabledPlannedCount ?? executedAttempts.length;
1824
+ evidenceSummary.push(`Attempt outcomes: executed=${executedCount}, success=${attemptStats.successful ?? successes.length}, failed=${attemptStats.failed ?? failures.length}, skipped=${attemptStats.skipped ?? 0}.`);
1825
+ evidenceSummary.push(`Flow outcomes: success=${flowSuccesses.length}, failures=${flowFailures.length}, friction=${flowFrictions.length}.`);
1826
+ if (marketImpact && marketImpact.highestSeverity) {
1827
+ evidenceSummary.push(`Market severity observed: ${marketImpact.highestSeverity}.`);
1828
+ } else {
1829
+ evidenceSummary.push('Market severity not reported.');
1830
+ }
1831
+ if (policyEval && typeof policyEval.passed === 'boolean') {
1832
+ evidenceSummary.push(`Policy evaluation: ${policyEval.passed ? 'passed' : 'failed'} (exit ${policyEval.exitCode ?? 'unknown'}).`);
1833
+ } else {
1834
+ evidenceSummary.push('Policy evaluation: not run.');
1835
+ }
1836
+ if (diff && diff.regressions) {
1837
+ const regressions = Object.keys(diff.regressions);
1838
+ evidenceSummary.push(`Baseline regressions: ${regressions.length > 0 ? regressions.join(', ') : 'none'}.`);
1839
+ } else {
1840
+ evidenceSummary.push('Baseline comparison: none or no regressions detected.');
1841
+ }
1842
+
1843
+ const limits = [];
1844
+ const gaps = coverage && typeof coverage.gaps === 'number' ? coverage.gaps : Math.max((totalPlanned || 0) - (executedCount || 0), 0);
1845
+ limits.push(`Coverage: ${executedCount}/${totalPlanned || 0} attempts executed; gaps=${gaps}.`);
1846
+ if (attemptStats.disabled) {
1847
+ limits.push(`Disabled by preset: ${attemptStats.disabled} attempt(s).`);
1848
+ }
1849
+ if (attemptStats.skippedUserFiltered) {
1850
+ limits.push(`User-filtered skips: ${attemptStats.skippedUserFiltered}.`);
1851
+ }
1852
+ if (attemptStats.skippedNotApplicable) {
1853
+ limits.push(`Not-applicable skips: ${attemptStats.skippedNotApplicable}.`);
1854
+ }
1855
+ if (attemptStats.skippedMissing) {
1856
+ limits.push(`Missing engine skips: ${attemptStats.skippedMissing}.`);
1857
+ }
1858
+ if (limits.length === 0) {
1859
+ limits.push('No additional limits detected beyond observed attempts.');
1860
+ }
1861
+
1862
+ let whyThisVerdict;
1863
+ const whyNotList = [];
1864
+ if (verdict === 'OBSERVED') {
1865
+ whyThisVerdict = 'All executed attempts succeeded; no failures, friction, or policy blockers detected.';
1866
+ whyNotList.push('Not PARTIAL because no failures, friction, or policy shortfalls remain.');
1867
+ whyNotList.push(`Not INSUFFICIENT_DATA because ${executedCount} attempt(s) executed with complete evidence.`);
1868
+ } else if (verdict === 'PARTIAL') {
1869
+ const partialDrivers = [];
1870
+ if (failures.length > 0) partialDrivers.push(`${failures.length} failed attempt(s)`);
1871
+ if (frictions.length > 0) partialDrivers.push(`${frictions.length} friction attempt(s)`);
1872
+ if (flowFailures.length > 0) partialDrivers.push(`${flowFailures.length} failed flow(s)`);
1873
+ if (policyEval && policyEval.passed === false) partialDrivers.push('policy not satisfied');
1874
+ whyThisVerdict = partialDrivers.length > 0
1875
+ ? `Evidence is mixed: ${partialDrivers.join('; ')}.`
1876
+ : 'Evidence incomplete or mixed; not all planned signals confirmed.';
1877
+ whyNotList.push('Not OBSERVED because at least one failure, friction, or policy issue remains.');
1878
+ whyNotList.push(`Not INSUFFICIENT_DATA because ${executedCount} attempt(s) executed and produced evidence.`);
1879
+ } else if (verdict === 'INSUFFICIENT_DATA') {
1880
+ whyThisVerdict = 'No meaningful user flows were executed; evidence is insufficient to claim readiness.';
1881
+ whyNotList.push('Not OBSERVED because no successful flows were confirmed.');
1882
+ whyNotList.push('Not PARTIAL because there was no executable evidence to partially support readiness.');
1883
+ } else {
1884
+ whyThisVerdict = 'Verdict unavailable; using conservative interpretation of observed evidence.';
1885
+ whyNotList.push('Alternative verdicts not evaluated due to unknown state.');
1886
+ }
1887
+
1888
+ const finalVerdictSection = {
1889
+ verdict,
1890
+ exitCode,
1891
+ explanation: whyThisVerdict,
1892
+ whyNot: whyNotList,
1893
+ reasons: (finalDecision.reasons || []).map(r => `${r.code}: ${r.message}`)
1894
+ };
1895
+
1896
+ const sections = {
1897
+ 'Final Verdict': finalVerdictSection,
1898
+ 'What Guardian Observed': {
1899
+ summary: observedDetails[0],
1900
+ details: observedDetails
1901
+ },
1902
+ 'What Guardian Could Not Confirm': {
1903
+ summary: couldNotConfirm[0],
1904
+ details: couldNotConfirm
1905
+ },
1906
+ 'Evidence Summary': {
1907
+ summary: evidenceSummary[0],
1908
+ details: evidenceSummary
1909
+ },
1910
+ 'Limits of This Run': {
1911
+ summary: limits[0],
1912
+ details: limits
1913
+ }
1914
+ };
1915
+
1916
+ return { verdict: finalVerdictSection, sections };
1917
+ }
1918
+
1919
+ function writeDecisionArtifact({ runDir, runId, baseUrl, policyName, preset, finalDecision, attemptStats, marketImpact, policyEval, baseline, flows, resolved, attestation, audit, attempts = [], coverage = {}, explanation }) {
1920
+ const structuredExplanation = explanation || buildRealityExplanation({ finalDecision, attemptStats, marketImpact, policyEval, baseline, flows, attempts, coverage });
1921
+ const safePolicyEval = policyEval || { passed: true, exitCode: 0, summary: 'Policy evaluation not run.' };
1922
+ const diff = baseline?.diffResult || baseline?.diff || {};
1923
+ const auditSummary = audit ? {
1924
+ tested: Array.isArray(audit.executedAttempts) ? audit.executedAttempts : [],
1925
+ notTested: {
1926
+ disabledByPreset: audit.notTested?.disabledByPreset || [],
1927
+ userFiltered: audit.notTested?.userFiltered || [],
1928
+ notApplicable: audit.notTested?.notApplicable || [],
1929
+ missing: audit.notTested?.missing || []
1930
+ }
1931
+ } : {
1932
+ tested: [],
1933
+ notTested: { disabledByPreset: [], userFiltered: [], notApplicable: [], missing: [] }
1934
+ };
1935
+
1936
+ const decision = {
1937
+ runId,
1938
+ url: baseUrl,
1939
+ timestamp: new Date().toISOString(),
1940
+ preset: preset || 'default',
1941
+ policyName: policyName || 'unknown',
1942
+ finalVerdict: finalDecision.finalVerdict,
1943
+ exitCode: finalDecision.exitCode,
1944
+ reasons: finalDecision.reasons,
1945
+ resolved: resolved || {},
1946
+ attestation: attestation || {},
1947
+ counts: {
1948
+ attemptsExecuted: attemptStats.executed || 0,
1949
+ successful: attemptStats.successful || 0,
1950
+ failed: attemptStats.failed || 0,
1951
+ skipped: attemptStats.skipped || 0,
1952
+ nearSuccess: attemptStats.nearSuccess || 0
1953
+ },
1954
+ inputs: {
1955
+ policy: safePolicyEval,
1956
+ baseline: diff,
1957
+ market: marketImpact || {},
1958
+ flows: {
1959
+ total: Array.isArray(flows) ? flows.length : 0,
1960
+ failures: Array.isArray(flows) ? flows.filter(f => f.outcome === 'FAILURE' || f.success === false).length : 0,
1961
+ frictions: Array.isArray(flows) ? flows.filter(f => f.outcome === 'FRICTION').length : 0
1962
+ }
1963
+ },
1964
+ coverage: {
1965
+ total: coverage.total || attemptStats.enabledPlannedCount || attemptStats.total || 0,
1966
+ executed: coverage.executed || attemptStats.executed || 0,
1967
+ gaps: coverage.gaps ?? Math.max((attemptStats.total || 0) - (attemptStats.executed || 0), 0),
1968
+ skipped: coverage.details || attemptStats.skippedDetails || [],
1969
+ disabled: coverage.disabled || attemptStats.disabledDetails || []
1970
+ },
1971
+ auditSummary,
1972
+ sections: structuredExplanation.sections,
1973
+ explanation: structuredExplanation.verdict
1974
+ };
1975
+
1976
+ const decisionPath = path.join(runDir, 'decision.json');
1977
+ fs.writeFileSync(decisionPath, JSON.stringify(decision, null, 2));
1978
+ return decisionPath;
1979
+ }
1980
+
1981
+ function writeRunSummary(runDir, finalDecision, attemptStats, marketImpact, policyEval, explanation) {
1982
+ const structuredExplanation = explanation || buildRealityExplanation({ finalDecision, attemptStats, marketImpact, policyEval });
1983
+ const sections = structuredExplanation.sections;
1984
+
1985
+ const lines = [];
1986
+ lines.push(`Final Verdict: ${finalDecision.finalVerdict} (exit ${finalDecision.exitCode})`);
1987
+ lines.push(`Why this verdict: ${sections['Final Verdict'].explanation}`);
1988
+ if (sections['Final Verdict'].whyNot && sections['Final Verdict'].whyNot.length > 0) {
1989
+ lines.push(`Why not alternatives: ${sections['Final Verdict'].whyNot.join(' ')}`);
1990
+ }
1991
+ lines.push('');
1992
+ lines.push('What Guardian Observed:');
1993
+ sections['What Guardian Observed'].details.forEach(d => lines.push(`- ${d}`));
1994
+ lines.push('');
1995
+ lines.push('What Guardian Could Not Confirm:');
1996
+ sections['What Guardian Could Not Confirm'].details.forEach(d => lines.push(`- ${d}`));
1997
+ lines.push('');
1998
+ lines.push('Evidence Summary:');
1999
+ sections['Evidence Summary'].details.forEach(d => lines.push(`- ${d}`));
2000
+ lines.push('');
2001
+ lines.push('Limits of This Run:');
2002
+ sections['Limits of This Run'].details.forEach(d => lines.push(`- ${d}`));
2003
+
2004
+ const summaryPath = path.join(runDir, 'summary.txt');
2005
+ fs.writeFileSync(summaryPath, lines.join('\n'));
2006
+ // Also emit a markdown version for consistency with report discovery
2007
+ try {
2008
+ const mdLines = [];
2009
+ mdLines.push(`# Guardian Reality Summary`);
2010
+ mdLines.push('');
2011
+
2012
+ mdLines.push(`## Final Verdict`);
2013
+ mdLines.push(`- Verdict: ${finalDecision.finalVerdict} (exit ${finalDecision.exitCode})`);
2014
+ mdLines.push(`- Why this verdict: ${sections['Final Verdict'].explanation}`);
2015
+ if (sections['Final Verdict'].whyNot && sections['Final Verdict'].whyNot.length > 0) {
2016
+ sections['Final Verdict'].whyNot.forEach(reason => mdLines.push(`- ${reason}`));
2017
+ }
2018
+ if (sections['Final Verdict'].reasons && sections['Final Verdict'].reasons.length > 0) {
2019
+ mdLines.push(`- Evidence reasons: ${sections['Final Verdict'].reasons.join(' ')}`);
2020
+ }
2021
+
2022
+ mdLines.push('');
2023
+ mdLines.push(`## What Guardian Observed`);
2024
+ sections['What Guardian Observed'].details.forEach(d => mdLines.push(`- ${d}`));
2025
+
2026
+ mdLines.push('');
2027
+ mdLines.push(`## What Guardian Could Not Confirm`);
2028
+ sections['What Guardian Could Not Confirm'].details.forEach(d => mdLines.push(`- ${d}`));
2029
+
2030
+ mdLines.push('');
2031
+ mdLines.push(`## Evidence Summary`);
2032
+ sections['Evidence Summary'].details.forEach(d => mdLines.push(`- ${d}`));
2033
+
2034
+ mdLines.push('');
2035
+ mdLines.push(`## Limits of This Run`);
2036
+ sections['Limits of This Run'].details.forEach(d => mdLines.push(`- ${d}`));
2037
+
2038
+ const summaryMdPath = path.join(runDir, 'summary.md');
2039
+ fs.writeFileSync(summaryMdPath, mdLines.join('\n'));
2040
+ } catch (_) {}
2041
+ return summaryPath;
2042
+ }
2043
+
2044
+ module.exports = { executeReality, runRealityCLI, computeFlowExitCode, applySafeDefaults, writeDecisionArtifact, computeFinalVerdict };