feat(tasks): give task rows a canonical record identifier
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 26s

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
This commit is contained in:
tegwick 2026-08-26 02:05:51 +02:00
parent 43ffe883c3
commit 8b207a991a
6 changed files with 325 additions and 6 deletions

View file

@ -36,6 +36,12 @@ class Task(Base, TimestampMixin):
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(