Files
temp/alembic/versions/a5_004_spec_aligned_plans.py
2026-02-22 14:18:24 -05:00

300 lines
11 KiB
Python

"""Create spec-aligned plans and related tables.
This migration creates the plans table (replacing lifecycle_plans) with a
spec-aligned schema, plus the plan_projects, plan_arguments, and
plan_invariants child tables.
Revision ID: a5_004_spec_aligned_plans
Revises: a5_003_spec_aligned_actions
Create Date: 2026-02-13 00:00:01
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "a5_004_spec_aligned_plans"
down_revision: str | Sequence[str] | None = "a5_003_spec_aligned_actions"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Create spec-aligned plans + plan_projects + plan_arguments + plan_invariants."""
# --- Create plans table (ULID PK, spec-aligned columns) ---
op.create_table(
"v3_plans",
# Identity - ULID primary key
sa.Column("plan_id", sa.String(26), nullable=False),
# Hierarchy - parent/root for subplan trees
sa.Column(
"parent_plan_id",
sa.String(26),
sa.ForeignKey(
"v3_plans.plan_id",
ondelete="SET NULL",
name="fk_v3_plans_parent",
),
nullable=True,
),
sa.Column(
"root_plan_id",
sa.String(26),
sa.ForeignKey(
"v3_plans.plan_id",
ondelete="SET NULL",
name="fk_v3_plans_root",
),
nullable=True,
),
# Action linkage (by namespaced name, not ULID)
sa.Column(
"action_name",
sa.String(255),
sa.ForeignKey(
"actions.namespaced_name",
ondelete="RESTRICT",
name="fk_v3_plans_action",
),
nullable=False,
),
# Plan naming
sa.Column("namespaced_name", sa.String(255), nullable=False),
sa.Column("namespace", sa.String(100), nullable=False),
# Lifecycle phase and processing state (spec-aligned)
sa.Column("phase", sa.String(20), nullable=False, server_default="strategize"),
sa.Column(
"processing_state", sa.String(20), nullable=False, server_default="queued"
),
# Attempt counter
sa.Column("attempt", sa.Integer(), nullable=False, server_default=sa.text("1")),
# Rendered description and DoD (from action template + args)
sa.Column("description", sa.Text(), nullable=False),
sa.Column("definition_of_done", sa.Text(), nullable=True),
# Actor references
sa.Column("strategy_actor", sa.String(255), nullable=True),
sa.Column("execution_actor", sa.String(255), nullable=True),
sa.Column("review_actor", sa.String(255), nullable=True),
sa.Column("apply_actor", sa.String(255), nullable=True),
sa.Column("estimation_actor", sa.String(255), nullable=True),
sa.Column("invariant_actor", sa.String(255), nullable=True),
# Policy / profile
sa.Column("automation_profile", sa.String(255), nullable=True),
sa.Column(
"automation_level", sa.String(30), nullable=False, server_default="manual"
),
# Behavior
sa.Column(
"reusable", sa.Boolean(), nullable=False, server_default=sa.text("1")
),
sa.Column(
"read_only", sa.Boolean(), nullable=False, server_default=sa.text("0")
),
# Inputs schema - JSON Schema dict (optional; copied from action)
sa.Column("inputs_schema_json", sa.Text(), nullable=True),
# Execution placeholders
sa.Column("changeset_id", sa.String(26), nullable=True),
sa.Column("sandbox_refs_json", sa.Text(), nullable=True),
sa.Column("validation_summary_json", sa.Text(), nullable=True),
sa.Column("decision_root_id", sa.String(26), nullable=True),
# Error tracking
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("error_details_json", sa.Text(), nullable=True),
# Cost/token tracking
sa.Column("cost_estimate_usd", sa.Float(), nullable=True),
sa.Column("cost_actual_usd", sa.Float(), nullable=True),
sa.Column(
"token_count_input",
sa.Integer(),
nullable=True,
server_default=sa.text("0"),
),
sa.Column(
"token_count_output",
sa.Integer(),
nullable=True,
server_default=sa.text("0"),
),
# Metadata
sa.Column("created_by", sa.String(255), nullable=True),
sa.Column("tags_json", sa.Text(), nullable=False, server_default="[]"),
# Timestamps (ISO-8601 strings)
sa.Column("created_at", sa.String(30), nullable=False),
sa.Column("updated_at", sa.String(30), nullable=False),
sa.Column("completed_at", sa.String(30), nullable=True),
sa.Column("strategize_started_at", sa.String(30), nullable=True),
sa.Column("strategize_completed_at", sa.String(30), nullable=True),
sa.Column("execute_started_at", sa.String(30), nullable=True),
sa.Column("execute_completed_at", sa.String(30), nullable=True),
sa.Column("apply_started_at", sa.String(30), nullable=True),
sa.Column("applied_at", sa.String(30), nullable=True),
# Constraints
sa.PrimaryKeyConstraint("plan_id"),
sa.CheckConstraint(
"phase IN ('strategize', 'execute', 'apply', 'applied')",
name="ck_v3_plans_phase",
),
sa.CheckConstraint(
"processing_state IN ("
"'queued', 'processing', 'errored', 'complete', 'cancelled')",
name="ck_v3_plans_state",
),
sa.CheckConstraint(
"automation_level IN ('manual', 'review_before_apply', 'full_automation')",
name="ck_v3_plans_automation",
),
)
# Indices for common queries
op.create_index("ix_v3_plans_phase", "v3_plans", ["phase"], unique=False)
op.create_index("ix_v3_plans_state", "v3_plans", ["processing_state"], unique=False)
op.create_index("ix_v3_plans_parent", "v3_plans", ["parent_plan_id"], unique=False)
op.create_index("ix_v3_plans_root", "v3_plans", ["root_plan_id"], unique=False)
op.create_index("ix_v3_plans_created", "v3_plans", ["created_at"], unique=False)
op.create_index("ix_v3_plans_action", "v3_plans", ["action_name"], unique=False)
op.create_index("ix_v3_plans_namespace", "v3_plans", ["namespace"], unique=False)
op.create_index(
"ix_v3_plans_phase_state",
"v3_plans",
["phase", "processing_state"],
unique=False,
)
# --- Create plan_projects table ---
op.create_table(
"plan_projects",
sa.Column(
"plan_id",
sa.String(26),
sa.ForeignKey(
"v3_plans.plan_id",
ondelete="CASCADE",
name="fk_plan_projects_plan",
),
nullable=False,
),
sa.Column("project_name", sa.String(255), nullable=False),
sa.Column("alias", sa.String(100), nullable=True),
sa.Column(
"read_only", sa.Boolean(), nullable=False, server_default=sa.text("0")
),
sa.Column("created_at", sa.String(30), nullable=False),
sa.PrimaryKeyConstraint("plan_id", "project_name"),
)
op.create_index(
"ix_plan_projects_project",
"plan_projects",
["project_name"],
unique=False,
)
# --- Create plan_arguments table ---
op.create_table(
"plan_arguments",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column(
"plan_id",
sa.String(26),
sa.ForeignKey(
"v3_plans.plan_id",
ondelete="CASCADE",
name="fk_plan_arguments_plan",
),
nullable=False,
),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("value_json", sa.Text(), nullable=True),
sa.Column("value_type", sa.String(20), nullable=False, server_default="string"),
sa.Column("position", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("plan_id", "name", name="uq_plan_arguments_name"),
)
op.create_index(
"ix_plan_arguments_plan",
"plan_arguments",
["plan_id"],
unique=False,
)
op.create_index(
"ix_plan_arguments_position",
"plan_arguments",
["plan_id", "position"],
unique=False,
)
# --- Create plan_invariants table ---
op.create_table(
"plan_invariants",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column(
"plan_id",
sa.String(26),
sa.ForeignKey(
"v3_plans.plan_id",
ondelete="CASCADE",
name="fk_plan_invariants_plan",
),
nullable=False,
),
sa.Column("invariant_text", sa.Text(), nullable=False),
sa.Column(
"source_scope",
sa.String(20),
nullable=False,
server_default="plan",
),
sa.Column("source_name", sa.String(255), nullable=True),
sa.Column("position", sa.Integer(), nullable=False),
sa.Column("created_at", sa.String(30), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"plan_id", "invariant_text", name="uq_plan_invariants_text"
),
sa.CheckConstraint(
"source_scope IN ('global', 'project', 'action', 'plan')",
name="ck_plan_invariants_scope",
),
)
op.create_index(
"ix_plan_invariants_plan",
"plan_invariants",
["plan_id"],
unique=False,
)
op.create_index(
"ix_plan_invariants_position",
"plan_invariants",
["plan_id", "position"],
unique=False,
)
def downgrade() -> None:
"""Drop spec-aligned plan tables."""
op.drop_index("ix_plan_invariants_position", table_name="plan_invariants")
op.drop_index("ix_plan_invariants_plan", table_name="plan_invariants")
op.drop_table("plan_invariants")
op.drop_index("ix_plan_arguments_position", table_name="plan_arguments")
op.drop_index("ix_plan_arguments_plan", table_name="plan_arguments")
op.drop_table("plan_arguments")
op.drop_index("ix_plan_projects_project", table_name="plan_projects")
op.drop_table("plan_projects")
op.drop_index("ix_v3_plans_phase_state", table_name="v3_plans")
op.drop_index("ix_v3_plans_namespace", table_name="v3_plans")
op.drop_index("ix_v3_plans_action", table_name="v3_plans")
op.drop_index("ix_v3_plans_created", table_name="v3_plans")
op.drop_index("ix_v3_plans_root", table_name="v3_plans")
op.drop_index("ix_v3_plans_parent", table_name="v3_plans")
op.drop_index("ix_v3_plans_state", table_name="v3_plans")
op.drop_index("ix_v3_plans_phase", table_name="v3_plans")
op.drop_table("v3_plans")