@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1345 -60
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +21 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +568 -7
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +24 -0
  21. package/src/guardian/baseline.js +12 -4
  22. package/src/guardian/breakage-intelligence.js +1 -0
  23. package/src/guardian/ci-cli.js +121 -0
  24. package/src/guardian/ci-output.js +4 -3
  25. package/src/guardian/cli-summary.js +79 -92
  26. package/src/guardian/config-loader.js +162 -0
  27. package/src/guardian/drift-detector.js +100 -0
  28. package/src/guardian/enhanced-html-reporter.js +221 -4
  29. package/src/guardian/env-guard.js +127 -0
  30. package/src/guardian/failure-intelligence.js +173 -0
  31. package/src/guardian/first-run-profile.js +89 -0
  32. package/src/guardian/first-run.js +6 -1
  33. package/src/guardian/flag-validator.js +17 -3
  34. package/src/guardian/html-reporter.js +2 -0
  35. package/src/guardian/human-reporter.js +431 -0
  36. package/src/guardian/index.js +22 -19
  37. package/src/guardian/init-command.js +9 -5
  38. package/src/guardian/intent-detector.js +146 -0
  39. package/src/guardian/journey-definitions.js +132 -0
  40. package/src/guardian/journey-scan-cli.js +145 -0
  41. package/src/guardian/journey-scanner.js +583 -0
  42. package/src/guardian/junit-reporter.js +18 -1
  43. package/src/guardian/live-cli.js +95 -0
  44. package/src/guardian/live-scheduler-runner.js +137 -0
  45. package/src/guardian/live-scheduler.js +146 -0
  46. package/src/guardian/market-reporter.js +341 -81
  47. package/src/guardian/pattern-analyzer.js +348 -0
  48. package/src/guardian/policy.js +80 -3
  49. package/src/guardian/preset-loader.js +9 -6
  50. package/src/guardian/reality.js +1278 -117
  51. package/src/guardian/reporter.js +27 -41
  52. package/src/guardian/run-artifacts.js +212 -0
  53. package/src/guardian/run-cleanup.js +207 -0
  54. package/src/guardian/run-latest.js +90 -0
  55. package/src/guardian/run-list.js +211 -0
  56. package/src/guardian/scan-presets.js +100 -11
  57. package/src/guardian/selector-fallbacks.js +394 -0
  58. package/src/guardian/semantic-contact-finder.js +2 -1
  59. package/src/guardian/site-introspection.js +257 -0
  60. package/src/guardian/smoke.js +2 -2
  61. package/src/guardian/snapshot-schema.js +25 -1
  62. package/src/guardian/snapshot.js +46 -2
  63. package/src/guardian/stability-scorer.js +169 -0
  64. package/src/guardian/template-command.js +184 -0
  65. package/src/guardian/text-formatters.js +426 -0
  66. package/src/guardian/verdict.js +320 -0
  67. package/src/guardian/verdicts.js +74 -0
  68. package/src/guardian/watch-runner.js +3 -7
  69. package/src/payments/stripe-checkout.js +169 -0
  70. package/src/plans/plan-definitions.js +148 -0
  71. package/src/plans/plan-manager.js +211 -0
  72. package/src/plans/usage-tracker.js +210 -0
  73. package/src/recipes/recipe-engine.js +188 -0
  74. package/src/recipes/recipe-failure-analysis.js +159 -0
  75. package/src/recipes/recipe-registry.js +134 -0
  76. package/src/recipes/recipe-runtime.js +507 -0
  77. package/src/recipes/recipe-store.js +410 -0
  78. package/guardian-contract-v1.md +0 -149
  79. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  80. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  81. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  82. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  83. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  84. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -45,7 +45,7 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
45
45
  * @property {string} attemptId - unique attempt identifier
46
46
  * @property {string} attemptName - human-readable name
47
47
  * @property {string} goal - what the user tried to achieve
48
- * @property {string} outcome - 'SUCCESS', 'FAILURE', or 'FRICTION'
48
+ * @property {string} outcome - 'SUCCESS', 'FAILURE', 'FRICTION', 'NOT_APPLICABLE', 'DISCOVERY_FAILED', 'SKIPPED'
49
49
  * @property {number} totalDurationMs - elapsed time
50
50
  * @property {number} stepCount - how many steps executed
51
51
  * @property {number} failedStepIndex - index of first failed step, or -1 if all succeeded
@@ -53,6 +53,10 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
53
53
  * @property {ValidatorResult[]} [validators] - soft failure detectors (Phase 2)
54
54
  * @property {number} [softFailureCount] - count of failed validators
55
55
  * @property {string} [riskCategory] - 'LEAD', 'REVENUE', 'TRUST/UX' (Phase 2)
56
+ * @property {string} [skipReason] - reason if SKIPPED, NOT_APPLICABLE, or DISCOVERY_FAILED
57
+ * @property {string[]} [selectorChainTried] - selectors attempted during discovery
58
+ * @property {Object} [discoverySignals] - element discovery signals and heuristics
59
+ * @property {string} [finalSelection] - which selector/strategy successfully matched element
56
60
  */
57
61
 
58
62
  /**
@@ -133,6 +137,13 @@ const SNAPSHOT_SCHEMA_VERSION = 'v1';
133
137
  * @typedef {Object} MarketRealitySnapshot
134
138
  * @property {string} schemaVersion - always 'v1'
135
139
  * @property {SnapshotMeta} meta
140
+ * @property {Object} [verdict] - unified run-level verdict
141
+ * @property {('READY'|'DO_NOT_LAUNCH'|'FRICTION')} verdict.verdict
142
+ * @property {{ level: ('low'|'medium'|'high'), score: number, reasons: string[] }} verdict.confidence
143
+ * @property {string} verdict.why
144
+ * @property {string[]} verdict.keyFindings
145
+ * @property {{ screenshots?: string[], traces?: string[], reportPaths?: string[], affectedPages?: string[] }} verdict.evidence
146
+ * @property {string[]} verdict.limits
136
147
  * @property {CrawlResult} [crawl]
137
148
  * @property {AttemptResult[]} attempts
138
149
  * @property {Array} flows
@@ -166,6 +177,7 @@ function createEmptySnapshot(baseUrl, runId, toolVersion) {
166
177
  attempts: [],
167
178
  flows: [],
168
179
  signals: [],
180
+ verdict: null,
169
181
  riskSummary: {
170
182
  totalSoftFailures: 0,
171
183
  totalFriction: 0,
@@ -253,6 +265,18 @@ function validateSnapshot(snapshot) {
253
265
  errors.push('Missing baseline section');
254
266
  }
255
267
 
268
+ // Basic verdict validation (if present)
269
+ if (snapshot.verdict) {
270
+ const v = snapshot.verdict;
271
+ const allowed = ['READY', 'DO_NOT_LAUNCH', 'FRICTION'];
272
+ if (!v.verdict || !allowed.includes(v.verdict)) {
273
+ errors.push('Invalid verdict.verdict');
274
+ }
275
+ if (!v.confidence || typeof v.confidence.score !== 'number' || v.confidence.score < 0 || v.confidence.score > 1) {
276
+ errors.push('Invalid verdict.confidence.score');
277
+ }
278
+ }
279
+
256
280
  return {
257
281
  valid: errors.length === 0,
258
282
  errors
@@ -34,10 +34,44 @@ class SnapshotBuilder {
34
34
  };
35
35
  }
36
36
 
37
+ /**
38
+ * Set unified verdict object
39
+ */
40
+ setVerdict(verdict) {
41
+ if (!verdict) return;
42
+ this.snapshot.verdict = {
43
+ verdict: verdict.verdict,
44
+ confidence: verdict.confidence,
45
+ why: verdict.why || '',
46
+ keyFindings: Array.isArray(verdict.keyFindings) ? verdict.keyFindings.slice(0, 7) : [],
47
+ evidence: verdict.evidence || {},
48
+ limits: Array.isArray(verdict.limits) ? verdict.limits.slice(0, 6) : []
49
+ };
50
+ }
51
+
37
52
  /**
38
53
  * Add attempt result to snapshot
39
54
  */
40
55
  addAttempt(attemptResult, artifactDir) {
56
+ // Handle NOT_APPLICABLE and DISCOVERY_FAILED attempts
57
+ if (attemptResult.outcome === 'NOT_APPLICABLE' || attemptResult.outcome === 'DISCOVERY_FAILED') {
58
+ this.snapshot.attempts.push({
59
+ attemptId: attemptResult.attemptId,
60
+ attemptName: attemptResult.attemptName,
61
+ goal: attemptResult.goal,
62
+ outcome: attemptResult.outcome,
63
+ executed: false,
64
+ skipReason: attemptResult.skipReason || (attemptResult.outcome === 'NOT_APPLICABLE' ? 'Feature not present' : 'Element discovery failed'),
65
+ skipReasonCode: attemptResult.skipReasonCode,
66
+ discoverySignals: attemptResult.discoverySignals || {},
67
+ totalDurationMs: attemptResult.totalDurationMs || 0,
68
+ stepCount: attemptResult.stepCount || 0,
69
+ failedStepIndex: -1,
70
+ friction: null
71
+ });
72
+ return; // Don't create signals for non-applicable attempts
73
+ }
74
+
41
75
  // Phase 7.4: Handle SKIPPED attempts (don't add as signal)
42
76
  if (attemptResult.outcome === 'SKIPPED') {
43
77
  this.snapshot.attempts.push({
@@ -45,7 +79,9 @@ class SnapshotBuilder {
45
79
  attemptName: attemptResult.attemptName,
46
80
  goal: attemptResult.goal,
47
81
  outcome: 'SKIPPED',
82
+ executed: false,
48
83
  skipReason: attemptResult.skipReason || 'Prerequisites not met',
84
+ skipReasonCode: attemptResult.skipReasonCode,
49
85
  totalDurationMs: 0,
50
86
  stepCount: 0,
51
87
  failedStepIndex: -1,
@@ -71,10 +107,17 @@ class SnapshotBuilder {
71
107
  attemptName: attemptResult.attemptName,
72
108
  goal: attemptResult.goal,
73
109
  outcome: attemptResult.outcome,
110
+ executed: true,
111
+ discoverySignals: attemptResult.discoverySignals || {},
74
112
  totalDurationMs: attemptResult.attemptResult?.totalDurationMs || 0,
75
113
  stepCount: (attemptResult.steps || []).length,
76
114
  failedStepIndex: (attemptResult.steps || []).findIndex(s => s.status === 'failed'),
77
- friction: attemptResult.friction || null
115
+ friction: attemptResult.friction || null,
116
+ evidenceSummary: {
117
+ screenshots: (attemptResult.steps || []).reduce((sum, s) => sum + (Array.isArray(s.screenshots) ? s.screenshots.length : 0), 0),
118
+ validators: Array.isArray(attemptResult.validators) ? attemptResult.validators.length : 0,
119
+ tracesCaptured: attemptResult.tracePath ? 1 : 0
120
+ }
78
121
  });
79
122
 
80
123
  // Track artifacts
@@ -82,7 +125,8 @@ class SnapshotBuilder {
82
125
  this.snapshot.evidence.attemptArtifacts[attemptResult.attemptId] = {
83
126
  reportJson: path.join(attemptResult.attemptId, 'attempt-report.json'),
84
127
  reportHtml: path.join(attemptResult.attemptId, 'attempt-report.html'),
85
- screenshotDir: path.join(attemptResult.attemptId, 'attempt-screenshots')
128
+ screenshotDir: path.join(attemptResult.attemptId, 'attempt-screenshots'),
129
+ attemptJson: attemptResult.attemptJsonPath ? path.relative(artifactDir, attemptResult.attemptJsonPath) : undefined
86
130
  };
87
131
  }
88
132
 
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Stability Scorer - Real-world reliability metrics
3
+ *
4
+ * Measures how stable a journey run was:
5
+ * - Per-step stability (transient vs deterministic failures)
6
+ * - Overall run stability score (0-100)
7
+ */
8
+
9
+ /**
10
+ * Classify error type for determining if it's transient or deterministic
11
+ * @param {string} errorMessage - Error message from step
12
+ * @returns {object} - { isTransient: boolean, classification: string }
13
+ */
14
+ function classifyErrorType(errorMessage) {
15
+ if (!errorMessage) return { isTransient: false, classification: 'UNKNOWN' };
16
+
17
+ const msg = errorMessage.toLowerCase();
18
+
19
+ // Transient errors (safe to retry)
20
+ if (msg.includes('timeout') || msg.includes('timed out')) {
21
+ return { isTransient: true, classification: 'TIMEOUT' };
22
+ }
23
+ if (msg.includes('navigation') && (msg.includes('timeout') || msg.includes('closed'))) {
24
+ return { isTransient: true, classification: 'NAVIGATION_TIMEOUT' };
25
+ }
26
+ if (msg.includes('detached') || msg.includes('frame')) {
27
+ return { isTransient: true, classification: 'DETACHED_FRAME' };
28
+ }
29
+ if (msg.includes('econnrefused') || msg.includes('network') || msg.includes('socket')) {
30
+ return { isTransient: true, classification: 'NETWORK_ERROR' };
31
+ }
32
+ if (msg.includes('connection') && (msg.includes('reset') || msg.includes('closed'))) {
33
+ return { isTransient: true, classification: 'CONNECTION_ERROR' };
34
+ }
35
+
36
+ // Deterministic errors (don't retry)
37
+ if (msg.includes('not found') && (msg.includes('cta') || msg.includes('element'))) {
38
+ return { isTransient: false, classification: 'ELEMENT_NOT_FOUND' };
39
+ }
40
+ if (msg.includes('not visible')) {
41
+ return { isTransient: false, classification: 'ELEMENT_NOT_VISIBLE' };
42
+ }
43
+ if (msg.includes('cta') && msg.includes('found')) {
44
+ return { isTransient: false, classification: 'CTA_NOT_FOUND' };
45
+ }
46
+
47
+ // Default: assume transient to be safe
48
+ return { isTransient: true, classification: 'UNKNOWN' };
49
+ }
50
+
51
+ /**
52
+ * Compute stability score for a single step
53
+ * @param {object} step - Executed step result { id, name, success, attemptNumber, error }
54
+ * @returns {object} - { attempts, finalStatus, stable, confidence, errorType }
55
+ */
56
+ function scoreStepStability(step) {
57
+ const attempts = step.attemptNumber || 1;
58
+ const finalStatus = step.success ? 'SUCCESS' : 'FAILED';
59
+ const errorType = classifyErrorType(step.error);
60
+
61
+ // Determine stability
62
+ let stable = true;
63
+ let confidence = 100;
64
+
65
+ if (finalStatus === 'SUCCESS') {
66
+ if (attempts > 1) {
67
+ // Success after retries = transient failure
68
+ stable = true; // The step ultimately worked
69
+ confidence = Math.max(30, 100 - (attempts - 1) * 20);
70
+ }
71
+ } else {
72
+ // Step failed all retries
73
+ stable = false;
74
+ confidence = 10; // Very low confidence in a consistently failing step
75
+ }
76
+
77
+ return {
78
+ stepId: step.id,
79
+ attempts,
80
+ finalStatus,
81
+ stable,
82
+ confidence,
83
+ errorType: errorType.classification,
84
+ isTransient: errorType.isTransient
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Compute overall run stability score (0-100)
90
+ * @param {object} result - Journey result with executedSteps array
91
+ * @returns {number} - Stability score 0-100
92
+ */
93
+ function computeRunStabilityScore(result) {
94
+ const steps = result.executedSteps || [];
95
+
96
+ if (steps.length === 0) return 0;
97
+
98
+ // Calculate step-level stability
99
+ const stepScores = steps.map(scoreStepStability);
100
+
101
+ // Count how many steps needed retries
102
+ const stepsWithRetries = stepScores.filter(s => s.attempts > 1).length;
103
+ const failedSteps = stepScores.filter(s => s.finalStatus === 'FAILED').length;
104
+
105
+ // Scoring algorithm:
106
+ // - Start at 100
107
+ // - Deduct 10 points per step that needed retries
108
+ // - Deduct 30 points per failed step
109
+ // - Floor at 0
110
+ let score = 100;
111
+ score -= stepsWithRetries * 10;
112
+ score -= failedSteps * 30;
113
+ score = Math.max(0, score);
114
+
115
+ // Consistency check: if goalReached varies, reduce score
116
+ // (This is a simple heuristic; more complex consistency checks could be added)
117
+ const hasInconsistency = false; // Would need multiple runs to detect
118
+ if (hasInconsistency) {
119
+ score = Math.max(0, score - 20);
120
+ }
121
+
122
+ return Math.round(score);
123
+ }
124
+
125
+ /**
126
+ * Build stability report from journey result
127
+ * @param {object} result - Journey scan result
128
+ * @returns {object} - Stability report with scores and metrics
129
+ */
130
+ function buildStabilityReport(result) {
131
+ const steps = result.executedSteps || [];
132
+ const stepStability = steps.map(scoreStepStability);
133
+ const runScore = computeRunStabilityScore(result);
134
+
135
+ const metrics = {
136
+ totalSteps: steps.length,
137
+ succeededSteps: stepStability.filter(s => s.finalStatus === 'SUCCESS').length,
138
+ failedSteps: stepStability.filter(s => s.finalStatus === 'FAILED').length,
139
+ stepsWithRetries: stepStability.filter(s => s.attempts > 1).length,
140
+ totalAttempts: stepStability.reduce((sum, s) => sum + s.attempts, 0)
141
+ };
142
+
143
+ return {
144
+ runStabilityScore: runScore,
145
+ metrics,
146
+ stepStability,
147
+ assessment: assessStability(runScore)
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Assess stability level based on score
153
+ * @param {number} score - Stability score 0-100
154
+ * @returns {string} - Assessment: 'excellent' | 'good' | 'fair' | 'poor'
155
+ */
156
+ function assessStability(score) {
157
+ if (score >= 80) return 'excellent';
158
+ if (score >= 60) return 'good';
159
+ if (score >= 40) return 'fair';
160
+ return 'poor';
161
+ }
162
+
163
+ module.exports = {
164
+ classifyErrorType,
165
+ scoreStepStability,
166
+ computeRunStabilityScore,
167
+ buildStabilityReport,
168
+ assessStability
169
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Template Command
3
+ *
4
+ * Generate minimal config templates:
5
+ * guardian template saas
6
+ * guardian template shop
7
+ * guardian template landing
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const TEMPLATES = {
14
+ saas: {
15
+ name: 'SaaS Startup Check',
16
+ description: 'Verify core SaaS flows: signup, login, dashboard',
17
+ intent: {
18
+ category: 'product_type',
19
+ value: 'saas',
20
+ confidence: 1.0
21
+ },
22
+ journeys: [
23
+ {
24
+ id: 'signup_flow',
25
+ name: 'User Signup',
26
+ description: 'Verify signup journey completes',
27
+ steps: [
28
+ { action: 'navigate', target: '/', name: 'Home' },
29
+ { action: 'click', target: 'a[href*="/signup"], button:has-text("Sign up")', name: 'Click signup' },
30
+ { action: 'wait', ms: 2000 },
31
+ { action: 'screenshot', name: 'Signup form loaded' }
32
+ ],
33
+ criticality: 'CRITICAL',
34
+ onFailure: 'FAIL'
35
+ },
36
+ {
37
+ id: 'landing_page',
38
+ name: 'Landing Page Load',
39
+ description: 'Verify homepage loads without errors',
40
+ steps: [
41
+ { action: 'navigate', target: '/', name: 'Home' },
42
+ { action: 'screenshot', name: 'Homepage loaded' }
43
+ ],
44
+ criticality: 'CRITICAL',
45
+ onFailure: 'WARN'
46
+ }
47
+ ],
48
+ policy: {
49
+ failOnSeverity: 'CRITICAL',
50
+ maxWarnings: 10,
51
+ requireBaseline: false
52
+ }
53
+ },
54
+
55
+ shop: {
56
+ name: 'E-Commerce Shop Check',
57
+ description: 'Verify core shop flows: browse, add to cart, checkout',
58
+ intent: {
59
+ category: 'product_type',
60
+ value: 'ecommerce',
61
+ confidence: 1.0
62
+ },
63
+ journeys: [
64
+ {
65
+ id: 'browse_products',
66
+ name: 'Browse Products',
67
+ description: 'Verify product catalog loads',
68
+ steps: [
69
+ { action: 'navigate', target: '/', name: 'Home' },
70
+ { action: 'click', target: 'a[href*="/shop"], a[href*="/products"], button:has-text("Shop")', name: 'Go to shop' },
71
+ { action: 'wait', ms: 2000 },
72
+ { action: 'screenshot', name: 'Products page' }
73
+ ],
74
+ criticality: 'CRITICAL',
75
+ onFailure: 'FAIL'
76
+ },
77
+ {
78
+ id: 'add_to_cart',
79
+ name: 'Add to Cart',
80
+ description: 'Verify add-to-cart flow',
81
+ steps: [
82
+ { action: 'navigate', target: '/', name: 'Home' },
83
+ { action: 'click', target: 'a[href*="/shop"], a[href*="/products"]', name: 'Go to shop' },
84
+ { action: 'wait', ms: 2000 },
85
+ { action: 'click', target: 'button:has-text("Add"), button:has-text("Cart")', name: 'Add item' },
86
+ { action: 'screenshot', name: 'Cart updated' }
87
+ ],
88
+ criticality: 'CRITICAL',
89
+ onFailure: 'WARN'
90
+ }
91
+ ],
92
+ policy: {
93
+ failOnSeverity: 'CRITICAL',
94
+ maxWarnings: 10,
95
+ requireBaseline: false
96
+ }
97
+ },
98
+
99
+ landing: {
100
+ name: 'Landing Page Check',
101
+ description: 'Verify landing page loads and core CTAs work',
102
+ intent: {
103
+ category: 'product_type',
104
+ value: 'landing',
105
+ confidence: 1.0
106
+ },
107
+ journeys: [
108
+ {
109
+ id: 'page_load',
110
+ name: 'Page Load',
111
+ description: 'Verify landing page loads',
112
+ steps: [
113
+ { action: 'navigate', target: '/', name: 'Home' },
114
+ { action: 'screenshot', name: 'Landing page' }
115
+ ],
116
+ criticality: 'CRITICAL',
117
+ onFailure: 'FAIL'
118
+ },
119
+ {
120
+ id: 'cta_click',
121
+ name: 'Call-to-Action',
122
+ description: 'Verify main CTA is clickable',
123
+ steps: [
124
+ { action: 'navigate', target: '/', name: 'Home' },
125
+ { action: 'click', target: 'button:has-text("Get started"), button:has-text("Start"), button:has-text("Sign up"), a[href*="/signup"]', name: 'Click CTA' },
126
+ { action: 'screenshot', name: 'CTA works' }
127
+ ],
128
+ criticality: 'CRITICAL',
129
+ onFailure: 'WARN'
130
+ }
131
+ ],
132
+ policy: {
133
+ failOnSeverity: 'CRITICAL',
134
+ maxWarnings: 5,
135
+ requireBaseline: false
136
+ }
137
+ }
138
+ };
139
+
140
+ /**
141
+ * Generate config file from template
142
+ * @param {string} templateName - saas, shop, or landing
143
+ * @param {object} options - Generation options
144
+ * @returns {object} Result with generated file path
145
+ */
146
+ function generateTemplate(templateName, options = {}) {
147
+ const cwd = options.cwd || process.cwd();
148
+ const outputFile = options.output || `guardian-${templateName}.json`;
149
+ const outputPath = path.join(cwd, outputFile);
150
+
151
+ if (!TEMPLATES[templateName]) {
152
+ throw new Error(`Unknown template: ${templateName}. Available: ${Object.keys(TEMPLATES).join(', ')}`);
153
+ }
154
+
155
+ const template = TEMPLATES[templateName];
156
+ const content = JSON.stringify(template, null, 2);
157
+
158
+ fs.writeFileSync(outputPath, content, 'utf-8');
159
+
160
+ return {
161
+ success: true,
162
+ template: templateName,
163
+ outputPath,
164
+ message: `Generated ${templateName} template: ${outputFile}`
165
+ };
166
+ }
167
+
168
+ /**
169
+ * List available templates
170
+ * @returns {array} Array of template names with descriptions
171
+ */
172
+ function listTemplates() {
173
+ return Object.entries(TEMPLATES).map(([name, config]) => ({
174
+ name,
175
+ description: config.description,
176
+ journeys: config.journeys.length
177
+ }));
178
+ }
179
+
180
+ module.exports = {
181
+ generateTemplate,
182
+ listTemplates,
183
+ TEMPLATES
184
+ };