-- v12.4 continuous model operations delta. -- Evaluation and proposal automation only. This migration does not enable automatic model promotion, -- automatic order submission or KIS submission. create schema if not exists evaluation; create schema if not exists governance; create table if not exists evaluation.dataset_manifest ( dataset_id text primary key, scope_key text not null, content_hash text not null unique, source_catalog_version text not null, lineage_hash text not null, status text not null check (status in ('PROPOSED', 'APPROVED', 'QUARANTINED', 'RETIRED')), frozen_at timestamptz not null, approved_by text null, approved_at timestamptz null, created_at timestamptz not null default now() ); create index if not exists ix_dataset_manifest_scope_frozen on evaluation.dataset_manifest (scope_key, frozen_at desc) where status = 'APPROVED'; create table if not exists governance.model_version_registry ( model_version text not null, scope_key text not null, config_version text not null, code_sha text not null, contract_version text not null, lifecycle_state text not null check (lifecycle_state in ('RESEARCH', 'CHALLENGER', 'SHADOW', 'CANDIDATE', 'APPROVED', 'RETIRED', 'ROLLED_BACK')), effective_at timestamptz not null, retired_at timestamptz null, model_card_hash text not null, approved_by text null, approved_at timestamptz null, created_at timestamptz not null default now(), primary key (model_version, scope_key, effective_at), check (lifecycle_state <> 'APPROVED' or (approved_by is not null and approved_at is not null)) ); create index if not exists ix_model_version_registry_active on governance.model_version_registry (scope_key, effective_at desc) where lifecycle_state in ('RESEARCH', 'CHALLENGER', 'SHADOW', 'CANDIDATE', 'APPROVED'); create table if not exists evaluation.model_operation_schedule ( schedule_id uuid primary key, operation_code text not null, operation_name text not null, scope_key text not null, cadence text not null check (cadence in ('DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'EVENT_DRIVEN')), automation_mode text not null check (automation_mode in ('EVALUATION_ONLY', 'PROPOSAL_ONLY', 'DRILL_ONLY')), queue_name text not null, schedule_version integer not null check (schedule_version > 0), enabled boolean not null default true, next_due_at timestamptz not null, last_dispatched_at timestamptz null, last_background_job_id text null, max_lag interval not null, primary_owner text not null, secondary_owner text not null, lease_owner text null, lease_until timestamptz null, last_error_code text null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (operation_code, scope_key, schedule_version) ); create index if not exists ix_model_operation_schedule_due on evaluation.model_operation_schedule (next_due_at, operation_code, scope_key) where enabled = true; create table if not exists evaluation.model_operation_request ( request_id uuid primary key, schedule_id uuid not null references evaluation.model_operation_schedule(schedule_id), operation_code text not null, scope_key text not null, automation_mode text not null check (automation_mode in ('EVALUATION_ONLY', 'PROPOSAL_ONLY', 'DRILL_ONLY')), idempotency_key text not null unique, dataset_id text not null, data_hash text not null, model_version text not null, config_version text not null, code_sha text not null, contract_version text not null, lifecycle_state text not null, correlation_id text not null, status text not null check (status in ('REQUESTED', 'RUNNING', 'SUCCEEDED', 'BUSINESS_HOLD', 'FAILED', 'QUARANTINED')), output_hash text null, error_code text null, requested_at timestamptz not null, started_at timestamptz null, finished_at timestamptz null ); create index if not exists ix_model_operation_request_scope_time on evaluation.model_operation_request (scope_key, operation_code, requested_at desc); -- Request is a lifecycle aggregate: status/timing fields are mutable under optimistic/transactional control. -- Every transition is additionally recorded as an immutable status event so operational history is never lost. create table if not exists evaluation.model_operation_status_event ( event_id uuid primary key, request_id uuid not null references evaluation.model_operation_request(request_id), from_status text null, to_status text not null check (to_status in ('REQUESTED', 'RUNNING', 'SUCCEEDED', 'BUSINESS_HOLD', 'FAILED', 'QUARANTINED')), reason_code text null, payload_hash text not null, actor_type text not null check (actor_type in ('SYSTEM', 'OPERATOR', 'REPLAY')), correlation_id text not null, occurred_at timestamptz not null, created_at timestamptz not null default now(), unique (request_id, to_status, occurred_at, payload_hash) ); create index if not exists ix_model_operation_status_event_request_time on evaluation.model_operation_status_event (request_id, occurred_at); create table if not exists evaluation.model_metric_definition ( metric_code text not null, definition_version integer not null check (definition_version > 0), numerator_definition text not null, denominator_definition text not null, evaluation_window text not null, aggregation_method text not null, threshold_json jsonb not null, status text not null check (status in ('PROPOSED', 'APPROVED', 'RETIRED')), content_hash text not null, effective_from timestamptz null, approved_by text null, approved_at timestamptz null, created_at timestamptz not null default now(), primary key (metric_code, definition_version), unique (content_hash) ); create table if not exists evaluation.model_metric_observation ( observation_id uuid primary key, request_id uuid not null references evaluation.model_operation_request(request_id), metric_code text not null, definition_version integer not null, scope_key text not null, cohort_key text not null, value_numeric numeric(30, 12) null, value_text text null, sample_size bigint not null check (sample_size >= 0), dataset_id text not null, model_version text not null, as_of timestamptz not null, content_hash text not null, created_at timestamptz not null default now(), foreign key (metric_code, definition_version) references evaluation.model_metric_definition(metric_code, definition_version), check ((value_numeric is null) <> (value_text is null)), unique (request_id, metric_code, definition_version, cohort_key, content_hash) ); create table if not exists evaluation.model_evaluation_snapshot ( snapshot_id uuid primary key, request_id uuid not null references evaluation.model_operation_request(request_id), scope_key text not null, model_version text not null, dataset_id text not null, gate_contract_version text not null, gate_decision text not null check (gate_decision in ('PASS', 'WARN', 'HOLD', 'FAIL')), blocking_reasons_json jsonb not null, warnings_json jsonb not null, evidence_hash text not null, as_of timestamptz not null, created_at timestamptz not null default now(), unique (request_id, evidence_hash) ); create table if not exists governance.model_improvement_proposal ( proposal_id uuid primary key, source_request_id uuid not null references evaluation.model_operation_request(request_id), scope_key text not null, base_model_version text not null, proposal_type text not null check (proposal_type in ('DATA_REMEDIATION', 'FEATURE_REVIEW', 'THRESHOLD_REVIEW', 'POLICY_REFACTORING', 'CALIBRATION_REVIEW', 'RISK_LIMIT_REVIEW', 'TECH_DEBT_REPAYMENT')), problem_statement text not null, supporting_evidence_json jsonb not null, counter_evidence_json jsonb not null, expected_benefit text not null, risks text not null, required_tests_json jsonb not null, status text not null check (status in ('DRAFT', 'REVIEW_REQUIRED', 'APPROVED_FOR_RESEARCH', 'REJECTED', 'EXPIRED')), created_by text not null, created_at timestamptz not null, reviewed_by text null, reviewed_at timestamptz null, expires_at timestamptz not null, content_hash text not null unique, check (status <> 'APPROVED_FOR_RESEARCH' or (reviewed_by is not null and reviewed_at is not null)) ); create table if not exists governance.model_promotion_review ( review_id uuid primary key, scope_key text not null, candidate_model_version text not null, evidence_snapshot_id uuid not null references evaluation.model_evaluation_snapshot(snapshot_id), independent_validation_hash text not null, compliance_review_hash text null, security_review_hash text null, decision text not null check (decision in ('PENDING', 'APPROVED_FOR_SHADOW', 'APPROVED_FOR_PILOT', 'REJECTED', 'EXPIRED')), maker_id text not null, checker_id text null, created_at timestamptz not null, decided_at timestamptz null, check (maker_id <> coalesce(checker_id, '')), check (decision = 'PENDING' or (checker_id is not null and decided_at is not null)) ); create table if not exists governance.model_rollback_drill ( drill_id uuid primary key, scope_key text not null, champion_model_version text not null, fallback_model_version text not null, runbook_version text not null, started_at timestamptz not null, finished_at timestamptz null, result text not null check (result in ('RUNNING', 'PASS', 'FAIL', 'BUSINESS_HOLD')), recovery_time_seconds integer null check (recovery_time_seconds is null or recovery_time_seconds >= 0), evidence_hash text null, owner text not null, secondary_owner text not null ); -- Append-only evidence. Request status is mutable, but status_event/observation/snapshot history is immutable. drop trigger if exists model_operation_status_event_append_only on evaluation.model_operation_status_event; create trigger model_operation_status_event_append_only before update or delete on evaluation.model_operation_status_event for each row execute function building_blocks.prevent_append_only_change(); drop trigger if exists model_metric_observation_append_only on evaluation.model_metric_observation; create trigger model_metric_observation_append_only before update or delete on evaluation.model_metric_observation for each row execute function building_blocks.prevent_append_only_change(); drop trigger if exists model_evaluation_snapshot_append_only on evaluation.model_evaluation_snapshot; create trigger model_evaluation_snapshot_append_only before update or delete on evaluation.model_evaluation_snapshot for each row execute function building_blocks.prevent_append_only_change(); -- Schedules intentionally automate evidence collection/proposal generation only. insert into evaluation.model_operation_schedule (schedule_id, operation_code, operation_name, scope_key, cadence, automation_mode, queue_name, schedule_version, enabled, next_due_at, max_lag, primary_owner, secondary_owner) values ('e4100000-0000-4000-8000-000000000010', 'J10', 'OutcomeEvaluationRun', 'GLOBAL', 'DAILY', 'EVALUATION_ONLY', 'q-evaluation', 1, true, now() + interval '1 hour', interval '1 day', 'Quant/Ops', 'Data/QA'), ('e4100000-0000-4000-8000-000000000011', 'J11', 'DailyScorecardBuild', 'GLOBAL', 'DAILY', 'EVALUATION_ONLY', 'q-evaluation', 1, true, now() + interval '2 hours', interval '1 day', 'Quant/Ops', 'Risk/SRE'), ('e4100000-0000-4000-8000-000000000017', 'J17', 'DriftDetectionRun', 'GLOBAL', 'DAILY', 'EVALUATION_ONLY', 'q-evaluation', 1, true, now() + interval '3 hours', interval '1 day', 'Quant/Risk', 'Data/SRE'), ('e4100000-0000-4000-8000-000000000018', 'J18', 'ChampionChallengerEvaluation', 'GLOBAL', 'WEEKLY', 'EVALUATION_ONLY', 'q-evaluation', 1, true, now() + interval '1 day', interval '7 days', 'Quant/Risk', 'QA/InvestmentCommittee'), ('e4100000-0000-4000-8000-000000000019', 'J19', 'FrozenOosBacktest', 'GLOBAL', 'MONTHLY', 'EVALUATION_ONLY', 'q-research', 1, true, now() + interval '7 days', interval '31 days', 'Quant/Data', 'QA/Risk'), ('e4100000-0000-4000-8000-000000000020', 'J20', 'RobustnessPboDsrRun', 'GLOBAL', 'QUARTERLY', 'EVALUATION_ONLY', 'q-research', 1, true, now() + interval '30 days', interval '95 days', 'Quant/Risk', 'IndependentValidation'), ('e4100000-0000-4000-8000-000000000021', 'J21', 'ModelImprovementProposalBuild', 'GLOBAL', 'MONTHLY', 'PROPOSAL_ONLY', 'q-research', 1, true, now() + interval '14 days', interval '31 days', 'Quant Lead', 'Risk/Architect'), ('e4100000-0000-4000-8000-000000000022', 'J22', 'PromotionEvidenceReviewBuild', 'GLOBAL', 'MONTHLY', 'PROPOSAL_ONLY', 'q-control', 1, true, now() + interval '21 days', interval '31 days', 'Risk/InvestmentCommittee', 'Compliance/QA'), ('e4100000-0000-4000-8000-000000000023', 'J23', 'ModelRollbackDrill', 'GLOBAL', 'QUARTERLY', 'DRILL_ONLY', 'q-control', 1, true, now() + interval '45 days', interval '95 days', 'SRE/Risk', 'Module Owner/QA'), ('e4100000-0000-4000-8000-000000000024', 'J24', 'DataRevisionRevalidation', 'GLOBAL', 'WEEKLY', 'EVALUATION_ONLY', 'q-backfill', 1, true, now() + interval '2 days', interval '7 days', 'Data/Quant', 'DBA/QA') on conflict (operation_code, scope_key, schedule_version) do nothing;