@odavl/guardian 0.1.0-rc1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -0
- package/README.md +155 -97
- package/bin/guardian.js +1544 -55
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +26 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +587 -12
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +85 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +50 -8
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +38 -0
- package/src/guardian/cli-summary.js +167 -67
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +54 -0
- package/src/guardian/flag-validator.js +111 -0
- package/src/guardian/flow-executor.js +309 -44
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/language-detection.js +99 -0
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +357 -82
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +27 -18
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +1612 -115
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/run-summary.js +20 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +201 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +69 -3
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +181 -0
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
package/src/guardian/reality.js
CHANGED
|
@@ -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 {
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
const
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
attemptId,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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:
|
|
192
|
-
goal:
|
|
193
|
-
riskCategory:
|
|
194
|
-
source:
|
|
195
|
-
|
|
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(
|
|
212
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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:
|
|
458
|
-
marketHtmlPath:
|
|
1379
|
+
snapshotPath: snapshotPathFinal,
|
|
1380
|
+
marketJsonPath: marketJsonPathFinal,
|
|
1381
|
+
marketHtmlPath: marketHtmlPathFinal,
|
|
459
1382
|
attemptResults,
|
|
460
1383
|
flowResults,
|
|
461
1384
|
baselineCreated,
|
|
462
1385
|
diffResult,
|
|
463
|
-
snapshot:
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 };
|