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 산출물은 커밋에서 제외했다.
404 lines
16 KiB
PL/PgSQL
404 lines
16 KiB
PL/PgSQL
\set ON_ERROR_STOP on
|
|
\if :{?app_role}
|
|
\else
|
|
\set app_role vignette
|
|
\endif
|
|
|
|
BEGIN;
|
|
|
|
INSERT INTO app.app_user (
|
|
user_id, external_id, role, cohort, account_status, consent_at
|
|
) VALUES
|
|
('41000000-0000-0000-0000-000000000001', 'g4-smoke-learner', 'learner', 'g4-smoke', 'approved', now()),
|
|
('41000000-0000-0000-0000-000000000002', 'g4-smoke-teacher', 'instructor', 'g4-smoke', 'approved', now()),
|
|
('41000000-0000-0000-0000-000000000003', 'g4-smoke-other', 'learner', 'g4-other', 'approved', now());
|
|
|
|
INSERT INTO app.sessions (id, learner_id, session_no)
|
|
VALUES ('42000000-0000-0000-0000-000000000001', '41000000-0000-0000-0000-000000000001', 1);
|
|
INSERT INTO app.turns (id, session_id, seq, speaker, text_masked, actor_kind)
|
|
VALUES (
|
|
'43000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
1,
|
|
'client',
|
|
'[G4 smoke masked scene]',
|
|
'client_ai'
|
|
);
|
|
|
|
SET LOCAL ROLE :"app_role";
|
|
SELECT set_config('app.ai_context', '1', true);
|
|
SELECT set_config('app.current_ai_view', 'evaluator', true);
|
|
SELECT set_config('app.current_role', 'admin', true);
|
|
SELECT set_config('app.current_uid', '41000000-0000-0000-0000-000000000002', true);
|
|
|
|
INSERT INTO app.practice_prescription_submission (
|
|
submission_id, session_id, learner_id, content_hash, created_by_role
|
|
) VALUES (
|
|
'44000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
repeat('a', 64),
|
|
'agent'
|
|
);
|
|
INSERT INTO app.practice_coaching_card (
|
|
coaching_card_record_id, submission_id, session_id, learner_id,
|
|
card_key, scene_id, coach_claim, card_payload, evidence_turn_ids,
|
|
source_refs, uncertainty, counterevidence
|
|
) VALUES (
|
|
'45000000-0000-0000-0000-000000000001',
|
|
'44000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'oas-g4-card-smoke',
|
|
'scene-smoke',
|
|
'내담자 반응을 확인하는 재연습 장면을 실행한다.',
|
|
'{"card_id":"oas-g4-card-smoke"}',
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
ARRAY['source-pack:g4-smoke:v1'],
|
|
0.2,
|
|
ARRAY[]::TEXT[]
|
|
);
|
|
INSERT INTO app.practice_prescription (
|
|
prescription_record_id, prescription_key, submission_id,
|
|
coaching_card_record_id, session_id, learner_id, competency_id,
|
|
criterion_id, observable_behavior, activity_mode, scenario_variant_id,
|
|
scenario_novelty, difficulty_level, prescription_payload,
|
|
evidence_turn_ids, uncertainty, counterevidence
|
|
) VALUES (
|
|
'46000000-0000-0000-0000-000000000001',
|
|
'oas-g4-practice-smoke',
|
|
'44000000-0000-0000-0000-000000000001',
|
|
'45000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'competency.empathy.smoke',
|
|
'criterion.reflect-and-check',
|
|
'정서를 반영한 뒤 내담자에게 맞는지 확인한다.',
|
|
'replay',
|
|
'variant-smoke-familiar',
|
|
'familiar',
|
|
1,
|
|
'{
|
|
"schema_version":"vignette.practice-prescription.v1",
|
|
"event_name":"practice.prescribed",
|
|
"prescription_id":"oas-g4-practice-smoke",
|
|
"coaching_card_id":"oas-g4-card-smoke",
|
|
"scene_id":"scene-smoke",
|
|
"competency_id":"competency.empathy.smoke",
|
|
"criterion_id":"criterion.reflect-and-check",
|
|
"observable_behavior":"정서를 반영한 뒤 내담자에게 맞는지 확인한다.",
|
|
"activity":{
|
|
"mode":"replay",
|
|
"launch_intent":"practice.replay.launch",
|
|
"scenario_variant_id":"variant-smoke-familiar",
|
|
"scenario_novelty":"familiar",
|
|
"difficulty_level":1,
|
|
"pause_at_evidence_ref":"43000000-0000-0000-0000-000000000001"
|
|
},
|
|
"can_launch":true,
|
|
"evidence_refs":[{
|
|
"ref_id":"43000000-0000-0000-0000-000000000001",
|
|
"scene_id":"scene-smoke","turn_index":1,"actor":"client","kind":"scene_context"
|
|
}],
|
|
"source_refs":["source-pack:g4-smoke:v1"],
|
|
"uncertainty":0.2,
|
|
"counterevidence":[]
|
|
}'::JSONB,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
0.2,
|
|
ARRAY[]::TEXT[]
|
|
);
|
|
INSERT INTO app.competency_graph_snapshot (
|
|
snapshot_id, learner_id, session_id, snapshot_no, content_hash,
|
|
source_prescription_submission_id, graph_payload, evidence_turn_ids,
|
|
created_by_role
|
|
) VALUES (
|
|
'47000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
1,
|
|
repeat('b', 64),
|
|
'44000000-0000-0000-0000-000000000001',
|
|
'{
|
|
"schema_version":"vignette.competency-graph.v1",
|
|
"definitions":[{
|
|
"competency_id":"competency.empathy.smoke",
|
|
"label_ko":"스모크 공감",
|
|
"description":"스모크 검증을 위한 합성 공감 역량 정의다.",
|
|
"prerequisite_ids":[]
|
|
}],
|
|
"states":[{
|
|
"competency_id":"competency.empathy.smoke",
|
|
"band":"unassessed","forgetting_risk":0.7,"uncertainty":1.0,
|
|
"attempt_count":0,"familiar_demonstrations":0,
|
|
"unseen_transfer_demonstrations":0,"highest_familiar_difficulty":0,
|
|
"evidence_refs":[],"counterevidence":[]
|
|
}]
|
|
}'::JSONB,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
'agent'
|
|
);
|
|
INSERT INTO app.practice_curriculum_decision_event (
|
|
decision_id, source_snapshot_id, selected_prescription_record_id,
|
|
learner_id, session_id, content_hash, decision_payload, created_by_role
|
|
) VALUES (
|
|
'48000000-0000-0000-0000-000000000001',
|
|
'47000000-0000-0000-0000-000000000001',
|
|
'46000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
repeat('c', 64),
|
|
'{
|
|
"schema_version":"vignette.curriculum-decision.v1",
|
|
"selected_prescription_id":"oas-g4-practice-smoke",
|
|
"competency_id":"competency.empathy.smoke",
|
|
"competency_band":"unassessed","forgetting_risk":0.7,
|
|
"mode":"replay",
|
|
"selection_basis":["weakest_available_band:unassessed","forgetting_risk:0.700"],
|
|
"deferred_prescription_ids":[],"blocked_prescription_reasons":[]
|
|
}'::JSONB,
|
|
'agent'
|
|
);
|
|
|
|
-- A client-model database context must not inherit the human admin GUC or see
|
|
-- evaluator-owned prescription/curriculum state.
|
|
SELECT set_config('app.current_ai_view', 'client', true);
|
|
DO $$ BEGIN
|
|
IF (
|
|
(SELECT count(*) FROM app.practice_prescription_submission)
|
|
+ (SELECT count(*) FROM app.practice_coaching_card)
|
|
+ (SELECT count(*) FROM app.practice_prescription)
|
|
+ (SELECT count(*) FROM app.competency_graph_snapshot)
|
|
+ (SELECT count(*) FROM app.practice_curriculum_decision_event)
|
|
) <> 0 THEN
|
|
RAISE EXCEPTION 'client AI view leaked evaluator practice data';
|
|
END IF;
|
|
END $$;
|
|
SELECT set_config('app.current_ai_view', 'evaluator', true);
|
|
|
|
INSERT INTO app.practice_episode_submission (
|
|
episode_submission_id, episode_key, prescription_record_id,
|
|
session_id, learner_id, content_hash, assessment_payload, progress,
|
|
mastery_allowed, mastery_blockers, uncertainty, evidence_turn_ids,
|
|
counterevidence, created_by_role
|
|
) VALUES (
|
|
'49000000-0000-0000-0000-000000000001',
|
|
'oas-g4-episode-smoke-valid',
|
|
'46000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
repeat('d', 64),
|
|
'{
|
|
"progress":"mastered","mastery_allowed":true,"competency_id":"competency.empathy.smoke",
|
|
"attempts":[
|
|
{"outcome":"needs_retry","scenario_novelty":"familiar","scenario_variant_id":"variant-smoke-familiar","utterance_template_id":"template-baseline"},
|
|
{"outcome":"passed","scenario_novelty":"familiar","scenario_variant_id":"variant-smoke-familiar","utterance_template_id":"template-familiar"},
|
|
{"outcome":"passed","scenario_novelty":"unseen_transfer","scenario_variant_id":"variant-smoke-unseen","utterance_template_id":"template-adapted"}
|
|
]
|
|
}'::JSONB,
|
|
'mastered',
|
|
TRUE,
|
|
ARRAY[]::TEXT[],
|
|
0.2,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
ARRAY[]::TEXT[],
|
|
'learner'
|
|
);
|
|
INSERT INTO app.practice_attempt_evidence (
|
|
attempt_record_id, attempt_key, episode_submission_id, session_id, learner_id,
|
|
sequence_no, scenario_variant_id, scenario_novelty, difficulty_level,
|
|
criterion_status, client_response, outcome, utterance_template_id,
|
|
uncertainty, evidence_turn_ids, counterevidence, attempt_payload
|
|
) VALUES
|
|
(
|
|
'4a000000-0000-0000-0000-000000000001','oas-g4-attempt-smoke-1',
|
|
'49000000-0000-0000-0000-000000000001','42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',1,'variant-smoke-familiar','familiar',1,
|
|
'not_observed','rejecting','needs_retry','template-baseline',0.2,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],ARRAY['baseline_miss'],
|
|
'{"kind":"baseline"}'::JSONB
|
|
),
|
|
(
|
|
'4a000000-0000-0000-0000-000000000002','oas-g4-attempt-smoke-2',
|
|
'49000000-0000-0000-0000-000000000001','42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',2,'variant-smoke-familiar','familiar',1,
|
|
'observed','engaged','passed','template-familiar',0.2,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],ARRAY[]::TEXT[],
|
|
'{"kind":"familiar-pass"}'::JSONB
|
|
),
|
|
(
|
|
'4a000000-0000-0000-0000-000000000003','oas-g4-attempt-smoke-3',
|
|
'49000000-0000-0000-0000-000000000001','42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',3,'variant-smoke-unseen','unseen_transfer',4,
|
|
'observed','explicit_alignment','passed','template-adapted',0.2,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],ARRAY[]::TEXT[],
|
|
'{"kind":"unseen-pass"}'::JSONB
|
|
);
|
|
|
|
INSERT INTO app.competency_graph_snapshot (
|
|
snapshot_id, learner_id, session_id, snapshot_no, content_hash,
|
|
supersedes_snapshot_id, source_episode_submission_id, graph_payload,
|
|
evidence_turn_ids, created_by_role
|
|
) VALUES (
|
|
'4b000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
2,
|
|
repeat('e', 64),
|
|
'47000000-0000-0000-0000-000000000001',
|
|
'49000000-0000-0000-0000-000000000001',
|
|
'{
|
|
"schema_version":"vignette.competency-graph.v1",
|
|
"definitions":[{
|
|
"competency_id":"competency.empathy.smoke","label_ko":"스모크 공감",
|
|
"description":"스모크 검증을 위한 합성 공감 역량 정의다.","prerequisite_ids":[]
|
|
}],
|
|
"states":[{
|
|
"competency_id":"competency.empathy.smoke","band":"transfer_verified",
|
|
"forgetting_risk":0.45,"uncertainty":0.2,"attempt_count":3,
|
|
"familiar_demonstrations":1,"unseen_transfer_demonstrations":1,
|
|
"highest_familiar_difficulty":1,"evidence_refs":[],"counterevidence":[]
|
|
}]
|
|
}'::JSONB,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
'learner'
|
|
);
|
|
INSERT INTO app.practice_curriculum_decision_event (
|
|
decision_id, source_snapshot_id, selected_prescription_record_id,
|
|
learner_id, session_id, content_hash, decision_payload, created_by_role
|
|
) VALUES (
|
|
'4b000000-0000-0000-0000-000000000002',
|
|
'4b000000-0000-0000-0000-000000000001',
|
|
'46000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
repeat('f', 64),
|
|
'{"selected_prescription_id":"oas-g4-practice-smoke"}'::JSONB,
|
|
'learner'
|
|
);
|
|
|
|
-- Learner-self, teacher cohort and unrelated-user isolation.
|
|
SELECT set_config('app.ai_context', '', true);
|
|
SELECT set_config('app.current_ai_view', '', true);
|
|
SELECT set_config('app.current_role', 'learner', true);
|
|
SELECT set_config('app.current_uid', '41000000-0000-0000-0000-000000000001', true);
|
|
DO $$ BEGIN
|
|
IF (SELECT count(*) FROM app.practice_prescription) <> 1 THEN
|
|
RAISE EXCEPTION 'learner self RLS smoke failed';
|
|
END IF;
|
|
END $$;
|
|
SELECT set_config('app.current_uid', '41000000-0000-0000-0000-000000000003', true);
|
|
DO $$ BEGIN
|
|
IF (SELECT count(*) FROM app.practice_prescription) <> 0 THEN
|
|
RAISE EXCEPTION 'unrelated learner RLS leaked practice data';
|
|
END IF;
|
|
END $$;
|
|
SELECT set_config('app.current_role', 'instructor', true);
|
|
SELECT set_config('app.current_uid', '41000000-0000-0000-0000-000000000002', true);
|
|
SELECT set_config('app.current_cohort', 'g4-smoke', true);
|
|
DO $$ BEGIN
|
|
IF (SELECT count(*) FROM app.practice_prescription) <> 1 THEN
|
|
RAISE EXCEPTION 'teacher cohort RLS smoke failed';
|
|
END IF;
|
|
END $$;
|
|
SELECT set_config('app.current_cohort', 'g4-other', true);
|
|
DO $$ BEGIN
|
|
IF (SELECT count(*) FROM app.practice_prescription) <> 0 THEN
|
|
RAISE EXCEPTION 'teacher cross-cohort RLS leaked practice data';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Teacher correction remains append-only and cannot mutate the original attempt.
|
|
SELECT set_config('app.current_cohort', 'g4-smoke', true);
|
|
INSERT INTO app.practice_teacher_correction (
|
|
correction_id, submission_id, content_hash, attempt_record_id,
|
|
episode_submission_id, session_id, learner_id, correction_no,
|
|
corrected_outcome, correction_reason, evidence_turn_ids, counterevidence,
|
|
created_by_uid, created_by_role
|
|
) VALUES (
|
|
'4c000000-0000-0000-0000-000000000001',
|
|
'4c000000-0000-0000-0000-000000000002',
|
|
repeat('1', 64),
|
|
'4a000000-0000-0000-0000-000000000001',
|
|
'49000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
1,
|
|
'needs_retry',
|
|
'내담자 후속 반응을 근거로 재시도가 필요하다고 확인했다.',
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[],
|
|
ARRAY['client_response_not_engaged'],
|
|
'41000000-0000-0000-0000-000000000002',
|
|
'instructor'
|
|
);
|
|
|
|
RESET ROLE;
|
|
|
|
-- Same submission id with changed content is rejected.
|
|
DO $$ BEGIN
|
|
BEGIN
|
|
INSERT INTO app.practice_prescription_submission (
|
|
submission_id, session_id, learner_id, content_hash, created_by_role
|
|
) VALUES (
|
|
'44000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001', repeat('9', 64), 'agent'
|
|
);
|
|
RAISE EXCEPTION 'changed idempotent submission was accepted';
|
|
EXCEPTION WHEN unique_violation THEN NULL;
|
|
END;
|
|
END $$;
|
|
|
|
-- Memorized phrase cannot be promoted to mastery.
|
|
DO $$ BEGIN
|
|
BEGIN
|
|
INSERT INTO app.practice_episode_submission (
|
|
episode_submission_id, episode_key, prescription_record_id,
|
|
session_id, learner_id, content_hash, assessment_payload, progress,
|
|
mastery_allowed, mastery_blockers, uncertainty, evidence_turn_ids,
|
|
counterevidence, created_by_role
|
|
) VALUES (
|
|
'49000000-0000-0000-0000-000000000009',
|
|
'oas-g4-episode-smoke-hack',
|
|
'46000000-0000-0000-0000-000000000001',
|
|
'42000000-0000-0000-0000-000000000001',
|
|
'41000000-0000-0000-0000-000000000001',
|
|
repeat('8', 64),
|
|
'{
|
|
"progress":"mastered","mastery_allowed":true,"competency_id":"competency.empathy.smoke",
|
|
"attempts":[
|
|
{"outcome":"passed","scenario_novelty":"familiar","scenario_variant_id":"v1","utterance_template_id":"memorized"},
|
|
{"outcome":"passed","scenario_novelty":"unseen_transfer","scenario_variant_id":"v2","utterance_template_id":"memorized"}
|
|
]
|
|
}'::JSONB,
|
|
'mastered', TRUE, ARRAY[]::TEXT[], 0.2,
|
|
ARRAY['43000000-0000-0000-0000-000000000001']::UUID[], ARRAY[]::TEXT[], 'learner'
|
|
);
|
|
RAISE EXCEPTION 'memorized phrase mastery was accepted';
|
|
EXCEPTION WHEN check_violation THEN NULL;
|
|
END;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
BEGIN
|
|
UPDATE app.practice_attempt_evidence
|
|
SET outcome = 'passed'
|
|
WHERE attempt_record_id = '4a000000-0000-0000-0000-000000000001';
|
|
RAISE EXCEPTION 'append-only practice attempt update was accepted';
|
|
EXCEPTION WHEN object_not_in_prerequisite_state THEN NULL;
|
|
END;
|
|
END $$;
|
|
|
|
ROLLBACK;
|
|
|
|
SELECT 'g4_smoke_rollback_residue' AS check_name,
|
|
(
|
|
(SELECT count(*) FROM app.practice_prescription_submission
|
|
WHERE submission_id = '44000000-0000-0000-0000-000000000001')
|
|
+ (SELECT count(*) FROM app.practice_episode_submission
|
|
WHERE episode_submission_id = '49000000-0000-0000-0000-000000000001')
|
|
+ (SELECT count(*) FROM app.practice_teacher_correction
|
|
WHERE correction_id = '4c000000-0000-0000-0000-000000000001')
|
|
+ (SELECT count(*) FROM app.app_user
|
|
WHERE user_id = '41000000-0000-0000-0000-000000000001')
|
|
) AS residue_count;
|