Prove and Admit, Don't Just Lint
Prove and Admit, Don't Just Lint
Why executable rules are the easy half, and why the checks around agent-written code must require presence, report their blind spots, and be written as a standard rather than collected as rules.
Sharing is caring: Share on X · Share on Bluesky · Comment on Hacker News
Part of the series: Encode the System · View all parts
Previously in this series: Enforce Architecture, Don't Trust Intent
Central idea: Everyone now agrees that rules must be executable, and the tools that ship are linters wired to agents. That is the easy half. What a codebase needs, once agents write most of it, is a standard that says what "right" means for it, proves it on every change by naming what it proved, and says what it did not look at. The last part is the one nobody ships, and it is the one that decides where a reviewer's attention goes.
The short version: 2 min Full article: 16 min
The short version
The previous article argued that an architecture is only real when its rules are executable. That argument has since become the consensus, faster than I expected. Instruction files for coding agents gave way to hooks; boundary checkers plugged themselves into the agent's loop; the phrase "an instruction is not a guardrail" is now said by the vendors themselves. Good. It is also, almost entirely, the linter half of the problem.
A linter looks for the presence of something forbidden. When it finds one, it points at a line. When it finds nothing, it says nothing, and that silence means exactly one thing: nothing found. It does not mean the validation is there. It does not mean the port has its adapter. It cannot mean that, because a linter never listed what it expected to see.
Three things separate a standard from a linter. It requires presence, which takes a model of what should exist. It reports the complement, which takes an enumeration of what it looked at. And it is written, not collected: someone owns the model, and that someone is an engineer writing down what "right" means for this codebase, once.
In three ideas
-
Silence is never proof. A check that reports nothing has told you what it found, not what is there. The value of a standard is that it leaves no silence: for every guarantee it declares, an explicit state.
-
Presence needs a model, and the model is the standard. You cannot detect a hole without knowing the shape. That shape is not a linter configuration; it is the thing engineers should be writing instead of reviewing agent output line by line.
-
Admit within your vocabulary. A standard can only confess the absence of what it declared. A boundary nobody wrote down is not "not looked at"; it is not on the map. Saying so is part of the standard.
Takeaway
Do not ask what your checker found. Ask what it looked at, and what it would have said if it had found nothing.
Full article
The last article ended on a claim: an architecture that lives in documents is a belief, an architecture that lives in executable rules is a property. I stand by it. But a property of what kind? Reading what has shipped around that idea over the past year, I think the claim was too easy to agree with, and that the agreement hides the harder part.
Here is what shipped. Architecture-as-code libraries for TypeScript now run as a hook after each agent iteration, feeding the violation back to the agent as deterministic feedback. Diff-based architecture linters ship with severity, remediation text, and an MCP server so the agent can query them. Academic work compiles the free-text rules of an agent instruction file into AST queries and shell shims, and measures compliance. A column in a major publisher's radar calls for an "engineering governance layer" that makes architectural decisions machine-readable and enforceable, and concludes that no such tool exists.
Every one of these is prohibition-shaped. Every one of them looks for a forbidden presence and points at a line. And that is not a coincidence or a failure of imagination. A prohibition can be checked on the import graph, for free, with no model of the system. An obligation cannot. So the tooling went where the checking was cheap, and the consensus formed around the half that was easy to build.
This article is about the other half, and about what it costs.
A linter's silence
Take the most ordinary guarantee in a web application: input is validated at the boundary.
Run a linter over a codebase with no validation at all. Zero findings. Run it over a codebase where every server function parses its input against a schema. Zero findings. The two codebases are indistinguishable to the tool, because the tool has no rule that could fire in either case. It has rules of the form this thing must not be here. The guarantee we care about is of the form this thing must be here, and its violation is a hole.
This is the distinction the previous article named: and , divergence and absence. What I did not say strongly enough is what it does to the meaning of a green result.
When a prohibition checker is green, it has told you that a certain class of bad things is not present in what it scanned. That is real information, and it is worth having. But it says nothing about presence. A codebase can pass every prohibition and have no validation, no wiring, no adapter behind half its ports. The system looks done and is not wired, and every tool in the loop is green, because every tool in the loop examines what is there and the defect is what is not there.
So the first move is to stop reading silence as reassurance. A tool's silence is never proof of anything. If you want proof of presence, the tool has to say, out loud, I looked for this, and it is there.
Presence needs a model
Which brings the cost. To say I looked for this and it is there, the tool must know what to look for. Something has to declare that a port is expected to have an adapter, that a server function is expected to carry a validator, that a form is expected to bind a schema. That declaration is a model of the system: its scopes, its concepts, the edges that should exist between them.
Linters do have obligation-shaped rules, and it would be wrong to claim otherwise. You can require a JSDoc block, require await, require that modules matching one pattern depend on modules matching another. But look at what those rules know: a file, a naming convention, a pattern. They are local. The obligation that matters, every port in the core has a wired adapter in the infrastructure, ranges over the system, and a linter has no system. It has files.
The model is therefore not a linter configuration file. It is a description of what "right" means for this codebase: which boundaries exist, which contracts sit on them, which parts must be present for a feature to count as real. It has to be written by someone who knows the system, and maintained by someone who owns it.
I want to be direct about who that someone is, because the whole scene is quietly wrong about it. The current framing gives engineers rules to collect: presets per framework, packs of best practices, cheat sheets of lint rules for agents. What engineers should be given is a standard to write. Engineers like writing things; they have always written the architecture, in diagrams and documents that could not refuse a commit. The change is not that they stop. The change is that what they write now executes.
That is the object this series has been circling without naming: an . Not a framework, because you are measured against it, you do not build inside it. Not a harness, because it does not run your agent; it plugs into whatever does. Not a linter, because a linter says what it found and a standard says what it proved. It is the engineer's description of the system, made into the thing that judges every change.
One word of placement, because the next article will use another term. The standard is what the engineer writes: the declared model and its rules. What verifies it is a composition, typechecker, schemas, tests, audits, traces, which the next article calls the guarantee system. The standard is the declared part of that system; it is not a tool, and no single tool is it.
Where an obligation is decidable: the seat
An obligation is only checkable if there is one place to look. I call that place the : the single, statically findable site where a guarantee attaches. The input validator of a server function. The validators of a form. The schema of a route's search params. The constructor of a domain type. The transition function of a state machine.
The idea has older names. Aspect-oriented programming called such places join points, and the query that selects them a pointcut; Michael Feathers called the places where behavior can be changed without editing them seams. A seat is a join point chosen for a guarantee, and one the library made explicit on purpose.
A seat is a property of a library's API design, not of any tool. A library that puts its validation in a callback buried in a closure, or leaves it to convention, or lets it live "somewhere in the handler", has no seat, and no obligation about it is decidable without running the code. This is a criterion for choosing libraries, and I think it is the right one: not "is it popular" but "does it have a seat for the guarantee I need, and how many seats will I have to build myself".
When the library has no seat, you make one. fetch has none: the response parse is ad hoc, everywhere. So the codebase gets a thin client that takes a schema, and a prohibition that says no fetch outside it. The wrapper is the seat, and the prohibition closes the chain. Thin, deletable by inlining, and, if a library later grows the seat, gone.
Here is what a seat looks like when the library has one. A TanStack Start server function carries its input contract in one place, and the handler receives the parsed value:
TypeScript
const createArticle = createServerFn({ method: "POST" })
.inputValidator(CreateArticleInput) // the seat, and the contract attached to it
.handler(async ({ data }) => { // `data` is the parsed value, nothing else
return articles.create(data);
});Three things are decidable from that text without running it: a validator is attached, it is applied before the handler, and the handler consumes what the parse produced. Compare the version that satisfies a weaker rule:
TypeScript
const createArticle = createServerFn({ method: "POST" })
.inputValidator(z.any()) // a validator is "present"
.handler(async ({ data, context }) => {
const body = await context.request.json(); // and the raw body is read anyway
return articles.create(body);
});Now the part that makes obligations honest. An obligation alone is gameable by an empty presence. "A validator is attached" is satisfied by a schema that accepts anything. So every seat carries three things, not one: the obligation that the contract is there, the obligation that it is applied (parsed, not merely declared), and a prohibition that nothing consumes the raw value around it. Presence is the obligation; form is a prohibition on the same site; the guarantee is the pair. Ship the obligation without its prohibition and an agent under pressure will satisfy the letter in the first commit.
Run that through the most common boundary and it reads like this. What comes in from a user: verify what is expected, reject what was not asked for, and nothing reads the raw body. What comes back from a third-party API: same, but strip the unknown rather than reject it, because they add fields on every deploy. What is read from a database: same again, because a database is shared in fact, another service writes to it, a column was typed by hand a year ago, and trust in a schema is an assumption about the world, not about the code. And what your server returns to the client: reduce to the declared contract, never more, because a database row returned as-is sends password_hash to the browser, and that is the over-sending that leaks.
Parse, don't validate, as a chain: a seat, a contract, a parse, and a lock. Every boundary is a trust boundary, in both directions.
A value enters at a seat, a contract is attached there, the parse applies it, and only the parsed value reaches the consumer. The lock is a prohibition: no path from the raw value to the consumer.
Loading diagram…
Announced early, judged late
One property of obligations is easy to get wrong in practice, and the previous article stated it without saying who is responsible for it.
A prohibition is wrong the instant it appears, so it can be checked at every step. An obligation is legitimately unmet while the feature is being assembled: the adapter is not there yet because the port was written ten minutes ago. Check it too early and you drown the builder in false reds; skip it and you ship the unwired handler. So obligations are judged when the scope they govern is .
The point I want to add: the standard does not know when that is. On its own, run in CI against a pull request, it treats every scope as closed, which is correct, because a pull request is complete by definition. Inside an agent's working loop, something else has to say "this scope will receive no more files", and that something is the harness, the thing that runs the agent and knows the plan. "Announced early, judged late" is a property of the pair, standard plus harness, not of the standard alone. A tool that promises it by itself is overclaiming.
Report the complement, not the coverage
Now the part nobody ships.
Once a standard enumerates what should exist, it can do something a linter structurally cannot: for each declared guarantee, on each change, say one of three things. Proved present. Proved absent. Not looked at.
That third state is the product. Not a coverage percentage, which is a number about the tool. Not a green badge, which is a statement about the absence of findings. A list, cell by cell, of what the machine had nothing to say about. Because that list is exactly where a human reviewer's attention should go, and nowhere else. This is the , and on the server function above it reads like this:
Guarantee at createArticle | State | Who proved it |
|---|---|---|
| A contract is attached to the input | proved present | requireAtSeat |
The contract is strict (no passthrough, no any) | proved present | zodSchemaStrictness |
| Nothing reads the raw request around the parse | proved present | the lock |
| The response is reduced to a declared output contract | proved absent | requireAtSeat (output) |
| The schema is the right one for an article | not looked at | nobody: this is meaning |
Four rows a reviewer can skip, one row that is red and says what to add, and one row that is honest: the machine has no opinion on whether the schema is the right schema. That last row is where the reviewer reads.
Consider what review looks like without it. An agent produces a change. The checks are green. The reviewer now has to decide how much of the change to read, and the honest answer is: all of it, because green tells them nothing about what was checked. So they read the diff, reconstruct the consequences in their head, and skip the parts they are tired of. Attention goes where the reviewer's eye lands, which is the wrong place by construction.
With the complement, the same reviewer opens a map. These boundaries were proved held. These contracts were proved present, to this grade. These cells, the standard did not look at, either because no rule exists for them yet or because the rule could not fire on this change. The reviewer reads those cells and skims the rest. Not because the rest is right, but because the rest is : each guarantee there names the rule that proved it, and a guarantee with a name is one the reader does not have to re-derive.
A word on proved, since I use it on purpose and the next article will insist on saying guarantee rather than proof. Here it means the mechanical sense only: a rule ran, it applied to this change, and it passed, with the rule's name attached. That is a proof of form. It is not a formal proof, and it is never a proof of the model; the distinction the next article draws holds in full, and everything on the map is a guarantee in its sense.
Two honesty rules follow, and I hold them strictly. Never a scalar: a coverage number invites the reader to feel reassured by a quantity, and reassurance is what we are trying to abolish. And "" means a proved form, never a proved meaning: a strict schema that accepts a negative price passes. The standard proves that the schema is there and strict; whether it is the right schema is the domain owner's judgment, and the map should say so rather than imply otherwise.
None of this is new outside our field. Safety-critical engineering has built assurance cases for decades: every claim attached to its evidence, every gap in the argument a named object that someone must sign off. The web never imported that discipline because nobody was forced to. Agents writing most of the code is the forcing function. The cost of writing went to zero; the cost of reading did not. A map of what was not proved is the cheapest way to spend the reading.
Admit only within your vocabulary
There is a limit to the admission, and a serious reader will find it in thirty seconds, so I would rather write it down.
The rows of the map come from the model. A boundary nobody declared is not "not looked at". It is not on the map at all. The standard confesses the absence of what it knows to expect, and it knows to expect only what an engineer wrote down. If the engineer forgot a boundary, the map is silent about it in the worst way: not as a hole, but as nothing.
This is not a flaw to hide; it is the reason the standard must be written by someone who owns the system, and reviewed as a document in its own right. The map's honesty is bounded by the model's completeness, and the model's completeness is a human responsibility. A standard that pretended otherwise would be selling the badge again, one level up.
The same limit says something about tests. It is tempting to add "every seat has a test" as one more obligation, and it would be the most gameable one of all: a test that asserts nothing satisfies it. Tests are not seats. They are oracles: they prove the substance of a guarantee whose form the standard proved. Two mechanisms, and the map should keep them apart, so that "proved present" never quietly means "there is a test file".
What this does to attention
Step back and ask what all of this is for.
The economics of building software just inverted. Writing was the expensive part; now it is nearly free. Reading code you did not write was the cheap part, done by the person who wrote it; now it is the whole cost, done by someone who did not. Every practice we used to call over-engineering, strict configs, explicit contracts, exhaustive matrices, was expensive because writing it was expensive. It is now the cheap side of the trade, because each explicit, attributed guarantee is one thing the reader does not have to verify.
That is what a standard is for: not to catch bugs, which tests do, and not to enforce taste, which nobody should, but to move judgment out of the reader's head and into something that runs and names what it did. It splits the work in two. The engineer writes the standard and arbitrates form: the container, the boundaries, the shapes. The person who owns the domain, engineer or not, watches it run and arbitrates substance: whether the thing built inside the container is the right thing. And the people who build inside it, whoever or whatever they are, produce work that can be reviewed on substance, because form has already been proved and attributed.
Build your executable standard so your attention goes where it matters. Everything else in this article is the mechanics of that sentence.
Conclusion
The previous article said: enforce the architecture, do not trust intent. This one narrows the claim, because "enforce" turned out to be the easy word. Prohibitions enforce; the tooling for that exists and is now wired to agents. What agents change is not whether rules run but what a green result is allowed to mean.
A standard requires presence, which takes a model. It proves by attribution, which takes seats. It reports its complement, which takes the discipline to say "not looked at" out loud and never to summarize it into a number. And it admits that its honesty stops at its own vocabulary, which is why an engineer has to write it and keep writing it.
Do not ask what your checker found. Ask what it looked at, and what it would have said if it had found nothing.
The strongest move is still the one the next article is about: not to check a violation but to make it inexpressible. But between checking and impossibility there is a wide country, and the standard lives there. Most guarantees will never become types. They can still be declared, proved on every change, and confessed when they were not.
Further reading
- Gail Murphy, David Notkin, and Kevin Sullivan. Software Reflexion Models: divergence and absence, the two polarities, thirty years ago.
- Neal Ford, Rebecca Parsons, and Patrick Kua. Building Evolutionary Architectures: architecture fitness functions, the prohibition half done well.
- Tim Kelly and Rob Weaver. The Goal Structuring Notation: assurance cases, where every claim carries its evidence and every gap is a named object.
- Gregor Kiczales et al. Aspect-Oriented Programming (1997): join points and pointcuts, the older names for a place where something attaches and the query that selects it.
- Michael Feathers. Working Effectively with Legacy Code (2004): seams and enabling points, the places where behavior can be changed without editing them.
- Alexis King. Parse, Don't Validate: the seat, the contract and the parse as one move, before anyone called it a chain.
- Standard Schema: the interface several validation libraries now share, and the vocabulary a seat can be requested in without naming any tool.
Sharing is caring: Share on X · Share on Bluesky · Comment on Hacker News
A thought after reading?
If you would like to discuss about this article, you can write to me here. I share because I care and I want to learn. Please teach me with care.