internal/store connects via pgx and runs golang-migrate migrations embedded in the binary (go:embed), so Deklarix stays a single binary despite the move to Postgres. Schema covers the five MVP tables (submission, asset, extraction, finding, evidence_package, participant). extraction, finding and evidence_package are append-only by design: a Postgres trigger rejects UPDATE/DELETE outright, since a corrigible evidence archive isn't an evidence archive. Corrections to a finding are new rows whose supersedes column points at the row they replace (set at INSERT time on the new row, since the trigger blocks UPDATE on the old one) — "currently valid" findings are the ones no other row supersedes. scripts/test.sh now spins up a disposable Postgres container so the store's integration tests (including the append-only guarantee) actually run on every test.sh/release.sh invocation instead of silently skipping for lack of DATABASE_URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
3.4 KiB
PL/PgSQL
90 lines
3.4 KiB
PL/PgSQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
CREATE TABLE submission (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
platform TEXT NOT NULL CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'linkedin')),
|
|
post_type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'checked', 'published', 'archived')),
|
|
caption TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE asset (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL REFERENCES submission (id),
|
|
kind TEXT NOT NULL CHECK (kind IN ('image', 'video', 'file')),
|
|
path TEXT NOT NULL,
|
|
sha256 TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Stufe 1: striktes JSON aus der Claude-Extraktion. Append-only, siehe Trigger unten.
|
|
CREATE TABLE extraction (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL REFERENCES submission (id),
|
|
payload JSONB NOT NULL,
|
|
model_version TEXT NOT NULL,
|
|
prompt_version TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Stufe 2: Ergebnis pro Regel. Append-only, Korrektur = neue Zeile.
|
|
-- supersedes zeigt auf die alte Zeile, die diese Zeile ersetzt (gesetzt
|
|
-- beim INSERT der Korrektur, nie per UPDATE — der Trigger würde das
|
|
-- verbieten). "Aktuell gültig" = Zeilen, auf die kein supersedes zeigt,
|
|
-- siehe Index unten für den Anti-Join.
|
|
CREATE TABLE finding (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL REFERENCES submission (id),
|
|
extraction_id UUID REFERENCES extraction (id),
|
|
rule_id TEXT NOT NULL,
|
|
rule_version INTEGER NOT NULL,
|
|
severity TEXT NOT NULL CHECK (severity IN ('niedrig', 'mittel', 'hoch')),
|
|
message TEXT NOT NULL,
|
|
supersedes UUID REFERENCES finding (id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX finding_supersedes_idx ON finding (supersedes) WHERE supersedes IS NOT NULL;
|
|
|
|
-- Dossier + Beweiskette (Hash, RFC-3161-Token). Append-only.
|
|
CREATE TABLE evidence_package (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL REFERENCES submission (id),
|
|
dossier_path TEXT NOT NULL,
|
|
sha256 TEXT NOT NULL,
|
|
timestamp_token BYTEA NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE participant (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
submission_id UUID NOT NULL REFERENCES submission (id),
|
|
role TEXT NOT NULL CHECK (role IN ('creator', 'agentur', 'marke', 'kanzlei')),
|
|
name TEXT NOT NULL,
|
|
vorgegeben BOOLEAN NOT NULL DEFAULT false,
|
|
freigegeben BOOLEAN NOT NULL DEFAULT false,
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Ein Beweisarchiv, in dem man Zeilen ändern kann, ist kein Beweisarchiv.
|
|
CREATE FUNCTION forbid_update_delete() RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
RAISE EXCEPTION 'append-only table: % on % is not allowed', TG_OP, TG_TABLE_NAME;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER extraction_append_only
|
|
BEFORE UPDATE OR DELETE ON extraction
|
|
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
|
|
|
|
CREATE TRIGGER finding_append_only
|
|
BEFORE UPDATE OR DELETE ON finding
|
|
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
|
|
|
|
CREATE TRIGGER evidence_package_append_only
|
|
BEFORE UPDATE OR DELETE ON evidence_package
|
|
FOR EACH ROW EXECUTE FUNCTION forbid_update_delete();
|