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