-- Outcome & Alliance OS G4/G5: cross-session self-directed practice runtime. -- A prescription is authored in one session; its observed attempt evidence belongs -- to a later completed session owned by the same learner. CREATE UNIQUE INDEX IF NOT EXISTS uq_practice_prescription_record_learner ON app.practice_prescription(prescription_record_id, learner_id); ALTER TABLE app.practice_episode_submission DROP CONSTRAINT IF EXISTS practice_episode_prescription_fkey; ALTER TABLE app.practice_episode_submission ADD CONSTRAINT practice_episode_prescription_fkey FOREIGN KEY (prescription_record_id, learner_id) REFERENCES app.practice_prescription(prescription_record_id, learner_id) ON DELETE RESTRICT; ALTER TABLE app.practice_curriculum_decision_event DROP CONSTRAINT IF EXISTS practice_decision_prescription_fkey; ALTER TABLE app.practice_curriculum_decision_event ADD CONSTRAINT practice_decision_prescription_fkey FOREIGN KEY (selected_prescription_record_id, learner_id) REFERENCES app.practice_prescription(prescription_record_id, learner_id) ON DELETE RESTRICT; CREATE OR REPLACE FUNCTION audit.enforce_practice_episode_transfer_gate() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE attempts JSONB; competency_key TEXT; has_familiar_pass BOOLEAN; has_unseen_pass BOOLEAN; has_transfer_collision BOOLEAN; claimed_prior_familiar INT; durable_prior_familiar INT; BEGIN IF NEW.assessment_payload->>'progress' IS DISTINCT FROM NEW.progress OR COALESCE((NEW.assessment_payload->>'mastery_allowed')::BOOLEAN, FALSE) IS DISTINCT FROM NEW.mastery_allowed THEN RAISE EXCEPTION 'practice episode columns must match assessment payload' USING ERRCODE = '23514'; END IF; attempts := COALESCE(NEW.assessment_payload->'attempts', '[]'::JSONB); IF jsonb_typeof(attempts) <> 'array' OR jsonb_array_length(attempts) = 0 THEN RAISE EXCEPTION 'practice episode requires attempt evidence' USING ERRCODE = '23514'; END IF; competency_key := NEW.assessment_payload->>'competency_id'; claimed_prior_familiar := COALESCE( (NEW.assessment_payload->>'prior_familiar_demonstrations')::INT, 0 ); SELECT COALESCE((state->>'familiar_demonstrations')::INT, 0) INTO durable_prior_familiar FROM app.competency_graph_snapshot snapshot, jsonb_array_elements(COALESCE(snapshot.graph_payload->'states', '[]'::JSONB)) state WHERE snapshot.learner_id = NEW.learner_id AND state->>'competency_id' = competency_key ORDER BY snapshot.snapshot_no DESC LIMIT 1; durable_prior_familiar := COALESCE(durable_prior_familiar, 0); IF claimed_prior_familiar > durable_prior_familiar THEN RAISE EXCEPTION 'practice episode prior familiar claim exceeds durable graph' USING ERRCODE = '23514'; END IF; SELECT EXISTS ( SELECT 1 FROM jsonb_array_elements(attempts) item WHERE item->>'outcome' = 'passed' AND item->>'scenario_novelty' = 'familiar' ) INTO has_familiar_pass; SELECT EXISTS ( SELECT 1 FROM jsonb_array_elements(attempts) item WHERE item->>'outcome' = 'passed' AND item->>'scenario_novelty' = 'unseen_transfer' AND length(btrim(COALESCE(item->>'utterance_template_id', ''))) > 0 ) INTO has_unseen_pass; SELECT EXISTS ( SELECT 1 FROM jsonb_array_elements(attempts) unseen, jsonb_array_elements(attempts) familiar WHERE unseen->>'outcome' = 'passed' AND unseen->>'scenario_novelty' = 'unseen_transfer' AND familiar->>'outcome' = 'passed' AND familiar->>'scenario_novelty' = 'familiar' AND ( unseen->>'scenario_variant_id' = familiar->>'scenario_variant_id' OR unseen->>'utterance_template_id' = familiar->>'utterance_template_id' ) ) INTO has_transfer_collision; IF NEW.progress = 'mastered' AND ( (NOT has_familiar_pass AND claimed_prior_familiar < 1) OR NOT has_unseen_pass OR has_transfer_collision ) THEN RAISE EXCEPTION 'practice mastery requires durable familiar success and novel unseen transfer evidence' USING ERRCODE = '23514'; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION audit.enforce_practice_attempt_sequence_and_transfer() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE latest_sequence INT; prescription_record UUID; competency_key TEXT; familiar_pass_exists BOOLEAN; durable_prior_familiar INT; repeated_template_exists BOOLEAN; repeated_variant_exists BOOLEAN; BEGIN PERFORM pg_advisory_xact_lock(hashtextextended(NEW.episode_submission_id::TEXT, 0)); SELECT max(attempt.sequence_no) INTO latest_sequence FROM app.practice_attempt_evidence attempt WHERE attempt.episode_submission_id = NEW.episode_submission_id; IF NEW.sequence_no <> COALESCE(latest_sequence, 0) + 1 THEN RAISE EXCEPTION 'practice attempt sequence must be contiguous' USING ERRCODE = '23514'; END IF; IF NEW.outcome = 'passed' AND NEW.scenario_novelty = 'unseen_transfer' THEN IF length(btrim(COALESCE(NEW.utterance_template_id, ''))) = 0 THEN RAISE EXCEPTION 'unseen transfer pass requires a nonblank utterance template id' USING ERRCODE = '23514'; END IF; SELECT episode.prescription_record_id, prescription.competency_id INTO prescription_record, competency_key FROM app.practice_episode_submission episode JOIN app.practice_prescription prescription ON prescription.prescription_record_id = episode.prescription_record_id AND prescription.learner_id = episode.learner_id WHERE episode.episode_submission_id = NEW.episode_submission_id AND episode.learner_id = NEW.learner_id; SELECT EXISTS ( SELECT 1 FROM app.practice_attempt_evidence prior JOIN app.practice_episode_submission episode ON episode.episode_submission_id = prior.episode_submission_id WHERE episode.prescription_record_id = prescription_record AND prior.learner_id = NEW.learner_id AND prior.scenario_novelty = 'familiar' AND prior.outcome = 'passed' ) INTO familiar_pass_exists; SELECT COALESCE((state->>'familiar_demonstrations')::INT, 0) INTO durable_prior_familiar FROM app.competency_graph_snapshot snapshot, jsonb_array_elements(COALESCE(snapshot.graph_payload->'states', '[]'::JSONB)) state WHERE snapshot.learner_id = NEW.learner_id AND state->>'competency_id' = competency_key ORDER BY snapshot.snapshot_no DESC LIMIT 1; familiar_pass_exists := familiar_pass_exists OR COALESCE(durable_prior_familiar, 0) > 0; SELECT EXISTS ( SELECT 1 FROM app.practice_attempt_evidence prior JOIN app.practice_episode_submission episode ON episode.episode_submission_id = prior.episode_submission_id WHERE episode.prescription_record_id = prescription_record AND prior.learner_id = NEW.learner_id AND prior.scenario_novelty = 'familiar' AND prior.utterance_template_id = NEW.utterance_template_id ) INTO repeated_template_exists; SELECT EXISTS ( SELECT 1 FROM app.practice_attempt_evidence prior JOIN app.practice_episode_submission episode ON episode.episode_submission_id = prior.episode_submission_id WHERE episode.prescription_record_id = prescription_record AND prior.learner_id = NEW.learner_id AND prior.scenario_novelty = 'familiar' AND prior.scenario_variant_id = NEW.scenario_variant_id ) INTO repeated_variant_exists; IF NOT familiar_pass_exists OR repeated_template_exists OR repeated_variant_exists THEN RAISE EXCEPTION 'unseen transfer pass cannot reuse familiar variant or memorized phrase' USING ERRCODE = '23514'; END IF; END IF; RETURN NEW; END; $$;