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,264 @@
-- Outcome & Alliance OS G5: completed-session transfer execution ledger.
-- Prerequisites: 06_session_evaluation.sql, 07_measurement_foundation.sql,
-- 11_calibration_transfer.sql, 15_self_directed_practice_runtime.sql.
CREATE TABLE IF NOT EXISTS app.calibration_transfer_execution_event (
execution_event_id UUID PRIMARY KEY,
original_transfer_trial_record_id UUID NOT NULL
REFERENCES app.calibration_transfer_trial(transfer_trial_record_id) ON DELETE RESTRICT,
transfer_suite_record_id UUID NOT NULL,
practice_session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT,
learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT,
competency_id TEXT NOT NULL CHECK (competency_id ~ '^competency\.[a-z0-9_.-]+$'),
scenario_variant_id TEXT NOT NULL CHECK (length(btrim(scenario_variant_id)) > 0),
scenario_novelty TEXT NOT NULL DEFAULT 'unseen_transfer'
CHECK (scenario_novelty = 'unseen_transfer'),
context_variant TEXT NOT NULL CHECK (length(btrim(context_variant)) > 0),
relationship_style TEXT NOT NULL CHECK (
relationship_style IN ('collaborative','withdrawn','confrontational','ambivalent')
),
difficulty_level INT NOT NULL CHECK (difficulty_level BETWEEN 1 AND 5),
expression_variant TEXT NOT NULL CHECK (length(btrim(expression_variant)) > 0),
synthetic_subgroup TEXT NOT NULL CHECK (synthetic_subgroup ~ '^synthetic-[a-z0-9-]+$'),
scenario_family_id TEXT NOT NULL CHECK (length(btrim(scenario_family_id)) > 0),
phrase_family_id TEXT NOT NULL CHECK (length(btrim(phrase_family_id)) > 0),
training_phrase_collision BOOLEAN NOT NULL DEFAULT FALSE,
status TEXT NOT NULL CHECK (status IN ('passed','failed','insufficient_evidence')),
uncertainty DOUBLE PRECISION NOT NULL CHECK (uncertainty BETWEEN 0 AND 1),
evidence_turn_ids UUID[] NOT NULL DEFAULT '{}',
normalized_evaluator_labels JSONB NOT NULL
CHECK (jsonb_typeof(normalized_evaluator_labels) = 'object'),
counterevidence TEXT[] NOT NULL DEFAULT '{}',
source_kind TEXT NOT NULL DEFAULT 'model_inferred' CHECK (source_kind = 'model_inferred'),
perspective TEXT NOT NULL DEFAULT 'independent_observer'
CHECK (perspective = 'independent_observer'),
model_run_id UUID NOT NULL REFERENCES audit.model_run(model_run_id) ON DELETE RESTRICT,
instrument_id TEXT NOT NULL DEFAULT 'unseen-transfer-g5',
instrument_version TEXT NOT NULL DEFAULT '1.0.0',
observer_version TEXT NOT NULL CHECK (length(btrim(observer_version)) > 0),
data_classification TEXT NOT NULL DEFAULT 'synthetic_educational'
CHECK (data_classification = 'synthetic_educational'),
clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE CHECK (clinical_claim_allowed = FALSE),
created_by_role TEXT NOT NULL DEFAULT 'learner' CHECK (created_by_role = 'learner'),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (original_transfer_trial_record_id, practice_session_id),
UNIQUE (practice_session_id),
CONSTRAINT calibration_transfer_execution_instrument_fkey
FOREIGN KEY (instrument_id, instrument_version)
REFERENCES app.measurement_instrument(instrument_id, instrument_version)
ON DELETE RESTRICT,
CONSTRAINT calibration_transfer_execution_instrument_check CHECK (
instrument_id = 'unseen-transfer-g5' AND instrument_version = '1.0.0'
),
CHECK (
(status = 'insufficient_evidence' AND uncertainty = 1 AND cardinality(evidence_turn_ids) = 0)
OR (status IN ('passed','failed') AND cardinality(evidence_turn_ids) > 0)
),
CHECK (status <> 'failed' OR cardinality(counterevidence) > 0)
);
-- This init file is also the forward owner migration for already-created
-- development/NAS schemas. Preserve existing rows while adding the G0
-- instrument provenance required by the current execution ledger.
ALTER TABLE app.calibration_transfer_execution_event
ADD COLUMN IF NOT EXISTS instrument_id TEXT DEFAULT 'unseen-transfer-g5';
ALTER TABLE app.calibration_transfer_execution_event
ADD COLUMN IF NOT EXISTS instrument_version TEXT DEFAULT '1.0.0';
UPDATE app.calibration_transfer_execution_event
SET instrument_id = 'unseen-transfer-g5'
WHERE instrument_id IS NULL;
UPDATE app.calibration_transfer_execution_event
SET instrument_version = '1.0.0'
WHERE instrument_version IS NULL;
ALTER TABLE app.calibration_transfer_execution_event
ALTER COLUMN instrument_id SET DEFAULT 'unseen-transfer-g5',
ALTER COLUMN instrument_id SET NOT NULL,
ALTER COLUMN instrument_version SET DEFAULT '1.0.0',
ALTER COLUMN instrument_version SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'calibration_transfer_execution_instrument_fkey'
AND conrelid = 'app.calibration_transfer_execution_event'::regclass
) THEN
ALTER TABLE app.calibration_transfer_execution_event
ADD CONSTRAINT calibration_transfer_execution_instrument_fkey
FOREIGN KEY (instrument_id, instrument_version)
REFERENCES app.measurement_instrument(instrument_id, instrument_version)
ON DELETE RESTRICT;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'calibration_transfer_execution_instrument_check'
AND conrelid = 'app.calibration_transfer_execution_event'::regclass
) THEN
ALTER TABLE app.calibration_transfer_execution_event
ADD CONSTRAINT calibration_transfer_execution_instrument_check CHECK (
instrument_id = 'unseen-transfer-g5' AND instrument_version = '1.0.0'
);
END IF;
END;
$$;
CREATE INDEX IF NOT EXISTS idx_calibration_transfer_execution_learner_competency
ON app.calibration_transfer_execution_event(learner_id, competency_id, created_at);
CREATE INDEX IF NOT EXISTS idx_calibration_transfer_execution_trial
ON app.calibration_transfer_execution_event(original_transfer_trial_record_id);
CREATE OR REPLACE FUNCTION audit.enforce_calibration_transfer_execution()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
original_row app.calibration_transfer_trial%ROWTYPE;
training_phrases TEXT[];
practice_learner UUID;
practice_started TIMESTAMPTZ;
practice_ended TIMESTAMPTZ;
original_created TIMESTAMPTZ;
evaluation_status TEXT;
evaluation_scope TEXT;
run_session UUID;
run_role TEXT;
run_status TEXT;
BEGIN
SELECT trial.*
INTO original_row
FROM app.calibration_transfer_trial trial
WHERE trial.transfer_trial_record_id = NEW.original_transfer_trial_record_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'original transfer trial does not exist';
END IF;
SELECT suite.training_phrase_family_ids
INTO training_phrases
FROM app.calibration_transfer_suite suite
WHERE suite.transfer_suite_record_id = original_row.transfer_suite_record_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'original transfer suite does not exist';
END IF;
original_created := original_row.created_at;
IF NEW.transfer_suite_record_id IS DISTINCT FROM original_row.transfer_suite_record_id
OR NEW.learner_id IS DISTINCT FROM original_row.learner_id
OR NEW.competency_id IS DISTINCT FROM original_row.competency_id
OR NEW.scenario_variant_id IS DISTINCT FROM original_row.scenario_variant_id
OR NEW.scenario_novelty IS DISTINCT FROM original_row.scenario_novelty
OR NEW.context_variant IS DISTINCT FROM original_row.context_variant
OR NEW.relationship_style IS DISTINCT FROM original_row.relationship_style
OR NEW.difficulty_level IS DISTINCT FROM original_row.difficulty_level
OR NEW.expression_variant IS DISTINCT FROM original_row.expression_variant
OR NEW.synthetic_subgroup IS DISTINCT FROM original_row.synthetic_subgroup
OR NEW.scenario_family_id IS DISTINCT FROM original_row.scenario_family_id
OR NEW.phrase_family_id IS DISTINCT FROM original_row.phrase_family_id
OR NEW.training_phrase_collision IS DISTINCT FROM
(original_row.phrase_family_id = ANY(training_phrases)) THEN
RAISE EXCEPTION 'actual transfer context must be copied from server ledger';
END IF;
IF NOT EXISTS (
SELECT 1
FROM app.calibration_prediction_history history
JOIN app.calibration_prediction_lock prediction_lock
ON prediction_lock.history_id = history.history_id
WHERE history.learner_id = NEW.learner_id
AND history.competency_id = NEW.competency_id
AND prediction_lock.created_at <= original_created
) THEN
RAISE EXCEPTION 'self-prediction lock must precede transfer reveal';
END IF;
SELECT session.learner_id, session.started_at, session.ended_at,
evaluation.status, evaluation.scope
INTO practice_learner, practice_started, practice_ended,
evaluation_status, evaluation_scope
FROM app.sessions session
LEFT JOIN app.session_evaluation evaluation ON evaluation.session_id = session.id
WHERE session.id = NEW.practice_session_id;
IF NOT FOUND OR practice_learner IS DISTINCT FROM NEW.learner_id THEN
RAISE EXCEPTION 'practice session must belong to learner';
END IF;
IF practice_ended IS NULL OR evaluation_status IS DISTINCT FROM 'ready'
OR evaluation_scope IS DISTINCT FROM 'session_end' THEN
RAISE EXCEPTION 'practice session must be ended with ready session_end evaluation';
END IF;
IF practice_started <= original_created THEN
RAISE EXCEPTION 'practice session must start after original transfer trial';
END IF;
IF EXISTS (
SELECT 1 FROM unnest(NEW.evidence_turn_ids) evidence_turn_id
LEFT JOIN app.turns turn_row ON turn_row.id = evidence_turn_id
WHERE turn_row.id IS NULL OR turn_row.session_id <> NEW.practice_session_id
) THEN
RAISE EXCEPTION 'actual transfer evidence turns must belong to practice session';
END IF;
IF NOT (
NEW.normalized_evaluator_labels
?& ARRAY[
'technique_codes','client_state_codes','appropriateness',
'intent_deviation_dimensions','evaluator_error_count'
]
) OR EXISTS (
SELECT 1
FROM jsonb_object_keys(NEW.normalized_evaluator_labels) AS keys(label_key)
WHERE label_key <> ALL(ARRAY[
'technique_codes','client_state_codes','appropriateness',
'intent_deviation_dimensions','evaluator_error_count'
])
) THEN
RAISE EXCEPTION 'actual transfer event accepts normalized evaluator labels only';
END IF;
SELECT session_id, agent_role, status
INTO run_session, run_role, run_status
FROM audit.model_run
WHERE model_run_id = NEW.model_run_id;
IF NOT FOUND OR run_session IS DISTINCT FROM NEW.practice_session_id
OR run_role IS DISTINCT FROM 'evaluator' OR run_status IS DISTINCT FROM 'ready' THEN
RAISE EXCEPTION 'actual transfer requires ready evaluator model provenance';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_calibration_transfer_execution_contract
ON app.calibration_transfer_execution_event;
CREATE TRIGGER trg_calibration_transfer_execution_contract
BEFORE INSERT ON app.calibration_transfer_execution_event
FOR EACH ROW EXECUTE FUNCTION audit.enforce_calibration_transfer_execution();
DROP TRIGGER IF EXISTS trg_calibration_transfer_execution_append_only
ON app.calibration_transfer_execution_event;
CREATE TRIGGER trg_calibration_transfer_execution_append_only
BEFORE UPDATE OR DELETE ON app.calibration_transfer_execution_event
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
ALTER TABLE app.calibration_transfer_execution_event ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_calibration_transfer_execution_event_select
ON app.calibration_transfer_execution_event;
CREATE POLICY p_calibration_transfer_execution_event_select
ON app.calibration_transfer_execution_event FOR SELECT USING (
(app.is_ai_context() AND current_setting('app.current_ai_view', true) = 'evaluator')
OR (NOT app.is_ai_context() AND (
app.current_role_name() = 'admin'
OR (app.current_role_name() = 'learner' AND learner_id = app.current_uid())
OR (app.current_role_name() = 'instructor' AND EXISTS (
SELECT 1 FROM app.app_user learner
WHERE learner.user_id = learner_id
AND learner.cohort = current_setting('app.current_cohort', true)
))
))
);
DROP POLICY IF EXISTS p_calibration_transfer_execution_event_insert
ON app.calibration_transfer_execution_event;
CREATE POLICY p_calibration_transfer_execution_event_insert
ON app.calibration_transfer_execution_event FOR INSERT WITH CHECK (
app.is_ai_context()
AND current_setting('app.current_ai_view', true) = 'evaluator'
AND learner_id = app.current_uid()
AND created_by_role = 'learner'
);