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

76
dist/db/AnomalyRepository.js vendored Normal file
View File

@@ -0,0 +1,76 @@
"use strict";
/**
* AnomalyRepository — CRUD for anomalies table with filters.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnomalyRepository = void 0;
function rowToAnomaly(row) {
return {
id: row.id,
sessionId: row.session_id,
type: row.type,
severity: row.severity,
description: row.description,
actionTrace: JSON.parse(row.action_trace_json),
evidence: {
...JSON.parse(row.evidence_json),
screenshotPath: row.screenshot_path ?? undefined,
domSnapshotPath: row.dom_snapshot_path ?? undefined,
},
observationId: '',
timestamp: row.detected_at,
};
}
class AnomalyRepository {
constructor(db) {
this.db = db;
}
create(anomaly, sessionId) {
this.db
.prepare(`INSERT INTO anomalies
(id, session_id, type, severity, description, action_trace_json, evidence_json, screenshot_path, dom_snapshot_path, detected_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(anomaly.id, sessionId, anomaly.type, anomaly.severity, anomaly.description, JSON.stringify(anomaly.actionTrace), JSON.stringify({ httpLog: anomaly.evidence.httpLog, rawErrors: anomaly.evidence.rawErrors }), anomaly.evidence.screenshotPath ?? null, anomaly.evidence.domSnapshotPath ?? null, anomaly.timestamp);
}
findById(id) {
const row = this.db
.prepare('SELECT * FROM anomalies WHERE id = ?')
.get(id);
return row ? rowToAnomaly(row) : undefined;
}
findAll(filters) {
const conditions = [];
const values = [];
if (filters?.sessionId) {
conditions.push('session_id = ?');
values.push(filters.sessionId);
}
if (filters?.severity) {
conditions.push('severity = ?');
values.push(filters.severity);
}
if (filters?.type) {
conditions.push('type = ?');
values.push(filters.type);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const rows = this.db
.prepare(`SELECT * FROM anomalies ${where} ORDER BY detected_at DESC`)
.all(...values);
return rows.map(rowToAnomaly);
}
countBySeverity(severities) {
if (severities.length === 0)
return 0;
const placeholders = severities.map(() => '?').join(', ');
const result = this.db
.prepare(`SELECT COUNT(*) as cnt FROM anomalies WHERE severity IN (${placeholders})`)
.get(...severities);
return result.cnt;
}
count() {
const result = this.db.prepare('SELECT COUNT(*) as cnt FROM anomalies').get();
return result.cnt;
}
}
exports.AnomalyRepository = AnomalyRepository;

82
dist/db/ScheduleRepository.js vendored Normal file
View File

@@ -0,0 +1,82 @@
"use strict";
/**
* ScheduleRepository — CRUD for schedules table.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScheduleRepository = void 0;
function rowToRecord(row) {
return {
id: row.id,
name: row.name,
url: row.url,
configJson: row.config_json,
cronExpression: row.cron_expression,
enabled: row.enabled === 1,
lastRunAt: row.last_run_at,
nextRunAt: row.next_run_at,
createdAt: row.created_at,
};
}
class ScheduleRepository {
constructor(db) {
this.db = db;
}
create(params) {
this.db
.prepare(`INSERT INTO schedules (id, name, url, config_json, cron_expression, enabled, next_run_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run(params.id, params.name, params.url, params.configJson, params.cronExpression, params.enabled !== false ? 1 : 0, params.nextRunAt ?? null, Date.now());
}
findById(id) {
const row = this.db
.prepare('SELECT * FROM schedules WHERE id = ?')
.get(id);
return row ? rowToRecord(row) : undefined;
}
findAll(enabledOnly = false) {
const rows = enabledOnly
? this.db.prepare('SELECT * FROM schedules WHERE enabled = 1 ORDER BY created_at DESC').all()
: this.db.prepare('SELECT * FROM schedules ORDER BY created_at DESC').all();
return rows.map(rowToRecord);
}
update(id, fields) {
const sets = [];
const values = [];
if (fields.name !== undefined) {
sets.push('name = ?');
values.push(fields.name);
}
if (fields.url !== undefined) {
sets.push('url = ?');
values.push(fields.url);
}
if (fields.configJson !== undefined) {
sets.push('config_json = ?');
values.push(fields.configJson);
}
if (fields.cronExpression !== undefined) {
sets.push('cron_expression = ?');
values.push(fields.cronExpression);
}
if (fields.enabled !== undefined) {
sets.push('enabled = ?');
values.push(fields.enabled ? 1 : 0);
}
if (fields.lastRunAt !== undefined) {
sets.push('last_run_at = ?');
values.push(fields.lastRunAt);
}
if (fields.nextRunAt !== undefined) {
sets.push('next_run_at = ?');
values.push(fields.nextRunAt);
}
if (sets.length === 0)
return;
values.push(id);
this.db.prepare(`UPDATE schedules SET ${sets.join(', ')} WHERE id = ?`).run(...values);
}
delete(id) {
this.db.prepare('DELETE FROM schedules WHERE id = ?').run(id);
}
}
exports.ScheduleRepository = ScheduleRepository;

53
dist/db/SessionRepository.js vendored Normal file
View File

@@ -0,0 +1,53 @@
"use strict";
/**
* SessionRepository — CRUD for sessions table.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionRepository = void 0;
class SessionRepository {
constructor(db) {
this.db = db;
}
create(params) {
this.db
.prepare(`INSERT INTO sessions (id, url, status, seed, max_states, started_at, config_json)
VALUES (?, ?, 'running', ?, ?, ?, ?)`)
.run(params.id, params.url, params.seed, params.maxStates, params.startedAt, params.configJson ?? '{}');
}
findById(id) {
return this.db
.prepare('SELECT * FROM sessions WHERE id = ?')
.get(id);
}
findAll() {
return this.db.prepare('SELECT * FROM sessions ORDER BY started_at DESC').all();
}
update(id, fields) {
const sets = [];
const values = [];
if (fields.status !== undefined) {
sets.push('status = ?');
values.push(fields.status);
}
if (fields.statesVisited !== undefined) {
sets.push('states_visited = ?');
values.push(fields.statesVisited);
}
if (fields.anomaliesFound !== undefined) {
sets.push('anomalies_found = ?');
values.push(fields.anomaliesFound);
}
if (fields.finishedAt !== undefined) {
sets.push('finished_at = ?');
values.push(fields.finishedAt);
}
if (sets.length === 0)
return;
values.push(id);
this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...values);
}
delete(id) {
this.db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
}
}
exports.SessionRepository = SessionRepository;

77
dist/db/VisualBaselineRepository.js vendored Normal file
View File

@@ -0,0 +1,77 @@
"use strict";
/**
* VisualBaselineRepository — CRUD for visual_baselines and visual_comparisons tables.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisualBaselineRepository = void 0;
class VisualBaselineRepository {
constructor(db) {
this.db = db;
}
// ─── Baselines ────────────────────────────────────────────────────────────
createBaseline(params) {
this.db.prepare(`
INSERT OR REPLACE INTO visual_baselines (id, state_id, url, screenshot_path, approved_at, approved_by, width, height)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(params.id, params.stateId, params.url, params.screenshotPath, Date.now(), params.approvedBy ?? 'user', params.width, params.height);
}
findBaselineByStateId(stateId) {
return this.db
.prepare('SELECT * FROM visual_baselines WHERE state_id = ? ORDER BY approved_at DESC LIMIT 1')
.get(stateId);
}
findBaselineById(id) {
return this.db
.prepare('SELECT * FROM visual_baselines WHERE id = ?')
.get(id);
}
// ─── Comparisons ──────────────────────────────────────────────────────────
createComparison(params) {
this.db.prepare(`
INSERT INTO visual_comparisons
(id, session_id, state_id, baseline_id, current_screenshot_path, diff_screenshot_path, diff_pixels, diff_percent, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(params.id, params.sessionId, params.stateId, params.baselineId ?? null, params.currentScreenshotPath, params.diffScreenshotPath ?? null, params.diffPixels ?? null, params.diffPercent ?? null, params.status, Date.now());
}
findComparisonById(id) {
return this.db
.prepare('SELECT * FROM visual_comparisons WHERE id = ?')
.get(id);
}
findComparisons(filters) {
const conditions = [];
const values = [];
if (filters?.sessionId) {
conditions.push('session_id = ?');
values.push(filters.sessionId);
}
if (filters?.status) {
conditions.push('status = ?');
values.push(filters.status);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
return this.db
.prepare(`SELECT * FROM visual_comparisons ${where} ORDER BY created_at DESC`)
.all(...values);
}
updateComparisonStatus(id, status) {
this.db.prepare('UPDATE visual_comparisons SET status = ? WHERE id = ?').run(status, id);
}
promoteToBaseline(comparisonId) {
const comparison = this.findComparisonById(comparisonId);
if (!comparison)
return null;
const baselineId = `baseline_${Date.now()}`;
this.createBaseline({
id: baselineId,
stateId: comparison.state_id,
url: comparison.session_id,
screenshotPath: comparison.current_screenshot_path,
width: 1280,
height: 720,
});
this.updateComparisonStatus(comparisonId, 'passed');
return baselineId;
}
}
exports.VisualBaselineRepository = VisualBaselineRepository;

43
dist/db/connection.js vendored Normal file
View File

@@ -0,0 +1,43 @@
"use strict";
/**
* ABE Database Connection
* Singleton SQLite connection using better-sqlite3.
* Runs migrations on first access.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDb = getDb;
exports.setDb = setDb;
exports.closeDb = closeDb;
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const migrations_1 = require("./migrations");
let _db = null;
function getDb() {
if (_db)
return _db;
const dbPath = process.env['ABE_DB_PATH'] ?? path_1.default.join(process.cwd(), 'data', 'abe.db');
const dir = path_1.default.dirname(dbPath);
if (!fs_1.default.existsSync(dir)) {
fs_1.default.mkdirSync(dir, { recursive: true });
}
_db = new better_sqlite3_1.default(dbPath);
_db.pragma('journal_mode = WAL');
_db.pragma('foreign_keys = ON');
(0, migrations_1.runMigrations)(_db);
return _db;
}
/** For testing — inject a custom (in-memory) database instance. */
function setDb(db) {
_db = db;
}
/** Close and reset. Used in tests. */
function closeDb() {
if (_db) {
_db.close();
_db = null;
}
}

126
dist/db/migrations.js vendored Normal file
View File

@@ -0,0 +1,126 @@
"use strict";
/**
* ABE Database Migrations
* Creates all tables if they do not exist.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.runMigrations = runMigrations;
function runMigrations(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running',
seed INTEGER NOT NULL,
max_states INTEGER NOT NULL DEFAULT 50,
states_visited INTEGER NOT NULL DEFAULT 0,
anomalies_found INTEGER NOT NULL DEFAULT 0,
started_at INTEGER NOT NULL,
finished_at INTEGER,
config_json TEXT NOT NULL DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS states (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
url TEXT NOT NULL,
title TEXT NOT NULL,
dom_snapshot_path TEXT,
visit_count INTEGER NOT NULL DEFAULT 0,
discovered_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS actions (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
state_id TEXT NOT NULL REFERENCES states(id),
type TEXT NOT NULL,
selector TEXT,
value TEXT,
url TEXT,
seed INTEGER NOT NULL,
executed_at INTEGER NOT NULL,
sequence_order INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS anomalies (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
type TEXT NOT NULL,
severity TEXT NOT NULL,
description TEXT NOT NULL,
action_trace_json TEXT NOT NULL,
evidence_json TEXT NOT NULL,
screenshot_path TEXT,
dom_snapshot_path TEXT,
detected_at INTEGER NOT NULL,
ai_enrichment_json TEXT,
ai_enriched_at INTEGER,
browser TEXT,
browser_version TEXT
);
CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
anomaly_id TEXT NOT NULL REFERENCES anomalies(id),
channel TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
sent_at INTEGER,
error TEXT
);
CREATE TABLE IF NOT EXISTS schedules (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
config_json TEXT NOT NULL,
cron_expression TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
last_run_at INTEGER,
next_run_at INTEGER,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS visual_baselines (
id TEXT PRIMARY KEY,
state_id TEXT NOT NULL,
url TEXT NOT NULL,
screenshot_path TEXT NOT NULL,
approved_at INTEGER NOT NULL,
approved_by TEXT DEFAULT 'user',
width INTEGER NOT NULL,
height INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS visual_comparisons (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
state_id TEXT NOT NULL,
baseline_id TEXT,
current_screenshot_path TEXT NOT NULL,
diff_screenshot_path TEXT,
diff_pixels INTEGER,
diff_percent REAL,
status TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS performance_metrics (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
state_id TEXT NOT NULL,
url TEXT NOT NULL,
ttfb INTEGER,
dom_content_loaded INTEGER,
load_complete INTEGER,
lcp INTEGER,
cls REAL,
fid INTEGER,
inp INTEGER,
total_requests INTEGER,
failed_requests INTEGER,
total_transfer_size INTEGER,
captured_at INTEGER NOT NULL
);
`);
}