fix(backfill): qualify short task ids with their workplan
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 28s

A task id written as a bare "T01" is unique only inside its own workplan.
Stored as a canonical identifier it makes every workplan's first task share one
identity: 51 such ids were assigned to 148 rows on central before this was
caught, found because identified rows outnumbered distinct identities.

Short ids are now qualified as WORKPLAN-ID-T01. A short id in a file with no
workplan id in frontmatter is left unidentified — an identity that is not unique
is worse than none, which is the same rule the rest of this module already
follows.

The 136 affected rows on central have been cleared so the corrected backfill can
reassign them; the backfill never overwrites an existing identity, so they had to
be nulled rather than re-derived over.

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:17:46 +02:00
parent b1cf1a3d97
commit 7f41d42488
2 changed files with 62 additions and 1 deletions

View file

@ -24,6 +24,23 @@ from typing import Any
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
_ID_RE = re.compile(r"^id:\s*(\S+)", re.MULTILINE)
_UUID_RE = re.compile(r'state_hub_task_id:\s*"?([0-9a-f-]{36})"?')
_FRONTMATTER_ID_RE = re.compile(r"^id:\s*(\S+)", re.MULTILINE)
# A task id written as a bare "T01" is unique only inside its own workplan.
# Storing it as a canonical identifier makes every workplan's first task share
# one identity — 51 such ids were assigned to 148 rows before this was caught.
_SHORT_TASK_ID_RE = re.compile(r"^T\d+$", re.IGNORECASE)
def qualify_task_id(task_id: str, workplan_id: str | None) -> str | None:
"""Return a canonical task id, or None when identity cannot be established."""
task_id = task_id.strip()
if not _SHORT_TASK_ID_RE.fullmatch(task_id):
return task_id
if not workplan_id:
# Unqualifiable: leaving it unidentified is correct, since an identity
# that is not unique is worse than none.
return None
return f"{workplan_id.strip()}-{task_id.upper()}"
@dataclass
@ -63,13 +80,19 @@ def collect_pairs(roots: list[Path]) -> tuple[dict[str, str], BackfillReport]:
except (OSError, UnicodeDecodeError):
continue
report.scanned_files += 1
head = text.split("---", 2)[1] if text.startswith("---") and text.count("---") >= 2 else ""
wp_match = _FRONTMATTER_ID_RE.search(head)
workplan_id = wp_match.group(1).strip() if wp_match else None
for block in _TASK_BLOCK_RE.finditer(text):
body = block.group(1)
rid = _ID_RE.search(body)
uid = _UUID_RE.search(body)
if not rid or not uid:
continue
record_id, task_uuid = rid.group(1).strip(), uid.group(1)
record_id = qualify_task_id(rid.group(1), workplan_id)
if record_id is None:
continue
task_uuid = uid.group(1)
prior = pairs.get(task_uuid)
if prior and prior != record_id:
# One UUID claimed by two canonical ids: a duplicate

View file

@ -121,3 +121,41 @@ async def test_existing_identity_is_never_overwritten(tmp_path):
rep = await bf.backfill_task_record_ids(_Session([row]), [root], dry_run=False)
assert row.record_id == "OTHER-WP-0001-T09"
assert rep.conflicts and rep.updated == 0
def test_a_bare_task_id_is_qualified_by_its_workplan(tmp_path):
"""`T01` is unique inside one workplan and meaningless outside it.
Stored raw, every workplan's first task shares one identity — 51 such ids
were assigned to 148 rows on central before this was caught.
"""
root = _repo(tmp_path, "a", (
"---\nid: LLM-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
pairs, _ = bf.collect_pairs([root])
assert pairs == {"11111111-1111-5111-8111-111111111111": "LLM-WP-0001-T01"}
def test_two_workplans_first_tasks_do_not_collide(tmp_path):
a = _repo(tmp_path, "a", (
"---\nid: A-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
b = _repo(tmp_path, "b", (
"---\nid: B-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "22222222-2222-5222-8222-222222222222"\n```\n'
))
pairs, rep = bf.collect_pairs([a, b])
assert set(pairs.values()) == {"A-WP-0001-T01", "B-WP-0001-T01"}
assert rep.conflicts == []
def test_unqualifiable_short_id_gets_no_identity(tmp_path):
"""No identity beats a non-unique one."""
root = _repo(tmp_path, "a", (
"id: not-frontmatter\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
pairs, _ = bf.collect_pairs([root])
assert pairs == {}