guardrail-ship 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 (119) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/mock-implementation.d.ts +1 -0
  6. package/dist/mock-implementation.d.ts.map +1 -0
  7. package/dist/mock-implementation.js +2 -0
  8. package/dist/mock-implementation.js.map +1 -0
  9. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
  10. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
  11. package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
  12. package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
  13. package/dist/mockproof/import-graph-scanner.d.ts +93 -0
  14. package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
  15. package/dist/mockproof/import-graph-scanner.js +411 -0
  16. package/dist/mockproof/import-graph-scanner.js.map +1 -0
  17. package/dist/mockproof/index.d.ts +10 -0
  18. package/dist/mockproof/index.d.ts.map +1 -0
  19. package/dist/mockproof/index.js +10 -0
  20. package/dist/mockproof/index.js.map +1 -0
  21. package/dist/reality-mode/auth-enforcer.d.ts +13 -0
  22. package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
  23. package/dist/reality-mode/auth-enforcer.js +90 -0
  24. package/dist/reality-mode/auth-enforcer.js.map +1 -0
  25. package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
  26. package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
  27. package/dist/reality-mode/explorer/critical-flows.js +463 -0
  28. package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
  29. package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
  30. package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
  31. package/dist/reality-mode/explorer/flow-parser.js +250 -0
  32. package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
  33. package/dist/reality-mode/explorer/index.d.ts +11 -0
  34. package/dist/reality-mode/explorer/index.d.ts.map +1 -0
  35. package/dist/reality-mode/explorer/index.js +11 -0
  36. package/dist/reality-mode/explorer/index.js.map +1 -0
  37. package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
  38. package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
  39. package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
  40. package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
  41. package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
  42. package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
  43. package/dist/reality-mode/explorer/surface-discovery.js +357 -0
  44. package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
  45. package/dist/reality-mode/explorer/types.d.ts +275 -0
  46. package/dist/reality-mode/explorer/types.d.ts.map +1 -0
  47. package/dist/reality-mode/explorer/types.js +8 -0
  48. package/dist/reality-mode/explorer/types.js.map +1 -0
  49. package/dist/reality-mode/fake-success-detector.d.ts +10 -0
  50. package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
  51. package/dist/reality-mode/fake-success-detector.js +76 -0
  52. package/dist/reality-mode/fake-success-detector.js.map +1 -0
  53. package/dist/reality-mode/index.d.ts +14 -0
  54. package/dist/reality-mode/index.d.ts.map +1 -0
  55. package/dist/reality-mode/index.js +14 -0
  56. package/dist/reality-mode/index.js.map +1 -0
  57. package/dist/reality-mode/reality-scanner.d.ts +48 -0
  58. package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
  59. package/dist/reality-mode/reality-scanner.js +516 -0
  60. package/dist/reality-mode/reality-scanner.js.map +1 -0
  61. package/dist/reality-mode/report-generator.d.ts +11 -0
  62. package/dist/reality-mode/report-generator.d.ts.map +1 -0
  63. package/dist/reality-mode/report-generator.js +233 -0
  64. package/dist/reality-mode/report-generator.js.map +1 -0
  65. package/dist/reality-mode/traffic-classifier.d.ts +14 -0
  66. package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
  67. package/dist/reality-mode/traffic-classifier.js +131 -0
  68. package/dist/reality-mode/traffic-classifier.js.map +1 -0
  69. package/dist/reality-mode/types.d.ts +90 -0
  70. package/dist/reality-mode/types.d.ts.map +1 -0
  71. package/dist/reality-mode/types.js +2 -0
  72. package/dist/reality-mode/types.js.map +1 -0
  73. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
  74. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
  75. package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
  76. package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
  77. package/dist/ship-badge/index.d.ts +9 -0
  78. package/dist/ship-badge/index.d.ts.map +1 -0
  79. package/dist/ship-badge/index.js +9 -0
  80. package/dist/ship-badge/index.js.map +1 -0
  81. package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
  82. package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
  83. package/dist/ship-badge/ship-badge-generator.js +681 -0
  84. package/dist/ship-badge/ship-badge-generator.js.map +1 -0
  85. package/package.json +20 -0
  86. package/src/index.ts +7 -0
  87. package/src/mock-implementation.ts +0 -0
  88. package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
  89. package/src/mockproof/import-graph-scanner.d.ts +93 -0
  90. package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
  91. package/src/mockproof/import-graph-scanner.js +482 -0
  92. package/src/mockproof/import-graph-scanner.ts +540 -0
  93. package/src/mockproof/index.ts +18 -0
  94. package/src/reality-mode/auth-enforcer.ts +97 -0
  95. package/src/reality-mode/explorer/critical-flows.ts +504 -0
  96. package/src/reality-mode/explorer/flow-parser.ts +293 -0
  97. package/src/reality-mode/explorer/index.ts +22 -0
  98. package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
  99. package/src/reality-mode/explorer/surface-discovery.ts +498 -0
  100. package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
  101. package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
  102. package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
  103. package/src/reality-mode/explorer/templates/github-action.yml +132 -0
  104. package/src/reality-mode/explorer/types.ts +356 -0
  105. package/src/reality-mode/fake-success-detector.ts +89 -0
  106. package/src/reality-mode/index.ts +19 -0
  107. package/src/reality-mode/reality-scanner.d.ts +123 -0
  108. package/src/reality-mode/reality-scanner.d.ts.map +1 -0
  109. package/src/reality-mode/reality-scanner.js +526 -0
  110. package/src/reality-mode/reality-scanner.ts +576 -0
  111. package/src/reality-mode/report-generator.ts +253 -0
  112. package/src/reality-mode/traffic-classifier.ts +169 -0
  113. package/src/reality-mode/types.ts +95 -0
  114. package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
  115. package/src/ship-badge/index.ts +16 -0
  116. package/src/ship-badge/ship-badge-generator.d.ts +136 -0
  117. package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
  118. package/src/ship-badge/ship-badge-generator.js +779 -0
  119. package/src/ship-badge/ship-badge-generator.ts +873 -0
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Runtime Explorer
3
+ *
4
+ * The "robot user" that actually clicks through the app using Playwright.
5
+ * - Navigates to all discovered routes
6
+ * - Clicks buttons, opens modals, interacts with dropdowns
7
+ * - Fills and submits forms with test data
8
+ * - Captures errors, network calls, state changes
9
+ */
10
+
11
+ import type {
12
+ AppSurface,
13
+ DiscoveredElement,
14
+ DiscoveredForm,
15
+ DiscoveredRoute,
16
+ ElementTestResult,
17
+ FormTestResult,
18
+ CapturedError,
19
+ NetworkCall,
20
+ PageState,
21
+ StateChange,
22
+ ExplorerConfig,
23
+ } from "./types";
24
+ import { SurfaceDiscovery } from "./surface-discovery";
25
+
26
+ // Test data generators for form filling
27
+ const TEST_DATA = {
28
+ email: "test-reality@guardrail.dev",
29
+ password: "TestPass123!",
30
+ name: "Reality Test User",
31
+ phone: "+1234567890",
32
+ address: "123 Test Street",
33
+ city: "Test City",
34
+ zip: "12345",
35
+ text: "This is a test input from Guardrail Reality Mode",
36
+ number: "42",
37
+ url: "https://example.com",
38
+ date: "2024-01-15",
39
+ };
40
+
41
+ export class RuntimeExplorer {
42
+ private config: ExplorerConfig;
43
+ private surfaceDiscovery: SurfaceDiscovery;
44
+ private errors: CapturedError[] = [];
45
+ private networkCalls: NetworkCall[] = [];
46
+ private isAuthenticated = false;
47
+
48
+ constructor(config: ExplorerConfig) {
49
+ this.config = config;
50
+ this.surfaceDiscovery = new SurfaceDiscovery(config.baseUrl);
51
+ }
52
+
53
+ /**
54
+ * Generate the complete Playwright test file for exploration
55
+ */
56
+ generateExplorerTest(): string {
57
+ const { baseUrl, outputDir, headless, allowDestructive, timeout } =
58
+ this.config;
59
+ const authConfig = this.config.auth;
60
+
61
+ return `/**
62
+ * Reality Explorer - Auto-generated Playwright Test
63
+ *
64
+ * This test ACTUALLY explores your app:
65
+ * - Visits every discoverable route
66
+ * - Clicks every safe button and element
67
+ * - Fills and submits forms
68
+ * - Captures what works and what breaks
69
+ *
70
+ * Generated by Guardrail Reality Mode
71
+ */
72
+
73
+ import { test, expect, Page, BrowserContext } from '@playwright/test';
74
+ import * as fs from 'fs';
75
+ import * as path from 'path';
76
+
77
+ // Configuration
78
+ const CONFIG = {
79
+ baseUrl: '${baseUrl}',
80
+ outputDir: '${outputDir.replace(/\\/g, "\\\\")}',
81
+ timeout: ${timeout},
82
+ allowDestructive: ${allowDestructive},
83
+ maxActionsPerPage: ${this.config.maxActionsPerPage || 50},
84
+ maxPages: ${this.config.maxPages || 20},
85
+ };
86
+
87
+ // Test data for form filling
88
+ const TEST_DATA = ${JSON.stringify(TEST_DATA, null, 2)};
89
+
90
+ // Destructive action patterns to avoid
91
+ const DESTRUCTIVE_PATTERNS = [
92
+ /delete/i, /remove/i, /destroy/i, /cancel.*subscription/i,
93
+ /deactivate/i, /terminate/i, /close.*account/i, /reset.*all/i,
94
+ ];
95
+
96
+ // Results storage
97
+ interface ExplorerResults {
98
+ routes: { path: string; status: string; error?: string; responseTime?: number }[];
99
+ elements: { selector: string; text: string; status: string; error?: string; changes: string[] }[];
100
+ forms: { selector: string; status: string; error?: string; fieldsFilledCount: number }[];
101
+ errors: { type: string; message: string; url: string; timestamp: number }[];
102
+ networkCalls: { url: string; method: string; status: number; duration: number }[];
103
+ coverage: { routes: number; elements: number; forms: number };
104
+ score: number;
105
+ }
106
+
107
+ const results: ExplorerResults = {
108
+ routes: [],
109
+ elements: [],
110
+ forms: [],
111
+ errors: [],
112
+ networkCalls: [],
113
+ coverage: { routes: 0, elements: 0, forms: 0 },
114
+ score: 0,
115
+ };
116
+
117
+ // Discovered surface
118
+ let discoveredRoutes: string[] = [];
119
+ let discoveredElements: any[] = [];
120
+ let discoveredForms: any[] = [];
121
+
122
+ test.describe('Reality Explorer', () => {
123
+ let context: BrowserContext;
124
+ let page: Page;
125
+
126
+ test.beforeAll(async ({ browser }) => {
127
+ // Create context with tracing
128
+ context = await browser.newContext({
129
+ viewport: { width: 1280, height: 720 },
130
+ recordVideo: { dir: path.join(CONFIG.outputDir, 'videos') },
131
+ });
132
+
133
+ // Start tracing
134
+ await context.tracing.start({ screenshots: true, snapshots: true });
135
+
136
+ page = await context.newPage();
137
+
138
+ // Setup error capture
139
+ page.on('console', msg => {
140
+ if (msg.type() === 'error') {
141
+ results.errors.push({
142
+ type: 'console',
143
+ message: msg.text(),
144
+ url: page.url(),
145
+ timestamp: Date.now(),
146
+ });
147
+ }
148
+ });
149
+
150
+ page.on('pageerror', error => {
151
+ results.errors.push({
152
+ type: 'uncaught',
153
+ message: error.message,
154
+ url: page.url(),
155
+ timestamp: Date.now(),
156
+ });
157
+ });
158
+
159
+ // Setup network capture
160
+ page.on('response', async response => {
161
+ const url = response.url();
162
+ if (url.includes('/api/') || url.includes('/graphql')) {
163
+ results.networkCalls.push({
164
+ url,
165
+ method: response.request().method(),
166
+ status: response.status(),
167
+ duration: 0, // Would need timing API for accurate duration
168
+ });
169
+ }
170
+ });
171
+
172
+ // Ensure output directory exists
173
+ if (!fs.existsSync(CONFIG.outputDir)) {
174
+ fs.mkdirSync(CONFIG.outputDir, { recursive: true });
175
+ }
176
+ });
177
+
178
+ test.afterAll(async () => {
179
+ // Save trace
180
+ await context.tracing.stop({
181
+ path: path.join(CONFIG.outputDir, 'trace.zip')
182
+ });
183
+
184
+ // Calculate score
185
+ results.score = calculateScore(results);
186
+
187
+ // Save results
188
+ fs.writeFileSync(
189
+ path.join(CONFIG.outputDir, 'explorer-results.json'),
190
+ JSON.stringify(results, null, 2)
191
+ );
192
+
193
+ // Generate HTML report
194
+ const html = generateHTMLReport(results);
195
+ fs.writeFileSync(
196
+ path.join(CONFIG.outputDir, 'reality-report.html'),
197
+ html
198
+ );
199
+
200
+ await context.close();
201
+ });
202
+
203
+ ${authConfig ? this.generateAuthTest(authConfig) : ""}
204
+
205
+ test('01 - Discover App Surface', async () => {
206
+ await page.goto(CONFIG.baseUrl);
207
+ await page.waitForLoadState('networkidle');
208
+
209
+ // Discover all routes from links
210
+ discoveredRoutes = await page.$$eval('a[href]', anchors =>
211
+ anchors
212
+ .map(a => a.getAttribute('href'))
213
+ .filter(href => href && href.startsWith('/') && !href.startsWith('//'))
214
+ .filter((v, i, a) => a.indexOf(v) === i) // unique
215
+ );
216
+
217
+ console.log(\`πŸ“ Discovered \${discoveredRoutes.length} routes\`);
218
+
219
+ // Discover interactive elements
220
+ discoveredElements = await discoverElements(page);
221
+ console.log(\`πŸ”˜ Discovered \${discoveredElements.length} interactive elements\`);
222
+
223
+ // Discover forms
224
+ discoveredForms = await discoverForms(page);
225
+ console.log(\`πŸ“ Discovered \${discoveredForms.length} forms\`);
226
+ });
227
+
228
+ test('02 - Visit All Routes', async () => {
229
+ const routesToVisit = discoveredRoutes.slice(0, CONFIG.maxPages);
230
+
231
+ for (const route of routesToVisit) {
232
+ const startTime = Date.now();
233
+
234
+ try {
235
+ const response = await page.goto(CONFIG.baseUrl + route, {
236
+ waitUntil: 'networkidle',
237
+ timeout: CONFIG.timeout
238
+ });
239
+
240
+ const status = response?.status() || 0;
241
+ const responseTime = Date.now() - startTime;
242
+
243
+ results.routes.push({
244
+ path: route,
245
+ status: status >= 200 && status < 400 ? 'success' : 'error',
246
+ responseTime,
247
+ error: status >= 400 ? \`HTTP \${status}\` : undefined,
248
+ });
249
+
250
+ // Re-discover elements on each page
251
+ const pageElements = await discoverElements(page);
252
+ const pageForms = await discoverForms(page);
253
+
254
+ // Merge newly discovered elements
255
+ for (const el of pageElements) {
256
+ if (!discoveredElements.find(e => e.selector === el.selector)) {
257
+ discoveredElements.push({ ...el, page: route });
258
+ }
259
+ }
260
+ for (const form of pageForms) {
261
+ if (!discoveredForms.find(f => f.selector === form.selector)) {
262
+ discoveredForms.push({ ...form, page: route });
263
+ }
264
+ }
265
+
266
+ await page.screenshot({
267
+ path: path.join(CONFIG.outputDir, \`route-\${route.replace(/\\//g, '_')}.png\`)
268
+ });
269
+
270
+ } catch (error: any) {
271
+ results.routes.push({
272
+ path: route,
273
+ status: 'error',
274
+ error: error.message,
275
+ });
276
+ }
277
+ }
278
+
279
+ results.coverage.routes = results.routes.filter(r => r.status === 'success').length;
280
+ console.log(\`βœ… Visited \${results.coverage.routes}/\${routesToVisit.length} routes successfully\`);
281
+ });
282
+
283
+ test('03 - Test Interactive Elements', async () => {
284
+ // Go back to home first
285
+ await page.goto(CONFIG.baseUrl);
286
+ await page.waitForLoadState('networkidle');
287
+
288
+ const elementsToTest = discoveredElements
289
+ .filter(el => !el.isDestructive || CONFIG.allowDestructive)
290
+ .slice(0, CONFIG.maxActionsPerPage);
291
+
292
+ for (const element of elementsToTest) {
293
+ const result = await testElement(page, element);
294
+ results.elements.push(result);
295
+ }
296
+
297
+ results.coverage.elements = results.elements.filter(e => e.status === 'success').length;
298
+ console.log(\`βœ… Tested \${results.coverage.elements}/\${elementsToTest.length} elements successfully\`);
299
+ });
300
+
301
+ test('04 - Test Forms', async () => {
302
+ // Go back to home first
303
+ await page.goto(CONFIG.baseUrl);
304
+ await page.waitForLoadState('networkidle');
305
+
306
+ for (const form of discoveredForms) {
307
+ const result = await testForm(page, form);
308
+ results.forms.push(result);
309
+ }
310
+
311
+ results.coverage.forms = results.forms.filter(f => f.status === 'success').length;
312
+ console.log(\`βœ… Tested \${results.coverage.forms}/\${discoveredForms.length} forms successfully\`);
313
+ });
314
+ });
315
+
316
+ // Helper: Discover interactive elements
317
+ async function discoverElements(page: Page) {
318
+ return page.$$eval(
319
+ 'button, [role="button"], a[href], [data-testid], [aria-haspopup], input[type="submit"]',
320
+ elements => elements.map((el, idx) => {
321
+ const text = el.textContent?.trim().slice(0, 50) || '';
322
+ const isDestructive = /delete|remove|destroy|cancel|deactivate/i.test(text);
323
+
324
+ return {
325
+ selector: el.id ? \`#\${el.id}\` :
326
+ el.getAttribute('data-testid') ? \`[data-testid="\${el.getAttribute('data-testid')}"]\` :
327
+ \`\${el.tagName.toLowerCase()}:nth-of-type(\${idx + 1})\`,
328
+ text,
329
+ type: el.tagName.toLowerCase(),
330
+ isDestructive,
331
+ page: '',
332
+ };
333
+ }).filter(el => el.text.length > 0)
334
+ );
335
+ }
336
+
337
+ // Helper: Discover forms
338
+ async function discoverForms(page: Page) {
339
+ return page.$$eval('form', forms =>
340
+ forms.map((form, idx) => ({
341
+ selector: form.id ? \`#\${form.id}\` : \`form:nth-of-type(\${idx + 1})\`,
342
+ action: form.action,
343
+ method: form.method,
344
+ fields: Array.from(form.querySelectorAll('input, textarea, select')).map((field: any) => ({
345
+ name: field.name || field.id,
346
+ type: field.type || 'text',
347
+ required: field.required,
348
+ selector: field.id ? \`#\${field.id}\` : \`[name="\${field.name}"]\`,
349
+ })),
350
+ page: '',
351
+ }))
352
+ );
353
+ }
354
+
355
+ // Helper: Test an element
356
+ async function testElement(page: Page, element: any) {
357
+ const beforeUrl = page.url();
358
+ const beforeHtml = await page.content();
359
+ const changes: string[] = [];
360
+
361
+ try {
362
+ // Check if element exists and is visible
363
+ const el = await page.$(element.selector);
364
+ if (!el) {
365
+ return { selector: element.selector, text: element.text, status: 'not-found', changes: [] };
366
+ }
367
+
368
+ const isVisible = await el.isVisible();
369
+ if (!isVisible) {
370
+ return { selector: element.selector, text: element.text, status: 'hidden', changes: [] };
371
+ }
372
+
373
+ // Click the element
374
+ await el.click({ timeout: 5000 });
375
+
376
+ // Wait for potential navigation or state change
377
+ await page.waitForTimeout(500);
378
+ await page.waitForLoadState('networkidle').catch(() => {});
379
+
380
+ // Check for changes
381
+ const afterUrl = page.url();
382
+ const afterHtml = await page.content();
383
+
384
+ if (afterUrl !== beforeUrl) {
385
+ changes.push(\`URL changed to \${afterUrl}\`);
386
+ }
387
+
388
+ if (afterHtml !== beforeHtml) {
389
+ changes.push('DOM changed');
390
+ }
391
+
392
+ // Check for modals
393
+ const modals = await page.$$('[role="dialog"], .modal, [data-modal]');
394
+ if (modals.length > 0) {
395
+ changes.push('Modal opened');
396
+ // Close modal if possible
397
+ await page.keyboard.press('Escape');
398
+ }
399
+
400
+ return {
401
+ selector: element.selector,
402
+ text: element.text,
403
+ status: changes.length > 0 ? 'success' : 'no-change',
404
+ changes,
405
+ };
406
+
407
+ } catch (error: any) {
408
+ return {
409
+ selector: element.selector,
410
+ text: element.text,
411
+ status: 'error',
412
+ error: error.message,
413
+ changes: [],
414
+ };
415
+ }
416
+ }
417
+
418
+ // Helper: Test a form
419
+ async function testForm(page: Page, form: any) {
420
+ let fieldsFilledCount = 0;
421
+
422
+ try {
423
+ // Navigate to the page with the form if needed
424
+ if (form.page && form.page !== page.url()) {
425
+ await page.goto(CONFIG.baseUrl + form.page);
426
+ await page.waitForLoadState('networkidle');
427
+ }
428
+
429
+ // Check if form exists
430
+ const formEl = await page.$(form.selector);
431
+ if (!formEl) {
432
+ return { selector: form.selector, status: 'not-found', fieldsFilledCount: 0 };
433
+ }
434
+
435
+ // Fill fields
436
+ for (const field of form.fields) {
437
+ try {
438
+ const fieldEl = await page.$(field.selector);
439
+ if (!fieldEl) continue;
440
+
441
+ const value = getTestValue(field.type, field.name);
442
+
443
+ if (field.type === 'checkbox' || field.type === 'radio') {
444
+ await fieldEl.check();
445
+ } else if (field.type === 'select') {
446
+ // Select first option
447
+ await page.selectOption(field.selector, { index: 1 });
448
+ } else {
449
+ await fieldEl.fill(value);
450
+ }
451
+
452
+ fieldsFilledCount++;
453
+ } catch (e) {
454
+ // Field couldn't be filled, continue
455
+ }
456
+ }
457
+
458
+ // Try to submit (but don't actually submit to avoid side effects)
459
+ // Just validate that the form is fillable
460
+
461
+ return {
462
+ selector: form.selector,
463
+ status: fieldsFilledCount > 0 ? 'success' : 'no-fields',
464
+ fieldsFilledCount,
465
+ };
466
+
467
+ } catch (error: any) {
468
+ return {
469
+ selector: form.selector,
470
+ status: 'error',
471
+ error: error.message,
472
+ fieldsFilledCount,
473
+ };
474
+ }
475
+ }
476
+
477
+ // Helper: Get appropriate test value for field type
478
+ function getTestValue(type: string, name: string): string {
479
+ const nameLower = name?.toLowerCase() || '';
480
+
481
+ if (nameLower.includes('email')) return TEST_DATA.email;
482
+ if (nameLower.includes('password')) return TEST_DATA.password;
483
+ if (nameLower.includes('phone') || nameLower.includes('tel')) return TEST_DATA.phone;
484
+ if (nameLower.includes('name')) return TEST_DATA.name;
485
+ if (nameLower.includes('address')) return TEST_DATA.address;
486
+ if (nameLower.includes('city')) return TEST_DATA.city;
487
+ if (nameLower.includes('zip') || nameLower.includes('postal')) return TEST_DATA.zip;
488
+ if (nameLower.includes('url') || nameLower.includes('website')) return TEST_DATA.url;
489
+
490
+ switch (type) {
491
+ case 'email': return TEST_DATA.email;
492
+ case 'password': return TEST_DATA.password;
493
+ case 'tel': return TEST_DATA.phone;
494
+ case 'number': return TEST_DATA.number;
495
+ case 'url': return TEST_DATA.url;
496
+ case 'date': return TEST_DATA.date;
497
+ default: return TEST_DATA.text;
498
+ }
499
+ }
500
+
501
+ // Helper: Calculate reality score
502
+ function calculateScore(results: ExplorerResults): number {
503
+ const totalRoutes = results.routes.length || 1;
504
+ const totalElements = results.elements.length || 1;
505
+ const totalForms = results.forms.length || 1;
506
+
507
+ // Coverage (40 points)
508
+ const routeCoverage = (results.coverage.routes / totalRoutes) * 15;
509
+ const elementCoverage = (results.coverage.elements / totalElements) * 15;
510
+ const formCoverage = (results.coverage.forms / totalForms) * 10;
511
+ const coverageScore = routeCoverage + elementCoverage + formCoverage;
512
+
513
+ // Functionality (35 points)
514
+ const successfulActions = results.elements.filter(e => e.status === 'success').length;
515
+ const successfulForms = results.forms.filter(f => f.status === 'success').length;
516
+ const functionalityScore =
517
+ ((successfulActions / totalElements) * 20) +
518
+ ((successfulForms / totalForms) * 15);
519
+
520
+ // Stability (15 points) - penalize errors
521
+ const errorPenalty = Math.min(results.errors.length * 3, 15);
522
+ const stabilityScore = 15 - errorPenalty;
523
+
524
+ // UX (10 points) - bonus for interactive elements that work
525
+ const interactiveElements = results.elements.filter(e => e.changes && e.changes.length > 0);
526
+ const uxScore = Math.min((interactiveElements.length / totalElements) * 10, 10);
527
+
528
+ return Math.round(Math.max(0, coverageScore + functionalityScore + stabilityScore + uxScore));
529
+ }
530
+
531
+ // Helper: Generate HTML report
532
+ function generateHTMLReport(results: ExplorerResults): string {
533
+ const score = results.score;
534
+ const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
535
+ const color = score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : '#ef4444';
536
+
537
+ return \`<!DOCTYPE html>
538
+ <html>
539
+ <head>
540
+ <title>Reality Mode Report - Guardrail</title>
541
+ <style>
542
+ * { box-sizing: border-box; margin: 0; padding: 0; }
543
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0a0a; color: #fff; padding: 2rem; }
544
+ .container { max-width: 1200px; margin: 0 auto; }
545
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
546
+ .subtitle { color: #888; margin-bottom: 2rem; }
547
+ .score-card { background: linear-gradient(135deg, #1a1a1a, #0d0d0d); border: 1px solid #333; border-radius: 1rem; padding: 2rem; text-align: center; margin-bottom: 2rem; }
548
+ .score { font-size: 6rem; font-weight: bold; color: \${color}; }
549
+ .grade { font-size: 2rem; color: \${color}; }
550
+ .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem; }
551
+ .metric { background: #1a1a1a; border: 1px solid #333; border-radius: 0.5rem; padding: 1rem; text-align: center; }
552
+ .metric-value { font-size: 2rem; font-weight: bold; color: #fff; }
553
+ .metric-label { color: #888; font-size: 0.875rem; }
554
+ .section { background: #1a1a1a; border: 1px solid #333; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1rem; }
555
+ .section h2 { font-size: 1.25rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
556
+ .item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid #333; }
557
+ .item:last-child { border-bottom: none; }
558
+ .status { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 600; }
559
+ .status-success { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
560
+ .status-error { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
561
+ .status-warning { background: rgba(234, 179, 8, 0.2); color: #eab308; }
562
+ .errors { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
563
+ .error-item { color: #ef4444; padding: 0.5rem 0; font-family: monospace; font-size: 0.875rem; }
564
+ </style>
565
+ </head>
566
+ <body>
567
+ <div class="container">
568
+ <h1>πŸ” Reality Mode Report</h1>
569
+ <p class="subtitle">Generated by Guardrail - \${new Date().toLocaleString()}</p>
570
+
571
+ <div class="score-card">
572
+ <div class="score">\${score}</div>
573
+ <div class="grade">Grade: \${grade}</div>
574
+ <p style="color: #888; margin-top: 1rem;">
575
+ \${score >= 80 ? 'βœ… Ready to ship!' : score >= 60 ? '⚠️ Needs some work' : '❌ Critical issues found'}
576
+ </p>
577
+ </div>
578
+
579
+ <div class="metrics">
580
+ <div class="metric">
581
+ <div class="metric-value">\${results.coverage.routes}/\${results.routes.length}</div>
582
+ <div class="metric-label">Routes Working</div>
583
+ </div>
584
+ <div class="metric">
585
+ <div class="metric-value">\${results.coverage.elements}/\${results.elements.length}</div>
586
+ <div class="metric-label">Elements Working</div>
587
+ </div>
588
+ <div class="metric">
589
+ <div class="metric-value">\${results.coverage.forms}/\${results.forms.length}</div>
590
+ <div class="metric-label">Forms Working</div>
591
+ </div>
592
+ <div class="metric">
593
+ <div class="metric-value">\${results.errors.length}</div>
594
+ <div class="metric-label">Errors Found</div>
595
+ </div>
596
+ </div>
597
+
598
+ <div class="section">
599
+ <h2>πŸ—ΊοΈ Routes</h2>
600
+ \${results.routes.map(r => \`
601
+ <div class="item">
602
+ <span>\${r.path}</span>
603
+ <span class="status status-\${r.status === 'success' ? 'success' : 'error'}">\${r.status}\${r.responseTime ? \` (\${r.responseTime}ms)\` : ''}</span>
604
+ </div>
605
+ \`).join('')}
606
+ </div>
607
+
608
+ <div class="section">
609
+ <h2>πŸ”˜ Interactive Elements</h2>
610
+ \${results.elements.slice(0, 20).map(e => \`
611
+ <div class="item">
612
+ <span>\${e.text || e.selector}</span>
613
+ <span class="status status-\${e.status === 'success' ? 'success' : e.status === 'no-change' ? 'warning' : 'error'}">\${e.status}</span>
614
+ </div>
615
+ \`).join('')}
616
+ \${results.elements.length > 20 ? \`<p style="color: #888; padding-top: 1rem;">... and \${results.elements.length - 20} more</p>\` : ''}
617
+ </div>
618
+
619
+ <div class="section">
620
+ <h2>πŸ“ Forms</h2>
621
+ \${results.forms.map(f => \`
622
+ <div class="item">
623
+ <span>\${f.selector}</span>
624
+ <span class="status status-\${f.status === 'success' ? 'success' : 'error'}">\${f.status} (\${f.fieldsFilledCount} fields)</span>
625
+ </div>
626
+ \`).join('')}
627
+ </div>
628
+
629
+ \${results.errors.length > 0 ? \`
630
+ <div class="section errors">
631
+ <h2>❌ Errors Captured</h2>
632
+ \${results.errors.slice(0, 10).map(e => \`
633
+ <div class="error-item">[\${e.type}] \${e.message}</div>
634
+ \`).join('')}
635
+ </div>
636
+ \` : ''}
637
+
638
+ <div class="section">
639
+ <h2>πŸ“Š API Calls</h2>
640
+ \${results.networkCalls.slice(0, 15).map(n => \`
641
+ <div class="item">
642
+ <span>\${n.method} \${n.url.split('?')[0]}</span>
643
+ <span class="status status-\${n.status < 400 ? 'success' : 'error'}">\${n.status}</span>
644
+ </div>
645
+ \`).join('')}
646
+ </div>
647
+ </div>
648
+ </body>
649
+ </html>\`;
650
+ }
651
+ `;
652
+ }
653
+
654
+ /**
655
+ * Generate auth test section if auth config is provided
656
+ */
657
+ private generateAuthTest(
658
+ authConfig: NonNullable<ExplorerConfig["auth"]>,
659
+ ): string {
660
+ return `
661
+ test('00 - Authenticate', async () => {
662
+ await page.goto('${authConfig.loginUrl}');
663
+ await page.waitForLoadState('networkidle');
664
+
665
+ // Fill credentials
666
+ await page.fill('${authConfig.credentials.emailField}', '${authConfig.credentials.email}');
667
+ await page.fill('${authConfig.credentials.passwordField}', '${authConfig.credentials.password}');
668
+
669
+ // Submit
670
+ await page.click('button[type="submit"]');
671
+
672
+ // Wait for success indicator
673
+ await page.waitForSelector('${authConfig.successIndicator}', { timeout: 10000 });
674
+
675
+ console.log('βœ… Authentication successful');
676
+ });
677
+ `;
678
+ }
679
+
680
+ /**
681
+ * Get the explorer configuration
682
+ */
683
+ getConfig(): ExplorerConfig {
684
+ return this.config;
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Create default explorer config
690
+ */
691
+ export function createDefaultConfig(
692
+ baseUrl: string,
693
+ outputDir: string,
694
+ ): ExplorerConfig {
695
+ return {
696
+ baseUrl,
697
+ maxPages: 20,
698
+ maxActionsPerPage: 50,
699
+ timeout: 30000,
700
+ headless: true,
701
+ allowDestructive: false,
702
+ destructivePatterns: [
703
+ "delete",
704
+ "remove",
705
+ "destroy",
706
+ "cancel",
707
+ "deactivate",
708
+ "terminate",
709
+ ],
710
+ outputDir,
711
+ captureVideo: true,
712
+ captureTrace: true,
713
+ captureScreenshots: true,
714
+ };
715
+ }