fix(projection): retirement must release the identifier it holds
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 0s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s

`slug` is unique across the whole table, so retiring a row by timestamp alone
leaves its identifier locked to a record nothing derives any more — and the
repository that legitimately owns it can never claim it. core-hub's inherited
REPO-WP-0001/0002 rows retired cleanly and repo-seed stayed refused with
"slug already belongs to another repository", pointing at two retired rows.

Retirement now stamps the slug `<slug>@retired-YYYYMMDD`. The row, its UUID and
its progress events are untouched, so history stays attached to the record it
happened under; only the human-facing identifier is freed. Re-retiring an
already-stamped row replaces the mark rather than stacking it, or the 100-char
column overflows after a few passes.

The UUIDs never collided here: core-hub's rows predate derived identity and
carry v4 UUIDs, while repo-seed derives v5 ones. The identifier was the only
thing in the way.

733 pass.

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:
tegwick 2026-08-28 01:15:02 +02:00
parent 4b121f3796
commit 70c585bb39
3 changed files with 60 additions and 11 deletions

View file

@ -752,3 +752,29 @@ class TestRekeyIsNotRename:
def test_title_slugs_keep_path_matching(self, slug):
"""Hub-first rows never claimed an identifier; the path is their only link."""
assert not fp._slug_is_identifier(slug)
class TestRetirementReleasesTheIdentifier:
"""`slug` is unique table-wide, so retirement must free it.
Retirement that only sets a timestamp leaves the identifier locked to a
record nothing derives, and the repository that owns it can never claim it.
"""
def test_the_identifier_is_released(self):
from datetime import datetime, timezone
when = datetime(2026, 8, 28, tzinfo=timezone.utc)
assert fp._tombstone_slug("repo-wp-0001", when) == "repo-wp-0001@retired-20260828"
def test_re_retiring_does_not_stack_marks(self):
"""Otherwise the 100-char column overflows after a few passes."""
from datetime import datetime, timezone
a = fp._tombstone_slug("repo-wp-0001", datetime(2026, 8, 28, tzinfo=timezone.utc))
b = fp._tombstone_slug(a, datetime(2026, 9, 1, tzinfo=timezone.utc))
assert b == "repo-wp-0001@retired-20260901"
assert b.count(fp.RETIRED_SLUG_MARK) == 1
def test_a_long_slug_stays_within_the_column(self):
from datetime import datetime, timezone
out = fp._tombstone_slug("x" * 140, datetime(2026, 8, 28, tzinfo=timezone.utc))
assert len(out) <= 100