fix(projection): a re-keyed record is new, not a renamed file
The ad-hoc requalification changed 30 identifiers while deliberately keeping the filenames, per canon. The matcher's path fallback therefore matched every old row to its new record and updated it in place — so the files said CUST-WP-ADHOC-2026-08-25 while the hub still said adhoc-2026-08-25, with the row's UUID still encoding the old identifier. Exactly the file/hub identity divergence ADR-007 exists to prevent. Path matching cannot distinguish a re-key from a rename when the path does not change. Identity can: a derived row is UUIDv5 over the record id, so a changed id is a different record — the old row retires and the new one is created. Rows predating derived identity carry v4 UUIDs, where the identifier is a label rather than the identity; those keep path matching, so rename detection still works where it is the right answer. The unmatched old row keeps its own slug as key, so it lands in `stale` and becomes a retirement candidate rather than disappearing from the outcome. 720 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:
parent
4f68c5609f
commit
b2efadf254
2 changed files with 71 additions and 0 deletions
|
|
@ -32,6 +32,18 @@ _HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)$", re.MULTILINE)
|
||||||
DEFAULT_FORGE_BASE = "https://forgejo.coulomb.social/coulomb"
|
DEFAULT_FORGE_BASE = "https://forgejo.coulomb.social/coulomb"
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_is_derived(row: Any) -> bool:
|
||||||
|
"""Whether this row's UUID was derived from its identifier (ADR-007).
|
||||||
|
|
||||||
|
Derived rows are UUIDv5 over the work-record namespace. Rows predating
|
||||||
|
derived identity carry a random v4 UUID, and for those the identifier is a
|
||||||
|
label rather than the identity — so the path fallback still applies, which
|
||||||
|
is what keeps rename detection working for legacy records.
|
||||||
|
"""
|
||||||
|
rid = getattr(row, "id", None)
|
||||||
|
return getattr(rid, "version", None) == 5
|
||||||
|
|
||||||
|
|
||||||
def derived_record_uuid(record_id: str) -> str:
|
def derived_record_uuid(record_id: str) -> str:
|
||||||
return str(uuid.uuid5(_WORK_RECORD_NAMESPACE, f"helixforge\n{record_id}"))
|
return str(uuid.uuid5(_WORK_RECORD_NAMESPACE, f"helixforge\n{record_id}"))
|
||||||
|
|
||||||
|
|
@ -629,6 +641,24 @@ async def reset_repository_projection(
|
||||||
for row in rows:
|
for row in rows:
|
||||||
key = (row.slug or "").strip().lower()
|
key = (row.slug or "").strip().lower()
|
||||||
if key not in want:
|
if key not in want:
|
||||||
|
if _identity_is_derived(row):
|
||||||
|
# ADR-007: a derived record's identity *is* a function of its
|
||||||
|
# identifier, so a changed identifier means a different record —
|
||||||
|
# the old one retires and the new one is created. Matching it to
|
||||||
|
# the file by path instead would update the row in place while
|
||||||
|
# its UUID still encodes the old identifier, leaving the file
|
||||||
|
# and the hub disagreeing about what the record is called.
|
||||||
|
#
|
||||||
|
# This is not hypothetical: the ad-hoc requalification
|
||||||
|
# (CUST-WP-0066) deliberately keeps the filename, so for those
|
||||||
|
# records a re-key *never* changes the path. Path matching
|
||||||
|
# cannot tell a re-key from a rename there, and silently chose
|
||||||
|
# rename for all 30 of them.
|
||||||
|
#
|
||||||
|
# The row keeps its own slug as key, so it lands in `stale` and
|
||||||
|
# becomes a retirement candidate rather than vanishing.
|
||||||
|
matched[key] = row
|
||||||
|
continue
|
||||||
bp = row.backing_relative_path
|
bp = row.backing_relative_path
|
||||||
if bp and _path_key(bp) in want_paths:
|
if bp and _path_key(bp) in want_paths:
|
||||||
key = want_paths[_path_key(bp)]
|
key = want_paths[_path_key(bp)]
|
||||||
|
|
|
||||||
|
|
@ -684,3 +684,44 @@ class TestForgeCredential:
|
||||||
monkeypatch.setattr(fp.subprocess, "run", fake_run)
|
monkeypatch.setattr(fp.subprocess, "run", fake_run)
|
||||||
assert fp._run_git("status") == "ok"
|
assert fp._run_git("status") == "ok"
|
||||||
assert "GIT_CONFIG_COUNT" not in seen["env"]
|
assert "GIT_CONFIG_COUNT" not in seen["env"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestRekeyIsNotRename:
|
||||||
|
"""A changed identifier is a new record, not a moved file.
|
||||||
|
|
||||||
|
The ad-hoc requalification (CUST-WP-0066) keeps the filename by design, so
|
||||||
|
for those records a re-key never changes the path. Matching on path there
|
||||||
|
cannot distinguish the two cases, and choosing rename updates a row whose
|
||||||
|
UUID still encodes the old identifier — leaving the file and the hub
|
||||||
|
disagreeing about what the record is called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class _Row:
|
||||||
|
def __init__(self, id, slug, path):
|
||||||
|
self.id = id
|
||||||
|
self.slug = slug
|
||||||
|
self.backing_relative_path = path
|
||||||
|
self.projection_retired_at = None
|
||||||
|
self.status = "finished"
|
||||||
|
|
||||||
|
def test_a_derived_row_is_not_matched_by_path(self):
|
||||||
|
"""uuid5 means identity is a function of the identifier."""
|
||||||
|
import uuid as _u
|
||||||
|
old = self._Row(
|
||||||
|
_u.UUID(fp.derived_record_uuid("ADHOC-2026-08-25")),
|
||||||
|
"adhoc-2026-08-25",
|
||||||
|
"workplans/ADHOC-2026-08-25.md",
|
||||||
|
)
|
||||||
|
assert fp._identity_is_derived(old)
|
||||||
|
|
||||||
|
def test_a_legacy_row_keeps_path_matching(self):
|
||||||
|
"""v4 rows predate derived identity; there the identifier is a label."""
|
||||||
|
import uuid as _u
|
||||||
|
legacy = self._Row(_u.uuid4(), "repo-wp-0001", "workplans/REPO-WP-0001.md")
|
||||||
|
assert not fp._identity_is_derived(legacy)
|
||||||
|
|
||||||
|
def test_the_two_uuids_actually_differ(self):
|
||||||
|
"""Guards the premise: if they were equal the distinction is moot."""
|
||||||
|
assert fp.derived_record_uuid("ADHOC-2026-08-25") != fp.derived_record_uuid(
|
||||||
|
"CUST-WP-ADHOC-2026-08-25"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue