fix(projection): qualify bare task ids before deriving their identity
Retiring llm-connect's five legacy rows failed on `duplicate key value violates
unique constraint "tasks_pkey"`, and the cause is that a bare `T01` is not an
identifier: it is unique within its workplan, not in the fleet. Unqualified,
uuid5("T01") is the same UUID for every workplan in the fleet that has one —
llm-connect's 91 task blocks derive 49 distinct UUIDs, so creating its
workplans inserts the same task primary key repeatedly in one flush.
Tasks are now qualified with their owning workplan before derivation, which is
the rule `task_record_id_backfill.qualify_task_id` already applies to stored
ids; the two must agree or the backfill and the projection disagree about what
a task is called. Already-qualified ids are untouched.
The create path's comment claimed it was "safe only because nothing exists to
mis-match against: this workplan is new to the hub". That was true of other
workplans and false within one: the collision was among the tasks it was
inserting itself.
The failed pass rolled back cleanly — llm-connect's five legacy rows are still
live and progress events are intact.
742 pass.
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:
parent
034eda9287
commit
b43a1e5728
2 changed files with 58 additions and 4 deletions
|
|
@ -26,6 +26,8 @@ import yaml
|
|||
|
||||
# Same derivation as ADR-007 / repo-manager, so a forge-derived projection and a
|
||||
# preliminary overlay compute identical identities for the same record.
|
||||
from api.services.task_record_id_backfill import qualify_task_id
|
||||
|
||||
_WORK_RECORD_NAMESPACE = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
|
||||
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
|
||||
_HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)$", re.MULTILINE)
|
||||
|
|
@ -258,7 +260,7 @@ def _split_frontmatter(text: str) -> tuple[dict, str]:
|
|||
return (meta if isinstance(meta, dict) else {}), body
|
||||
|
||||
|
||||
def _parse_tasks(body: str) -> list[DerivedTask]:
|
||||
def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
|
||||
headings = [
|
||||
(m.start(), m.group(2).strip()) for m in _HEADING_RE.finditer(body)
|
||||
]
|
||||
|
|
@ -279,10 +281,20 @@ def _parse_tasks(body: str) -> list[DerivedTask]:
|
|||
title = prev[-1] if prev else None
|
||||
out.append(
|
||||
DerivedTask(
|
||||
record_id=rid,
|
||||
# A bare `T01` is not an identifier: it is unique only within
|
||||
# its workplan, so `uuid5("T01")` is the same UUID for every
|
||||
# workplan in the fleet. llm-connect's 91 task blocks derive
|
||||
# just 49 distinct UUIDs unqualified, and creating its
|
||||
# workplans fails on a duplicate task primary key.
|
||||
#
|
||||
# Qualifying with the owning workplan is the same rule
|
||||
# `task_record_id_backfill.qualify_task_id` applies to stored
|
||||
# ids; both must agree or the backfill and the projection
|
||||
# disagree about what a task is called.
|
||||
record_id=qualify_task_id(rid, workplan_id) or rid,
|
||||
# Derived, not read from the file: the forge projection must not
|
||||
# inherit an identifier the file happens to carry.
|
||||
uuid=derived_record_uuid(rid),
|
||||
uuid=derived_record_uuid(qualify_task_id(rid, workplan_id) or rid),
|
||||
title=title,
|
||||
status=(str(block["status"]).strip() if block.get("status") else None),
|
||||
priority=(str(block["priority"]).strip() if block.get("priority") else None),
|
||||
|
|
@ -319,7 +331,7 @@ def derive_from_checkout(repo_root: Path, repo_slug: str, commit: str) -> Derive
|
|||
status=(str(meta["status"]).strip() if meta.get("status") else None),
|
||||
relative_path=str(path.relative_to(repo_root).as_posix()),
|
||||
archived=path.parent.name == "archived",
|
||||
tasks=_parse_tasks(body),
|
||||
tasks=_parse_tasks(body, rid),
|
||||
)
|
||||
)
|
||||
proj.workplans.sort(key=lambda w: w.record_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue