\set ON_ERROR_STOP on -- Destructive-looking guard checks are isolated inside this transaction. The -- final ROLLBACK guarantees that no smoke fixture survives in the live DB. BEGIN; INSERT INTO app.app_user ( user_id, external_id, email, display_name, role, cohort ) VALUES ('00000000-0000-0000-0000-00000000c101', 'smoke:g2:learner', 'g2-learner@example.invalid', 'G2 Learner', 'learner', 'g2-cohort-a'), ('00000000-0000-0000-0000-00000000c102', 'smoke:g2:other', 'g2-other@example.invalid', 'G2 Other', 'learner', 'g2-cohort-b'), ('00000000-0000-0000-0000-00000000c103', 'smoke:g2:teacher', 'g2-teacher@example.invalid', 'G2 Teacher', 'instructor', 'g2-cohort-a'); INSERT INTO app.case_profile ( case_id, persona_id, learner_id, last_session_no, case_digest ) VALUES ( '00000000-0000-0000-0000-00000000c110', '00000000-0000-0000-0000-00000000c109', '00000000-0000-0000-0000-00000000c101', 1, 'G2 rollback smoke case' ); INSERT INTO app.sessions ( id, case_id, learner_id, session_no, theory_mode, ended_at ) VALUES ( '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', '00000000-0000-0000-0000-00000000c101', 1, 'integrative', now() ); INSERT INTO app.turns ( id, session_id, seq, speaker, text, actor_kind, visible_to ) VALUES ( '00000000-0000-0000-0000-00000000c121', '00000000-0000-0000-0000-00000000c111', 1, 'client', '교육용 G2 근거 발화', 'client_ai', ARRAY['client','counselor','evaluator','supervisor']::text[] ); INSERT INTO app.measurement_event ( measurement_id, session_id, construct, dimension, perspective, source_kind, instrument_id, instrument_version, value, scale_min, scale_max, confidence, status, evidence_turn_ids, visible_to ) VALUES ( '00000000-0000-0000-0000-00000000c131', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'distress_load', 'client_simulation', 'simulated_state', 'vignette-session-outcome', '1.0.0', 0.7, 0, 1, 0.9, 'ready', ARRAY['00000000-0000-0000-0000-00000000c121']::uuid[], ARRAY['counselor','evaluator','supervisor']::text[] ); SET LOCAL ROLE vignette; SELECT set_config('app.ai_context', '', true); SELECT set_config('app.current_role', 'learner', true); SELECT set_config('app.current_uid', '00000000-0000-0000-0000-00000000c101', true); SELECT set_config('app.current_cohort', 'g2-cohort-a', true); -- A learner check-in is a three-axis immutable submission. Empty transcript -- evidence is valid because the learner response itself is the provenance. INSERT INTO app.measurement_event ( measurement_id, session_id, construct, dimension, perspective, source_kind, instrument_id, instrument_version, value, scale_min, scale_max, confidence, status, evidence_turn_ids, visible_to, metadata ) VALUES ( '00000000-0000-0000-0000-00000000c161', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'distress_load', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.72, 0, 1, 0.9, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c160','submission_hash',repeat('d',64)) ), ( '00000000-0000-0000-0000-00000000c162', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'daily_functioning', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.44, 0, 1, 0.8, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c160','submission_hash',repeat('d',64)) ), ( '00000000-0000-0000-0000-00000000c163', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'learning_engagement', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.81, 0, 1, 0.85, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c160','submission_hash',repeat('d',64)) ); -- A later submission creates one leaf per axis and supersedes the first set. INSERT INTO app.measurement_event ( measurement_id, session_id, supersedes_id, construct, dimension, perspective, source_kind, instrument_id, instrument_version, value, scale_min, scale_max, confidence, status, evidence_turn_ids, visible_to, metadata ) VALUES ( '00000000-0000-0000-0000-00000000c171', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c161', 'session_outcome', 'distress_load', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.68, 0, 1, 0.91, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c170','submission_hash',repeat('e',64)) ), ( '00000000-0000-0000-0000-00000000c172', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c162', 'session_outcome', 'daily_functioning', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.48, 0, 1, 0.82, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c170','submission_hash',repeat('e',64)) ), ( '00000000-0000-0000-0000-00000000c173', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c163', 'session_outcome', 'learning_engagement', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.84, 0, 1, 0.87, 'ready', ARRAY[]::uuid[], ARRAY['counselor','evaluator','supervisor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c170','submission_hash',repeat('e',64)) ); DO $$ BEGIN IF ( SELECT count(*) FROM app.measurement_event m WHERE m.session_id = '00000000-0000-0000-0000-00000000c111' AND m.instrument_id = 'vignette-session-outcome-checkin' AND NOT EXISTS ( SELECT 1 FROM app.measurement_event child WHERE child.supersedes_id = m.measurement_id ) ) <> 3 THEN RAISE EXCEPTION 'learner check-in must keep exactly one leaf per outcome axis'; END IF; BEGIN INSERT INTO app.measurement_event ( measurement_id, session_id, construct, dimension, perspective, source_kind, instrument_id, instrument_version, value, scale_min, scale_max, confidence, status, evidence_turn_ids, visible_to, metadata ) VALUES ( '00000000-0000-0000-0000-00000000c164', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'distress_load', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.2, 0, 1, 0.2, 'ready', ARRAY[]::uuid[], ARRAY['counselor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c160','submission_hash',repeat('f',64)) ); RAISE EXCEPTION 'duplicate submission axis was not rejected'; EXCEPTION WHEN unique_violation THEN NULL; END; BEGIN INSERT INTO app.measurement_event ( measurement_id, session_id, construct, dimension, perspective, source_kind, instrument_id, instrument_version, value, scale_min, scale_max, confidence, status, evidence_turn_ids, visible_to, metadata ) VALUES ( '00000000-0000-0000-0000-00000000c165', '00000000-0000-0000-0000-00000000c111', 'session_outcome', 'distress_load', 'learner_self_report', 'learner_reported', 'vignette-session-outcome-checkin', '1.0.0', 0.2, 0, 1, 0.2, 'ready', ARRAY['00000000-0000-0000-0000-00000000c199']::uuid[], ARRAY['counselor']::text[], jsonb_build_object('submission_id','00000000-0000-0000-0000-00000000c166','submission_hash',repeat('f',64)) ); RAISE EXCEPTION 'cross-session or missing evidence turn was not rejected'; EXCEPTION WHEN check_violation THEN NULL; END; END; $$; INSERT INTO app.outcome_trajectory_revision ( revision_id, anchor_session_id, case_id, learner_id, expected_arc_id, revision_no, source_fingerprint, assessment, observation_count, missing_observation_count, recompute_reason, computed_by, computed_role ) VALUES ( '00000000-0000-0000-0000-00000000c141', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', '00000000-0000-0000-0000-00000000c101', 'oas-g2-arc-001', 1, repeat('a', 64), '{"schema_version":"vignette.outcome-trajectory-assessment.v1","expected_arc_id":"oas-g2-arc-001","data_classification":"synthetic_educational","clinical_claim_allowed":false,"sessions":[]}'::jsonb, 3, 2, 'rollback_smoke_initial', '00000000-0000-0000-0000-00000000c101', 'learner' ); INSERT INTO app.outcome_trajectory_observation ( revision_id, measurement_id, session_id, session_no, axis, status, value, raw_value, scale_min, scale_max, confidence, source_kind, perspective, instrument_id, instrument_version, evidence_turn_ids, missing_reason, source_created_at ) VALUES ( '00000000-0000-0000-0000-00000000c141', '00000000-0000-0000-0000-00000000c131', '00000000-0000-0000-0000-00000000c111', 1, 'distress_load', 'observed', 0.7, 0.7, 0, 1, 0.9, 'simulated_state', 'client_simulation', 'vignette-session-outcome', '1.0.0', ARRAY['00000000-0000-0000-0000-00000000c121']::uuid[], NULL, now() ), ( '00000000-0000-0000-0000-00000000c141', NULL, '00000000-0000-0000-0000-00000000c111', 1, 'daily_functioning', 'missing', NULL, NULL, NULL, NULL, NULL, 'observed_runtime', 'runtime_observation', 'vignette-outcome-evidence-gap', '1.0.0', ARRAY[]::uuid[], 'measurement_not_collected', NULL ), ( '00000000-0000-0000-0000-00000000c141', NULL, '00000000-0000-0000-0000-00000000c111', 1, 'learning_engagement', 'missing', NULL, NULL, NULL, NULL, NULL, 'observed_runtime', 'runtime_observation', 'vignette-outcome-evidence-gap', '1.0.0', ARRAY[]::uuid[], 'measurement_not_collected', NULL ); -- Same evidence can be recomputed only as a new revision linked to its predecessor. INSERT INTO app.outcome_trajectory_revision ( revision_id, anchor_session_id, case_id, learner_id, expected_arc_id, revision_no, supersedes_revision_id, source_fingerprint, assessment, observation_count, missing_observation_count, recompute_reason, computed_by, computed_role ) VALUES ( '00000000-0000-0000-0000-00000000c142', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', '00000000-0000-0000-0000-00000000c101', 'oas-g2-arc-001', 2, '00000000-0000-0000-0000-00000000c141', repeat('a', 64), '{"schema_version":"vignette.outcome-trajectory-assessment.v1","expected_arc_id":"oas-g2-arc-001","data_classification":"synthetic_educational","clinical_claim_allowed":false,"sessions":[]}'::jsonb, 3, 2, 'rollback_smoke_recompute', '00000000-0000-0000-0000-00000000c101', 'learner' ); DO $$ BEGIN IF (SELECT count(*) FROM app.outcome_trajectory_revision) <> 2 THEN RAISE EXCEPTION 'owner learner must see its two trajectory revisions'; END IF; IF ( SELECT count(*) FROM app.outcome_trajectory_observation WHERE revision_id = '00000000-0000-0000-0000-00000000c141' ) <> 3 THEN RAISE EXCEPTION 'trajectory revision must preserve all three explicit axes'; END IF; IF ( SELECT count(*) FROM app.outcome_trajectory_observation WHERE revision_id = '00000000-0000-0000-0000-00000000c141' AND status = 'missing' AND value IS NULL ) <> 2 THEN RAISE EXCEPTION 'missing outcome axes must stay scoreless'; END IF; END; $$; RESET ROLE; SET LOCAL ROLE vignette; SELECT set_config('app.current_role', 'instructor', true); SELECT set_config('app.current_uid', '00000000-0000-0000-0000-00000000c103', true); SELECT set_config('app.current_cohort', 'g2-cohort-a', true); INSERT INTO app.relationship_memory_event ( memory_event_id, session_id, case_id, event_type, visible_to, evidence_turn_ids, source_kind, created_by ) VALUES ( '00000000-0000-0000-0000-00000000c151', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', 'unresolved_rupture', ARRAY['counselor','supervisor']::text[], ARRAY['00000000-0000-0000-0000-00000000c121']::uuid[], 'human_rated', '00000000-0000-0000-0000-00000000c103' ); INSERT INTO app.relationship_memory_projection ( memory_event_id, ai_view, summary ) VALUES ('00000000-0000-0000-0000-00000000c151', 'counselor', '과제 합의를 다음 회기에 다시 확인한다.'), ('00000000-0000-0000-0000-00000000c151', 'supervisor', '표면 동의 뒤 남은 과제 부담을 재확인한다.'); INSERT INTO app.relationship_memory_event ( memory_event_id, session_id, case_id, event_type, resolves_event_id, visible_to, evidence_turn_ids, source_kind, created_by ) VALUES ( '00000000-0000-0000-0000-00000000c153', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', 'repair_confirmed', '00000000-0000-0000-0000-00000000c151', ARRAY['supervisor']::text[], ARRAY['00000000-0000-0000-0000-00000000c121']::uuid[], 'human_rated', '00000000-0000-0000-0000-00000000c103' ); INSERT INTO app.relationship_memory_projection ( memory_event_id, ai_view, summary ) VALUES ( '00000000-0000-0000-0000-00000000c153', 'supervisor', '미해결 균열을 다음 회기 재합의로 복구했다.' ); DO $$ BEGIN BEGIN INSERT INTO app.relationship_memory_event ( memory_event_id, session_id, case_id, event_type, visible_to, evidence_turn_ids, source_kind, created_by ) VALUES ( '00000000-0000-0000-0000-00000000c152', '00000000-0000-0000-0000-00000000c111', '00000000-0000-0000-0000-00000000c110', 'goal_agreement', ARRAY['supervisor']::text[], ARRAY['00000000-0000-0000-0000-00000000c199']::uuid[], 'human_rated', '00000000-0000-0000-0000-00000000c103' ); RAISE EXCEPTION 'relationship evidence ownership violation was not rejected'; EXCEPTION WHEN check_violation THEN NULL; END; END; $$; DO $$ BEGIN IF (SELECT count(*) FROM app.outcome_trajectory_revision) <> 2 THEN RAISE EXCEPTION 'same-cohort instructor must see trajectory revisions'; END IF; IF (SELECT count(*) FROM app.relationship_memory_projection) <> 2 THEN RAISE EXCEPTION 'instructor must see only two supervisor/evaluator projections'; END IF; END; $$; SELECT set_config('app.current_cohort', 'g2-cohort-b', true); DO $$ BEGIN IF (SELECT count(*) FROM app.outcome_trajectory_revision) <> 0 THEN RAISE EXCEPTION 'cross-cohort instructor trajectory leak'; END IF; IF (SELECT count(*) FROM app.relationship_memory_projection) <> 0 THEN RAISE EXCEPTION 'cross-cohort instructor memory leak'; END IF; END; $$; SELECT set_config('app.current_role', 'learner', true); SELECT set_config('app.current_uid', '00000000-0000-0000-0000-00000000c102', true); DO $$ BEGIN IF (SELECT count(*) FROM app.outcome_trajectory_revision) <> 0 THEN RAISE EXCEPTION 'other learner trajectory leak'; END IF; END; $$; SELECT set_config('app.current_uid', '00000000-0000-0000-0000-00000000c101', true); DO $$ BEGIN IF (SELECT count(*) FROM app.relationship_memory_projection) <> 1 THEN RAISE EXCEPTION 'owner learner must see exactly the counselor projection'; END IF; IF EXISTS ( SELECT 1 FROM app.relationship_memory_projection WHERE ai_view <> 'counselor' ) THEN RAISE EXCEPTION 'supervisor relationship summary leaked to learner'; END IF; END; $$; RESET ROLE; DO $$ BEGIN BEGIN UPDATE app.outcome_trajectory_revision SET recompute_reason = 'forbidden mutation' WHERE revision_id = '00000000-0000-0000-0000-00000000c141'; RAISE EXCEPTION 'trajectory revision mutation was not rejected'; EXCEPTION WHEN object_not_in_prerequisite_state THEN NULL; END; BEGIN DELETE FROM app.relationship_memory_projection WHERE projection_id = ( SELECT projection_id FROM app.relationship_memory_projection LIMIT 1 ); RAISE EXCEPTION 'relationship projection deletion was not rejected'; EXCEPTION WHEN object_not_in_prerequisite_state THEN NULL; END; END; $$; SELECT (SELECT count(*) FROM app.outcome_trajectory_revision WHERE case_id = '00000000-0000-0000-0000-00000000c110') AS revisions_before_rollback, (SELECT count(*) FROM app.outcome_trajectory_observation WHERE session_id = '00000000-0000-0000-0000-00000000c111') AS observations_before_rollback, (SELECT count(*) FROM app.relationship_memory_projection p JOIN app.relationship_memory_event e USING (memory_event_id) WHERE e.case_id = '00000000-0000-0000-0000-00000000c110') AS projections_before_rollback, (SELECT count(*) FROM app.measurement_event WHERE session_id = '00000000-0000-0000-0000-00000000c111' AND instrument_id = 'vignette-session-outcome-checkin') AS checkins_before_rollback; ROLLBACK;