-- G8 Autonomous Content & Continuous Improvement OS. -- Only synthetic replay/red-team/coverage-drift metadata is accepted. Every decision and -- lifecycle transition is immutable; qualification never silently promotes a candidate. CREATE TABLE IF NOT EXISTS app.ci_ingestion_submission ( submission_id UUID PRIMARY KEY, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), operation_kind TEXT NOT NULL CHECK (operation_kind IN ( 'content_pipeline','model_change_gate','release_gate','incident_dag', 'human_approval','monitor_event' )), result_id UUID NOT NULL, data_classification TEXT NOT NULL DEFAULT 'synthetic_replay_red_team_coverage_drift' CHECK (data_classification = 'synthetic_replay_red_team_coverage_drift'), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.ci_source_artifact ( source_record_id UUID PRIMARY KEY, source_id TEXT NOT NULL CHECK (source_id ~ '^oas-g8-source-[a-z0-9-]+$'), source_version TEXT NOT NULL CHECK (length(btrim(source_version)) > 0), content_sha256 TEXT NOT NULL CHECK (content_sha256 ~ '^[a-f0-9]{64}$'), provenance_uri TEXT NOT NULL CHECK (provenance_uri ~ '^(repo|db|audit)://[a-zA-Z0-9_./:-]+$'), usage_status TEXT NOT NULL CHECK (usage_status IN ('approved','restricted','rejected')), citation_label TEXT NOT NULL CHECK (length(btrim(citation_label)) > 0), content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (source_id, source_version) ); -- Mutable scheduler control-plane. Source/configuration fields are immutable after -- enqueue; only lease/retry/result state may change. It never represents approval. CREATE TABLE IF NOT EXISTS app.ci_agentic_job ( job_id UUID PRIMARY KEY, job_key TEXT NOT NULL UNIQUE CHECK (job_key ~ '^oas-g8-job-[a-z0-9-]+$'), source_packs JSONB NOT NULL CHECK ( jsonb_typeof(source_packs) = 'array' AND jsonb_array_length(source_packs) >= 1 ), source_fingerprint TEXT NOT NULL CHECK (source_fingerprint ~ '^[a-f0-9]{64}$'), data_classification TEXT NOT NULL CHECK (data_classification = 'synthetic_replay_red_team_coverage_drift'), content_kind TEXT NOT NULL CHECK (content_kind IN ('case','rupture','practice','benchmark')), difficulty_level INT NOT NULL CHECK (difficulty_level BETWEEN 1 AND 5), variant_count INT NOT NULL CHECK (variant_count BETWEEN 3 AND 12), prompt_version TEXT NOT NULL CHECK (length(btrim(prompt_version)) > 0), trigger_kind TEXT NOT NULL CHECK (trigger_kind IN ('scheduled_repo_source','scheduled_incident')), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','processing','retry_wait','rejected','completed')), attempt_count INT NOT NULL DEFAULT 0 CHECK (attempt_count >= 0), next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(), lease_started_at TIMESTAMPTZ, last_error_code TEXT, last_error_message TEXT CHECK ( last_error_message IS NULL OR length(last_error_message) <= 500 ), result_submission_id UUID REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, result_qualification_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, CHECK ( (status = 'completed') = (result_submission_id IS NOT NULL AND result_qualification_id IS NOT NULL AND completed_at IS NOT NULL) ) ); CREATE INDEX IF NOT EXISTS idx_ci_agentic_job_ready ON app.ci_agentic_job(status, next_attempt_at, created_at); CREATE TABLE IF NOT EXISTS app.ci_content_pipeline ( pipeline_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, draft_id TEXT NOT NULL UNIQUE CHECK (draft_id ~ '^oas-g8-draft-[a-z0-9-]+$'), content_kind TEXT NOT NULL CHECK (content_kind IN ('case','rupture','practice','benchmark')), source_record_ids UUID[] NOT NULL CHECK (cardinality(source_record_ids) >= 1), generation_model TEXT NOT NULL CHECK (length(btrim(generation_model)) > 0), prompt_version TEXT NOT NULL CHECK (length(btrim(prompt_version)) > 0), prompt_sha256 TEXT NOT NULL CHECK (prompt_sha256 ~ '^[a-f0-9]{64}$'), payload_sha256 TEXT NOT NULL CHECK (payload_sha256 ~ '^[a-f0-9]{64}$'), draft_payload JSONB, synthetic_identity_id TEXT NOT NULL CHECK (synthetic_identity_id ~ '^synthetic-identity-[a-z0-9-]+$'), difficulty_level INT NOT NULL CHECK (difficulty_level BETWEEN 1 AND 5), hidden_answer_fingerprint TEXT NOT NULL CHECK (hidden_answer_fingerprint ~ '^[a-f0-9]{64}$'), visible_answer_overlap_tokens INT NOT NULL CHECK (visible_answer_overlap_tokens = 0), pii_findings INT NOT NULL CHECK (pii_findings = 0), unsupported_clinical_claims INT NOT NULL CHECK (unsupported_clinical_claims = 0), state TEXT NOT NULL DEFAULT 'pending_human_approval' CHECK (state = 'pending_human_approval'), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE app.ci_content_pipeline ADD COLUMN IF NOT EXISTS draft_payload JSONB; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'ci_content_pipeline_draft_payload_object' AND conrelid = 'app.ci_content_pipeline'::regclass ) THEN ALTER TABLE app.ci_content_pipeline ADD CONSTRAINT ci_content_pipeline_draft_payload_object CHECK (draft_payload IS NULL OR jsonb_typeof(draft_payload) = 'object'); END IF; END; $$; CREATE TABLE IF NOT EXISTS app.ci_red_team_review ( review_record_id UUID PRIMARY KEY, pipeline_id UUID NOT NULL REFERENCES app.ci_content_pipeline(pipeline_id) ON DELETE RESTRICT, review_id TEXT NOT NULL UNIQUE CHECK (review_id ~ '^oas-g8-review-[a-z0-9-]+$'), reviewer_agent_id TEXT NOT NULL CHECK (length(btrim(reviewer_agent_id)) > 0), dimensions TEXT[] NOT NULL CHECK ( cardinality(dimensions) >= 3 AND dimensions <@ ARRAY['safety','identity','answer_leakage','cultural_bias','difficulty','pii','grounding']::TEXT[] ), reviewed_payload_sha256 TEXT NOT NULL CHECK (reviewed_payload_sha256 ~ '^[a-f0-9]{64}$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (pipeline_id, reviewer_agent_id) ); CREATE TABLE IF NOT EXISTS app.ci_red_team_finding ( finding_record_id UUID PRIMARY KEY, review_record_id UUID NOT NULL REFERENCES app.ci_red_team_review(review_record_id) ON DELETE RESTRICT, finding_id TEXT NOT NULL UNIQUE CHECK (finding_id ~ '^oas-g8-finding-[a-z0-9-]+$'), dimension TEXT NOT NULL CHECK (dimension IN ( 'safety','identity','answer_leakage','cultural_bias','difficulty','pii','grounding' )), severity TEXT NOT NULL CHECK (severity IN ('blocker','high','moderate','low')), finding_state TEXT NOT NULL CHECK (finding_state IN ('open','resolved','accepted_risk')), evidence_ref TEXT NOT NULL CHECK (length(btrim(evidence_ref)) > 0), remediation_ref TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (finding_state <> 'resolved' OR length(btrim(remediation_ref)) > 0), CHECK (finding_state <> 'accepted_risk' OR severity NOT IN ('blocker','high')) ); CREATE TABLE IF NOT EXISTS app.ci_content_benchmark ( benchmark_record_id UUID PRIMARY KEY, pipeline_id UUID NOT NULL UNIQUE REFERENCES app.ci_content_pipeline(pipeline_id) ON DELETE RESTRICT, benchmark_id TEXT NOT NULL UNIQUE CHECK (benchmark_id ~ '^oas-g8-benchmark-[a-z0-9-]+$'), variant_count INT NOT NULL CHECK (variant_count >= 3), variant_pass_rate DOUBLE PRECISION NOT NULL CHECK (variant_pass_rate BETWEEN 0 AND 1), answer_leakage_count INT NOT NULL CHECK (answer_leakage_count = 0), pii_finding_count INT NOT NULL CHECK (pii_finding_count = 0), unsupported_claim_count INT NOT NULL CHECK (unsupported_claim_count = 0), safety_failure_count INT NOT NULL CHECK (safety_failure_count = 0), reward_hacking_count INT NOT NULL CHECK (reward_hacking_count = 0), evidence_refs TEXT[] NOT NULL CHECK (cardinality(evidence_refs) >= 1), qualified BOOLEAN NOT NULL CHECK (qualified = (variant_pass_rate >= 0.85)), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.ci_content_qualification ( qualification_id UUID PRIMARY KEY, pipeline_id UUID NOT NULL UNIQUE REFERENCES app.ci_content_pipeline(pipeline_id) ON DELETE RESTRICT, benchmark_record_id UUID NOT NULL UNIQUE REFERENCES app.ci_content_benchmark(benchmark_record_id) ON DELETE RESTRICT, catalog_entry_id TEXT NOT NULL UNIQUE CHECK (catalog_entry_id ~ '^oas-g8-catalog-[a-z0-9-]+$'), payload_sha256 TEXT NOT NULL CHECK (payload_sha256 ~ '^[a-f0-9]{64}$'), source_record_ids UUID[] NOT NULL CHECK (cardinality(source_record_ids) >= 1), review_record_ids UUID[] NOT NULL CHECK (cardinality(review_record_ids) >= 2), gate_state TEXT NOT NULL DEFAULT 'pending_human_approval' CHECK (gate_state = 'pending_human_approval'), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.ci_gate_artifact ( artifact_record_id UUID PRIMARY KEY, owner_kind TEXT NOT NULL CHECK (owner_kind IN ('model_change_gate','release_gate')), owner_id UUID NOT NULL, artifact_kind TEXT NOT NULL CHECK (artifact_kind IN ('baseline','threshold','provenance','rollback')), artifact_id TEXT NOT NULL CHECK (length(btrim(artifact_id)) > 0), content_sha256 TEXT NOT NULL CHECK (content_sha256 ~ '^[a-f0-9]{64}$'), provenance_uri TEXT NOT NULL CHECK (provenance_uri ~ '^(repo|db|audit)://[a-zA-Z0-9_./:-]+$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (owner_kind, owner_id, artifact_kind, artifact_id) ); CREATE TABLE IF NOT EXISTS app.ci_model_calibration_snapshot ( snapshot_record_id UUID PRIMARY KEY, gate_id UUID NOT NULL, snapshot_role TEXT NOT NULL CHECK (snapshot_role IN ('baseline','candidate')), snapshot_id TEXT NOT NULL CHECK (snapshot_id ~ '^oas-g8-model-snapshot-[a-z0-9-]+$'), model_name TEXT NOT NULL CHECK (length(btrim(model_name)) > 0), prompt_version TEXT NOT NULL CHECK (length(btrim(prompt_version)) > 0), benchmark_version TEXT NOT NULL CHECK (length(btrim(benchmark_version)) > 0), task_accuracy DOUBLE PRECISION NOT NULL CHECK (task_accuracy BETWEEN 0 AND 1), critical_miss_count INT NOT NULL CHECK (critical_miss_count >= 0), leakage_count INT NOT NULL CHECK (leakage_count >= 0), pii_count INT NOT NULL CHECK (pii_count >= 0), calibration_error DOUBLE PRECISION NOT NULL CHECK (calibration_error BETWEEN 0 AND 1), subgroup_max_gap DOUBLE PRECISION NOT NULL CHECK (subgroup_max_gap BETWEEN 0 AND 1), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (gate_id, snapshot_role), UNIQUE (gate_id, snapshot_id) ); CREATE TABLE IF NOT EXISTS app.ci_model_change_gate ( gate_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, baseline_snapshot_record_id UUID NOT NULL UNIQUE REFERENCES app.ci_model_calibration_snapshot(snapshot_record_id) ON DELETE RESTRICT, candidate_snapshot_record_id UUID NOT NULL UNIQUE REFERENCES app.ci_model_calibration_snapshot(snapshot_record_id) ON DELETE RESTRICT, baseline_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, threshold_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, provenance_artifact_ids UUID[] NOT NULL CHECK (cardinality(provenance_artifact_ids) >= 1), rollback_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, gate_decision TEXT NOT NULL CHECK (gate_decision IN ('promote','rollback','quarantine')), reasons TEXT[] NOT NULL CHECK (cardinality(reasons) >= 1), rollback_target_snapshot_id TEXT, state TEXT NOT NULL DEFAULT 'pending_human_approval' CHECK (state = 'pending_human_approval'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (baseline_snapshot_record_id <> candidate_snapshot_record_id), CHECK ((gate_decision = 'rollback') = (rollback_target_snapshot_id IS NOT NULL)) ); CREATE TABLE IF NOT EXISTS app.ci_release_gate ( gate_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, release_id TEXT NOT NULL UNIQUE CHECK (release_id ~ '^oas-g8-release-[a-z0-9-]+$'), red_green_passed BOOLEAN NOT NULL, contract_passed BOOLEAN NOT NULL, e2e_passed BOOLEAN NOT NULL, runtime_proof_passed BOOLEAN NOT NULL, public_proof_passed BOOLEAN NOT NULL, ssot_synced BOOLEAN NOT NULL, evidence_refs TEXT[] NOT NULL, baseline_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, threshold_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, provenance_artifact_ids UUID[] NOT NULL CHECK (cardinality(provenance_artifact_ids) >= 1), rollback_artifact_id UUID NOT NULL REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, qualified BOOLEAN NOT NULL, state TEXT NOT NULL DEFAULT 'pending_human_approval' CHECK (state = 'pending_human_approval'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (qualified = ( red_green_passed AND contract_passed AND e2e_passed AND runtime_proof_passed AND public_proof_passed AND ssot_synced AND cardinality(evidence_refs) >= 1 )) ); CREATE TABLE IF NOT EXISTS audit.ci_human_approval_event ( approval_event_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, target_kind TEXT NOT NULL CHECK (target_kind IN ('content_qualification','model_change_gate','release_gate')), target_id UUID NOT NULL, decision TEXT NOT NULL CHECK (decision IN ( 'approve_content','approve_promotion','authorize_rollback','reject','keep_quarantine' )), actor_uid UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, reason_code TEXT NOT NULL CHECK (length(btrim(reason_code)) > 0), evidence_refs TEXT[] NOT NULL CHECK (cardinality(evidence_refs) >= 1), content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (target_kind, target_id) ); CREATE TABLE IF NOT EXISTS app.ci_catalog_entry ( catalog_record_id UUID PRIMARY KEY, qualification_id UUID NOT NULL UNIQUE REFERENCES app.ci_content_qualification(qualification_id) ON DELETE RESTRICT, approval_event_id UUID NOT NULL UNIQUE REFERENCES audit.ci_human_approval_event(approval_event_id) ON DELETE RESTRICT, catalog_entry_id TEXT NOT NULL UNIQUE CHECK (catalog_entry_id ~ '^oas-g8-catalog-[a-z0-9-]+$'), payload_sha256 TEXT NOT NULL CHECK (payload_sha256 ~ '^[a-f0-9]{64}$'), source_record_ids UUID[] NOT NULL CHECK (cardinality(source_record_ids) >= 1), review_record_ids UUID[] NOT NULL CHECK (cardinality(review_record_ids) >= 2), benchmark_record_id UUID NOT NULL REFERENCES app.ci_content_benchmark(benchmark_record_id) ON DELETE RESTRICT, status TEXT NOT NULL DEFAULT 'approved' CHECK (status = 'approved'), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS audit.ci_lifecycle_event ( lifecycle_event_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, target_kind TEXT NOT NULL CHECK (target_kind IN ('model_change_gate','release_gate')), target_id UUID NOT NULL, event_type TEXT NOT NULL CHECK (event_type IN ('promotion','rollback','monitor')), event_status TEXT NOT NULL CHECK (event_status IN ( 'approved','requested','executed','failed','healthy','drift_detected', 'rollback_recommended','rollback_verified' )), approval_event_id UUID REFERENCES audit.ci_human_approval_event(approval_event_id) ON DELETE RESTRICT, artifact_record_id UUID REFERENCES app.ci_gate_artifact(artifact_record_id) ON DELETE RESTRICT, evidence_refs TEXT[] NOT NULL CHECK (cardinality(evidence_refs) >= 1), content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), executor_receipt_id TEXT, executor_evidence_refs TEXT[], created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK ( (event_type = 'monitor' AND approval_event_id IS NULL) OR (event_type IN ('promotion','rollback') AND approval_event_id IS NOT NULL) ), CHECK (event_type <> 'rollback' OR artifact_record_id IS NOT NULL) ); -- Idempotent upgrade for databases that already created the G8 ledger. NOT VALID -- preserves legacy rows while still enforcing these fail-closed rules on every new row. ALTER TABLE audit.ci_lifecycle_event ADD COLUMN IF NOT EXISTS executor_receipt_id TEXT, ADD COLUMN IF NOT EXISTS executor_evidence_refs TEXT[]; ALTER TABLE audit.ci_lifecycle_event DROP CONSTRAINT IF EXISTS ci_lifecycle_event_event_status_check; ALTER TABLE audit.ci_lifecycle_event ADD CONSTRAINT ci_lifecycle_event_event_status_check CHECK (event_status IN ( 'approved','requested','executed','failed','healthy','drift_detected', 'rollback_recommended','rollback_verified' )); ALTER TABLE audit.ci_lifecycle_event DROP CONSTRAINT IF EXISTS ci_lifecycle_event_status_by_type_check; ALTER TABLE audit.ci_lifecycle_event ADD CONSTRAINT ci_lifecycle_event_status_by_type_check CHECK ( (event_type = 'promotion' AND event_status = 'approved') OR (event_type = 'rollback' AND event_status IN ('requested','executed','failed')) OR (event_type = 'monitor' AND event_status IN ( 'healthy','drift_detected','rollback_recommended','rollback_verified' )) ) NOT VALID; ALTER TABLE audit.ci_lifecycle_event DROP CONSTRAINT IF EXISTS ci_lifecycle_event_rollback_receipt_check; ALTER TABLE audit.ci_lifecycle_event ADD CONSTRAINT ci_lifecycle_event_rollback_receipt_check CHECK ( event_type <> 'rollback' OR ( event_status = 'executed' AND length(btrim(executor_receipt_id)) > 0 AND cardinality(executor_evidence_refs) >= 1 ) OR ( event_status IN ('requested','failed') AND executor_receipt_id IS NULL AND executor_evidence_refs IS NULL ) ) NOT VALID; CREATE TABLE IF NOT EXISTS app.ci_operational_incident ( incident_record_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE REFERENCES app.ci_ingestion_submission(submission_id) ON DELETE RESTRICT, incident_id TEXT NOT NULL UNIQUE CHECK (incident_id ~ '^oas-g8-incident-[a-z0-9-]+$'), error_fingerprint TEXT NOT NULL CHECK (error_fingerprint ~ '^[a-f0-9]{64}$'), affected_contract TEXT NOT NULL CHECK (length(btrim(affected_contract)) > 0), evidence_refs TEXT[] NOT NULL CHECK (cardinality(evidence_refs) >= 1), pii_included BOOLEAN NOT NULL DEFAULT FALSE CHECK (pii_included = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.ci_regression_dag_node ( node_record_id UUID PRIMARY KEY, incident_record_id UUID NOT NULL REFERENCES app.ci_operational_incident(incident_record_id) ON DELETE RESTRICT, node_id TEXT NOT NULL UNIQUE CHECK (node_id ~ '^oas-g8-node-[a-z0-9-]+$'), node_type TEXT NOT NULL CHECK (node_type IN ('reproduction_test','implementation','e2e','runtime_proof')), depends_on_record_ids UUID[] NOT NULL DEFAULT '{}', evidence_ref TEXT, node_status TEXT NOT NULL CHECK (node_status IN ('pending','passed','failed')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (incident_record_id, node_type) ); CREATE OR REPLACE FUNCTION audit.enforce_ci_uuid_array() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE ids UUID[]; DECLARE matched INT; DECLARE expected_sources UUID[]; DECLARE expected_payload TEXT; DECLARE benchmark_ready BOOLEAN; DECLARE covered_dimensions TEXT[]; BEGIN IF TG_TABLE_NAME = 'ci_content_pipeline' THEN ids := NEW.source_record_ids; SELECT count(DISTINCT source_record_id) INTO matched FROM app.ci_source_artifact WHERE source_record_id = ANY(ids) AND usage_status = 'approved'; ELSIF TG_TABLE_NAME = 'ci_content_qualification' THEN SELECT p.source_record_ids, p.payload_sha256 INTO expected_sources, expected_payload FROM app.ci_content_pipeline p WHERE p.pipeline_id = NEW.pipeline_id; IF expected_sources IS NULL OR NEW.source_record_ids IS DISTINCT FROM expected_sources OR NEW.payload_sha256 IS DISTINCT FROM expected_payload THEN RAISE EXCEPTION 'qualification does not match immutable draft provenance' USING ERRCODE = '23514'; END IF; SELECT count(DISTINCT source_record_id) INTO matched FROM app.ci_source_artifact WHERE source_record_id = ANY(NEW.source_record_ids) AND usage_status = 'approved'; IF matched <> cardinality(ARRAY(SELECT DISTINCT unnest(NEW.source_record_ids))) THEN RAISE EXCEPTION 'qualification source UUIDs are missing or not approved' USING ERRCODE = '23514'; END IF; SELECT count(DISTINCT review_record_id) INTO matched FROM app.ci_red_team_review WHERE review_record_id = ANY(NEW.review_record_ids) AND pipeline_id = NEW.pipeline_id; IF matched <> cardinality(ARRAY(SELECT DISTINCT unnest(NEW.review_record_ids))) THEN RAISE EXCEPTION 'qualification review UUIDs are missing or cross-pipeline' USING ERRCODE = '23514'; END IF; SELECT array_agg(DISTINCT dimension) INTO covered_dimensions FROM app.ci_red_team_review r CROSS JOIN LATERAL unnest(r.dimensions) AS dims(dimension) WHERE r.review_record_id = ANY(NEW.review_record_ids); IF NOT ARRAY['safety','identity','answer_leakage','cultural_bias','difficulty','pii','grounding']::TEXT[] <@ COALESCE(covered_dimensions, '{}'::TEXT[]) THEN RAISE EXCEPTION 'qualification red-team coverage is incomplete' USING ERRCODE = '23514'; END IF; IF EXISTS ( SELECT 1 FROM app.ci_red_team_finding f WHERE f.review_record_id = ANY(NEW.review_record_ids) AND f.finding_state = 'open' ) THEN RAISE EXCEPTION 'qualification has unresolved red-team findings' USING ERRCODE = '23514'; END IF; SELECT b.qualified INTO benchmark_ready FROM app.ci_content_benchmark b WHERE b.benchmark_record_id = NEW.benchmark_record_id AND b.pipeline_id = NEW.pipeline_id; IF benchmark_ready IS DISTINCT FROM TRUE THEN RAISE EXCEPTION 'qualification benchmark is missing, cross-pipeline, or failed' USING ERRCODE = '23514'; END IF; RETURN NEW; ELSIF TG_TABLE_NAME = 'ci_model_change_gate' OR TG_TABLE_NAME = 'ci_release_gate' THEN ids := ARRAY[NEW.baseline_artifact_id,NEW.threshold_artifact_id,NEW.rollback_artifact_id] || NEW.provenance_artifact_ids; SELECT count(DISTINCT artifact_record_id) INTO matched FROM app.ci_gate_artifact WHERE artifact_record_id = ANY(ids) AND owner_kind = replace(TG_TABLE_NAME::TEXT, 'ci_', '') AND owner_id = NEW.gate_id; IF matched <> cardinality(ARRAY(SELECT DISTINCT unnest(ids))) THEN RAISE EXCEPTION 'release gate artifacts are missing or assigned to another gate' USING ERRCODE = '23514'; END IF; IF NOT EXISTS (SELECT 1 FROM app.ci_gate_artifact WHERE artifact_record_id = NEW.baseline_artifact_id AND artifact_kind = 'baseline') OR NOT EXISTS (SELECT 1 FROM app.ci_gate_artifact WHERE artifact_record_id = NEW.threshold_artifact_id AND artifact_kind = 'threshold') OR NOT EXISTS (SELECT 1 FROM app.ci_gate_artifact WHERE artifact_record_id = NEW.rollback_artifact_id AND artifact_kind = 'rollback') OR EXISTS (SELECT 1 FROM app.ci_gate_artifact WHERE artifact_record_id = ANY(NEW.provenance_artifact_ids) AND artifact_kind <> 'provenance') THEN RAISE EXCEPTION 'baseline/threshold/provenance/rollback artifact kinds are incomplete' USING ERRCODE = '23514'; END IF; RETURN NEW; ELSIF TG_TABLE_NAME = 'ci_regression_dag_node' THEN ids := NEW.depends_on_record_ids; IF cardinality(ids) = 0 THEN RETURN NEW; END IF; SELECT count(DISTINCT node_record_id) INTO matched FROM app.ci_regression_dag_node WHERE node_record_id = ANY(ids) AND incident_record_id = NEW.incident_record_id; ELSE RETURN NEW; END IF; IF matched <> cardinality(ARRAY(SELECT DISTINCT unnest(ids))) THEN RAISE EXCEPTION 'UUID provenance array contains missing or cross-owner records' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_review_contract() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE reviewed_dimensions TEXT[]; BEGIN IF TG_TABLE_NAME = 'ci_red_team_review' THEN IF NOT EXISTS (SELECT 1 FROM app.ci_content_pipeline p WHERE p.pipeline_id = NEW.pipeline_id AND p.payload_sha256 = NEW.reviewed_payload_sha256) THEN RAISE EXCEPTION 'red-team payload hash does not match draft' USING ERRCODE = '23514'; END IF; ELSE SELECT dimensions INTO reviewed_dimensions FROM app.ci_red_team_review WHERE review_record_id = NEW.review_record_id; IF reviewed_dimensions IS NULL OR NOT NEW.dimension = ANY(reviewed_dimensions) THEN RAISE EXCEPTION 'finding dimension was not reviewed' USING ERRCODE = '23514'; END IF; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_human_approval() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE expected_decision TEXT; DECLARE qualified BOOLEAN; BEGIN IF app.current_role_name() <> 'admin' OR NEW.actor_uid IS DISTINCT FROM app.current_uid() THEN RAISE EXCEPTION 'continuous improvement approval requires current admin actor' USING ERRCODE = '42501'; END IF; IF NEW.target_kind = 'content_qualification' THEN SELECT 'approve_content', TRUE INTO expected_decision, qualified FROM app.ci_content_qualification WHERE qualification_id = NEW.target_id; ELSIF NEW.target_kind = 'model_change_gate' THEN SELECT CASE gate_decision WHEN 'promote' THEN 'approve_promotion' WHEN 'rollback' THEN 'authorize_rollback' ELSE 'keep_quarantine' END, TRUE INTO expected_decision, qualified FROM app.ci_model_change_gate WHERE gate_id = NEW.target_id; ELSE SELECT 'approve_promotion', g.qualified INTO expected_decision, qualified FROM app.ci_release_gate g WHERE gate_id = NEW.target_id; END IF; IF expected_decision IS NULL THEN RAISE EXCEPTION 'approval target does not exist' USING ERRCODE = '23514'; END IF; IF NEW.decision = 'authorize_rollback' AND NEW.target_kind IN ('model_change_gate','release_gate') AND qualified THEN RETURN NEW; END IF; -- reject와 keep_quarantine는 부작용 없이 감사 이벤트만 남기는 fail-closed 선택이다. -- 카탈로그·승격·롤백 효과를 만드는 긍정 결정만 계산된 게이트와 일치해야 한다. IF NEW.decision IN ('reject', 'keep_quarantine') THEN RETURN NEW; END IF; IF NEW.decision <> expected_decision OR (NEW.decision = expected_decision AND NOT qualified) THEN RAISE EXCEPTION 'human decision cannot bypass computed fail-closed gate' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_catalog_approval() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM app.ci_content_qualification q JOIN audit.ci_human_approval_event a ON a.approval_event_id = NEW.approval_event_id AND a.target_kind = 'content_qualification' AND a.target_id = q.qualification_id AND a.decision = 'approve_content' WHERE q.qualification_id = NEW.qualification_id AND q.catalog_entry_id = NEW.catalog_entry_id AND q.payload_sha256 = NEW.payload_sha256 AND q.source_record_ids = NEW.source_record_ids AND q.review_record_ids = NEW.review_record_ids AND q.benchmark_record_id = NEW.benchmark_record_id ) THEN RAISE EXCEPTION 'catalog promotion requires matching human approval' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_lifecycle_event() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE gate_decision TEXT; DECLARE rollback_id UUID; BEGIN IF NEW.target_kind = 'model_change_gate' THEN SELECT g.gate_decision, g.rollback_artifact_id INTO gate_decision, rollback_id FROM app.ci_model_change_gate g WHERE g.gate_id = NEW.target_id; ELSE SELECT CASE WHEN g.qualified THEN 'promote' ELSE 'quarantine' END, g.rollback_artifact_id INTO gate_decision, rollback_id FROM app.ci_release_gate g WHERE g.gate_id = NEW.target_id; END IF; IF gate_decision IS NULL THEN RAISE EXCEPTION 'lifecycle target does not exist' USING ERRCODE = '23514'; END IF; IF NEW.event_type = 'promotion' AND ( gate_decision <> 'promote' OR NOT EXISTS ( SELECT 1 FROM audit.ci_human_approval_event a WHERE a.approval_event_id = NEW.approval_event_id AND a.target_id = NEW.target_id AND a.target_kind = NEW.target_kind AND a.decision = 'approve_promotion' ) ) THEN RAISE EXCEPTION 'silent or failed-gate promotion is forbidden' USING ERRCODE = '23514'; END IF; IF NEW.event_type = 'rollback' AND ( NEW.artifact_record_id IS DISTINCT FROM rollback_id OR NOT EXISTS ( SELECT 1 FROM audit.ci_human_approval_event a WHERE a.approval_event_id = NEW.approval_event_id AND a.target_id = NEW.target_id AND a.target_kind = NEW.target_kind AND a.decision = 'authorize_rollback' ) ) THEN RAISE EXCEPTION 'rollback requires human authorization and pinned artifact' USING ERRCODE = '23514'; END IF; IF NEW.event_type = 'rollback' AND NEW.event_status = 'executed' AND (NULLIF(btrim(NEW.executor_receipt_id), '') IS NULL OR COALESCE(cardinality(NEW.executor_evidence_refs), 0) < 1) THEN RAISE EXCEPTION 'executed rollback requires executor receipt evidence' USING ERRCODE = '23514'; END IF; IF NEW.event_type = 'monitor' AND NEW.event_status = 'rollback_verified' AND NOT EXISTS (SELECT 1 FROM audit.ci_lifecycle_event e WHERE e.target_kind = NEW.target_kind AND e.target_id = NEW.target_id AND e.event_type = 'rollback' AND e.event_status = 'executed') THEN RAISE EXCEPTION 'rollback verification requires an earlier executed rollback' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_model_snapshot_gate() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE baseline app.ci_model_calibration_snapshot%ROWTYPE; DECLARE candidate app.ci_model_calibration_snapshot%ROWTYPE; DECLARE expected_decision TEXT; BEGIN SELECT * INTO baseline FROM app.ci_model_calibration_snapshot WHERE snapshot_record_id = NEW.baseline_snapshot_record_id AND gate_id = NEW.gate_id AND snapshot_role = 'baseline'; SELECT * INTO candidate FROM app.ci_model_calibration_snapshot WHERE snapshot_record_id = NEW.candidate_snapshot_record_id AND gate_id = NEW.gate_id AND snapshot_role = 'candidate'; IF baseline.snapshot_record_id IS NULL OR candidate.snapshot_record_id IS NULL THEN RAISE EXCEPTION 'model gate snapshots are missing or role-mismatched' USING ERRCODE = '23514'; END IF; IF candidate.critical_miss_count > 0 OR candidate.leakage_count > 0 OR candidate.pii_count > 0 THEN expected_decision := 'rollback'; ELSIF candidate.task_accuracy < baseline.task_accuracy - 0.02 OR candidate.calibration_error > baseline.calibration_error + 0.02 OR candidate.subgroup_max_gap > baseline.subgroup_max_gap + 0.05 THEN expected_decision := 'quarantine'; ELSE expected_decision := 'promote'; END IF; IF NEW.gate_decision IS DISTINCT FROM expected_decision OR (expected_decision = 'rollback' AND NEW.rollback_target_snapshot_id IS DISTINCT FROM baseline.snapshot_id) THEN RAISE EXCEPTION 'model gate decision bypasses calibration baseline thresholds' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_ci_agentic_job_update() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF ROW( NEW.job_key, NEW.source_packs, NEW.source_fingerprint, NEW.data_classification, NEW.content_kind, NEW.difficulty_level, NEW.variant_count, NEW.prompt_version, NEW.trigger_kind, NEW.created_at ) IS DISTINCT FROM ROW( OLD.job_key, OLD.source_packs, OLD.source_fingerprint, OLD.data_classification, OLD.content_kind, OLD.difficulty_level, OLD.variant_count, OLD.prompt_version, OLD.trigger_kind, OLD.created_at ) THEN RAISE EXCEPTION 'agentic job source and configuration are immutable' USING ERRCODE = '23514'; END IF; IF NOT ( (OLD.status = 'pending' AND NEW.status = 'processing') OR (OLD.status = 'retry_wait' AND NEW.status = 'processing') OR (OLD.status = 'processing' AND NEW.status IN ('processing','retry_wait','rejected','completed')) ) THEN RAISE EXCEPTION 'invalid agentic job state transition: % -> %', OLD.status, NEW.status USING ERRCODE = '23514'; END IF; IF NEW.status <> 'processing' AND NEW.lease_started_at IS NOT NULL THEN RAISE EXCEPTION 'only processing agentic jobs may hold a lease' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_ci_content_pipeline_sources ON app.ci_content_pipeline; CREATE TRIGGER trg_ci_content_pipeline_sources BEFORE INSERT ON app.ci_content_pipeline FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_uuid_array(); DROP TRIGGER IF EXISTS trg_ci_agentic_job_update_contract ON app.ci_agentic_job; CREATE TRIGGER trg_ci_agentic_job_update_contract BEFORE UPDATE ON app.ci_agentic_job FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_agentic_job_update(); DROP TRIGGER IF EXISTS trg_ci_content_qualification_sources ON app.ci_content_qualification; CREATE TRIGGER trg_ci_content_qualification_sources BEFORE INSERT ON app.ci_content_qualification FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_uuid_array(); DROP TRIGGER IF EXISTS trg_ci_model_gate_artifacts ON app.ci_model_change_gate; CREATE TRIGGER trg_ci_model_gate_artifacts BEFORE INSERT ON app.ci_model_change_gate FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_uuid_array(); DROP TRIGGER IF EXISTS trg_ci_release_gate_artifacts ON app.ci_release_gate; CREATE TRIGGER trg_ci_release_gate_artifacts BEFORE INSERT ON app.ci_release_gate FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_uuid_array(); DROP TRIGGER IF EXISTS trg_ci_dag_dependencies ON app.ci_regression_dag_node; CREATE TRIGGER trg_ci_dag_dependencies BEFORE INSERT ON app.ci_regression_dag_node FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_uuid_array(); DROP TRIGGER IF EXISTS trg_ci_red_team_review_contract ON app.ci_red_team_review; CREATE TRIGGER trg_ci_red_team_review_contract BEFORE INSERT ON app.ci_red_team_review FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_review_contract(); DROP TRIGGER IF EXISTS trg_ci_red_team_finding_contract ON app.ci_red_team_finding; CREATE TRIGGER trg_ci_red_team_finding_contract BEFORE INSERT ON app.ci_red_team_finding FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_review_contract(); DROP TRIGGER IF EXISTS trg_ci_human_approval_contract ON audit.ci_human_approval_event; CREATE TRIGGER trg_ci_human_approval_contract BEFORE INSERT ON audit.ci_human_approval_event FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_human_approval(); DROP TRIGGER IF EXISTS trg_ci_catalog_approval ON app.ci_catalog_entry; CREATE TRIGGER trg_ci_catalog_approval BEFORE INSERT ON app.ci_catalog_entry FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_catalog_approval(); DROP TRIGGER IF EXISTS trg_ci_lifecycle_contract ON audit.ci_lifecycle_event; CREATE TRIGGER trg_ci_lifecycle_contract BEFORE INSERT ON audit.ci_lifecycle_event FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_lifecycle_event(); DROP TRIGGER IF EXISTS trg_ci_model_snapshot_contract ON app.ci_model_change_gate; CREATE TRIGGER trg_ci_model_snapshot_contract BEFORE INSERT ON app.ci_model_change_gate FOR EACH ROW EXECUTE FUNCTION audit.enforce_ci_model_snapshot_gate(); DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'ci_ingestion_submission','ci_source_artifact','ci_content_pipeline','ci_red_team_review', 'ci_red_team_finding','ci_content_benchmark','ci_content_qualification','ci_gate_artifact', 'ci_model_calibration_snapshot','ci_model_change_gate','ci_release_gate','ci_catalog_entry', 'ci_operational_incident','ci_regression_dag_node' ] LOOP EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_append_only ON app.%I', table_name, table_name); EXECUTE format('CREATE TRIGGER trg_%s_append_only BEFORE UPDATE OR DELETE ON app.%I FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation()', table_name, table_name); EXECUTE format('ALTER TABLE app.%I ENABLE ROW LEVEL SECURITY', table_name); END LOOP; END; $$; DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY['ci_human_approval_event','ci_lifecycle_event'] LOOP EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_append_only ON audit.%I', table_name, table_name); EXECUTE format('CREATE TRIGGER trg_%s_append_only BEFORE UPDATE OR DELETE ON audit.%I FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation()', table_name, table_name); EXECUTE format('ALTER TABLE audit.%I ENABLE ROW LEVEL SECURITY', table_name); END LOOP; END; $$; ALTER TABLE app.ci_agentic_job ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_ci_agentic_job_select ON app.ci_agentic_job; DROP POLICY IF EXISTS p_ci_agentic_job_insert ON app.ci_agentic_job; DROP POLICY IF EXISTS p_ci_agentic_job_update ON app.ci_agentic_job; CREATE POLICY p_ci_agentic_job_select ON app.ci_agentic_job FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research') OR (NOT app.is_ai_context() AND app.current_role_name() = 'admin') ); CREATE POLICY p_ci_agentic_job_insert ON app.ci_agentic_job FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research' AND data_classification = 'synthetic_replay_red_team_coverage_drift' AND status = 'pending' ); CREATE POLICY p_ci_agentic_job_update ON app.ci_agentic_job FOR UPDATE USING ( app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research' ) WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research' AND data_classification = 'synthetic_replay_red_team_coverage_drift' ); -- Research automation can see/write candidates and evidence; only administrators approve. DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'ci_ingestion_submission','ci_source_artifact','ci_content_pipeline','ci_red_team_review', 'ci_red_team_finding','ci_content_benchmark','ci_content_qualification','ci_gate_artifact', 'ci_model_calibration_snapshot','ci_model_change_gate','ci_release_gate', 'ci_operational_incident','ci_regression_dag_node' ] LOOP EXECUTE format('DROP POLICY IF EXISTS p_%s_select ON app.%I', table_name, table_name); EXECUTE format('DROP POLICY IF EXISTS p_%s_insert ON app.%I', table_name, table_name); EXECUTE format('CREATE POLICY p_%s_select ON app.%I FOR SELECT USING ((app.is_ai_context() AND current_setting(''app.current_ai_view'',true) = ''research'') OR (NOT app.is_ai_context() AND app.current_role_name() = ''admin''))', table_name, table_name); EXECUTE format('CREATE POLICY p_%s_insert ON app.%I FOR INSERT WITH CHECK (app.is_ai_context() AND current_setting(''app.current_ai_view'',true) = ''research'')', table_name, table_name); END LOOP; END; $$; DROP POLICY IF EXISTS p_ci_ingestion_submission_admin_insert ON app.ci_ingestion_submission; CREATE POLICY p_ci_ingestion_submission_admin_insert ON app.ci_ingestion_submission FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND app.current_role_name() = 'admin' AND operation_kind = 'human_approval' ); DROP POLICY IF EXISTS p_ci_catalog_entry_select ON app.ci_catalog_entry; DROP POLICY IF EXISTS p_ci_catalog_entry_insert ON app.ci_catalog_entry; CREATE POLICY p_ci_catalog_entry_select ON app.ci_catalog_entry FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research') OR (NOT app.is_ai_context() AND app.current_role_name() = 'admin') ); CREATE POLICY p_ci_catalog_entry_insert ON app.ci_catalog_entry FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND app.current_role_name() = 'admin' ); DROP POLICY IF EXISTS p_ci_human_approval_select ON audit.ci_human_approval_event; DROP POLICY IF EXISTS p_ci_human_approval_insert ON audit.ci_human_approval_event; CREATE POLICY p_ci_human_approval_select ON audit.ci_human_approval_event FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research') OR (NOT app.is_ai_context() AND app.current_role_name() = 'admin') ); CREATE POLICY p_ci_human_approval_insert ON audit.ci_human_approval_event FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND app.current_role_name() = 'admin' AND actor_uid = app.current_uid() ); DROP POLICY IF EXISTS p_ci_lifecycle_select ON audit.ci_lifecycle_event; DROP POLICY IF EXISTS p_ci_lifecycle_monitor_insert ON audit.ci_lifecycle_event; DROP POLICY IF EXISTS p_ci_lifecycle_admin_insert ON audit.ci_lifecycle_event; CREATE POLICY p_ci_lifecycle_select ON audit.ci_lifecycle_event FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research') OR (NOT app.is_ai_context() AND app.current_role_name() = 'admin') ); CREATE POLICY p_ci_lifecycle_monitor_insert ON audit.ci_lifecycle_event FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view',true) = 'research' AND event_type = 'monitor' ); CREATE POLICY p_ci_lifecycle_admin_insert ON audit.ci_lifecycle_event FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND app.current_role_name() = 'admin' AND event_type IN ('promotion','rollback') ); DO $$ DECLARE app_role TEXT; BEGIN FOREACH app_role IN ARRAY ARRAY['vignette_app','vignette'] LOOP IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = app_role) THEN EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app, audit TO %I', app_role); EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA audit TO %I', app_role); END IF; END LOOP; END; $$;