docs: enterprise refactor plan with ralph specs

This commit is contained in:
debian
2026-03-04 16:17:03 -05:00
parent 4c92712d20
commit f8191133c8
204 changed files with 32722 additions and 422 deletions

501
dist/plugins/agents/PlaywrightAgent.js vendored Normal file
View File

@@ -0,0 +1,501 @@
"use strict";
/**
* PlaywrightAgent — implements IInteractionAgent using Playwright.
* All random choices use a deterministic seed and are logged.
* Supports scope enforcement, auth injection, and action delay.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaywrightAgent = void 0;
const crypto = __importStar(require("crypto"));
const playwright_1 = require("playwright");
const Logger_1 = require("../../core/Logger");
const ExplorationConfig_1 = require("../../core/ExplorationConfig");
/** Simple deterministic pseudo-random number generator (LCG) */
class SeededRandom {
constructor(seed) {
this.state = seed;
}
/** Returns a float in [0, 1) */
next() {
this.state = (this.state * 1664525 + 1013904223) & 0xffffffff;
return (this.state >>> 0) / 0x100000000;
}
/** Returns an integer in [0, max) */
nextInt(max) {
return Math.floor(this.next() * max);
}
}
function generateId() {
return crypto.randomUUID();
}
function domHash(url, domSnapshot) {
return crypto
.createHash('sha1')
.update(url + domSnapshot)
.digest('hex')
.substring(0, 16);
}
class PlaywrightAgent {
constructor(config = {}) {
/** Captured HTTP responses for the current action */
this.pendingResponses = [];
this.pendingConsoleErrors = [];
this.pendingJsExceptions = [];
this.seed = config.seed ?? 42;
this.rng = new SeededRandom(this.seed);
this.headless = config.headless ?? true;
this.timeoutMs = config.timeoutMs ?? 30000;
this.logger = config.logger ?? new Logger_1.NullLogger();
this.explorationConfig = config.explorationConfig ?? {};
}
async launch(url) {
// Select browser type
const browserType = this.explorationConfig.browsers?.[0] ?? 'chromium';
const launcher = browserType === 'firefox' ? playwright_1.firefox : browserType === 'webkit' ? playwright_1.webkit : playwright_1.chromium;
this.browser = await launcher.launch({ headless: this.headless });
// Apply auth headers if configured
const auth = this.explorationConfig.auth;
let contextOptions = {};
// Mobile device emulation
const mobileDevice = this.explorationConfig.mobileDevice;
if (mobileDevice && mobileDevice !== 'none') {
const device = playwright_1.devices[mobileDevice];
if (device) {
contextOptions = { ...device, ...contextOptions };
}
}
// Custom viewport
if (this.explorationConfig.viewport) {
contextOptions.viewport = this.explorationConfig.viewport;
}
if (auth?.type === 'headers') {
contextOptions.extraHTTPHeaders = auth.headers;
}
this.context = await this.browser.newContext(contextOptions);
// Apply auth cookies if configured
if (auth?.type === 'cookies') {
await this.context.addCookies(auth.cookies);
}
this.page = await this.context.newPage();
this.setupListeners(this.page);
// Apply network chaos conditions
await this.applyNetworkChaos(this.page);
// Login flow auth
if (auth?.type === 'login_flow') {
await this.performLoginFlow(auth);
}
await this.page.goto(url, { timeout: this.timeoutMs });
}
async close() {
await this.browser?.close();
this.browser = undefined;
this.context = undefined;
this.page = undefined;
}
async captureState() {
const page = this.requirePage();
const url = page.url();
const title = await page.title();
const domSnapshot = await page.evaluate(() => document.body.outerHTML);
const stateId = domHash(url, domSnapshot);
return {
id: stateId,
url,
title,
timestamp: Date.now(),
domSnapshot,
visitCount: 0,
};
}
async discoverActions(state) {
const page = this.requirePage();
const actions = [];
const now = Date.now();
const currentUrl = page.url();
if (this.isExcludedPath(currentUrl)) {
return [];
}
// Discover clickable elements
const clickableSelectors = [
'a[href]',
'button',
'[role="button"]',
'input[type="submit"]',
'input[type="button"]',
];
for (const selector of clickableSelectors) {
if (this.isExcludedSelector(selector))
continue;
const elements = await page.locator(selector).all();
for (const el of elements) {
const isVisible = await el.isVisible().catch(() => false);
if (!isVisible)
continue;
// Check element against excluded selectors
const elSel = await this.buildSelector(el, selector);
if (this.isExcludedSelector(elSel))
continue;
// For links, check domain is allowed
if (selector === 'a[href]') {
const href = await el.getAttribute('href').catch(() => null);
if (href && this.isExternalLink(href, currentUrl))
continue;
}
const actionSeed = this.rng.nextInt(0x7fffffff);
actions.push({
id: generateId(),
type: 'click',
selector: elSel,
timestamp: now,
seed: actionSeed,
stateId: state.id,
});
}
}
// Discover fillable inputs
const inputSelectors = [
'input[type="text"]',
'input[type="email"]',
'input[type="password"]',
'textarea',
];
for (const selector of inputSelectors) {
if (this.isExcludedSelector(selector))
continue;
const elements = await page.locator(selector).all();
for (const el of elements) {
const isVisible = await el.isVisible().catch(() => false);
if (!isVisible)
continue;
const elSel = await this.buildSelector(el, selector);
if (this.isExcludedSelector(elSel))
continue;
const actionSeed = this.rng.nextInt(0x7fffffff);
actions.push({
id: generateId(),
type: 'fill',
selector: elSel,
value: '',
timestamp: now,
seed: actionSeed,
stateId: state.id,
});
}
}
return actions;
}
async executeAction(action) {
const page = this.requirePage();
this.resetPending();
const observationId = generateId();
const actionDelayMs = this.explorationConfig.actionDelayMs ?? 0;
// Skip actions targeting excluded paths
if (action.url && this.isExcludedPath(action.url)) {
const state = await this.captureState();
return this.buildObservation(observationId, action.id, state.id);
}
// Enforce allowed domains for navigate actions
if (action.type === 'navigate' && action.url) {
if (!this.isAllowedUrl(action.url)) {
const state = await this.captureState();
return this.buildObservation(observationId, action.id, state.id);
}
}
try {
switch (action.type) {
case 'click':
await page.locator(action.selector).first().click({ timeout: this.timeoutMs });
break;
case 'fill':
await page.locator(action.selector).first().fill(action.value ?? '', { timeout: this.timeoutMs });
break;
case 'navigate':
await page.goto(action.url, { timeout: this.timeoutMs });
break;
case 'submit':
await page.locator(action.selector).first().dispatchEvent('submit');
break;
case 'select':
await page.locator(action.selector).first().selectOption(action.value ?? '');
break;
}
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.pendingJsExceptions.push(`Action ${action.type} failed: ${msg}`);
}
// Wait for async effects to settle + configured delay
await page.waitForTimeout(200 + actionDelayMs);
const newState = await this.captureState();
return this.buildObservation(observationId, action.id, newState.id);
}
// ─── Private helpers ──────────────────────────────────────────────────────
getPage() {
return this.requirePage();
}
requirePage() {
if (!this.page)
throw new Error('PlaywrightAgent: not launched. Call launch() first.');
return this.page;
}
resetPending() {
this.pendingResponses = [];
this.pendingConsoleErrors = [];
this.pendingJsExceptions = [];
}
buildObservation(observationId, actionId, newStateId) {
return {
id: observationId,
actionId,
newStateId,
httpResponses: [...this.pendingResponses],
consoleErrors: [...this.pendingConsoleErrors],
jsExceptions: [...this.pendingJsExceptions],
timestamp: Date.now(),
};
}
setupListeners(page) {
const requestTimestamps = new Map();
page.on('request', (req) => {
requestTimestamps.set(req.url(), Date.now());
});
page.on('response', (res) => {
const start = requestTimestamps.get(res.url()) ?? Date.now();
const durationMs = Date.now() - start;
this.pendingResponses.push({
url: res.url(),
status: res.status(),
method: res.request().method(),
durationMs,
});
});
page.on('console', (msg) => {
if (msg.type() === 'error') {
this.pendingConsoleErrors.push(msg.text());
}
});
page.on('pageerror', (err) => {
this.pendingJsExceptions.push(err.message);
});
}
async buildSelector(el, fallback) {
try {
const id = await el.getAttribute('id').catch(() => null);
if (id)
return `#${id}`;
const name = await el.getAttribute('name').catch(() => null);
if (name)
return `[name="${name}"]`;
}
catch {
// ignore
}
return fallback;
}
isExcludedPath(urlOrPath) {
const excludedPaths = this.explorationConfig.excludedPaths ?? [];
if (excludedPaths.length === 0)
return false;
try {
const parsed = new URL(urlOrPath, 'http://placeholder');
return excludedPaths.some((p) => parsed.pathname.startsWith(p));
}
catch {
return false;
}
}
isExcludedSelector(selector) {
const excludedSelectors = this.explorationConfig.excludedSelectors ?? [];
return excludedSelectors.includes(selector);
}
isExternalLink(href, currentUrl) {
const allowedDomains = this.explorationConfig.allowedDomains ?? [];
if (allowedDomains.length === 0)
return false;
try {
const base = new URL(currentUrl);
const target = new URL(href, base.origin);
return !allowedDomains.includes(target.hostname);
}
catch {
return false;
}
}
isAllowedUrl(url) {
const allowedDomains = this.explorationConfig.allowedDomains ?? [];
if (allowedDomains.length === 0)
return true;
try {
const parsed = new URL(url);
return allowedDomains.includes(parsed.hostname);
}
catch {
return false;
}
}
async performLoginFlow(auth) {
const page = this.requirePage();
await page.goto(auth.loginUrl, { timeout: this.timeoutMs });
await page.locator(auth.usernameSelector).first().fill(auth.username);
await page.locator(auth.passwordSelector).first().fill(auth.password);
await page.locator(auth.submitSelector).first().click();
await page.waitForNavigation({ timeout: this.timeoutMs }).catch(() => undefined);
const currentUrl = page.url();
if (currentUrl === auth.loginUrl || currentUrl.includes(new URL(auth.loginUrl).pathname)) {
throw new Error(`Login failed: still on login page ${currentUrl}`);
}
}
async applyNetworkChaos(page) {
const chaos = this.explorationConfig.networkChaos;
if (!chaos?.enabled)
return;
// Apply network condition via CDP (Chromium only)
const profile = chaos.profile ?? 'none';
const condition = ExplorationConfig_1.NETWORK_PROFILES[profile];
if (condition) {
try {
const client = await this.context.newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: condition.offline,
downloadThroughput: condition.offline ? -1 : (condition.downloadKbps * 1024) / 8,
uploadThroughput: condition.offline ? -1 : (condition.uploadKbps * 1024) / 8,
latency: condition.latencyMs,
});
}
catch {
// CDP not available (e.g. non-Chromium browser) — ignore
}
}
// Block specified endpoints
if (chaos.blockedEndpoints && chaos.blockedEndpoints.length > 0) {
await page.route('**/*', (route) => {
const url = route.request().url();
const isBlocked = chaos.blockedEndpoints.some((pattern) => this.matchGlob(url, pattern));
if (isBlocked) {
route.fulfill({ status: 503, body: 'Service Unavailable (ABE Network Chaos)' });
}
else {
route.continue();
}
});
}
// Slow down specified endpoints
if (chaos.slowEndpoints && chaos.slowEndpoints.length > 0) {
for (const slowEp of chaos.slowEndpoints) {
await page.route(slowEp.pattern, async (route) => {
await new Promise((r) => setTimeout(r, slowEp.delayMs));
route.continue();
});
}
}
}
matchGlob(url, pattern) {
// Convert glob pattern to regex
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
try {
return new RegExp(escaped).test(url);
}
catch {
return false;
}
}
/**
* Detects mobile layout issues on the current page:
* - Touch targets smaller than 44x44px (WCAG 2.5.5 / Apple HIG)
* - Horizontal content overflow beyond viewport width
*/
async detectMobileLayoutIssues(stateId, sessionId, actionTrace) {
const page = this.requirePage();
const anomalies = [];
try {
const issues = await page.evaluate(() => {
const findings = [];
// Check for horizontal overflow
const docWidth = document.documentElement.scrollWidth;
const viewportWidth = window.innerWidth;
if (docWidth > viewportWidth) {
findings.push(`horizontal_overflow: ${docWidth}px > ${viewportWidth}px viewport`);
}
// Check for small touch targets (< 44x44 px)
const interactiveSelectors = ['a', 'button', '[role="button"]', 'input', 'select', 'textarea'];
const seen = new Set();
for (const sel of interactiveSelectors) {
for (const el of document.querySelectorAll(sel)) {
if (seen.has(el))
continue;
seen.add(el);
const rect = el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0)
continue;
if (rect.width < 44 || rect.height < 44) {
const label = el.getAttribute('aria-label') ||
el.textContent?.trim().slice(0, 30) ||
el.tagName.toLowerCase();
findings.push(`small_touch_target: "${label}" (${Math.round(rect.width)}x${Math.round(rect.height)}px)`);
if (findings.filter((f) => f.startsWith('small_touch_target')).length >= 5)
break;
}
}
if (findings.filter((f) => f.startsWith('small_touch_target')).length >= 5)
break;
}
return findings;
}).catch(() => []);
if (issues.length === 0)
return anomalies;
const hasOverflow = issues.some((i) => i.startsWith('horizontal_overflow'));
const smallTargetCount = issues.filter((i) => i.startsWith('small_touch_target')).length;
const severity = hasOverflow ? 'high' : smallTargetCount >= 3 ? 'medium' : 'low';
anomalies.push({
id: generateId(),
type: 'mobile_layout_issue',
severity,
observationId: stateId,
actionTrace,
description: `Mobile layout issues detected: ${issues.slice(0, 3).join('; ')}`,
evidence: { rawErrors: issues },
timestamp: Date.now(),
});
}
catch {
// Page may be in invalid state — ignore
}
return anomalies;
}
}
exports.PlaywrightAgent = PlaywrightAgent;

View File

@@ -0,0 +1,124 @@
"use strict";
/**
* AccessibilityCollector — runs axe-core after state changes to detect WCAG violations.
* Converts axe violations to IAnomaly with severity mapped from impact level.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessibilityCollector = exports.DEFAULT_A11Y_CONFIG = void 0;
const crypto = __importStar(require("crypto"));
exports.DEFAULT_A11Y_CONFIG = {
enabled: true,
minImpact: 'serious',
wcagLevel: 'AA',
};
const IMPACT_TO_SEVERITY = {
minor: 'low',
moderate: 'medium',
serious: 'high',
critical: 'critical',
};
const IMPACT_RANK = {
minor: 0,
moderate: 1,
serious: 2,
critical: 3,
};
class AccessibilityCollector {
constructor(config = {}) {
this.config = { ...exports.DEFAULT_A11Y_CONFIG, ...config };
}
async collect(page, stateId, sessionId, actionTrace) {
if (!this.config.enabled)
return [];
try {
const violations = await this.runAxe(page);
const minRank = IMPACT_RANK[this.config.minImpact] ?? 2;
const anomalies = [];
for (const violation of violations) {
const impact = violation.impact ?? 'minor';
if ((IMPACT_RANK[impact] ?? 0) < minRank)
continue;
const severity = IMPACT_TO_SEVERITY[impact] ?? 'medium';
anomalies.push({
id: crypto.randomUUID(),
type: 'accessibility_violation',
severity,
observationId: stateId,
actionTrace,
description: `[axe] ${violation.description}`,
evidence: {
rawErrors: [
`Rule: ${violation.id}`,
`Impact: ${impact}`,
`Affected nodes: ${violation.nodes.length}`,
`Help: ${violation.helpUrl}`,
],
},
timestamp: Date.now(),
});
}
return anomalies;
}
catch {
// axe might fail if page is not in a valid state
return [];
}
}
async runAxe(page) {
try {
const { AxeBuilder } = await Promise.resolve().then(() => __importStar(require('@axe-core/playwright')));
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
return results.violations;
}
catch {
// Fallback: try via page.evaluate if AxeBuilder fails
const results = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window;
if (typeof win.axe === 'undefined')
return [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const r = await win.axe.run();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return r.violations;
}).catch(() => []);
return Array.isArray(results) ? results : [];
}
}
}
exports.AccessibilityCollector = AccessibilityCollector;

View File

@@ -0,0 +1,56 @@
"use strict";
/**
* DOMSnapshotCollector — writes the DOM snapshot at anomaly moment to disk.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOMSnapshotCollector = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class DOMSnapshotCollector {
constructor(outputDir = './reports') {
this.outputDir = outputDir;
this.name = 'DOMSnapshotCollector';
}
async collect(anomaly, agent) {
const state = await agent.captureState();
const dir = path.join(this.outputDir, anomaly.id);
fs.mkdirSync(dir, { recursive: true });
const domPath = path.join(dir, 'dom.html');
fs.writeFileSync(domPath, state.domSnapshot, 'utf8');
return { domSnapshotPath: path.relative(this.outputDir, domPath) };
}
}
exports.DOMSnapshotCollector = DOMSnapshotCollector;

View File

@@ -0,0 +1,18 @@
"use strict";
/**
* NetworkCollector — logs all HTTP responses from the current observation.
* The data is already captured in the observation; this collector formats it.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkCollector = void 0;
class NetworkCollector {
constructor() {
this.name = 'NetworkCollector';
}
async collect(anomaly, _agent) {
// HTTP responses are captured in the observation → anomaly evidence
const httpLog = anomaly.evidence.httpLog ?? [];
return { httpLog };
}
}
exports.NetworkCollector = NetworkCollector;

View File

@@ -0,0 +1,177 @@
"use strict";
/**
* PerformanceCollector — captures Navigation Timing and Core Web Vitals after each navigation.
* Detects performance_degradation anomalies based on configurable thresholds.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PerformanceCollector = exports.DEFAULT_PERF_CONFIG = void 0;
const crypto = __importStar(require("crypto"));
exports.DEFAULT_PERF_CONFIG = {
enabled: true,
lcpThresholdMs: 4000,
clsThreshold: 0.25,
inpThresholdMs: 500,
ttfbThresholdMs: 1800,
};
class PerformanceCollector {
constructor(config = {}) {
this.metricsStore = [];
this.config = { ...exports.DEFAULT_PERF_CONFIG, ...config };
}
async collect(page, stateId, sessionId, actionTrace) {
if (!this.config.enabled) {
const empty = {
id: crypto.randomUUID(), sessionId, stateId, url: page.url(),
ttfb: 0, domContentLoaded: 0, loadComplete: 0,
lcp: null, cls: null, fid: null, inp: null,
totalRequests: 0, failedRequests: 0, capturedAt: Date.now(),
};
return { metrics: empty, anomalies: [] };
}
// Capture Navigation Timing
const timing = await page.evaluate(() => {
const t = performance.timing;
return {
ttfb: t.responseStart - t.requestStart,
domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
loadComplete: t.loadEventEnd - t.navigationStart,
};
}).catch(() => ({ ttfb: 0, domContentLoaded: 0, loadComplete: 0 }));
// Capture Core Web Vitals via PerformanceObserver
const vitals = await page.evaluate(() => {
return new Promise((resolve) => {
const result = {
lcp: null, cls: null, inp: null,
};
try {
// Try to observe LCP
if ('PerformanceObserver' in window) {
try {
const lcpObs = new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
result.lcp = entries[entries.length - 1].startTime;
}
});
lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });
}
catch { /* not supported */ }
try {
const clsObs = new PerformanceObserver((list) => {
let clsScore = 0;
for (const entry of list.getEntries()) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clsScore += entry.value ?? 0;
}
result.cls = clsScore;
});
clsObs.observe({ type: 'layout-shift', buffered: true });
}
catch { /* not supported */ }
}
}
catch { /* ignore */ }
// Resolve after short wait
setTimeout(() => resolve(result), 500);
});
}).catch(() => ({ lcp: null, cls: null, inp: null }));
const metrics = {
id: crypto.randomUUID(),
sessionId,
stateId,
url: page.url(),
ttfb: timing.ttfb,
domContentLoaded: timing.domContentLoaded,
loadComplete: timing.loadComplete,
lcp: vitals.lcp,
cls: vitals.cls,
fid: null,
inp: vitals.inp,
totalRequests: 0,
failedRequests: 0,
capturedAt: Date.now(),
};
this.metricsStore.push(metrics);
const anomalies = this.detectAnomalies(metrics, stateId, actionTrace);
return { metrics, anomalies };
}
getMetrics() {
return this.metricsStore;
}
detectAnomalies(metrics, stateId, actionTrace) {
const anomalies = [];
const issues = [];
let severityRank = 0; // 0=low,1=medium,2=high,3=critical
if (metrics.lcp !== null && metrics.lcp > this.config.lcpThresholdMs) {
issues.push(`LCP: ${metrics.lcp}ms (threshold: ${this.config.lcpThresholdMs}ms)`);
if (severityRank < 2)
severityRank = 2; // high
}
if (metrics.cls !== null && metrics.cls > this.config.clsThreshold) {
issues.push(`CLS: ${metrics.cls.toFixed(3)} (threshold: ${this.config.clsThreshold})`);
if (severityRank < 1)
severityRank = 1; // medium
}
if (metrics.inp !== null && metrics.inp > this.config.inpThresholdMs) {
issues.push(`INP: ${metrics.inp}ms (threshold: ${this.config.inpThresholdMs}ms)`);
if (severityRank < 2)
severityRank = 2; // high
}
if (metrics.ttfb > this.config.ttfbThresholdMs) {
issues.push(`TTFB: ${metrics.ttfb}ms (threshold: ${this.config.ttfbThresholdMs}ms)`);
if (severityRank < 1)
severityRank = 1; // medium
}
const RANK_TO_SEVERITY = ['low', 'medium', 'high', 'critical'];
const maxSeverity = RANK_TO_SEVERITY[severityRank] ?? 'low';
if (issues.length === 0)
return anomalies;
anomalies.push({
id: crypto.randomUUID(),
type: 'performance_degradation',
severity: maxSeverity,
observationId: stateId,
actionTrace,
description: `Performance degradation at ${metrics.url}: ${issues[0]}`,
evidence: {
rawErrors: issues,
},
timestamp: Date.now(),
});
return anomalies;
}
}
exports.PerformanceCollector = PerformanceCollector;

View File

@@ -0,0 +1,63 @@
"use strict";
/**
* ScreenshotCollector — captures a PNG screenshot at anomaly moment.
* Requires the agent to be a PlaywrightAgent (duck-typing check).
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScreenshotCollector = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
function isPlaywrightAgent(agent) {
return typeof agent.getPage === 'function';
}
class ScreenshotCollector {
constructor(outputDir = './reports') {
this.outputDir = outputDir;
this.name = 'ScreenshotCollector';
}
async collect(anomaly, agent) {
if (!isPlaywrightAgent(agent)) {
return {};
}
const page = agent.getPage();
const dir = path.join(this.outputDir, anomaly.id);
fs.mkdirSync(dir, { recursive: true });
const screenshotPath = path.join(dir, 'screenshot.png');
await page.screenshot({ path: screenshotPath, fullPage: true });
return { screenshotPath: path.relative(this.outputDir, screenshotPath) };
}
}
exports.ScreenshotCollector = ScreenshotCollector;

View File

@@ -0,0 +1,155 @@
"use strict";
/**
* VisualRegressionCollector — captures screenshots and compares against baselines.
* Uses pixelmatch for pixel-level comparison and sharp for image normalization.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisualRegressionCollector = exports.DEFAULT_VISUAL_CONFIG = void 0;
exports.compareScreenshots = compareScreenshots;
const crypto = __importStar(require("crypto"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
exports.DEFAULT_VISUAL_CONFIG = {
enabled: true,
threshold: 0.001,
screenshotFullPage: false,
ignoreSelectors: [],
};
async function compareScreenshots(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
// Dynamic imports to avoid loading heavy deps at startup
const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
const pixelmatch = (await Promise.resolve().then(() => __importStar(require('pixelmatch')))).default;
const [baselineRaw, currentRaw] = await Promise.all([
sharp(baselinePath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
sharp(currentPath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
]);
const { width, height } = baselineRaw.info;
const diffBuffer = Buffer.alloc(width * height * 4);
const diffPixels = pixelmatch(baselineRaw.data, currentRaw.data, diffBuffer, width, height, { threshold });
const totalPixels = width * height;
const diffPercent = totalPixels > 0 ? diffPixels / totalPixels : 0;
// Write diff image
await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
.png()
.toFile(diffOutputPath);
return { diffPixels, diffPercent, hasDiff: diffPixels > 0 };
}
class VisualRegressionCollector {
constructor(outputDir, repo, config = {}) {
this.outputDir = outputDir;
this.repo = repo;
this.config = { ...exports.DEFAULT_VISUAL_CONFIG, ...config };
}
/**
* Process a screenshot for visual regression.
* Returns an anomaly if a regression is detected, otherwise null.
*/
async processScreenshot(screenshotPath, state, sessionId, actionTrace) {
if (!this.config.enabled)
return null;
const comparisonId = crypto.randomUUID();
const baseline = this.repo.findBaselineByStateId(state.id);
if (!baseline) {
// No baseline: create a new_state comparison record
this.repo.createComparison({
id: comparisonId,
sessionId,
stateId: state.id,
currentScreenshotPath: screenshotPath,
status: 'new_state',
});
return null;
}
// Compare against baseline
const diffDir = path.join(this.outputDir, 'visual', comparisonId);
if (!fs.existsSync(diffDir)) {
fs.mkdirSync(diffDir, { recursive: true });
}
const diffPath = path.join(diffDir, 'diff.png');
let diffPixels = 0;
let diffPercent = 0;
try {
const result = await compareScreenshots(baseline.screenshot_path, screenshotPath, diffPath, this.config.threshold);
diffPixels = result.diffPixels;
diffPercent = result.diffPercent;
}
catch {
// If comparison fails (e.g. image format issues), skip
return null;
}
const thresholdPct = this.config.threshold;
const status = diffPercent > thresholdPct ? 'failed' : 'passed';
this.repo.createComparison({
id: comparisonId,
sessionId,
stateId: state.id,
baselineId: baseline.id,
currentScreenshotPath: screenshotPath,
diffScreenshotPath: status === 'failed' ? diffPath : undefined,
diffPixels,
diffPercent,
status,
});
if (status !== 'failed')
return null;
// Determine severity from diff percent
const pct = diffPercent * 100;
let severity;
if (pct > 15)
severity = 'critical';
else if (pct > 5)
severity = 'high';
else if (pct > 1)
severity = 'medium';
else
severity = 'low';
const anomaly = {
id: crypto.randomUUID(),
type: 'visual_regression',
severity,
observationId: state.id,
actionTrace,
description: `Visual regression detected: ${(pct).toFixed(2)}% of pixels changed`,
evidence: {
screenshotPath: diffPath,
rawErrors: [`Diff: ${diffPixels} pixels (${(pct).toFixed(2)}%)`],
},
timestamp: Date.now(),
};
return anomaly;
}
}
exports.VisualRegressionCollector = VisualRegressionCollector;

97
dist/plugins/exporters/JSONExporter.js vendored Normal file
View File

@@ -0,0 +1,97 @@
"use strict";
/**
* JSONExporter — produces a structured JSON report for AI debugging workflows.
* Output: reports/{anomaly-id}/report.json
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.JSONExporter = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
class JSONExporter {
constructor(targetUrl = '', abeVersion = '0.1.0') {
this.targetUrl = targetUrl;
this.abeVersion = abeVersion;
this.format = 'json';
}
async export(anomaly, outputDir) {
fs.mkdirSync(outputDir, { recursive: true });
const report = {
version: '1.0',
generated_at: new Date(anomaly.timestamp).toISOString(),
environment: {
target_url: this.targetUrl,
abe_version: this.abeVersion,
os: os.platform(),
node_version: process.version,
},
anomaly: {
id: anomaly.id,
type: anomaly.type,
severity: anomaly.severity,
description: anomaly.description,
timestamp: anomaly.timestamp,
},
reproduction: {
seed: anomaly.actionTrace[0]?.seed ?? null,
steps: anomaly.actionTrace.map((action, index) => ({
step: index + 1,
action_type: action.type,
selector: action.selector,
value: action.value,
url: action.url,
timestamp: action.timestamp,
})),
},
evidence: {
screenshot: anomaly.evidence.screenshotPath ?? null,
dom_snapshot: anomaly.evidence.domSnapshotPath ?? null,
http_log: (anomaly.evidence.httpLog ?? []).map((r) => ({
url: r.url,
method: r.method,
status: r.status,
duration_ms: r.durationMs,
})),
console_errors: anomaly.evidence.rawErrors?.filter((e) => e.startsWith('console:')) ?? [],
js_exceptions: anomaly.evidence.rawErrors?.filter((e) => !e.startsWith('console:')) ?? [],
},
};
const filePath = path.join(outputDir, 'report.json');
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf8');
return filePath;
}
}
exports.JSONExporter = JSONExporter;

View File

@@ -0,0 +1,113 @@
"use strict";
/**
* MarkdownExporter — produces a human-readable bug report.
* Output: reports/{anomaly-id}/report.md
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MarkdownExporter = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class MarkdownExporter {
constructor() {
this.format = 'markdown';
}
async export(anomaly, outputDir) {
fs.mkdirSync(outputDir, { recursive: true });
const date = new Date(anomaly.timestamp).toISOString().split('T')[0];
const seed = anomaly.actionTrace[0]?.seed ?? 'N/A';
const replayCmd = `npm run replay -- --report ${outputDir}/report.json`;
const steps = anomaly.actionTrace
.map((action, i) => {
switch (action.type) {
case 'navigate':
return `${i + 1}. Navigate to \`${action.url}\``;
case 'click':
return `${i + 1}. Click element \`${action.selector}\``;
case 'fill':
return `${i + 1}. Fill \`${action.selector}\` with \`${JSON.stringify(action.value ?? '')}\``;
case 'select':
return `${i + 1}. Select \`${action.value}\` in \`${action.selector}\``;
case 'submit':
return `${i + 1}. Submit form \`${action.selector}\``;
default:
return `${i + 1}. ${action.type}`;
}
})
.join('\n');
const httpTable = (anomaly.evidence.httpLog ?? []).length > 0
? [
'| Method | URL | Status | Duration |',
'|--------|-----|--------|----------|',
...(anomaly.evidence.httpLog ?? []).map((r) => `| ${r.method} | ${r.url} | ${r.status} | ${r.durationMs}ms |`),
].join('\n')
: '_No HTTP log available._';
const rawErrors = (anomaly.evidence.rawErrors ?? []).length > 0
? '```\n' + anomaly.evidence.rawErrors.join('\n') + '\n```'
: '_No raw errors recorded._';
const md = `# Bug Report — ${anomaly.type}${date}
## Summary
${anomaly.description}
## Severity
**${anomaly.severity}** — detected by ABE heuristic rule \`${anomaly.type}\`
## Reproduction Steps
${steps.length > 0 ? steps : '_No steps recorded._'}
**Seed used**: \`${seed}\`
**Replay command**: \`${replayCmd}\`
## Observed Behavior
${anomaly.description}
## Evidence
- Screenshot: \`${anomaly.evidence.screenshotPath ?? 'N/A'}\`
- DOM Snapshot: \`${anomaly.evidence.domSnapshotPath ?? 'N/A'}\`
- HTTP Log:
${httpTable}
## Raw Errors
${rawErrors}
`;
const filePath = path.join(outputDir, 'report.md');
fs.writeFileSync(filePath, md, 'utf8');
return filePath;
}
}
exports.MarkdownExporter = MarkdownExporter;

139
dist/plugins/fuzzers/FuzzingEngine.js vendored Normal file
View File

@@ -0,0 +1,139 @@
"use strict";
/**
* FuzzingEngine — orchestrates fuzzing strategies for form inputs.
* Implements IFuzzingPlugin so ExplorationEngine doesn't need to import it directly.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FuzzingEngine = void 0;
const crypto = __importStar(require("crypto"));
const InputTypeDetector_1 = require("./InputTypeDetector");
const EmptyValueStrategy_1 = require("./strategies/EmptyValueStrategy");
const OversizedStringStrategy_1 = require("./strategies/OversizedStringStrategy");
const SpecialCharsStrategy_1 = require("./strategies/SpecialCharsStrategy");
const TypeMismatchStrategy_1 = require("./strategies/TypeMismatchStrategy");
const BoundaryValueStrategy_1 = require("./strategies/BoundaryValueStrategy");
/** Regex to match basic input elements in an HTML string */
const INPUT_RE = /<(input|textarea|select)[^>]*>/gi;
const ATTR_RE = (name) => new RegExp(`${name}="([^"]*)"`, 'i');
function extractFields(domSnapshot) {
const fields = [];
let match;
while ((match = INPUT_RE.exec(domSnapshot)) !== null) {
const tag = match[0] ?? '';
const tagName = match[1] ?? 'input';
const idMatch = ATTR_RE('id').exec(tag);
const nameMatch = ATTR_RE('name').exec(tag);
const typeMatch = ATTR_RE('type').exec(tag);
const placeholderMatch = ATTR_RE('placeholder').exec(tag);
const ariaMatch = ATTR_RE('aria-label').exec(tag);
const selector = idMatch?.[1]
? `#${idMatch[1]}`
: nameMatch?.[1]
? `[name="${nameMatch[1]}"]`
: tagName;
fields.push({
selector,
tagName,
inputType: typeMatch?.[1],
name: nameMatch?.[1],
placeholder: placeholderMatch?.[1],
ariaLabel: ariaMatch?.[1],
});
}
return fields;
}
class FuzzingEngine {
constructor(config) {
this.intensity = config.intensity;
this.seed = config.seed;
}
/** IFuzzingPlugin implementation — parses fields from DOM snapshot */
generateFuzzActions(domSnapshot, state) {
const fields = extractFields(domSnapshot);
return this.generateFuzzActionsForFields(fields, state);
}
/** Generate fuzz actions from explicit field descriptors */
generateFuzzActionsForFields(fields, state) {
const actions = [];
const now = Date.now();
const strategies = this.selectStrategies();
for (const field of fields) {
const detectedType = (0, InputTypeDetector_1.detectInputType)({
tagName: field.tagName,
inputType: field.inputType,
name: field.name,
placeholder: field.placeholder,
ariaLabel: field.ariaLabel,
});
for (const strategy of strategies) {
const values = this.getValuesFromStrategy(strategy, detectedType);
for (const value of values) {
actions.push({
id: crypto.randomUUID(),
type: 'fill',
selector: field.selector,
value,
timestamp: now,
seed: this.seed,
stateId: state.id,
});
}
}
}
return actions;
}
selectStrategies() {
const empty = new EmptyValueStrategy_1.EmptyValueStrategy();
const typeMismatch = new TypeMismatchStrategy_1.TypeMismatchStrategy();
const oversized = new OversizedStringStrategy_1.OversizedStringStrategy(this.intensity);
const boundary = new BoundaryValueStrategy_1.BoundaryValueStrategy();
const special = new SpecialCharsStrategy_1.SpecialCharsStrategy();
switch (this.intensity) {
case 'low':
return [empty, typeMismatch];
case 'medium':
return [empty, typeMismatch, oversized, boundary];
case 'high':
return [empty, typeMismatch, oversized, boundary, special];
}
}
getValuesFromStrategy(strategy, type) {
if (!strategy.appliesTo(type))
return [];
return strategy.values(type);
}
}
exports.FuzzingEngine = FuzzingEngine;

View File

@@ -0,0 +1,52 @@
"use strict";
/**
* InputTypeDetector — detects field type from DOM attributes.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectInputType = detectInputType;
/** Detect type from input[type], name, placeholder, aria-label */
function detectInputType(attrs) {
const tag = (attrs.tagName ?? '').toLowerCase();
if (tag === 'textarea')
return 'textarea';
if (tag === 'select')
return 'select';
const inputType = (attrs.inputType ?? '').toLowerCase();
if (inputType === 'email')
return 'email';
if (inputType === 'password')
return 'password';
if (inputType === 'number')
return 'number';
if (inputType === 'date')
return 'date';
if (inputType === 'tel')
return 'phone';
if (inputType === 'url')
return 'url';
if (inputType === 'search')
return 'search';
if (inputType === 'file')
return 'file';
// Infer from name/placeholder/aria-label
const hints = [
(attrs.name ?? '').toLowerCase(),
(attrs.placeholder ?? '').toLowerCase(),
(attrs.ariaLabel ?? '').toLowerCase(),
].join(' ');
if (/email/.test(hints))
return 'email';
if (/password|pass/.test(hints))
return 'password';
if (/phone|tel|mobile/.test(hints))
return 'phone';
if (/date|birth|dob/.test(hints))
return 'date';
if (/number|qty|quantity|age/.test(hints))
return 'number';
if (/search/.test(hints))
return 'search';
if (/url|website|link/.test(hints))
return 'url';
return 'text';
}

View File

@@ -0,0 +1,26 @@
"use strict";
/**
* BoundaryValueStrategy — tests values at the edges of expected ranges.
* Applies to: number, date.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoundaryValueStrategy = void 0;
class BoundaryValueStrategy {
constructor() {
this.name = 'BoundaryValueStrategy';
}
appliesTo(type) {
return type === 'number' || type === 'date';
}
values(type) {
switch (type) {
case 'number':
return ['0', '-1', '2147483647', '2147483648', '-2147483648'];
case 'date':
return ['1900-01-01', '2099-12-31', '1970-01-01'];
default:
return [];
}
}
}
exports.BoundaryValueStrategy = BoundaryValueStrategy;

View File

@@ -0,0 +1,19 @@
"use strict";
/**
* EmptyValueStrategy — submits empty/whitespace values to catch missing server-side validation.
* Applies to: all input types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmptyValueStrategy = void 0;
class EmptyValueStrategy {
constructor() {
this.name = 'EmptyValueStrategy';
}
appliesTo(_type) {
return true;
}
values() {
return ['', ' ', '\t'];
}
}
exports.EmptyValueStrategy = EmptyValueStrategy;

View File

@@ -0,0 +1,28 @@
"use strict";
/**
* OversizedStringStrategy — submits strings far beyond expected length.
* Applies to: text, email, password, textarea.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OversizedStringStrategy = void 0;
const APPLICABLE_TYPES = ['text', 'email', 'password', 'textarea'];
class OversizedStringStrategy {
constructor(intensity) {
this.intensity = intensity;
this.name = 'OversizedStringStrategy';
}
appliesTo(type) {
return APPLICABLE_TYPES.includes(type);
}
values() {
switch (this.intensity) {
case 'low':
return ['A'.repeat(256)];
case 'medium':
return ['A'.repeat(1024)];
case 'high':
return ['A'.repeat(10000) + '日本語テスト𠮷野家'];
}
}
}
exports.OversizedStringStrategy = OversizedStringStrategy;

View File

@@ -0,0 +1,26 @@
"use strict";
/**
* SpecialCharsStrategy — injects characters that break SQL, HTML, and shell contexts.
* Applies to: text, email, search, textarea.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpecialCharsStrategy = void 0;
const APPLICABLE_TYPES = ['text', 'email', 'search', 'textarea'];
class SpecialCharsStrategy {
constructor() {
this.name = 'SpecialCharsStrategy';
}
appliesTo(type) {
return APPLICABLE_TYPES.includes(type);
}
values() {
return [
"' OR 1=1 --",
'<script>alert(1)</script>',
'../../etc/passwd',
'${7*7}',
'\x00\x01\x02',
];
}
}
exports.SpecialCharsStrategy = SpecialCharsStrategy;

View File

@@ -0,0 +1,31 @@
"use strict";
/**
* TypeMismatchStrategy — submits wrong data types for the detected field type.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeMismatchStrategy = void 0;
class TypeMismatchStrategy {
constructor() {
this.name = 'TypeMismatchStrategy';
}
appliesTo(type) {
return ['email', 'number', 'date', 'url', 'phone'].includes(type);
}
values(type) {
switch (type) {
case 'email':
return ['not-an-email', '12345', '@@@'];
case 'number':
return ['abc', '-999999', '9.9.9', 'NaN'];
case 'date':
return ['yesterday', '32/13/2025', '0000-00-00'];
case 'url':
return ['javascript:alert(1)', 'not a url'];
case 'phone':
return ['000', '++++', 'abcdefghij'];
default:
return [];
}
}
}
exports.TypeMismatchStrategy = TypeMismatchStrategy;

7
dist/plugins/interfaces.js vendored Normal file
View File

@@ -0,0 +1,7 @@
"use strict";
/**
* Plugin interfaces re-exported from core for plugin implementations to use.
* All interface definitions live in src/core/interfaces.ts so that core
* code can depend on them without creating a circular dependency.
*/
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,59 @@
"use strict";
/**
* PlaywrightReproducer — serializes an action trace and generates a
* deterministic Playwright script for replay.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaywrightReproducer = void 0;
class PlaywrightReproducer {
serialize(trace) {
return JSON.stringify(trace, null, 2);
}
deserialize(raw) {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
throw new Error('PlaywrightReproducer.deserialize: expected a JSON array');
}
return parsed;
}
generateScript(trace) {
const lines = [
'// Auto-generated replay script by ABE (Autonomous Bug Explorer)',
`// Generated at: ${new Date().toISOString()}`,
`// Steps: ${trace.length}`,
'',
"const { chromium } = require('playwright');",
'',
'(async () => {',
' const browser = await chromium.launch({ headless: true });',
' const context = await browser.newContext();',
' const page = await context.newPage();',
'',
];
for (let i = 0; i < trace.length; i++) {
const action = trace[i];
lines.push(` // Step ${i + 1}: ${action.type} (seed=${action.seed})`);
switch (action.type) {
case 'navigate':
lines.push(` await page.goto(${JSON.stringify(action.url)});`);
break;
case 'click':
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().click();`);
break;
case 'fill':
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().fill(${JSON.stringify(action.value ?? '')});`);
break;
case 'select':
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().selectOption(${JSON.stringify(action.value ?? '')});`);
break;
case 'submit':
lines.push(` await page.locator(${JSON.stringify(action.selector)}).first().dispatchEvent('submit');`);
break;
}
lines.push('');
}
lines.push(" console.log('Replay complete');", ' await browser.close();', '})();');
return lines.join('\n');
}
}
exports.PlaywrightReproducer = PlaywrightReproducer;