| š§ Verification has to be structural, not voluntary |
| āø1.1 | A hosted CI pipeline, not just local hooks GitHub Actions Ā· .github/workflows/ci.yml Ā· permissions: contents: read Ā· concurrency: cancel-in-progress Ā· actions/checkout@<sha> # v7.0.1 Ā· pnpm install --frozen-lockfile | A pull-request workflow running typecheck, lint, format check, unit tests and build as separate named jobs on a hosted runner.š§ ELI5Checking your own homework at home is great, but the teacher still checks it at school. If you are the only one who checks, you can skip it on a busy day and nobody would ever know. CI is the teacher who checks every paper, every time, the same way. | Keeping every check in a local hook and calling it verification ā no pipeline at all, so one bypass flag skips the whole project. | P0 |
| āø1.2 | A staged-files-only pre-commit hook husky Ā· lint-staged Ā· .husky/pre-commit ā pnpm exec lint-staged Ā· prettier --write on staged paths Ā· pre-push ā tsc, not next build | One line in the hook ā format the staged files and re-stage them. About a second, regardless of how large the repo gets.š§ ELI5If brushing your teeth took a whole hour, you would start skipping it. So keep the every-night routine tiny ā a quick brush ā and save the deep clean for the dentist. The commit hook is the quick brush; CI is the dentist. | Running the full suite, plus a second language's dependency sync, on every commit ā until people quietly start bypassing the hook. | P0 |
| āø1.3 | Read-only lint in every automated path eslint (CI) vs eslint --fix (human) Ā· prettier --check vs prettier --write Ā· pnpm run ci ā "lint", never "lint:fix" | Two scripts: a checking one wired into the hook and CI, and a fixing one that only a human ever invokes.š§ ELI5A checker's job is to say 'this is messy', not to secretly tidy up and announce 'all clean!'. If the robot quietly fixes the mess and then throws the fix away, nobody ever finds out where the mess keeps coming from. | Wiring the auto-fix variant into a hook or a runner, so violations get repaired, discarded, and never reported. | P0 |
| āø1.4 | Required status checks on the default branch GitHub branch protection / rulesets Ā· required contexts by job name Ā· Detect source changes Ā· Types, lint, format Ā· Unit tests Ā· Migrations apply from empty Ā· Production build Ā· Docs links | Branch protection naming each job explicitly, so a red or missing run blocks the merge button rather than merely annotating it.š§ ELI5A red traffic light only works if cars actually have to stop. If the light is just a suggestion, someone will drive through it ā probably on the busiest day. Marking checks as required turns the light into a gate arm that physically blocks the road. | Shipping the workflows and stopping there, leaving runs that look authoritative, are required by nothing, and merge red. | P0 |
| āø1.5 | A host-faithful production build before every deploy next build Ā· .husky/pre-push ā pnpm run build Ā· Production build as a required CI job Ā· vercel pull + vercel build for host parity | A required CI job that installs from the frozen lockfile and runs the production build on the host's runtime version with its build-time env ā and, until that job exists, the same build in a pre-push hook.š§ ELI5Before you bring a cake to the party, bake a test one at home ā using the party's recipe card, not the one in your head. If it is going to collapse, find out in your own kitchen, not in front of the guests. And remember your oven is not their oven: the real test is baking it the way they will. | Pushing to an auto-deploying branch when typecheck, lint and tests all pass but nothing has run the build ā so the platform's build log is where a build-only failure is discovered. | P1 |
| š Encode the rule in the compiler, not in a document |
| āø2.1 | A compiler-enforced client/server boundary server-only Ā· import "server-only" as line 1 Ā· declared in dependencies, not framework-aliased Ā· eslint no-restricted-imports as a fallback | A server-only marker as the first line of every module that reads a credential or opens a database connection, declared as a real dependency.š§ ELI5The house key is secret, so don't just put up a sign saying 'please don't take the key outside' ā bolt the key to the wall with a chain. Now taking it outside isn't against the rules; it's impossible. The compiler is the chain. | Leaving the boundary as a sentence in the contributing guide, where nothing objects until a key is already in a shipped bundle. | P0 |
| āø2.2 | Every committed config has its tool installed and running prettier + .prettierrc Ā· @ianvs/prettier-plugin-sort-imports Ā· prettier-plugin-tailwindcss Ā· format:check in CI Ā· .git-blame-ignore-revs | Each config file maps to a declared dependency and a CI job ā plus a one-off mechanical reformat landed as its own commit and recorded in the blame-ignore file.š§ ELI5A chore chart on the fridge doesn't clean the room ā someone has to actually do the chores. If nobody ever does them, the chart just makes the mess look handled. Either do the chores, or take the chart down so everyone can see the mess. | Committing a formatter config without installing the formatter, so the file reads as evidence the concern is handled while the codebase drifts. | P0 |
| āø2.3 | Guides that describe the code that exists CONTRIBUTING.md (human process) Ā· AGENTS.md / CLAUDE.md (agent guardrails) Ā· .github/pull_request_template.md | Contributor and agent guides naming the data layer, test runner and directory layout actually present, with abandoned directions marked as abandoned.š§ ELI5A treasure map showing a bridge that was never built sends every new explorer the wrong way. Worse, they'll trust the map over their own eyes, because maps look official. Redraw the map so it shows the paths that really exist. | Leaving a guide that mandates a library which is not a dependency, so new work ā including agent work ā aims at an architecture you already rejected. | P0 |
| šÆ Every version and constant gets exactly one home |
| āø3.1 | One declared runtime version .nvmrc Ā· engines: { node: ">=22" } Ā· @types/node held at the runtime major Ā· actions/setup-node with node-version-file: .nvmrc | A version file, an engines range and runtime type definitions all naming the same major, with dependency automation told not to bump past it.š§ ELI5If the three clocks in your house all show different times, you'll miss the bus and won't even know which clock lied to you. Keep one clock, and make every other clock in the house copy it. | Restating the runtime in three places ā version file, type definitions, engines field ā and pinning none of them. | P1 |
| āø3.2 | One composite setup step, reading versions from the repo .github/actions/setup/action.yml Ā· pnpm/action-setup reading packageManager Ā· actions/cache keyed on resolved version Ā· install-deps still runs on cache hit | A single shared setup action doing checkout, runtime, package manager and install ā runtime read from the version file, package manager from the manifest.š§ ELI5Writing your address by hand on twenty envelopes means twenty chances for a typo ā and if you move, twenty corrections. Make one address stamp and use it everywhere; moving house becomes changing the stamp once. | Pasting version literals into pipeline YAML and copy-pasting the install steps into each job, where they drift job by job. | P1 |
| āø3.3 | Derived categorizations, generated from one constant one exported const as the source Ā· SQL CASE built from it, not typed twice Ā· Zod schema and DB column from the same union Ā· test-runner path aliases read from tsconfig, not restated as regexes | The query layer building its bucket expression from the same exported constant the UI renders labels from, so the two are provably identical.š§ ELI5If two friends each keep their own copy of the game rules, the copies slowly drift apart, and one day you're arguing mid-game about who is right. Keep one rulebook and make every other copy a photocopy of it. | Hand-maintaining the same boundaries in application code and again in SQL, then finding out via a chart that is subtly wrong rather than broken. | P1 |
| āø3.4 | Dependency automation that respects your pins .github/dependabot.yml Ā· npm minors grouped weekly, majors separate Ā· github-actions ecosystem grouped (keeps SHA pins fresh) Ā· ignore rules for @types/node and typescript | Grouped weekly updates for minors and patches, majors split into their own reviewable pull requests, and an explicit hold on anything pinned to match the runtime.š§ ELI5Some of your toys are old because you forgot about them; one is old because it's your favorite and you want it exactly like that. The robot that replaces old toys needs a note saying which ones are old on purpose ā or it will 'helpfully' swap your favorite every week. | No automation at all, so SHA-pinned actions and a deliberately-held type package quietly become the oldest thing in the repo. | P1 |
| š¬ Make checks deterministic, and prove they can fail |
| āø4.1 | Typegen inside the typecheck command next typegen && tsc --noEmit Ā· tsconfig includes .next/types/**/*.ts Ā· same rule for drizzle-kit generate, prisma generate, graphql-codegen | A typecheck script that regenerates route and schema types first, so a clean checkout and a warm working tree give the same answer.š§ ELI5Checking yesterday's weather doesn't tell you whether you need an umbrella today. Look out the window right before you leave, every time, as part of leaving ā regenerate the files right before you check them, in the same command. | Typechecking against whatever generated types the last build happened to leave on disk. | P1 |
| āø4.2 | A proof-of-failure test for every custom check a negative fixture per validator Ā· avoid git ls-files ā it omits untracked and newly added files Ā· assert the exact status, not status < 400 | Each hand-written validator shipping with a fixture that makes it fail, run in CI beside it ā so green means it looked and found nothing.š§ ELI5A smoke alarm that has never made a sound might be quiet because there's no fire ā or because the battery is dead. From the couch, those look exactly the same. Press the test button once, so you know it can actually scream. | Shipping a bespoke checker without ever watching it fail, so it can enumerate the wrong set of files and report success forever. | P0 |
| āø4.3 | An import smoke test over real entry points scripts/smoke-imports.ts Ā· tsx, the same launcher production uses Ā· react-server vs default export conditions Ā· runner module stubs hide this: vitest alias, jest moduleNameMapper | A CI script that imports the module graph of every CLI, daemon and scheduled job the way that process actually starts it.š§ ELI5The car starting in the warm garage doesn't prove it starts in the snow. If the snow is where you actually drive it, the snow is where you need to turn the key. Start each program the way it really starts, not the way the test lab starts it. | Treating a green build and a green test suite as proof the program starts. | P1 |
| āø4.4 | A fail-safe changed-files gate .github/actions/changed-source/action.yml Ā· gh pr diff --name-only Ā· allowlist: **/*.md, docs/**, .gitignore Ā· any failure or non-PR event ā source_changed=true | A gate that reports source-changed on an unknown path, a failed diff or a non-pull-request event, and skips only on an explicit docs-only allowlist.š§ ELI5If the shortcut sign is blurry and you're not sure it's really a shortcut, take the long road you know. Being slow and right beats being fast and lost ā especially when nobody is watching to catch the mistake. | Skip logic that defaults to skipping, where a failed diff quietly produces a run that did nothing and looks exactly like one that passed. | P2 |
| āø4.5 | Job-level skip conditions, never workflow path filters jobs.<id>.if: needs.detect.outputs.source_changed == 'true' Ā· never on.pull_request.paths | Every job starting and then deciding immediately to do nothing, so the required context still reports a result.š§ ELI5If the referee stays home, the game can never officially end ā nobody is there to say 'nothing to call today'. The referee should always show up, look around, and then say out loud 'nothing for me to do'. | Filtering the workflow itself by path, which leaves the required check pending forever and the pull request permanently unmergeable. | P2 |
| šļø Durable state needs a version history |
| āø5.1 | Numbered migration files, not create-if-not-exists migrations/0001_baseline.sql Ā· schema_migrations applied-log table Ā· CI job applying from empty against a postgres service container Ā· drizzle-kit if you want generated types with it | An ordered migrations directory, an applied-migrations table, today's schema captured as migration 0001, and a CI job applying all of it to an empty database.š§ ELI5A photo of your LEGO castle shows what it looks like, but not the steps that built it. When you want to add a tower, the photo can't help ā you need the numbered instruction booklet. Migrations are the booklet; keep every page. | Building the schema from idempotent DDL at startup, then applying the first column addition by hand and out of band. | P1 |
| āø5.2 | A language-neutral migration format plain .sql files, one runner per runtime Ā· one shared applied-migrations table Ā· the failure mode of drizzle-kit push / alembic autogenerate | Plain SQL files plus a small runner per runtime, all reading and writing one shared applied-migrations table.š§ ELI5If two people share a room, the room rules have to be written in a language both of them can read. And a robot butler that only knows one person's belongings will throw out the other person's things as clutter. | Pointing a model-diffing generator at a database another runtime also writes to, where every table it cannot see reads as one to drop. | P1 |
| āø5.3 | Harvest the accidental migration log you already have ALTER TABLE ... ADD COLUMN IF NOT EXISTS blocks Ā· split in written order into 0002, 0003, ... Ā· no schema redesign in the same change | The existing conditional ALTER block split into ordered files, in the order it was written ā a mechanical move, not a redesign.š§ ELI5Those sticky-note fixes you re-apply every single morning? That pile is already your instruction list ā it's just unnumbered and stuck to the fridge. Number the notes and put them in a binder, in the order you wrote them. | Scoping the conversion as a schema redesign, when the block of conditional ALTERs running at every startup already is the history. | P1 |
| āø5.4 | Retire the replaced mechanism in the same change delete the ensureSchema() entry point Ā· delete its package script Ā· one writer: pnpm run db:migrate | The change that adds the migration runner also deleting the old setup entry point and its script, leaving exactly one writer.š§ ELI5When you put a new lock on the door, take the old lock off. With two locks and two keys, you never know which door was left open ā and everyone swears it wasn't them. | Landing the new runner and leaving the old path in place for now, since it still works ā which is how the drift gets created. | P1 |
| āø5.5 | An advisory lock around the migration runner pg_advisory_lock(<constant>) before the first statement Ā· pg_advisory_unlock in a finally block Ā· each migration in its own transaction | The runner taking a database-level advisory lock before its first statement, so a concurrent restart waits instead of interleaving.š§ ELI5One bathroom needs one lock on the door. 'We agreed to take turns' works right up until two people are in a hurry at the same time. The lock isn't rude; the lock is how taking turns actually happens. | Relying on a rule about who may apply migrations ā enforced by nobody, at three in the morning, mid-deploy. | P1 |
| š§ Adopt techniques, not vendors |
| āø6.1 | Name the invariant before choosing the vendor versioned migrations ā supabase/migrations Ā· schema invariant tests ā pgTAP Ā· background jobs ā Inngest | Each adopted item written as the invariant ā versioned migrations, tests that assert access-control rules ā with the tool picked in a separate step.š§ ELI5You don't need your friend's exact bike to learn what they learned ā you need a bike. Copy the skill, not the brand. | Rejecting a sound practice wholesale because the hosted product that implemented it elsewhere does not fit this stack. | P2 |
| āø6.2 | Evaluate tools by failure mode, not feature list a destructive-default column in the comparison Ā· would you notice? as a scored row Ā· model-diffing generators drop what they cannot see | An evaluation that records each candidate's destructive default and whether it is detectable, alongside the capability comparison.š§ ELI5Don't just ask what the robot vacuum picks up ā ask what it eats that it shouldn't, and whether you'd notice before your LEGO is gone. What a helper breaks matters more than what it helps with. | Choosing on the feature table, and meeting the destructive default for the first time in production. | P2 |
| āø6.3 | Score bundled benefits one at a time score the query builder, the types, the migrations and the studio separately Ā· mark the ones you would route around | A per-benefit table for each integrated tool, marking which of its linked features this codebase would use and which it would route around.š§ ELI5A meal deal is only a deal if you were going to eat more than the fries. Price each item on its own before you buy the combo ā sometimes you're paying for four things to get one. | Paying the full migration, learning and failure-mode cost of five linked benefits in order to get one of them. | P2 |
| āø6.4 | A rejected-items section inside the audit an Explicitly NOT recommended heading Ā· the reasoning, and the context that would flip it | Declined recommendations living in the same document as the adopted ones, each with its reasoning and the context in which it would be right.š§ ELI5Write down why you said 'no thanks' to the toy, or you'll have the same argument again every birthday. 'We thought about it, and here's why not' is the note that ends the rerun. | Recording only what you adopted, so the same already-considered proposal returns every six months with its reasoning gone. | P2 |
| š¤ The gate's credibility is a social property |
| āø7.1 | Infra-dependent suites on a schedule, not on the gate on: schedule + workflow_dispatch Ā· actions/upload-artifact with if: !cancelled() Ā· the suite's HTML report retained Ā· promote to required only once the database is disposable | The infra-dependent suite running nightly and on manual dispatch with its report kept as an artifact; only jobs that are hermetic on a fresh runner are required.š§ ELI5If the fire alarm goes off every time someone makes toast, people stop looking up when it rings ā and then they miss the real fire. Only alarms that really mean it get to be alarms; the toast-sensitive one gets checked on a schedule instead. | Making a suite that needs live shared infrastructure a required check, and teaching the team that red means try again. | P0 |
| āø7.2 | A port and resource registry instead of a documented table .locks/ claim-and-release directory Ā· a host-global lock so two worktrees cannot collide Ā· shifted port ranges per workspace | Sessions claiming a port from a lock directory at startup and releasing it on exit, so no document has to assign numbers by hand.š§ ELI5Instead of a list on the wall saying which kid gets which swing, let whoever arrives take a ticket from the machine. Nobody argues, nobody has to update the list, and the list can never go stale. | Assigning ports to worktrees in a table in the guide, then adding a standing rule never to kill a running server. | P2 |
| āø7.3 | A shared environment preflight at every entry point assertEnv([...]) called at every entry point Ā· scripts/check-dev-env.mjs before pnpm dev Ā· the error names the variables and links the doc | One assertion helper called at the top of each script, daemon and scheduled unit, naming the missing variables and the doc that explains them.š§ ELI5Check for your shoes, lunch and homework at the front door ā not when you're already on the bus. And the reminder should say 'you forgot your lunch', not just 'something, somewhere, is missing'. | Letting a missing variable surface as a stack trace deep in a library, naming everything except the setting that is absent. | P2 |
| āø7.4 | One living document per subsystem, link-checked in CI docs/ ā one document per subsystem, plus an ownership table Ā· ai/YYYY-MM-DD-*.md stays append-only Ā· pnpm docs:check as its own CI job Ā· markdown AST link + heading-anchor validation | Dated notes staying append-only, plus a small set of current-state documents per subsystem, validated by a link-and-anchor checker in CI.š§ ELI5A diary tells you what happened each day; a map tells you where things are right now. You need both ā and someone should walk the streets once in a while to make sure the map still matches them. | Relying on the dated archive alone, so answering how something works today means replaying it in order and guessing what was superseded. | P2 |
| āø7.5 | Remediation ordered by leverage, with time estimates a Suggested order section with minutes/hours/days attached Ā· a priority column: P0 / P1 / P2 Ā· a status and pull-request column, updated as it lands | The audit shipping as an ordered list: several-minute fixes first with minutes attached, half-day items next, decision-shaped items last and named as such.š§ ELI5Faced with a messy room, start with the toys you can put away in one minute ā the room looks better right away, and now you can see what's actually hard. A giant unsorted pile of chores just gets ignored. | Delivering the audit as one undifferentiated list, which reads as a project and gets deferred as one. | P1 |
| āø7.6 | A pull request only when it buys a review .husky/pre-push ā pnpm run ci and the production build Ā· on: push to main, plus pull_request, in the CI workflow Ā· gh pr create reserved for reviewed changes Ā· one line in the agent guardrails: push directly; no pull request unless asked | A pre-push hook and push-triggered CI as the gate, routine changes pushed directly to the default branch, and pull requests reserved for reviewed work, external contributions and bot-authored dependency bumps ā written down where agents read it.š§ ELI5If you're the only one in the kitchen, you don't write a note asking yourself to taste the soup, wait for yourself to read it, and then sign off. You taste it and serve it. You write the note when someone else is going to taste it. | Opening a branch and a pull request for every one-line change on a solo repo, then spending more tokens creating, polling, describing and merging it than the change itself took. | P1 |
š¤The gate's credibility is a social property
A pipeline's real output is trust. Once people learn that a red check usually means nothing, you have lost the signal even though every job still runs.
Infra-dependent suites on a schedule, not on the gate
P0A required check that goes red because of network flakiness or shared infrastructure teaches the team to ignore red, which costs you every other check too.
If the fire alarm goes off every time someone makes toast, people stop looking up when it rings ā and then they miss the real fire. Only alarms that really mean it get to be alarms; the toast-sensitive one gets checked on a schedule instead.
This is the most expensive mistake in the list, because the damage is not confined to the flaky job. Suites that need live external services, real data volumes, or long timeouts belong on a schedule and on manual trigger, where a failure is investigated rather than retried. Promote them to required status only once they can run against disposable infrastructure that the pipeline itself controls. Keep the HTML report as an artifact with a not-cancelled condition, so the overnight failure is still debuggable in the morning.
A suite added as a merge gate read a live database over a public proxy; from a hosted runner the first request exceeded a 60-second timeout on all three attempts. It was moved to nightly plus manual dispatch.
A port and resource registry instead of a documented table
P2When your documentation assigns each developer or worktree a distinct port, number, or directory by hand, that is an unautomated lock.
Instead of a list on the wall saying which kid gets which swing, let whoever arrives take a ticket from the machine. Nobody argues, nobody has to update the list, and the list can never go stale.
Conventions like this are a reliable signal that a structural fix is missing, and they fail exactly when the project is busiest ā the moment two sessions run at once. A lock file, a port registry, or a workspace that allocates its own resources removes the class of problem instead of documenting it. The full hermetic-environment version is a large project, but the coordination primitive underneath it usually is not.
The audited guide assigned each worktree its own dev-server port by hand and added a standing rule never to kill a running server ā two manual workarounds for one missing lock.
A shared environment preflight at every entry point
P2Check required configuration at startup and exit with the exact list of what is missing, plus where to get it.
Check for your shoes, lunch and homework at the front door ā not when you're already on the bus. And the reminder should say 'you forgot your lunch', not just 'something, somewhere, is missing'.
Systems with many entry points ā scripts, daemons, scheduled units ā fail deep inside a call stack when a variable is absent, and the stack trace names a library rather than the missing setting. A shared assertion helper called at the top of each entry point converts that into a one-line failure that says what to do next. It is cheap, and it matters most for the unattended processes nobody is watching.
One living document per subsystem, link-checked in CI
P2Dated, append-only notes are true as of their date; you also need one living document per subsystem that is true now.
A diary tells you what happened each day; a map tells you where things are right now. You need both ā and someone should walk the streets once in a while to make sure the map still matches them.
Both forms are valuable and they are not substitutes. The journal preserves why a decision was made and what was known at the time; the living document answers what the pipeline does today, without requiring a reader to replay the archive in order and guess which parts were superseded. Once the living documents exist, a link-and-anchor checker is a small script that keeps them from rotting quietly ā roughly a hundred lines that parses each markdown file and resolves every relative link and heading anchor.
Remediation ordered by leverage, with time estimates
P1Order remediation so the one-line fixes land before the ones that need a design decision, and attach a time estimate to each.
Faced with a messy room, start with the toys you can put away in one minute ā the room looks better right away, and now you can see what's actually hard. A giant unsorted pile of chores just gets ignored.
An audit that arrives as an undifferentiated list gets deferred as a project. The same audit ordered by effort ā with the several-minute items first, the half-day items next, and the ones requiring a decision last and named as such ā gets started, and the early wins make the rest easier to justify. The estimate is also the honest way to surface that most of the value is usually in the first afternoon.
In the source audit, the first seven items totalled roughly half a day and were what converted 'the hooks probably ran' into 'the main branch is provably green'.
A pull request only when it buys a review
P1Open a pull request when a person will review it or the change comes from outside the repo; otherwise push straight to the gated branch, because a pull request nobody reads costs time and tokens and buys nothing.
If you're the only one in the kitchen, you don't write a note asking yourself to taste the soup, wait for yourself to read it, and then sign off. You taste it and serve it. You write the note when someone else is going to taste it.
The gate is the pre-push hook plus the workflow that runs on every push. A pull request adds exactly two things on top of that: a reviewer and a merge button. When nobody is going to review, the branch, the description, the wait for checks that already ran locally, the merge and the branch deletion are ceremony, and for an agent each step is a round trip that burns context on the same green result. Required status checks stay in place for the day a pull request does exist; they are not a reason to create one. The rule has to live in the guardrails file, because an agent left to its defaults will open a pull request for a typo.
The repository serving this page has no merge commits: every change, including the one that added this technique, went to main through the pre-push build and the push-triggered workflow. The pull-request path exists for its dependency bot and for anyone outside the repo.