Skip to content

Are We Drifting? — Part 1: The Drift Problem

Are We Drifting? — Part 1: The Drift Problem

Section titled “Are We Drifting? — Part 1: The Drift Problem”

Pick any concept your system knows about.

A project status. An error. A permission. A config key. The shape of a “create workout” request.

Now count how many places that one concept is written down.

The database has a column for it. The domain model has a field. The API contract has a message. The frontend has a TypeScript type, a dropdown, a validation rule. The event log has a payload. The docs have a table. A test has a fixture. An agent has an instruction.

That is not one concept. That is one concept and a dozen copies of it.

And copies drift.

Are we drifting? is the only question that matters once code is cheap to produce. This series asks it of every layer in our stack and shows the same answer each time.

is what happens when two representations of the same concept stop agreeing.

It is rarely dramatic. It is a slow accumulation of small disagreements:

  • The backend rejects an enum value the frontend dropdown happily offers.
  • An error means 404 in one handler and 409 in another, because someone guessed.
  • The DB column is snake_case, the API field is camelCase, and a mapping function in the middle silently swallows the one that was renamed.
  • A screen imports a fixture that no longer matches the real product state.
  • A handler quietly diverges from the contract it was generated against.
  • The docs describe a flag that was removed two sprints ago.

None of these is a crash. Each is a small lie the codebase tells about itself.

Here is what those lies look like when they are actually caught — by name, in this codebase:

The driftThe gate that catches it
A screen imports its own fixtureliftmere/no-fixture-outside-route — lint-error, PR fails
A component uses #0f0f17 instead of a design tokenliftmere/no-raw-hex-color — lint-error, ambient
A handler returns fmt.Errorf("not found") directlycheck-connect-errors — CI script, build fails
A committed .gen.go drifts from its YAML manifestmake check-vocab — buildmere drift gate, exits non-zero
A generated feature registry goes stalegenerate-features --check — codegen gate, PR fails
A migration file is edited after it was appliedcheck-migrations.sh — append-only enforcement
A handler’s interface assertion fails after a proto changevar _ ServiceHandler = (*Service)(nil) — compile error
A smart quote ' slips into a TS string literalliftmere/no-smart-quotes — ambient lint-error

Each row is a real rule in this repo. Each one was built because the drift it catches had already happened, or was structurally guaranteed to happen once generation got fast enough.

Three of those rows have stories worth naming.

liftmere/no-smart-quotes exists because a curly quote — ' (U+2018) — slipped into a TypeScript string literal inside a fixture file.

The JS parser accepted it silently; the rendered copy looked correct to the eye.

The corruption only surfaced when the fixture value was compared programmatically.

The rule is ambient — it runs across all source, not just component files — because the failure mode requires no special structure to trigger: any file, any context, same silent breakage.

liftmere/no-fixture-outside-route fires today at warn, not error, because there are still ~113 violations in apps/client/src.

The rule exists and the violations are known; the flip to error is gated on reaching zero, and the count is a number a single eslint run prints — the burndown is visible, not a guess.

The frontend’s burndown is legible. The backend is leaner, and the next guardrails land there.

On the backend, exactly one sub-system is genuinely gated today: the compile assertion var _ WorkoutServiceHandler = (*Service)(nil) in handler.go.

Every other guardrail in the table — buf lint, the forbidigo ban on bare fmt.Errorf, depguard for domain layering — is documented convention that does not yet run in CI.

The frontend runs 13 custom ESLint rules on every PR; the backend runs one compile check.

That asymmetry is not a confession — it is an ordered work list. The code already follows the conventions; the work is to make them fail the build, by wiring buf lint, the forbidigo error ban, the depguard layering rules, and sloglint into the same CI gate the frontend rules already run in. The conventions are real today; turning each into a gate is the next increment, and the backend guardrails come in that order.

Part of why the gap exists is cost: ESLint rules are file-granular and syntactic — an hour of work, precise triggers.

Go’s equivalents (go/analysis analyzers, most golangci-lint linters) are package-granular and often need type information, which puts a backend guardrail closer to a day of work than an hour.

A documented rule that doesn’t run in CI is theatre. A rule that fails the build is a guardrail. The work is turning the first into the second, backend first.

The same pattern that makes the frontend rules fine-grained — opt-in by structure, dormant until you adopt the shape — holds on both stacks; it just costs more per guardrail on the backend side.

The cost is not the individual bug. The cost is that, after enough of them, nobody trusts any single representation to be authoritative — so every change requires re-checking all of them by hand.

Software used to be bottlenecked by how fast humans could write code.

That bottleneck made deep governance feel expensive. Writing a linter, a code generator, a schema registry — these were investments you made only at significant scale, because the thing they protected against (drift) accumulated slowly, at human typing speed.

Agentic workflows remove that bottleneck.

Agents make generation cheap. They make refactoring cheap. They make the repetitive infrastructure that surrounds a feature cheap.

But cheap generation does not reduce drift. It accelerates it.

An agent can produce six new representations of a concept in the time it used to take a human to write one — and unless something forces those six to agree, the agent has just multiplied the surface area where they can disagree.

When generation is cheap, the advantage moves from how fast can we write code? to how reliably can we keep it all agreeing with itself?

This is the thesis of an earlier post, Vocabulary-Driven Governance. That post argued the case. This series is the proof: the concrete, shipped systems we use to answer “are we drifting?” with no on every layer we can reach.

The instinct is to file all of this under “quality” — a tax you pay to keep the codebase tidy, drawn from the same budget you would rather spend on shipping.

In the agentic era that instinct is not merely incomplete. It is backwards, for two reasons.

First: the rails are what let you move fast.

When writing code is cheap, and the patterns you would have reached for are general knowledge an agent already holds, typing and expertise stop being the bottleneck.

What stays scarce is coherence — whether the thing you just generated still agrees with the dozen others that describe the same concept.

Cheap generation does not pay that cost down. It runs the bill up faster, because there is more being produced that can disagree.

So the teams that move fastest are not the ones generating the most code. They are the ones who can generate confidently: structure says where a thing goes, a linter refuses the shapes that would drift, and a manifest is the one place a concept is defined while the many places it appears are produced from it.

Those three let you accept a large diff from an agent and trust it, because anything incoherent fails at PR time instead of in production three weeks later.

That trust is the velocity. Not the typing speed. The not-having-to-recheck.

Second: the rails themselves just got cheap.

The old reason deep governance was rare is the one named above — building and maintaining it was expensive. A custom linter, a code generator, a schema registry, the manifest plumbing: real engineering cost, justified only at scale.

That assumption no longer holds.

The same economics that made generation cheap also collapsed the cost of the cure — and that inversion is why now is the right moment, not just a good one. An agent can write the lint rule, add a codegen kind, wire the --check gate, and backfill the manifest in an afternoon.

So the calculus inverts twice over: the structure is now cheap to lay and cheap to maintain, and it is the thing that unlocks the speed. The same force that creates the drift problem is what funds the solution.

The cost of building the rails, then and now Building a custom linter, a codegen kind, or a drift gate used to take weeks to a quarter; cheap code generation collapses each to hours or an afternoon. then — before generation was cheap now A custom linter weeks hours A codegen kind a quarter an afternoon A drift gate days minutes
Conceptual — relative effort, not measured data.

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.

Keep this in view through every part that follows: each guardrail we describe earns its place by removing a manual coherence check, so the cheap part — the generation — can run at full speed without leaving a mess behind it.

There is one pattern underneath everything that follows. It does not change from layer to layer; only the materials change.

One declarative source
-> many generated projections
-> drift gates keep them honest

Read it as three commitments:

  1. One source. A concept is declared once, in a place that is obviously canonical — a manifest, a contract, a single definition. Not written by hand in twelve places.
  2. Many projections. Every representation the system needs — Go types, TypeScript constants, validators, SQL constraints, docs, agent rules — is generated from that one source. A projection is never the truth; it is a view of the truth.
  3. . A check runs in CI that fails the build if any committed no longer matches what the source would generate. The gate is the load-bearing part. Without it, “generated” is just a suggestion.

A concept that follows this shape cannot drift, because there is only one place it can be changed, and the gate refuses to let the copies disagree with it.

That is the whole game. The rest of this series is what it looks like at each altitude.

The same shape recurs at three very different levels of the stack. Watching it repeat is the point.

  • Vocabularies. A closed, named set of values — an enum, an error, a config key, a metric — declared in a YAML manifest and projected into Go, TypeScript, Zod, JSON Schema, SQL, and docs. Our toolkit for this is called buildmere, and it is a kernel with pluggable “kinds.” Parts 2 through 6.
  • Operations. A single operation, defined once with its input and output schemas, exposed simultaneously as a REST endpoint, an MCP tool, and a CLI command — one handler behind all three. This is the op-server layer. Part 9.
  • Agents. A single agent manifest, rendered into the formats Claude Code and Cursor each expect, with provider-agnostic execution tiers resolved at dispatch time. This is the Agent OS. Part 10.

Between them sit two more pieces: projected truth — read models rebuilt from an append-only event log, where the log is the source and every table is a projection (Part 8) — and the edge of the pattern, where the shape goes next and what it unlocks (Part 14).

This series shows the pattern and the trail markers for where it goes next.

When a guardrail runs in CI, you will see the rule that fails the build. When a guardrail is the natural next step, you will see exactly what it looks like and where it lands.

The shape — one source, many projections, drift gates — is a direction we have walked a long way down, and the trail keeps going.

Part 2: The Tagged Vocabulary starts at the smallest, most concrete unit: the tagged vocabulary — the humble idea that a closed set of named values, declared once, is the atom the entire pattern is built from.

Everything else is that idea, scaled up.