fix(projection): the derived UUID is identity, check it before the slug
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 26s

Two rows were being proposed for retirement that were correct all along.
`testdrive-jsui-publication` and `three-phoenix-ha-cluster` carry legacy title
slugs, but their UUIDs are exactly uuid5 of MARKITECT-WP-0002 and
RCLUSTER-WP-0007 — they are those records. `_identity_is_derived` short-circuits
on the UUID *version* before the slug rule ever runs, so both read as re-keys.
Acknowledging retirements for either repository would have retired a live
record on the strength of its slug.

The derived UUID is the strongest key available and was not used in matching at
all. It is now checked first: if a row's id equals uuid5 of a wanted
identifier, that row is that record, whatever its slug says. The slug and path
heuristics now only decide rows the UUID cannot identify.

This also corrects a claim I made from the retirement list alone: those files
derive normally. Nothing was wrong with them.

738 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 03:17:21 +02:00
parent b53daf648a
commit d4ca94198e
2 changed files with 63 additions and 11 deletions

View file

@ -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()