The hook
First triptych: three cards on what lies — status file, counter, delegate's context. Three defensive hooks that force the observable to speak. Second triptych, opening here: no longer what lies, but what it unlocks. When distrust is ritualised, audacity becomes possible. Card #04: a fifteen-line contract test that turns schema refactor — the operation you avoided out of fear of missing a reference — into a routine gesture you do before coffee.
What was blocked
For six weeks, I dragged an obvious rename. The domain status eleve (French for "student") should have been called inscrit ("enrolled") from day one — eleve covers former students, blocked accounts, no-response leads, it's too broad. But the status was used in the Postgres CHECK constraint, in a TypeScript constant, in twelve UI components, in three Brevo exports, in four SQL views, in two batch scripts. A blind grep = guaranteed miss. A rename PR = fear of silent break in production.
The fear wasn't irrational. On May 11, a HubSpot import introduced a status suspect that the CHECK constraint refused. The TS code didn't know — it was silently swallowing Postgres exceptions as network errors. Symptom: three imports fail in prod, no alert because the try / catch swallowed everything. Diagnosis two days later. If I renamed eleve without syncing DB and TS, I'd reproduce the exact same drift class.
The contract test that unlocks
Drop in tests/contracts/statuts.contract.test.ts. Fifteen lines of Jest doing one thing: grep the Postgres CHECK constraint, compare to the values declared in a TypeScript constant, fail the build with a message that tells you exactly where the drift appeared.
import { CONTACT_STATUT_VALID } from '@/lib/contacts'
import { createSupabaseAdmin } from '@/lib/supabase-admin'
test('contacts.statut: DB CHECK matches TS whitelist', async () => {
const { data } = await createSupabaseAdmin().rpc('introspect_check_values', {
p_table: 'contacts', p_column: 'statut',
})
const db = new Set(data as string[])
const ts = new Set(CONTACT_STATUT_VALID)
const dbOnly = [...db].filter(v => !ts.has(v))
const tsOnly = [...ts].filter(v => !db.has(v))
if (dbOnly.length || tsOnly.length) {
throw new Error(`Drift: DB-only=[${dbOnly}], TS-only=[${tsOnly}]`)
}
})
The set diff is what makes the error message useful at scale. With five values it feels over-engineered; with thirty, the raw enumeration becomes unreadable and hides exactly what the test should pinpoint. The message Drift: DB-only=[suspect], TS-only=[en_attente] tells you in two words where to fix. The introspect_check_values function is a three-line Postgres RPC that parses pg_get_constraintdef with regex; you can also replace it with a plain SELECT on pg_constraint if you prefer staying in SQL.
What it unlocks concretely
The pattern doesn't save time on the test itself — it costs you fifteen lines to write. The gain is downstream, on the refactors you no longer felt like attempting. With this test in CI, the eleve → inscrit rename took thirty minutes: change the DB migration, change the TS constant, push. CI red if either side forgot the other, with a message that tells you exactly which value is missing where. You fix, you repush, CI green. The cross-file refactor stops being an act of bravery; it becomes a workflow.
Beyond rename, the test opens a whole category of operations that fear used to close off: adding a status without risking the TS oversight, removing a status without risking a silent INSERT crash, merging two statuses into one with automatic perimeter control. Anything you hesitated to do because "I might break something else somewhere" becomes bounded by an explicit error message. The defensive doctrine of triptych 1 produces here its positive yield: the trust isn't in the tool, it's in the material net under the tool. You jump because you saw the net with your own eyes.
Apply now
Identify in your project a TypeScript constant that must match a Postgres CHECK constraint (or a DB enum, or any application whitelist). Copy the template above, adapt the table and column name, write the three-line introspection RPC in SQL. Push the test into CI. Next time you hesitate to rename or modify that column, look at the test. It does the grep for you. You don't have to do it in your head anymore. The refactor you'd been pushing for six weeks becomes a Friday afternoon PR.
And when you delegate the refactor to a sub-agent (cf. QW-03), the test in CI catches what your brief might have missed. The net holds for your delegate as much as for you.
Your quick win takes five minutes — the time to copy the test, adapt the constant name, add the introspection RPC. The doctrine of triptych 1 taught you to distrust summaries; that of triptych 2 teaches you to use that distrust to permit yourself what you used to forbid.
Quick Win Card series, episode 04. Opening of the 2nd triptych: what doctrine unlocks. Reference ADR-0044 (DB↔code contract tests) — doctrine repo: github.com/michelfaure/doctrine-counterpart. Foreseen sequel triptych: QW-05 on the [spike] tag that unlocks prototyping without debt, QW-06 on the inline brief that unlocks fearless delegation.

Top comments (0)