"""Phase 11: Knowledge Management — Playbooks + Lessons Learned Revision ID: b037know Revises: b036atk Create Date: 2026-05-20 Uses raw SQL to bypass SQLAlchemy DDL hooks (no enum types — string columns with Pydantic-layer validation instead, so no PostgreSQL enums needed). """ from typing import Union from alembic import op import sqlalchemy as sa revision: str = "b037know" down_revision: Union[str, None] = "b036atk" branch_labels = None depends_on = None def upgrade() -> None: conn = op.get_bind() # ── playbooks ────────────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS playbooks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), technique_id UUID NOT NULL REFERENCES techniques(id) ON DELETE CASCADE, playbook_type VARCHAR(32) NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL DEFAULT '', version INTEGER NOT NULL DEFAULT 1, tools JSONB, prerequisites JSONB, created_by UUID REFERENCES users(id) ON DELETE SET NULL, updated_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now(), is_active BOOLEAN DEFAULT TRUE, CONSTRAINT uq_playbook_technique_type UNIQUE (technique_id, playbook_type) ) """)) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_playbooks_technique_id ON playbooks (technique_id)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_playbooks_type ON playbooks (playbook_type)" )) # ── playbook_versions ────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS playbook_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), playbook_id UUID NOT NULL REFERENCES playbooks(id) ON DELETE CASCADE, version INTEGER NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NOT NULL DEFAULT '', tools JSONB, prerequisites JSONB, changed_by UUID REFERENCES users(id) ON DELETE SET NULL, change_note VARCHAR(500), created_at TIMESTAMP DEFAULT now() ) """)) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_pb_versions_playbook_id ON playbook_versions (playbook_id)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_pb_versions_version ON playbook_versions (playbook_id, version)" )) # ── lessons_learned ──────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS lessons_learned ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, what_happened TEXT NOT NULL DEFAULT '', root_cause TEXT NOT NULL DEFAULT '', fix_applied TEXT, severity VARCHAR(16) NOT NULL DEFAULT 'medium', entity_type VARCHAR(32) NOT NULL DEFAULT 'manual', entity_id UUID, technique_ids JSONB, tags JSONB, created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now(), is_active BOOLEAN DEFAULT TRUE ) """)) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_ll_entity ON lessons_learned (entity_type, entity_id)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_ll_severity ON lessons_learned (severity)" )) conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_ll_created_by ON lessons_learned (created_by)" )) def downgrade() -> None: conn = op.get_bind() conn.execute(sa.text("DROP TABLE IF EXISTS lessons_learned")) conn.execute(sa.text("DROP TABLE IF EXISTS playbook_versions")) conn.execute(sa.text("DROP TABLE IF EXISTS playbooks"))