fix(projection): rank competing claims instead of overwriting
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 24s

Two rows can claim one record. `cust-wp-0010` claimed CUST-WP-0010 by its own
identifier while `workstream-lifecycle-documentation` claimed it by path, both
pointing at that workplan's file; `kont-wp-0013` and
`KONT-WP-0013-blob-storage-content-streaming` did the same. Assigning into
`matched` unconditionally let whichever row came last win and dropped the other
silently — never matched, so never stale, so never reported by any pass. It was
also order-dependent, so which row survived depended on row iteration order.

Claims are now ranked: derived UUID, then the record's own identifier, then
backing path, then prefix heuristic. The strongest wins and the loser is
displaced into `stale`, which makes it a retirement candidate rather than
invisible. Ranking is order-independent, verified both ways round.

Under ADR-007 the identifier is the identity, so the row naming the record wins
and its stale title and path are repaired from the file; the hub-first row that
never held the identifier retires with its history intact.

751 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 15:01:58 +02:00
parent 1af9ec2f0c
commit 218f7fd8b5
2 changed files with 63 additions and 7 deletions

View file

@ -949,3 +949,33 @@ class TestRetirementIsNotUndoneByPath:
zombie = src.index("projection_retired_at is None and RETIRED_SLUG_MARK in")
tail = src[zombie:zombie + 700]
assert "matched[key] = row" in tail, "a resurrected row must enter `matched`"
class TestCompetingClaims:
"""Two rows can claim one record; the weaker must not vanish.
`cust-wp-0010` claimed CUST-WP-0010 by its own identifier while
`workstream-lifecycle-documentation` claimed it by path. Assigning into
`matched` unconditionally let the later one win and dropped the other
silently never matched, so never stale, so never reported by any pass.
"""
def test_claims_are_ranked_not_overwritten(self):
import inspect
src = inspect.getsource(fp.reset_repository_projection)
assert "def _claim(" in src
assert "displaced.append" in src
def test_a_displaced_row_becomes_stale(self):
import inspect
src = inspect.getsource(fp.reset_repository_projection)
stale_at = src.index("stale = [")
assert "displaced" in src[stale_at:stale_at + 400]
def test_identifier_beats_path(self):
"""A row naming the record outranks one that merely shares its file."""
import inspect
src = inspect.getsource(fp.reset_repository_projection)
assert src.index("UUID_MATCH, SLUG_MATCH, PATH_MATCH, PREFIX_MATCH = 0, 1, 2, 3") > 0
# Strength is ordered strongest-first, so a lower number wins.
assert "if strength < held:" in src