@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,261 @@
1
+ /**
2
+ * Phase 5 — Behavioral Signal Detector
3
+ * Detects non-visual but UI-affecting changes that break user trust
4
+ */
5
+
6
+ /**
7
+ * Detect behavioral changes (missing elements, layout shifts, disabled CTAs)
8
+ */
9
+ class BehavioralSignalDetector {
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ }
13
+
14
+ /**
15
+ * Check if critical element is visible and accessible
16
+ */
17
+ async checkElementVisibility(page, selector) {
18
+ try {
19
+ const element = await page.$(selector);
20
+ if (!element) {
21
+ return {
22
+ visible: false,
23
+ accessible: false,
24
+ signal: 'ELEMENT_MISSING',
25
+ severity: 'CRITICAL',
26
+ description: `Critical element not found: ${selector}`
27
+ };
28
+ }
29
+
30
+ const boundingBox = await element.boundingBox();
31
+ if (!boundingBox) {
32
+ return {
33
+ visible: false,
34
+ accessible: false,
35
+ signal: 'OFFSCREEN_ELEMENT',
36
+ severity: 'CRITICAL',
37
+ description: `Element off-screen: ${selector}`
38
+ };
39
+ }
40
+
41
+ const isHidden = await element.isHidden();
42
+ if (isHidden) {
43
+ return {
44
+ visible: false,
45
+ accessible: false,
46
+ signal: 'HIDDEN_ELEMENT',
47
+ severity: 'CRITICAL',
48
+ description: `Element hidden: ${selector}`
49
+ };
50
+ }
51
+
52
+ const isDisabled = await element.isDisabled();
53
+ if (isDisabled) {
54
+ return {
55
+ visible: true,
56
+ accessible: false,
57
+ signal: 'DISABLED_ELEMENT',
58
+ severity: 'WARNING',
59
+ description: `Element disabled: ${selector}`
60
+ };
61
+ }
62
+
63
+ return {
64
+ visible: true,
65
+ accessible: true,
66
+ signal: null,
67
+ severity: 'INFO'
68
+ };
69
+ } catch (err) {
70
+ return {
71
+ visible: false,
72
+ accessible: false,
73
+ signal: 'CHECK_FAILED',
74
+ severity: 'INFO',
75
+ description: `Check failed: ${err.message}`
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Detect layout shifts - elements that moved unexpectedly
82
+ */
83
+ async detectLayoutShift(page, selectors) {
84
+ const shifts = [];
85
+
86
+ for (const selector of selectors) {
87
+ try {
88
+ const element = await page.$(selector);
89
+ if (!element) continue;
90
+
91
+ const boundingBox = await element.boundingBox();
92
+ if (!boundingBox) continue;
93
+
94
+ // Check if element is near viewport edges (shifted)
95
+ const viewportSize = page.viewportSize();
96
+ if (viewportSize) {
97
+ const { x, y, width, height } = boundingBox;
98
+
99
+ // Completely off-screen
100
+ if (x + width <= 0 || x >= viewportSize.width || y + height <= 0 || y >= viewportSize.height) {
101
+ shifts.push({
102
+ selector,
103
+ signal: 'OFFSCREEN_SHIFT',
104
+ severity: 'CRITICAL',
105
+ description: `Element shifted off-screen: ${selector}`,
106
+ position: { x, y, width, height }
107
+ });
108
+ }
109
+ // Partially off-screen
110
+ else if (x < 0 || y < 0 || x + width > viewportSize.width) {
111
+ shifts.push({
112
+ selector,
113
+ signal: 'PARTIAL_SHIFT',
114
+ severity: 'WARNING',
115
+ description: `Element partially shifted: ${selector}`,
116
+ position: { x, y, width, height }
117
+ });
118
+ }
119
+ }
120
+ } catch (err) {
121
+ // Silently skip errors
122
+ }
123
+ }
124
+
125
+ return shifts;
126
+ }
127
+
128
+ /**
129
+ * Check if critical CTA (button, link) is clickable
130
+ */
131
+ async checkCTAAccessibility(page, selector) {
132
+ try {
133
+ const element = await page.$(selector);
134
+ if (!element) {
135
+ return {
136
+ clickable: false,
137
+ signal: 'CTA_MISSING',
138
+ severity: 'CRITICAL',
139
+ description: `Call-to-action not found: ${selector}`
140
+ };
141
+ }
142
+
143
+ const isDisabled = await element.isDisabled();
144
+ if (isDisabled) {
145
+ return {
146
+ clickable: false,
147
+ signal: 'CTA_DISABLED',
148
+ severity: 'CRITICAL',
149
+ description: `CTA disabled unexpectedly: ${selector}`
150
+ };
151
+ }
152
+
153
+ const isHidden = await element.isHidden();
154
+ if (isHidden) {
155
+ return {
156
+ clickable: false,
157
+ signal: 'CTA_HIDDEN',
158
+ severity: 'CRITICAL',
159
+ description: `CTA hidden unexpectedly: ${selector}`
160
+ };
161
+ }
162
+
163
+ const isEnabled = await element.isEnabled();
164
+ if (!isEnabled) {
165
+ return {
166
+ clickable: false,
167
+ signal: 'CTA_UNAVAILABLE',
168
+ severity: 'CRITICAL',
169
+ description: `CTA unavailable: ${selector}`
170
+ };
171
+ }
172
+
173
+ return {
174
+ clickable: true,
175
+ signal: null,
176
+ severity: 'INFO'
177
+ };
178
+ } catch (err) {
179
+ return {
180
+ clickable: false,
181
+ signal: 'CHECK_FAILED',
182
+ severity: 'INFO',
183
+ description: `CTA check failed: ${err.message}`
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Detect color/styling changes on critical elements
190
+ */
191
+ async detectStyleChanges(page, selector, expectedStyles = {}) {
192
+ const changes = [];
193
+
194
+ try {
195
+ const element = await page.$(selector);
196
+ if (!element) return changes;
197
+
198
+ for (const [property, expectedValue] of Object.entries(expectedStyles)) {
199
+ const actualValue = await element.evaluate((el, prop) => {
200
+ return window.getComputedStyle(el).getPropertyValue(prop);
201
+ }, property);
202
+
203
+ if (actualValue !== expectedValue) {
204
+ changes.push({
205
+ selector,
206
+ property,
207
+ expectedValue,
208
+ actualValue,
209
+ signal: 'STYLE_CHANGE',
210
+ severity: 'WARNING',
211
+ description: `Style changed: ${property} from ${expectedValue} to ${actualValue}`
212
+ });
213
+ }
214
+ }
215
+ } catch (err) {
216
+ // Silently skip
217
+ }
218
+
219
+ return changes;
220
+ }
221
+
222
+ /**
223
+ * Comprehensive behavioral audit
224
+ */
225
+ async auditBehavior(page, config) {
226
+ const signals = [];
227
+
228
+ // Check critical elements
229
+ if (config.criticalElements) {
230
+ for (const selector of config.criticalElements) {
231
+ const check = await this.checkElementVisibility(page, selector);
232
+ if (check.signal) signals.push(check);
233
+ }
234
+ }
235
+
236
+ // Check CTAs
237
+ if (config.criticalCTAs) {
238
+ for (const selector of config.criticalCTAs) {
239
+ const check = await this.checkCTAAccessibility(page, selector);
240
+ if (check.signal) signals.push(check);
241
+ }
242
+ }
243
+
244
+ // Check layout shifts
245
+ if (config.monitoredElements) {
246
+ const shifts = await this.detectLayoutShift(page, config.monitoredElements);
247
+ signals.push(...shifts);
248
+ }
249
+
250
+ return {
251
+ hasSignals: signals.length > 0,
252
+ signals,
253
+ criticalCount: signals.filter(s => s.severity === 'CRITICAL').length,
254
+ warningCount: signals.filter(s => s.severity === 'WARNING').length
255
+ };
256
+ }
257
+ }
258
+
259
+ module.exports = {
260
+ BehavioralSignalDetector
261
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Phase 4 — Breakage Intelligence
3
+ * Aggregate failure taxonomy and hints into actionable summaries
4
+ */
5
+
6
+ const {
7
+ BREAK_TYPES,
8
+ IMPACT_DOMAINS,
9
+ SEVERITY_LEVELS,
10
+ getImpactDomain,
11
+ classifyBreakType,
12
+ determineSeverity
13
+ } = require('./failure-taxonomy');
14
+ const { deriveRootCauseHints } = require('./root-cause-analysis');
15
+
16
+ /**
17
+ * Analyze a single attempt/flow failure and produce intelligence
18
+ * @param {Object} item - Attempt or flow result with outcome, error, validators, visualDiff, behavioralSignals, etc.
19
+ * @param {boolean} isFlow - true if this is a flow
20
+ * @returns {Object} Intelligence object with taxonomy, hints, actions
21
+ */
22
+ function analyzeFailure(item, isFlow = false) {
23
+ if (!item || item.outcome === 'SUCCESS') {
24
+ return null;
25
+ }
26
+
27
+ const domain = getImpactDomain(isFlow ? item.flowId : item.attemptId);
28
+ const breakType = classifyBreakType(item);
29
+ const severity = determineSeverity(domain, breakType, isFlow);
30
+ const { hints, primaryHint } = deriveRootCauseHints(item, breakType);
31
+
32
+ // Phase 5: Include visual regression metadata
33
+ const intelligence = {
34
+ id: isFlow ? item.flowId : item.attemptId,
35
+ name: isFlow ? item.flowName : item.attemptName,
36
+ outcome: item.outcome,
37
+ breakType,
38
+ domain,
39
+ severity,
40
+ primaryHint,
41
+ hints,
42
+ whyItMatters: generateWhyItMatters(domain, severity, breakType),
43
+ topActions: generateTopActions(breakType, domain)
44
+ };
45
+
46
+ // Phase 5: Add visual regression details if available
47
+ if (item.visualDiff) {
48
+ intelligence.visualDiff = {
49
+ hasDiff: item.visualDiff.hasDiff,
50
+ percentChange: item.visualDiff.percentChange,
51
+ reason: item.visualDiff.reason,
52
+ diffRegions: item.visualDiff.diffRegions
53
+ };
54
+ }
55
+
56
+ // Phase 5: Add behavioral signals if available
57
+ if (item.behavioralSignals) {
58
+ intelligence.behavioralSignals = item.behavioralSignals;
59
+ }
60
+
61
+ return intelligence;
62
+ }
63
+
64
+ /**
65
+ * Generate 1–3 bullet "Why It Matters" summary
66
+ * @param {string} domain - IMPACT_DOMAIN
67
+ * @param {string} severity - SEVERITY_LEVEL
68
+ * @returns {string[]} Array of 1–3 bullets
69
+ */
70
+ function generateWhyItMatters(domain, severity, breakType = null) {
71
+ const bullets = [];
72
+
73
+ // Domain-specific impact
74
+ if (domain === IMPACT_DOMAINS.REVENUE) {
75
+ bullets.push('🚨 Revenue impact: Checkout/payment flow is broken. Customers cannot complete purchases.');
76
+ } else if (domain === IMPACT_DOMAINS.LEAD) {
77
+ bullets.push('📉 Lead gen impact: Signup/contact flow is broken. Cannot capture customer interest.');
78
+ } else if (domain === IMPACT_DOMAINS.TRUST) {
79
+ bullets.push('⚠️ Trust impact: Auth/account flow is broken. Users cannot access their data.');
80
+ } else {
81
+ bullets.push('📊 UX impact: Core interaction is broken. User journey degraded.');
82
+ }
83
+
84
+ // Severity escalation
85
+ if (severity === SEVERITY_LEVELS.CRITICAL) {
86
+ bullets.push('🔴 CRITICAL: Escalate immediately. Page/API down or core feature broken.');
87
+ } else if (severity === SEVERITY_LEVELS.WARNING) {
88
+ bullets.push('🟡 WARNING: High priority. Fix before peak traffic to avoid customer impact.');
89
+ }
90
+
91
+ // Phase 5: Visual regression context
92
+ if (breakType === BREAK_TYPES.VISUAL) {
93
+ bullets.push('👁️ Visual regression: UI elements changed from baseline (CSS, layout, or styling).');
94
+ if (severity === SEVERITY_LEVELS.CRITICAL) {
95
+ bullets.push('Critical visual change may completely obscure content or block user interaction.');
96
+ } else if (severity === SEVERITY_LEVELS.WARNING) {
97
+ bullets.push('Visual change may degrade readability, accessibility, or user experience.');
98
+ }
99
+ }
100
+
101
+ return bullets;
102
+ }
103
+
104
+ /**
105
+ * Generate top 3 actionable next steps
106
+ * @param {string} breakType - BREAK_TYPE
107
+ * @param {string} domain - IMPACT_DOMAIN
108
+ * @returns {string[]} Array of 3 action strings
109
+ */
110
+ function generateTopActions(breakType, domain) {
111
+ const actions = [];
112
+
113
+ // Break-type-specific actions
114
+ if (breakType === BREAK_TYPES.NAVIGATION || breakType === BREAK_TYPES.NETWORK) {
115
+ actions.push('1. Check server status and network logs for errors');
116
+ actions.push('2. Verify DNS and SSL certificate validity');
117
+ actions.push('3. Check CDN/load balancer for 5xx errors');
118
+ } else if (breakType === BREAK_TYPES.TIMEOUT) {
119
+ actions.push('1. Check server response times and database queries');
120
+ actions.push('2. Review recent deployments for performance regressions');
121
+ actions.push('3. Check if rate limiting is too strict');
122
+ } else if (breakType === BREAK_TYPES.VISUAL) {
123
+ // Phase 5: Visual-specific diagnostic actions
124
+ actions.push('1. Compare baseline screenshot to current; identify CSS/layout changes');
125
+ actions.push('2. Check recent CSS commits, theme changes, or Tailwind/Bootstrap updates');
126
+ actions.push('3. Validate element positioning using browser DevTools layout analysis');
127
+ } else if (breakType === BREAK_TYPES.VALIDATION) {
128
+ actions.push('1. Review frontend code for recent CSS/JS changes');
129
+ actions.push('2. Check browser console for JavaScript errors');
130
+ actions.push('3. Verify DOM selectors match current HTML structure');
131
+ } else if (breakType === BREAK_TYPES.SUBMISSION) {
132
+ actions.push('1. Check form validation rules and error messages');
133
+ actions.push('2. Verify backend API endpoint is reachable');
134
+ actions.push('3. Check for CORS or authentication failures');
135
+ } else if (breakType === BREAK_TYPES.CONSOLE) {
136
+ actions.push('1. Review browser console error logs');
137
+ actions.push('2. Check for third-party script failures');
138
+ actions.push('3. Verify API endpoints and authentication tokens');
139
+ } else {
140
+ actions.push('1. Check application logs for errors');
141
+ actions.push('2. Verify all dependencies are deployed');
142
+ actions.push('3. Check recent changes that might affect this flow');
143
+ }
144
+
145
+ return actions;
146
+ }
147
+
148
+ /**
149
+ * Aggregate all failures into intelligence summary
150
+ * @param {Array} attempts - Attempt results
151
+ * @param {Array} flows - Flow results
152
+ * @returns {Object} Summary with failures, by-domain counts, escalation signals
153
+ */
154
+ function aggregateIntelligence(attempts = [], flows = []) {
155
+ const allFailures = [];
156
+ const byDomain = {
157
+ [IMPACT_DOMAINS.REVENUE]: [],
158
+ [IMPACT_DOMAINS.LEAD]: [],
159
+ [IMPACT_DOMAINS.TRUST]: [],
160
+ [IMPACT_DOMAINS.UX]: []
161
+ };
162
+ const bySeverity = {
163
+ [SEVERITY_LEVELS.CRITICAL]: [],
164
+ [SEVERITY_LEVELS.WARNING]: [],
165
+ [SEVERITY_LEVELS.INFO]: []
166
+ };
167
+
168
+ // Analyze attempts
169
+ for (const attempt of attempts) {
170
+ const intel = analyzeFailure(attempt, false);
171
+ if (intel) {
172
+ allFailures.push(intel);
173
+ byDomain[intel.domain].push(intel);
174
+ bySeverity[intel.severity].push(intel);
175
+ }
176
+ }
177
+
178
+ // Analyze flows (higher weight)
179
+ for (const flow of flows) {
180
+ const intel = analyzeFailure(flow, true);
181
+ if (intel) {
182
+ allFailures.push(intel);
183
+ byDomain[intel.domain].push(intel);
184
+ bySeverity[intel.severity].push(intel);
185
+ }
186
+ }
187
+
188
+ // Escalation signals
189
+ const escalationSignals = [];
190
+ if (bySeverity[SEVERITY_LEVELS.CRITICAL].length > 0) {
191
+ escalationSignals.push('CRITICAL failures detected - immediate action required');
192
+ }
193
+ if (byDomain[IMPACT_DOMAINS.REVENUE].length > 0) {
194
+ escalationSignals.push('REVENUE domain affected - financial impact likely');
195
+ }
196
+ if (
197
+ flows.filter(f => f.outcome === 'FAILURE').length > 0 ||
198
+ allFailures.filter(f => f.breakType === BREAK_TYPES.NETWORK || f.breakType === BREAK_TYPES.TIMEOUT).length > 0
199
+ ) {
200
+ escalationSignals.push('Infrastructure/availability issue indicated');
201
+ }
202
+
203
+ return {
204
+ totalFailures: allFailures.length,
205
+ failures: allFailures,
206
+ byDomain,
207
+ bySeverity,
208
+ escalationSignals,
209
+ criticalCount: bySeverity[SEVERITY_LEVELS.CRITICAL].length,
210
+ warningCount: bySeverity[SEVERITY_LEVELS.WARNING].length,
211
+ infoCount: bySeverity[SEVERITY_LEVELS.INFO].length
212
+ };
213
+ }
214
+
215
+ module.exports = {
216
+ analyzeFailure,
217
+ aggregateIntelligence,
218
+ generateWhyItMatters,
219
+ generateTopActions,
220
+ BREAK_TYPES,
221
+ IMPACT_DOMAINS,
222
+ SEVERITY_LEVELS
223
+ };
@@ -0,0 +1,92 @@
1
+ const { chromium } = require('playwright');
2
+
3
+ class GuardianBrowser {
4
+ constructor() {
5
+ this.browser = null;
6
+ this.context = null;
7
+ this.page = null;
8
+ }
9
+
10
+ async launch(timeout = 20000, options = {}) {
11
+ try {
12
+ const launchOptions = {
13
+ headless: options.headless !== undefined ? options.headless : true,
14
+ args: options.args || []
15
+ };
16
+
17
+ this.browser = await chromium.launch(launchOptions);
18
+
19
+ const contextOptions = {
20
+ ...options,
21
+ };
22
+
23
+ // Enable HAR recording if requested
24
+ if (options.recordHar) {
25
+ // Note: HAR path must be provided in the actual implementation
26
+ // For now, we'll prepare the context for HAR recording
27
+ contextOptions.recordHar = options.harPath ? { path: options.harPath } : undefined;
28
+ }
29
+
30
+ this.context = await this.browser.newContext(contextOptions);
31
+ this.page = await this.context.newPage();
32
+ this.page.setDefaultTimeout(timeout);
33
+ return true;
34
+ } catch (err) {
35
+ throw new Error(`Failed to launch browser: ${err.message}`);
36
+ }
37
+ }
38
+
39
+ async navigate(url, timeout = 20000) {
40
+ try {
41
+ const response = await this.page.goto(url, {
42
+ waitUntil: 'networkidle',
43
+ timeout
44
+ });
45
+
46
+ return {
47
+ success: true,
48
+ status: response?.status() || 200,
49
+ url: this.page.url()
50
+ };
51
+ } catch (err) {
52
+ return {
53
+ success: false,
54
+ status: null,
55
+ error: err.message
56
+ };
57
+ }
58
+ }
59
+
60
+ async getLinks() {
61
+ try {
62
+ const links = await this.page.locator('a[href]').evaluateAll(elements =>
63
+ elements.map(el => ({
64
+ href: el.href,
65
+ text: el.innerText?.trim() || ''
66
+ }))
67
+ );
68
+ return links;
69
+ } catch (err) {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ async takeScreenshot(filePath) {
75
+ try {
76
+ await this.page.screenshot({ path: filePath });
77
+ return true;
78
+ } catch (err) {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async close() {
84
+ try {
85
+ if (this.browser) await this.browser.close();
86
+ } catch (err) {
87
+ // Ignore close errors
88
+ }
89
+ }
90
+ }
91
+
92
+ module.exports = { GuardianBrowser };