G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -0,0 +1,794 @@
-- 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;
IF NEW.decision NOT IN ('reject', 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;
$$;