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:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
264
infra/db/init/16_calibration_transfer_actual_execution.sql
Normal file
264
infra/db/init/16_calibration_transfer_actual_execution.sql
Normal 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'
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue