Skip to content

Give every migration a version the ledger can record - #417

Merged
NewWorldVenture merged 1 commit into
mainfrom
claude/bubaly-repo-connect-etzqg7
Sep 7, 2026
Merged

NewWorldVenture merged 1 commit into
mainfrom
claude/bubaly-repo-connect-etzqg7

Conversation

@NewWorldVenture

Copy link
Copy Markdown
Owner

supabase_migrations.schema_migrations has a PRIMARY KEY on version, so it physically cannot hold two rows for 0010. This repo carried 17 duplicate version numbers, which made that table unusable for it:

ERROR: duplicate key value violates unique constraint "schema_migrations_pkey"
Key (version)=(0010) already exists.

That blocked Supabase branching outright, and it sits directly in the path of the production ledger repair (LB-016 §4), which replays the history into the same table. The duplicates were dormant only while nothing tried to record them.

The rename

Every colliding group gets a unique version preserving apply order — the scheme scripts/ci-dedupe-migration-versions.mjs already used in the disposable CI checkout and proves order-preserving:

0010_blog_posts.sql                  → 00100_blog_posts.sql
0010_support_tickets_admin_users.sql → 00101_support_tickets_admin_users.sql

"00100" < "00101" < "0011" and > "0009", so nothing moves relative to its neighbours. 35 files across 17 groups; 291 migrations, contents unchanged.

Why now, and not later

The audit warned that renaming risks drift against a production database that "records applied versions by that prefix". Production's ledger holds only 0001–0003, so none of these 17 versions is recorded anywhere — there is nothing to drift from. Once the ledger is repaired that stops being true, so the rename had to come first. This was the free window.

Guardrails

KNOWN_DUPLICATE_MIGRATIONS is now empty and stays exported — the collision check still runs, so a new duplicate fails the audit instead of being absorbed. migration-version-safety now asserts there are no duplicates at all, that the escape hatch is empty, and that every version is unique, replacing the test that asserted collisions matched an approved list.

nextVersion needed fixing to survive this: it took Math.max over Number(version), and Number('01421') is 1421 — which would have pointed the next migration at 1422. It now reads the first four digits (the generation), so the 5th digit that orders files within a generation doesn't leak into numbering. Audit now reports 0276.

Ledger repair (the other half)

Adds docs/audit/migration-ledger-state.sql — a read-only pre-flight — and rewrites LB-016 §4 from "repair the ledger baseline" into an actual procedure:

  • What the repair is: not stamping. Every migration is additive and idempotent (migrations-are-additive.test.ts), and CI replays all 291 against real Postgres, so letting db push run from 0004 repairs the ledger by doing the work. Writing rows by hand asserts work was done without doing it, and a migration wrongly marked applied is skipped forever.
  • A window nobody had flagged: a replay runs 0006's is_family_member finance policies before 0267 narrows them and 0275 sweeps. End state correct; intermediate state briefly open. Maintenance window, not live traffic.

Verification

tsc --noEmit clean · 900 files / 7,700 tests pass · migration audit reports next version 0276 · dedupe script now finds 0 files to rename.

Two tests opened a renamed migration by name and failed to load; both fixed. Swept for other hardcoded paths — the remaining misses are synthetic fixtures and three stale references in production-migration-contract that predate this change on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4


Generated by Claude Code

supabase_migrations.schema_migrations has a PRIMARY KEY on version, so it
physically cannot hold two rows for 0010. This repository carried 17 duplicate
version numbers, and that made the table unusable for it:

  ERROR: duplicate key value violates unique constraint "schema_migrations_pkey"
  Key (version)=(0010) already exists.

It blocked Supabase branching outright, and it sits directly in the path of the
production ledger repair (LB-016 §4), which replays the history into that same
table. The duplicates were dormant only for as long as nothing tried to record
them.

Every colliding group is renamed to a unique version that preserves apply order,
using the scheme scripts/ci-dedupe-migration-versions.mjs already applied in the
disposable CI checkout and proves order-preserving:

  0010_blog_posts.sql                  -> 00100_blog_posts.sql
  0010_support_tickets_admin_users.sql -> 00101_support_tickets_admin_users.sql

"00100" < "00101" < "0011" and > "0009", so nothing moves relative to its
neighbours. 35 files across 17 groups; 291 migrations, unchanged in content.

Why this was safe to do NOW and would not have been later: the audit warned that
renaming risks drift against a production database that "records applied
versions by that prefix". Production's ledger holds only 0001-0003, so none of
these 17 versions is recorded anywhere — there is nothing to drift from. Once
the ledger is repaired that stops being true, so the rename had to come first.

KNOWN_DUPLICATE_MIGRATIONS is now empty and stays exported: the collision check
still runs, so a NEW duplicate fails the audit rather than being absorbed.
migration-version-safety now asserts there are no duplicates at all, that the
escape hatch is empty, and that every version is unique — replacing the test
that asserted the collisions matched an approved list.

nextVersion needed fixing to survive this. It took Math.max over Number(version),
and Number('01421') is 1421, which would have pointed the next migration at 1422.
It now reads the first four digits — the generation — so the 5th digit that
orders files within a generation does not leak into the numbering.

Two tests opened a renamed migration by name and failed to load; both now open
the new name. Swept the repo for other hardcoded paths: the remaining misses are
synthetic fixtures and three stale references in production-migration-contract
that predate this change on main.

Also adds docs/audit/migration-ledger-state.sql, a read-only pre-flight for the
ledger repair, and rewrites LB-016 §4 into an actual procedure. That section
previously said only "repair the ledger baseline". It now says what the repair
is (let db push replay from 0004 — every migration is additive and idempotent
and CI replays all 291 against real Postgres, so the ledger repairs itself
rather than being stamped by hand), what its prerequisite was (this rename), and
records a window nobody had flagged: a replay runs 0006's is_family_member
finance policies BEFORE 0267 narrows them and 0275 sweeps, so production is
briefly open mid-replay. End state correct, intermediate state not — do it in a
maintenance window, not against live traffic.

tsc --noEmit clean, 900 files / 7,700 tests pass, migration audit reports next
version 0276.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
@vercel

vercel Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bubaly Ready Ready Preview Sep 7, 2026 6:43pm UTC

Request Review

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review September 7, 2026 18:46
@NewWorldVenture
NewWorldVenture merged commit bc30ce6 into main Sep 7, 2026
7 checks passed
NewWorldVenture added a commit that referenced this pull request Sep 7, 2026
A French visitor was getting a translated header above an English page, and a
raw catalogue key in the footer reading siteFooter.acceptableUse. This makes the
whole public site render in the reader's language, and fixes the tooling that
reported it clean the entire time.

What was showing English: the site footer (every heading and link, on every
marketing page), the Knowledge Center block, all of /mobile, all four legal
pages (123 strings), the FAQ page, and inLanguage: 'en-US' in the JSON-LD, which
told every crawler the French page was English.

Why nothing caught it — three defects in the i18n tooling, each fixed:

  * walk() skipped any directory named mobile/ios/android at ANY depth. The
    intent was the sibling native projects at the repo root, so app/(marketing)/mobile
    — a real public page — was invisible to the gate. Root-anchored now.
  * the scanner matched copy against an allowlist of property names, so every
    gap had to be found by reading English off a shipped page. It now recognises
    copy by VALUE, with a list of properties that are never copy — wrong in the
    over-reporting direction, which is the direction to be wrong in.
  * i18n-lift stored the string exactly as it sat between the quotes, so a JS
    escape survived into the catalogue and 29 values held a literal backslash.
    Repaired, and a catalogue test now fails on any value carrying one.

`everything` is removed from the gated set, deliberately. It was gated on a zero
from a scanner that could not see data literals, could not see props outside its
allowlist, and skipped every directory named mobile. Widening it turned that
zero into 2,343 real strings, so the promise had never been true. It goes back
when it scans clean — a surface gated while dirty teaches everyone to ignore the
gate.

Brought up to date with main twice during review, across #414, #416, #417, #419
and #420. Conflicts in site-footer, structured-data and en-US resolved by
keeping main's titleKey/labelKey naming (the shape that makes a key in a
copy field visible on sight, which is what this PR exists to fix) and main's
relocated social row, while keeping BOTH sides in structured-data: the cached
social-links reader and this branch's translations.

The merged tree then failed catalogue-key-not-rendered-raw with 20 offenders,
and the test was right: those lists were half-lifted, a key beside a literal in
the same field. The render sites were already correct, so the literals were the
defect. Nine lifted — Children's privacy, two security commitments, the GDPR /
HIPAA / CCPA labels, two aiShowcase strings and three pricing tier labels.

Footer spacing: the language pill and social row share one wrapping flex row,
and gap-y-2 left them reading as one crowded block once wrapped. gap-y-4.

Known gaps, stated rather than hidden: 293 lifted keys are en-US only and render
English by the documented fallback; 2,343 strings remain across app/(app) and
components/modules; and the FAQ answers are database rows that no amount of code
translation reaches.

All six checks green. E2E needed one re-run: 9 iPad page.goto timeouts, no
assertion failures, 395 passing at three other viewports — the same signature
seen on #412, which contained no application code at all.

This branch was successfully deployed

1 active deployment
Preview — 4744687f Deployed Sep 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants