diff --git a/api/services/forge_projection.py b/api/services/forge_projection.py index ef4735b..b503c78 100644 --- a/api/services/forge_projection.py +++ b/api/services/forge_projection.py @@ -680,12 +680,34 @@ async def reset_repository_projection( ).scalars() ) + # Release identifiers held by rows retired before retirement freed them. + # This runs here, ahead of every refusal path, because it completes a + # decision already taken rather than making a new one — gating it behind an + # unrelated pending decision left four identifiers locked in repositories + # that refuse for reasons having nothing to do with them. + for r in rows: + if r.projection_retired_at is not None and RETIRED_SLUG_MARK not in (r.slug or ""): + before = r.slug + r.slug = _tombstone_slug(r.slug or str(r.id), r.projection_retired_at) + outcome.released.append(before) + want = {w.record_id.strip().lower(): w for w in derived.workplans} want_paths = {_path_key(w.relative_path): k for k, w in want.items()} + # The derived UUID is the strongest key there is: if a row's id equals + # uuid5 of a wanted record's identifier, that row *is* that record, whatever + # its slug says. Checking it first is what stops a correct record with a + # legacy title slug — `testdrive-jsui-publication` for MARKITECT-WP-0002 — + # from being read as a re-key and proposed for retirement. + want_by_uuid = {w.uuid: k for k, w in want.items()} + matched: dict[str, Any] = {} for row in rows: key = (row.slug or "").strip().lower() + by_uuid = want_by_uuid.get(str(row.id)) + if by_uuid is not None: + matched[by_uuid] = row + continue if key not in want: if _identity_is_derived(row) or _slug_is_identifier(row.slug or ""): # ADR-007: a record identified by an identifier *is* that @@ -889,16 +911,6 @@ async def reset_repository_projection( if changed: outcome.updated.append(w.record_id) - # Rows retired before retirement released identifiers still hold theirs, and - # `stale` excludes anything already retired — so nothing would ever revisit - # them. Completing a decision already taken is not a new retirement, so this - # runs without acknowledgement and is reported on its own. - for r in rows: - if r.projection_retired_at is not None and RETIRED_SLUG_MARK not in (r.slug or ""): - before = r.slug - r.slug = _tombstone_slug(r.slug or str(r.id), r.projection_retired_at) - outcome.released.append(before) - for r in stale: outcome.retired.append(r.slug or str(r.id)) r.projection_retired_at = now @@ -980,7 +992,11 @@ async def reset_fleet_projection( acknowledge_retirements=acknowledge_retirements, forge_base=forge_base, ) - if result.status == "applied": + # A released identifier is a repair that stands on its own, so + # it must survive a refusal in the same repository — otherwise + # moving the release ahead of the refusal returns achieves + # nothing and the rollback quietly undoes it. + if result.status == "applied" or result.released: await session.commit() else: await session.rollback() diff --git a/tests/test_forge_projection.py b/tests/test_forge_projection.py index b9fced9..32eca8d 100644 --- a/tests/test_forge_projection.py +++ b/tests/test_forge_projection.py @@ -809,3 +809,39 @@ class TestRetirementReleasesTheIdentifier: "repo-wp-0001@retired-20260828", datetime(2026, 8, 28, tzinfo=timezone.utc) ) assert fp.RETIRED_SLUG_MARK in row.slug + + def test_a_release_is_committed_even_when_the_repo_refuses(self): + """The fleet driver commits only on `applied` and rolls back otherwise. + + Moving the release ahead of the refusal returns achieves nothing if the + rollback then discards it — which is the state railiance-cluster and + inter-hub were in. + """ + import inspect + src = inspect.getsource(fp.reset_fleet_projection) + assert 'result.status == "applied" or result.released' in src + + +class TestUuidMatchWins: + """A row whose UUID is uuid5 of the identifier IS that record. + + `testdrive-jsui-publication` and `three-phoenix-ha-cluster` carry legacy + title slugs while their UUIDs are derived from MARKITECT-WP-0002 and + RCLUSTER-WP-0007. The slug-identifier rule alone reads them as re-keys and + proposes retiring records that were correct all along. + """ + + def test_a_title_slugged_row_is_still_its_derived_record(self): + import uuid as _u + rid = "MARKITECT-WP-0002" + row_id = _u.UUID(fp.derived_record_uuid(rid)) + # The slug says one thing, the UUID says another; the UUID is identity. + assert not fp._slug_is_identifier("testdrive-jsui-publication") + assert str(row_id) == fp.derived_record_uuid(rid) + + def test_uuid_match_is_checked_before_the_rekey_rules(self): + import inspect + src = inspect.getsource(fp.reset_repository_projection) + uuid_at = src.index("want_by_uuid.get(str(row.id))") + rekey_at = src.index("_identity_is_derived(row) or _slug_is_identifier") + assert uuid_at < rekey_at, "UUID match must precede the re-key heuristics"