From 16b9abf2e278ac5729cfaad2209d0a397e4e1a73 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 29 May 2026 14:42:45 -0400 Subject: [PATCH] feat(l1): add ai_build session kind (model + migration) Teaches l1_walk_sessions a new session_kind='ai_build' for AI-generated decision-tree walks. FK shape matches adhoc: both flow_id and flow_proposal_id must be NULL. Drops and recreates the two affected CHECK constraints (session_kind allowlist + target_consistency). Migration beca7464b6b4 chains from b3358ba0e48c. Co-Authored-By: Claude Opus 4.7 --- .../beca7464b6b4_add_ai_build_session_kind.py | 48 +++++++++++++++++++ backend/app/models/l1_walk_session.py | 5 +- backend/tests/test_l1_ai_build_model.py | 16 +++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 backend/alembic/versions/beca7464b6b4_add_ai_build_session_kind.py create mode 100644 backend/tests/test_l1_ai_build_model.py diff --git a/backend/alembic/versions/beca7464b6b4_add_ai_build_session_kind.py b/backend/alembic/versions/beca7464b6b4_add_ai_build_session_kind.py new file mode 100644 index 00000000..ca247718 --- /dev/null +++ b/backend/alembic/versions/beca7464b6b4_add_ai_build_session_kind.py @@ -0,0 +1,48 @@ +"""add ai_build session kind + +Revision ID: beca7464b6b4 +Revises: b3358ba0e48c +Create Date: 2026-05-29 18:41:38.601537 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'beca7464b6b4' +down_revision: Union[str, None] = 'b3358ba0e48c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.drop_constraint("ck_l1_walk_sessions_session_kind", "l1_walk_sessions", type_="check") + op.create_check_constraint( + "ck_l1_walk_sessions_session_kind", "l1_walk_sessions", + "session_kind IN ('flow', 'proposal', 'adhoc', 'ai_build')", + ) + op.drop_constraint("ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", type_="check") + op.create_check_constraint( + "ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", + "(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) " + "OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) " + "OR (session_kind IN ('adhoc', 'ai_build') AND flow_id IS NULL AND flow_proposal_id IS NULL)", + ) + + +def downgrade() -> None: + op.drop_constraint("ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", type_="check") + op.create_check_constraint( + "ck_l1_walk_sessions_target_consistency", "l1_walk_sessions", + "(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) " + "OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) " + "OR (session_kind = 'adhoc' AND flow_id IS NULL AND flow_proposal_id IS NULL)", + ) + op.drop_constraint("ck_l1_walk_sessions_session_kind", "l1_walk_sessions", type_="check") + op.create_check_constraint( + "ck_l1_walk_sessions_session_kind", "l1_walk_sessions", + "session_kind IN ('flow', 'proposal', 'adhoc')", + ) diff --git a/backend/app/models/l1_walk_session.py b/backend/app/models/l1_walk_session.py index 072fd587..2595571e 100644 --- a/backend/app/models/l1_walk_session.py +++ b/backend/app/models/l1_walk_session.py @@ -30,6 +30,7 @@ class L1WalkSession(Base): - flow: Walking a published flow (flow_id required, flow_proposal_id null). - proposal: Walking a draft flow proposal (flow_proposal_id required, flow_id null). - adhoc: Free-form investigation (both flow_id and flow_proposal_id null). + - ai_build: AI-generated decision-tree walk (both flow_id and flow_proposal_id null). status lifecycle: - active: Session is in progress. @@ -45,7 +46,7 @@ class L1WalkSession(Base): name="ck_l1_walk_sessions_ticket_kind", ), CheckConstraint( - "session_kind IN ('flow', 'proposal', 'adhoc')", + "session_kind IN ('flow', 'proposal', 'adhoc', 'ai_build')", name="ck_l1_walk_sessions_session_kind", ), CheckConstraint( @@ -55,7 +56,7 @@ class L1WalkSession(Base): CheckConstraint( "(session_kind = 'flow' AND flow_id IS NOT NULL AND flow_proposal_id IS NULL) " "OR (session_kind = 'proposal' AND flow_proposal_id IS NOT NULL AND flow_id IS NULL) " - "OR (session_kind = 'adhoc' AND flow_id IS NULL AND flow_proposal_id IS NULL)", + "OR (session_kind IN ('adhoc', 'ai_build') AND flow_id IS NULL AND flow_proposal_id IS NULL)", name="ck_l1_walk_sessions_target_consistency", ), ) diff --git a/backend/tests/test_l1_ai_build_model.py b/backend/tests/test_l1_ai_build_model.py new file mode 100644 index 00000000..a23bf0f1 --- /dev/null +++ b/backend/tests/test_l1_ai_build_model.py @@ -0,0 +1,16 @@ +import uuid + +from app.models.l1_walk_session import L1WalkSession + + +def test_ai_build_session_kind_allowed_by_model_constraint(): + """ai_build is a valid session_kind with both target FKs null (like adhoc).""" + s = L1WalkSession( + account_id=uuid.uuid4(), + created_by_user_id=uuid.uuid4(), + ticket_id="t1", + ticket_kind="internal", + session_kind="ai_build", + ) + assert s.session_kind == "ai_build" + assert s.flow_id is None and s.flow_proposal_id is None