From efa2bc026b570df70606bc2aaa2ff57c229f2624 Mon Sep 17 00:00:00 2001 From: Georgy Butaev <41178744+g-but@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:04:19 +0200 Subject: [PATCH] fix(invariants): the email-handle floor is zero now, because they are all retired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ratchet was set to 77 — a live population that could not simply be renamed, because a username on OrangeCat is also a Lightning address and a public profile URL. scripts/rename-email-derived-usernames.sql retired every one of them on production, behind profile_username_history so nothing that pointed at an old handle broke: INSERT 0 53 history rows recorded first UPDATE 53 handles replaced with user_<12 hex> UPDATE 14 display names that were also the email local part cleared Verified against production afterwards, not assumed: email_derived_remaining 0 name_eq_localpart_remaining 0 history_rows 54, none orphaned count_email_derived_usernames() 0 And end to end on a real retired handle: the old profile URL returns 308 to the new one, and the LNURL endpoint resolves the owner through history (it answers under the NEW handle, which is what proves the lookup found the right account). The count of 53 rather than the 77 originally measured is test-account churn between the two readings — the population moved 108 -> 96 profiles while E2E accounts were created and deleted, and 16 accounts had already signed up under the fixed trigger. So the constant becomes 0 and stops being a ratchet. A violation now means a write path started minting them again; the three that used to — the handle_new_user trigger, ensureProfile(), and two profile form pre-fills — are each covered by a test, so a hit here means a fourth was added. The message says that instead of talking about a baseline that no longer exists. npm run verify green; 2385 tests pass. Co-Authored-By: Claude Opus 5 --- scripts/check-data-invariants.mjs | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/scripts/check-data-invariants.mjs b/scripts/check-data-invariants.mjs index 7083dbd38..a93c9cbcf 100644 --- a/scripts/check-data-invariants.mjs +++ b/scripts/check-data-invariants.mjs @@ -289,13 +289,21 @@ async function checkSilentlyDroppedCatTurns() { * number RISES, which means a new write path started deriving handles from * emails again. */ -// Measured on production 2026-08-26 AFTER the trigger fix landed. The first -// reading taken while writing that fix said 72; five accounts signed up in the -// hour between, so a baseline of 72 would have failed on its very first -// nightly run — a gate red about code that is fine, which is the habit this -// ratchet exists to avoid. The number is a floor now rather than a moving -// target: handle_new_user no longer mints these, so nothing can add to it. -const EMAIL_DERIVED_USERNAME_BASELINE = 77; +// Zero, and it stays zero. +// +// This started as a ratchet against a live population: 77 profiles published +// their owner's email local part as a public, crawlable handle, and they could +// not simply be renamed because a username here is also a Lightning address. +// scripts/rename-email-derived-usernames.sql retired all of them on +// 2026-08-26 behind profile_username_history, so the old handles still resolve +// and the count is genuinely 0 — verified against production, along with 0 +// display names still set to an email local part. +// +// A violation now means a write path started minting them again. There are +// three that used to: the handle_new_user trigger, ensureProfile(), and two +// profile form pre-fills. Each is covered by a test, so a regression here means +// a FOURTH one was added. +const EMAIL_DERIVED_USERNAME_BASELINE = 0; async function checkEmailDerivedUsernames() { const count = Number(await rpc('count_email_derived_usernames')); @@ -303,18 +311,14 @@ async function checkEmailDerivedUsernames() { if (count > EMAIL_DERIVED_USERNAME_BASELINE) { violation( 'profiles.username_from_email', - `${count} profiles publish their email local part as a public, crawlable handle — ` + - `up from the ${EMAIL_DERIVED_USERNAME_BASELINE} known on 2026-08-26, so a write path ` + - `is minting them again (handle_new_user, a script, or a manual fix)`, + `${count} profile(s) publish their email local part as a public, crawlable handle. ` + + `All of them were retired on 2026-08-26 and every known write path is covered by a ` + + `test, so this means a NEW one is minting them — find it before the count grows, and ` + + `retire these with scripts/rename-email-derived-usernames.sql`, [] ); - } else if (count < EMAIL_DERIVED_USERNAME_BASELINE) { - notes.push( - `profiles: ${count} email-derived handles left (was ${EMAIL_DERIVED_USERNAME_BASELINE}) — ` + - `lower EMAIL_DERIVED_USERNAME_BASELINE so the ratchet holds the new floor` - ); } else { - notes.push(`profiles: ${count} email-derived handles, unchanged — no new ones minted`); + notes.push('profiles: no handle is an email local part'); } }