PMFlow/backend/alembic/versions/20260421_add_ai_project_con...

497 lines
30 KiB
Python

"""add ai project control tables
Revision ID: 20260421_add_ai_project_control_tables
Revises: 20260415_add_semantic_facts
Create Date: 2026-04-21
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
try:
from sqlalchemy.dialects.postgresql import JSONB
except Exception: # pragma: no cover
JSONB = sa.JSON
revision = "20260421_add_ai_project_control_tables"
down_revision = "20260415_add_semantic_facts"
branch_labels = None
depends_on = None
def _json_type(bind):
return JSONB() if bind.dialect.name == "postgresql" else sa.JSON()
def _table_exists(bind, table_name: str) -> bool:
return sa.inspect(bind).has_table(table_name)
def _seed_model_catalog(bind) -> None:
table = sa.table(
"model_catalog_items",
sa.column("tenant_id", sa.BigInteger()),
sa.column("provider", sa.String()),
sa.column("model_key", sa.String()),
sa.column("display_name", sa.String()),
sa.column("version", sa.String()),
sa.column("modalities", _json_type(bind)),
sa.column("capability_tags", _json_type(bind)),
sa.column("strengths", _json_type(bind)),
sa.column("limitations", _json_type(bind)),
sa.column("compliance_status", sa.String()),
sa.column("filing_status", sa.String()),
sa.column("input_cost_per_mtok", sa.Numeric(12, 4)),
sa.column("output_cost_per_mtok", sa.Numeric(12, 4)),
sa.column("official_docs_url", sa.Text()),
sa.column("is_active", sa.Boolean()),
)
op.bulk_insert(
table,
[
{
"tenant_id": 1,
"provider": "阿里云",
"model_key": "qwen-plus",
"display_name": "通义千问 Plus",
"version": "latest",
"modalities": ["text", "image"],
"capability_tags": ["general", "rag", "reasoning", "agent", "code"],
"strengths": ["中文通用能力强", "RAG 适配稳定", "工具调用成熟"],
"limitations": ["不适合视频生成"],
"compliance_status": "ready",
"filing_status": "filed",
"input_cost_per_mtok": 0.8,
"output_cost_per_mtok": 2.0,
"official_docs_url": "https://help.aliyun.com/zh/model-studio/",
"is_active": True,
},
{
"tenant_id": 1,
"provider": "火山引擎",
"model_key": "doubao-pro",
"display_name": "豆包 Pro",
"version": "latest",
"modalities": ["text", "image"],
"capability_tags": ["general", "rag", "reasoning", "multimodal"],
"strengths": ["长文本稳", "中文体验好", "成本适中"],
"limitations": ["代码生成弱于专用模型"],
"compliance_status": "ready",
"filing_status": "filed",
"input_cost_per_mtok": 0.8,
"output_cost_per_mtok": 2.0,
"official_docs_url": "https://www.volcengine.com/product/doubao",
"is_active": True,
},
{
"tenant_id": 1,
"provider": "百度智能云",
"model_key": "ernie-4.0-turbo",
"display_name": "文心一言 4.0 Turbo",
"version": "latest",
"modalities": ["text", "image"],
"capability_tags": ["general", "rag", "reasoning", "multimodal"],
"strengths": ["企业落地常见", "多模态均衡"],
"limitations": ["综合成本偏高"],
"compliance_status": "ready",
"filing_status": "filed",
"input_cost_per_mtok": 1.2,
"output_cost_per_mtok": 3.2,
"official_docs_url": "https://cloud.baidu.com/product/wenxinworkshop",
"is_active": True,
},
{
"tenant_id": 1,
"provider": "DeepSeek",
"model_key": "deepseek-chat",
"display_name": "DeepSeek Chat",
"version": "latest",
"modalities": ["text"],
"capability_tags": ["general", "reasoning", "code", "agent"],
"strengths": ["代码与推理性价比高", "适合技术团队"],
"limitations": ["需要留意合规和交付边界"],
"compliance_status": "review_required",
"filing_status": "unknown",
"input_cost_per_mtok": 0.27,
"output_cost_per_mtok": 1.1,
"official_docs_url": "https://platform.deepseek.com/",
"is_active": True,
},
{
"tenant_id": 1,
"provider": "Moonshot AI",
"model_key": "kimi-k2",
"display_name": "Kimi K2",
"version": "latest",
"modalities": ["text"],
"capability_tags": ["general", "long_context", "rag", "reasoning"],
"strengths": ["长上下文强", "资料理解稳定"],
"limitations": ["对多模态场景支持有限"],
"compliance_status": "ready",
"filing_status": "filed",
"input_cost_per_mtok": 1.0,
"output_cost_per_mtok": 3.0,
"official_docs_url": "https://platform.moonshot.cn/",
"is_active": True,
},
],
)
def upgrade() -> None:
bind = op.get_bind()
json_type = _json_type(bind)
if not _table_exists(bind, "model_catalog_items"):
op.create_table(
"model_catalog_items",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("provider", sa.String(length=120), nullable=False),
sa.Column("model_key", sa.String(length=120), nullable=False),
sa.Column("display_name", sa.String(length=255), nullable=False),
sa.Column("version", sa.String(length=120), nullable=False),
sa.Column("modalities", json_type, nullable=False),
sa.Column("capability_tags", json_type, nullable=False),
sa.Column("strengths", json_type, nullable=False),
sa.Column("limitations", json_type, nullable=False),
sa.Column("compliance_status", sa.String(length=50), nullable=False, server_default="review_required"),
sa.Column("filing_status", sa.String(length=50), nullable=False, server_default="unknown"),
sa.Column("input_cost_per_mtok", sa.DECIMAL(12, 4), nullable=True),
sa.Column("output_cost_per_mtok", sa.DECIMAL(12, 4), nullable=True),
sa.Column("official_docs_url", sa.Text(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.UniqueConstraint(
"tenant_id",
"provider",
"model_key",
"version",
name="uq_model_catalog_items_tenant_provider_key_version",
),
)
op.create_index("ix_model_catalog_items_tenant_id", "model_catalog_items", ["tenant_id"], unique=False)
op.create_index("ix_model_catalog_items_provider", "model_catalog_items", ["provider"], unique=False)
op.create_index("ix_model_catalog_items_model_key", "model_catalog_items", ["model_key"], unique=False)
op.create_index("ix_model_catalog_items_is_active", "model_catalog_items", ["is_active"], unique=False)
_seed_model_catalog(bind)
table_specs = {
"project_model_bindings": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("model_catalog_item_id", sa.BigInteger(), sa.ForeignKey("model_catalog_items.id"), nullable=False),
sa.Column("selected_by_user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("selection_reason", json_type, nullable=False),
sa.Column("estimated_daily_tokens", sa.Integer(), nullable=False, server_default="0"),
sa.Column("estimated_monthly_cost_usd", sa.DECIMAL(12, 2), nullable=True),
sa.Column("active_from", sa.TIMESTAMP(), nullable=True),
sa.Column("archived_at", sa.TIMESTAMP(), nullable=True),
],
"model_recommendation_snapshots": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("recommendation_scope", sa.String(length=32), nullable=False, server_default="charter"),
sa.Column("snapshot_input", json_type, nullable=False),
sa.Column("recommendations", json_type, nullable=False),
sa.Column("top_model_catalog_item_id", sa.BigInteger(), sa.ForeignKey("model_catalog_items.id"), nullable=True),
sa.Column("generated_by", sa.String(length=32), nullable=False, server_default="rule_engine"),
],
"model_usage_events": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True),
sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"),
sa.Column("event_key", sa.String(length=255), nullable=True),
sa.Column("input_tokens", sa.Integer(), nullable=False, server_default="0"),
sa.Column("output_tokens", sa.Integer(), nullable=False, server_default="0"),
sa.Column("cost_usd", sa.DECIMAL(12, 4), nullable=False, server_default="0"),
sa.Column("event_payload", json_type, nullable=False),
sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True),
sa.UniqueConstraint("tenant_id", "event_key", name="uq_model_usage_events_tenant_event_key"),
],
"model_eval_runs": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
sa.Column("success_criteria", json_type, nullable=False),
sa.Column("test_cases", json_type, nullable=False),
sa.Column("summary", json_type, nullable=False),
sa.Column("score", sa.DECIMAL(5, 2), nullable=True),
],
"prompt_packages": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=False),
sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=True),
sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True),
sa.Column("package_kind", sa.String(length=32), nullable=False, server_default="development"),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("package_text", sa.Text(), nullable=False),
sa.Column("package_payload", json_type, nullable=False),
sa.Column("generated_from", json_type, nullable=False),
],
"dev_workspace_bindings": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("workspace_key", sa.String(length=255), nullable=False),
sa.Column("repo_root", sa.Text(), nullable=True),
sa.Column("editor_type", sa.String(length=32), nullable=False, server_default="vscode"),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("last_seen_at", sa.TIMESTAMP(), nullable=True),
sa.UniqueConstraint("tenant_id", "workspace_key", name="uq_dev_workspace_bindings_tenant_workspace_key"),
],
"repo_bindings": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("provider", sa.String(length=32), nullable=False, server_default="github"),
sa.Column("external_repo_id", sa.String(length=255), nullable=True),
sa.Column("owner", sa.String(length=255), nullable=False),
sa.Column("repo_name", sa.String(length=255), nullable=False),
sa.Column("default_branch", sa.String(length=255), nullable=False, server_default="main"),
sa.Column("installation_id", sa.String(length=255), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("webhook_secret_hint", sa.String(length=255), nullable=True),
sa.UniqueConstraint("tenant_id", "provider", "owner", "repo_name", name="uq_repo_bindings_tenant_provider_repo"),
],
"task_code_links": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=False),
sa.Column("repo_binding_id", sa.BigInteger(), sa.ForeignKey("repo_bindings.id"), nullable=False),
sa.Column("branch_name", sa.String(length=255), nullable=True),
sa.Column("pr_number", sa.Integer(), nullable=True),
sa.Column("last_commit_sha", sa.String(length=255), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="linked"),
sa.UniqueConstraint(
"tenant_id",
"task_id",
"repo_binding_id",
"branch_name",
name="uq_task_code_links_tenant_task_repo_branch",
),
],
"progress_events": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True),
sa.Column("repo_binding_id", sa.BigInteger(), sa.ForeignKey("repo_bindings.id"), nullable=True),
sa.Column("workspace_binding_id", sa.BigInteger(), sa.ForeignKey("dev_workspace_bindings.id"), nullable=True),
sa.Column("event_type", sa.String(length=32), nullable=False),
sa.Column("source", sa.String(length=32), nullable=False, server_default="manual"),
sa.Column("derived_task_status", sa.String(length=32), nullable=True),
sa.Column("summary", sa.Text(), nullable=True),
sa.Column("payload", json_type, nullable=False),
sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True),
],
"dev_sessions": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("workspace_binding_id", sa.BigInteger(), sa.ForeignKey("dev_workspace_bindings.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("started_at", sa.TIMESTAMP(), nullable=True),
sa.Column("heartbeat_at", sa.TIMESTAMP(), nullable=True),
sa.Column("ended_at", sa.TIMESTAMP(), nullable=True),
sa.Column("session_payload", json_type, nullable=False),
],
"release_validation_runs": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("release_node_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
sa.Column("score", sa.DECIMAL(5, 2), nullable=True),
sa.Column("key_metrics", json_type, nullable=False),
sa.Column("risk_items", json_type, nullable=False),
sa.Column("rollback_plan", json_type, nullable=False),
sa.Column("validation_summary", json_type, nullable=False),
],
"feedback_ingests": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("ingested_by_user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True),
sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"),
sa.Column("title", sa.String(length=255), nullable=True),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("metrics", json_type, nullable=False),
sa.Column("samples", json_type, nullable=False),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
],
"iteration_recommendations": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("feedback_ingest_id", sa.BigInteger(), sa.ForeignKey("feedback_ingests.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="draft"),
sa.Column("priority", sa.String(length=32), nullable=False, server_default="medium"),
sa.Column("recommendation_summary", json_type, nullable=False),
sa.Column("proposed_charter_patch", json_type, nullable=False),
sa.Column("proposed_tasks", json_type, nullable=False),
],
"revenue_events": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"),
sa.Column("currency", sa.String(length=16), nullable=False, server_default="USD"),
sa.Column("amount_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"),
sa.Column("external_ref", sa.String(length=255), nullable=True),
sa.Column("event_payload", json_type, nullable=False),
sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True),
],
"roi_snapshots": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("range_key", sa.String(length=32), nullable=False, server_default="monthly"),
sa.Column("window_start", sa.TIMESTAMP(), nullable=True),
sa.Column("window_end", sa.TIMESTAMP(), nullable=True),
sa.Column("total_revenue_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"),
sa.Column("total_cost_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"),
sa.Column("roi_ratio", sa.DECIMAL(12, 4), nullable=True),
sa.Column("snapshot_payload", json_type, nullable=False),
],
"compliance_check_runs": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("release_node_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True),
sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
sa.Column("score", sa.DECIMAL(5, 2), nullable=True),
sa.Column("checklist", json_type, nullable=False),
sa.Column("issues", json_type, nullable=False),
sa.Column("summary", json_type, nullable=False),
],
"evidence_packs": [
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("tenant_id", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False),
sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False),
sa.Column("compliance_check_run_id", sa.BigInteger(), sa.ForeignKey("compliance_check_runs.id"), nullable=True),
sa.Column("package_kind", sa.String(length=32), nullable=False, server_default="compliance_release"),
sa.Column("status", sa.String(length=32), nullable=False, server_default="draft"),
sa.Column("content", json_type, nullable=False),
sa.Column("export_payload", json_type, nullable=False),
],
}
for table_name, columns in table_specs.items():
if _table_exists(bind, table_name):
continue
op.create_table(table_name, *columns)
op.create_index(f"ix_{table_name}_tenant_id", table_name, ["tenant_id"], unique=False)
extra_indexes = {
"project_model_bindings": ["project_id", "charter_id", "model_catalog_item_id", "status"],
"model_recommendation_snapshots": ["project_id", "charter_id"],
"model_usage_events": ["project_id", "project_model_binding_id", "source_type", "occurred_at"],
"model_eval_runs": ["project_id", "charter_id", "project_model_binding_id", "status"],
"prompt_packages": ["project_id", "charter_id", "task_id", "project_model_binding_id", "status"],
"dev_workspace_bindings": ["project_id", "user_id", "status"],
"repo_bindings": ["project_id", "provider", "status"],
"task_code_links": ["task_id", "repo_binding_id", "pr_number", "status"],
"progress_events": ["task_id", "project_id", "user_id", "repo_binding_id", "workspace_binding_id", "event_type", "source", "occurred_at"],
"dev_sessions": ["project_id", "user_id", "workspace_binding_id", "status"],
"release_validation_runs": ["project_id", "charter_id", "release_node_id", "project_model_binding_id", "status"],
"feedback_ingests": ["project_id", "charter_id", "ingested_by_user_id", "source_type", "status"],
"iteration_recommendations": ["project_id", "charter_id", "feedback_ingest_id", "status", "priority"],
"revenue_events": ["project_id", "source_type", "external_ref", "occurred_at"],
"roi_snapshots": ["project_id", "range_key"],
"compliance_check_runs": ["project_id", "charter_id", "release_node_id", "project_model_binding_id", "status"],
"evidence_packs": ["project_id", "compliance_check_run_id", "package_kind", "status"],
}
for table_name, columns in extra_indexes.items():
for column_name in columns:
op.create_index(f"ix_{table_name}_{column_name}", table_name, [column_name], unique=False)
def downgrade() -> None:
bind = op.get_bind()
tables = [
"evidence_packs",
"compliance_check_runs",
"roi_snapshots",
"revenue_events",
"iteration_recommendations",
"feedback_ingests",
"release_validation_runs",
"dev_sessions",
"progress_events",
"task_code_links",
"repo_bindings",
"dev_workspace_bindings",
"prompt_packages",
"model_eval_runs",
"model_usage_events",
"model_recommendation_snapshots",
"project_model_bindings",
"model_catalog_items",
]
for table_name in tables:
if not _table_exists(bind, table_name):
continue
for index in sa.inspect(bind).get_indexes(table_name):
op.drop_index(index["name"], table_name=table_name)
op.drop_table(table_name)