\set ON_ERROR_STOP on \echo 'G3 rupture/repair ledger live smoke: begin' BEGIN; INSERT INTO app.app_user (user_id, external_id, email, display_name, role, cohort) VALUES ('91000000-0000-0000-0000-000000000001','smoke:g3:learner:a','g3-a@example.invalid','G3 A','learner','g3-a'), ('91000000-0000-0000-0000-000000000002','smoke:g3:learner:b','g3-b@example.invalid','G3 B','learner','g3-b'), ('91000000-0000-0000-0000-000000000003','smoke:g3:teacher','g3-t@example.invalid','G3 T','instructor','g3-a'), ('91000000-0000-0000-0000-000000000004','smoke:g3:admin','g3-admin@example.invalid','G3 Admin','admin',NULL); INSERT INTO app.case_profile (case_id, persona_id, learner_id) VALUES ('81000000-0000-0000-0000-000000000001','71000000-0000-0000-0000-000000000001','91000000-0000-0000-0000-000000000001'), ('81000000-0000-0000-0000-000000000002','71000000-0000-0000-0000-000000000002','91000000-0000-0000-0000-000000000002'); INSERT INTO app.sessions (id, case_id, learner_id, session_no, ended_at) VALUES ('61000000-0000-0000-0000-000000000001','81000000-0000-0000-0000-000000000001','91000000-0000-0000-0000-000000000001',1,now()), ('61000000-0000-0000-0000-000000000002','81000000-0000-0000-0000-000000000002','91000000-0000-0000-0000-000000000002',1,now()); INSERT INTO app.turns (id, session_id, seq, speaker, text, text_masked) VALUES ('51000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001',1,'client','[smoke] 불편함','[smoke] 불편함'), ('51000000-0000-0000-0000-000000000002','61000000-0000-0000-0000-000000000001',2,'counselor','[smoke] 다시 확인','[smoke] 다시 확인'), ('51000000-0000-0000-0000-000000000003','61000000-0000-0000-0000-000000000002',1,'client','[smoke] other','[smoke] other'); INSERT INTO audit.model_run ( model_run_id, session_id, agent_role, provider, model, prompt_bundle_id, prompt_bundle_version, prompt_bundle_hash, structured_schema_version, input_evidence_hash, status ) VALUES ( '41000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001', 'evaluator','smoke','smoke-fast','rupture-observer','1.0.0',repeat('a',64), 'vignette.rupture-repair-assessment.v1',repeat('b',64),'ready' ), ( '41000000-0000-0000-0000-000000000002','61000000-0000-0000-0000-000000000001', 'evaluator','smoke','smoke-deep','rupture-observer','1.0.0',repeat('c',64), 'vignette.rupture-repair-assessment.v1',repeat('d',64),'ready' ); INSERT INTO app.safety_events ( id, session_id, turn_id, trigger_type, ko_risk_level, escalated, detail ) OVERRIDING SYSTEM VALUE VALUES (980000001,'61000000-0000-0000-0000-000000000001', '51000000-0000-0000-0000-000000000001','smoke_reference',2,true, '{"smoke":true}'::jsonb), (980000002,'61000000-0000-0000-0000-000000000002', '51000000-0000-0000-0000-000000000003','smoke_other_session',1,false, '{"smoke":true}'::jsonb); SET LOCAL ROLE vignette; SELECT set_config('app.ai_context','1',true); SELECT set_config('app.current_ai_view','evaluator',true); SELECT set_config('app.current_role','',true); SELECT set_config('app.current_uid','',true); SELECT set_config('app.current_cohort','',true); INSERT INTO app.rupture_episode ( episode_id, session_id, case_id, learner_id, episode_key, created_by_role ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001', '81000000-0000-0000-0000-000000000001','91000000-0000-0000-0000-000000000001', 'smoke-g3-episode','agent' ); INSERT INTO app.rupture_observation_event ( observation_id, episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, confidence, uncertainty, evidence_turn_ids, model_run_id, created_by_role ) VALUES ( '21000000-0000-0000-0000-000000000001','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000001',repeat('1',64), 'rupture.detected',NULL,'onset','withdrawal','model_inferred','independent_observer', 'evaluator',0.80,0.20,ARRAY['51000000-0000-0000-0000-000000000001']::uuid[], '41000000-0000-0000-0000-000000000001','agent' ), ( '21000000-0000-0000-0000-000000000002','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000002',repeat('2',64), 'rupture.recognized','onset','recognized','withdrawal','model_inferred','independent_observer', 'evaluator',0.90,0.10,ARRAY['51000000-0000-0000-0000-000000000002']::uuid[], '41000000-0000-0000-0000-000000000002','agent' ), ( '21000000-0000-0000-0000-000000000003','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000003',repeat('3',64), 'repair.attempted','recognized','repair_attempted','withdrawal','model_inferred','independent_observer', 'evaluator',0.90,0.10,ARRAY['51000000-0000-0000-0000-000000000002']::uuid[], '41000000-0000-0000-0000-000000000002','agent' ), ( '21000000-0000-0000-0000-000000000004','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000004',repeat('4',64), 'repair.resolved','repair_attempted','resolved','withdrawal','model_inferred','independent_observer', 'evaluator',0.94,0.06,ARRAY['51000000-0000-0000-0000-000000000001','51000000-0000-0000-0000-000000000002']::uuid[], '41000000-0000-0000-0000-000000000002','agent' ); -- Same idempotency key + same hash is a no-op. INSERT INTO app.rupture_observation_event ( observation_id, episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, confidence, uncertainty, evidence_turn_ids, model_run_id, created_by_role ) VALUES ( '21000000-0000-0000-0000-000000000011','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000001',repeat('1',64), 'rupture.detected',NULL,'onset','withdrawal','model_inferred','independent_observer', 'evaluator',0.80,0.20,ARRAY['51000000-0000-0000-0000-000000000001']::uuid[], '41000000-0000-0000-0000-000000000001','agent' ) ON CONFLICT DO NOTHING; DO $$ BEGIN BEGIN INSERT INTO app.rupture_observation_event ( episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, confidence, uncertainty, evidence_turn_ids, model_run_id, created_by_role ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001', '11000000-0000-0000-0000-000000000001',repeat('9',64), 'rupture.detected',NULL,'onset','withdrawal','model_inferred','independent_observer', 'evaluator',0.80,0.20,ARRAY['51000000-0000-0000-0000-000000000001']::uuid[], '41000000-0000-0000-0000-000000000001','agent' ); RAISE EXCEPTION 'mismatched observation idempotency key was accepted'; EXCEPTION WHEN unique_violation THEN NULL; END; BEGIN INSERT INTO app.rupture_observation_event ( episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, confidence, uncertainty, evidence_turn_ids, model_run_id, created_by_role ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001', '11000000-0000-0000-0000-000000000012',repeat('a',64), 'repair.attempted','resolved','repair_attempted','withdrawal','model_inferred','independent_observer', 'evaluator',0.80,0.20,ARRAY['51000000-0000-0000-0000-000000000002']::uuid[], '41000000-0000-0000-0000-000000000002','agent' ); RAISE EXCEPTION 'resolved rupture was allowed to move backward'; EXCEPTION WHEN check_violation THEN NULL; END; BEGIN INSERT INTO app.rupture_observation_event ( episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, confidence, uncertainty, evidence_turn_ids, model_run_id, created_by_role ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001', '11000000-0000-0000-0000-000000000013',repeat('b',64), 'repair.attempted','resolved','repair_attempted','withdrawal','model_inferred','independent_observer', 'evaluator',0.80,0.20,ARRAY['51000000-0000-0000-0000-000000000003']::uuid[], '41000000-0000-0000-0000-000000000002','agent' ); RAISE EXCEPTION 'cross-session evidence turn was accepted'; EXCEPTION WHEN check_violation THEN NULL; END; END; $$; INSERT INTO app.rupture_safety_reference ( episode_id, session_id, safety_event_id ) VALUES ( '31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001',980000001 ); DO $$ BEGIN BEGIN INSERT INTO app.rupture_safety_reference ( episode_id, session_id, safety_event_id ) VALUES ( '31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001',980000002 ); RAISE EXCEPTION 'cross-session safety reference was accepted'; EXCEPTION WHEN check_violation THEN NULL; END; END; $$; INSERT INTO app.rupture_reconciliation_revision ( revision_id, episode_id, session_id, revision_no, idempotency_key, content_hash, fast_warning_observation_id, deep_observation_id, fast_warning_id, provisional_status, deep_status, disposition, uncertainty, evidence_turn_ids, model_run_id ) VALUES ( '20000000-0000-0000-0000-000000000001','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001',1, '10000000-0000-0000-0000-000000000001',repeat('5',64), '21000000-0000-0000-0000-000000000001','21000000-0000-0000-0000-000000000004', 'smoke-fast-warning','missed','resolved','superseded_resolved',0.06, ARRAY['51000000-0000-0000-0000-000000000001','51000000-0000-0000-0000-000000000002']::uuid[], '41000000-0000-0000-0000-000000000002' ); INSERT INTO app.rupture_reconciliation_revision ( revision_id, episode_id, session_id, revision_no, idempotency_key, content_hash, fast_warning_observation_id, deep_observation_id, fast_warning_id, provisional_status, deep_status, disposition, uncertainty, evidence_turn_ids, model_run_id ) VALUES ( '20000000-0000-0000-0000-000000000011','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001',1, '10000000-0000-0000-0000-000000000001',repeat('5',64), '21000000-0000-0000-0000-000000000001','21000000-0000-0000-0000-000000000004', 'smoke-fast-warning','missed','resolved','superseded_resolved',0.06, ARRAY['51000000-0000-0000-0000-000000000001']::uuid[], '41000000-0000-0000-0000-000000000002' ) ON CONFLICT DO NOTHING; DO $$ BEGIN BEGIN INSERT INTO app.rupture_reconciliation_revision ( episode_id, session_id, revision_no, idempotency_key, content_hash, fast_warning_observation_id, deep_observation_id, fast_warning_id, provisional_status, deep_status, disposition, uncertainty, model_run_id ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001',1, '10000000-0000-0000-0000-000000000001',repeat('6',64), '21000000-0000-0000-0000-000000000001','21000000-0000-0000-0000-000000000004', 'smoke-fast-warning','missed','resolved','superseded_resolved',0.06, '41000000-0000-0000-0000-000000000002' ); RAISE EXCEPTION 'mismatched reconciliation idempotency key was accepted'; EXCEPTION WHEN unique_violation THEN NULL; END; BEGIN INSERT INTO app.rupture_reconciliation_revision ( episode_id, session_id, revision_no, idempotency_key, content_hash, supersedes_revision_id, fast_warning_observation_id, deep_observation_id, fast_warning_id, provisional_status, deep_status, disposition, uncertainty, model_run_id ) VALUES ( '31000000-0000-0000-0000-000000000001','61000000-0000-0000-0000-000000000001',2, '10000000-0000-0000-0000-000000000002',repeat('7',64), '20000000-0000-0000-0000-000000000001', '21000000-0000-0000-0000-000000000001','21000000-0000-0000-0000-000000000004', 'smoke-fast-warning','partial','partial','confirmed',0.10, '41000000-0000-0000-0000-000000000002' ); RAISE EXCEPTION 'resolved reconciliation was allowed to move backward'; EXCEPTION WHEN check_violation THEN NULL; END; END; $$; -- Human RLS: owner learner, other learner, instructor cohort, wrong cohort, admin. SELECT set_config('app.ai_context','',true); SELECT set_config('app.current_ai_view','',true); SELECT set_config('app.current_role','learner',true); SELECT set_config('app.current_uid','91000000-0000-0000-0000-000000000001',true); DO $$ BEGIN IF (SELECT count(*) FROM app.rupture_episode) <> 1 THEN RAISE EXCEPTION 'owner learner RLS failed'; END IF; END $$; SELECT set_config('app.current_uid','91000000-0000-0000-0000-000000000002',true); DO $$ BEGIN IF (SELECT count(*) FROM app.rupture_episode) <> 0 THEN RAISE EXCEPTION 'cross-learner RLS leak'; END IF; END $$; SELECT set_config('app.current_role','instructor',true); SELECT set_config('app.current_uid','91000000-0000-0000-0000-000000000003',true); SELECT set_config('app.current_cohort','g3-a',true); DO $$ BEGIN IF (SELECT count(*) FROM app.rupture_episode) <> 1 THEN RAISE EXCEPTION 'instructor cohort RLS failed'; END IF; END $$; INSERT INTO app.rupture_observation_event ( observation_id, episode_id, session_id, idempotency_key, content_hash, event_kind, from_state, to_state, rupture_type, source_kind, perspective, ai_view, uncertainty, evidence_turn_ids, supersedes_observation_id, correction_reason, created_by_uid, created_by_role ) VALUES ( '21000000-0000-0000-0000-000000000005','31000000-0000-0000-0000-000000000001', '61000000-0000-0000-0000-000000000001','11000000-0000-0000-0000-000000000005',repeat('8',64), 'human.corrected','resolved','resolved','withdrawal','human_rated','supervisor_human', 'supervisor',0.04,ARRAY['51000000-0000-0000-0000-000000000002']::uuid[], '21000000-0000-0000-0000-000000000004','교수자 근거 확인', '91000000-0000-0000-0000-000000000003','instructor' ); SELECT set_config('app.current_cohort','g3-b',true); DO $$ BEGIN IF (SELECT count(*) FROM app.rupture_episode) <> 0 THEN RAISE EXCEPTION 'cross-cohort instructor RLS leak'; END IF; END $$; SELECT set_config('app.current_role','admin',true); SELECT set_config('app.current_uid','91000000-0000-0000-0000-000000000004',true); SELECT set_config('app.current_cohort','',true); DO $$ BEGIN IF (SELECT count(*) FROM app.rupture_episode) <> 1 THEN RAISE EXCEPTION 'admin RLS failed'; END IF; END $$; -- Append-only mutation guard. -- Reset to the table owner so the row reaches the mutation trigger instead of -- being filtered to zero rows by the intentionally absent UPDATE/DELETE policy. RESET ROLE; DO $$ BEGIN BEGIN UPDATE app.rupture_observation_event SET uncertainty = 0.5 WHERE observation_id = '21000000-0000-0000-0000-000000000004'; RAISE EXCEPTION 'append-only observation update was accepted'; EXCEPTION WHEN SQLSTATE '55000' THEN NULL; END; BEGIN DELETE FROM app.rupture_reconciliation_revision WHERE revision_id = '20000000-0000-0000-0000-000000000001'; RAISE EXCEPTION 'append-only reconciliation delete was accepted'; EXCEPTION WHEN SQLSTATE '55000' THEN NULL; END; END; $$; ROLLBACK; DO $$ BEGIN IF EXISTS ( SELECT 1 FROM app.rupture_episode WHERE episode_key = 'smoke-g3-episode' ) OR EXISTS ( SELECT 1 FROM app.app_user WHERE external_id LIKE 'smoke:g3:%' ) THEN RAISE EXCEPTION 'G3 smoke rollback left residue'; END IF; END; $$; SELECT 1 AS schema_idempotent, 1 AS rls_pass, 1 AS append_only_pass, 1 AS reconciliation_pass, 1 AS idempotency_pass, 0 AS residue_count; \echo 'G3 rupture/repair ledger live smoke: PASS'