-- ============================================================================= -- Vignette · 17_improvement_workbook_contracts.sql -- 개선관리 워크북의 기존 DB forward 계약. -- 신규 볼륨의 02/03/05 foundation과 중복 적용되어도 안전해야 한다. -- ============================================================================= BEGIN; ALTER TABLE app.app_user ADD COLUMN IF NOT EXISTS learner_feedback_enabled BOOLEAN NOT NULL DEFAULT TRUE; ALTER TABLE app.sessions ADD COLUMN IF NOT EXISTS learner_feedback_enabled BOOLEAN NOT NULL DEFAULT TRUE; CREATE SCHEMA IF NOT EXISTS kb; -- 과거 행도 외부 LLM 경계를 위반하지 않도록 먼저 fail-closed로 정규화한다. UPDATE kb.source SET external_llm_ok = FALSE WHERE license_class IN ('C', 'D') AND external_llm_ok IS TRUE; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'kb' AND t.relname = 'source' AND c.conname = 'ck_kb_source_external_license' ) THEN ALTER TABLE kb.source ADD CONSTRAINT ck_kb_source_external_license CHECK (license_class IN ('A', 'B') OR external_llm_ok = FALSE) NOT VALID; END IF; END $$; ALTER TABLE kb.source VALIDATE CONSTRAINT ck_kb_source_external_license; CREATE TABLE IF NOT EXISTS kb.protocol_registration ( protocol_id UUID PRIMARY KEY, source_id TEXT NOT NULL UNIQUE, title TEXT NOT NULL CHECK (btrim(title) <> ''), source_ref TEXT NOT NULL CHECK (btrim(source_ref) <> ''), version INT NOT NULL CHECK (version > 0), license_class CHAR(1) NOT NULL CHECK (license_class IN ('A', 'B', 'C', 'D')), external_llm_ok BOOLEAN NOT NULL DEFAULT FALSE, content TEXT NOT NULL CHECK (btrim(content) <> ''), content_hash CHAR(64) NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'), status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'retired')), registered_by UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE RESTRICT, registered_at TIMESTAMPTZ NOT NULL DEFAULT now(), activated_at TIMESTAMPTZ, retired_at TIMESTAMPTZ ); -- 테이블이 이전 런타임 DDL로 만들어졌어도 정책 제약을 같은 이름으로 보강한다. UPDATE kb.protocol_registration SET external_llm_ok = FALSE WHERE license_class IN ('C', 'D') AND external_llm_ok IS TRUE; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'kb' AND t.relname = 'protocol_registration' AND c.conname = 'ck_protocol_external_license' ) THEN ALTER TABLE kb.protocol_registration ADD CONSTRAINT ck_protocol_external_license CHECK (license_class IN ('A', 'B') OR external_llm_ok = FALSE) NOT VALID; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'kb' AND t.relname = 'protocol_registration' AND c.conname = 'ck_protocol_lifecycle_timestamps' ) THEN ALTER TABLE kb.protocol_registration ADD CONSTRAINT ck_protocol_lifecycle_timestamps CHECK ( (status = 'draft' AND activated_at IS NULL AND retired_at IS NULL) OR (status = 'active' AND activated_at IS NOT NULL AND retired_at IS NULL) OR (status = 'retired' AND activated_at IS NOT NULL AND retired_at IS NOT NULL) ) NOT VALID; END IF; END $$; ALTER TABLE kb.protocol_registration VALIDATE CONSTRAINT ck_protocol_external_license; ALTER TABLE kb.protocol_registration VALIDATE CONSTRAINT ck_protocol_lifecycle_timestamps; CREATE INDEX IF NOT EXISTS idx_protocol_registration_status ON kb.protocol_registration(status, registered_at DESC); -- 활성화/퇴역은 admin 역할로 kb.chunk를 같은 트랜잭션에서 갱신한다. -- 기존 볼륨에도 신규 볼륨과 동일한 admin-only write RLS 계약을 보강한다. ALTER TABLE kb.chunk ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_kb_chunk_admin_write ON kb.chunk; CREATE POLICY p_kb_chunk_admin_write ON kb.chunk FOR ALL USING (app.current_role_name() = 'admin') WITH CHECK (app.current_role_name() = 'admin'); COMMIT;