diff --git a/api/services/forge_projection.py b/api/services/forge_projection.py index 343caf3..ee38ddd 100644 --- a/api/services/forge_projection.py +++ b/api/services/forge_projection.py @@ -32,6 +32,26 @@ _HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)$", re.MULTILINE) DEFAULT_FORGE_BASE = "https://forgejo.coulomb.social/coulomb" +_IDENTIFIER_SLUG = re.compile( + r"^[a-z0-9-]*-wp-\d{3,}(?:-t\d{2})?$" # PREFIX-WP-0001, -T01 + r"|^[a-z0-9-]*adhoc[a-z0-9-]*?-\d{4}-\d{2}-\d{2}(?:-t\d{2})?$" # daily ad-hoc, qualified either side +) + + +def _slug_is_identifier(slug: str) -> bool: + """Whether a row's slug is a work-record identifier or a title slug. + + This is what separates a re-key from a rename when the path cannot. A row + whose slug is an identifier is *claiming* to be that record, so if the forge + derives a different identifier for the same file, they are two records and + the old one retires. A row whose slug is a title (`three-phoenix-ha-cluster` + for `RCLUSTER-WP-0007`) never claimed one: those are hub-first rows from + before ADR-001, where the backing path is the only link there has ever been, + and path matching is the only thing that can hold them together. + """ + return bool(_IDENTIFIER_SLUG.match((slug or "").strip().lower())) + + def _identity_is_derived(row: Any) -> bool: """Whether this row's UUID was derived from its identifier (ADR-007). @@ -641,7 +661,12 @@ async def reset_repository_projection( for row in rows: key = (row.slug or "").strip().lower() if key not in want: - if _identity_is_derived(row): + if _identity_is_derived(row) or _slug_is_identifier(row.slug or ""): + # ADR-007: a record identified by an identifier *is* that + # identifier. Two cases reach here — a derived (v5) row, and a + # legacy row whose slug is still a work-record identifier. Both + # are re-keys, not renames. + # # 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 diff --git a/tests/test_forge_projection.py b/tests/test_forge_projection.py index b9b2bac..c77ee55 100644 --- a/tests/test_forge_projection.py +++ b/tests/test_forge_projection.py @@ -725,3 +725,30 @@ class TestRekeyIsNotRename: assert fp.derived_record_uuid("ADHOC-2026-08-25") != fp.derived_record_uuid( "CUST-WP-ADHOC-2026-08-25" ) + + @pytest.mark.parametrize( + "slug", + [ + "adhoc-2026-07-02", # grandfathered daily ad-hoc + "cust-wp-adhoc-2026-08-25", # qualified daily ad-hoc + "net-kingdom-adhoc-2026-07-02", # repo-name-qualified legacy form + "adhoc-llmc-2026-06-02", # another legacy qualification + "rcluster-wp-0007", # ordinary workplan + "cust-wp-0066-t01", # task + ], + ) + def test_identifier_slugs_are_recognised(self, slug): + """A slug that claims to be an identifier is one, whatever its UUID. + + 25 of 44 ad-hoc rows are legacy v4, so a UUID-version test alone would + leave them path-matched and silently diverged from their files. + """ + assert fp._slug_is_identifier(slug) + + @pytest.mark.parametrize( + "slug", + ["three-phoenix-ha-cluster", "testdrive-jsui-publication", "state-hub-v0.1", ""], + ) + def test_title_slugs_keep_path_matching(self, slug): + """Hub-first rows never claimed an identifier; the path is their only link.""" + assert not fp._slug_is_identifier(slug)