80 lines
2.5 KiB
JavaScript
80 lines
2.5 KiB
JavaScript
import { DatabaseSync } from 'node:sqlite';
|
|||
|
|
import { mkdirSync } from 'node:fs';
|
||
|
|
|
||
|
|
// Schema for the private incident notebook and the Git fix-candidate inbox.
|
||
|
|
// Deployment-receipt tables live in receipts.mjs, next to the code that
|
||
|
|
// actually owns that data — this file only owns the notebook's own shape.
|
||
|
|
const SCHEMA = `
|
||
|
|
PRAGMA journal_mode=WAL;
|
||
|
|
PRAGMA foreign_keys=ON;
|
||
|
|
CREATE TABLE IF NOT EXISTS incidents(
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
project TEXT,
|
||
|
|
status TEXT NOT NULL DEFAULT 'investigating',
|
||
|
|
severity TEXT NOT NULL DEFAULT 'annoyance',
|
||
|
|
detected_at TEXT NOT NULL,
|
||
|
|
symptom TEXT NOT NULL,
|
||
|
|
impact TEXT DEFAULT '',
|
||
|
|
root_cause TEXT DEFAULT '',
|
||
|
|
confidence TEXT DEFAULT 'unknown',
|
||
|
|
fix TEXT DEFAULT '',
|
||
|
|
verification TEXT DEFAULT '',
|
||
|
|
prevention TEXT DEFAULT '',
|
||
|
|
remaining_risk TEXT DEFAULT '',
|
||
|
|
lesson TEXT DEFAULT '',
|
||
|
|
public_summary TEXT DEFAULT '',
|
||
|
|
created_at TEXT NOT NULL,
|
||
|
|
updated_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
CREATE TABLE IF NOT EXISTS timeline(
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
incident_id TEXT NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
||
|
|
at TEXT NOT NULL,
|
||
|
|
kind TEXT NOT NULL,
|
||
|
|
body TEXT NOT NULL
|
||
|
|
);
|
||
|
|
CREATE TABLE IF NOT EXISTS hypotheses(
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
incident_id TEXT NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
||
|
|
statement TEXT NOT NULL,
|
||
|
|
status TEXT NOT NULL DEFAULT 'untested',
|
||
|
|
evidence TEXT DEFAULT ''
|
||
|
|
);
|
||
|
|
CREATE TABLE IF NOT EXISTS candidates(
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
repo TEXT NOT NULL,
|
||
|
|
sha TEXT NOT NULL,
|
||
|
|
committed_at TEXT NOT NULL,
|
||
|
|
subject TEXT NOT NULL,
|
||
|
|
paths TEXT DEFAULT '',
|
||
|
|
reason TEXT NOT NULL,
|
||
|
|
state TEXT NOT NULL DEFAULT 'new',
|
||
|
|
incident_id TEXT,
|
||
|
|
UNIQUE(repo, sha)
|
||
|
|
);`;
|
||
|
|
|
||
|
|
// Additive, best-effort migrations for columns that shipped after the
|
||
|
|
// initial schema. SQLite has no "ADD COLUMN IF NOT EXISTS", so each one is
|
||
|
|
// tried and a "duplicate column" failure is the expected steady state once
|
||
|
|
// it's already applied everywhere.
|
||
|
|
const MIGRATIONS = [
|
||
|
|
"ALTER TABLE incidents ADD COLUMN published_at TEXT DEFAULT ''",
|
||
|
|
"ALTER TABLE incidents ADD COLUMN public_slug TEXT DEFAULT ''",
|
||
|
|
];
|
||
|
|
|
||
|
|
export function openDb(path = 'data/trace.db') {
|
||
|
|
mkdirSync('data', { recursive: true });
|
||
|
|
mkdirSync('data/exports', { recursive: true });
|
||
|
|
const db = new DatabaseSync(path);
|
||
|
|
db.exec(SCHEMA);
|
||
|
|
for (const sql of MIGRATIONS) {
|
||
|
|
try {
|
||
|
|
db.exec(sql);
|
||
|
|
} catch (error) {
|
||
|
|
if (!String(error.message).includes('duplicate column')) throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return db;
|
||
|
|
}
|