CUST-WP-0061-T01: intake work-record entity (stage 3)
Fresh hub entity per the founder-reviewed decision (not a suggestions rename-bridge): kind: intake per canon/standards/work-record-types_v0.1.md, lifecycle open -> vetted -> routed -> closed(promoted|declined|absorbed). - api/models/base.py::new_uuid7 -- dependency-free RFC 9562 UUIDv7 generator (48-bit ms timestamp, version/variant bits, random remainder); existing tables keep new_uuid (UUIDv4) unchanged, this is opt-in for new work-record entities per the identity-layering canon - api/models/intake.py: Intake + IntakeNote ORM models, mirroring Decision's shape (topic/workplan/repo scope, lane, status, outcome, promoted_to back-link); CHECK constraints enforce scope-required, closed-requires-outcome, promoted-requires-promoted_to at the DB level - migrations/a7c3e9f1b4d2: intakes + intake_notes tables, 3 enum types - api/routers/intake.py: list/create/get/patch + /route + /close + /notes actions, mirroring decisions.py's pattern (409 on invalid transitions, progress event on close) - api/schemas/intake.py: Pydantic create/update/route/close/note schemas - mcp_server/server.py: create_intake, list_intakes, route_intake, close_intake tool wrappers - tests/test_intake.py: 12 tests against the real Postgres test DB (create/list/scope-validation, full lifecycle incl. 409s and the promoted-requires-promoted_to constraint, notes, UUIDv7 verification) Verified live against the running dev API + DB (not just pytest): applied the migration, restarted the MCP server, and ran a full create -> route -> close cycle over the real REST endpoints. No regressions: full existing suite (test_routers_core, test_suggestions, test_mcp_smoke, test_mcp_write_tools, test_mcp_registration, test_consistency_check, test_consistency_sweep) all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4541f1d6fc
commit
88ba666c95
9 changed files with 789 additions and 1 deletions
100
migrations/versions/a7c3e9f1b4d2_intake_work_record_entity.py
Normal file
100
migrations/versions/a7c3e9f1b4d2_intake_work_record_entity.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""add intakes + intake_notes tables (CUST-WP-0061-T01, work-record stage 3)
|
||||
|
||||
Revision ID: a7c3e9f1b4d2
|
||||
Revises: f0a1b2c3d4e5
|
||||
Create Date: 2026-07-21
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "a7c3e9f1b4d2"
|
||||
down_revision = "f0a1b2c3d4e5"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
intakelane = postgresql.ENUM(
|
||||
"green", "blue", "yellow", "orange", "red",
|
||||
name="intakelane",
|
||||
create_type=False,
|
||||
)
|
||||
intakestatus = postgresql.ENUM(
|
||||
"open", "vetted", "routed", "closed",
|
||||
name="intakestatus",
|
||||
create_type=False,
|
||||
)
|
||||
intakeoutcome = postgresql.ENUM(
|
||||
"promoted", "declined", "absorbed",
|
||||
name="intakeoutcome",
|
||||
create_type=False,
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
intakelane.create(op.get_bind(), checkfirst=True)
|
||||
intakestatus.create(op.get_bind(), checkfirst=True)
|
||||
intakeoutcome.create(op.get_bind(), checkfirst=True)
|
||||
|
||||
op.create_table(
|
||||
"intakes",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("topic_id", UUID(as_uuid=True), sa.ForeignKey("topics.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("workplan_id", UUID(as_uuid=True), sa.ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("repo_id", UUID(as_uuid=True), sa.ForeignKey("managed_repos.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("title", sa.String(length=500), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("lane", intakelane, nullable=False, server_default="green"),
|
||||
sa.Column("status", intakestatus, nullable=False, server_default="open"),
|
||||
sa.Column("outcome", intakeoutcome, nullable=True),
|
||||
sa.Column("origin", sa.String(length=200), nullable=True),
|
||||
sa.Column("origin_ref", sa.String(length=200), nullable=True),
|
||||
sa.Column("promoted_to", sa.String(length=200), nullable=True),
|
||||
sa.Column("source_repo_path", sa.Text(), nullable=True),
|
||||
sa.Column("routed_note", sa.Text(), nullable=True),
|
||||
sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.CheckConstraint(
|
||||
"topic_id IS NOT NULL OR workplan_id IS NOT NULL OR repo_id IS NOT NULL",
|
||||
name="ck_intakes_topic_or_workplan_or_repo",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(status != 'closed') OR (outcome IS NOT NULL)",
|
||||
name="ck_intakes_closed_requires_outcome",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(outcome != 'promoted') OR (promoted_to IS NOT NULL)",
|
||||
name="ck_intakes_promoted_requires_promoted_to",
|
||||
),
|
||||
)
|
||||
op.create_index("ix_intakes_topic_id", "intakes", ["topic_id"])
|
||||
op.create_index("ix_intakes_workplan_id", "intakes", ["workplan_id"])
|
||||
op.create_index("ix_intakes_repo_id", "intakes", ["repo_id"])
|
||||
op.create_index("ix_intakes_status", "intakes", ["status"])
|
||||
op.create_index("ix_intakes_origin_ref", "intakes", ["origin_ref"])
|
||||
|
||||
op.create_table(
|
||||
"intake_notes",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("intake_id", UUID(as_uuid=True), sa.ForeignKey("intakes.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("author", sa.String(length=100), nullable=True),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
op.create_index("ix_intake_notes_intake_id", "intake_notes", ["intake_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_intake_notes_intake_id", table_name="intake_notes")
|
||||
op.drop_table("intake_notes")
|
||||
op.drop_index("ix_intakes_origin_ref", table_name="intakes")
|
||||
op.drop_index("ix_intakes_status", table_name="intakes")
|
||||
op.drop_index("ix_intakes_repo_id", table_name="intakes")
|
||||
op.drop_index("ix_intakes_workplan_id", table_name="intakes")
|
||||
op.drop_index("ix_intakes_topic_id", table_name="intakes")
|
||||
op.drop_table("intakes")
|
||||
intakeoutcome.drop(op.get_bind(), checkfirst=True)
|
||||
intakestatus.drop(op.get_bind(), checkfirst=True)
|
||||
intakelane.drop(op.get_bind(), checkfirst=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue