forked from HAL9000/cleveragents-core
300 lines
11 KiB
Python
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")
|