"""Add optimistic document review decisions and immutable review audit. Revision ID: 0004_document_review Revises: 0003_document_ingestion Create Date: 2026-07-13 """ from collections.abc import Sequence from alembic import op revision: str = "0004_document_review" down_revision: str | None = "0003_document_ingestion" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.execute( """ ALTER TABLE rag.document_versions ADD COLUMN review_revision integer NOT NULL DEFAULT 0, ADD CONSTRAINT document_versions_review_revision_valid CHECK (review_revision >= 0); """ ) op.execute( """ CREATE TABLE rag.document_review_events ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, document_id uuid NOT NULL, document_version_id uuid NOT NULL, actor_subject text NOT NULL, decision text NOT NULL, reason_code text NOT NULL, previous_revision integer NOT NULL, resulting_revision integer NOT NULL, outbound_manifest_sha256 char(64), embedding_profile_hash char(64), trace_id uuid NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT document_review_events_document_fk FOREIGN KEY (document_id, document_version_id) REFERENCES rag.document_versions (document_id, id) ON DELETE CASCADE, CONSTRAINT document_review_events_actor_nonempty CHECK (btrim(actor_subject) <> '' AND length(actor_subject) <= 200), CONSTRAINT document_review_events_decision_valid CHECK (decision IN ('APPROVE', 'REJECT')), CONSTRAINT document_review_events_reason_valid CHECK (reason_code IN ( 'SYNTHETIC_REVIEW_APPROVED', 'RIGHTS_NOT_VERIFIED', 'CONTENT_QUALITY_REJECTED', 'CLOUD_PROCESSING_REJECTED' )), CONSTRAINT document_review_events_revision_valid CHECK ( previous_revision >= 0 AND resulting_revision = previous_revision + 1 ), CONSTRAINT document_review_events_hashes_valid CHECK ( ( decision = 'APPROVE' AND outbound_manifest_sha256 ~ '^[0-9a-f]{64}$' AND embedding_profile_hash ~ '^[0-9a-f]{64}$' ) OR ( decision = 'REJECT' AND embedding_profile_hash IS NULL ) ), CONSTRAINT document_review_events_version_revision_key UNIQUE (document_version_id, resulting_revision) ); """ ) op.execute( """ CREATE INDEX document_review_events_document_timeline ON rag.document_review_events (document_id, created_at, id); """ ) op.execute( """ CREATE FUNCTION rag.reject_document_review_event_mutation() RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog, rag AS $function$ BEGIN RAISE EXCEPTION 'document review events are append-only' USING ERRCODE = '23514'; END $function$; """ ) op.execute( """ CREATE TRIGGER document_review_events_append_only BEFORE UPDATE OR DELETE ON rag.document_review_events FOR EACH ROW EXECUTE FUNCTION rag.reject_document_review_event_mutation(); """ ) def downgrade() -> None: op.execute( "DROP TRIGGER IF EXISTS document_review_events_append_only ON rag.document_review_events;" ) op.execute("DROP FUNCTION IF EXISTS rag.reject_document_review_event_mutation();") op.execute("DROP TABLE IF EXISTS rag.document_review_events;") op.execute( """ ALTER TABLE rag.document_versions DROP CONSTRAINT IF EXISTS document_versions_review_revision_valid, DROP COLUMN IF EXISTS review_revision; """ )