@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,181 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class GuardianReporter {
5
+ /**
6
+ * Prepare artifacts directory for the current run
7
+ * @param {string} artifactsDir - Base artifacts directory
8
+ * @returns {object} { runDir, runId }
9
+ */
10
+ prepareArtifactsDir(artifactsDir) {
11
+ const now = new Date();
12
+ const dateStr = now.toISOString()
13
+ .replace(/[:\-]/g, '')
14
+ .substring(0, 15)
15
+ .replace('T', '-');
16
+ const runId = `run-${dateStr}`;
17
+
18
+ const runDir = path.join(artifactsDir, runId);
19
+ if (!fs.existsSync(runDir)) {
20
+ fs.mkdirSync(runDir, { recursive: true });
21
+ }
22
+
23
+ return { runDir, runId };
24
+ }
25
+
26
+ createReport(crawlResult, baseUrl) {
27
+ const { visited, totalDiscovered, totalVisited } = crawlResult;
28
+
29
+ const coverage = totalDiscovered > 0
30
+ ? parseFloat(((totalVisited / totalDiscovered) * 100).toFixed(2))
31
+ : 0;
32
+
33
+ // Calculate confidence
34
+ let confidenceLevel = 'LOW';
35
+ if (coverage >= 85) {
36
+ confidenceLevel = 'HIGH';
37
+ } else if (coverage >= 60) {
38
+ confidenceLevel = 'MEDIUM';
39
+ }
40
+
41
+ // Calculate decision
42
+ let decision = 'READY';
43
+ if (coverage < 30) {
44
+ decision = 'DO_NOT_LAUNCH';
45
+ } else if (coverage < 60) {
46
+ decision = 'INSUFFICIENT_CONFIDENCE';
47
+ }
48
+
49
+ // Check for critical errors (server errors only, not 404s)
50
+ const failedPages = visited.filter(p => p.status && p.status >= 500);
51
+ if (failedPages.length > 0) {
52
+ decision = 'DO_NOT_LAUNCH';
53
+ }
54
+
55
+ const reasons = [];
56
+ if (coverage < 30) reasons.push(`Low coverage (${coverage}%)`);
57
+ if (failedPages.length > 0) reasons.push(`${failedPages.length} pages failed to load`);
58
+ if (coverage >= 60) reasons.push(`Coverage is ${coverage}%`);
59
+ if (failedPages.length === 0 && coverage >= 60) reasons.push('All visited pages loaded successfully');
60
+
61
+ return {
62
+ version: 'mvp-0.1',
63
+ timestamp: new Date().toISOString(),
64
+ baseUrl: baseUrl,
65
+ summary: {
66
+ visitedPages: totalVisited,
67
+ discoveredPages: totalDiscovered,
68
+ coverage: coverage,
69
+ failedPages: failedPages.length
70
+ },
71
+ confidence: {
72
+ level: confidenceLevel,
73
+ reasoning: `Coverage is ${coverage}% with ${failedPages.length} failed pages`
74
+ },
75
+ finalJudgment: {
76
+ decision: decision,
77
+ reasons: reasons.length > 0 ? reasons : ['All checks passed']
78
+ },
79
+ pages: visited.map((p, i) => ({
80
+ index: i + 1,
81
+ url: p.url,
82
+ status: p.status,
83
+ links: p.linkCount,
84
+ depth: p.depth,
85
+ error: p.error || null
86
+ }))
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Create report from flow execution result
92
+ * @param {object} flowResult - Flow execution result
93
+ * @param {string} baseUrl - Base URL
94
+ * @returns {object} Report object
95
+ */
96
+ createFlowReport(flowResult, baseUrl) {
97
+ const { flowId, flowName, success, stepsExecuted, stepsTotal, failedStep, error } = flowResult;
98
+
99
+ const coverage = stepsTotal > 0
100
+ ? parseFloat(((stepsExecuted / stepsTotal) * 100).toFixed(2))
101
+ : 0;
102
+
103
+ // For flows: success = READY, failure = DO_NOT_LAUNCH
104
+ const decision = success ? 'READY' : 'DO_NOT_LAUNCH';
105
+ const confidenceLevel = success ? 'HIGH' : 'LOW';
106
+
107
+ const reasons = [];
108
+ if (success) {
109
+ reasons.push(`Flow "${flowName}" completed successfully`);
110
+ reasons.push(`All ${stepsTotal} steps executed`);
111
+ } else {
112
+ reasons.push(`Flow "${flowName}" failed at step ${failedStep}`);
113
+ reasons.push(`Error: ${error}`);
114
+ }
115
+
116
+ return {
117
+ version: 'mvp-0.2',
118
+ timestamp: new Date().toISOString(),
119
+ baseUrl: baseUrl,
120
+ mode: 'flow',
121
+ flow: {
122
+ id: flowId,
123
+ name: flowName,
124
+ stepsTotal: stepsTotal,
125
+ stepsExecuted: stepsExecuted,
126
+ failedStep: failedStep || null,
127
+ error: error || null
128
+ },
129
+ summary: {
130
+ visitedPages: stepsExecuted,
131
+ discoveredPages: stepsTotal,
132
+ coverage: coverage,
133
+ failedPages: success ? 0 : 1
134
+ },
135
+ confidence: {
136
+ level: confidenceLevel,
137
+ reasoning: success
138
+ ? `Flow completed successfully (${stepsExecuted}/${stepsTotal} steps)`
139
+ : `Flow failed at step ${failedStep}: ${error}`
140
+ },
141
+ finalJudgment: {
142
+ decision: decision,
143
+ reasons: reasons
144
+ },
145
+ pages: flowResult.screenshots ? flowResult.screenshots.map((screenshot, i) => ({
146
+ index: i + 1,
147
+ url: `${baseUrl} (Flow step ${i + 1})`,
148
+ status: 200,
149
+ links: 0,
150
+ screenshot: screenshot
151
+ })) : []
152
+ };
153
+ }
154
+
155
+ saveReport(report, artifactsDir) {
156
+ // Create run directory
157
+ const now = new Date();
158
+ const dateStr = now.toISOString()
159
+ .replace(/[:\-]/g, '')
160
+ .substring(0, 15)
161
+ .replace('T', '-');
162
+ const runId = `run-${dateStr}`;
163
+
164
+ const runDir = path.join(artifactsDir, runId);
165
+ if (!fs.existsSync(runDir)) {
166
+ fs.mkdirSync(runDir, { recursive: true });
167
+ }
168
+
169
+ // Save report.json
170
+ const reportPath = path.join(runDir, 'report.json');
171
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
172
+
173
+ return {
174
+ runId: runId,
175
+ runDir: runDir,
176
+ reportPath: reportPath
177
+ };
178
+ }
179
+ }
180
+
181
+ module.exports = { GuardianReporter };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Phase 4 — Root-Cause Analysis
3
+ * Deterministically derive hints from failure evidence
4
+ */
5
+
6
+ const { BREAK_TYPES } = require('./failure-taxonomy');
7
+
8
+ /**
9
+ * Extract hints from step execution failure
10
+ * @param {Object} step - { type, target, status, error, duration }
11
+ * @returns {string[]} Array of hint strings
12
+ */
13
+ function hintsFromStep(step) {
14
+ const hints = [];
15
+
16
+ if (!step) return hints;
17
+
18
+ const error = (step.error || '').toLowerCase();
19
+ const type = step.type || '';
20
+
21
+ // Navigation
22
+ if (type === 'navigate' && error.includes('timeout')) {
23
+ hints.push('Server not responding or slow to load');
24
+ } else if (type === 'navigate' && error.includes('failed')) {
25
+ hints.push('Navigation failed; verify base URL is correct');
26
+ }
27
+
28
+ // Click failures
29
+ if (type === 'click') {
30
+ if (error.includes('selector') || error.includes('found')) {
31
+ hints.push(`Element selector not found: ${step.target}`);
32
+ } else if (error.includes('visible')) {
33
+ hints.push(`Element not visible or clickable: ${step.target}`);
34
+ } else if (error.includes('timeout')) {
35
+ hints.push(`Timeout clicking ${step.target}; page may be loading slowly`);
36
+ }
37
+ }
38
+
39
+ // Type failures
40
+ if (type === 'type') {
41
+ if (error.includes('selector') || error.includes('found')) {
42
+ hints.push(`Input field not found: ${step.target}`);
43
+ } else if (error.includes('timeout')) {
44
+ hints.push(`Timeout typing into ${step.target}; page lag detected`);
45
+ }
46
+ }
47
+
48
+ // WaitFor failures
49
+ if (type === 'waitFor') {
50
+ if (error.includes('timeout')) {
51
+ hints.push(`Success element never appeared: ${step.target}`);
52
+ hints.push('Submission may have silently failed or redirected');
53
+ }
54
+ }
55
+
56
+ // Submit failures
57
+ if (type === 'submit') {
58
+ hints.push('Form submission did not complete; check form validation');
59
+ }
60
+
61
+ return hints;
62
+ }
63
+
64
+ /**
65
+ * Extract hints from validator results
66
+ * @param {Array} validators - Array of { type, status, message, evidence }
67
+ * @returns {string[]} Array of hint strings
68
+ */
69
+ function hintsFromValidators(validators) {
70
+ const hints = [];
71
+
72
+ if (!Array.isArray(validators)) return hints;
73
+
74
+ for (const v of validators) {
75
+ if (v.status === 'FAIL') {
76
+ if (v.type === 'elementVisible') {
77
+ hints.push(`Expected element not visible: ${v.evidence?.selector}`);
78
+ } else if (v.type === 'pageContainsAnyText') {
79
+ hints.push(`Page text check failed; expected one of: ${v.evidence?.searchTerms?.join(', ')}`);
80
+ } else if (v.type === 'elementContainsText') {
81
+ hints.push(`Element text mismatch; expected: ${v.evidence?.expectedText}`);
82
+ } else if (v.type === 'htmlLangAttribute') {
83
+ hints.push(`HTML lang attribute unexpected; expected: ${v.evidence?.expected}`);
84
+ } else if (v.type === 'urlIncludes' || v.type === 'urlMatches') {
85
+ hints.push(`URL does not match expected pattern: ${v.evidence?.pattern || v.evidence?.expected}`);
86
+ } else {
87
+ hints.push(`Validator failed: ${v.type}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ return hints;
93
+ }
94
+
95
+ /**
96
+ * Extract hints from friction signals
97
+ * @param {Array} signals - Array of { id, description, threshold, observedValue }
98
+ * @returns {string[]} Array of hint strings
99
+ */
100
+ function hintsFromFriction(signals) {
101
+ const hints = [];
102
+
103
+ if (!Array.isArray(signals)) return hints;
104
+
105
+ for (const sig of signals) {
106
+ if (sig.id === 'slow_step_execution') {
107
+ hints.push(`Step took ${sig.observedValue}ms (threshold ${sig.threshold}ms); network or page lag`);
108
+ } else if (sig.id === 'multiple_retries_required') {
109
+ hints.push(`Step needed ${sig.observedValue} retries; interaction unreliable`);
110
+ } else if (sig.id === 'slow_total_duration') {
111
+ hints.push(`Total attempt took ${sig.observedValue}ms; slow server response`);
112
+ }
113
+ }
114
+
115
+ return hints;
116
+ }
117
+
118
+ /**
119
+ * Derive root-cause hints from complete failure evidence
120
+ * @param {Object} failure - Attempt or flow result
121
+ * @param {string} breakType - BREAK_TYPE
122
+ * @returns {Object} { hints: string[], primaryHint: string }
123
+ */
124
+ function deriveRootCauseHints(failure, breakType) {
125
+ const allHints = new Set();
126
+
127
+ // Step-level evidence
128
+ if (failure.steps && Array.isArray(failure.steps)) {
129
+ const failedStep = failure.steps.find(s => s.status === 'failed');
130
+ if (failedStep) {
131
+ hintsFromStep(failedStep).forEach(h => allHints.add(h));
132
+ }
133
+ }
134
+
135
+ // Validator evidence
136
+ if (failure.validators) {
137
+ hintsFromValidators(failure.validators).forEach(h => allHints.add(h));
138
+ }
139
+
140
+ // Friction signals
141
+ if (failure.friction && failure.friction.signals) {
142
+ hintsFromFriction(failure.friction.signals).forEach(h => allHints.add(h));
143
+ }
144
+
145
+ // Fallback hints by break type
146
+ if (allHints.size === 0) {
147
+ const fallbacks = {
148
+ [BREAK_TYPES.NAVIGATION]: 'Server unreachable or incorrect URL',
149
+ [BREAK_TYPES.SUBMISSION]: 'Form submission did not complete',
150
+ [BREAK_TYPES.VALIDATION]: 'Success validation did not pass',
151
+ [BREAK_TYPES.TIMEOUT]: 'Step timed out; slow response or missing element',
152
+ [BREAK_TYPES.VISUAL]: 'Expected element not found on page',
153
+ [BREAK_TYPES.CONSOLE]: 'Console error prevented progress',
154
+ [BREAK_TYPES.NETWORK]: 'Network error or lost connection'
155
+ };
156
+ allHints.add(fallbacks[breakType] || 'Unknown failure');
157
+ }
158
+
159
+ const hints = Array.from(allHints);
160
+ return {
161
+ hints,
162
+ primaryHint: hints[0] || 'Unknown failure'
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ deriveRootCauseHints,
168
+ hintsFromStep,
169
+ hintsFromValidators,
170
+ hintsFromFriction
171
+ };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Guardian Safety Guards Module
3
+ * Prevents destructive or dangerous actions during testing
4
+ */
5
+
6
+ class GuardianSafety {
7
+ constructor(options = {}) {
8
+ // URL patterns to avoid (logout, delete, admin, etc.)
9
+ this.denyUrlPatterns = options.denyUrlPatterns || [
10
+ 'logout',
11
+ 'signout',
12
+ 'sign-out',
13
+ 'log-out',
14
+ 'delete',
15
+ 'remove',
16
+ 'destroy',
17
+ 'admin',
18
+ 'settings',
19
+ 'account/close',
20
+ 'account/delete',
21
+ 'unsubscribe',
22
+ 'cancel',
23
+ ];
24
+
25
+ // CSS selectors to avoid clicking
26
+ this.denySelectors = options.denySelectors || [
27
+ '[data-danger]',
28
+ '[data-destructive]',
29
+ '.btn-delete',
30
+ '.btn-danger',
31
+ '.delete-button',
32
+ 'button[type="reset"]',
33
+ 'a[href*="logout"]',
34
+ 'a[href*="delete"]',
35
+ 'a[href*="remove"]',
36
+ ];
37
+
38
+ // Form submissions require explicit permission
39
+ this.blockFormSubmitsByDefault = options.blockFormSubmitsByDefault !== false;
40
+
41
+ // Payment-related actions require explicit permission
42
+ this.blockPaymentsByDefault = options.blockPaymentsByDefault !== false;
43
+
44
+ // Payment-related keywords
45
+ this.paymentKeywords = [
46
+ 'payment',
47
+ 'checkout',
48
+ 'purchase',
49
+ 'buy',
50
+ 'pay',
51
+ 'card',
52
+ 'billing',
53
+ 'stripe',
54
+ 'paypal',
55
+ ];
56
+ }
57
+
58
+ /**
59
+ * Check if URL is safe to visit
60
+ * @param {string} url - URL to check
61
+ * @returns {object} { safe: boolean, reason: string }
62
+ */
63
+ isUrlSafe(url) {
64
+ try {
65
+ const urlLower = url.toLowerCase();
66
+
67
+ for (const pattern of this.denyUrlPatterns) {
68
+ if (urlLower.includes(pattern.toLowerCase())) {
69
+ return {
70
+ safe: false,
71
+ reason: `URL contains blocked pattern: "${pattern}"`,
72
+ };
73
+ }
74
+ }
75
+
76
+ return { safe: true, reason: null };
77
+ } catch (error) {
78
+ return {
79
+ safe: false,
80
+ reason: `Invalid URL: ${error.message}`,
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if selector is safe to click
87
+ * @param {string} selector - CSS selector
88
+ * @returns {object} { safe: boolean, reason: string }
89
+ */
90
+ isSelectorSafe(selector) {
91
+ try {
92
+ const selectorLower = selector.toLowerCase();
93
+
94
+ for (const denyPattern of this.denySelectors) {
95
+ if (selectorLower.includes(denyPattern.toLowerCase())) {
96
+ return {
97
+ safe: false,
98
+ reason: `Selector matches blocked pattern: "${denyPattern}"`,
99
+ };
100
+ }
101
+ }
102
+
103
+ return { safe: true, reason: null };
104
+ } catch (error) {
105
+ return {
106
+ safe: false,
107
+ reason: `Invalid selector: ${error.message}`,
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check if element text suggests dangerous action
114
+ * @param {string} text - Element text content
115
+ * @returns {object} { safe: boolean, reason: string }
116
+ */
117
+ isTextSafe(text) {
118
+ if (!text) {
119
+ return { safe: true, reason: null };
120
+ }
121
+
122
+ const textLower = text.toLowerCase().trim();
123
+ const dangerousWords = [
124
+ 'logout',
125
+ 'log out',
126
+ 'sign out',
127
+ 'delete',
128
+ 'remove',
129
+ 'destroy',
130
+ 'cancel account',
131
+ 'close account',
132
+ 'unsubscribe',
133
+ ];
134
+
135
+ for (const word of dangerousWords) {
136
+ if (textLower.includes(word)) {
137
+ return {
138
+ safe: false,
139
+ reason: `Text contains dangerous word: "${word}"`,
140
+ };
141
+ }
142
+ }
143
+
144
+ return { safe: true, reason: null };
145
+ }
146
+
147
+ /**
148
+ * Check if action involves payment
149
+ * @param {string} context - Context (URL, selector, or text)
150
+ * @returns {boolean} True if payment-related
151
+ */
152
+ isPaymentRelated(context) {
153
+ if (!context) {
154
+ return false;
155
+ }
156
+
157
+ const contextLower = context.toLowerCase();
158
+
159
+ for (const keyword of this.paymentKeywords) {
160
+ if (contextLower.includes(keyword)) {
161
+ return true;
162
+ }
163
+ }
164
+
165
+ return false;
166
+ }
167
+
168
+ /**
169
+ * Check if form submission is safe
170
+ * @param {string} formAction - Form action URL or selector
171
+ * @param {object} formData - Form data being submitted
172
+ * @returns {object} { safe: boolean, reason: string }
173
+ */
174
+ isFormSubmitSafe(formAction, formData = {}) {
175
+ // Check if form submissions are globally blocked
176
+ if (this.blockFormSubmitsByDefault) {
177
+ return {
178
+ safe: false,
179
+ reason: 'Form submissions are blocked by default (safety guard)',
180
+ };
181
+ }
182
+
183
+ // Check if form action URL is safe
184
+ if (formAction) {
185
+ const urlCheck = this.isUrlSafe(formAction);
186
+ if (!urlCheck.safe) {
187
+ return urlCheck;
188
+ }
189
+
190
+ // Check if payment-related
191
+ if (this.blockPaymentsByDefault && this.isPaymentRelated(formAction)) {
192
+ return {
193
+ safe: false,
194
+ reason: 'Form submission appears payment-related (blocked by safety guard)',
195
+ };
196
+ }
197
+ }
198
+
199
+ // Check form data for sensitive fields
200
+ const formDataStr = JSON.stringify(formData).toLowerCase();
201
+ if (this.blockPaymentsByDefault && this.isPaymentRelated(formDataStr)) {
202
+ return {
203
+ safe: false,
204
+ reason: 'Form data contains payment-related fields (blocked by safety guard)',
205
+ };
206
+ }
207
+
208
+ return { safe: true, reason: null };
209
+ }
210
+
211
+ /**
212
+ * Filter URLs to remove unsafe ones
213
+ * @param {string[]} urls - Array of URLs
214
+ * @returns {object} { safe: string[], blocked: Array<{url, reason}> }
215
+ */
216
+ filterUrls(urls) {
217
+ const safe = [];
218
+ const blocked = [];
219
+
220
+ for (const url of urls) {
221
+ const check = this.isUrlSafe(url);
222
+ if (check.safe) {
223
+ safe.push(url);
224
+ } else {
225
+ blocked.push({ url, reason: check.reason });
226
+ }
227
+ }
228
+
229
+ return { safe, blocked };
230
+ }
231
+
232
+ /**
233
+ * Get safety summary (how many URLs/actions were blocked)
234
+ * @param {object} stats - Statistics object
235
+ * @returns {object} Safety summary
236
+ */
237
+ getSummary(stats = {}) {
238
+ return {
239
+ urlsBlocked: stats.urlsBlocked || 0,
240
+ selectorsBlocked: stats.selectorsBlocked || 0,
241
+ formsBlocked: stats.formsBlocked || 0,
242
+ totalBlocked: (stats.urlsBlocked || 0) + (stats.selectorsBlocked || 0) + (stats.formsBlocked || 0),
243
+ safetyEnabled: true,
244
+ };
245
+ }
246
+ }
247
+
248
+ module.exports = GuardianSafety;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Scan Presets (Phase 6)
3
+ * Opinionated defaults for one-command scans
4
+ * Deterministic mappings: attempts, flows, policy thresholds.
5
+ */
6
+
7
+ const { getDefaultAttemptIds } = require('./attempt-registry');
8
+ const { getDefaultFlowIds } = require('./flow-registry');
9
+
10
+ function resolveScanPreset(name = 'landing') {
11
+ const preset = (name || '').toLowerCase();
12
+
13
+ // Defaults: curated attempts + curated flows
14
+ const defaults = {
15
+ attempts: getDefaultAttemptIds(),
16
+ flows: getDefaultFlowIds(),
17
+ policy: null
18
+ };
19
+
20
+ switch (preset) {
21
+ case 'landing':
22
+ return {
23
+ attempts: ['contact_form', 'language_switch', 'newsletter_signup'],
24
+ flows: [], // focus on landing conversion, flows optional
25
+ policy: {
26
+ // lenient warnings, strict criticals
27
+ failOnSeverity: 'CRITICAL',
28
+ maxWarnings: 999,
29
+ visualGates: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 25 }
30
+ }
31
+ };
32
+ case 'saas':
33
+ return {
34
+ attempts: ['language_switch', 'contact_form', 'newsletter_signup'],
35
+ flows: ['signup_flow', 'login_flow'],
36
+ policy: {
37
+ failOnSeverity: 'CRITICAL',
38
+ maxWarnings: 1,
39
+ failOnNewRegression: true,
40
+ visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 20 }
41
+ }
42
+ };
43
+ case 'shop':
44
+ case 'ecommerce':
45
+ return {
46
+ attempts: ['language_switch', 'contact_form', 'newsletter_signup'],
47
+ flows: ['checkout_flow'],
48
+ policy: {
49
+ failOnSeverity: 'CRITICAL',
50
+ maxWarnings: 0,
51
+ failOnNewRegression: true,
52
+ visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 15 }
53
+ }
54
+ };
55
+ default:
56
+ return defaults;
57
+ }
58
+ }
59
+
60
+ module.exports = { resolveScanPreset };