@odavl/guardian 0.1.0-rc1 → 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 (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -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 +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Wave 1.3 — Outcome-Based Success Evaluator
3
+ * Deterministic success evaluation for form submissions and key flows.
4
+ * No reliance on specific confirmation text; uses signals: Network, Navigation, DOM, Errors.
5
+ */
6
+
7
+ const URL_SAFE_STATUSES = new Set([200, 201, 202, 204, 302, 303]);
8
+
9
+ /**
10
+ * Capture pre-submit state around a submit element's owning form.
11
+ * @param {import('playwright').Page} page
12
+ * @param {import('playwright').ElementHandle|null} submitHandle
13
+ */
14
+ async function captureBeforeState(page, submitHandle) {
15
+ const url = page.url();
16
+ const state = await page.evaluate((submitEl) => {
17
+ const result = {
18
+ formSelector: null,
19
+ formExists: false,
20
+ inputsFilledCount: 0,
21
+ inputsTotal: 0,
22
+ ariaInvalidCount: 0,
23
+ hasAlertRegion: false,
24
+ alertTextLength: 0,
25
+ liveRegionTextLength: 0,
26
+ };
27
+
28
+ let form = null;
29
+ if (submitEl) {
30
+ form = submitEl.closest('form');
31
+ } else {
32
+ form = document.querySelector('form');
33
+ }
34
+
35
+ if (form) {
36
+ result.formSelector = form.getAttribute('id') ? `#${form.id}` : null;
37
+ result.formExists = true;
38
+ const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
39
+ result.inputsTotal = inputs.length;
40
+ result.inputsFilledCount = inputs.filter((el) => {
41
+ const val = (el.value || '').trim();
42
+ return val.length > 0;
43
+ }).length;
44
+ result.ariaInvalidCount = inputs.filter((el) => el.getAttribute('aria-invalid') === 'true').length;
45
+ }
46
+
47
+ // role=alert or [aria-live]
48
+ const alertEl = document.querySelector('[role="alert"], .alert, .error, .invalid');
49
+ if (alertEl) {
50
+ result.hasAlertRegion = true;
51
+ result.alertTextLength = (alertEl.textContent || '').trim().length;
52
+ }
53
+ const liveEl = document.querySelector('[aria-live]');
54
+ if (liveEl) {
55
+ result.liveRegionTextLength = (liveEl.textContent || '').trim().length;
56
+ }
57
+ return result;
58
+ }, submitHandle);
59
+
60
+ return { url, state };
61
+ }
62
+
63
+ /**
64
+ * Capture post-submit state.
65
+ * @param {import('playwright').Page} page
66
+ * @param {string|null} originalFormSelector
67
+ */
68
+ async function captureAfterState(page, originalFormSelector) {
69
+ const url = page.url();
70
+ const state = await page.evaluate((formSelector) => {
71
+ const result = {
72
+ formExists: false,
73
+ formDisabled: false,
74
+ inputsFilledCount: 0,
75
+ inputsTotal: 0,
76
+ ariaInvalidCount: 0,
77
+ hasAlertRegion: false,
78
+ alertTextLength: 0,
79
+ liveRegionTextLength: 0,
80
+ };
81
+
82
+ let form = null;
83
+ if (formSelector) {
84
+ form = document.querySelector(formSelector);
85
+ }
86
+ if (!form) {
87
+ form = document.querySelector('form');
88
+ }
89
+
90
+ if (form) {
91
+ result.formExists = true;
92
+ result.formDisabled = !!form.getAttribute('disabled') || (form.classList.contains('disabled'));
93
+ const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
94
+ result.inputsTotal = inputs.length;
95
+ result.inputsFilledCount = inputs.filter((el) => (el.value || '').trim().length > 0).length;
96
+ result.ariaInvalidCount = inputs.filter((el) => el.getAttribute('aria-invalid') === 'true').length;
97
+ }
98
+
99
+ const alertEl = document.querySelector('[role="alert"], .alert, .error, .invalid');
100
+ if (alertEl) {
101
+ result.hasAlertRegion = true;
102
+ result.alertTextLength = (alertEl.textContent || '').trim().length;
103
+ }
104
+ const liveEl = document.querySelector('[aria-live]');
105
+ if (liveEl) {
106
+ result.liveRegionTextLength = (liveEl.textContent || '').trim().length;
107
+ }
108
+ return result;
109
+ }, originalFormSelector);
110
+
111
+ return { url, state };
112
+ }
113
+
114
+ /**
115
+ * Evaluate success based on signals.
116
+ * @param {{url:string,state:Object}} before
117
+ * @param {{url:string,state:Object}} after
118
+ * @param {{requests:Array,responses:Array,consoleErrors:Array,navChanged:boolean,baseOrigin:string}} events
119
+ * @returns {{status:'success'|'friction'|'failure',confidence:'high'|'medium'|'low',reasons:string[],evidence:Object}}
120
+ */
121
+ function evaluateSuccess(before, after, events) {
122
+ const reasons = [];
123
+ const evidence = {
124
+ network: [],
125
+ urlChanged: before.url !== after.url,
126
+ formCleared: false,
127
+ formDisappeared: false,
128
+ formDisabled: false,
129
+ alertRegionDelta: 0,
130
+ liveRegionDelta: 0,
131
+ ariaInvalidDelta: 0,
132
+ consoleErrors: events.consoleErrors || [],
133
+ };
134
+
135
+ // A) Network success
136
+ const baseOrigin = events.baseOrigin;
137
+ let strongNetworkPositive = false;
138
+ for (const r of (events.responses || [])) {
139
+ const req = typeof r.request === 'function' ? r.request() : r.request;
140
+ if (!req || typeof req.url !== 'function') continue;
141
+ const url = req.url();
142
+ const originOk = safeSameOrAllowedOrigin(url, baseOrigin);
143
+ const method = (typeof req.method === 'function' ? req.method() : '').toUpperCase();
144
+ const status = typeof r.status === 'function' ? r.status() : r.status;
145
+ const statusOk = URL_SAFE_STATUSES.has(status);
146
+ evidence.network.push({ method, url, status, originOk, statusOk });
147
+ if (originOk && (method === 'POST' || method === 'PUT') && statusOk) {
148
+ strongNetworkPositive = true;
149
+ }
150
+ }
151
+
152
+ if (strongNetworkPositive) {
153
+ reasons.push('Network submit succeeded (safe status and origin)');
154
+ }
155
+
156
+ // B) Navigation
157
+ const navPositive = !!events.navChanged;
158
+ if (navPositive) reasons.push('URL changed after submit');
159
+
160
+ // C) DOM outcome
161
+ const beforeState = before.state;
162
+ const afterState = after.state;
163
+ evidence.formDisappeared = !!beforeState.formExists && !afterState.formExists;
164
+ evidence.formDisabled = !!afterState.formDisabled;
165
+ evidence.formCleared = beforeState.inputsFilledCount > 0 && afterState.inputsFilledCount < beforeState.inputsFilledCount;
166
+ evidence.alertRegionDelta = (afterState.alertTextLength || 0) - (beforeState.alertTextLength || 0);
167
+ evidence.liveRegionDelta = (afterState.liveRegionTextLength || 0) - (beforeState.liveRegionTextLength || 0);
168
+ evidence.ariaInvalidDelta = (afterState.ariaInvalidCount || 0) - (beforeState.ariaInvalidCount || 0);
169
+
170
+ const domPositive = evidence.formDisappeared || evidence.formDisabled || evidence.formCleared || evidence.liveRegionDelta > 0;
171
+ if (domPositive) reasons.push('Form outcome indicates completion (cleared/disabled/disappeared or live region updated)');
172
+
173
+ // D) Error outcome
174
+ const strongNegative = evidence.ariaInvalidDelta > 0 || (afterState.hasAlertRegion && evidence.alertRegionDelta > 0);
175
+ if (strongNegative) reasons.push('Error markers increased after submit');
176
+
177
+ // E) Console errors
178
+ const consoleNegative = (events.consoleErrors || []).length > 0;
179
+ if (consoleNegative) reasons.push('Console errors after submit');
180
+
181
+ // Decision table
182
+ let status = 'failure';
183
+ let confidence = 'low';
184
+ if (strongNetworkPositive && (navPositive || domPositive) && !strongNegative && !consoleNegative) {
185
+ status = 'success';
186
+ confidence = 'high';
187
+ } else if ((strongNetworkPositive || navPositive || domPositive) && (strongNegative || consoleNegative)) {
188
+ status = 'friction';
189
+ confidence = strongNetworkPositive ? 'medium' : 'low';
190
+ } else if (strongNetworkPositive || navPositive || domPositive) {
191
+ status = 'success';
192
+ confidence = 'medium';
193
+ } else {
194
+ status = 'failure';
195
+ confidence = strongNegative ? 'medium' : 'low';
196
+ }
197
+
198
+ return { status, confidence, reasons, evidence };
199
+ }
200
+
201
+ function safeSameOrAllowedOrigin(url, baseOrigin) {
202
+ try {
203
+ const u = new URL(url);
204
+ return u.origin === baseOrigin;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ module.exports = {
211
+ captureBeforeState,
212
+ captureAfterState,
213
+ evaluateSuccess,
214
+ };
@@ -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
+ };