@odavl/guardian 0.1.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/guardian.js +690 -0
  5. package/flows/example-login-flow.json +36 -0
  6. package/flows/example-signup-flow.json +44 -0
  7. package/guardian-contract-v1.md +149 -0
  8. package/guardian.config.json +54 -0
  9. package/guardian.policy.json +12 -0
  10. package/guardian.profile.docs.yaml +18 -0
  11. package/guardian.profile.ecommerce.yaml +17 -0
  12. package/guardian.profile.marketing.yaml +18 -0
  13. package/guardian.profile.saas.yaml +21 -0
  14. package/package.json +69 -0
  15. package/policies/enterprise.json +12 -0
  16. package/policies/saas.json +12 -0
  17. package/policies/startup.json +12 -0
  18. package/src/guardian/attempt-engine.js +454 -0
  19. package/src/guardian/attempt-registry.js +227 -0
  20. package/src/guardian/attempt-reporter.js +507 -0
  21. package/src/guardian/attempt.js +227 -0
  22. package/src/guardian/auto-attempt-builder.js +283 -0
  23. package/src/guardian/baseline-reporter.js +143 -0
  24. package/src/guardian/baseline-storage.js +285 -0
  25. package/src/guardian/baseline.js +492 -0
  26. package/src/guardian/behavioral-signals.js +261 -0
  27. package/src/guardian/breakage-intelligence.js +223 -0
  28. package/src/guardian/browser.js +92 -0
  29. package/src/guardian/cli-summary.js +141 -0
  30. package/src/guardian/crawler.js +142 -0
  31. package/src/guardian/discovery-engine.js +661 -0
  32. package/src/guardian/enhanced-html-reporter.js +305 -0
  33. package/src/guardian/failure-taxonomy.js +169 -0
  34. package/src/guardian/flow-executor.js +374 -0
  35. package/src/guardian/flow-registry.js +67 -0
  36. package/src/guardian/html-reporter.js +414 -0
  37. package/src/guardian/index.js +218 -0
  38. package/src/guardian/init-command.js +139 -0
  39. package/src/guardian/junit-reporter.js +264 -0
  40. package/src/guardian/market-criticality.js +335 -0
  41. package/src/guardian/market-reporter.js +305 -0
  42. package/src/guardian/network-trace.js +178 -0
  43. package/src/guardian/policy.js +357 -0
  44. package/src/guardian/preset-loader.js +148 -0
  45. package/src/guardian/reality.js +547 -0
  46. package/src/guardian/reporter.js +181 -0
  47. package/src/guardian/root-cause-analysis.js +171 -0
  48. package/src/guardian/safety.js +248 -0
  49. package/src/guardian/scan-presets.js +60 -0
  50. package/src/guardian/screenshot.js +152 -0
  51. package/src/guardian/sitemap.js +225 -0
  52. package/src/guardian/snapshot-schema.js +266 -0
  53. package/src/guardian/snapshot.js +327 -0
  54. package/src/guardian/validators.js +323 -0
  55. package/src/guardian/visual-diff.js +247 -0
  56. package/src/guardian/webhook.js +206 -0
@@ -0,0 +1,547 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { executeAttempt } = require('./attempt');
4
+ const { MarketReporter } = require('./market-reporter');
5
+ const { getDefaultAttemptIds, getAttemptDefinition, registerAttempt } = require('./attempt-registry');
6
+ const { GuardianFlowExecutor } = require('./flow-executor');
7
+ const { getDefaultFlowIds, getFlowDefinition } = require('./flow-registry');
8
+ const { GuardianBrowser } = require('./browser');
9
+ const { GuardianCrawler } = require('./crawler');
10
+ const { SnapshotBuilder, saveSnapshot, loadSnapshot } = require('./snapshot');
11
+ const { DiscoveryEngine } = require('./discovery-engine');
12
+ const { buildAutoAttempts } = require('./auto-attempt-builder');
13
+ const { baselineExists, loadBaseline, saveBaselineAtomic, createBaselineFromSnapshot, compareSnapshots } = require('./baseline-storage');
14
+ const { analyzeMarketImpact, determineExitCodeFromEscalation } = require('./market-criticality');
15
+ const { parsePolicyOption } = require('./preset-loader');
16
+ const { evaluatePolicy } = require('./policy');
17
+ const { aggregateIntelligence } = require('./breakage-intelligence');
18
+ const { writeEnhancedHtml } = require('./enhanced-html-reporter');
19
+ const { printCliSummary } = require('./cli-summary');
20
+ const { sendWebhooks, getWebhookUrl, buildWebhookPayload } = require('./webhook');
21
+
22
+ function generateRunId(prefix = 'market-run') {
23
+ const now = new Date();
24
+ const dateStr = now.toISOString().replace(/[:\-]/g, '').substring(0, 15).replace('T', '-');
25
+ return `${prefix}-${dateStr}`;
26
+ }
27
+
28
+ async function executeReality(config) {
29
+ const {
30
+ baseUrl,
31
+ attempts = getDefaultAttemptIds(),
32
+ artifactsDir = './artifacts',
33
+ headful = false,
34
+ enableTrace = true,
35
+ enableScreenshots = true,
36
+ enableCrawl = true,
37
+ enableDiscovery = false,
38
+ enableAutoAttempts = false,
39
+ maxPages = 25,
40
+ maxDepth = 3,
41
+ timeout = 20000,
42
+ storageDir = '.odavl-guardian',
43
+ toolVersion = '0.2.0-phase2',
44
+ policy = null,
45
+ webhook = null,
46
+ includeUniversal = false,
47
+ autoAttemptOptions = {},
48
+ enableFlows = true,
49
+ flows = getDefaultFlowIds(),
50
+ flowOptions = {}
51
+ } = config;
52
+
53
+ // Validate baseUrl
54
+ try {
55
+ new URL(baseUrl);
56
+ } catch (e) {
57
+ throw new Error(`Invalid URL: ${baseUrl}`);
58
+ }
59
+
60
+ const runId = generateRunId();
61
+ const runDir = path.join(artifactsDir, runId);
62
+ fs.mkdirSync(runDir, { recursive: true });
63
+
64
+ console.log(`\n🧪 Market Reality Snapshot v1`);
65
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
66
+ console.log(`šŸ“ Base URL: ${baseUrl}`);
67
+ console.log(`šŸŽÆ Attempts: ${attempts.join(', ')}`);
68
+ console.log(`šŸ“ Run Dir: ${runDir}`);
69
+
70
+ // Initialize snapshot builder
71
+ const snapshotBuilder = new SnapshotBuilder(baseUrl, runId, toolVersion);
72
+ snapshotBuilder.setArtifactDir(runDir);
73
+
74
+ let crawlResult = null;
75
+ let discoveryResult = null;
76
+
77
+ // Optional: Crawl to discover URLs (lightweight, first N pages)
78
+ if (enableCrawl) {
79
+ console.log(`\nšŸ” Crawling for discovered URLs...`);
80
+ const browser = new GuardianBrowser();
81
+ try {
82
+ await browser.launch(timeout);
83
+ const crawler = new GuardianCrawler(baseUrl, maxPages, maxDepth);
84
+ crawlResult = await crawler.crawl(browser);
85
+ console.log(`āœ… Crawl complete: discovered ${crawlResult.totalDiscovered}, visited ${crawlResult.totalVisited}`);
86
+ snapshotBuilder.addCrawlResults(crawlResult);
87
+ await browser.close();
88
+ } catch (crawlErr) {
89
+ console.log(`āš ļø Crawl failed (non-critical): ${crawlErr.message}`);
90
+ // Continue anyway - crawl is optional
91
+ }
92
+ }
93
+
94
+ // Optional: Discovery Engine (Phase 4) — deterministic safe exploration
95
+ if (enableDiscovery) {
96
+ console.log(`\nšŸ”Ž Running discovery engine...`);
97
+ const browser = new GuardianBrowser();
98
+ try {
99
+ await browser.launch(timeout);
100
+ const engine = new DiscoveryEngine({
101
+ baseUrl,
102
+ maxPages,
103
+ timeout,
104
+ executeInteractions: false,
105
+ browser,
106
+ });
107
+ discoveryResult = await engine.discover(browser.page);
108
+ snapshotBuilder.setDiscoveryResults(discoveryResult);
109
+ console.log(`āœ… Discovery complete: visited ${discoveryResult.pagesVisitedCount}, interactions ${discoveryResult.interactionsDiscovered}`);
110
+ await browser.close();
111
+ } catch (discErr) {
112
+ console.log(`āš ļø Discovery failed (non-critical): ${discErr.message}`);
113
+ }
114
+ }
115
+
116
+ // Phase 2: Generate auto-attempts from discovered interactions
117
+ let autoAttempts = [];
118
+ if (enableAutoAttempts && discoveryResult && discoveryResult.interactionsDiscovered > 0) {
119
+ console.log(`\nšŸ¤– Generating auto-attempts from discoveries...`);
120
+ try {
121
+ // Get discovered interactions (stored in engine instance)
122
+ const discoveredInteractions = discoveryResult.interactions || [];
123
+
124
+ // Build auto-attempts with safety filters
125
+ const autoAttemptOptions = {
126
+ minConfidence: config.autoAttemptOptions?.minConfidence || 60,
127
+ maxAttempts: config.autoAttemptOptions?.maxAttempts || 10,
128
+ excludeRisky: true,
129
+ includeClasses: config.autoAttemptOptions?.includeClasses || ['NAVIGATION', 'ACTION', 'SUBMISSION', 'TOGGLE']
130
+ };
131
+
132
+ autoAttempts = buildAutoAttempts(discoveredInteractions, autoAttemptOptions);
133
+
134
+ // Register auto-attempts dynamically
135
+ for (const autoAttempt of autoAttempts) {
136
+ registerAttempt(autoAttempt);
137
+ }
138
+
139
+ console.log(`āœ… Generated ${autoAttempts.length} auto-attempts`);
140
+ } catch (autoErr) {
141
+ console.log(`āš ļø Auto-attempt generation failed (non-critical): ${autoErr.message}`);
142
+ }
143
+ }
144
+
145
+ const attemptResults = [];
146
+ const flowResults = [];
147
+
148
+ // Determine attempts to run (manual + auto-generated)
149
+ let attemptsToRun = Array.isArray(attempts) ? attempts.slice() : getDefaultAttemptIds();
150
+
151
+ // Phase 2: Add auto-generated attempts
152
+ if (enableAutoAttempts && autoAttempts.length > 0) {
153
+ const autoAttemptIds = autoAttempts.map(a => a.attemptId);
154
+ attemptsToRun.push(...autoAttemptIds);
155
+ console.log(`āž• Added ${autoAttemptIds.length} auto-generated attempts`);
156
+ }
157
+
158
+ if (includeUniversal && !attemptsToRun.includes('universal_reality')) {
159
+ attemptsToRun.push('universal_reality');
160
+ }
161
+ // If discovery enabled and site is simple (few interactions), add universal pack
162
+ if (enableDiscovery && discoveryResult && !attemptsToRun.includes('universal_reality')) {
163
+ const simpleSite = (discoveryResult.interactionsDiscovered || 0) === 0 || (discoveryResult.pagesVisitedCount || 0) <= 1;
164
+ if (simpleSite) {
165
+ attemptsToRun.push('universal_reality');
166
+ console.log(`āž• Added Universal Reality Pack (simple site detected)`);
167
+ }
168
+ }
169
+
170
+ // Execute all registered attempts
171
+ console.log(`\nšŸŽ¬ Executing attempts...`);
172
+ for (const attemptId of attemptsToRun) {
173
+ const attemptDef = getAttemptDefinition(attemptId);
174
+ if (!attemptDef) {
175
+ throw new Error(`Attempt ${attemptId} not found in registry`);
176
+ }
177
+
178
+ console.log(` • ${attemptDef.name}...`);
179
+ const attemptArtifactsDir = path.join(runDir, attemptId);
180
+ const result = await executeAttempt({
181
+ baseUrl,
182
+ attemptId,
183
+ artifactsDir: attemptArtifactsDir,
184
+ headful,
185
+ enableTrace,
186
+ enableScreenshots
187
+ });
188
+
189
+ attemptResults.push({
190
+ attemptId,
191
+ attemptName: attemptDef.name,
192
+ goal: attemptDef.goal,
193
+ riskCategory: attemptDef.riskCategory || 'UNKNOWN',
194
+ source: attemptDef.source || 'manual',
195
+ ...result
196
+ });
197
+
198
+ }
199
+
200
+ // Phase 3: Execute intent flows (deterministic, curated)
201
+ if (enableFlows) {
202
+ console.log(`\nšŸŽÆ Executing intent flows...`);
203
+ const flowExecutor = new GuardianFlowExecutor({
204
+ timeout,
205
+ screenshotOnStep: enableScreenshots,
206
+ ...flowOptions
207
+ });
208
+ const browser = new GuardianBrowser();
209
+
210
+ try {
211
+ await browser.launch(timeout);
212
+ for (const flowId of (Array.isArray(flows) && flows.length ? flows : getDefaultFlowIds())) {
213
+ const flowDef = getFlowDefinition(flowId);
214
+ if (!flowDef) {
215
+ console.warn(`āš ļø Flow ${flowId} not found, skipping`);
216
+ continue;
217
+ }
218
+
219
+ console.log(` • ${flowDef.name}...`);
220
+ const flowArtifactsDir = path.join(runDir, 'flows', flowId);
221
+ fs.mkdirSync(flowArtifactsDir, { recursive: true });
222
+
223
+ const flowResult = await flowExecutor.executeFlow(browser.page, flowDef, flowArtifactsDir, baseUrl);
224
+ flowResults.push({
225
+ flowId,
226
+ flowName: flowDef.name,
227
+ riskCategory: flowDef.riskCategory || 'TRUST/UX',
228
+ description: flowDef.description,
229
+ outcome: flowResult.success ? 'SUCCESS' : 'FAILURE',
230
+ stepsExecuted: flowResult.stepsExecuted,
231
+ stepsTotal: flowResult.stepsTotal,
232
+ durationMs: flowResult.durationMs,
233
+ failedStep: flowResult.failedStep,
234
+ error: flowResult.error,
235
+ screenshots: flowResult.screenshots,
236
+ source: 'flow'
237
+ });
238
+ }
239
+ } catch (flowErr) {
240
+ console.warn(`āš ļø Flow execution failed (non-critical): ${flowErr.message}`);
241
+ } finally {
242
+ await browser.close().catch(() => {});
243
+ }
244
+ }
245
+
246
+ // Generate market report (existing flow)
247
+ const reporter = new MarketReporter();
248
+ const report = reporter.createReport({
249
+ runId,
250
+ baseUrl,
251
+ attemptsRun: attemptsToRun,
252
+ results: attemptResults.map(r => ({
253
+ attemptId: r.attemptId,
254
+ attemptName: r.attemptName,
255
+ goal: r.goal,
256
+ outcome: r.outcome,
257
+ exitCode: r.exitCode,
258
+ totalDurationMs: r.attemptResult ? r.attemptResult.totalDurationMs : null,
259
+ friction: r.friction,
260
+ steps: r.steps,
261
+ reportJsonPath: r.reportJsonPath,
262
+ reportHtmlPath: r.reportHtmlPath
263
+ })),
264
+ flows: flowResults
265
+ });
266
+
267
+ const jsonPath = reporter.saveJsonReport(report, runDir);
268
+ const html = reporter.generateHtmlReport(report);
269
+ const htmlPath = reporter.saveHtmlReport(html, runDir);
270
+
271
+ // Add market report paths to snapshot
272
+ snapshotBuilder.addMarketResults(
273
+ {
274
+ attemptResults,
275
+ marketJsonPath: jsonPath,
276
+ marketHtmlPath: htmlPath,
277
+ flowResults
278
+ },
279
+ runDir
280
+ );
281
+
282
+ // Phase 2: Compute market risk summary
283
+ const riskSummary = computeMarketRiskSummary(attemptResults);
284
+ snapshotBuilder.snapshot.riskSummary = riskSummary;
285
+
286
+ // Handle baseline: load existing or auto-create
287
+ console.log(`\nšŸ“Š Baseline check...`);
288
+ let baselineCreated = false;
289
+ let baselineSnapshot = null;
290
+ let diffResult = null;
291
+
292
+ if (baselineExists(baseUrl, storageDir)) {
293
+ console.log(`āœ… Baseline found`);
294
+ baselineSnapshot = loadBaseline(baseUrl, storageDir);
295
+ snapshotBuilder.setBaseline({
296
+ baselineFound: true,
297
+ baselineCreatedThisRun: false,
298
+ baselinePath: path.join(storageDir, 'baselines', require('./baseline-storage').urlToSlug(baseUrl), 'baseline.json')
299
+ });
300
+
301
+ // Compare current against baseline
302
+ diffResult = compareSnapshots(baselineSnapshot, snapshotBuilder.getSnapshot());
303
+ snapshotBuilder.addDiff(diffResult);
304
+
305
+ if (diffResult.regressions && Object.keys(diffResult.regressions).length > 0) {
306
+ console.log(`āš ļø Regressions detected: ${Object.keys(diffResult.regressions).join(', ')}`);
307
+ }
308
+ if (diffResult.improvements && Object.keys(diffResult.improvements).length > 0) {
309
+ console.log(`✨ Improvements: ${Object.keys(diffResult.improvements).join(', ')}`);
310
+ }
311
+ } else {
312
+ // Auto-create baseline on first run
313
+ console.log(`šŸ’¾ Baseline not found - creating auto-baseline...`);
314
+ const newBaseline = createBaselineFromSnapshot(snapshotBuilder.getSnapshot());
315
+ await saveBaselineAtomic(baseUrl, newBaseline, storageDir);
316
+ baselineCreated = true;
317
+ baselineSnapshot = newBaseline;
318
+
319
+ snapshotBuilder.setBaseline({
320
+ baselineFound: false,
321
+ baselineCreatedThisRun: true,
322
+ baselineCreatedAt: new Date().toISOString(),
323
+ baselinePath: path.join(storageDir, 'baselines', require('./baseline-storage').urlToSlug(baseUrl), 'baseline.json')
324
+ });
325
+
326
+ console.log(`āœ… Baseline created`);
327
+ }
328
+
329
+ // Analyze market impact (Phase 3)
330
+ console.log(`\nšŸ“Š Analyzing market criticality...`);
331
+ const currentSnapshot = snapshotBuilder.getSnapshot();
332
+ const marketImpact = analyzeMarketImpact(
333
+ [
334
+ ...currentSnapshot.attempts,
335
+ ...(flowResults.map(f => ({
336
+ attemptId: f.flowId,
337
+ outcome: f.outcome,
338
+ riskCategory: f.riskCategory,
339
+ validators: [],
340
+ friction: { signals: [] },
341
+ pageUrl: baseUrl
342
+ })) || [])
343
+ ],
344
+ baseUrl
345
+ );
346
+ snapshotBuilder.setMarketImpactSummary(marketImpact);
347
+ console.log(`āœ… Market impact analyzed: ${marketImpact.highestSeverity} severity`);
348
+
349
+ // Phase 4: Add breakage intelligence (deterministic failure analysis)
350
+ const intelligence = aggregateIntelligence(attemptResults, flowResults);
351
+ snapshotBuilder.addIntelligence(intelligence);
352
+ if (intelligence.escalationSignals.length > 0) {
353
+ console.log(`🚨 Escalation signals: ${intelligence.escalationSignals.slice(0, 3).join('; ')}`);
354
+ }
355
+
356
+ // Save snapshot itself
357
+ console.log(`\nšŸ’¾ Saving snapshot...`);
358
+ const snapshotPath = path.join(runDir, 'snapshot.json');
359
+ await saveSnapshot(snapshotBuilder.getSnapshot(), snapshotPath);
360
+ console.log(`āœ… Snapshot saved: snapshot.json`);
361
+
362
+ // Phase 6: Generate enhanced HTML report
363
+ try {
364
+ const enhancedHtmlPath = writeEnhancedHtml(snapshotBuilder.getSnapshot(), runDir);
365
+ console.log(`āœ… Enhanced HTML report: ${path.basename(enhancedHtmlPath)}`);
366
+ } catch (htmlErr) {
367
+ console.warn(`āš ļø Enhanced HTML report failed (non-critical): ${htmlErr.message}`);
368
+ }
369
+
370
+ // Phase 5/6: Evaluate policy
371
+ let policyEval = null;
372
+ if (policy) {
373
+ try {
374
+ const policyObj = parsePolicyOption(policy);
375
+ if (policyObj) {
376
+ console.log(`\nšŸ›”ļø Evaluating policy...`);
377
+ policyEval = evaluatePolicy(snapshotBuilder.getSnapshot(), policyObj);
378
+ console.log(`Policy: ${policyEval.passed ? 'āœ… PASSED' : 'āŒ FAILED'}`);
379
+ if (!policyEval.passed && policyEval.reasons) {
380
+ policyEval.reasons.slice(0, 3).forEach(r => console.log(` • ${r}`));
381
+ }
382
+ }
383
+ } catch (policyErr) {
384
+ console.warn(`āš ļø Policy evaluation failed (non-critical): ${policyErr.message}`);
385
+ }
386
+ }
387
+
388
+ // Phase 5/6: Send webhook notifications
389
+ if (webhook) {
390
+ try {
391
+ const webhookUrl = getWebhookUrl('GUARDIAN_WEBHOOK_URL', webhook);
392
+ if (webhookUrl) {
393
+ console.log(`\nšŸ“” Sending webhook notifications...`);
394
+ const payload = buildWebhookPayload(
395
+ snapshotBuilder.getSnapshot(),
396
+ policyEval,
397
+ { snapshotPath, marketJsonPath: jsonPath, marketHtmlPath: htmlPath }
398
+ );
399
+ const urls = webhookUrl.split(',').map(u => u.trim());
400
+ await sendWebhooks(urls, payload);
401
+ console.log(`āœ… Webhook notifications sent`);
402
+ }
403
+ } catch (webhookErr) {
404
+ console.warn(`āš ļø Webhook notification failed (non-critical): ${webhookErr.message}`);
405
+ }
406
+ }
407
+
408
+ // Determine exit code (including market criticality escalation + policy)
409
+ let exitCode = 0;
410
+ const finalSnapshot = snapshotBuilder.getSnapshot();
411
+
412
+ if (baselineCreated) {
413
+ // First run: check market criticality
414
+ exitCode = 0;
415
+ if (marketImpact.highestSeverity === 'CRITICAL') {
416
+ console.log(`🚨 First run with CRITICAL market risks`);
417
+ exitCode = 1;
418
+ } else if (marketImpact.highestSeverity === 'WARNING') {
419
+ console.log(`āš ļø First run with WARNING market risks`);
420
+ exitCode = 2;
421
+ }
422
+ console.log(`āœ… Baseline created`);
423
+ } else if (baselineSnapshot) {
424
+ // Subsequent runs: check for regressions + severity escalation
425
+ const baselineSeverity = baselineSnapshot.marketImpactSummary?.highestSeverity || 'INFO';
426
+ const currentSeverity = marketImpact.highestSeverity;
427
+ const escalation = determineExitCodeFromEscalation(baselineSeverity, currentSeverity);
428
+
429
+ if (escalation.escalated) {
430
+ // Severity escalation is a FAILURE
431
+ exitCode = 1;
432
+ console.log(`🚨 Severity escalated: ${baselineSeverity} → ${currentSeverity}`);
433
+ } else if (diffResult && Object.keys(diffResult.regressions).length > 0) {
434
+ exitCode = 1;
435
+ console.log(`āŒ Regressions detected`);
436
+ } else if (currentSeverity !== 'INFO') {
437
+ // Still have market risks but didn't escalate
438
+ exitCode = 2;
439
+ console.log(`āš ļø ${currentSeverity} market risks present`);
440
+ } else {
441
+ exitCode = 0;
442
+ console.log(`āœ… No critical changes`);
443
+ }
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}`);
450
+ }
451
+
452
+ return {
453
+ exitCode,
454
+ report,
455
+ runDir,
456
+ snapshotPath,
457
+ marketJsonPath: jsonPath,
458
+ marketHtmlPath: htmlPath,
459
+ attemptResults,
460
+ flowResults,
461
+ baselineCreated,
462
+ diffResult,
463
+ snapshot: finalSnapshot,
464
+ policyEval
465
+ };
466
+ }
467
+
468
+ async function runRealityCLI(config) {
469
+ try {
470
+ const result = await executeReality(config);
471
+
472
+ // Phase 6: Print enhanced CLI summary
473
+ printCliSummary(result.snapshot, result.policyEval);
474
+
475
+ process.exit(result.exitCode);
476
+ } catch (err) {
477
+ console.error(`\nāŒ Error: ${err.message}`);
478
+ if (err.stack) console.error(err.stack);
479
+ process.exit(1);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Phase 2: Compute market risk summary from attempt results
485
+ * Deterministic scoring based on attempt outcomes and risk categories
486
+ */
487
+ function computeMarketRiskSummary(attemptResults) {
488
+ const summary = {
489
+ totalSoftFailures: 0,
490
+ totalFriction: 0,
491
+ totalFailures: 0,
492
+ failuresByCategory: {},
493
+ softFailuresByAttempt: {},
494
+ topRisks: []
495
+ };
496
+
497
+ // Categorize failures
498
+ for (const attempt of attemptResults) {
499
+ const category = attempt.riskCategory || 'UNKNOWN';
500
+ if (!summary.failuresByCategory[category]) {
501
+ summary.failuresByCategory[category] = { failures: 0, friction: 0, softFailures: 0 };
502
+ }
503
+
504
+ // Count outcomes
505
+ if (attempt.outcome === 'FAILURE') {
506
+ summary.totalFailures++;
507
+ summary.failuresByCategory[category].failures++;
508
+ } else if (attempt.outcome === 'FRICTION') {
509
+ summary.totalFriction++;
510
+ summary.failuresByCategory[category].friction++;
511
+ }
512
+
513
+ // Count soft failures (Phase 2)
514
+ if (attempt.softFailureCount > 0) {
515
+ summary.totalSoftFailures += attempt.softFailureCount;
516
+ summary.failuresByCategory[category].softFailures += attempt.softFailureCount;
517
+ summary.softFailuresByAttempt[attempt.attemptId] = attempt.softFailureCount;
518
+ }
519
+ }
520
+
521
+ // Build top risks (sorted by severity)
522
+ const riskList = [];
523
+ for (const [category, counts] of Object.entries(summary.failuresByCategory)) {
524
+ if (counts.failures > 0 || counts.friction > 0 || counts.softFailures > 0) {
525
+ riskList.push({
526
+ category,
527
+ severity: counts.failures > 0 ? 'CRITICAL' : 'MEDIUM',
528
+ failures: counts.failures,
529
+ frictionCount: counts.friction,
530
+ softFailures: counts.softFailures
531
+ });
532
+ }
533
+ }
534
+
535
+ summary.topRisks = riskList.sort((a, b) => {
536
+ // CRITICAL before MEDIUM
537
+ if (a.severity !== b.severity) {
538
+ return a.severity === 'CRITICAL' ? -1 : 1;
539
+ }
540
+ // Then by failure count
541
+ return (b.failures + b.softFailures) - (a.failures + a.softFailures);
542
+ });
543
+
544
+ return summary;
545
+ }
546
+
547
+ module.exports = { executeReality, runRealityCLI };