-- +goose Up -- Document revision ledger: immutable snapshots, Version/Revision, agent candidates, change reports. -- Pointer columns use ledger_*/head_* to avoid colliding with freeform_documents.current_version_id. CREATE TABLE IF NOT EXISTS document_content_blobs ( content_sha256 CHAR(64) PRIMARY KEY, content_markdown TEXT NOT NULL, byte_size BIGINT NOT NULL CHECK (byte_size >= 0), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS document_snapshots ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, content_sha256 CHAR(64) NOT NULL REFERENCES document_content_blobs(content_sha256), parent_snapshot_id BIGINT REFERENCES document_snapshots(id), block_map JSONB NOT NULL DEFAULT '[]'::jsonb, source_kind TEXT NOT NULL, actor_user_id BIGINT, actor_ref TEXT NOT NULL DEFAULT '', actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, source_run_id TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT chk_snapshot_source_kind CHECK (source_kind IN ('template','human','agent','agent_squad','migration','system')) ); CREATE INDEX IF NOT EXISTS idx_document_snapshots_doc ON document_snapshots(doc_id, created_at); CREATE TABLE IF NOT EXISTS document_versions ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, version_no INT NOT NULL, status TEXT NOT NULL, origin_kind TEXT NOT NULL, baseline_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), head_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), human_revision_count INT NOT NULL DEFAULT 0, source_run_id TEXT NOT NULL DEFAULT '', change_report_id BIGINT, opened_at TIMESTAMPTZ NOT NULL DEFAULT now(), sealed_at TIMESTAMPTZ, CONSTRAINT uq_document_versions_doc_no UNIQUE (doc_id, version_no), CONSTRAINT chk_document_versions_status CHECK (status IN ('open','sealed')), CONSTRAINT chk_document_versions_origin CHECK (origin_kind IN ('template','agent','agent_squad','migration')) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_document_versions_one_open ON document_versions(doc_id) WHERE status = 'open'; CREATE TABLE IF NOT EXISTS document_change_events ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, version_id BIGINT NOT NULL REFERENCES document_versions(id), event_no INT NOT NULL, from_snapshot_id BIGINT REFERENCES document_snapshots(id), to_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), change_kind TEXT NOT NULL, actor_kind TEXT NOT NULL, actor_user_id BIGINT, actor_ref TEXT NOT NULL DEFAULT '', actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, change_set JSONB NOT NULL DEFAULT '[]'::jsonb, edit_session_id TEXT NOT NULL DEFAULT '', request_id TEXT NOT NULL DEFAULT '', idempotency_key TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_document_change_events_doc_event UNIQUE (doc_id, event_no), CONSTRAINT uq_document_change_events_idemp UNIQUE (doc_id, idempotency_key), CONSTRAINT chk_change_kind CHECK (change_kind IN ('human_revision','agent_commit','agent_noop','import','restore_as_new')), CONSTRAINT chk_actor_kind CHECK (actor_kind IN ('human','agent','agent_squad','template','migration','system')) ); CREATE TABLE IF NOT EXISTS document_change_reports ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, from_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), to_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), schema_version INT NOT NULL DEFAULT 1, summary JSONB NOT NULL DEFAULT '[]'::jsonb, risk_level TEXT NOT NULL, risk_items JSONB NOT NULL DEFAULT '[]'::jsonb, project_impacts JSONB NOT NULL DEFAULT '{}'::jsonb, document_impacts JSONB NOT NULL DEFAULT '[]'::jsonb, open_questions JSONB NOT NULL DEFAULT '[]'::jsonb, analysis_status TEXT NOT NULL, model_snapshot TEXT NOT NULL DEFAULT '', prompt_version TEXT NOT NULL DEFAULT '', deterministic_facts JSONB NOT NULL DEFAULT '{}'::jsonb, corrects_report_id BIGINT REFERENCES document_change_reports(id), raw_json TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT chk_report_risk CHECK (risk_level IN ('low','medium','high','critical')), CONSTRAINT chk_report_analysis CHECK (analysis_status IN ('complete','fallback','unassessed','corrected')) ); ALTER TABLE document_versions ADD CONSTRAINT fk_document_versions_change_report FOREIGN KEY (change_report_id) REFERENCES document_change_reports(id); CREATE TABLE IF NOT EXISTS document_agent_candidates ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, input_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id), output_snapshot_id BIGINT REFERENCES document_snapshots(id), run_id TEXT NOT NULL, proposal_id TEXT NOT NULL DEFAULT '', status TEXT NOT NULL, authorized_scope TEXT NOT NULL DEFAULT '', quality_result TEXT NOT NULL DEFAULT '', change_report_id BIGINT REFERENCES document_change_reports(id), actor_kind TEXT NOT NULL, actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), decided_at TIMESTAMPTZ, CONSTRAINT uq_document_agent_candidates_run UNIQUE (doc_id, run_id), CONSTRAINT chk_candidate_status CHECK (status IN ('ready','needs_confirmation','committed','rejected','stale','quality_failed')), CONSTRAINT chk_candidate_actor CHECK (actor_kind IN ('agent','agent_squad')) ); ALTER TABLE document_records ADD COLUMN IF NOT EXISTS ledger_version_id BIGINT REFERENCES document_versions(id), ADD COLUMN IF NOT EXISTS head_snapshot_id BIGINT REFERENCES document_snapshots(id), ADD COLUMN IF NOT EXISTS head_revision_no INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS review_snapshot_id BIGINT REFERENCES document_snapshots(id), ADD COLUMN IF NOT EXISTS revision_schema_version INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS row_revision BIGINT NOT NULL DEFAULT 0; -- Rename freeform legacy pointer to avoid semantic collision with ledger version id. ALTER TABLE freeform_documents RENAME COLUMN current_version_id TO legacy_version_id; ALTER TABLE freeform_documents ADD COLUMN IF NOT EXISTS ledger_version_id BIGINT REFERENCES document_versions(id), ADD COLUMN IF NOT EXISTS head_snapshot_id BIGINT REFERENCES document_snapshots(id), ADD COLUMN IF NOT EXISTS head_revision_no INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS revision_schema_version INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS row_revision BIGINT NOT NULL DEFAULT 0; -- Prevent UPDATE/DELETE on immutable snapshot bodies via trigger. -- +goose StatementBegin CREATE OR REPLACE FUNCTION document_snapshots_immutable() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'document_snapshots is immutable'; END; $$ LANGUAGE plpgsql; -- +goose StatementEnd DROP TRIGGER IF EXISTS trg_document_snapshots_immutable ON document_snapshots; CREATE TRIGGER trg_document_snapshots_immutable BEFORE UPDATE OR DELETE ON document_snapshots FOR EACH ROW EXECUTE FUNCTION document_snapshots_immutable(); -- +goose StatementBegin CREATE OR REPLACE FUNCTION document_content_blobs_immutable() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'document_content_blobs is immutable'; END; $$ LANGUAGE plpgsql; -- +goose StatementEnd DROP TRIGGER IF EXISTS trg_document_content_blobs_immutable ON document_content_blobs; CREATE TRIGGER trg_document_content_blobs_immutable BEFORE UPDATE OR DELETE ON document_content_blobs FOR EACH ROW EXECUTE FUNCTION document_content_blobs_immutable(); -- +goose Down DROP TRIGGER IF EXISTS trg_document_content_blobs_immutable ON document_content_blobs; DROP FUNCTION IF EXISTS document_content_blobs_immutable(); DROP TRIGGER IF EXISTS trg_document_snapshots_immutable ON document_snapshots; DROP FUNCTION IF EXISTS document_snapshots_immutable(); ALTER TABLE freeform_documents DROP COLUMN IF EXISTS row_revision, DROP COLUMN IF EXISTS revision_schema_version, DROP COLUMN IF EXISTS head_revision_no, DROP COLUMN IF EXISTS head_snapshot_id, DROP COLUMN IF EXISTS ledger_version_id; ALTER TABLE freeform_documents RENAME COLUMN legacy_version_id TO current_version_id; ALTER TABLE document_records DROP COLUMN IF EXISTS row_revision, DROP COLUMN IF EXISTS revision_schema_version, DROP COLUMN IF EXISTS review_snapshot_id, DROP COLUMN IF EXISTS head_revision_no, DROP COLUMN IF EXISTS head_snapshot_id, DROP COLUMN IF EXISTS ledger_version_id; ALTER TABLE document_versions DROP CONSTRAINT IF EXISTS fk_document_versions_change_report; DROP TABLE IF EXISTS document_agent_candidates; DROP TABLE IF EXISTS document_change_reports; DROP TABLE IF EXISTS document_change_events; DROP TABLE IF EXISTS document_versions; DROP TABLE IF EXISTS document_snapshots; DROP TABLE IF EXISTS document_content_blobs;