Chapter 32. The Verification Law
Types, build, and lint all went green and production still broke on click, because none of those checks renders a page and interacts with it. On 2026-07-17 a wallet-connect lazy-load refactor landed in Suede-AI-App as PR #660 and shipped to production under my standing merge and deploy autonomy, and six marketing pages served a Connect Wallet button that threw a client-side exception on click.
No automated check caught it. I caught it about ten minutes later by opening an isolated browser tab and clicking the button with my own hand. PR #661 reverted the refactor.
The mechanism is worth stating precisely, because the shape of it
repeats. The component was loaded through
next/dynamic(..., { ssr: false }) and lost access to a
context provider it needed. Next.js compiles that tree without
complaint. The compiler asks whether the module graph resolves and
whether the types agree, and both answers were yes. Whether a provider
is mounted above a component at the instant a click handler fires is a
property of a rendered page under interaction, and no step in the
pipeline rendered that page and interacted with it.
Ten minutes of production exposure on six pages is a cheap tuition payment. The reason it stayed at ten minutes is that clicking the button was already on my post-merge list. The reason the bug reached production at all is that clicking it was not on the pre-merge list.
What a green check is evidence of
Each check that passed on PR #660 answered its own question and none
of them answered mine, which was whether a person could connect a wallet
on ip.suedeai.ai and the other five pages after the deploy. Every one of
them was honest: tsc --noEmit told the truth about types,
and the build told the truth about compilation.
The law I run on now:
Evidence transfers only across an identical artifact, an identical environment, and an identical operation. Change any one of the three and the evidence stops applying.
PR #660 changed all three at once. The artifact I checked was a local compilation of a working tree; the artifact that broke was a Vercel production build. The environment I checked was a Node process on this Mac; the environment that broke was a browser holding a React tree. The operation I checked was compilation; the operation that broke was a click.
The law is not a demand for more checks. It is a rule for reading the checks you already ran. Before I trust a green result as a merge gate, I name what artifact it exercised, where, and under what operation, and then I ask whether the thing I am about to claim matches all three. When it does not, the gap is the test I still owe.
Four corollaries carry most of the weight in practice.
Corollary one: verify the committed artifact, not the working tree
On 2026-07-18, a rebase in Suede-AI-App produced a duplicate
articlePath function declaration in
magazine.ts. Git’s three-way merge did not see the two
versions as overlapping, so there were no conflict markers and no
prompt. tsc --noEmit caught the duplicate. I fixed it with
an Edit-tool change, re-ran tsc, the build, and the test suite, and all
three came back green.
Then I ran git push --force-with-lease and merged the PR
without ever running git add and git commit on
the fix.
Every production build after that merge failed with:
Module parse failed: Identifier 'articlePath' has already been declared
The green checks were reading a file on disk. The push was sending
committed history. Those are two different artifacts that happen to
share a path, and the local toolchain gives you no signal at all when
they diverge. tsc --noEmit, npm run build, and
npm run test:run all read the working tree, uncommitted
edits included, and they will go green on a fix that is about to be left
behind.
Hotfix PR #673 landed the same day. The live site was never down, because Vercel does not promote a failed build, and that piece of luck is exactly what makes the failure mode dangerous: a broken deploy pipeline with a healthy-looking site produces no user-visible symptom to alert on. What found it was checking Vercel’s deploy status after the merge rather than stopping at local green.
The operational form is small. Commit the fix before running the verification pass you intend to trust as a gate:
git add src/lib/magazine.ts
git commit --amend --no-edit # or a new commit
npm run build && npx tsc --noEmitThen, after the merge lands, spot-check the deploy itself with
vercel ls or gh pr checks. Local checks and
the deployed build are separate artifacts assembled from separate
inputs, and in a monorepo frontend with an external build pipeline the
second one is the one your visitors get.
There is a harsher version of this corollary on a machine running
concurrent agents. On 2026-06-10 I was editing
~/code/Suede-AI-App directly when another live session
switched the shared main checkout from main to its own
feature branch, feat/master-registry-consolidation. My
uncommitted edits went with it. Two edits in, everything was gone, with
no warning and nothing to recover from. A working tree that another
process can rewrite underneath you is not a workspace and not evidence,
which is why every task on this estate now gets its own worktree cut
from origin/main.
The overlap problem also changes how you read history. The
settlement_live hotfix was written from an isolated
worktree while a concurrent session was counter-editing the same
defaults in the main checkout. Under those conditions a commit message
describes what an author intended at the time they typed it, and the
contents describe what is now in the branch. When sessions overlap, read
the diff.
Corollary two: a compiler cannot prove that a button works
A compiler proves the code can be turned into a program. It says
nothing about what the program does when someone touches it. That
distinction is invisible in most work and decisive in a narrow class of
components: buttons, modals, and forms that do real work in an
onClick handler. Wallet connect, auth, checkout.
For those, the check is a click. Not a snapshot test that renders the component in isolation with its providers helpfully supplied, because the failure in PR #660 lived in the provider relationship that an isolated render restores for free. The click has to happen against a deployed build or a local dev build of the real page, in a real browser, with the real tree above the component.
Two details make the click trustworthy on this machine. The first is
that I run it against the actual host, not a component playground. The
second concerns the browser tooling itself, and it cost me a near-miss
on the same day as PR #660. Concurrent agent sessions on this Mac share
one browser-automation tab pool. Tabs get reused and overwritten by
whichever session grabs them, and the tool reported content from a
different origin than the one I had navigated to a moment before. Read
at face value, that stale content would have been a false regression
call on a page that was fine. A dedicated tab (tabs_create)
and a re-check gave the real answer.
Verification tooling on a shared machine is shared mutable state. An inconsistent or suspicious browser result is a signal about the tool, and the response is to isolate and re-check before it becomes a finding.
Corollary three: verify a public fix against the live URL
Production config does not all live in your repository. On
2026-07-17, while verifying PR #658, a live check against
https://social.suedeai.ai/ surfaced this in the
console:
Origin https://social.suedeai.ai not found on Allowlist - update configuration on cloud.reown.com
Two network 403s from the WalletConnect relay came with it, same root
cause. SignInButton lazily mounts SignInModal,
which calls wallet hooks without condition for signed-out visitors on
feed, forum, notifications, and settings, so social needs the wallet
stack. Nobody had ever registered social.suedeai.ai in
Reown’s allowed-origins list. It had been broken in production with no
local symptom of any kind.
Nothing in the repository, the build, or the test suite can catch
that, because the missing thing is a row in a third-party dashboard with
no representation in code. The fix was a dashboard edit adding
https://social.suedeai.ai and
https://suede.social to the allowed origins of the Reown
project referenced by NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID.
It needed my own hands, because it is an account-settings change behind
credentials I do not hand to an agent. I made it on 2026-07-17 and
confirmed the console came back clean. Chapter 27 tells that one in
full.
The generalization I took from it: when a config is per-origin and
one app tree serves multiple custom domains, enumerate every domain from
the source of truth for routing and register them together.
next.config.ts confirmed the same tree serves
suede.social and play.suedeai.ai. A per-origin
config verified on one origin is verified on one origin.
The same shape shows up in rendered output. PR #751 fixed two
missing-space typos on a live marketing page where JSX whitespace
collapsing around a {expression} followed by text on the
next line produced $7.99/mowhen, with zero space between
the price and the next word. The source looked correct. Reformatting a
JSX line changes the rendered string, so the string you check has to be
the one the browser produced, not the one in the editor. Same lesson at
the character level: read the output, at the URL, in the form a visitor
receives it.
Done right, this corollary produces a proof you can paste. The shared
root layout at Suede-AI-App/frontend/src/app/layout.tsx
wraps all eight hosts (app, social, studio, play, agents, ip, fretpulse,
distro) and called headers() without condition to answer
one question: is this app.suedeai.ai? True for one host, false for the
other seven, and that single call forced dynamic rendering across the
whole tree. studio.suedeai.ai served
Cache-Control: private, no-cache, no-store on every
request, with no CDN caching at all. PR #653, merged 2026-07-17, moved
the app-host-exclusive routes into an (app-host) route
group with their own layout and removed headers() from the
shared root. What closed the finding was a header read from the live
surfaces afterward: studio, social, play, and distro confirmed serving
Cache-Control: public, max-age=0, must-revalidate. A prior
middleware-level Cache-Control override, PR #612, is on record as not
working on Vercel, so a repo diff would have looked equally convincing
in both cases. The response header decided it.
Corollary four: a 200 response does not name what served it
This is the corollary that took the longest to learn, and the one with the ugliest receipt.
On 2026-06-11 I recorded that Agent Studio’s production migrations
were applied and verified live: the gateway LLM endpoint returned 200
with metering, and topup returned 402 challenges with a real
payTo. Five weeks later, on 2026-07-16, the same record
states that Agent Studio’s Supabase runtime had never been implemented,
despite Phase 9 assuming DB_DRIVER=supabase in
production.
The June verification was insufficient and does not support a migration claim. A 200 response and a 402 challenge can both come from the SQLite fallback, so those checks proved endpoint behavior without proving which repository served it. For this book, the 2026-07-16 production audit supersedes the June status note: the durable Supabase path was not operating at the start of that audit, and the later end-to-end canary is the evidence that closed the repair. The Phase 8 note’s conflicting pending label is historical bookkeeping, not a current deployment fact.
What the 2026-07-16 outage established is not in dispute. Vercel
production env vars (DB_DRIVER, SUPABASE_*)
were pulling as empty strings, and the code took a fallback path to a
non-durable SQLite repository inside a serverless function with no error
raised. An env var that reads as an empty string is worse than one that
is missing, because a missing var can throw and an empty one selects a
branch. The same-day audit found live /api/v2/* endpoints
returning 503, a disabled Run button, and a false save/recovery conflict
that persisted a draft on page view. PRs #71 and #72 restored proper
Supabase config, hardened RLS, removed the service-role key from Vercel,
and added atomic RPCs; an end-to-end canary covering create, bind,
version, and run passed. PR #73 followed. Agent Studio’s tables sit in
the same shared Suede Supabase project as the rest of the estate, a
deliberate and temporary choice I made rather than an accident, and it
has not been split out.
The durability claim needs a durability test: write through the API, redeploy, read back. An HTTP 200 proves an endpoint answered. It does not name the database that served it.
The money path taught the same lesson from the other direction. Phase
9 shipped the x402 settlement toggle opt-in with a default of false, and
production had no settlement_live column, so every agent
mapped to dry-run, and dry-run skips the 402 payment challenge. Every
priced agent on Suede Agent Studio was free to call while settlement was
live. An orchestrator spot-curl of the money path caught it. Hotfix
12d841b inverted the default to opt-out: a missing or NULL
settlement_live now means live, only an owner toggle-off
disables it, and the env var X402_SKIP_SETTLEMENT is the
sole kill-switch. Money-path defaults fail closed. Every deploy that
touches the money path gets a curl against the money path.
A cousin of this: process liveness is not function. Render was
projecting about $16.83 for July across two Starter services on
2026-07-16, and producer-suedelabs-worker had zero
successful job logs, with nothing but DB-connection failures since its
2026-07-14 deploy. It was up. It had never completed a job. Monitoring
watched the process, and the thing worth watching was the success log.
Suspending it removed the cost and created a new risk in the same move:
Suede-AI-App, the live x402 API service, went to Render’s
Free tier, which can cold-spin-down after 15 minutes idle with less CPU
and traffic headroom than Starter. That risk is recorded as an open
watch item next to the saving, unresolved, because a cost cut is a
change to production topology.
When verification has to run before the action
Some actions do not grant a second look. The four corollaries above
all assume you get one. In May 2026 a PR comment on the external repo
solana-foundation/awesome-solana-ai#155 went out under the
Suede-AI bot account instead of my founder account.
gh had the secondary account active in the keyring and
nothing checked which identity was live before the mutating command ran.
The comment was already public and already attributed by the time anyone
could audit it.
For a solo founder, the byline on external work is a distribution
asset. Investors and collaborators read GitHub activity and package
metadata, and a bot account earns nothing back. That makes identity a
precondition rather than a review item: gh auth status gets
checked before gh pr create, gh pr comment,
gh issue create, gh issue comment,
gh pr review, gh pr merge, and
gh release create. Read-only commands can run under
whichever account is active.
Where an action publishes, sends, charges, or writes under a name, the verification moves in front of it. Everything else can be caught on the way out.
The pre-merge and post-merge checklist
This is the version short enough that I run it instead of admiring it. It fits between the last edit and the merge.
Before the merge:
git statusandgit diff --stat. Every fix you intend to gate on must be committed, not sitting in the working tree.- Re-run the gate after committing: build,
tsc --noEmit, tests. These read the tree, so the tree must match the commit. - If the diff touches a client-interactive component, click it. Real browser, real page, dedicated tab, deployed or local dev build.
- If the diff touches a money path, curl the money path and confirm the 402 challenge.
- Name what a green check exercised. Artifact, environment, operation. If your claim covers more than the check did, the gap is a test you owe.
After the merge:
vercel lsorgh pr checks. Confirm the deploy that visitors get, not the one you built.- Curl or load the live URL for the surface you changed, and read the rendered output and the console, not the source.
- For anything touching a datastore, write through the API and read back after the deploy.
- Record what you verified and what you did not. An unverified item written down is an open item; an unverified item left out becomes a claim.
The fifth line is the one doing the work. The rest are instances of it that recur often enough on this estate to deserve their own numbers.