Migration f9a0b1c2d3e4 (revises e8f9a0b1c2d3) adds tasks.blocked_on (String 200) and tasks.wait_until (Date); TaskCreate/Update/Read carry both. Read model (api/services/wait_kind.py): a wait task is external when it has a dependency edge, a blocked_on matching ^message-from: (rule 6), or a wait_until date (a window clears by itself). human/both/unqualified and blocked_kind are unchanged in shape. fix-consistency: _task_wait_fields carries blocked_on and wait_until (ISO date, PyYAML date objects normalised), so the C-06/C-11 creates and the C-10 status PATCH send them, and C-41 compares and syncs them like the other qualifiers. C-39 accepts wait_until: YYYY-MM-DD as a qualifier; C-40 also fires when wait_until is before UTC today (report only). /state/deps: depends_on stubs gain from_task_id / from_task_title so consumers see task-scoped edges; both optional, response otherwise as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826
79 lines
3.3 KiB
Python
79 lines
3.3 KiB
Python
import enum
|
|
import uuid
|
|
from datetime import date
|
|
|
|
from sqlalchemy import Boolean, Date, Enum, ForeignKey, String, Text
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from api.models.base import Base, TimestampMixin, new_uuid
|
|
|
|
|
|
class TaskStatus(str, enum.Enum):
|
|
wait = "wait"
|
|
todo = "todo"
|
|
progress = "progress"
|
|
done = "done"
|
|
cancel = "cancel"
|
|
|
|
|
|
class TaskPriority(str, enum.Enum):
|
|
low = "low"
|
|
medium = "medium"
|
|
high = "high"
|
|
critical = "critical"
|
|
|
|
|
|
class Task(Base, TimestampMixin):
|
|
__tablename__ = "tasks"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), primary_key=True, default=new_uuid
|
|
)
|
|
workplan_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("workplans.id", ondelete="RESTRICT", onupdate="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
# Canonical work-record identifier, e.g. "CUST-WP-0067-T01" (ADR-007).
|
|
# Nullable because rows created before STATE-WP-0083-T06 have none; only the
|
|
# repository files know the mapping. Without it a task can be matched only by
|
|
# title, so renaming a heading looks like one task vanishing and another
|
|
# appearing.
|
|
record_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True)
|
|
flavor: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
status: Mapped[TaskStatus] = mapped_column(
|
|
Enum(TaskStatus), nullable=False, default=TaskStatus.todo
|
|
)
|
|
priority: Mapped[TaskPriority] = mapped_column(
|
|
Enum(TaskPriority), nullable=False, default=TaskPriority.medium
|
|
)
|
|
assignee: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
|
blocking_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
needs_human: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
|
intervention_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
# Record id of the tracked decision a human-gated wait hangs on, e.g.
|
|
# "CCR-2026-0004" (CUST-WP-0074 Case B). Free text: the decision may live
|
|
# in another repo and may not be indexed in the hub yet.
|
|
decision_id: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
|
# Owner-return marker "message-from:<agent>" (CUST-WP-0074 rule 6) and a
|
|
# date after which the wait clears by itself; either counts as external.
|
|
blocked_on: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
|
wait_until: Mapped[date | None] = mapped_column(Date, nullable=True)
|
|
parent_task_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("tasks.id", ondelete="SET NULL", onupdate="CASCADE"),
|
|
nullable=True,
|
|
)
|
|
|
|
workplan: Mapped["Workplan"] = relationship("Workplan", back_populates="tasks") # noqa: F821
|
|
subtasks: Mapped[list["Task"]] = relationship(
|
|
"Task", foreign_keys=[parent_task_id], lazy="selectin"
|
|
)
|
|
progress_events: Mapped[list["ProgressEvent"]] = relationship( # noqa: F821
|
|
"ProgressEvent", back_populates="task", lazy="selectin"
|
|
)
|