From 89524d83e47732b7945a15603399950ce20a0044 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 9 Sep 2026 12:47:43 +0200 Subject: [PATCH] fix: preserve scoped Custodian historical work identities Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc --- config/work-record-types.yaml | 32 ++ ...026-09-09-custodian-legacy-identities.json | 366 ++++++++++++++++++ src/repo_manager/record_identity.py | 68 +++- tests/test_custodian_legacy_identity.py | 130 +++++++ ...odian-historical-identity-compatibility.md | 71 ++++ 5 files changed, 657 insertions(+), 10 deletions(-) create mode 100644 docs/evidence/2026-09-09-custodian-legacy-identities.json create mode 100644 tests/test_custodian_legacy_identity.py create mode 100644 workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md diff --git a/config/work-record-types.yaml b/config/work-record-types.yaml index 919f124..7fed01d 100644 --- a/config/work-record-types.yaml +++ b/config/work-record-types.yaml @@ -9,6 +9,18 @@ kinds: - "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-[0-9]{4}$" - "^[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}$" legacy_patterns: + - pattern: "^CUST-WP-0000b$" + source: Custodian historical workplan suffix retained by source and hub + grandfathered: true + - pattern: "^CUST-WP-0000c$" + source: Custodian archived historical workplan suffix retained by source and hub + grandfathered: true + - pattern: "^CUST-WP-0010b$" + source: Custodian historical workplan suffix retained by source and hub + grandfathered: true + - pattern: "^CUST-WP-(0017|0053)-LEGACY$" + source: Custodian archived legacy workplan identity retained by source and hub + grandfathered: true - pattern: "^ADHOC-[0-9]{4}-[0-9]{2}-[0-9]{2}$" source: daily ad-hoc containers created before repository-qualified ad-hoc ids grandfathered: true @@ -32,6 +44,26 @@ kinds: - pattern: "^MASON-0001-T[0-9]{2,3}$" source: ops-mason bootstrap tasks created before WP infix convention grandfathered: true + - pattern: "^CUST-WP-(?:0000c-T0[1-4]|0010b-T0[1-2])$" + source: Custodian historical task identity retained by source and hub + grandfathered: true + scoped_legacy_patterns: + - pattern: "^(?:T01|T02|T03|T04|T05)$" + parent_pattern: "^CUST-WP-0006$" + source: Custodian historical task blocks with parent-qualified UUID bindings + grandfathered: true + - pattern: "^(?:T01|T02|T03|T04|T05|T06|T07|T08|T09|T10|T11|T12|T13|T14|T15|T16|T17)$" + parent_pattern: "^CUST-WP-0007$" + source: Custodian historical task blocks with parent-qualified UUID bindings + grandfathered: true + - pattern: "^(?:T01|T02|T03|T04|T05|T06|T07|T08|T09)$" + parent_pattern: "^CUST-WP-0030$" + source: Custodian historical task blocks with parent-qualified UUID bindings + grandfathered: true + - pattern: "^(?:T01|T02|T03|T04|T05|T06|T07)$" + parent_pattern: "^CUST-WP-0031$" + source: Custodian historical task blocks with parent-qualified UUID bindings + grandfathered: true - kind: intake id_patterns: - "^[A-Z]+-IN-[0-9]{4}$" diff --git a/docs/evidence/2026-09-09-custodian-legacy-identities.json b/docs/evidence/2026-09-09-custodian-legacy-identities.json new file mode 100644 index 0000000..6783e7c --- /dev/null +++ b/docs/evidence/2026-09-09-custodian-legacy-identities.json @@ -0,0 +1,366 @@ +{ + "schema": "hfact.legacy-task-preservation.v1", + "production_path": "derive_from_checkout -> _sync_existing_workplan_tasks", + "database": "read-only HTTP snapshot; merge in memory", + "source": "the-custodian", + "verified_count": 54, + "records": [ + { + "uuid": "b771f327-128e-4923-a7ad-2080b4e49eb9", + "parent_uuid": "7ce13282-d534-492a-8d42-b3a134028823", + "record_id": "CUST-WP-0006-T01", + "status": "done" + }, + { + "uuid": "2be639ea-a19e-4c80-bcde-c3da06ec5a49", + "parent_uuid": "7ce13282-d534-492a-8d42-b3a134028823", + "record_id": "CUST-WP-0006-T02", + "status": "done" + }, + { + "uuid": "da639706-3b14-42b8-92de-b9de84dbb2be", + "parent_uuid": "7ce13282-d534-492a-8d42-b3a134028823", + "record_id": "CUST-WP-0006-T03", + "status": "done" + }, + { + "uuid": "96dd379a-02b4-4629-8db7-3bef15b9639d", + "parent_uuid": "7ce13282-d534-492a-8d42-b3a134028823", + "record_id": "CUST-WP-0006-T04", + "status": "done" + }, + { + "uuid": "032649fb-2d21-44b7-9735-346405168d8e", + "parent_uuid": "7ce13282-d534-492a-8d42-b3a134028823", + "record_id": "CUST-WP-0006-T05", + "status": "done" + }, + { + "uuid": "1c21c419-30f8-4208-9a55-c2fd83d5005a", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T01", + "status": "done" + }, + { + "uuid": "fe4f5673-6053-404a-8930-4bc0c7d29fd9", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T02", + "status": "done" + }, + { + "uuid": "c1ccf2ae-6241-4281-a443-12953796c1ee", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T03", + "status": "done" + }, + { + "uuid": "4997aa59-39c0-46d6-8c63-f13fffd8d6ea", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T04", + "status": "done" + }, + { + "uuid": "50fdb7ee-91c3-4a2b-be27-e171c144aec6", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T05", + "status": "done" + }, + { + "uuid": "4b19bb95-7200-4fa4-a240-afe14012bafa", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T06", + "status": "done" + }, + { + "uuid": "b34b6bb0-3968-464f-b340-389c4758821e", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T07", + "status": "done" + }, + { + "uuid": "6f1fcf2c-824b-4e3e-884f-5e48b5dea51d", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T08", + "status": "done" + }, + { + "uuid": "58a23afa-601a-40a5-b658-2603dc006d13", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T09", + "status": "done" + }, + { + "uuid": "b0bf1338-b097-4130-ab18-95b4980cf551", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T10", + "status": "done" + }, + { + "uuid": "2a49ad8e-8d6d-4082-8833-a79d9ace0b34", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T11", + "status": "done" + }, + { + "uuid": "1ab3b919-64f7-432a-b173-7b66b042955f", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T12", + "status": "done" + }, + { + "uuid": "015462de-2095-48ff-8b2e-3f53e41dfe32", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T13", + "status": "done" + }, + { + "uuid": "f0f1a2d0-f0a3-45a4-ad10-b86f32849a84", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T14", + "status": "done" + }, + { + "uuid": "2a90b3f7-4938-4235-8ab6-1f9ad9cb06a7", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T15", + "status": "done" + }, + { + "uuid": "081ef72e-e19c-4938-b6de-c0c17b98d99a", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T16", + "status": "done" + }, + { + "uuid": "5626cd89-ff77-4f45-90e6-2059673e4247", + "parent_uuid": "22e18151-fc83-438c-b732-10e056e64a20", + "record_id": "CUST-WP-0007-T17", + "status": "done" + }, + { + "uuid": "8ee527cf-436b-4bab-bdb8-406314a38d99", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T01", + "status": "done" + }, + { + "uuid": "02c27d25-d744-4da0-9bcb-b40ada54d5a5", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T02", + "status": "done" + }, + { + "uuid": "7721d884-bd70-459f-b36d-450d69aac549", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T03", + "status": "done" + }, + { + "uuid": "dc63746d-74b3-434a-925c-1cead480198f", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T04", + "status": "done" + }, + { + "uuid": "3225cc6c-2574-41e9-b8fd-e5e703a9dd7c", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T05", + "status": "done" + }, + { + "uuid": "c01cead4-ff9c-4533-b3d8-9f7554387771", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T07", + "status": "done" + }, + { + "uuid": "84183245-5016-4d87-ad6a-9cd5f6873245", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T08", + "status": "done" + }, + { + "uuid": "3ef43135-fb65-4cca-b8c3-4c7eeb52107c", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T09", + "status": "done" + }, + { + "uuid": "107cb5bb-ff0c-4c97-af04-2cdeff11f0b2", + "parent_uuid": "9d8e1c33-2067-4593-a5d8-d28dda3b1d21", + "record_id": "CUST-WP-0030-T06", + "status": "done" + }, + { + "uuid": "8d5e3e37-c753-4cdc-9211-83ee39f6b0f2", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T01", + "status": "done" + }, + { + "uuid": "ac47994c-efe9-404f-8065-9adf2e923d4c", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T02", + "status": "done" + }, + { + "uuid": "b380fd2b-28fa-4c47-96d6-4c65a0300c44", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T03", + "status": "done" + }, + { + "uuid": "00d44110-fcb4-45cc-8bc8-454af2629d2f", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T04", + "status": "done" + }, + { + "uuid": "7f0748c7-bdee-4801-b870-d4940a5a2e63", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T05", + "status": "done" + }, + { + "uuid": "c6e1be52-961c-4e34-96b1-e450d64298df", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T06", + "status": "done" + }, + { + "uuid": "7e07e32c-683d-47a1-951c-beace9f245a6", + "parent_uuid": "7b480543-b18a-4d79-a30b-7b34bce27ee5", + "record_id": "CUST-WP-0031-T07", + "status": "done" + }, + { + "uuid": "f0d2ad65-c8ed-4f36-bb48-ca4b6b033439", + "parent_uuid": "b802bcf5-da82-4778-a2d8-fec2b02d877e", + "record_id": "CUST-WP-0000c-T01", + "status": "done" + }, + { + "uuid": "77276b8b-fff3-474b-ab27-bd99f1f1a54c", + "parent_uuid": "b802bcf5-da82-4778-a2d8-fec2b02d877e", + "record_id": "CUST-WP-0000c-T02", + "status": "done" + }, + { + "uuid": "cbca1200-26c2-43e7-a92f-97fc2e53a27b", + "parent_uuid": "b802bcf5-da82-4778-a2d8-fec2b02d877e", + "record_id": "CUST-WP-0000c-T03", + "status": "cancel" + }, + { + "uuid": "d53e1c7f-acea-458b-b6c2-0ce384fa946e", + "parent_uuid": "b802bcf5-da82-4778-a2d8-fec2b02d877e", + "record_id": "CUST-WP-0000c-T04", + "status": "cancel" + }, + { + "uuid": "051fd9e1-d15c-4665-adc9-704c4d2b24ea", + "parent_uuid": "def8455a-9de4-4891-a15a-5b09ca98ab1e", + "record_id": "CUST-WP-0010b-T01", + "status": "done" + }, + { + "uuid": "103b46d9-6241-4ef1-9063-054478900b5c", + "parent_uuid": "def8455a-9de4-4891-a15a-5b09ca98ab1e", + "record_id": "CUST-WP-0010b-T02", + "status": "done" + }, + { + "uuid": "a90728f0-4882-4bd5-8b27-531a4f33ba12", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T01", + "status": "done" + }, + { + "uuid": "9b7c4ff3-befb-4d38-aa2d-4af7b74979b6", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T02", + "status": "done" + }, + { + "uuid": "bf491d00-5f9f-495e-b94d-2480f37e2fc1", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T03", + "status": "cancel" + }, + { + "uuid": "cd2eefb5-3238-42c8-aa9c-0819141dde35", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T04", + "status": "cancel" + }, + { + "uuid": "2f6cfff1-a5bf-4d59-82e1-c7a907ea9a42", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T05", + "status": "cancel" + }, + { + "uuid": "5c4769cc-e8f6-46d9-ac65-e11aafb2fb21", + "parent_uuid": "d3e1e974-377b-4b2d-a373-af696035a516", + "record_id": "CUST-WP-0017-LEGACY-T06", + "status": "cancel" + }, + { + "uuid": "b5ae1c35-3bfa-4410-ba7a-79a61b1e9a2d", + "parent_uuid": "a4422777-8d93-4aab-8380-23773bd1525f", + "record_id": "CUST-WP-0053-LEGACY-T01", + "status": "cancel" + }, + { + "uuid": "dca40d4e-338d-4abe-9e95-9f5514fc314e", + "parent_uuid": "a4422777-8d93-4aab-8380-23773bd1525f", + "record_id": "CUST-WP-0053-LEGACY-T02", + "status": "cancel" + }, + { + "uuid": "1e0997db-01bb-4293-811b-8c8087a3a7fd", + "parent_uuid": "a4422777-8d93-4aab-8380-23773bd1525f", + "record_id": "CUST-WP-0053-LEGACY-T03", + "status": "cancel" + }, + { + "uuid": "deff9b36-cbd0-4630-a34a-095098f50293", + "parent_uuid": "a4422777-8d93-4aab-8380-23773bd1525f", + "record_id": "CUST-WP-0053-LEGACY-T04", + "status": "cancel" + } + ], + "created": 0, + "cancelled": 0, + "identity_or_status_changes": 0, + "repo_manager_tests_passed": 169, + "source_workplans": 9, + "parent_local_aliases": 38, + "remaining_missing_bindings": [ + { + "kind": "task", + "record_id": "CUST-WP-ADHOC-2026-05-02-T01", + "uuid": "be709e52-5e23-5339-8aa3-a2ea5412c9a9", + "path": "workplans/ADHOC-2026-05-02.md" + }, + { + "kind": "task", + "record_id": "CUST-WP-ADHOC-2026-07-02-T01", + "uuid": "1d2714e2-6d21-50c3-88ce-8cbb8c9f904e", + "path": "workplans/ADHOC-2026-07-02.md" + }, + { + "kind": "task", + "record_id": "CUST-WP-ADHOC-2026-08-25-T01", + "uuid": "9a7e4024-23ea-59a9-8f1a-68284db059a1", + "path": "workplans/ADHOC-2026-08-25.md" + } + ], + "missing_bindings_match_existing_hub": true, + "residuals": [ + "CUST-IN-0017", + "HFACT-WP-0001-T02" + ], + "delegated_model": "gpt-5.6-luna", + "reviewed_by": "codex/root" +} diff --git a/src/repo_manager/record_identity.py b/src/repo_manager/record_identity.py index c6dc9d9..82c54a7 100644 --- a/src/repo_manager/record_identity.py +++ b/src/repo_manager/record_identity.py @@ -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: diff --git a/tests/test_custodian_legacy_identity.py b/tests/test_custodian_legacy_identity.py new file mode 100644 index 0000000..dff7a91 --- /dev/null +++ b/tests/test_custodian_legacy_identity.py @@ -0,0 +1,130 @@ +from pathlib import Path + +from repo_manager.identifiers import ensure_missing_work_record_identifiers +from repo_manager.record_identity import scan_record_identities + + +def test_custodian_scoped_legacy_ids_are_grandfathered_and_parent_scoped(tmp_path: Path) -> None: + root = tmp_path + for number, workplan_id in enumerate( + ("CUST-WP-0000c", "CUST-WP-0010b", "CUST-WP-0017-LEGACY", "CUST-WP-0053-LEGACY"), start=1 + ): + _write_workplan( + root / "workplans" / f"{number}.md", + workplan_id, + f"{workplan_id}-T01", + f"{number:08d}-1111-4111-8111-111111111111", + ) + report = scan_record_identities(root) + assert report["invalid_identifiers"] == [] + assert report["identity_collisions"] == [] + assert report["duplicate_source_occurrences"] == [] + + +def test_full_legacy_bindings_dry_run_has_no_assignments(tmp_path: Path) -> None: + for number, workplan_id in enumerate( + ("CUST-WP-0000c", "CUST-WP-0010b", "CUST-WP-0017-LEGACY", "CUST-WP-0053-LEGACY"), start=1 + ): + _write_workplan( + tmp_path / "workplans" / f"{number}.md", + workplan_id, + f"{workplan_id}-T01", + f"{number:08d}-1111-4111-8111-111111111111", + ) + result = ensure_missing_work_record_identifiers(tmp_path, execute=False) + assert result["assignments"] == [] + assert result["files_changed"] == [] + + +def test_unqualified_task_remains_invalid_outside_allowlisted_parents(tmp_path: Path) -> None: + workplan = tmp_path / "workplans" / "CUST-WP-0099.md" + workplan.parent.mkdir() + workplan.write_text( + '---\nid: CUST-WP-0099\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---\n' + '\n```task\nid: T01\nstatus: done\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```\n', + encoding="utf-8", + ) + report = scan_record_identities(tmp_path) + assert report["invalid_identifiers"][0]["id"] == "T01" + + +def _write_workplan(path: Path, workplan_id: str, task_id: str, task_uuid: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + f'---\nid: {workplan_id}\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---\n' + f'\n```task\nid: {task_id}\nstatus: done\nstate_hub_task_id: "{task_uuid}"\n```\n', + encoding="utf-8", + ) + + +def test_canonical_qualified_task_collision_remains_global(tmp_path: Path) -> None: + _write_workplan( + tmp_path / "workplans" / "a.md", + "CUST-WP-0099", + "CUST-WP-0099-T01", + "22222222-2222-4222-8222-222222222222", + ) + _write_workplan( + tmp_path / "workplans" / "b.md", + "CUST-WP-0100", + "CUST-WP-0099-T01", + "33333333-3333-4333-8333-333333333333", + ) + report = scan_record_identities(tmp_path) + assert report["identity_collisions"][0]["id"] == "CUST-WP-0099-T01" + + +def test_same_parent_local_alias_collision_fails_closed(tmp_path: Path) -> None: + path = tmp_path / "workplans" / "CUST-WP-0006.md" + path.parent.mkdir() + path.write_text( + '---\nid: CUST-WP-0006\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---\n' + '\n```task\nid: T01\nstatus: done\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```\n' + '\n```task\nid: T01\nstatus: done\nstate_hub_task_id: "33333333-3333-4333-8333-333333333333"\n```\n', + encoding="utf-8", + ) + report = scan_record_identities(tmp_path) + assert report["identity_collisions"][0]["id"] == "T01" + + +def test_unknown_alias_and_missing_managed_uuid_are_invalid(tmp_path: Path) -> None: + _write_workplan( + tmp_path / "workplans" / "unknown.md", + "CUST-WP-0006", + "T99", + "22222222-2222-4222-8222-222222222222", + ) + _write_workplan(tmp_path / "workplans" / "missing.md", "CUST-WP-0006", "T01", "") + report = scan_record_identities(tmp_path) + assert {item["id"] for item in report["invalid_identifiers"]} == {"T99", "T01"} + + +def test_all_38_local_aliases_preserve_binding_and_source_bytes(tmp_path): + before = {} + for parent, count in ( + ("CUST-WP-0006", 5), + ("CUST-WP-0007", 17), + ("CUST-WP-0030", 9), + ("CUST-WP-0031", 7), + ): + path = tmp_path / "workplans" / (parent + ".md") + path.parent.mkdir(exist_ok=True) + body = f'---\nid: {parent}\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---\n' + for n in range(1, count + 1): + body += f'\n```task\nid: T{n:02d}\nstatus: done\nstate_hub_task_id: "{int(parent[-4:]):08d}-1111-4111-8111-{n:012d}"\n```\n' + path.write_text(body) + before[path] = path.read_bytes() + assert scan_record_identities(tmp_path)["ok"] + result = ensure_missing_work_record_identifiers(tmp_path, execute=True) + assert result["assignments"] == [] and result["files_changed"] == [] + assert all(path.read_bytes() == body for path, body in before.items()) + + +def test_new_suffix_task_is_not_grandfathered(tmp_path): + _write_workplan( + tmp_path / "workplans" / "new.md", + "CUST-WP-0000c", + "CUST-WP-0000c-T99", + "22222222-2222-4222-8222-222222222222", + ) + assert not scan_record_identities(tmp_path)["ok"] diff --git a/workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md b/workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md new file mode 100644 index 0000000..b6c7653 --- /dev/null +++ b/workplans/RMGR-WP-0014-custodian-historical-identity-compatibility.md @@ -0,0 +1,71 @@ +--- +id: RMGR-WP-0014 +type: workplan +title: "Preserve Custodian historical identities during ordinary synchronization" +domain: infotech +repo: repo-manager +status: finished +owner: codex +topic_slug: infotech +created: "2026-09-09" +updated: "2026-09-09" +--- + +# Custodian historical identity compatibility + +Origin: the user's HFACT-WP-0001 critical-path continuation and delegated legacy +ID side quest; historical reconciliation remains CUST-IN-0017 in the-custodian. +Review found valid historical suffix IDs and parent-local TNN aliases rejected by +the new global identity gate. Preserve the existing source labels and UUIDs. + +## Audit the exact source and Hub bindings + +```task +id: RMGR-WP-0014-T01 +status: done +priority: medium +``` + +Read-only audit covers nine historical workplans and 54 existing task UUIDs. +The 38 local aliases are qualified by their original parent in the projection. +The actual State Hub derive/merge path retained all 54 task UUIDs, parents and +statuses in memory against a current HTTP snapshot; no rows were created or +cancelled. Three unrelated missing qualified ad-hoc source bindings already +match the deterministic UUIDs held by the Hub and may be restored by the normal +identifier tool. See docs/evidence/2026-09-09-custodian-legacy-identities.json. + +## Add bounded compatibility without weakening new identities + +```task +id: RMGR-WP-0014-T02 +status: done +priority: medium +``` + +The kind registry enumerates the historical suffix IDs and each observed local +alias range. Parent scoping applies only to those accepted local aliases with +an existing managed UUID. Canonical qualified IDs remain globally checked. +Unknown aliases, same-parent UUID collisions and missing local UUIDs refuse +ordinary registration. The Custodian machine canon carries the same historical +interpretation; no new identifier convention is introduced. + +## Verify safe registration and hand off broader reconciliation + +```task +id: RMGR-WP-0014-T03 +status: done +priority: medium +``` + +Repo Manager suite: 169 passed. Portable tests cover global canonical collisions, +parent-local collisions, unknown parent/alias, missing UUID, immutable source +bindings and all 38 aliases. Full Custodian dry run now passes the identity gate. +Ordinary sync must still prove its exact pushed commit and primary receipt. +No retirement acknowledgement is granted by this compatibility change. + +Residual handoff: CUST-IN-0017 remains open for historical duplicate projection +rows (including CUST-WP-0010b source UUID def8455a-9de4-4891-a15a-5b09ca98ab1e +versus stale duplicate 416ff571-18cf-5cb2-9c8b-836f68a301b3), retired/orphan +history and the CUST/THE instruction-generation conflict. HFACT-WP-0001-T02 +retains task-field projection completeness and semantic idempotency keys. This +workplan closes only the bounded identifier-compatibility defect.