Skip to content

Are We Drifting? — Part 14: The Wall of Gates and What Comes Next

Are We Drifting? — Part 14: The Wall of Gates and What Comes Next

Section titled “Are We Drifting? — Part 14: The Wall of Gates and What Comes Next”

Every layer of the stack turns out to be the same move: one declarative source → many generated keep them honest. This part collects the gates built so far, grades them on the ladder, answers the title, and points at where the pattern goes next.

A guardrail that does not run is theatre. So here is the actual set of checks that run — most on every pull request — each tagged with its tier and the part of the series that covers it:

GateWhat it catchesTierSeries
var _ ServiceHandler = (*Service)(nil)handler drifts from generated interface1 — compilePart 11: The FE↔BE Mirror
tscexhaustive matchState, typed everything1 — compilePart 11: The FE↔BE Mirror
liftmere/no-fixture-outside-routefixture imported outside route/story/test2 — lint-errorPart 12: Testing Without Drift
liftmere/no-raw-hex-color / no-raw-tok-varsraw color values in component code2 — lint-error (ambient)Part 11: The FE↔BE Mirror
liftmere/no-uistate-screen-propparallel uiState prop on a screen2 — lint-errorPart 11: The FE↔BE Mirror
liftmere/no-null-suspense-fallbackblank first paint in the router2 — lint-errorPart 11: The FE↔BE Mirror
liftmere/no-smart-quotescurly quotes in TS string literals2 — lint-error (ambient)Part 1: The Drift Problem
liftmere/no-direct-event-log-appendraw event-log writes outside the sanctioned writer2 — lint-errorPart 8: Projected Truth
buf lintproto contract stays well-formed2 — lint-errorPart 11: The FE↔BE Mirror
make check-vocabcommitted .gen.go drifts from its YAML manifest3 — CI scriptParts 3–6
make check-sqlcgenerated DB models drift from hand-authored SQL3 — CI scriptPart 7: Models at Boundaries
make check-mocksgenerated mocks drift from their interfaces3 — CI scriptPart 12: Testing Without Drift
check-connect-errorsconnect.NewError called outside the error adapter3 — CI scriptPart 5: The Error Manifest
check-sql-scopeowner-scoped table queried without a user_id filter3 — CI scriptPart 7: Models at Boundaries
check-migrations.shmigration file edited after applied3 — CI scriptPart 8: Projected Truth
generate-features --checkcommitted feature registry is stale3 — CI scriptPart 11: The FE↔BE Mirror
defineMcpServerContractMCP tool surface diverges from the operations registry3 — contract testPart 9: One Operation, Three Interfaces
make agents-checkrendered agent file diverges from its manifest3 — CI scriptPart 10: The Agent OS
rebuild-from-logany projection is reproducible from the event logrecoverablePart 8: Projected Truth
liftmere/no-fixture-outside-route (burndown)~113 remaining violations4 — warn (scoreboard)Part 12: Testing Without Drift

No single one of these is clever. Together they are the difference between “we have a convention” and “the convention is true.” Each removes a coherence check a human would otherwise perform by eye — and each is the reason a large, AI-generated diff can be trusted instead of re-audited.

Not every rule sits at the same strength, and pretending otherwise would itself be drift — between what a doc claims and what the build enforces. So every guardrail carries a tier on a shared :

1 compile-enforced the compiler/type system catches it — cannot ship
2 lint-error a rule at error — PR fails CI
3 coverage-script a CI script fails on a missing pairing or forbidden diff
4 warn (scoreboard) reported, non-blocking, during a burndown to zero
5 convention documented but unenforced — a known gap, a future target

The tier tells you the real status of a rule at a glance. A sub-system at tier 5 is honest about being a wish; one at tier 1 cannot be violated. The frontend mostly lives at tiers 1–2; the backend has a genuinely-gated core and a set of sub-systems still climbing from convention toward a wired linter. The point of the ladder is not to claim everything is enforced — it is to make the gaps legible so they can be closed on purpose.

Strength is one axis; timing is the other. A gate runs at some point in the life of a change — and the earlier it fires, the cheaper the failure.

When the gate fires A gate can fire at commit-time, at CI, at compile, or at runtime; the cost of the failure it catches rises the later it fires, so commit-time is the cheapest place to catch drift. cost of the failure → pre-commit hooks commit-time seconds · local the lint umbrella CI / PR minutes · remote the type system compile in the build a request, a boot runtime production cheapest an incident earlier ───────────────▶ later
The timing axis of the wall — conceptual, not measured. The earlier a gate fires, the cheaper the failure it catches.

The same gate is a different cost at each tier. A migration mistake refused on git commit costs the developer ten seconds; the same mistake caught at runtime is an incident. So the wall is not only a set of rules — it is a set of rules placed as early as each one can run.

The earliest tier is the pre-commit subsystem: a handful of hooks that fire on every commit, before code reaches CI — a secret scan, buf lint, migration discipline, the connect.NewError boundary check, the owner-scope check. Several of them appear in the wall above and run again in CI on purpose: the commit-time copy exists for the feedback loop, the CI copy for the guarantee, so bypassing local hooks still gets caught at the PR. The full commit-time tier — every hook and why it earns its place at the front — is its own reference: Commit-time gates.

A gate’s value is inseparable from when it fires. Move the check as early as it can run, and the expensive version of the failure never happens.

On every layer where a gate runs: no, structurally. A value cannot exist on one side that another side has not heard of. A generated file cannot disagree with its manifest. A handler cannot diverge from its contract. An agent cannot behave differently across frameworks. A projection cannot win an argument with the event log. Those are not promises maintained by discipline; they are properties maintained by the build, on every PR, by machines rather than memory.

On the layers where a gate does not yet run: maybe — and we know exactly where. That is what the ladder has been tracking the whole way, and what the next section builds on.

Here is the frontier the whole pattern points at — the next set of seams that want to become one source with many projections and a gate.

  • The capstone: a frontend form and its backend validation from one manifest. This is the unbuilt piece the pattern keeps pointing at — the one place where the shape exists in every adjacent layer but has not yet been closed. The plumbing is already there — operation inputs are Zod schemas, zod-to-json-schema is in use, the vocabularies generate validators — and the natural extension is to generate a form and its server-side validation from a single declaration. When that lands, the tightest FE↔BE seam there is — “what may the user submit, and what will the server accept?” — becomes one source with two projections and a gate. That is where this goes.
  • More vocabulary kinds. permission, route, and event are sketched in buildmere’s plugin contract, and bringing them under the same manifest is the next reach; permissions and routes are governed by other means today.
  • More language emitters. The error kind emits Go; a TypeScript emitter generates the frontend’s error constants and closes the Part 5: The Error Manifest coupling completely. The same move applies to a metric TypeScript emitter.
  • buf breaking as a blocking gate. The rule is configured; wiring it to fail CI while the proto is mid-rename is the next turn of the ratchet.
  • Production observability. Config declares an OTLP endpoint, and the direction is full production OTLP with real spans built on top of the first tracing cut.
  • A devportal-state CLI fallback and a pair of platform adapters (AGENTS.md, a Cursor _platform.mdc) extend the agent-OS pattern to the next surfaces.

Step back to where Part 1: The Drift Problem started. Two forces make this the right architecture for this moment, not just good hygiene.

The rails unlock velocity. When writing code and recalling patterns are cheap, the scarce resource is coherence. The gates above are what let you accept a large generated diff and trust it, because every way it could drift is a way the build refuses. The speed is not the typing — it is the not-having-to-recheck.

And the rails got cheap to build. The old reason this kind of governance was rare is that a manifest system, a custom linter, a codegen kernel, an event-projection plane were expensive to build and maintain. The same economics that made generation cheap collapsed that cost: a new vocabulary kind, a new lint rule, a new gate is now an afternoon. The force that created the drift problem is the one that funds the cure.

Constraints are not the brake on cheap generation. They are what makes it safe to trust — and trust is what lets you go fast. “Governance is expensive” is an old-world assumption that no longer holds.

“Are we drifting?” is not a question you answer once. It is asked continuously — by check-vocab on every commit, by the contract test on every MCP boot, by agents-check on every sync, by the compiler on every build. The work was never to answer it by hand. The work was to build the gates that ask it for you, forever, and to be honest about the few places they do not run yet.

Define the concept once. Generate the rest. Let the gates keep them honest. That is the pattern — and as the system grows, each new layer joins the same shape. This corpus grows with it: the question does not change, the gates keep answering it, and new parts land as new seams come under the same discipline.