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 산출물은 커밋에서 제외했다.
794 lines
41 KiB
PL/PgSQL
794 lines
41 KiB
PL/PgSQL
-- 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;
|
|
$$;
|