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

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;