Skip to content

The demo teardown needs 0249 first, and must not be one transaction - #416

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

Two defects in docs/audit/demo-mode-teardown.sql, both found by actually running it against production rather than reasoning about it.

1. It failed outright on the family delete

ERROR: 23503: insert or update on table "family_model_dirty" violates
foreign key constraint "family_model_dirty_family_id_fkey"
CONTEXT: PL/pgSQL function mark_model_dirty() line 7
         SQL statement "delete from public.families where name = any (...)"

0134 attached trg_mark_model_dirty to ten family-scoped tables. Deleting a family cascades into them, each cascaded DELETE fires the trigger with old.family_id, and the trigger's INSERT references a families row that is already gone.

The repo already fixes this — 0249_model_dirty_delete_safe.sql, whose header quotes this exact error. Production's ledger stops at 0001–0003, so 0249 has never been applied there. Same root cause as everything else in LB-016: the script assumed a database further ahead than production actually is.

Step 2 now carries 0249's function verbatim as a stated prerequisite — one function replaced, no data read or written, idempotent. Confirmed applied on production.

2. The first attempt rolled back everything

Failed to run sql query: Connection terminated due to connection timeout

Everything sat inside one do $$ … $$, which Postgres runs as a single transaction — so the dashboard dropping the connection undid the two cheap table drops along with the delete that was actually slow.

Step 3 is now separate statements:

3a drop the two tables — instant
3b delete the demo family — the slow, destructive, cascading one, with a note to use a direct psql connection if the SQL editor gives up first
3c remove the demo auth user

Progress survives a timeout, and it's visible which step you're on.

Step 4 is a new read-only end-state check: one row per leftover, nothing when the teardown is complete. It replaces the raise-based assertions, which cannot report anything once their transaction is gone.

Note

Applying 0249 by hand is the same trade LB-016 §5 describes for 0254 and 0275: it closes the finding now and widens the unrecorded-ledger gap by one more migration.

Verification

migrations-are-additive and migration-version-safety both pass. This file is documentation and stays outside the *-check.sql probe glob, so CI does not execute it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4


Generated by Claude Code

Two defects, both found by running it against production.

1. IT FAILED OUTRIGHT ON THE FAMILY DELETE.

     ERROR: 23503: insert or update on table "family_model_dirty" violates
     foreign key constraint "family_model_dirty_family_id_fkey"
     CONTEXT: PL/pgSQL function mark_model_dirty() line 7
              SQL statement "delete from public.families where name = any (...)"

   0134 attached trg_mark_model_dirty to ten family-scoped tables. Deleting a
   family cascades into them, each cascaded DELETE fires the trigger with
   old.family_id, and the trigger's INSERT references a families row that is
   already gone.

   The repository already fixes this — 0249_model_dirty_delete_safe.sql, whose
   header quotes this exact error — but production's ledger stops at 0001-0003,
   so 0249 has never been applied there. Same root cause as everything else in
   LB-016: the script assumed a database further ahead than production is.

   Step 2 now carries 0249's function verbatim as a stated prerequisite. It
   replaces one function, reads and writes no data, and is idempotent.

2. THE FIRST ATTEMPT ROLLED BACK EVERYTHING.

     Failed to run sql query: Connection terminated due to connection timeout

   Everything was inside one do $$ … $$, which Postgres runs as a single
   transaction, so the dashboard dropping the connection undid the two cheap
   table drops along with the delete that was actually slow. Step 3 is now
   separate statements: 3a drops the tables (instant), 3b deletes the family
   (the slow, destructive one, with a note to use a direct psql connection if
   the editor gives up first), 3c removes the auth user. Progress survives a
   timeout, and it is visible which step you are on.

   Step 4 is a new read-only end-state check that returns one row per leftover
   and nothing when the teardown is complete — replacing the raise-based
   assertions, which cannot report anything once their transaction is gone.

Applying 0249 by hand is the same trade LB-016 §5 describes for 0254 and 0275:
it closes the finding now and widens the unrecorded-ledger gap by one.

migrations-are-additive and migration-version-safety both still pass; this
file is documentation and remains outside the *-check.sql probe glob.

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 Building Building Preview Sep 7, 2026 5:55pm UTC

Request Review

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review September 7, 2026 18:02
@NewWorldVenture
NewWorldVenture merged commit 3222568 into main Sep 7, 2026
6 of 7 checks passed
NewWorldVenture pushed a commit that referenced this pull request Sep 7, 2026
pg_stat_statements named marketing_aeo_questions as the top recurring
reader on the production database: 7,627 calls in 24 hours, 9.7ms each,
against a t4g.nano that is reporting CONNECT_TIMEOUT and an 86.8% success
rate. Nothing about those answers changes between page views — they are a
handful of rows an admin edits occasionally — so essentially all of that
was waste.

MarketingAeoSection renders on the homepage, /features, /ai, /contact,
every legal page and every /f/[id], and read the table on each render.
Looking for the same shape found two more: /faq pulls 60 rows on every
view, and every blog article ran TWO of these — one for its path, one for
its category.

All of them are cached now, keyed by their arguments so pages do not share
each other's answers, and under one `marketing-aeo` tag rather than a
short TTL. The tag matters: the AEO console's whole promise is that one
edit propagates to every matching public route immediately, and every
write path — insert, update, delete, plus the blog content loop — drops
the tag, so that still holds.

A failed read is never cached. These readers degrade to
`available: false` when the database is unreachable, which is correct for
rendering and wrong to store: caching it would leave the Knowledge Center
empty for an hour after a blip, during exactly the incident that caused
it. The cached functions throw instead, so nothing is stored, and the
catch sits outside the cache. (Same mistake I made with the social links
in #419 and fixed there — worth not repeating.)

This is the third instance of one pattern, after the footer's social links
and /pricing's 20-second poll: an uncached Supabase read inside something
that renders on every public page view.

On the rest of that pg_stat_statements output, for the record: the two
large DELETEs are one call each — the demo teardown from #416, already
run — and family deletion is not reachable in-app except as rollback
cleanup on a just-created family with no child rows, so the 93 MB cascade
does not recur. The catalog introspection is Supabase Studio, and the
index creations are one-off migrations. This was the only recurring
application workload among them.

tests/marketing-aeo-closed-loop.test.ts asserts on source text, so the
renamed callers needed its pattern widened — the same way it already
accepts either name for the FAQ reader. The behaviour it guards is
unchanged.

tsc, i18n gate, 901 files / 7,701 tests and next build all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRUgA6hD6QgzmtpSP6TUmP
NewWorldVenture added a commit that referenced this pull request Sep 7, 2026
pg_stat_statements named marketing_aeo_questions as the top recurring
reader on the production database: 7,627 calls in 24 hours, 9.7ms each,
against a t4g.nano that is reporting CONNECT_TIMEOUT and an 86.8% success
rate. Nothing about those answers changes between page views — they are a
handful of rows an admin edits occasionally — so essentially all of that
was waste.

MarketingAeoSection renders on the homepage, /features, /ai, /contact,
every legal page and every /f/[id], and read the table on each render.
Looking for the same shape found two more: /faq pulls 60 rows on every
view, and every blog article ran TWO of these — one for its path, one for
its category.

All of them are cached now, keyed by their arguments so pages do not share
each other's answers, and under one `marketing-aeo` tag rather than a
short TTL. The tag matters: the AEO console's whole promise is that one
edit propagates to every matching public route immediately, and every
write path — insert, update, delete, plus the blog content loop — drops
the tag, so that still holds.

A failed read is never cached. These readers degrade to
`available: false` when the database is unreachable, which is correct for
rendering and wrong to store: caching it would leave the Knowledge Center
empty for an hour after a blip, during exactly the incident that caused
it. The cached functions throw instead, so nothing is stored, and the
catch sits outside the cache. (Same mistake I made with the social links
in #419 and fixed there — worth not repeating.)

This is the third instance of one pattern, after the footer's social links
and /pricing's 20-second poll: an uncached Supabase read inside something
that renders on every public page view.

On the rest of that pg_stat_statements output, for the record: the two
large DELETEs are one call each — the demo teardown from #416, already
run — and family deletion is not reachable in-app except as rollback
cleanup on a just-created family with no child rows, so the 93 MB cascade
does not recur. The catalog introspection is Supabase Studio, and the
index creations are one-off migrations. This was the only recurring
application workload among them.

tests/marketing-aeo-closed-loop.test.ts asserts on source text, so the
renamed callers needed its pattern widened — the same way it already
accepts either name for the FAQ reader. The behaviour it guards is
unchanged.

tsc, i18n gate, 901 files / 7,701 tests and next build all pass.


Claude-Session: https://claude.ai/code/session_01KRUgA6hD6QgzmtpSP6TUmP

Co-authored-by: Claude <noreply@anthropic.com>
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 had an error being deployed

1 failed deployment
Preview — c171abeb 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