G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -0,0 +1,189 @@
-- 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;
$$;