docs: enterprise refactor plan with ralph specs
This commit is contained in:
76
dist/db/AnomalyRepository.js
vendored
Normal file
76
dist/db/AnomalyRepository.js
vendored
Normal 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
82
dist/db/ScheduleRepository.js
vendored
Normal 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
53
dist/db/SessionRepository.js
vendored
Normal 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
77
dist/db/VisualBaselineRepository.js
vendored
Normal 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
43
dist/db/connection.js
vendored
Normal 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
126
dist/db/migrations.js
vendored
Normal 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
|
||||
);
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user