-- G6 Supervision & Research OS: immutable supervision queue and research evidence ledger. -- This schema stores metadata and UUID/hash provenance only. Transcript text and clinical claims -- are intentionally outside the contract. CREATE TABLE IF NOT EXISTS app.supervision_evidence_pointer ( pointer_id UUID PRIMARY KEY, learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), consumer_view TEXT NOT NULL CHECK (consumer_view IN ('supervisor','research')), ledger TEXT NOT NULL CHECK (ledger IN ( 'measurement_event','outcome_trajectory_revision','rupture_observation_event', 'rupture_reconciliation_revision','safety_event','calibration_assessment', 'transfer_assessment','practice_attempt' )), event_id TEXT NOT NULL CHECK (length(btrim(event_id)) > 0), session_id UUID, route_hint TEXT NOT NULL CHECK (route_hint ~ '^/[a-zA-Z0-9_{}?&=./-]+$'), content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (learner_id, consumer_view, ledger, event_id, route_hint) ); CREATE TABLE IF NOT EXISTS app.supervision_attention_snapshot ( snapshot_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), item_count INT NOT NULL CHECK (item_count >= 1), source_pointer_ids UUID[] NOT NULL CHECK (cardinality(source_pointer_ids) >= 1), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_by_role TEXT NOT NULL DEFAULT 'agent' CHECK (created_by_role = 'agent'), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.supervision_attention_item ( item_id UUID PRIMARY KEY, snapshot_id UUID NOT NULL REFERENCES app.supervision_attention_snapshot(snapshot_id) ON DELETE RESTRICT, learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, learner_ref TEXT NOT NULL CHECK (learner_ref ~ '^learner-[a-z0-9-]+$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), queue_position INT NOT NULL CHECK (queue_position >= 1), primary_signal TEXT NOT NULL CHECK (primary_signal IN ( 'deterioration','unresolved_rupture','safety_boundary', 'persistent_overconfidence','growth_stagnation','transfer_failure' )), oldest_active_sequence INT NOT NULL CHECK (oldest_active_sequence >= 1), drilldown_routes TEXT[] NOT NULL CHECK ( cardinality(drilldown_routes) BETWEEN 1 AND 3 ), evidence_pointer_ids UUID[] NOT NULL CHECK ( cardinality(evidence_pointer_ids) BETWEEN 1 AND 3 ), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (snapshot_id, learner_id), UNIQUE (snapshot_id, queue_position) ); CREATE TABLE IF NOT EXISTS app.supervision_attention_reason ( reason_id UUID PRIMARY KEY, item_id UUID NOT NULL REFERENCES app.supervision_attention_item(item_id) ON DELETE RESTRICT, signal_id TEXT NOT NULL CHECK (signal_id ~ '^oas-g6-signal-[a-z0-9-]+$'), signal_type TEXT NOT NULL CHECK (signal_type IN ( 'deterioration','unresolved_rupture','safety_boundary', 'persistent_overconfidence','growth_stagnation','transfer_failure' )), severity TEXT NOT NULL CHECK (severity IN ('high','moderate','low')), uncertainty DOUBLE PRECISION NOT NULL CHECK (uncertainty BETWEEN 0 AND 1), evidence_pointer_ids UUID[] NOT NULL CHECK ( cardinality(evidence_pointer_ids) BETWEEN 1 AND 3 ), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (item_id, signal_id) ); CREATE TABLE IF NOT EXISTS app.supervision_teacher_ai_disagreement ( disagreement_record_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), disagreement_id TEXT NOT NULL UNIQUE CHECK (disagreement_id ~ '^oas-g6-disagreement-[a-z0-9-]+$'), learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), case_ref TEXT NOT NULL CHECK (length(btrim(case_ref)) > 0), competency_id TEXT NOT NULL CHECK (competency_id ~ '^competency\.[a-z0-9_.-]+$'), ai_label TEXT NOT NULL CHECK (length(btrim(ai_label)) > 0), teacher_label TEXT NOT NULL CHECK (length(btrim(teacher_label)) > 0), ai_model TEXT NOT NULL CHECK (length(btrim(ai_model)) > 0), prompt_version TEXT NOT NULL CHECK (length(btrim(prompt_version)) > 0), instrument_id TEXT NOT NULL CHECK (length(btrim(instrument_id)) > 0), instrument_version TEXT NOT NULL CHECK (length(btrim(instrument_version)) > 0), correction_reason_code TEXT NOT NULL CHECK (length(btrim(correction_reason_code)) > 0), ai_evidence_pointer_ids UUID[] NOT NULL CHECK (cardinality(ai_evidence_pointer_ids) >= 1), teacher_evidence_pointer_ids UUID[] NOT NULL CHECK (cardinality(teacher_evidence_pointer_ids) >= 1), created_by_uid UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, raw_transcript_included BOOLEAN NOT NULL DEFAULT FALSE CHECK (raw_transcript_included = FALSE), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (ai_label <> teacher_label) ); CREATE TABLE IF NOT EXISTS app.supervision_calibration_dataset_row ( dataset_row_id UUID PRIMARY KEY, disagreement_record_id UUID NOT NULL UNIQUE REFERENCES app.supervision_teacher_ai_disagreement(disagreement_record_id) ON DELETE RESTRICT, learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), row_hash TEXT NOT NULL UNIQUE CHECK (row_hash ~ '^[a-f0-9]{64}$'), evidence_pointer_ids UUID[] NOT NULL CHECK (cardinality(evidence_pointer_ids) >= 2), raw_transcript_included BOOLEAN NOT NULL DEFAULT FALSE CHECK (raw_transcript_included = FALSE), 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.supervision_teacher_event ( audit_event_id UUID PRIMARY KEY, disagreement_record_id UUID NOT NULL UNIQUE REFERENCES app.supervision_teacher_ai_disagreement(disagreement_record_id) ON DELETE RESTRICT, actor_uid UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), action TEXT NOT NULL CHECK (action = 'teacher_ai_disagreement.corrected'), content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), evidence_pointer_ids UUID[] NOT NULL CHECK (cardinality(evidence_pointer_ids) >= 2), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app.supervision_curriculum_gap_snapshot ( gap_snapshot_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), competency_id TEXT NOT NULL CHECK (competency_id ~ '^competency\.[a-z0-9_.-]+$'), gap_kind TEXT NOT NULL CHECK (gap_kind IN ( 'coverage','growth_stagnation','rupture_repair','transfer','calibration' )), status TEXT NOT NULL CHECK (status IN ('observed','monitoring','insufficient_evidence')), uncertainty DOUBLE PRECISION NOT NULL CHECK (uncertainty BETWEEN 0 AND 1), affected_learner_count INT NOT NULL CHECK (affected_learner_count >= 0), evidence_pointer_ids UUID[] NOT NULL, clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK ( (status = 'insufficient_evidence' AND uncertainty = 1 AND cardinality(evidence_pointer_ids) = 0) OR (status <> 'insufficient_evidence' AND cardinality(evidence_pointer_ids) >= 1) ) ); CREATE TABLE IF NOT EXISTS app.supervision_evaluation_batch ( batch_record_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), batch_id TEXT NOT NULL UNIQUE CHECK (batch_id ~ '^oas-g6-batch-[a-z0-9-]+$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), model_name TEXT NOT NULL CHECK (length(btrim(model_name)) > 0), prompt_version TEXT NOT NULL CHECK (length(btrim(prompt_version)) > 0), instrument_id TEXT NOT NULL CHECK (length(btrim(instrument_id)) > 0), instrument_version TEXT NOT NULL CHECK (length(btrim(instrument_version)) > 0), observation_count INT NOT NULL CHECK (observation_count >= 1), data_classification TEXT NOT NULL DEFAULT 'synthetic_educational' CHECK (data_classification = 'synthetic_educational'), 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.supervision_evaluation_observation ( observation_record_id UUID PRIMARY KEY, batch_record_id UUID NOT NULL REFERENCES app.supervision_evaluation_batch(batch_record_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), case_ref TEXT NOT NULL CHECK (length(btrim(case_ref)) > 0), competency_id TEXT NOT NULL CHECK (competency_id ~ '^competency\.[a-z0-9_.-]+$'), synthetic_subgroup TEXT NOT NULL CHECK (synthetic_subgroup ~ '^synthetic-[a-z0-9-]+$'), gold_label TEXT NOT NULL CHECK (length(btrim(gold_label)) > 0), predicted_label TEXT NOT NULL CHECK (length(btrim(predicted_label)) > 0), evidence_pointer_id UUID NOT NULL REFERENCES app.supervision_evidence_pointer(pointer_id) ON DELETE RESTRICT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (batch_record_id, case_ref, competency_id) ); CREATE TABLE IF NOT EXISTS app.supervision_drift_report ( drift_report_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), baseline_batch_record_id UUID NOT NULL REFERENCES app.supervision_evaluation_batch(batch_record_id) ON DELETE RESTRICT, candidate_batch_record_id UUID NOT NULL REFERENCES app.supervision_evaluation_batch(batch_record_id) ON DELETE RESTRICT, matched_count INT NOT NULL CHECK (matched_count >= 0), status TEXT NOT NULL CHECK (status IN ('stable','drift_flagged','insufficient_evidence')), baseline_accuracy DOUBLE PRECISION CHECK (baseline_accuracy BETWEEN 0 AND 1), candidate_accuracy DOUBLE PRECISION CHECK (candidate_accuracy BETWEEN 0 AND 1), accuracy_delta DOUBLE PRECISION CHECK (accuracy_delta BETWEEN -1 AND 1), disagreement_case_refs TEXT[] NOT NULL DEFAULT '{}', alerts TEXT[] NOT NULL DEFAULT '{}', evidence_pointer_ids UUID[] NOT NULL CHECK (cardinality(evidence_pointer_ids) >= 1), data_classification TEXT NOT NULL DEFAULT 'synthetic_educational' CHECK (data_classification = 'synthetic_educational'), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (baseline_batch_record_id <> candidate_batch_record_id), CHECK ( (status = 'insufficient_evidence' AND baseline_accuracy IS NULL AND candidate_accuracy IS NULL AND accuracy_delta IS NULL) OR (status <> 'insufficient_evidence' AND baseline_accuracy IS NOT NULL AND candidate_accuracy IS NOT NULL AND accuracy_delta IS NOT NULL) ) ); CREATE TABLE IF NOT EXISTS app.supervision_drift_subgroup_metric ( subgroup_metric_id UUID PRIMARY KEY, drift_report_id UUID NOT NULL REFERENCES app.supervision_drift_report(drift_report_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), subgroup TEXT NOT NULL CHECK (length(btrim(subgroup)) > 0), matched_count INT NOT NULL CHECK (matched_count >= 0), baseline_accuracy DOUBLE PRECISION CHECK (baseline_accuracy BETWEEN 0 AND 1), candidate_accuracy DOUBLE PRECISION CHECK (candidate_accuracy BETWEEN 0 AND 1), accuracy_delta DOUBLE PRECISION CHECK (accuracy_delta BETWEEN -1 AND 1), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (drift_report_id, subgroup), CHECK ( (baseline_accuracy IS NULL AND candidate_accuracy IS NULL AND accuracy_delta IS NULL) OR (baseline_accuracy IS NOT NULL AND candidate_accuracy IS NOT NULL AND accuracy_delta IS NOT NULL) ) ); CREATE TABLE IF NOT EXISTS app.supervision_phase3_manifest ( manifest_id UUID PRIMARY KEY, submission_id UUID NOT NULL UNIQUE, content_hash TEXT NOT NULL CHECK (content_hash ~ '^[a-f0-9]{64}$'), cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), schema_version TEXT NOT NULL CHECK (schema_version = 'vignette.phase3-outcome-evidence-manifest.v1'), artifact_count INT NOT NULL CHECK (artifact_count = 4), 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.supervision_phase3_artifact ( artifact_record_id UUID PRIMARY KEY, manifest_id UUID NOT NULL REFERENCES app.supervision_phase3_manifest(manifest_id) ON DELETE RESTRICT, cohort_id TEXT NOT NULL CHECK (length(btrim(cohort_id)) > 0), domain TEXT NOT NULL CHECK (domain IN ('alliance','rupture','transfer','calibration')), artifact_id TEXT NOT NULL CHECK (length(btrim(artifact_id)) > 0), schema_version TEXT NOT NULL CHECK (length(btrim(schema_version)) > 0), content_sha256 TEXT NOT NULL CHECK (content_sha256 ~ '^[a-f0-9]{64}$'), record_count INT NOT NULL CHECK (record_count >= 1), provenance_uri TEXT NOT NULL CHECK (provenance_uri ~ '^(repo|db|audit)://[a-zA-Z0-9_./:-]+$'), source_pointer_id UUID NOT NULL REFERENCES app.supervision_evidence_pointer(pointer_id) ON DELETE RESTRICT, clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (manifest_id, domain) ); CREATE OR REPLACE FUNCTION audit.enforce_supervision_evidence_pointer() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE found_session UUID; found_learner UUID; found_cohort TEXT; BEGIN IF NEW.ledger = 'measurement_event' THEN SELECT m.session_id, s.learner_id INTO found_session, found_learner FROM app.measurement_event m JOIN app.sessions s ON s.id = m.session_id WHERE m.measurement_id = NEW.event_id::UUID; ELSIF NEW.ledger = 'outcome_trajectory_revision' THEN SELECT anchor_session_id, learner_id INTO found_session, found_learner FROM app.outcome_trajectory_revision WHERE revision_id = NEW.event_id::UUID; ELSIF NEW.ledger = 'rupture_observation_event' THEN SELECT o.session_id, s.learner_id INTO found_session, found_learner FROM app.rupture_observation_event o JOIN app.sessions s ON s.id = o.session_id WHERE o.observation_id = NEW.event_id::UUID; ELSIF NEW.ledger = 'rupture_reconciliation_revision' THEN SELECT r.session_id, s.learner_id INTO found_session, found_learner FROM app.rupture_reconciliation_revision r JOIN app.sessions s ON s.id = r.session_id WHERE r.revision_id = NEW.event_id::UUID; ELSIF NEW.ledger = 'safety_event' THEN SELECT e.session_id, s.learner_id INTO found_session, found_learner FROM app.safety_events e JOIN app.sessions s ON s.id = e.session_id WHERE e.id = NEW.event_id::BIGINT; ELSIF NEW.ledger = 'calibration_assessment' THEN SELECT session_id, learner_id INTO found_session, found_learner FROM app.calibration_assessment_snapshot WHERE assessment_snapshot_id = NEW.event_id::UUID; ELSIF NEW.ledger = 'transfer_assessment' THEN SELECT session_id, learner_id INTO found_session, found_learner FROM app.calibration_transfer_assessment WHERE transfer_assessment_id = NEW.event_id::UUID; ELSE SELECT session_id, learner_id INTO found_session, found_learner FROM app.practice_attempt_evidence WHERE attempt_record_id = NEW.event_id::UUID; END IF; SELECT cohort INTO found_cohort FROM app.app_user WHERE user_id = found_learner; IF found_learner IS NULL OR NEW.learner_id IS DISTINCT FROM found_learner OR (NEW.session_id IS NOT NULL AND NEW.session_id IS DISTINCT FROM found_session) OR NEW.cohort_id IS DISTINCT FROM found_cohort THEN RAISE EXCEPTION 'evidence pointer target is missing or outside learner/cohort scope' USING ERRCODE = '23514'; END IF; RETURN NEW; EXCEPTION WHEN invalid_text_representation THEN RAISE EXCEPTION 'evidence pointer event id has invalid ledger type' USING ERRCODE = '23514'; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_supervision_pointer_array() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE ids UUID[]; expected_view TEXT; expected_cohort TEXT; expected_learner UUID; matched_count INT; BEGIN IF TG_TABLE_NAME = 'supervision_attention_snapshot' THEN ids := NEW.source_pointer_ids; expected_view := 'supervisor'; expected_cohort := NEW.cohort_id; ELSIF TG_TABLE_NAME = 'supervision_attention_item' THEN ids := NEW.evidence_pointer_ids; expected_view := 'supervisor'; expected_cohort := NEW.cohort_id; expected_learner := NEW.learner_id; ELSIF TG_TABLE_NAME = 'supervision_attention_reason' THEN ids := NEW.evidence_pointer_ids; expected_view := 'supervisor'; ELSIF TG_TABLE_NAME = 'supervision_teacher_ai_disagreement' THEN ids := NEW.ai_evidence_pointer_ids || NEW.teacher_evidence_pointer_ids; expected_view := 'research'; expected_cohort := NEW.cohort_id; expected_learner := NEW.learner_id; ELSIF TG_TABLE_NAME = 'supervision_calibration_dataset_row' THEN ids := NEW.evidence_pointer_ids; expected_view := 'research'; expected_cohort := NEW.cohort_id; expected_learner := NEW.learner_id; ELSIF TG_TABLE_NAME = 'supervision_curriculum_gap_snapshot' THEN ids := NEW.evidence_pointer_ids; expected_view := 'supervisor'; expected_cohort := NEW.cohort_id; ELSIF TG_TABLE_NAME = 'supervision_drift_report' THEN ids := NEW.evidence_pointer_ids; expected_view := 'research'; expected_cohort := NEW.cohort_id; ELSIF TG_TABLE_NAME = 'supervision_evaluation_observation' THEN ids := ARRAY[NEW.evidence_pointer_id]; expected_view := 'research'; expected_cohort := NEW.cohort_id; ELSIF TG_TABLE_NAME = 'supervision_phase3_artifact' THEN ids := ARRAY[NEW.source_pointer_id]; expected_view := 'research'; expected_cohort := NEW.cohort_id; ELSIF TG_TABLE_SCHEMA = 'audit' AND TG_TABLE_NAME = 'supervision_teacher_event' THEN ids := NEW.evidence_pointer_ids; expected_view := 'research'; expected_cohort := NEW.cohort_id; expected_learner := NEW.learner_id; ELSE RETURN NEW; END IF; IF cardinality(ids) = 0 THEN RETURN NEW; END IF; SELECT count(DISTINCT p.pointer_id) INTO matched_count FROM app.supervision_evidence_pointer p WHERE p.pointer_id = ANY(ids) AND p.consumer_view = expected_view AND (expected_cohort IS NULL OR p.cohort_id = expected_cohort) AND (expected_learner IS NULL OR p.learner_id = expected_learner); IF matched_count <> cardinality(ARRAY(SELECT DISTINCT unnest(ids))) THEN RAISE EXCEPTION 'aggregate evidence pointer is missing or assigned to another AI view' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_supervision_attention_child() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE parent_item app.supervision_attention_item%ROWTYPE; BEGIN IF TG_TABLE_NAME = 'supervision_attention_item' THEN IF NOT EXISTS (SELECT 1 FROM app.supervision_attention_snapshot s WHERE s.snapshot_id = NEW.snapshot_id AND s.cohort_id = NEW.cohort_id) THEN RAISE EXCEPTION 'attention item cohort does not match snapshot' USING ERRCODE = '23514'; END IF; IF NOT EXISTS (SELECT 1 FROM app.app_user u WHERE u.user_id = NEW.learner_id AND u.cohort = NEW.cohort_id) THEN RAISE EXCEPTION 'attention learner is outside snapshot cohort' USING ERRCODE = '23514'; END IF; ELSE SELECT * INTO parent_item FROM app.supervision_attention_item WHERE item_id = NEW.item_id; IF parent_item.item_id IS NULL OR NOT NEW.evidence_pointer_ids <@ parent_item.evidence_pointer_ids THEN RAISE EXCEPTION 'attention reason evidence must be within item drilldown set' USING ERRCODE = '23514'; END IF; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_supervision_teacher_actor() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT EXISTS (SELECT 1 FROM app.app_user u WHERE u.user_id = NEW.learner_id AND u.cohort = NEW.cohort_id) THEN RAISE EXCEPTION 'teacher correction learner is outside cohort' USING ERRCODE = '23514'; END IF; IF app.current_role_name() = 'instructor' AND NEW.created_by_uid IS DISTINCT FROM app.current_uid() THEN RAISE EXCEPTION 'teacher correction actor must match current user' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'supervision_evidence_pointer','supervision_attention_snapshot','supervision_attention_item', 'supervision_attention_reason','supervision_teacher_ai_disagreement', 'supervision_calibration_dataset_row','supervision_curriculum_gap_snapshot', 'supervision_evaluation_batch','supervision_evaluation_observation', 'supervision_drift_report','supervision_drift_subgroup_metric', 'supervision_phase3_manifest','supervision_phase3_artifact' ] 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; $$; DROP TRIGGER IF EXISTS trg_supervision_teacher_event_append_only ON audit.supervision_teacher_event; CREATE TRIGGER trg_supervision_teacher_event_append_only BEFORE UPDATE OR DELETE ON audit.supervision_teacher_event FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); ALTER TABLE audit.supervision_teacher_event ENABLE ROW LEVEL SECURITY; DROP TRIGGER IF EXISTS trg_supervision_teacher_event_pointer_array ON audit.supervision_teacher_event; CREATE TRIGGER trg_supervision_teacher_event_pointer_array BEFORE INSERT ON audit.supervision_teacher_event FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_pointer_array(); DROP TRIGGER IF EXISTS trg_supervision_evidence_pointer_contract ON app.supervision_evidence_pointer; CREATE TRIGGER trg_supervision_evidence_pointer_contract BEFORE INSERT ON app.supervision_evidence_pointer FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_evidence_pointer(); DROP TRIGGER IF EXISTS trg_supervision_attention_item_contract ON app.supervision_attention_item; CREATE TRIGGER trg_supervision_attention_item_contract BEFORE INSERT ON app.supervision_attention_item FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_attention_child(); DROP TRIGGER IF EXISTS trg_supervision_attention_reason_contract ON app.supervision_attention_reason; CREATE TRIGGER trg_supervision_attention_reason_contract BEFORE INSERT ON app.supervision_attention_reason FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_attention_child(); DROP TRIGGER IF EXISTS trg_supervision_teacher_ai_actor ON app.supervision_teacher_ai_disagreement; CREATE TRIGGER trg_supervision_teacher_ai_actor BEFORE INSERT ON app.supervision_teacher_ai_disagreement FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_teacher_actor(); DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'supervision_attention_snapshot','supervision_attention_item','supervision_attention_reason', 'supervision_teacher_ai_disagreement','supervision_calibration_dataset_row', 'supervision_curriculum_gap_snapshot','supervision_evaluation_observation', 'supervision_drift_report','supervision_phase3_artifact' ] LOOP EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_pointer_array ON app.%I', table_name, table_name); EXECUTE format('CREATE TRIGGER trg_%s_pointer_array BEFORE INSERT ON app.%I FOR EACH ROW EXECUTE FUNCTION audit.enforce_supervision_pointer_array()', table_name, table_name); END LOOP; END; $$; -- Evidence pointer is view-partitioned for AI and learner/cohort scoped for humans. DROP POLICY IF EXISTS p_supervision_evidence_pointer_select ON app.supervision_evidence_pointer; DROP POLICY IF EXISTS p_supervision_evidence_pointer_insert ON app.supervision_evidence_pointer; CREATE POLICY p_supervision_evidence_pointer_select ON app.supervision_evidence_pointer FOR SELECT USING ( (app.is_ai_context() AND consumer_view = current_setting('app.current_ai_view', true)) OR (NOT app.is_ai_context() AND ( app.current_role_name() = 'admin' OR (app.current_role_name() = 'learner' AND learner_id = app.current_uid()) OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true)) )) ); CREATE POLICY p_supervision_evidence_pointer_insert ON app.supervision_evidence_pointer FOR INSERT WITH CHECK ( (app.is_ai_context() AND consumer_view = current_setting('app.current_ai_view', true) AND consumer_view IN ('supervisor','research')) OR (NOT app.is_ai_context() AND app.current_role_name() IN ('instructor','admin') AND consumer_view = 'research' AND (app.current_role_name() = 'admin' OR cohort_id = current_setting('app.current_cohort', true))) ); -- Supervisor-only AI relations. Learners can see only their own queue rows, never cohort snapshots/gaps. DROP POLICY IF EXISTS p_supervision_attention_snapshot_select ON app.supervision_attention_snapshot; DROP POLICY IF EXISTS p_supervision_attention_snapshot_insert ON app.supervision_attention_snapshot; CREATE POLICY p_supervision_attention_snapshot_select ON app.supervision_attention_snapshot FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor') OR (NOT app.is_ai_context() AND (app.current_role_name() = 'admin' OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true)))) ); CREATE POLICY p_supervision_attention_snapshot_insert ON app.supervision_attention_snapshot FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor' ); DROP POLICY IF EXISTS p_supervision_attention_item_select ON app.supervision_attention_item; DROP POLICY IF EXISTS p_supervision_attention_item_insert ON app.supervision_attention_item; CREATE POLICY p_supervision_attention_item_select ON app.supervision_attention_item FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor') OR (NOT app.is_ai_context() AND (app.current_role_name() = 'admin' OR (app.current_role_name() = 'learner' AND learner_id = app.current_uid()) OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true)))) ); CREATE POLICY p_supervision_attention_item_insert ON app.supervision_attention_item FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor' ); DROP POLICY IF EXISTS p_supervision_attention_reason_select ON app.supervision_attention_reason; DROP POLICY IF EXISTS p_supervision_attention_reason_insert ON app.supervision_attention_reason; CREATE POLICY p_supervision_attention_reason_select ON app.supervision_attention_reason FOR SELECT USING ( EXISTS (SELECT 1 FROM app.supervision_attention_item i WHERE i.item_id = item_id) ); CREATE POLICY p_supervision_attention_reason_insert ON app.supervision_attention_reason FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor' ); DROP POLICY IF EXISTS p_supervision_curriculum_gap_select ON app.supervision_curriculum_gap_snapshot; DROP POLICY IF EXISTS p_supervision_curriculum_gap_insert ON app.supervision_curriculum_gap_snapshot; CREATE POLICY p_supervision_curriculum_gap_select ON app.supervision_curriculum_gap_snapshot FOR SELECT USING ( (app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor') OR (NOT app.is_ai_context() AND (app.current_role_name() = 'admin' OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true)))) ); CREATE POLICY p_supervision_curriculum_gap_insert ON app.supervision_curriculum_gap_snapshot FOR INSERT WITH CHECK ( app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'supervisor' ); -- Research-only AI relations, cohort-scoped for humans. Dataset/audit remain metadata-only. DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'supervision_teacher_ai_disagreement','supervision_calibration_dataset_row', 'supervision_evaluation_batch','supervision_evaluation_observation', 'supervision_drift_report','supervision_drift_subgroup_metric', 'supervision_phase3_manifest','supervision_phase3_artifact' ] LOOP EXECUTE format('DROP POLICY IF EXISTS p_%s_select 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'' OR ' || '(app.current_role_name() = ''instructor'' AND cohort_id = current_setting(''app.current_cohort'', true))' || CASE WHEN table_name IN ('supervision_teacher_ai_disagreement','supervision_calibration_dataset_row') THEN ' OR (app.current_role_name() = ''learner'' AND learner_id = app.current_uid())' ELSE '' END || ')))', table_name, table_name ); END LOOP; END; $$; DROP POLICY IF EXISTS p_supervision_teacher_ai_disagreement_insert ON app.supervision_teacher_ai_disagreement; CREATE POLICY p_supervision_teacher_ai_disagreement_insert ON app.supervision_teacher_ai_disagreement FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND ( app.current_role_name() = 'admin' OR (app.current_role_name() = 'instructor' AND created_by_uid = app.current_uid() AND cohort_id = current_setting('app.current_cohort', true)) ) ); DROP POLICY IF EXISTS p_supervision_calibration_dataset_row_insert ON app.supervision_calibration_dataset_row; CREATE POLICY p_supervision_calibration_dataset_row_insert ON app.supervision_calibration_dataset_row FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND (app.current_role_name() = 'admin' OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true))) ); DO $$ DECLARE table_name TEXT; BEGIN FOREACH table_name IN ARRAY ARRAY[ 'supervision_evaluation_batch','supervision_evaluation_observation', 'supervision_drift_report','supervision_drift_subgroup_metric', 'supervision_phase3_manifest','supervision_phase3_artifact' ] LOOP EXECUTE format('DROP POLICY IF EXISTS p_%s_insert ON app.%I', 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_supervision_teacher_event_select ON audit.supervision_teacher_event; DROP POLICY IF EXISTS p_supervision_teacher_event_insert ON audit.supervision_teacher_event; CREATE POLICY p_supervision_teacher_event_select ON audit.supervision_teacher_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' OR (app.current_role_name() = 'learner' AND learner_id = app.current_uid()) OR (app.current_role_name() = 'instructor' AND cohort_id = current_setting('app.current_cohort', true)))) ); CREATE POLICY p_supervision_teacher_event_insert ON audit.supervision_teacher_event FOR INSERT WITH CHECK ( NOT app.is_ai_context() AND (app.current_role_name() = 'admin' OR (app.current_role_name() = 'instructor' AND actor_uid = app.current_uid() AND cohort_id = current_setting('app.current_cohort', true))) ); 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 FUNCTION audit.enforce_supervision_evidence_pointer() TO %I', app_role); EXECUTE format('GRANT EXECUTE ON FUNCTION audit.enforce_supervision_pointer_array() TO %I', app_role); EXECUTE format('GRANT EXECUTE ON FUNCTION audit.enforce_supervision_attention_child() TO %I', app_role); EXECUTE format('GRANT EXECUTE ON FUNCTION audit.enforce_supervision_teacher_actor() TO %I', app_role); END IF; END LOOP; END; $$;