vignette/scripts/check-outcome-trajectory-ledger.sql
Yun Chan 16e791e044 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 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

454 lines
17 KiB
PL/PgSQL

\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;