From 702578cca4b6631ae251a74e52ce4df870a76c08 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 28 Aug 2026 11:30:25 +0200 Subject: [PATCH] fix(projection): a stamped slug counts as retired even if the flag is clear 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 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006 --- api/services/forge_projection.py | 8 +++++++- tests/test_forge_projection.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/api/services/forge_projection.py b/api/services/forge_projection.py index 7773269..28f1f35 100644 --- a/api/services/forge_projection.py +++ b/api/services/forge_projection.py @@ -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. diff --git a/tests/test_forge_projection.py b/tests/test_forge_projection.py index 3787c31..a1ff7ac 100644 --- a/tests/test_forge_projection.py +++ b/tests/test_forge_projection.py @@ -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