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

@ -0,0 +1,36 @@
"""task canonical record id (STATE-WP-0083-T06)
Every work-record type carries a stable canonical identifier except tasks. A
task row holds `id, workplan_id, title, status, priority` nothing connecting it
to `CUST-WP-0067-T01` in the file it came from.
The consequence is that a file task and a hub task can only be matched by title,
so a renamed heading looks like one task disappearing and another appearing. That
is why the forge-derived reset deliberately refuses to touch tasks of existing
workplans: destroying and recreating a record because someone edited a heading is
not an acceptable failure mode.
Nullable on purpose. Existing rows have no canonical id and cannot be given one
by this migration only the repository files know the mapping, and backfilling
from them is a separate, reversible step.
Revision ID: e2b3c4d5f6a7
Revises: d1a2b3c4e5f6
"""
from alembic import op
import sqlalchemy as sa
revision = "e2b3c4d5f6a7"
down_revision = "d1a2b3c4e5f6"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("tasks", sa.Column("record_id", sa.String(length=120), nullable=True))
op.create_index("ix_tasks_record_id", "tasks", ["record_id"])
def downgrade() -> None:
op.drop_index("ix_tasks_record_id", table_name="tasks")
op.drop_column("tasks", "record_id")