@odavl/guardian 0.1.0-rc1 → 0.2.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 +62 -0
- package/README.md +3 -3
- package/bin/guardian.js +212 -8
- package/package.json +6 -1
- package/src/guardian/attempt-engine.js +19 -5
- package/src/guardian/attempt.js +61 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +44 -10
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +37 -0
- package/src/guardian/cli-summary.js +117 -4
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -0
- package/src/guardian/first-run.js +49 -0
- package/src/guardian/flag-validator.js +97 -0
- package/src/guardian/flow-executor.js +309 -44
- package/src/guardian/language-detection.js +99 -0
- package/src/guardian/market-reporter.js +16 -1
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +18 -12
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +382 -46
- package/src/guardian/run-summary.js +20 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +200 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot.js +23 -1
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +185 -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');
|
|
@@ -18,6 +18,19 @@ const { aggregateIntelligence } = require('./breakage-intelligence');
|
|
|
18
18
|
const { writeEnhancedHtml } = require('./enhanced-html-reporter');
|
|
19
19
|
const { printCliSummary } = require('./cli-summary');
|
|
20
20
|
const { sendWebhooks, getWebhookUrl, buildWebhookPayload } = require('./webhook');
|
|
21
|
+
const { findContactOnPage, formatDetectionForReport } = require('./semantic-contact-finder');
|
|
22
|
+
const { formatRunSummary } = require('./run-summary');
|
|
23
|
+
const { isCiMode } = require('./ci-mode');
|
|
24
|
+
const { formatCiSummary, deriveBaselineVerdict } = require('./ci-output');
|
|
25
|
+
// Phase 7.1: Performance modes
|
|
26
|
+
const { getTimeoutProfile } = require('./timeout-profiles');
|
|
27
|
+
const { validateAttemptFilter, filterAttempts, filterFlows } = require('./attempts-filter');
|
|
28
|
+
// Phase 7.2: Parallel execution
|
|
29
|
+
const { executeParallel, validateParallel } = require('./parallel-executor');
|
|
30
|
+
// Phase 7.3: Browser reuse
|
|
31
|
+
const { BrowserPool } = require('./browser-pool');
|
|
32
|
+
// Phase 7.4: Smart skips
|
|
33
|
+
const { checkPrerequisites } = require('./prerequisite-checker');
|
|
21
34
|
|
|
22
35
|
function generateRunId(prefix = 'market-run') {
|
|
23
36
|
const now = new Date();
|
|
@@ -25,7 +38,23 @@ function generateRunId(prefix = 'market-run') {
|
|
|
25
38
|
return `${prefix}-${dateStr}`;
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
function applySafeDefaults(config, warn) {
|
|
42
|
+
const updated = { ...config };
|
|
43
|
+
if (!Array.isArray(updated.attempts) || updated.attempts.length === 0) {
|
|
44
|
+
if (warn) warn('No attempts provided; using curated defaults.');
|
|
45
|
+
updated.attempts = getDefaultAttemptIds();
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(updated.flows) || updated.flows.length === 0) {
|
|
48
|
+
if (warn) warn('No flows provided; using curated defaults.');
|
|
49
|
+
updated.flows = getDefaultFlowIds();
|
|
50
|
+
}
|
|
51
|
+
return updated;
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
async function executeReality(config) {
|
|
55
|
+
const baseWarn = (...args) => console.warn(...args);
|
|
56
|
+
const safeConfig = applySafeDefaults(config, baseWarn);
|
|
57
|
+
|
|
29
58
|
const {
|
|
30
59
|
baseUrl,
|
|
31
60
|
attempts = getDefaultAttemptIds(),
|
|
@@ -47,8 +76,52 @@ async function executeReality(config) {
|
|
|
47
76
|
autoAttemptOptions = {},
|
|
48
77
|
enableFlows = true,
|
|
49
78
|
flows = getDefaultFlowIds(),
|
|
50
|
-
flowOptions = {}
|
|
51
|
-
|
|
79
|
+
flowOptions = {},
|
|
80
|
+
// Phase 7.1: Performance modes
|
|
81
|
+
timeoutProfile = 'default',
|
|
82
|
+
failFast = false,
|
|
83
|
+
fast = false,
|
|
84
|
+
attemptsFilter = null,
|
|
85
|
+
// Phase 7.2: Parallel execution
|
|
86
|
+
parallel = 1
|
|
87
|
+
} = safeConfig;
|
|
88
|
+
|
|
89
|
+
// Phase 7.1: Validate and apply attempts filter
|
|
90
|
+
let validation = null;
|
|
91
|
+
if (attemptsFilter) {
|
|
92
|
+
validation = validateAttemptFilter(attemptsFilter);
|
|
93
|
+
if (!validation.valid) {
|
|
94
|
+
console.error(`Error: ${validation.error}`);
|
|
95
|
+
if (validation.hint) console.error(`Hint: ${validation.hint}`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Phase 7.2: Validate parallel value
|
|
101
|
+
const parallelValidation = validateParallel(parallel);
|
|
102
|
+
if (!parallelValidation.valid) {
|
|
103
|
+
console.error(`Error: ${parallelValidation.error}`);
|
|
104
|
+
if (parallelValidation.hint) console.error(`Hint: ${parallelValidation.hint}`);
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
const validatedParallel = parallelValidation.parallel || 1;
|
|
108
|
+
|
|
109
|
+
// Phase 7.1: Filter attempts and flows
|
|
110
|
+
let filteredAttempts = attempts;
|
|
111
|
+
let filteredFlows = flows;
|
|
112
|
+
if (attemptsFilter && validation && validation.valid && validation.ids.length > 0) {
|
|
113
|
+
filteredAttempts = filterAttempts(attempts, validation.ids);
|
|
114
|
+
filteredFlows = filterFlows(flows, validation.ids);
|
|
115
|
+
if (filteredAttempts.length === 0 && filteredFlows.length === 0) {
|
|
116
|
+
console.error('Error: No matching attempts or flows found after filtering');
|
|
117
|
+
console.error(`Hint: Check your --attempts filter: ${attemptsFilter}`);
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Phase 7.1: Resolve timeout profile
|
|
123
|
+
const timeoutProfileConfig = getTimeoutProfile(timeoutProfile);
|
|
124
|
+
const resolvedTimeout = timeout || timeoutProfileConfig.default;
|
|
52
125
|
|
|
53
126
|
// Validate baseUrl
|
|
54
127
|
try {
|
|
@@ -60,12 +133,32 @@ async function executeReality(config) {
|
|
|
60
133
|
const runId = generateRunId();
|
|
61
134
|
const runDir = path.join(artifactsDir, runId);
|
|
62
135
|
fs.mkdirSync(runDir, { recursive: true });
|
|
136
|
+
const ciMode = isCiMode();
|
|
137
|
+
|
|
138
|
+
// Phase 7.1: Print mode info
|
|
139
|
+
if (!ciMode) {
|
|
140
|
+
const modeLines = [];
|
|
141
|
+
if (fast) modeLines.push('MODE: fast');
|
|
142
|
+
if (failFast) modeLines.push('FAIL-FAST: enabled');
|
|
143
|
+
if (timeoutProfile !== 'default') modeLines.push(`TIMEOUT: ${timeoutProfile}`);
|
|
144
|
+
if (attemptsFilter) modeLines.push(`ATTEMPTS: ${attemptsFilter}`);
|
|
145
|
+
if (modeLines.length > 0) {
|
|
146
|
+
console.log(`\n⚡ ${modeLines.join(' | ')}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
63
149
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
if (ciMode) {
|
|
151
|
+
console.log(`\nCI RUN: Market Reality Snapshot`);
|
|
152
|
+
console.log(`Base URL: ${baseUrl}`);
|
|
153
|
+
console.log(`Attempts: ${filteredAttempts.join(', ')}`);
|
|
154
|
+
console.log(`Run Dir: ${runDir}`);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`\n🧪 Market Reality Snapshot v1`);
|
|
157
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
158
|
+
console.log(`📍 Base URL: ${baseUrl}`);
|
|
159
|
+
console.log(`🎯 Attempts: ${filteredAttempts.join(', ')}`);
|
|
160
|
+
console.log(`📁 Run Dir: ${runDir}`);
|
|
161
|
+
}
|
|
69
162
|
|
|
70
163
|
// Initialize snapshot builder
|
|
71
164
|
const snapshotBuilder = new SnapshotBuilder(baseUrl, runId, toolVersion);
|
|
@@ -73,13 +166,27 @@ async function executeReality(config) {
|
|
|
73
166
|
|
|
74
167
|
let crawlResult = null;
|
|
75
168
|
let discoveryResult = null;
|
|
169
|
+
let pageLanguage = 'unknown';
|
|
170
|
+
let contactDetectionResult = null;
|
|
76
171
|
|
|
77
172
|
// Optional: Crawl to discover URLs (lightweight, first N pages)
|
|
78
173
|
if (enableCrawl) {
|
|
79
174
|
console.log(`\n🔍 Crawling for discovered URLs...`);
|
|
80
175
|
const browser = new GuardianBrowser();
|
|
81
176
|
try {
|
|
82
|
-
await browser.launch(
|
|
177
|
+
await browser.launch(resolvedTimeout);
|
|
178
|
+
await browser.page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: resolvedTimeout });
|
|
179
|
+
|
|
180
|
+
// Wave 1.1: Detect page language and contact
|
|
181
|
+
try {
|
|
182
|
+
contactDetectionResult = await findContactOnPage(browser.page, baseUrl);
|
|
183
|
+
pageLanguage = contactDetectionResult.language;
|
|
184
|
+
console.log(`\n${formatDetectionForReport(contactDetectionResult)}\n`);
|
|
185
|
+
} catch (detectionErr) {
|
|
186
|
+
// Language detection non-critical
|
|
187
|
+
console.warn(`⚠️ Language/contact detection failed: ${detectionErr.message}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
83
190
|
const crawler = new GuardianCrawler(baseUrl, maxPages, maxDepth);
|
|
84
191
|
crawlResult = await crawler.crawl(browser);
|
|
85
192
|
console.log(`✅ Crawl complete: discovered ${crawlResult.totalDiscovered}, visited ${crawlResult.totalVisited}`);
|
|
@@ -96,11 +203,11 @@ async function executeReality(config) {
|
|
|
96
203
|
console.log(`\n🔎 Running discovery engine...`);
|
|
97
204
|
const browser = new GuardianBrowser();
|
|
98
205
|
try {
|
|
99
|
-
await browser.launch(
|
|
206
|
+
await browser.launch(resolvedTimeout);
|
|
100
207
|
const engine = new DiscoveryEngine({
|
|
101
208
|
baseUrl,
|
|
102
209
|
maxPages,
|
|
103
|
-
timeout,
|
|
210
|
+
timeout: resolvedTimeout,
|
|
104
211
|
executeInteractions: false,
|
|
105
212
|
browser,
|
|
106
213
|
});
|
|
@@ -167,74 +274,237 @@ async function executeReality(config) {
|
|
|
167
274
|
}
|
|
168
275
|
}
|
|
169
276
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
277
|
+
// Phase 7.1: Apply attempts filter
|
|
278
|
+
if (attemptsFilter && validation && validation.valid && validation.ids.length > 0) {
|
|
279
|
+
attemptsToRun = filterAttempts(attemptsToRun, validation.ids);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Phase 7.2: Print parallel mode if enabled
|
|
283
|
+
if (!ciMode && validatedParallel > 1) {
|
|
284
|
+
console.log(`\n⚡ PARALLEL: ${validatedParallel} concurrent attempts`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Phase 7.3: Initialize browser pool (single browser per run)
|
|
288
|
+
const browserPool = new BrowserPool();
|
|
289
|
+
const browserOptions = {
|
|
290
|
+
headless: !headful,
|
|
291
|
+
args: !headful ? [] : [],
|
|
292
|
+
timeout: resolvedTimeout
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
await browserPool.launch(browserOptions);
|
|
297
|
+
if (!ciMode) {
|
|
298
|
+
console.log(`🌐 Browser pool ready (reuse enabled)`);
|
|
176
299
|
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
throw new Error(`Failed to launch browser pool: ${err.message}`);
|
|
302
|
+
}
|
|
177
303
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
304
|
+
// Execute all registered attempts (with optional parallelism)
|
|
305
|
+
console.log(`\n🎬 Executing attempts...`);
|
|
306
|
+
|
|
307
|
+
// Shared state for fail-fast coordination
|
|
308
|
+
let shouldStopScheduling = false;
|
|
309
|
+
|
|
310
|
+
// Phase 7.2: Execute attempts with bounded parallelism
|
|
311
|
+
// Phase 7.3: Pass browser pool to attempts
|
|
312
|
+
const attemptResults_parallel = await executeParallel(
|
|
313
|
+
attemptsToRun,
|
|
314
|
+
async (attemptId) => {
|
|
315
|
+
const attemptDef = getAttemptDefinition(attemptId);
|
|
316
|
+
if (!attemptDef) {
|
|
317
|
+
throw new Error(`Attempt ${attemptId} not found in registry`);
|
|
318
|
+
}
|
|
188
319
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
320
|
+
if (!ciMode) {
|
|
321
|
+
console.log(` • ${attemptDef.name}...`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const attemptArtifactsDir = path.join(runDir, attemptId);
|
|
325
|
+
|
|
326
|
+
// Phase 7.3: Create isolated context for this attempt
|
|
327
|
+
const { context, page } = await browserPool.createContext({
|
|
328
|
+
timeout: resolvedTimeout
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
let result;
|
|
332
|
+
try {
|
|
333
|
+
// Phase 7.4: Check prerequisites before executing attempt
|
|
334
|
+
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: resolvedTimeout });
|
|
335
|
+
const prereqCheck = await checkPrerequisites(page, attemptId, 2000);
|
|
336
|
+
|
|
337
|
+
if (!prereqCheck.canProceed) {
|
|
338
|
+
// Skip attempt - prerequisites not met
|
|
339
|
+
if (!ciMode) {
|
|
340
|
+
console.log(` ⊘ Skipped: ${prereqCheck.reason}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
result = {
|
|
344
|
+
outcome: 'SKIPPED',
|
|
345
|
+
skipReason: prereqCheck.reason,
|
|
346
|
+
exitCode: 0, // SKIPPED does not affect exit code
|
|
347
|
+
steps: [],
|
|
348
|
+
friction: null,
|
|
349
|
+
error: null
|
|
350
|
+
};
|
|
351
|
+
} else {
|
|
352
|
+
// Prerequisites met - execute normally
|
|
353
|
+
result = await executeAttempt({
|
|
354
|
+
baseUrl,
|
|
355
|
+
attemptId,
|
|
356
|
+
artifactsDir: attemptArtifactsDir,
|
|
357
|
+
headful,
|
|
358
|
+
enableTrace,
|
|
359
|
+
enableScreenshots,
|
|
360
|
+
quiet: ciMode,
|
|
361
|
+
timeout: resolvedTimeout,
|
|
362
|
+
// Phase 7.3: Pass context from pool
|
|
363
|
+
browserContext: context,
|
|
364
|
+
browserPage: page
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
// Phase 7.3: Cleanup context after attempt
|
|
369
|
+
await browserPool.closeContext(context);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const attemptResult = {
|
|
373
|
+
attemptId,
|
|
374
|
+
attemptName: attemptDef.name,
|
|
375
|
+
goal: attemptDef.goal,
|
|
376
|
+
riskCategory: attemptDef.riskCategory || 'UNKNOWN',
|
|
377
|
+
source: attemptDef.source || 'manual',
|
|
378
|
+
...result
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Phase 7.1: Fail-fast logic (stop on FAILURE, not FRICTION)
|
|
382
|
+
// Phase 7.4: SKIPPED does NOT trigger fail-fast
|
|
383
|
+
if (failFast && attemptResult.outcome === 'FAILURE') {
|
|
384
|
+
shouldStopScheduling = true;
|
|
385
|
+
if (!ciMode) {
|
|
386
|
+
console.log(`\n⚡ FAIL-FAST: stopping after failure: ${attemptDef.name}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
197
389
|
|
|
390
|
+
return attemptResult;
|
|
391
|
+
},
|
|
392
|
+
validatedParallel,
|
|
393
|
+
{ shouldStop: () => shouldStopScheduling }
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Collect results in order
|
|
397
|
+
for (const result of attemptResults_parallel) {
|
|
398
|
+
if (result && !result.skipped) {
|
|
399
|
+
attemptResults.push(result);
|
|
400
|
+
}
|
|
198
401
|
}
|
|
199
402
|
|
|
200
403
|
// Phase 3: Execute intent flows (deterministic, curated)
|
|
201
404
|
if (enableFlows) {
|
|
202
405
|
console.log(`\n🎯 Executing intent flows...`);
|
|
203
406
|
const flowExecutor = new GuardianFlowExecutor({
|
|
204
|
-
timeout,
|
|
407
|
+
timeout: resolvedTimeout,
|
|
205
408
|
screenshotOnStep: enableScreenshots,
|
|
409
|
+
baseUrl,
|
|
410
|
+
quiet: ciMode,
|
|
206
411
|
...flowOptions
|
|
207
412
|
});
|
|
208
413
|
const browser = new GuardianBrowser();
|
|
209
414
|
|
|
210
415
|
try {
|
|
211
|
-
await browser.launch(
|
|
212
|
-
|
|
416
|
+
await browser.launch(resolvedTimeout);
|
|
417
|
+
// Phase 7.1: Apply flows filter
|
|
418
|
+
let flowsToRun = Array.isArray(filteredFlows) && filteredFlows.length ? filteredFlows : getDefaultFlowIds();
|
|
419
|
+
|
|
420
|
+
for (const flowId of flowsToRun) {
|
|
213
421
|
const flowDef = getFlowDefinition(flowId);
|
|
214
422
|
if (!flowDef) {
|
|
215
423
|
console.warn(`⚠️ Flow ${flowId} not found, skipping`);
|
|
216
424
|
continue;
|
|
217
425
|
}
|
|
218
426
|
|
|
427
|
+
const validation = validateFlowDefinition(flowDef);
|
|
428
|
+
if (!validation.ok) {
|
|
429
|
+
const reason = validation.reason || 'Flow misconfigured';
|
|
430
|
+
const flowResult = {
|
|
431
|
+
flowId,
|
|
432
|
+
flowName: flowDef.name,
|
|
433
|
+
riskCategory: flowDef.riskCategory || 'TRUST/UX',
|
|
434
|
+
description: flowDef.description,
|
|
435
|
+
outcome: 'FAILURE',
|
|
436
|
+
stepsExecuted: 0,
|
|
437
|
+
stepsTotal: Array.isArray(flowDef.steps) ? flowDef.steps.length : 0,
|
|
438
|
+
failedStep: 0,
|
|
439
|
+
error: reason,
|
|
440
|
+
screenshots: [],
|
|
441
|
+
failureReasons: [reason],
|
|
442
|
+
source: 'flow'
|
|
443
|
+
};
|
|
444
|
+
flowResults.push(flowResult);
|
|
445
|
+
|
|
446
|
+
// Phase 7.1: Fail-fast on flow failure
|
|
447
|
+
if (failFast && flowResult.outcome === 'FAILURE') {
|
|
448
|
+
console.log(`\n⚡ FAIL-FAST: stopping after first failure: ${flowDef.name}`);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
219
454
|
console.log(` • ${flowDef.name}...`);
|
|
220
455
|
const flowArtifactsDir = path.join(runDir, 'flows', flowId);
|
|
221
456
|
fs.mkdirSync(flowArtifactsDir, { recursive: true });
|
|
222
457
|
|
|
223
|
-
|
|
224
|
-
|
|
458
|
+
let flowResult;
|
|
459
|
+
try {
|
|
460
|
+
flowResult = await flowExecutor.executeFlow(browser.page, flowDef, flowArtifactsDir, baseUrl);
|
|
461
|
+
} catch (flowErr) {
|
|
462
|
+
console.warn(`⚠️ Flow ${flowDef.name} crashed: ${flowErr.message}`);
|
|
463
|
+
flowResult = {
|
|
464
|
+
flowId,
|
|
465
|
+
flowName: flowDef.name,
|
|
466
|
+
riskCategory: flowDef.riskCategory || 'TRUST/UX',
|
|
467
|
+
description: flowDef.description,
|
|
468
|
+
outcome: 'FAILURE',
|
|
469
|
+
stepsExecuted: 0,
|
|
470
|
+
stepsTotal: flowDef.steps.length,
|
|
471
|
+
durationMs: 0,
|
|
472
|
+
failedStep: null,
|
|
473
|
+
error: flowErr.message,
|
|
474
|
+
screenshots: [],
|
|
475
|
+
failureReasons: [`flow crashed: ${flowErr.message}`]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const resultWithMetadata = {
|
|
225
480
|
flowId,
|
|
226
481
|
flowName: flowDef.name,
|
|
227
482
|
riskCategory: flowDef.riskCategory || 'TRUST/UX',
|
|
228
483
|
description: flowDef.description,
|
|
229
|
-
outcome: flowResult.success ? 'SUCCESS' : 'FAILURE',
|
|
484
|
+
outcome: flowResult.outcome || (flowResult.success ? 'SUCCESS' : 'FAILURE'),
|
|
230
485
|
stepsExecuted: flowResult.stepsExecuted,
|
|
231
486
|
stepsTotal: flowResult.stepsTotal,
|
|
232
487
|
durationMs: flowResult.durationMs,
|
|
233
488
|
failedStep: flowResult.failedStep,
|
|
234
489
|
error: flowResult.error,
|
|
235
490
|
screenshots: flowResult.screenshots,
|
|
236
|
-
|
|
237
|
-
|
|
491
|
+
failureReasons: flowResult.failureReasons || [],
|
|
492
|
+
source: 'flow',
|
|
493
|
+
successEval: flowResult.successEval ? {
|
|
494
|
+
status: flowResult.successEval.status,
|
|
495
|
+
confidence: flowResult.successEval.confidence,
|
|
496
|
+
reasons: (flowResult.successEval.reasons || []).slice(0, 3),
|
|
497
|
+
evidence: flowResult.successEval.evidence || {}
|
|
498
|
+
} : null
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
flowResults.push(resultWithMetadata);
|
|
502
|
+
|
|
503
|
+
// Phase 7.1: Fail-fast logic for flows (stop on FAILURE, not FRICTION)
|
|
504
|
+
if (failFast && resultWithMetadata.outcome === 'FAILURE') {
|
|
505
|
+
console.log(`\n⚡ FAIL-FAST: stopping after first failure: ${flowDef.name}`);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
238
508
|
}
|
|
239
509
|
} catch (flowErr) {
|
|
240
510
|
console.warn(`⚠️ Flow execution failed (non-critical): ${flowErr.message}`);
|
|
@@ -243,6 +513,19 @@ async function executeReality(config) {
|
|
|
243
513
|
}
|
|
244
514
|
}
|
|
245
515
|
|
|
516
|
+
// Flow summary logging
|
|
517
|
+
if (flowResults.length > 0 && !ciMode) {
|
|
518
|
+
const successCount = flowResults.filter(f => (f.outcome || f.success === true ? f.outcome === 'SUCCESS' || f.success === true : false)).length;
|
|
519
|
+
const frictionCount = flowResults.filter(f => f.outcome === 'FRICTION').length;
|
|
520
|
+
const failureCount = flowResults.filter(f => f.outcome === 'FAILURE' || f.success === false).length;
|
|
521
|
+
console.log(`\nRun completed: ${flowResults.length} flows (${successCount} successes, ${frictionCount} frictions, ${failureCount} failures)`);
|
|
522
|
+
const troubled = flowResults.filter(f => f.outcome === 'FRICTION' || f.outcome === 'FAILURE');
|
|
523
|
+
troubled.forEach(f => {
|
|
524
|
+
const reason = (f.failureReasons && f.failureReasons[0]) || (f.error) || (f.successEval && f.successEval.reasons && f.successEval.reasons[0]) || 'no reason captured';
|
|
525
|
+
console.log(` - ${f.flowName}: ${reason}`);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
246
529
|
// Generate market report (existing flow)
|
|
247
530
|
const reporter = new MarketReporter();
|
|
248
531
|
const report = reporter.createReport({
|
|
@@ -449,6 +732,16 @@ async function executeReality(config) {
|
|
|
449
732
|
console.log(`🛡️ Policy override: exit code ${exitCode}`);
|
|
450
733
|
}
|
|
451
734
|
|
|
735
|
+
// Flow-based exit code aggregation (0/1/2)
|
|
736
|
+
const flowExitCode = computeFlowExitCode(flowResults);
|
|
737
|
+
exitCode = flowExitCode;
|
|
738
|
+
if (!ciMode && flowResults.length > 0) {
|
|
739
|
+
console.log(`Exit code (flows): ${flowExitCode}`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Phase 7.3: Cleanup browser pool
|
|
743
|
+
await browserPool.close();
|
|
744
|
+
|
|
452
745
|
return {
|
|
453
746
|
exitCode,
|
|
454
747
|
report,
|
|
@@ -467,15 +760,49 @@ async function executeReality(config) {
|
|
|
467
760
|
|
|
468
761
|
async function runRealityCLI(config) {
|
|
469
762
|
try {
|
|
763
|
+
if (config.watch) {
|
|
764
|
+
const { startWatchMode } = require('./watch-runner');
|
|
765
|
+
const watchResult = await startWatchMode(config);
|
|
766
|
+
if (watchResult && watchResult.watchStarted === false && typeof watchResult.exitCode === 'number') {
|
|
767
|
+
process.exit(watchResult.exitCode);
|
|
768
|
+
}
|
|
769
|
+
// When watch is active, do not exit; watcher owns lifecycle
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
470
773
|
const result = await executeReality(config);
|
|
471
774
|
|
|
472
775
|
// Phase 6: Print enhanced CLI summary
|
|
473
|
-
|
|
776
|
+
const ciMode = isCiMode();
|
|
777
|
+
if (ciMode) {
|
|
778
|
+
const ciSummary = formatCiSummary({
|
|
779
|
+
flowResults: result.flowResults || [],
|
|
780
|
+
diffResult: result.diffResult || null,
|
|
781
|
+
baselineCreated: result.baselineCreated || false,
|
|
782
|
+
exitCode: result.exitCode,
|
|
783
|
+
maxReasons: 5
|
|
784
|
+
});
|
|
785
|
+
console.log(ciSummary);
|
|
786
|
+
} else {
|
|
787
|
+
printCliSummary(result.snapshot, result.policyEval);
|
|
788
|
+
console.log(formatRunSummary({
|
|
789
|
+
flowResults: result.flowResults || [],
|
|
790
|
+
diffResult: result.diffResult || null,
|
|
791
|
+
baselineCreated: result.baselineCreated || false,
|
|
792
|
+
exitCode: result.exitCode
|
|
793
|
+
}, { label: 'Summary' }));
|
|
794
|
+
}
|
|
474
795
|
|
|
475
796
|
process.exit(result.exitCode);
|
|
476
797
|
} catch (err) {
|
|
477
798
|
console.error(`\n❌ Error: ${err.message}`);
|
|
478
|
-
if (
|
|
799
|
+
if (process.env.GUARDIAN_DEBUG) {
|
|
800
|
+
if (err.stack) console.error(err.stack);
|
|
801
|
+
} else if (err.stack) {
|
|
802
|
+
const stackLine = (err.stack.split('\n')[1] || '').trim();
|
|
803
|
+
if (stackLine) console.error(` at ${stackLine}`);
|
|
804
|
+
console.error(' (Set GUARDIAN_DEBUG=1 for full stack)');
|
|
805
|
+
}
|
|
479
806
|
process.exit(1);
|
|
480
807
|
}
|
|
481
808
|
}
|
|
@@ -544,4 +871,13 @@ function computeMarketRiskSummary(attemptResults) {
|
|
|
544
871
|
return summary;
|
|
545
872
|
}
|
|
546
873
|
|
|
547
|
-
|
|
874
|
+
function computeFlowExitCode(flowResults) {
|
|
875
|
+
if (!Array.isArray(flowResults) || flowResults.length === 0) return 0;
|
|
876
|
+
const hasFailure = flowResults.some(f => f.outcome === 'FAILURE' || f.success === false);
|
|
877
|
+
if (hasFailure) return 2;
|
|
878
|
+
const hasFriction = flowResults.some(f => f.outcome === 'FRICTION');
|
|
879
|
+
if (hasFriction) return 1;
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
module.exports = { executeReality, runRealityCLI, computeFlowExitCode, applySafeDefaults };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function deriveBaselineVerdict({ baselineCreated, diffResult }) {
|
|
2
|
+
if (baselineCreated) return 'BASELINE_CREATED';
|
|
3
|
+
if (!diffResult) return 'NO_BASELINE';
|
|
4
|
+
const hasRegressions = diffResult.regressions && Object.keys(diffResult.regressions).length > 0;
|
|
5
|
+
const hasImprovements = diffResult.improvements && Object.keys(diffResult.improvements).length > 0;
|
|
6
|
+
if (hasRegressions) return 'REGRESSION_DETECTED';
|
|
7
|
+
if (hasImprovements) return 'IMPROVEMENT_DETECTED';
|
|
8
|
+
return 'NO_REGRESSION';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatRunSummary({ flowResults = [], diffResult = null, baselineCreated = false, exitCode = 0 }, options = {}) {
|
|
12
|
+
const success = flowResults.filter(f => f.outcome === 'SUCCESS').length;
|
|
13
|
+
const friction = flowResults.filter(f => f.outcome === 'FRICTION').length;
|
|
14
|
+
const failure = flowResults.filter(f => f.outcome === 'FAILURE').length;
|
|
15
|
+
const baseline = deriveBaselineVerdict({ baselineCreated, diffResult });
|
|
16
|
+
const label = options.label || 'Summary';
|
|
17
|
+
return `${label}: flows=${flowResults.length} success=${success} friction=${friction} failure=${failure} | baseline=${baseline} | exit=${exitCode}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { formatRunSummary, deriveBaselineVerdict };
|