-- Outcome & Alliance OS G2: synthetic expected arcs, longitudinal revisions, -- observation provenance, and role-safe relationship memory. -- Prerequisites: 02_schema.sql, 04_audit_eval_rls.sql, 07_measurement_foundation.sql. CREATE SCHEMA IF NOT EXISTS audit; CREATE SCHEMA IF NOT EXISTS ds; -- The expected arc is an educational synthetic reference, never a clinical norm. CREATE TABLE IF NOT EXISTS ds.synthetic_outcome_arc ( arc_id TEXT PRIMARY KEY, schema_version TEXT NOT NULL CHECK (schema_version = 'vignette.synthetic-outcome-arc.v1'), title_ko TEXT NOT NULL CHECK (length(btrim(title_ko)) > 0), data_classification TEXT NOT NULL CHECK (data_classification = 'synthetic_educational'), clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE), provenance_note TEXT NOT NULL CHECK (length(btrim(provenance_note)) >= 20), expected_arc JSONB NOT NULL CHECK ( jsonb_typeof(expected_arc) = 'object' AND expected_arc->>'data_classification' = 'synthetic_educational' AND (expected_arc->>'clinical_claim_allowed')::boolean = FALSE AND jsonb_array_length(expected_arc->'distributions') = 15 ), content_hash TEXT NOT NULL UNIQUE CHECK (content_hash ~ '^[a-f0-9]{64}$'), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO ds.synthetic_outcome_arc ( arc_id, schema_version, title_ko, data_classification, clinical_claim_allowed, provenance_note, expected_arc, content_hash ) VALUES ( 'oas-g2-arc-001', 'vignette.synthetic-outcome-arc.v1', '교육용 초기 5회기 기대 궤적', 'synthetic_educational', FALSE, '교육용 합성 사례의 결정론 테스트 분포이며 실제 내담자, 임상 규준, 치료 효과 또는 진단 예측을 나타내지 않는다.', $arc${ "schema_version":"vignette.synthetic-outcome-arc.v1", "arc_id":"oas-g2-arc-001", "title_ko":"교육용 초기 5회기 기대 궤적", "data_classification":"synthetic_educational", "clinical_claim_allowed":false, "provenance_note":"교육용 합성 사례의 결정론 테스트 분포이며 실제 내담자, 임상 규준, 치료 효과 또는 진단 예측을 나타내지 않는다.", "distributions":[ {"session_no":1,"axis":"distress_load","mean":0.70,"standard_deviation":0.10,"lower_reference":0.50,"upper_reference":0.90,"sample_size":200,"expected_direction":"lower_is_better"}, {"session_no":1,"axis":"daily_functioning","mean":0.30,"standard_deviation":0.10,"lower_reference":0.10,"upper_reference":0.50,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":1,"axis":"learning_engagement","mean":0.40,"standard_deviation":0.10,"lower_reference":0.20,"upper_reference":0.60,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":2,"axis":"distress_load","mean":0.62,"standard_deviation":0.10,"lower_reference":0.42,"upper_reference":0.82,"sample_size":200,"expected_direction":"lower_is_better"}, {"session_no":2,"axis":"daily_functioning","mean":0.40,"standard_deviation":0.10,"lower_reference":0.20,"upper_reference":0.60,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":2,"axis":"learning_engagement","mean":0.48,"standard_deviation":0.10,"lower_reference":0.28,"upper_reference":0.68,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":3,"axis":"distress_load","mean":0.54,"standard_deviation":0.10,"lower_reference":0.34,"upper_reference":0.74,"sample_size":200,"expected_direction":"lower_is_better"}, {"session_no":3,"axis":"daily_functioning","mean":0.50,"standard_deviation":0.10,"lower_reference":0.30,"upper_reference":0.70,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":3,"axis":"learning_engagement","mean":0.56,"standard_deviation":0.10,"lower_reference":0.36,"upper_reference":0.76,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":4,"axis":"distress_load","mean":0.46,"standard_deviation":0.10,"lower_reference":0.26,"upper_reference":0.66,"sample_size":200,"expected_direction":"lower_is_better"}, {"session_no":4,"axis":"daily_functioning","mean":0.60,"standard_deviation":0.10,"lower_reference":0.40,"upper_reference":0.80,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":4,"axis":"learning_engagement","mean":0.64,"standard_deviation":0.10,"lower_reference":0.44,"upper_reference":0.84,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":5,"axis":"distress_load","mean":0.38,"standard_deviation":0.10,"lower_reference":0.18,"upper_reference":0.58,"sample_size":200,"expected_direction":"lower_is_better"}, {"session_no":5,"axis":"daily_functioning","mean":0.68,"standard_deviation":0.10,"lower_reference":0.48,"upper_reference":0.88,"sample_size":200,"expected_direction":"higher_is_better"}, {"session_no":5,"axis":"learning_engagement","mean":0.72,"standard_deviation":0.10,"lower_reference":0.52,"upper_reference":0.92,"sample_size":200,"expected_direction":"higher_is_better"} ] }$arc$::jsonb, '4a813340ededc4624e1607308579d0c2cfff23024ac68de0cd3c8d4b8706ab8f' ) ON CONFLICT (arc_id) DO NOTHING; INSERT INTO app.measurement_instrument ( instrument_id, instrument_version, name_ko, instrument_kind, construct, validation_basis, scoring_schema, metadata ) VALUES ( 'vignette-session-outcome', '1.0.0', '교육용 회기 성과 3축 관측', 'training_metric', 'session_outcome', '교육용 합성 사례에서 부담감·일상 기능·학습 참여를 축별로 기록한다. 임상 척도나 치료 효과 지표가 아니다.', '{"dimensions":["distress_load","daily_functioning","learning_engagement"],"min":0,"max":1}'::jsonb, '{"clinical_claim_allowed":false,"data_classification":"synthetic_educational"}'::jsonb ), ( 'vignette-session-outcome-checkin', '1.0.0', '학습자 회기 성과 3축 체크인', 'training_metric', 'session_outcome', '수련생이 종료된 합성 회기를 돌아보며 기록하는 부담감·일상 기능·학습 참여 자기보고. 임상 척도나 치료 효과 지표가 아니다.', '{"dimensions":["distress_load","daily_functioning","learning_engagement"],"min":0,"max":1}'::jsonb, '{"clinical_claim_allowed":false,"data_classification":"synthetic_educational","perspective":"learner_self_report"}'::jsonb ) ON CONFLICT (instrument_id, instrument_version) DO NOTHING; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'ck_measurement_event_outcome_submission_metadata' AND conrelid = 'app.measurement_event'::regclass ) THEN ALTER TABLE app.measurement_event ADD CONSTRAINT ck_measurement_event_outcome_submission_metadata CHECK ( NOT ( construct = 'session_outcome' AND source_kind = 'learner_reported' AND instrument_id = 'vignette-session-outcome-checkin' ) OR ( instrument_version = '1.0.0' AND perspective = 'learner_self_report' AND dimension IN ('distress_load','daily_functioning','learning_engagement') AND metadata ?& ARRAY['submission_id','submission_hash'] AND metadata->>'submission_id' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' AND metadata->>'submission_hash' ~ '^[a-f0-9]{64}$' ) ) NOT VALID; ALTER TABLE app.measurement_event VALIDATE CONSTRAINT ck_measurement_event_outcome_submission_metadata; END IF; END; $$; CREATE UNIQUE INDEX IF NOT EXISTS uq_measurement_event_outcome_submission_axis ON app.measurement_event ( session_id, (metadata->>'submission_id'), dimension ) WHERE construct = 'session_outcome' AND source_kind = 'learner_reported' AND perspective = 'learner_self_report' AND instrument_id = 'vignette-session-outcome-checkin' AND instrument_version = '1.0.0'; CREATE OR REPLACE FUNCTION audit.enforce_outcome_submission_turn_ownership() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT ( NEW.construct = 'session_outcome' AND NEW.source_kind = 'learner_reported' AND NEW.perspective = 'learner_self_report' AND NEW.instrument_id = 'vignette-session-outcome-checkin' ) THEN RETURN NEW; END IF; IF cardinality(NEW.evidence_turn_ids) > 0 AND ( SELECT count(DISTINCT t.id) FROM app.turns t WHERE t.id = ANY(NEW.evidence_turn_ids) AND t.session_id = NEW.session_id ) <> cardinality(NEW.evidence_turn_ids) THEN RAISE EXCEPTION 'outcome submission evidence turns must belong to its session' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_outcome_submission_turn_ownership ON app.measurement_event; CREATE TRIGGER trg_outcome_submission_turn_ownership BEFORE INSERT ON app.measurement_event FOR EACH ROW EXECUTE FUNCTION audit.enforce_outcome_submission_turn_ownership(); -- Every computation is a new immutable revision. A recomputation never overwrites -- the previous assessment, even when the evidence fingerprint is unchanged. CREATE TABLE IF NOT EXISTS app.outcome_trajectory_revision ( revision_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), anchor_session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT, case_id UUID NOT NULL, learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, expected_arc_id TEXT NOT NULL REFERENCES ds.synthetic_outcome_arc(arc_id) ON DELETE RESTRICT, revision_no INT NOT NULL CHECK (revision_no >= 1), supersedes_revision_id UUID REFERENCES app.outcome_trajectory_revision(revision_id) ON DELETE RESTRICT, source_fingerprint TEXT NOT NULL CHECK (source_fingerprint ~ '^[a-f0-9]{64}$'), assessment JSONB NOT NULL CHECK ( jsonb_typeof(assessment) = 'object' AND assessment->>'schema_version' = 'vignette.outcome-trajectory-assessment.v1' AND assessment->>'data_classification' = 'synthetic_educational' AND (assessment->>'clinical_claim_allowed')::boolean = FALSE ), observation_count INT NOT NULL CHECK (observation_count BETWEEN 3 AND 15), missing_observation_count INT NOT NULL CHECK ( missing_observation_count BETWEEN 0 AND observation_count ), recompute_reason TEXT NOT NULL CHECK (length(btrim(recompute_reason)) > 0), computed_by UUID REFERENCES app.app_user(user_id) ON DELETE RESTRICT, computed_role TEXT NOT NULL CHECK ( computed_role IN ('learner','instructor','admin','agent','migration') ), computed_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (case_id, revision_no), UNIQUE (supersedes_revision_id), CHECK (revision_id IS DISTINCT FROM supersedes_revision_id) ); CREATE INDEX IF NOT EXISTS idx_outcome_trajectory_case_revision ON app.outcome_trajectory_revision(case_id, revision_no DESC); CREATE INDEX IF NOT EXISTS idx_outcome_trajectory_anchor ON app.outcome_trajectory_revision(anchor_session_id, revision_no DESC); CREATE TABLE IF NOT EXISTS app.outcome_trajectory_observation ( observation_snapshot_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), revision_id UUID NOT NULL REFERENCES app.outcome_trajectory_revision(revision_id) ON DELETE RESTRICT, measurement_id UUID REFERENCES app.measurement_event(measurement_id) ON DELETE RESTRICT, session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT, session_no INT NOT NULL CHECK (session_no BETWEEN 1 AND 5), axis TEXT NOT NULL CHECK ( axis IN ('distress_load','daily_functioning','learning_engagement') ), status TEXT NOT NULL CHECK (status IN ('observed','missing','error')), value DOUBLE PRECISION CHECK (value BETWEEN 0 AND 1), raw_value DOUBLE PRECISION, scale_min DOUBLE PRECISION, scale_max DOUBLE PRECISION, confidence DOUBLE PRECISION CHECK (confidence BETWEEN 0 AND 1), source_kind TEXT NOT NULL CHECK ( source_kind IN ( 'simulated_state','model_inferred','agent_reported', 'learner_reported','human_rated','observed_runtime' ) ), perspective TEXT NOT NULL CHECK ( perspective IN ( 'client_agent_report','learner_self_report','independent_observer', 'supervisor_human','client_simulation','runtime_observation' ) ), instrument_id TEXT NOT NULL, instrument_version TEXT NOT NULL, model_run_id UUID REFERENCES audit.model_run(model_run_id) ON DELETE RESTRICT, evidence_turn_ids UUID[] NOT NULL DEFAULT '{}', missing_reason TEXT, source_created_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (revision_id, session_no, axis), CHECK ( (status = 'observed' AND measurement_id IS NOT NULL AND value IS NOT NULL AND raw_value IS NOT NULL AND scale_min IS NOT NULL AND scale_max IS NOT NULL AND scale_max > scale_min AND confidence IS NOT NULL AND missing_reason IS NULL) OR (status IN ('missing','error') AND value IS NULL AND raw_value IS NULL AND confidence IS NULL AND missing_reason IS NOT NULL) ), CHECK ( (source_kind = 'simulated_state' AND perspective = 'client_simulation') OR (source_kind = 'model_inferred' AND perspective = 'independent_observer') OR (source_kind = 'agent_reported' AND perspective = 'client_agent_report') OR (source_kind = 'learner_reported' AND perspective = 'learner_self_report') OR (source_kind = 'human_rated' AND perspective = 'supervisor_human') OR (source_kind = 'observed_runtime' AND perspective = 'runtime_observation') ), CHECK (source_kind NOT IN ('model_inferred','agent_reported') OR model_run_id IS NOT NULL) ); CREATE INDEX IF NOT EXISTS idx_outcome_observation_revision ON app.outcome_trajectory_observation(revision_id, session_no, axis); CREATE INDEX IF NOT EXISTS idx_outcome_observation_measurement ON app.outcome_trajectory_observation(measurement_id) WHERE measurement_id IS NOT NULL; -- Relationship events contain no role-private sentences. Role-specific summaries -- live in the projection table so RLS protects the text itself, not only the row. CREATE TABLE IF NOT EXISTS app.relationship_memory_event ( memory_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT, case_id UUID NOT NULL, event_type TEXT NOT NULL CHECK ( event_type IN ( 'goal_agreement','task_agreement','rupture_withdrawal', 'rupture_confrontation','repair_attempt','repair_confirmed','unresolved_rupture' ) ), resolves_event_id UUID REFERENCES app.relationship_memory_event(memory_event_id) ON DELETE RESTRICT, visible_to TEXT[] NOT NULL CHECK ( cardinality(visible_to) > 0 AND visible_to <@ ARRAY['client','counselor','evaluator','supervisor','research']::TEXT[] ), evidence_turn_ids UUID[] NOT NULL CHECK (cardinality(evidence_turn_ids) > 0), source_kind TEXT NOT NULL CHECK ( source_kind IN ('model_inferred','agent_reported','human_rated','observed_runtime') ), model_run_id UUID REFERENCES audit.model_run(model_run_id) ON DELETE RESTRICT, created_by UUID REFERENCES app.app_user(user_id) ON DELETE RESTRICT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (memory_event_id IS DISTINCT FROM resolves_event_id), CHECK (resolves_event_id IS NULL OR event_type = 'repair_confirmed'), CHECK (source_kind NOT IN ('model_inferred','agent_reported') OR model_run_id IS NOT NULL) ); CREATE INDEX IF NOT EXISTS idx_relationship_memory_case_created ON app.relationship_memory_event(case_id, created_at, memory_event_id); CREATE INDEX IF NOT EXISTS idx_relationship_memory_session ON app.relationship_memory_event(session_id, created_at); CREATE TABLE IF NOT EXISTS app.relationship_memory_projection ( projection_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), memory_event_id UUID NOT NULL REFERENCES app.relationship_memory_event(memory_event_id) ON DELETE RESTRICT, ai_view TEXT NOT NULL CHECK ( ai_view IN ('client','counselor','evaluator','supervisor','research') ), summary TEXT NOT NULL CHECK (length(btrim(summary)) > 0), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (memory_event_id, ai_view) ); CREATE INDEX IF NOT EXISTS idx_relationship_projection_view ON app.relationship_memory_projection(ai_view, memory_event_id); CREATE OR REPLACE FUNCTION audit.enforce_relationship_memory_link() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE resolved_case UUID; resolved_type TEXT; BEGIN IF ( SELECT count(DISTINCT t.id) FROM app.turns t WHERE t.id = ANY(NEW.evidence_turn_ids) AND t.session_id = NEW.session_id ) <> cardinality(NEW.evidence_turn_ids) THEN RAISE EXCEPTION 'relationship memory evidence turns must belong to its session' USING ERRCODE = '23514'; END IF; IF NEW.resolves_event_id IS NULL THEN RETURN NEW; END IF; SELECT case_id, event_type INTO resolved_case, resolved_type FROM app.relationship_memory_event WHERE memory_event_id = NEW.resolves_event_id; IF resolved_case IS NULL OR resolved_case IS DISTINCT FROM NEW.case_id THEN RAISE EXCEPTION 'relationship repair must reference an event in the same case' USING ERRCODE = '23514'; END IF; IF resolved_type NOT IN ( 'rupture_withdrawal','rupture_confrontation','unresolved_rupture','repair_attempt' ) THEN RAISE EXCEPTION 'relationship repair must resolve a rupture or repair attempt' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_relationship_projection_view() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM app.relationship_memory_event e WHERE e.memory_event_id = NEW.memory_event_id AND NEW.ai_view = ANY(e.visible_to) ) THEN RAISE EXCEPTION 'relationship projection view must be declared by its event' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_outcome_trajectory_revision_append_only ON app.outcome_trajectory_revision; CREATE TRIGGER trg_outcome_trajectory_revision_append_only BEFORE UPDATE OR DELETE ON app.outcome_trajectory_revision FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); DROP TRIGGER IF EXISTS trg_outcome_trajectory_observation_append_only ON app.outcome_trajectory_observation; CREATE TRIGGER trg_outcome_trajectory_observation_append_only BEFORE UPDATE OR DELETE ON app.outcome_trajectory_observation FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); DROP TRIGGER IF EXISTS trg_synthetic_outcome_arc_append_only ON ds.synthetic_outcome_arc; CREATE TRIGGER trg_synthetic_outcome_arc_append_only BEFORE UPDATE OR DELETE ON ds.synthetic_outcome_arc FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); DROP TRIGGER IF EXISTS trg_relationship_memory_link ON app.relationship_memory_event; CREATE TRIGGER trg_relationship_memory_link BEFORE INSERT ON app.relationship_memory_event FOR EACH ROW EXECUTE FUNCTION audit.enforce_relationship_memory_link(); DROP TRIGGER IF EXISTS trg_relationship_memory_event_append_only ON app.relationship_memory_event; CREATE TRIGGER trg_relationship_memory_event_append_only BEFORE UPDATE OR DELETE ON app.relationship_memory_event FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); DROP TRIGGER IF EXISTS trg_relationship_projection_view ON app.relationship_memory_projection; CREATE TRIGGER trg_relationship_projection_view BEFORE INSERT ON app.relationship_memory_projection FOR EACH ROW EXECUTE FUNCTION audit.enforce_relationship_projection_view(); DROP TRIGGER IF EXISTS trg_relationship_memory_projection_append_only ON app.relationship_memory_projection; CREATE TRIGGER trg_relationship_memory_projection_append_only BEFORE UPDATE OR DELETE ON app.relationship_memory_projection FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation(); ALTER TABLE ds.synthetic_outcome_arc ENABLE ROW LEVEL SECURITY; ALTER TABLE app.outcome_trajectory_revision ENABLE ROW LEVEL SECURITY; ALTER TABLE app.outcome_trajectory_observation ENABLE ROW LEVEL SECURITY; ALTER TABLE app.relationship_memory_event ENABLE ROW LEVEL SECURITY; ALTER TABLE app.relationship_memory_projection ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_synthetic_outcome_arc_select ON ds.synthetic_outcome_arc; DROP POLICY IF EXISTS p_synthetic_outcome_arc_insert ON ds.synthetic_outcome_arc; CREATE POLICY p_synthetic_outcome_arc_select ON ds.synthetic_outcome_arc FOR SELECT USING (true); CREATE POLICY p_synthetic_outcome_arc_insert ON ds.synthetic_outcome_arc FOR INSERT WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') ); DROP POLICY IF EXISTS p_outcome_trajectory_revision_select ON app.outcome_trajectory_revision; DROP POLICY IF EXISTS p_outcome_trajectory_revision_insert ON app.outcome_trajectory_revision; CREATE POLICY p_outcome_trajectory_revision_select ON app.outcome_trajectory_revision FOR SELECT USING ( app.is_ai_context() OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.outcome_trajectory_revision.anchor_session_id ) ); CREATE POLICY p_outcome_trajectory_revision_insert ON app.outcome_trajectory_revision FOR INSERT WITH CHECK ( app.is_ai_context() OR ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.outcome_trajectory_revision.anchor_session_id AND s.case_id = app.outcome_trajectory_revision.case_id AND s.learner_id = app.outcome_trajectory_revision.learner_id ) AND ( app.current_role_name() IN ('admin','instructor') OR ( app.current_role_name() = 'learner' AND learner_id = app.current_uid() AND computed_by = app.current_uid() ) ) ) ); DROP POLICY IF EXISTS p_outcome_trajectory_observation_select ON app.outcome_trajectory_observation; DROP POLICY IF EXISTS p_outcome_trajectory_observation_insert ON app.outcome_trajectory_observation; CREATE POLICY p_outcome_trajectory_observation_select ON app.outcome_trajectory_observation FOR SELECT USING ( EXISTS ( SELECT 1 FROM app.outcome_trajectory_revision r WHERE r.revision_id = app.outcome_trajectory_observation.revision_id ) ); CREATE POLICY p_outcome_trajectory_observation_insert ON app.outcome_trajectory_observation FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM app.outcome_trajectory_revision r WHERE r.revision_id = app.outcome_trajectory_observation.revision_id AND r.case_id = ( SELECT s.case_id FROM app.sessions s WHERE s.id = app.outcome_trajectory_observation.session_id ) ) ); DROP POLICY IF EXISTS p_relationship_memory_event_select ON app.relationship_memory_event; DROP POLICY IF EXISTS p_relationship_memory_event_insert ON app.relationship_memory_event; CREATE POLICY p_relationship_memory_event_select ON app.relationship_memory_event FOR SELECT USING ( ( app.is_ai_context() AND current_setting('app.current_ai_view', true) = ANY(visible_to) ) OR ( NOT app.is_ai_context() AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.relationship_memory_event.session_id ) AND ( (app.current_role_name() = 'learner' AND 'counselor' = ANY(visible_to)) OR (app.current_role_name() = 'instructor' AND visible_to && ARRAY['evaluator','supervisor']::TEXT[]) OR app.current_role_name() = 'admin' ) ) ); CREATE POLICY p_relationship_memory_event_insert ON app.relationship_memory_event FOR INSERT WITH CHECK ( app.is_ai_context() OR ( app.current_role_name() IN ('admin','instructor') AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.relationship_memory_event.session_id AND s.case_id = app.relationship_memory_event.case_id ) ) ); DROP POLICY IF EXISTS p_relationship_memory_projection_select ON app.relationship_memory_projection; DROP POLICY IF EXISTS p_relationship_memory_projection_insert ON app.relationship_memory_projection; CREATE POLICY p_relationship_memory_projection_select ON app.relationship_memory_projection FOR SELECT USING ( ( app.is_ai_context() AND ai_view = current_setting('app.current_ai_view', true) ) OR ( NOT app.is_ai_context() AND EXISTS ( SELECT 1 FROM app.relationship_memory_event e WHERE e.memory_event_id = app.relationship_memory_projection.memory_event_id ) AND ( (app.current_role_name() = 'learner' AND ai_view = 'counselor') OR (app.current_role_name() = 'instructor' AND ai_view IN ('evaluator','supervisor')) OR app.current_role_name() = 'admin' ) ) ); CREATE POLICY p_relationship_memory_projection_insert ON app.relationship_memory_projection FOR INSERT WITH CHECK ( app.is_ai_context() OR ( app.current_role_name() IN ('admin','instructor') AND EXISTS ( SELECT 1 FROM app.relationship_memory_event e WHERE e.memory_event_id = app.relationship_memory_projection.memory_event_id ) ) );