-- Persist session-level evaluator output for learner reviews and teacher/admin views. CREATE TABLE IF NOT EXISTS app.session_evaluation ( session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE, status TEXT NOT NULL CHECK (status IN ('ready','degraded','error')), source TEXT NOT NULL, scope TEXT NOT NULL, stage TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}'::jsonb, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE app.session_evaluation ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_session_evaluation_select ON app.session_evaluation; DROP POLICY IF EXISTS p_session_evaluation_insert ON app.session_evaluation; DROP POLICY IF EXISTS p_session_evaluation_update ON app.session_evaluation; DROP POLICY IF EXISTS p_session_evaluation_delete ON app.session_evaluation; CREATE POLICY p_session_evaluation_select ON app.session_evaluation FOR SELECT USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_evaluation.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_evaluation_insert ON app.session_evaluation FOR INSERT WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_evaluation.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_evaluation_update ON app.session_evaluation FOR UPDATE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_evaluation.session_id AND s.learner_id = app.current_uid() ) ) WITH CHECK ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_evaluation.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_evaluation_delete ON app.session_evaluation FOR DELETE USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_evaluation.session_id AND s.learner_id = app.current_uid() ) ); -- Learner-edited case formulation worksheet for session review. CREATE TABLE IF NOT EXISTS app.case_worksheet ( session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE app.case_worksheet ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_case_worksheet_select ON app.case_worksheet; DROP POLICY IF EXISTS p_case_worksheet_insert ON app.case_worksheet; DROP POLICY IF EXISTS p_case_worksheet_update ON app.case_worksheet; DROP POLICY IF EXISTS p_case_worksheet_delete ON app.case_worksheet; CREATE POLICY p_case_worksheet_select ON app.case_worksheet FOR SELECT USING ( app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.case_worksheet.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_case_worksheet_insert ON app.case_worksheet FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.case_worksheet.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_case_worksheet_update ON app.case_worksheet FOR UPDATE USING ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.case_worksheet.session_id AND s.learner_id = app.current_uid() ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.case_worksheet.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_case_worksheet_delete ON app.case_worksheet FOR DELETE USING ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.case_worksheet.session_id AND s.learner_id = app.current_uid() ) ); -- Live coaching interventions delivered during a session. CREATE TABLE IF NOT EXISTS app.live_coach_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE CASCADE, turn_seq INT NOT NULL CHECK (turn_seq >= 1), stage TEXT NOT NULL, event_type TEXT NOT NULL DEFAULT 'use' CHECK (event_type IN ('use', 'recharge')), credit_delta INT NOT NULL DEFAULT -1, credit_balance INT, reason TEXT, learner_text_excerpt TEXT, client_reply_excerpt TEXT, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE app.live_coach_events ADD COLUMN IF NOT EXISTS event_type TEXT NOT NULL DEFAULT 'use'; ALTER TABLE app.live_coach_events ADD COLUMN IF NOT EXISTS credit_delta INT NOT NULL DEFAULT -1; ALTER TABLE app.live_coach_events ADD COLUMN IF NOT EXISTS credit_balance INT; ALTER TABLE app.live_coach_events ADD COLUMN IF NOT EXISTS reason TEXT; UPDATE app.live_coach_events SET event_type = 'use' WHERE event_type IS NULL OR event_type NOT IN ('use', 'recharge'); UPDATE app.live_coach_events SET credit_delta = CASE WHEN event_type = 'recharge' THEN 1 ELSE -1 END WHERE credit_delta IS NULL; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'live_coach_events_event_type_check' ) THEN ALTER TABLE app.live_coach_events ADD CONSTRAINT live_coach_events_event_type_check CHECK (event_type IN ('use', 'recharge')); END IF; END $$; CREATE INDEX IF NOT EXISTS idx_live_coach_events_session_turn ON app.live_coach_events(session_id, turn_seq, created_at); CREATE INDEX IF NOT EXISTS idx_live_coach_events_session_type ON app.live_coach_events(session_id, event_type, created_at); ALTER TABLE app.live_coach_events ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_live_coach_events_select ON app.live_coach_events; DROP POLICY IF EXISTS p_live_coach_events_insert ON app.live_coach_events; CREATE POLICY p_live_coach_events_select ON app.live_coach_events FOR SELECT USING ( app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.live_coach_events.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_live_coach_events_insert ON app.live_coach_events FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.live_coach_events.session_id AND s.learner_id = app.current_uid() ) ); -- Public share cards for learner-owned session reviews. Payload is intentionally -- a sanitized preview, not the transcript or worksheet raw source. CREATE TABLE IF NOT EXISTS app.session_share_link ( session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE, created_by UUID NOT NULL REFERENCES app.app_user(user_id), token_hash TEXT NOT NULL UNIQUE, payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), revoked_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_session_share_token_active ON app.session_share_link(token_hash) WHERE revoked_at IS NULL; ALTER TABLE app.session_share_link ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_session_share_select ON app.session_share_link; DROP POLICY IF EXISTS p_session_share_insert ON app.session_share_link; DROP POLICY IF EXISTS p_session_share_update ON app.session_share_link; DROP POLICY IF EXISTS p_session_share_delete ON app.session_share_link; CREATE POLICY p_session_share_select ON app.session_share_link FOR SELECT USING ( app.is_ai_context() OR app.current_role_name() IN ('admin','instructor') OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_share_link.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_share_insert ON app.session_share_link FOR INSERT WITH CHECK ( app.is_ai_context() OR ( created_by = app.current_uid() AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_share_link.session_id AND s.learner_id = app.current_uid() ) ) ); CREATE POLICY p_session_share_update ON app.session_share_link FOR UPDATE USING ( app.is_ai_context() OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_share_link.session_id AND s.learner_id = app.current_uid() ) ) WITH CHECK ( app.is_ai_context() OR ( created_by = app.current_uid() AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_share_link.session_id AND s.learner_id = app.current_uid() ) ) ); CREATE POLICY p_session_share_delete ON app.session_share_link FOR DELETE USING ( app.is_ai_context() OR EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_share_link.session_id AND s.learner_id = app.current_uid() ) ); -- Learner-owned history archive state. This is display state only; session, -- turn, review, share, research, and audit evidence remain intact. CREATE TABLE IF NOT EXISTS app.session_archive_state ( session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE, learner_id UUID NOT NULL REFERENCES app.app_user(user_id), archived_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_session_archive_learner ON app.session_archive_state(learner_id, archived_at DESC); ALTER TABLE app.session_archive_state ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_session_archive_select ON app.session_archive_state; DROP POLICY IF EXISTS p_session_archive_insert ON app.session_archive_state; DROP POLICY IF EXISTS p_session_archive_update ON app.session_archive_state; DROP POLICY IF EXISTS p_session_archive_delete ON app.session_archive_state; CREATE POLICY p_session_archive_select ON app.session_archive_state FOR SELECT USING ( app.current_role_name() IN ('admin','instructor') OR learner_id = app.current_uid() ); CREATE POLICY p_session_archive_insert ON app.session_archive_state FOR INSERT WITH CHECK ( learner_id = app.current_uid() AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_archive_state.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_archive_update ON app.session_archive_state FOR UPDATE USING ( learner_id = app.current_uid() ) WITH CHECK ( learner_id = app.current_uid() AND EXISTS ( SELECT 1 FROM app.sessions s WHERE s.id = app.session_archive_state.session_id AND s.learner_id = app.current_uid() ) ); CREATE POLICY p_session_archive_delete ON app.session_archive_state FOR DELETE USING ( learner_id = app.current_uid() );