@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,374 @@
1
+ /**
2
+ * Guardian Flow Execution Module
3
+ * Executes predefined user interaction flows (click, type, submit, etc.)
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class GuardianFlowExecutor {
10
+ constructor(options = {}) {
11
+ this.timeout = options.timeout || 30000; // 30 seconds per step
12
+ this.screenshotOnStep = options.screenshotOnStep !== false; // Screenshot each step by default
13
+ this.safety = options.safety || null; // Safety guard instance (optional)
14
+ }
15
+
16
+ /**
17
+ * Load flow definition from JSON file
18
+ * @param {string} flowPath - Path to flow JSON file
19
+ * @returns {object|null} Flow definition
20
+ */
21
+ loadFlow(flowPath) {
22
+ try {
23
+ const content = fs.readFileSync(flowPath, 'utf8');
24
+ return JSON.parse(content);
25
+ } catch (error) {
26
+ console.error(`❌ Failed to load flow: ${error.message}`);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Execute a single flow step
33
+ * @param {Page} page - Playwright page
34
+ * @param {object} step - Step definition
35
+ * @param {number} stepIndex - Step index for logging
36
+ * @returns {Promise<object>} { success: boolean, error: string|null }
37
+ */
38
+ async executeStep(page, step, stepIndex) {
39
+ try {
40
+ console.log(` Step ${stepIndex + 1}: ${step.type} ${step.target || ''}`);
41
+
42
+ // Safety check for destructive actions
43
+ if (this.safety) {
44
+ const safetyCheck = this.checkStepSafety(step);
45
+ if (!safetyCheck.safe) {
46
+ return {
47
+ success: false,
48
+ error: `Safety guard blocked step: ${safetyCheck.reason}`,
49
+ };
50
+ }
51
+ }
52
+
53
+ switch (step.type) {
54
+ case 'navigate':
55
+ return await this.stepNavigate(page, step);
56
+
57
+ case 'click':
58
+ return await this.stepClick(page, step);
59
+
60
+ case 'type':
61
+ return await this.stepType(page, step);
62
+
63
+ case 'submit':
64
+ return await this.stepSubmit(page, step);
65
+
66
+ case 'waitFor':
67
+ return await this.stepWaitFor(page, step);
68
+
69
+ case 'wait':
70
+ return await this.stepWait(page, step);
71
+
72
+ default:
73
+ return {
74
+ success: false,
75
+ error: `Unknown step type: ${step.type}`,
76
+ };
77
+ }
78
+ } catch (error) {
79
+ return {
80
+ success: false,
81
+ error: error.message,
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Navigate to URL
88
+ * @param {Page} page - Playwright page
89
+ * @param {object} step - Step definition
90
+ * @returns {Promise<object>} Result
91
+ */
92
+ async stepNavigate(page, step) {
93
+ try {
94
+ await page.goto(step.target, {
95
+ timeout: this.timeout,
96
+ waitUntil: 'domcontentloaded',
97
+ });
98
+ return { success: true, error: null };
99
+ } catch (error) {
100
+ return { success: false, error: error.message };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Click element
106
+ * @param {Page} page - Playwright page
107
+ * @param {object} step - Step definition
108
+ * @returns {Promise<object>} Result
109
+ */
110
+ async stepClick(page, step) {
111
+ try {
112
+ await page.click(step.target, { timeout: this.timeout });
113
+
114
+ // Wait for navigation if expected
115
+ if (step.waitForNavigation) {
116
+ await page.waitForLoadState('domcontentloaded');
117
+ }
118
+
119
+ return { success: true, error: null };
120
+ } catch (error) {
121
+ return { success: false, error: error.message };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Type into input field
127
+ * @param {Page} page - Playwright page
128
+ * @param {object} step - Step definition
129
+ * @returns {Promise<object>} Result
130
+ */
131
+ async stepType(page, step) {
132
+ try {
133
+ // Clear field first if specified
134
+ if (step.clear !== false) {
135
+ await page.fill(step.target, '');
136
+ }
137
+
138
+ await page.type(step.target, step.value, {
139
+ timeout: this.timeout,
140
+ delay: step.delay || 50, // Simulate human typing
141
+ });
142
+
143
+ return { success: true, error: null };
144
+ } catch (error) {
145
+ return { success: false, error: error.message };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Submit form
151
+ * @param {Page} page - Playwright page
152
+ * @param {object} step - Step definition
153
+ * @returns {Promise<object>} Result
154
+ */
155
+ async stepSubmit(page, step) {
156
+ try {
157
+ // Find submit button within form or use provided selector
158
+ if (step.target) {
159
+ await page.click(step.target, { timeout: this.timeout });
160
+ } else {
161
+ // Find first submit button
162
+ await page.click('button[type="submit"]', { timeout: this.timeout });
163
+ }
164
+
165
+ // Wait for navigation
166
+ await page.waitForLoadState('domcontentloaded');
167
+
168
+ return { success: true, error: null };
169
+ } catch (error) {
170
+ return { success: false, error: error.message };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Wait for element to appear
176
+ * @param {Page} page - Playwright page
177
+ * @param {object} step - Step definition
178
+ * @returns {Promise<object>} Result
179
+ */
180
+ async stepWaitFor(page, step) {
181
+ try {
182
+ await page.waitForSelector(step.target, {
183
+ timeout: step.timeout || this.timeout,
184
+ state: step.state || 'visible',
185
+ });
186
+
187
+ return { success: true, error: null };
188
+ } catch (error) {
189
+ return { success: false, error: error.message };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Wait for specified time
195
+ * @param {Page} page - Playwright page
196
+ * @param {object} step - Step definition
197
+ * @returns {Promise<object>} Result
198
+ */
199
+ async stepWait(page, step) {
200
+ try {
201
+ const duration = step.duration || 1000;
202
+ await page.waitForTimeout(duration);
203
+ return { success: true, error: null };
204
+ } catch (error) {
205
+ return { success: false, error: error.message };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Check if step is safe to execute
211
+ * @param {object} step - Step definition
212
+ * @returns {object} { safe: boolean, reason: string|null }
213
+ */
214
+ checkStepSafety(step) {
215
+ if (!this.safety) {
216
+ return { safe: true, reason: null };
217
+ }
218
+
219
+ // Check URL safety for navigate steps
220
+ if (step.type === 'navigate') {
221
+ return this.safety.isUrlSafe(step.target);
222
+ }
223
+
224
+ // Check selector safety for click/type steps
225
+ if (step.type === 'click' || step.type === 'type') {
226
+ return this.safety.isSelectorSafe(step.target);
227
+ }
228
+
229
+ // Check form submission safety
230
+ if (step.type === 'submit') {
231
+ return this.safety.isFormSubmitSafe(step.target);
232
+ }
233
+
234
+ return { safe: true, reason: null };
235
+ }
236
+
237
+ /**
238
+ * Execute complete flow
239
+ * @param {Page} page - Playwright page
240
+ * @param {object} flow - Flow definition
241
+ * @param {string} artifactsDir - Directory for screenshots
242
+ * @returns {Promise<object>} Flow result
243
+ */
244
+ async executeFlow(page, flow, artifactsDir, baseUrl = null) {
245
+ const result = {
246
+ flowId: flow.id,
247
+ flowName: flow.name,
248
+ success: false,
249
+ stepsExecuted: 0,
250
+ stepsTotal: flow.steps.length,
251
+ failedStep: null,
252
+ error: null,
253
+ screenshots: [],
254
+ durationMs: 0
255
+ };
256
+
257
+ // Normalize steps with optional baseUrl substitution
258
+ const steps = (flow.steps || []).map((step) => {
259
+ if (baseUrl && typeof step.target === 'string' && step.target.includes('$BASEURL')) {
260
+ return { ...step, target: step.target.replace('$BASEURL', baseUrl) };
261
+ }
262
+ return step;
263
+ });
264
+
265
+ // Ensure artifact subfolder exists for screenshots
266
+ if (artifactsDir) {
267
+ const pagesDir = path.join(artifactsDir, 'pages');
268
+ fs.mkdirSync(pagesDir, { recursive: true });
269
+ }
270
+
271
+ let startedAt = Date.now();
272
+ try {
273
+ console.log(`\n🎬 Executing flow: ${flow.name}`);
274
+ console.log(`📋 Steps: ${steps.length}`);
275
+
276
+ for (let i = 0; i < steps.length; i++) {
277
+ const step = steps[i];
278
+
279
+ // Execute step
280
+ const stepResult = await this.executeStep(page, step, i);
281
+
282
+ // Capture screenshot after step
283
+ if (this.screenshotOnStep && artifactsDir) {
284
+ const screenshotPath = path.join(
285
+ artifactsDir,
286
+ 'pages',
287
+ `flow-step-${i + 1}.jpeg`
288
+ );
289
+
290
+ try {
291
+ await page.screenshot({ path: screenshotPath, type: 'jpeg', quality: 80 });
292
+ result.screenshots.push(`flow-step-${i + 1}.jpeg`);
293
+ } catch (error) {
294
+ console.error(`⚠️ Failed to capture screenshot: ${error.message}`);
295
+ }
296
+ }
297
+
298
+ // Check if step failed
299
+ if (!stepResult.success) {
300
+ result.failedStep = i + 1;
301
+ result.error = stepResult.error;
302
+ console.log(` ❌ Step failed: ${stepResult.error}`);
303
+ return result;
304
+ }
305
+
306
+ result.stepsExecuted++;
307
+ console.log(` ✅ Step ${i + 1} completed`);
308
+ }
309
+
310
+ result.success = true;
311
+ result.durationMs = Date.now() - startedAt;
312
+ console.log(`✅ Flow completed successfully`);
313
+
314
+ return result;
315
+ } catch (error) {
316
+ if (!result.durationMs) {
317
+ result.durationMs = Date.now() - (startedAt || Date.now());
318
+ }
319
+ result.error = error.message;
320
+ console.error(`❌ Flow execution failed: ${error.message}`);
321
+ return result;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Validate flow definition
327
+ * @param {object} flow - Flow definition
328
+ * @returns {object} { valid: boolean, errors: string[] }
329
+ */
330
+ validateFlow(flow) {
331
+ const errors = [];
332
+
333
+ if (!flow.id) {
334
+ errors.push('Flow missing required field: id');
335
+ }
336
+
337
+ if (!flow.name) {
338
+ errors.push('Flow missing required field: name');
339
+ }
340
+
341
+ if (!flow.steps || !Array.isArray(flow.steps)) {
342
+ errors.push('Flow missing required field: steps (must be array)');
343
+ } else if (flow.steps.length === 0) {
344
+ errors.push('Flow has no steps');
345
+ } else {
346
+ // Validate each step
347
+ flow.steps.forEach((step, index) => {
348
+ if (!step.type) {
349
+ errors.push(`Step ${index + 1}: missing type`);
350
+ }
351
+
352
+ const validTypes = ['navigate', 'click', 'type', 'submit', 'waitFor', 'wait'];
353
+ if (step.type && !validTypes.includes(step.type)) {
354
+ errors.push(`Step ${index + 1}: invalid type "${step.type}"`);
355
+ }
356
+
357
+ if ((step.type === 'navigate' || step.type === 'click' || step.type === 'type' || step.type === 'waitFor') && !step.target) {
358
+ errors.push(`Step ${index + 1}: missing target`);
359
+ }
360
+
361
+ if (step.type === 'type' && !step.value) {
362
+ errors.push(`Step ${index + 1}: missing value for type step`);
363
+ }
364
+ });
365
+ }
366
+
367
+ return {
368
+ valid: errors.length === 0,
369
+ errors,
370
+ };
371
+ }
372
+ }
373
+
374
+ module.exports = { GuardianFlowExecutor };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Guardian Flow Registry (Phase 3)
3
+ * Curated intent flows executed via Flow Executor
4
+ */
5
+
6
+ const DEFAULT_FLOWS = ['signup_flow', 'login_flow', 'checkout_flow'];
7
+
8
+ const flowDefinitions = {
9
+ signup_flow: {
10
+ id: 'signup_flow',
11
+ name: 'Intent: Account Signup',
12
+ description: 'User creates an account successfully',
13
+ riskCategory: 'LEAD',
14
+ steps: [
15
+ { id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
16
+ { id: 'open_signup', type: 'click', target: 'a[data-guardian="account-signup-link"], a:has-text("Account Signup")', description: 'Open account signup page', waitForNavigation: true },
17
+ { id: 'fill_email', type: 'type', target: '[data-guardian="signup-email"]', value: 'newuser@example.com', description: 'Enter signup email' },
18
+ { id: 'fill_password', type: 'type', target: '[data-guardian="signup-password"]', value: 'P@ssword123', description: 'Enter signup password' },
19
+ { id: 'submit', type: 'click', target: '[data-guardian="signup-account-submit"], button:has-text("Sign up")', description: 'Submit signup form' },
20
+ { id: 'wait_success', type: 'waitFor', target: '[data-guardian="signup-account-success"]', description: 'Wait for signup success', timeout: 7000 }
21
+ ]
22
+ },
23
+ login_flow: {
24
+ id: 'login_flow',
25
+ name: 'Intent: Account Login',
26
+ description: 'User logs in successfully',
27
+ riskCategory: 'TRUST/UX',
28
+ steps: [
29
+ { id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
30
+ { id: 'open_login', type: 'click', target: 'a[data-guardian="account-login-link"], a:has-text("Login"), a[href*="login"]', description: 'Open login page', waitForNavigation: true },
31
+ { id: 'fill_email', type: 'type', target: '[data-guardian="login-email"]', value: 'user@example.com', description: 'Enter login email' },
32
+ { id: 'fill_password', type: 'type', target: '[data-guardian="login-password"]', value: 'password123', description: 'Enter login password' },
33
+ { id: 'submit', type: 'click', target: '[data-guardian="login-submit"], button:has-text("Login")', description: 'Submit login form' },
34
+ { id: 'wait_success', type: 'waitFor', target: '[data-guardian="login-success"]', description: 'Wait for login success', timeout: 7000 }
35
+ ]
36
+ },
37
+ checkout_flow: {
38
+ id: 'checkout_flow',
39
+ name: 'Intent: Checkout',
40
+ description: 'User reviews cart and places order (no payment)',
41
+ riskCategory: 'REVENUE',
42
+ steps: [
43
+ { id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
44
+ { id: 'open_checkout', type: 'click', target: 'a[data-guardian="checkout-link"], a:has-text("Checkout"), a[href*="checkout"]', description: 'Open checkout page', waitForNavigation: true },
45
+ { id: 'place_order', type: 'click', target: '[data-guardian="checkout-place-order"], button:has-text("Place order"), button:has-text("Place Order")', description: 'Place order' },
46
+ { id: 'wait_success', type: 'waitFor', target: '[data-guardian="checkout-success"]', description: 'Wait for order confirmation', timeout: 7000 }
47
+ ]
48
+ }
49
+ };
50
+
51
+ function getFlowDefinition(flowId) {
52
+ return flowDefinitions[flowId] || null;
53
+ }
54
+
55
+ function getDefaultFlowIds() {
56
+ return DEFAULT_FLOWS.slice();
57
+ }
58
+
59
+ function listFlowDefinitions() {
60
+ return Object.values(flowDefinitions);
61
+ }
62
+
63
+ module.exports = {
64
+ getFlowDefinition,
65
+ getDefaultFlowIds,
66
+ listFlowDefinitions
67
+ };