@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- 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 };
|