fix(backfill): qualify short task ids with their workplan
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:
parent
b1cf1a3d97
commit
7f41d42488
2 changed files with 62 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue