A rule that cannot fail is a suggestion
Our repository carries an architecture guide: layered boundaries, a single owner per piece of state, a rule that services are defined before they are implemented, a rule that every layer earns its existence. It is a good guide. It is also prose, and prose cannot fail a build.
The failure mode was not that people disagreed with it. It was that code drifted from it quietly — a module placed one level too deep, a second writer for a value that was supposed to have exactly one, a logger constructed per-file instead of in the designated owner. Each of those is a small, defensible-looking edit. Each would have been caught by a reviewer who already had the whole rule in mind, and missed by one who did not, in a diff that had six other things in it.
Then the tooling changed the arithmetic. A large share of the code in this repository is now written by an automated editor rather than typed by a person. That does not make the rules less true; it makes the gap between "rule exists" and "rule is followed" much larger, and much faster to accumulate. An editor does not get tired at the fortieth file in a diff. It does not read a contributor guide. And it cannot be told "hey, this one is a real violation" in a code review, because there is no code review — there is a tool call that either succeeds or does not.
So the question stopped being "how do we remind people" and became "how do we make a rule into something a tool can fail on?"
Give every invariant a manifest
The answer is a directory per invariant, containing a manifest that a runner loads and enforces. We call each one an arbiter — something that arbitrates a single rule, and can be asked to explain itself. There are thirty-three of them today. Each looks roughly like this:
schema_version = 1
id = "analytics-autocapture-interaction-granularity"
risk = "high"
paths = [
"Foundation/iOS/.../AnalyticsReporterService/.../Autocapture/**/*.swift",
]
invariant = "One $autocapture event represents one user intent, never one UIKit target-action dispatch. ..."
Five fields carry the weight, and each answers a different question:
paths— which files this rule governs. Scoping is not a detail here. A rule applied too broadly stops being a rule and becomes noise, because the first false positive teaches everyone to ignore the mechanism.invariant— the rule, in one paragraph, in prose a human can argue with. It has to be true and complete on its own, because it is the first thing a machine will not be able to check and a human will have to.read— the files to load before touching this area. Context is part of the contract. An invariant that can only be honored by reading three files should say so, rather than assuming the editor already knows.forbid— the substrings that, appearing in an added line, violate the rule.checks— which language-specific gates to run after the edit lands.
The invariant text above is worth reading in full, because it is written the way this whole system wants them written. It does not say "avoid over-capturing." It says what one event means, and then it states the incident: reporting per dispatch is what made a single slider drag emit four-fifths of a product's entire telemetry, evicting real business events out of the upload queue under budget pressure. A rule with a number in it can be defended by someone who disagrees with it. A rule that only says "be careful" cannot.
Addressed to the line, not the diff
The property that makes this work for an automated editor is that forbid globs match added lines, case-insensitively, at the moment of writing — not a diff, not a review, not a later audit.
That is the only point at which a machine can still be stopped. A linter that runs on a diff has already let the change exist; a reviewer sees it too late to be cheap. A rule that fires on the line you are typing can be respected without anyone knowing they respected it.
It also inverts the usual burden. Under review-based enforcement, the person who knows the rule is the person who has to remember it, and the person who has to remember it is often the one with the least context — the reviewer skimming a six-file change. Under line-based enforcement, the rule travels with the path. Touch a file under an arbiter, and the arbiter comes with it.
The other half of the machinery is asking the same question in advance: given a set of paths, which invariants are at stake, and which checks do they require? An editor can ask that before it writes anything, and load the read list first. You cannot enforce a rule you never showed, and you cannot show thirty-three rules at once.
The inconvenience that is the point
Two properties of the matcher look like flaws and are not.
Comments and strings count. The glob matches whole lines, so a comment that mentions a forbidden API is itself a hit. The consequence is that prose about these functions has to refer to them indirectly — the comment above the offending line in our own rule files says "the poller APIs" rather than naming them. It is mildly annoying to write.
It is also exactly the property you want. An editor that can write a convincing comment has, given any rule that only matches code, a way to talk its way past it. Closing that door costs a few sentences of awkward phrasing and buys a rule that cannot be argued with in-band.
Blast radius is measured. Our first drafts used broad globs, because broad globs are easier to write and easier to imagine. They were wrong. The progress monitor in the analytics code has helper methods whose names contain the forbidden substring even though they have nothing to do with the forbidden call, so a bare substring match flagged files that were entirely innocent. The fix was to pin the call shape rather than the word, and then to check the result against the trees:
Measured against every Go file under the three trees: together they select the nine known copies and nothing else.
That sentence is in the rule, and it is the standard every glob in the system is held to. A rule that fires on files it should not is not a strict rule; it is a bug with a red build attached, and its first cost is paid by whoever hits it on an unrelated change and concludes the whole mechanism is broken.
The other half: rules that check shape, not substrings
Not every invariant is expressible as a forbidden substring, and pretending otherwise produces rules that look enforced and are not.
For those, the mechanism is a check rather than a glob. Whole-tree structural rules live in a separate table, and each one is required to name the incident it protects:
[[rule]]
id = "LOGGER-SINGLE-OWNER"
kind = "forbid_pattern"
source_view = "code_only"
incident = "…the rule this exists to prevent is named here, not described in the abstract"
The requirement that every rule carry an incident is doing quiet, heavy work. A rule nobody can name a failure for is a rule nobody will defend when it becomes inconvenient — and it will become inconvenient, because rules that fire on the refactor someone is in the middle of always do. Keeping the incident attached means the person who wants to weaken the rule has to delete the evidence of why it exists, in the same file, where the diff will show it.
The implementation is a Rust workspace of roughly seventy thousand lines across several hundred files, entered through a single command, with a bootstrap that discards any ambient build configuration and loads only its own — so that a developer's personal Bazel settings cannot change what a guard decides. That is a small thing that took real thought: a policy gate whose result depends on someone's ~/.bazelrc is not a policy gate, it is a lottery.
Where it stops
Being honest about the boundary is part of making the rest credible, and this one bit us. The release-signing rule is marked critical, which reads like a secret scanner. It is not. It is a check over the shape of the change — did you delete the profile this build needs — and it would have reported green while six signing private keys sat in the repository, because a mutation check and a credential scanner are entirely different instruments. We found them by accident, and fixing them is the subject of a different post.
The lesson generalizes past secrets. A mechanism with a reassuring severity label is easy to over-trust, precisely because it has been earning trust on other axes. If your gate says "critical," it is worth asking what class of problem it can actually see.
Takeaways
If a rule can only be read, it will eventually be drifted from. The upgrade path is not a better-written paragraph; it is giving the rule a manifest — the paths it governs, its one-sentence text, the context to load first, and the shapes it forbids — so that a tool can act on it.
Point enforcement at the line being written, not the diff being reviewed. That is the only moment a machine can still be stopped, and it moves the burden from the person with the least context onto the path being touched.
Assume the party writing the code will try to comply by writing words. Matching comments and strings costs you a little phrasing and closes the most obvious bypass.
And measure the blast radius. Check every glob against the files it governs before you ship it, and write the result into the rule. A guard that fires on innocent files does not cost one bad report — it costs you the mechanism, permanently, because nobody overrides a rule they have learned to route around.