state-hub/api/models/task.py
tegwick a495a27e0e
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Build and Publish Multi-Context Image / build-and-push (push) Waiting to run
CUST-WP-0074-T02 follow-up: blocked_on / wait_until reach the hub read model
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
2026-09-29 21:38:27 +02:00

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"
)