fix: preserve scoped Custodian historical work identities
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
6854ea10b2
commit
89524d83e4
5 changed files with 657 additions and 10 deletions
|
|
@ -24,7 +24,11 @@ def load_kind_registry(path: Path | None = None) -> dict[str, Any]:
|
|||
|
||||
|
||||
def classify_record_id(
|
||||
kind: str, identifier: str, *, registry_path: Path | None = None
|
||||
kind: str,
|
||||
identifier: str,
|
||||
*,
|
||||
parent_id: str | None = None,
|
||||
registry_path: Path | None = None,
|
||||
) -> str | None:
|
||||
"""Return canonical/grandfathered when the canon registry accepts an id."""
|
||||
registry = load_kind_registry(registry_path)
|
||||
|
|
@ -38,35 +42,77 @@ def classify_record_id(
|
|||
str(legacy.get("pattern") or ""), identifier
|
||||
):
|
||||
return "grandfathered"
|
||||
# Historical task blocks may use a local TNN label. Accept these only
|
||||
# under an explicitly enumerated parent workplan; the parent scope is
|
||||
# part of identity and no source id/UUID is rewritten.
|
||||
if parent_id:
|
||||
for legacy in entry.get("scoped_legacy_patterns") or []:
|
||||
if (
|
||||
legacy.get("grandfathered")
|
||||
and re.fullmatch(str(legacy.get("pattern") or ""), identifier)
|
||||
and re.fullmatch(str(legacy.get("parent_pattern") or ""), parent_id)
|
||||
):
|
||||
return "grandfathered"
|
||||
return None
|
||||
|
||||
|
||||
def scan_record_identities(repo_root: Path, *, registry_path: Path | None = None) -> dict[str, Any]:
|
||||
"""Describe invalid ids, benign duplicate sources, and hard collisions."""
|
||||
occurrences: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
||||
occurrences: dict[tuple[str, str, str | None], list[dict[str, Any]]] = defaultdict(list)
|
||||
invalid: list[dict[str, str]] = []
|
||||
|
||||
def add(kind: str, identifier: str | None, record_uuid: str | None, source: str) -> None:
|
||||
def add(
|
||||
kind: str,
|
||||
identifier: str | None,
|
||||
record_uuid: str | None,
|
||||
source: str,
|
||||
*,
|
||||
parent_id: str | None = None,
|
||||
) -> None:
|
||||
if not identifier:
|
||||
return
|
||||
classification = classify_record_id(kind, identifier, registry_path=registry_path)
|
||||
item = {"kind": kind, "id": identifier, "uuid": record_uuid, "source": source}
|
||||
occurrences[(kind, identifier)].append(item)
|
||||
if classification is None:
|
||||
classification = classify_record_id(
|
||||
kind, identifier, parent_id=parent_id, registry_path=registry_path
|
||||
)
|
||||
item = {
|
||||
"kind": kind,
|
||||
"id": identifier,
|
||||
"uuid": record_uuid,
|
||||
"source": source,
|
||||
**({"parent_id": parent_id} if parent_id else {}),
|
||||
}
|
||||
# Only the explicitly scoped local aliases use parent scope. Qualified
|
||||
# canonical IDs remain globally collision checked.
|
||||
local_alias = (
|
||||
kind == "task"
|
||||
and parent_id is not None
|
||||
and re.fullmatch(r"T[0-9]{2}", identifier) is not None
|
||||
and classification == "grandfathered"
|
||||
)
|
||||
occurrences[(kind, identifier, parent_id if local_alias else None)].append(item)
|
||||
if classification is None or (local_alias and not record_uuid):
|
||||
invalid.append({"kind": kind, "id": identifier, "source": source})
|
||||
|
||||
for path in iter_workplan_files(repo_root):
|
||||
workplan = parse_workplan_file(path, repo_root=repo_root)
|
||||
add("workplan", workplan.id, workplan.state_hub_workstream_id, workplan.path)
|
||||
for number, task in enumerate(workplan.tasks, start=1):
|
||||
add("task", task.id, task.state_hub_task_id, f"{workplan.path}#task-block-{number}")
|
||||
add(
|
||||
"task",
|
||||
task.id,
|
||||
task.state_hub_task_id,
|
||||
f"{workplan.path}#task-block-{number}",
|
||||
parent_id=workplan.id,
|
||||
)
|
||||
for path in iter_record_files(repo_root):
|
||||
for number, record in enumerate(parse_record_file(path, repo_root=repo_root), start=1):
|
||||
add(record.kind, record.id, record.uuid, f"{record.source_path}#record-block-{number}")
|
||||
|
||||
duplicates: list[dict[str, Any]] = []
|
||||
collisions: list[dict[str, Any]] = []
|
||||
for (kind, identifier), items in sorted(occurrences.items()):
|
||||
for (kind, identifier, _parent_id), items in sorted(
|
||||
occurrences.items(), key=lambda pair: (pair[0][0], pair[0][1], pair[0][2] or "")
|
||||
):
|
||||
if len(items) < 2:
|
||||
continue
|
||||
uuids = {item["uuid"] for item in items}
|
||||
|
|
@ -74,8 +120,10 @@ def scan_record_identities(repo_root: Path, *, registry_path: Path | None = None
|
|||
"kind": kind,
|
||||
"id": identifier,
|
||||
"uuid": items[0]["uuid"] if len(uuids) == 1 else None,
|
||||
"sources": [item["source"] for item in items],
|
||||
"sources": sorted(item["source"] for item in items),
|
||||
}
|
||||
if items[0].get("parent_id"):
|
||||
result["parent_id"] = items[0]["parent_id"]
|
||||
if len(uuids) == 1 and None not in uuids:
|
||||
duplicates.append(result)
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue