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
553
infra/db/init/08_outcome_trajectory.sql
Normal file
553
infra/db/init/08_outcome_trajectory.sql
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
-- Outcome & Alliance OS G2: synthetic expected arcs, longitudinal revisions,
|
||||
-- observation provenance, and role-safe relationship memory.
|
||||
-- Prerequisites: 02_schema.sql, 04_audit_eval_rls.sql, 07_measurement_foundation.sql.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS audit;
|
||||
CREATE SCHEMA IF NOT EXISTS ds;
|
||||
|
||||
-- The expected arc is an educational synthetic reference, never a clinical norm.
|
||||
CREATE TABLE IF NOT EXISTS ds.synthetic_outcome_arc (
|
||||
arc_id TEXT PRIMARY KEY,
|
||||
schema_version TEXT NOT NULL
|
||||
CHECK (schema_version = 'vignette.synthetic-outcome-arc.v1'),
|
||||
title_ko TEXT NOT NULL CHECK (length(btrim(title_ko)) > 0),
|
||||
data_classification TEXT NOT NULL
|
||||
CHECK (data_classification = 'synthetic_educational'),
|
||||
clinical_claim_allowed BOOLEAN NOT NULL DEFAULT FALSE
|
||||
CHECK (clinical_claim_allowed = FALSE),
|
||||
provenance_note TEXT NOT NULL CHECK (length(btrim(provenance_note)) >= 20),
|
||||
expected_arc JSONB NOT NULL CHECK (
|
||||
jsonb_typeof(expected_arc) = 'object'
|
||||
AND expected_arc->>'data_classification' = 'synthetic_educational'
|
||||
AND (expected_arc->>'clinical_claim_allowed')::boolean = FALSE
|
||||
AND jsonb_array_length(expected_arc->'distributions') = 15
|
||||
),
|
||||
content_hash TEXT NOT NULL UNIQUE CHECK (content_hash ~ '^[a-f0-9]{64}$'),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO ds.synthetic_outcome_arc (
|
||||
arc_id, schema_version, title_ko, data_classification,
|
||||
clinical_claim_allowed, provenance_note, expected_arc, content_hash
|
||||
) VALUES (
|
||||
'oas-g2-arc-001',
|
||||
'vignette.synthetic-outcome-arc.v1',
|
||||
'교육용 초기 5회기 기대 궤적',
|
||||
'synthetic_educational',
|
||||
FALSE,
|
||||
'교육용 합성 사례의 결정론 테스트 분포이며 실제 내담자, 임상 규준, 치료 효과 또는 진단 예측을 나타내지 않는다.',
|
||||
$arc${
|
||||
"schema_version":"vignette.synthetic-outcome-arc.v1",
|
||||
"arc_id":"oas-g2-arc-001",
|
||||
"title_ko":"교육용 초기 5회기 기대 궤적",
|
||||
"data_classification":"synthetic_educational",
|
||||
"clinical_claim_allowed":false,
|
||||
"provenance_note":"교육용 합성 사례의 결정론 테스트 분포이며 실제 내담자, 임상 규준, 치료 효과 또는 진단 예측을 나타내지 않는다.",
|
||||
"distributions":[
|
||||
{"session_no":1,"axis":"distress_load","mean":0.70,"standard_deviation":0.10,"lower_reference":0.50,"upper_reference":0.90,"sample_size":200,"expected_direction":"lower_is_better"},
|
||||
{"session_no":1,"axis":"daily_functioning","mean":0.30,"standard_deviation":0.10,"lower_reference":0.10,"upper_reference":0.50,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":1,"axis":"learning_engagement","mean":0.40,"standard_deviation":0.10,"lower_reference":0.20,"upper_reference":0.60,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":2,"axis":"distress_load","mean":0.62,"standard_deviation":0.10,"lower_reference":0.42,"upper_reference":0.82,"sample_size":200,"expected_direction":"lower_is_better"},
|
||||
{"session_no":2,"axis":"daily_functioning","mean":0.40,"standard_deviation":0.10,"lower_reference":0.20,"upper_reference":0.60,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":2,"axis":"learning_engagement","mean":0.48,"standard_deviation":0.10,"lower_reference":0.28,"upper_reference":0.68,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":3,"axis":"distress_load","mean":0.54,"standard_deviation":0.10,"lower_reference":0.34,"upper_reference":0.74,"sample_size":200,"expected_direction":"lower_is_better"},
|
||||
{"session_no":3,"axis":"daily_functioning","mean":0.50,"standard_deviation":0.10,"lower_reference":0.30,"upper_reference":0.70,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":3,"axis":"learning_engagement","mean":0.56,"standard_deviation":0.10,"lower_reference":0.36,"upper_reference":0.76,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":4,"axis":"distress_load","mean":0.46,"standard_deviation":0.10,"lower_reference":0.26,"upper_reference":0.66,"sample_size":200,"expected_direction":"lower_is_better"},
|
||||
{"session_no":4,"axis":"daily_functioning","mean":0.60,"standard_deviation":0.10,"lower_reference":0.40,"upper_reference":0.80,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":4,"axis":"learning_engagement","mean":0.64,"standard_deviation":0.10,"lower_reference":0.44,"upper_reference":0.84,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":5,"axis":"distress_load","mean":0.38,"standard_deviation":0.10,"lower_reference":0.18,"upper_reference":0.58,"sample_size":200,"expected_direction":"lower_is_better"},
|
||||
{"session_no":5,"axis":"daily_functioning","mean":0.68,"standard_deviation":0.10,"lower_reference":0.48,"upper_reference":0.88,"sample_size":200,"expected_direction":"higher_is_better"},
|
||||
{"session_no":5,"axis":"learning_engagement","mean":0.72,"standard_deviation":0.10,"lower_reference":0.52,"upper_reference":0.92,"sample_size":200,"expected_direction":"higher_is_better"}
|
||||
]
|
||||
}$arc$::jsonb,
|
||||
'4a813340ededc4624e1607308579d0c2cfff23024ac68de0cd3c8d4b8706ab8f'
|
||||
)
|
||||
ON CONFLICT (arc_id) DO NOTHING;
|
||||
|
||||
INSERT INTO app.measurement_instrument (
|
||||
instrument_id, instrument_version, name_ko, instrument_kind, construct,
|
||||
validation_basis, scoring_schema, metadata
|
||||
) VALUES (
|
||||
'vignette-session-outcome', '1.0.0', '교육용 회기 성과 3축 관측',
|
||||
'training_metric', 'session_outcome',
|
||||
'교육용 합성 사례에서 부담감·일상 기능·학습 참여를 축별로 기록한다. 임상 척도나 치료 효과 지표가 아니다.',
|
||||
'{"dimensions":["distress_load","daily_functioning","learning_engagement"],"min":0,"max":1}'::jsonb,
|
||||
'{"clinical_claim_allowed":false,"data_classification":"synthetic_educational"}'::jsonb
|
||||
), (
|
||||
'vignette-session-outcome-checkin', '1.0.0', '학습자 회기 성과 3축 체크인',
|
||||
'training_metric', 'session_outcome',
|
||||
'수련생이 종료된 합성 회기를 돌아보며 기록하는 부담감·일상 기능·학습 참여 자기보고. 임상 척도나 치료 효과 지표가 아니다.',
|
||||
'{"dimensions":["distress_load","daily_functioning","learning_engagement"],"min":0,"max":1}'::jsonb,
|
||||
'{"clinical_claim_allowed":false,"data_classification":"synthetic_educational","perspective":"learner_self_report"}'::jsonb
|
||||
)
|
||||
ON CONFLICT (instrument_id, instrument_version) DO NOTHING;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_measurement_event_outcome_submission_metadata'
|
||||
AND conrelid = 'app.measurement_event'::regclass
|
||||
) THEN
|
||||
ALTER TABLE app.measurement_event
|
||||
ADD CONSTRAINT ck_measurement_event_outcome_submission_metadata CHECK (
|
||||
NOT (
|
||||
construct = 'session_outcome'
|
||||
AND source_kind = 'learner_reported'
|
||||
AND instrument_id = 'vignette-session-outcome-checkin'
|
||||
)
|
||||
OR (
|
||||
instrument_version = '1.0.0'
|
||||
AND perspective = 'learner_self_report'
|
||||
AND dimension IN ('distress_load','daily_functioning','learning_engagement')
|
||||
AND metadata ?& ARRAY['submission_id','submission_hash']
|
||||
AND metadata->>'submission_id' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
AND metadata->>'submission_hash' ~ '^[a-f0-9]{64}$'
|
||||
)
|
||||
) NOT VALID;
|
||||
ALTER TABLE app.measurement_event
|
||||
VALIDATE CONSTRAINT ck_measurement_event_outcome_submission_metadata;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_measurement_event_outcome_submission_axis
|
||||
ON app.measurement_event (
|
||||
session_id,
|
||||
(metadata->>'submission_id'),
|
||||
dimension
|
||||
)
|
||||
WHERE construct = 'session_outcome'
|
||||
AND source_kind = 'learner_reported'
|
||||
AND perspective = 'learner_self_report'
|
||||
AND instrument_id = 'vignette-session-outcome-checkin'
|
||||
AND instrument_version = '1.0.0';
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit.enforce_outcome_submission_turn_ownership()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT (
|
||||
NEW.construct = 'session_outcome'
|
||||
AND NEW.source_kind = 'learner_reported'
|
||||
AND NEW.perspective = 'learner_self_report'
|
||||
AND NEW.instrument_id = 'vignette-session-outcome-checkin'
|
||||
) THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF cardinality(NEW.evidence_turn_ids) > 0 AND (
|
||||
SELECT count(DISTINCT t.id)
|
||||
FROM app.turns t
|
||||
WHERE t.id = ANY(NEW.evidence_turn_ids)
|
||||
AND t.session_id = NEW.session_id
|
||||
) <> cardinality(NEW.evidence_turn_ids) THEN
|
||||
RAISE EXCEPTION 'outcome submission evidence turns must belong to its session'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_outcome_submission_turn_ownership
|
||||
ON app.measurement_event;
|
||||
CREATE TRIGGER trg_outcome_submission_turn_ownership
|
||||
BEFORE INSERT ON app.measurement_event
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.enforce_outcome_submission_turn_ownership();
|
||||
|
||||
-- Every computation is a new immutable revision. A recomputation never overwrites
|
||||
-- the previous assessment, even when the evidence fingerprint is unchanged.
|
||||
CREATE TABLE IF NOT EXISTS app.outcome_trajectory_revision (
|
||||
revision_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
anchor_session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT,
|
||||
case_id UUID NOT NULL,
|
||||
learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT,
|
||||
expected_arc_id TEXT NOT NULL REFERENCES ds.synthetic_outcome_arc(arc_id) ON DELETE RESTRICT,
|
||||
revision_no INT NOT NULL CHECK (revision_no >= 1),
|
||||
supersedes_revision_id UUID REFERENCES app.outcome_trajectory_revision(revision_id) ON DELETE RESTRICT,
|
||||
source_fingerprint TEXT NOT NULL CHECK (source_fingerprint ~ '^[a-f0-9]{64}$'),
|
||||
assessment JSONB NOT NULL CHECK (
|
||||
jsonb_typeof(assessment) = 'object'
|
||||
AND assessment->>'schema_version' = 'vignette.outcome-trajectory-assessment.v1'
|
||||
AND assessment->>'data_classification' = 'synthetic_educational'
|
||||
AND (assessment->>'clinical_claim_allowed')::boolean = FALSE
|
||||
),
|
||||
observation_count INT NOT NULL CHECK (observation_count BETWEEN 3 AND 15),
|
||||
missing_observation_count INT NOT NULL CHECK (
|
||||
missing_observation_count BETWEEN 0 AND observation_count
|
||||
),
|
||||
recompute_reason TEXT NOT NULL CHECK (length(btrim(recompute_reason)) > 0),
|
||||
computed_by UUID REFERENCES app.app_user(user_id) ON DELETE RESTRICT,
|
||||
computed_role TEXT NOT NULL CHECK (
|
||||
computed_role IN ('learner','instructor','admin','agent','migration')
|
||||
),
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (case_id, revision_no),
|
||||
UNIQUE (supersedes_revision_id),
|
||||
CHECK (revision_id IS DISTINCT FROM supersedes_revision_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_outcome_trajectory_case_revision
|
||||
ON app.outcome_trajectory_revision(case_id, revision_no DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_outcome_trajectory_anchor
|
||||
ON app.outcome_trajectory_revision(anchor_session_id, revision_no DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.outcome_trajectory_observation (
|
||||
observation_snapshot_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
revision_id UUID NOT NULL REFERENCES app.outcome_trajectory_revision(revision_id) ON DELETE RESTRICT,
|
||||
measurement_id UUID REFERENCES app.measurement_event(measurement_id) ON DELETE RESTRICT,
|
||||
session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT,
|
||||
session_no INT NOT NULL CHECK (session_no BETWEEN 1 AND 5),
|
||||
axis TEXT NOT NULL CHECK (
|
||||
axis IN ('distress_load','daily_functioning','learning_engagement')
|
||||
),
|
||||
status TEXT NOT NULL CHECK (status IN ('observed','missing','error')),
|
||||
value DOUBLE PRECISION CHECK (value BETWEEN 0 AND 1),
|
||||
raw_value DOUBLE PRECISION,
|
||||
scale_min DOUBLE PRECISION,
|
||||
scale_max DOUBLE PRECISION,
|
||||
confidence DOUBLE PRECISION CHECK (confidence BETWEEN 0 AND 1),
|
||||
source_kind TEXT NOT NULL CHECK (
|
||||
source_kind IN (
|
||||
'simulated_state','model_inferred','agent_reported',
|
||||
'learner_reported','human_rated','observed_runtime'
|
||||
)
|
||||
),
|
||||
perspective TEXT NOT NULL CHECK (
|
||||
perspective IN (
|
||||
'client_agent_report','learner_self_report','independent_observer',
|
||||
'supervisor_human','client_simulation','runtime_observation'
|
||||
)
|
||||
),
|
||||
instrument_id TEXT NOT NULL,
|
||||
instrument_version TEXT NOT NULL,
|
||||
model_run_id UUID REFERENCES audit.model_run(model_run_id) ON DELETE RESTRICT,
|
||||
evidence_turn_ids UUID[] NOT NULL DEFAULT '{}',
|
||||
missing_reason TEXT,
|
||||
source_created_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (revision_id, session_no, axis),
|
||||
CHECK (
|
||||
(status = 'observed'
|
||||
AND measurement_id IS NOT NULL
|
||||
AND value IS NOT NULL
|
||||
AND raw_value IS NOT NULL
|
||||
AND scale_min IS NOT NULL
|
||||
AND scale_max IS NOT NULL
|
||||
AND scale_max > scale_min
|
||||
AND confidence IS NOT NULL
|
||||
AND missing_reason IS NULL)
|
||||
OR
|
||||
(status IN ('missing','error')
|
||||
AND value IS NULL
|
||||
AND raw_value IS NULL
|
||||
AND confidence IS NULL
|
||||
AND missing_reason IS NOT NULL)
|
||||
),
|
||||
CHECK (
|
||||
(source_kind = 'simulated_state' AND perspective = 'client_simulation')
|
||||
OR (source_kind = 'model_inferred' AND perspective = 'independent_observer')
|
||||
OR (source_kind = 'agent_reported' AND perspective = 'client_agent_report')
|
||||
OR (source_kind = 'learner_reported' AND perspective = 'learner_self_report')
|
||||
OR (source_kind = 'human_rated' AND perspective = 'supervisor_human')
|
||||
OR (source_kind = 'observed_runtime' AND perspective = 'runtime_observation')
|
||||
),
|
||||
CHECK (source_kind NOT IN ('model_inferred','agent_reported') OR model_run_id IS NOT NULL)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_outcome_observation_revision
|
||||
ON app.outcome_trajectory_observation(revision_id, session_no, axis);
|
||||
CREATE INDEX IF NOT EXISTS idx_outcome_observation_measurement
|
||||
ON app.outcome_trajectory_observation(measurement_id)
|
||||
WHERE measurement_id IS NOT NULL;
|
||||
|
||||
-- Relationship events contain no role-private sentences. Role-specific summaries
|
||||
-- live in the projection table so RLS protects the text itself, not only the row.
|
||||
CREATE TABLE IF NOT EXISTS app.relationship_memory_event (
|
||||
memory_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE RESTRICT,
|
||||
case_id UUID NOT NULL,
|
||||
event_type TEXT NOT NULL CHECK (
|
||||
event_type IN (
|
||||
'goal_agreement','task_agreement','rupture_withdrawal',
|
||||
'rupture_confrontation','repair_attempt','repair_confirmed','unresolved_rupture'
|
||||
)
|
||||
),
|
||||
resolves_event_id UUID REFERENCES app.relationship_memory_event(memory_event_id) ON DELETE RESTRICT,
|
||||
visible_to TEXT[] NOT NULL CHECK (
|
||||
cardinality(visible_to) > 0
|
||||
AND visible_to <@ ARRAY['client','counselor','evaluator','supervisor','research']::TEXT[]
|
||||
),
|
||||
evidence_turn_ids UUID[] NOT NULL CHECK (cardinality(evidence_turn_ids) > 0),
|
||||
source_kind TEXT NOT NULL CHECK (
|
||||
source_kind IN ('model_inferred','agent_reported','human_rated','observed_runtime')
|
||||
),
|
||||
model_run_id UUID REFERENCES audit.model_run(model_run_id) ON DELETE RESTRICT,
|
||||
created_by UUID REFERENCES app.app_user(user_id) ON DELETE RESTRICT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (memory_event_id IS DISTINCT FROM resolves_event_id),
|
||||
CHECK (resolves_event_id IS NULL OR event_type = 'repair_confirmed'),
|
||||
CHECK (source_kind NOT IN ('model_inferred','agent_reported') OR model_run_id IS NOT NULL)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_relationship_memory_case_created
|
||||
ON app.relationship_memory_event(case_id, created_at, memory_event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_relationship_memory_session
|
||||
ON app.relationship_memory_event(session_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app.relationship_memory_projection (
|
||||
projection_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
memory_event_id UUID NOT NULL REFERENCES app.relationship_memory_event(memory_event_id) ON DELETE RESTRICT,
|
||||
ai_view TEXT NOT NULL CHECK (
|
||||
ai_view IN ('client','counselor','evaluator','supervisor','research')
|
||||
),
|
||||
summary TEXT NOT NULL CHECK (length(btrim(summary)) > 0),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (memory_event_id, ai_view)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_relationship_projection_view
|
||||
ON app.relationship_memory_projection(ai_view, memory_event_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit.enforce_relationship_memory_link()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
resolved_case UUID;
|
||||
resolved_type TEXT;
|
||||
BEGIN
|
||||
IF (
|
||||
SELECT count(DISTINCT t.id)
|
||||
FROM app.turns t
|
||||
WHERE t.id = ANY(NEW.evidence_turn_ids)
|
||||
AND t.session_id = NEW.session_id
|
||||
) <> cardinality(NEW.evidence_turn_ids) THEN
|
||||
RAISE EXCEPTION 'relationship memory evidence turns must belong to its session'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF NEW.resolves_event_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
SELECT case_id, event_type INTO resolved_case, resolved_type
|
||||
FROM app.relationship_memory_event
|
||||
WHERE memory_event_id = NEW.resolves_event_id;
|
||||
|
||||
IF resolved_case IS NULL OR resolved_case IS DISTINCT FROM NEW.case_id THEN
|
||||
RAISE EXCEPTION 'relationship repair must reference an event in the same case'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF resolved_type NOT IN (
|
||||
'rupture_withdrawal','rupture_confrontation','unresolved_rupture','repair_attempt'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'relationship repair must resolve a rupture or repair attempt'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit.enforce_relationship_projection_view()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM app.relationship_memory_event e
|
||||
WHERE e.memory_event_id = NEW.memory_event_id
|
||||
AND NEW.ai_view = ANY(e.visible_to)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'relationship projection view must be declared by its event'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_outcome_trajectory_revision_append_only
|
||||
ON app.outcome_trajectory_revision;
|
||||
CREATE TRIGGER trg_outcome_trajectory_revision_append_only
|
||||
BEFORE UPDATE OR DELETE ON app.outcome_trajectory_revision
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_outcome_trajectory_observation_append_only
|
||||
ON app.outcome_trajectory_observation;
|
||||
CREATE TRIGGER trg_outcome_trajectory_observation_append_only
|
||||
BEFORE UPDATE OR DELETE ON app.outcome_trajectory_observation
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_synthetic_outcome_arc_append_only
|
||||
ON ds.synthetic_outcome_arc;
|
||||
CREATE TRIGGER trg_synthetic_outcome_arc_append_only
|
||||
BEFORE UPDATE OR DELETE ON ds.synthetic_outcome_arc
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_relationship_memory_link
|
||||
ON app.relationship_memory_event;
|
||||
CREATE TRIGGER trg_relationship_memory_link
|
||||
BEFORE INSERT ON app.relationship_memory_event
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.enforce_relationship_memory_link();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_relationship_memory_event_append_only
|
||||
ON app.relationship_memory_event;
|
||||
CREATE TRIGGER trg_relationship_memory_event_append_only
|
||||
BEFORE UPDATE OR DELETE ON app.relationship_memory_event
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_relationship_projection_view
|
||||
ON app.relationship_memory_projection;
|
||||
CREATE TRIGGER trg_relationship_projection_view
|
||||
BEFORE INSERT ON app.relationship_memory_projection
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.enforce_relationship_projection_view();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_relationship_memory_projection_append_only
|
||||
ON app.relationship_memory_projection;
|
||||
CREATE TRIGGER trg_relationship_memory_projection_append_only
|
||||
BEFORE UPDATE OR DELETE ON app.relationship_memory_projection
|
||||
FOR EACH ROW EXECUTE FUNCTION audit.reject_measurement_mutation();
|
||||
|
||||
ALTER TABLE ds.synthetic_outcome_arc ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE app.outcome_trajectory_revision ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE app.outcome_trajectory_observation ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE app.relationship_memory_event ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE app.relationship_memory_projection ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS p_synthetic_outcome_arc_select ON ds.synthetic_outcome_arc;
|
||||
DROP POLICY IF EXISTS p_synthetic_outcome_arc_insert ON ds.synthetic_outcome_arc;
|
||||
CREATE POLICY p_synthetic_outcome_arc_select ON ds.synthetic_outcome_arc
|
||||
FOR SELECT USING (true);
|
||||
CREATE POLICY p_synthetic_outcome_arc_insert ON ds.synthetic_outcome_arc
|
||||
FOR INSERT WITH CHECK (
|
||||
app.is_ai_context() OR app.current_role_name() IN ('admin','instructor')
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS p_outcome_trajectory_revision_select
|
||||
ON app.outcome_trajectory_revision;
|
||||
DROP POLICY IF EXISTS p_outcome_trajectory_revision_insert
|
||||
ON app.outcome_trajectory_revision;
|
||||
CREATE POLICY p_outcome_trajectory_revision_select ON app.outcome_trajectory_revision
|
||||
FOR SELECT USING (
|
||||
app.is_ai_context()
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM app.sessions s
|
||||
WHERE s.id = app.outcome_trajectory_revision.anchor_session_id
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_outcome_trajectory_revision_insert ON app.outcome_trajectory_revision
|
||||
FOR INSERT WITH CHECK (
|
||||
app.is_ai_context()
|
||||
OR (
|
||||
EXISTS (
|
||||
SELECT 1 FROM app.sessions s
|
||||
WHERE s.id = app.outcome_trajectory_revision.anchor_session_id
|
||||
AND s.case_id = app.outcome_trajectory_revision.case_id
|
||||
AND s.learner_id = app.outcome_trajectory_revision.learner_id
|
||||
)
|
||||
AND (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
OR (
|
||||
app.current_role_name() = 'learner'
|
||||
AND learner_id = app.current_uid()
|
||||
AND computed_by = app.current_uid()
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS p_outcome_trajectory_observation_select
|
||||
ON app.outcome_trajectory_observation;
|
||||
DROP POLICY IF EXISTS p_outcome_trajectory_observation_insert
|
||||
ON app.outcome_trajectory_observation;
|
||||
CREATE POLICY p_outcome_trajectory_observation_select
|
||||
ON app.outcome_trajectory_observation
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM app.outcome_trajectory_revision r
|
||||
WHERE r.revision_id = app.outcome_trajectory_observation.revision_id
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_outcome_trajectory_observation_insert
|
||||
ON app.outcome_trajectory_observation
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM app.outcome_trajectory_revision r
|
||||
WHERE r.revision_id = app.outcome_trajectory_observation.revision_id
|
||||
AND r.case_id = (
|
||||
SELECT s.case_id FROM app.sessions s
|
||||
WHERE s.id = app.outcome_trajectory_observation.session_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS p_relationship_memory_event_select
|
||||
ON app.relationship_memory_event;
|
||||
DROP POLICY IF EXISTS p_relationship_memory_event_insert
|
||||
ON app.relationship_memory_event;
|
||||
CREATE POLICY p_relationship_memory_event_select ON app.relationship_memory_event
|
||||
FOR SELECT USING (
|
||||
(
|
||||
app.is_ai_context()
|
||||
AND current_setting('app.current_ai_view', true) = ANY(visible_to)
|
||||
)
|
||||
OR (
|
||||
NOT app.is_ai_context()
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM app.sessions s
|
||||
WHERE s.id = app.relationship_memory_event.session_id
|
||||
)
|
||||
AND (
|
||||
(app.current_role_name() = 'learner' AND 'counselor' = ANY(visible_to))
|
||||
OR (app.current_role_name() = 'instructor' AND visible_to && ARRAY['evaluator','supervisor']::TEXT[])
|
||||
OR app.current_role_name() = 'admin'
|
||||
)
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_relationship_memory_event_insert ON app.relationship_memory_event
|
||||
FOR INSERT WITH CHECK (
|
||||
app.is_ai_context()
|
||||
OR (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM app.sessions s
|
||||
WHERE s.id = app.relationship_memory_event.session_id
|
||||
AND s.case_id = app.relationship_memory_event.case_id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS p_relationship_memory_projection_select
|
||||
ON app.relationship_memory_projection;
|
||||
DROP POLICY IF EXISTS p_relationship_memory_projection_insert
|
||||
ON app.relationship_memory_projection;
|
||||
CREATE POLICY p_relationship_memory_projection_select
|
||||
ON app.relationship_memory_projection
|
||||
FOR SELECT USING (
|
||||
(
|
||||
app.is_ai_context()
|
||||
AND ai_view = current_setting('app.current_ai_view', true)
|
||||
)
|
||||
OR (
|
||||
NOT app.is_ai_context()
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM app.relationship_memory_event e
|
||||
WHERE e.memory_event_id = app.relationship_memory_projection.memory_event_id
|
||||
)
|
||||
AND (
|
||||
(app.current_role_name() = 'learner' AND ai_view = 'counselor')
|
||||
OR (app.current_role_name() = 'instructor' AND ai_view IN ('evaluator','supervisor'))
|
||||
OR app.current_role_name() = 'admin'
|
||||
)
|
||||
)
|
||||
);
|
||||
CREATE POLICY p_relationship_memory_projection_insert
|
||||
ON app.relationship_memory_projection
|
||||
FOR INSERT WITH CHECK (
|
||||
app.is_ai_context()
|
||||
OR (
|
||||
app.current_role_name() IN ('admin','instructor')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM app.relationship_memory_event e
|
||||
WHERE e.memory_event_id = app.relationship_memory_projection.memory_event_id
|
||||
)
|
||||
)
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue