Repository navigation
The demo teardown needs 0249 first, and must not be one transaction - #416
Merged
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
NewWorldVenture
marked this pull request as ready for review
September 7, 2026 18:02
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
0134attachedtrg_mark_model_dirtyto ten family-scoped tables. Deleting a family cascades into them, each cascaded DELETE fires the trigger withold.family_id, and the trigger's INSERT references afamiliesrow 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 at0001–0003, so0249has 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
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:
psqlconnection if the SQL editor gives up firstProgress 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
0249by hand is the same trade LB-016 §5 describes for0254and0275: it closes the finding now and widens the unrecorded-ledger gap by one more migration.Verification
migrations-are-additiveandmigration-version-safetyboth pass. This file is documentation and stays outside the*-check.sqlprobe glob, so CI does not execute it.🤖 Generated with Claude Code
https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
Generated by Claude Code