fix(projection): a stamped slug counts as retired even if the flag is clear
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 36s

Six rows were resurrected before the previous fix landed: tombstoned slug,
cleared retirement flag, sitting beside the correct record derived from the
same file. Nothing can see them. They are not retired, so they are not stale;
they path-match their own file, so every reset reports `noop` and they persist
indefinitely.

The stamp in the slug is itself evidence that a retirement happened, so a row
carrying the mark with a cleared flag is a contradiction to repair rather than
a live record. Such a row is now skipped by the heuristics like any retired
one, which makes it visible as a retirement candidate again.

747 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 11:30:25 +02:00
parent d1ace1295f
commit 702578cca4
2 changed files with 15 additions and 3 deletions

View file

@ -732,7 +732,13 @@ async def reset_repository_projection(
# which is then deliberately un-retired below.
matched[key] = row
continue
if row.projection_retired_at is not None:
if row.projection_retired_at is not None or RETIRED_SLUG_MARK in (row.slug or ""):
# A stamped slug is itself evidence of a retirement, so a row
# carrying the mark with a cleared flag is a contradiction to
# repair rather than a live record — six rows reached that state
# before the resurrection was fixed, and nothing else can see them:
# they path-match their own file and report `noop` forever.
#
# Past this point only the heuristics remain, and a retired row must
# not be matched by those. Retirement is a
# decision, and re-deriving the same file must not silently undo it.

View file

@ -907,7 +907,7 @@ class TestRetirementIsNotUndoneByPath:
def test_retired_rows_are_skipped_before_path_matching(self):
import inspect
src = inspect.getsource(fp.reset_repository_projection)
skip_at = src.index("if row.projection_retired_at is not None:")
skip_at = src.index("if row.projection_retired_at is not None or RETIRED_SLUG_MARK")
path_at = src.index("if bp and _path_key(bp) in want_paths:")
assert skip_at < path_at, "retired rows must be skipped before path matching"
@ -916,7 +916,7 @@ class TestRetirementIsNotUndoneByPath:
import inspect
src = inspect.getsource(fp.reset_repository_projection)
assert src.index("want_by_uuid.get(str(row.id))") < src.index(
"if row.projection_retired_at is not None:"
"if row.projection_retired_at is not None or RETIRED_SLUG_MARK"
)
def test_a_row_that_derives_again_is_not_tombstoned(self):
@ -930,3 +930,9 @@ class TestRetirementIsNotUndoneByPath:
rel = src.index("outcome.released.append(before)")
guard = src.index('in want or str(r.id) in want_by_uuid')
assert guard < rel, "the release must exempt records the forge still derives"
def test_a_stamped_slug_counts_as_retired_even_if_the_flag_is_clear(self):
"""Otherwise a resurrected row path-matches its file and reports noop forever."""
import inspect
src = inspect.getsource(fp.reset_repository_projection)
assert 'projection_retired_at is not None or RETIRED_SLUG_MARK in' in src