From 65b7919ec7ec4edd338372d0b9be795559ede94c Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 6 Oct 2026 09:56:54 +0200 Subject: [PATCH] RMGR-WP-0019-T03: add read-only retirement disposition check Re-ran the registrar suite on current source and pinned the three empty duplicate Activity projections the receiving run refused, so the Activity disposition can be verified before STATE-WP-0095-T04 applies it. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 57383@bnt-lap001 Assistant-Session: ef71febe-2a37-4c34-a979-2508ca973c91 --- src/repo_manager/retirement_disposition.py | 81 +++++++++++++++++++ tests/test_retirement_disposition.py | 38 +++++++++ ...MGR-WP-0019-historical-source-admission.md | 15 +++- 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/repo_manager/retirement_disposition.py create mode 100644 tests/test_retirement_disposition.py diff --git a/src/repo_manager/retirement_disposition.py b/src/repo_manager/retirement_disposition.py new file mode 100644 index 0000000..2ca6ba4 --- /dev/null +++ b/src/repo_manager/retirement_disposition.py @@ -0,0 +1,81 @@ +"""Read-only check of an Activity Core retirement disposition (RMGR-WP-0019-T03). + +The 2026-10-04 Activity receiving run refused exactly three empty duplicate +projections. Before STATE-WP-0095-T04 may apply them, the owner's +``expected_retirement_uuids`` must match what Repo Manager observed: the same +three projections, none bound to tasks, and never the source-bound parent. +This module only compares data; it performs no hub or source writes. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +# Projections the receiving run refused as "would be retired" (evidence file +# docs/evidence/2026-10-04-historical-registrar-verification.json) with the +# hub ids observed read-only on 2026-10-06. +REFUSED_DUPLICATE_PROJECTIONS: dict[str, dict[str, str]] = { + "683724d9-4249-5e0e-88e0-7baff4330d58": { + "slug": "custodian-wp-0003", + "backing_relative_path": "workplans/archived/260707-custodian-WP-0003-event-bridge.md", + }, + "644017c7-c5f2-5a64-81c3-d8ee806f8251": { + "slug": "custodian-wp-0003a", + "backing_relative_path": "workplans/custodian-WP-0003a-event-bridge-model-rules-registry.md", + }, + "a5806df9-5b9a-50da-81c1-7e606076bed9": { + "slug": "custodian-wp-0003b", + "backing_relative_path": "workplans/custodian-WP-0003b-event-bridge-parser-workflow-triggers.md", + }, +} + +# Source-bound parent every custodian-WP-0003* file names; it must survive. +PRESERVED_PARENT_UUIDS = frozenset({"b4eb45a9-69e3-4ab0-b00c-67a53c3117c5"}) + + +def check_disposition( + expected_retirement_uuids: list[str], + task_counts: dict[str, int] | None = None, +) -> list[str]: + """Return problems; an empty list means the disposition is consistent.""" + problems: list[str] = [] + given = [u.lower() for u in expected_retirement_uuids] + if len(given) != len(set(given)): + problems.append("duplicate UUIDs in expected_retirement_uuids") + given_set = set(given) + known = set(REFUSED_DUPLICATE_PROJECTIONS) + for uuid in sorted(given_set & PRESERVED_PARENT_UUIDS): + problems.append(f"{uuid} is the preserved source-bound parent") + for uuid in sorted(given_set - known - PRESERVED_PARENT_UUIDS): + problems.append(f"{uuid} was not refused by the receiving run") + for uuid in sorted(known - given_set): + slug = REFUSED_DUPLICATE_PROJECTIONS[uuid]["slug"] + problems.append(f"{uuid} ({slug}) refused but missing from disposition") + for uuid in sorted(given_set & known): + count = (task_counts or {}).get(uuid) + if count is None: + problems.append(f"{uuid} task count not verified") + elif count != 0: + problems.append(f"{uuid} holds {count} tasks; not an empty projection") + return problems + + +def main(argv: list[str]) -> int: + """Usage: python -m repo_manager.retirement_disposition DISPOSITION.json + + The JSON needs ``expected_retirement_uuids`` and may carry ``task_counts``. + """ + if len(argv) != 2: + print(main.__doc__, file=sys.stderr) + return 2 + data = json.loads(Path(argv[1]).read_text()) + problems = check_disposition( + data.get("expected_retirement_uuids", []), data.get("task_counts") + ) + print(json.dumps({"ok": not problems, "problems": problems}, indent=2)) + return 0 if not problems else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/tests/test_retirement_disposition.py b/tests/test_retirement_disposition.py new file mode 100644 index 0000000..2654c85 --- /dev/null +++ b/tests/test_retirement_disposition.py @@ -0,0 +1,38 @@ +from repo_manager.retirement_disposition import ( + PRESERVED_PARENT_UUIDS, + REFUSED_DUPLICATE_PROJECTIONS, + check_disposition, +) + +EXACT = sorted(REFUSED_DUPLICATE_PROJECTIONS) +EMPTY = {u: 0 for u in EXACT} + + +def test_exact_empty_set_is_consistent(): + assert check_disposition(EXACT, EMPTY) == [] + + +def test_missing_projection_is_reported(): + problems = check_disposition(EXACT[:2], EMPTY) + assert any("missing from disposition" in p for p in problems) + + +def test_preserved_parent_is_refused(): + parent = next(iter(PRESERVED_PARENT_UUIDS)) + problems = check_disposition(EXACT + [parent], EMPTY) + assert any("preserved source-bound parent" in p for p in problems) + + +def test_unknown_uuid_and_nonempty_projection_are_reported(): + counts = dict(EMPTY, **{EXACT[0]: 3}) + problems = check_disposition(EXACT + ["00000000-0000-0000-0000-000000000000"], counts) + assert any("not refused" in p for p in problems) + assert any("holds 3 tasks" in p for p in problems) + + +def test_unverified_task_counts_are_not_assumed_empty(): + assert all("not verified" in p for p in check_disposition(EXACT)) + + +def test_duplicates_are_reported(): + assert "duplicate UUIDs" in check_disposition(EXACT + [EXACT[0].upper()], EMPTY)[0] diff --git a/workplans/RMGR-WP-0019-historical-source-admission.md b/workplans/RMGR-WP-0019-historical-source-admission.md index f241d86..b2deb41 100644 --- a/workplans/RMGR-WP-0019-historical-source-admission.md +++ b/workplans/RMGR-WP-0019-historical-source-admission.md @@ -7,7 +7,7 @@ repo: repo-manager status: active owner: codex created: "2026-10-04" -updated: "2026-10-04" +updated: "2026-10-06" flavor: implementation depends_on: [] state_hub_workstream_id: "efe0ecb2-2f3f-5f49-baee-b90ad1d353ab" @@ -93,3 +93,16 @@ normal path; frozen historical source files stayed unchanged. T03 remains wait for explicit disposition and bounded receiving support under STATE-WP-0095-T04 (decision91b7c633-a66f-5d43-9f4b-6565aefb9356). Evidence: docs/evidence/2026-10-04-historical-registrar-verification.json. + +## Ungated T03 preparation, 2026-10-06 + +Re-ran the registrar suite at 8a47a5c (registrar source unchanged since +c41d36c2): 89 focused registrar/identity/historical cases and the full suite +(226) passed. Read-only hub check: the three refused projections are +683724d9 (custodian-wp-0003), 644017c7 (custodian-wp-0003a) and a5806df9 +(custodian-wp-0003b), each with zero tasks; every custodian-WP-0003* source +file binds parent b4eb45a9, which holds the tasks and must not be retired. +Added repo_manager.retirement_disposition, a read-only check that an Activity +expected_retirement_uuids set equals exactly those three empty projections and +excludes the preserved parent. It authorizes nothing; retirement stays with +Activity receiving under STATE-WP-0095-T04.