@odavl/guardian 0.2.0 → 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 +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -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 +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -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 +6 -1
- package/src/guardian/flag-validator.js +17 -3
- 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/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 +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- 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/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- 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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run Listing CLI Command
|
|
3
|
+
* Lists completed Guardian runs with their metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { readMetaJson, formatDuration } = require('./run-artifacts');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find all run directories in artifacts folder
|
|
12
|
+
*
|
|
13
|
+
* @param {string} artifactsDir - Path to artifacts directory
|
|
14
|
+
* @returns {string[]} array of run directory paths
|
|
15
|
+
*/
|
|
16
|
+
function findRunDirs(artifactsDir) {
|
|
17
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const entries = fs.readdirSync(artifactsDir, { withFileTypes: true });
|
|
23
|
+
return entries
|
|
24
|
+
.filter(e => e.isDirectory())
|
|
25
|
+
.map(e => path.join(artifactsDir, e.name));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load run metadata with fallback to directory name parsing
|
|
33
|
+
*
|
|
34
|
+
* @param {string} runDir - Run directory path
|
|
35
|
+
* @returns {Object|null} run info or null if cannot parse
|
|
36
|
+
*/
|
|
37
|
+
function loadRunInfo(runDir) {
|
|
38
|
+
const dirName = path.basename(runDir);
|
|
39
|
+
|
|
40
|
+
// Try META.json first
|
|
41
|
+
const meta = readMetaJson(runDir);
|
|
42
|
+
if (meta) {
|
|
43
|
+
return {
|
|
44
|
+
path: runDir,
|
|
45
|
+
dirName,
|
|
46
|
+
timestamp: meta.timestamp,
|
|
47
|
+
url: meta.url,
|
|
48
|
+
siteSlug: meta.siteSlug,
|
|
49
|
+
policy: meta.policy,
|
|
50
|
+
result: meta.result,
|
|
51
|
+
durationMs: meta.durationMs,
|
|
52
|
+
attempts: meta.attempts
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Silently skip if no META.json
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format and sort runs for display
|
|
62
|
+
*
|
|
63
|
+
* @param {Array} runs - Array of run info objects
|
|
64
|
+
* @returns {Array} sorted by timestamp (newest first)
|
|
65
|
+
*/
|
|
66
|
+
function sortRuns(runs) {
|
|
67
|
+
return runs
|
|
68
|
+
.filter(r => r !== null)
|
|
69
|
+
.sort((a, b) => {
|
|
70
|
+
const timeA = new Date(a.timestamp).getTime();
|
|
71
|
+
const timeB = new Date(b.timestamp).getTime();
|
|
72
|
+
return timeB - timeA; // newest first
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format time string from ISO timestamp
|
|
78
|
+
*
|
|
79
|
+
* @param {string} isoString - ISO-8601 timestamp
|
|
80
|
+
* @returns {string} formatted local time (YYYY-MM-DD HH:MM:SS)
|
|
81
|
+
*/
|
|
82
|
+
function formatTime(isoString) {
|
|
83
|
+
const date = new Date(isoString);
|
|
84
|
+
const year = date.getFullYear();
|
|
85
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
86
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
87
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
88
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
89
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
90
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Calculate column widths for table display
|
|
95
|
+
*
|
|
96
|
+
* @param {Array} runs - Run records
|
|
97
|
+
* @returns {Object} widths for each column
|
|
98
|
+
*/
|
|
99
|
+
function calculateColumnWidths(runs) {
|
|
100
|
+
// Use fixed reasonable widths for 100-char display
|
|
101
|
+
return {
|
|
102
|
+
time: 19, // "YYYY-MM-DD HH:MM:SS"
|
|
103
|
+
site: 14,
|
|
104
|
+
policy: 10,
|
|
105
|
+
result: 7,
|
|
106
|
+
duration: 10,
|
|
107
|
+
path: 30
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format a single row for table output
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} run - Run info
|
|
115
|
+
* @param {Object} widths - Column widths
|
|
116
|
+
* @returns {string} formatted row
|
|
117
|
+
*/
|
|
118
|
+
function formatRow(run, widths) {
|
|
119
|
+
const time = formatTime(run.timestamp);
|
|
120
|
+
const site = (run.siteSlug || 'unknown').substring(0, widths.site);
|
|
121
|
+
const policy = (run.policy || 'default').substring(0, widths.policy);
|
|
122
|
+
const result = (run.result || 'UNKNOWN').substring(0, widths.result);
|
|
123
|
+
const duration = formatDuration(run.durationMs).substring(0, widths.duration);
|
|
124
|
+
const path = run.dirName.substring(0, widths.path);
|
|
125
|
+
|
|
126
|
+
return `${time} ${site.padEnd(widths.site)} ${policy.padEnd(widths.policy)} ${result.padEnd(widths.result)} ${duration.padEnd(widths.duration)} ${path}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Main list command with filtering
|
|
131
|
+
*
|
|
132
|
+
* @param {string} artifactsDir - Path to artifacts directory
|
|
133
|
+
* @param {Object} filters - Filter options
|
|
134
|
+
* @param {boolean} filters.failed - Show only FAILED runs
|
|
135
|
+
* @param {string} filters.site - Show only runs for specific site slug
|
|
136
|
+
* @param {number} filters.limit - Show only newest N runs
|
|
137
|
+
*/
|
|
138
|
+
function listRuns(artifactsDir = './artifacts', filters = {}) {
|
|
139
|
+
const runDirs = findRunDirs(artifactsDir);
|
|
140
|
+
|
|
141
|
+
if (runDirs.length === 0) {
|
|
142
|
+
console.log('No runs found.');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Load metadata for all runs
|
|
147
|
+
let runs = runDirs
|
|
148
|
+
.map(dir => loadRunInfo(dir))
|
|
149
|
+
.filter(r => r !== null);
|
|
150
|
+
|
|
151
|
+
if (runs.length === 0) {
|
|
152
|
+
console.log('No completed runs with META.json found.');
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Apply filters
|
|
157
|
+
if (filters.failed) {
|
|
158
|
+
runs = runs.filter(r => r.result === 'FAILED');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (filters.site) {
|
|
162
|
+
runs = runs.filter(r => r.siteSlug === filters.site);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (runs.length === 0) {
|
|
166
|
+
console.log('No runs matching the filters.');
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sort by timestamp (newest first)
|
|
171
|
+
const sorted = sortRuns(runs);
|
|
172
|
+
|
|
173
|
+
// Apply limit filter
|
|
174
|
+
let displayed = sorted;
|
|
175
|
+
if (filters.limit && filters.limit > 0) {
|
|
176
|
+
displayed = sorted.slice(0, filters.limit);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Calculate column widths
|
|
180
|
+
const widths = calculateColumnWidths(displayed);
|
|
181
|
+
|
|
182
|
+
// Print header
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log('Guardian Runs');
|
|
185
|
+
console.log('='.repeat(100));
|
|
186
|
+
const header = 'Time'.padEnd(widths.time + 2) +
|
|
187
|
+
'Site'.padEnd(widths.site + 2) +
|
|
188
|
+
'Policy'.padEnd(widths.policy + 2) +
|
|
189
|
+
'Result'.padEnd(widths.result + 2) +
|
|
190
|
+
'Duration'.padEnd(widths.duration + 2) +
|
|
191
|
+
'Path';
|
|
192
|
+
console.log(header);
|
|
193
|
+
console.log('-'.repeat(100));
|
|
194
|
+
|
|
195
|
+
// Print rows
|
|
196
|
+
for (const run of displayed) {
|
|
197
|
+
console.log(formatRow(run, widths));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log('='.repeat(100));
|
|
201
|
+
console.log(`Total: ${runs.length} run(s)${displayed.length < runs.length ? ` (showing ${displayed.length})` : ''}\n`);
|
|
202
|
+
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
listRuns,
|
|
208
|
+
findRunDirs,
|
|
209
|
+
loadRunInfo,
|
|
210
|
+
sortRuns
|
|
211
|
+
};
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Scan Presets (Phase 6)
|
|
3
3
|
* Opinionated defaults for one-command scans
|
|
4
4
|
* Deterministic mappings: attempts, flows, policy thresholds.
|
|
5
|
+
* Each preset must fully define coverage (enabled/disabled attempts), flows,
|
|
6
|
+
* policy strictness, and operational defaults (fail-fast, evidence expectations).
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
const { getDefaultAttemptIds } = require('./attempt-registry');
|
|
@@ -10,50 +12,137 @@ const { getDefaultFlowIds } = require('./flow-registry');
|
|
|
10
12
|
function resolveScanPreset(name = 'landing') {
|
|
11
13
|
const preset = (name || '').toLowerCase();
|
|
12
14
|
|
|
15
|
+
if (!preset) {
|
|
16
|
+
throw new Error('Preset name is required');
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
// Defaults: curated attempts + curated flows
|
|
14
20
|
const defaults = {
|
|
21
|
+
id: 'default',
|
|
15
22
|
attempts: getDefaultAttemptIds(),
|
|
23
|
+
disabledAttempts: [],
|
|
16
24
|
flows: getDefaultFlowIds(),
|
|
17
|
-
policy: null
|
|
25
|
+
policy: null,
|
|
26
|
+
failFast: true,
|
|
27
|
+
evidence: {
|
|
28
|
+
requireScreenshots: true,
|
|
29
|
+
requireTraces: true,
|
|
30
|
+
minCompleteness: 1.0,
|
|
31
|
+
minIntegrity: 0.9
|
|
32
|
+
}
|
|
18
33
|
};
|
|
19
34
|
|
|
20
35
|
switch (preset) {
|
|
21
36
|
case 'landing':
|
|
22
37
|
return {
|
|
23
|
-
|
|
38
|
+
id: 'landing',
|
|
39
|
+
attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'contact_form', 'language_switch', 'newsletter_signup'],
|
|
40
|
+
disabledAttempts: ['signup', 'login', 'checkout'],
|
|
24
41
|
flows: [], // focus on landing conversion, flows optional
|
|
25
42
|
policy: {
|
|
26
43
|
// lenient warnings, strict criticals
|
|
27
44
|
failOnSeverity: 'CRITICAL',
|
|
28
|
-
maxWarnings:
|
|
29
|
-
visualGates: { CRITICAL: 0, WARNING:
|
|
30
|
-
|
|
45
|
+
maxWarnings: 5,
|
|
46
|
+
visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 25 },
|
|
47
|
+
coverage: { failOnGap: false, warnOnGap: true },
|
|
48
|
+
evidence: { minCompleteness: 0.6, minIntegrity: 0.7, requireScreenshots: true, requireTraces: false }
|
|
49
|
+
},
|
|
50
|
+
failFast: true,
|
|
51
|
+
evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.6, minIntegrity: 0.7 }
|
|
52
|
+
};
|
|
53
|
+
case 'landing-demo':
|
|
54
|
+
return {
|
|
55
|
+
id: 'landing-demo',
|
|
56
|
+
attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'contact_form', 'language_switch'],
|
|
57
|
+
disabledAttempts: ['signup', 'login', 'checkout', 'newsletter_signup'],
|
|
58
|
+
flows: [],
|
|
59
|
+
policy: {
|
|
60
|
+
// Strict on broken navigation and CTA, lenient on revenue-related issues
|
|
61
|
+
failOnSeverity: 'CRITICAL',
|
|
62
|
+
maxWarnings: 5,
|
|
63
|
+
failOnNewRegression: false,
|
|
64
|
+
visualGates: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 30 },
|
|
65
|
+
coverage: { failOnGap: true, warnOnGap: false },
|
|
66
|
+
evidence: { minCompleteness: 0.7, minIntegrity: 0.8, requireScreenshots: true, requireTraces: false }
|
|
67
|
+
},
|
|
68
|
+
failFast: true,
|
|
69
|
+
evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.7, minIntegrity: 0.8 }
|
|
70
|
+
};
|
|
71
|
+
case 'startup':
|
|
72
|
+
return {
|
|
73
|
+
id: 'startup',
|
|
74
|
+
attempts: getDefaultAttemptIds(),
|
|
75
|
+
disabledAttempts: [],
|
|
76
|
+
flows: getDefaultFlowIds(),
|
|
77
|
+
policy: {
|
|
78
|
+
failOnSeverity: 'CRITICAL',
|
|
79
|
+
maxWarnings: 3,
|
|
80
|
+
maxInfo: 999,
|
|
81
|
+
failOnNewRegression: false,
|
|
82
|
+
visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 30 },
|
|
83
|
+
coverage: { failOnGap: true, warnOnGap: false },
|
|
84
|
+
evidence: { minCompleteness: 0.7, minIntegrity: 0.8, requireScreenshots: true, requireTraces: false }
|
|
85
|
+
},
|
|
86
|
+
failFast: true,
|
|
87
|
+
evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.7, minIntegrity: 0.8 }
|
|
31
88
|
};
|
|
32
89
|
case 'saas':
|
|
33
90
|
return {
|
|
34
|
-
|
|
91
|
+
id: 'saas',
|
|
92
|
+
attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'language_switch', 'contact_form', 'newsletter_signup'],
|
|
35
93
|
flows: ['signup_flow', 'login_flow'],
|
|
36
94
|
policy: {
|
|
37
95
|
failOnSeverity: 'CRITICAL',
|
|
38
96
|
maxWarnings: 1,
|
|
39
97
|
failOnNewRegression: true,
|
|
40
|
-
visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 20 }
|
|
41
|
-
|
|
98
|
+
visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 20 },
|
|
99
|
+
coverage: { failOnGap: true, warnOnGap: false },
|
|
100
|
+
evidence: { minCompleteness: 0.8, minIntegrity: 0.9, requireScreenshots: true, requireTraces: true }
|
|
101
|
+
},
|
|
102
|
+
failFast: true,
|
|
103
|
+
evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 0.8, minIntegrity: 0.9 }
|
|
42
104
|
};
|
|
43
105
|
case 'shop':
|
|
44
106
|
case 'ecommerce':
|
|
45
107
|
return {
|
|
108
|
+
id: 'shop',
|
|
46
109
|
attempts: ['language_switch', 'contact_form', 'newsletter_signup'],
|
|
47
110
|
flows: ['checkout_flow'],
|
|
48
111
|
policy: {
|
|
49
112
|
failOnSeverity: 'CRITICAL',
|
|
50
113
|
maxWarnings: 0,
|
|
51
114
|
failOnNewRegression: true,
|
|
52
|
-
visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 15 }
|
|
53
|
-
|
|
115
|
+
visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 15 },
|
|
116
|
+
coverage: { failOnGap: true, warnOnGap: false },
|
|
117
|
+
evidence: { minCompleteness: 0.9, minIntegrity: 0.95, requireScreenshots: true, requireTraces: true }
|
|
118
|
+
},
|
|
119
|
+
failFast: true,
|
|
120
|
+
evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 0.9, minIntegrity: 0.95 }
|
|
121
|
+
};
|
|
122
|
+
case 'enterprise':
|
|
123
|
+
return {
|
|
124
|
+
id: 'enterprise',
|
|
125
|
+
attempts: getDefaultAttemptIds(),
|
|
126
|
+
disabledAttempts: [],
|
|
127
|
+
flows: getDefaultFlowIds(),
|
|
128
|
+
policy: {
|
|
129
|
+
failOnSeverity: 'WARNING',
|
|
130
|
+
maxWarnings: 0,
|
|
131
|
+
maxInfo: 0,
|
|
132
|
+
maxTotalRisk: 0,
|
|
133
|
+
failOnNewRegression: true,
|
|
134
|
+
failOnSoftFailures: true,
|
|
135
|
+
softFailureThreshold: 0,
|
|
136
|
+
requireBaseline: true,
|
|
137
|
+
visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 10 },
|
|
138
|
+
coverage: { failOnGap: true, warnOnGap: false },
|
|
139
|
+
evidence: { minCompleteness: 1.0, minIntegrity: 0.98, requireScreenshots: true, requireTraces: true }
|
|
140
|
+
},
|
|
141
|
+
failFast: true,
|
|
142
|
+
evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 1.0, minIntegrity: 0.98 }
|
|
54
143
|
};
|
|
55
144
|
default:
|
|
56
|
-
|
|
145
|
+
throw new Error(`Unknown preset: ${name}`);
|
|
57
146
|
}
|
|
58
147
|
}
|
|
59
148
|
|