Every work-record type carried a stable identifier except tasks, whose rows held only id, workplan_id, title, status and priority — nothing connecting a row to CUST-WP-0067-T01 in the file it came from. Matching was therefore by title, so a renamed heading looked like one task vanishing and another appearing, and the forge-derived reset had to refuse to touch tasks at all. Adds tasks.record_id (nullable: no migration can invent an identity for an existing row) and a backfill that reads the pairing from the repository files, where a task declares both its canonical id and its projection UUID. 5516 pairs across 121 repositories with zero conflicts; 4456 of 6073 cache task rows identified. Diff and reset now key on record_id where present, falling back to a title-prefixed key so an unidentified row stays visibly unidentified. Unknown stays unknown: a row the files do not claim keeps no identity and the reset keeps refusing to act on it, and an existing identity is never overwritten — a mismatch is recorded as a conflict rather than resolved. Refs STATE-WP-0083-T06 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
70 lines
2.6 KiB
Python
70 lines
2.6 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)
|
|
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)
|
|
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"
|
|
)
|