An Unreadable Document Is Not a Missing Field

A release checker collapsed three different failures into one answer and turned confusion into an accusation. The argument about AI and code makes the same mistake, at scale.

· 23 min read

agents verification methodology culture


The public conversation about AI and code keeps returning two answers: it is slop, or it is over. What disappears is the conditional sentence: I use it for everything and verify like I trust nothing. That sentence is not a midpoint between the camps. It is a different question, and the public record is a poor instrument for measuring how often people ask it.

I learned to distrust that kind of inference from a bug in a tool I maintain.

A tool I maintain checks release contracts: given a published artifact, does it actually carry what the contract says it carries? One clause type asks whether a field is present in a published JSON document. The reader was this:

fn json_field(doc: &str, path: &str) -> Option<String>

That signature is the bug. None was returned for three unrelated situations: the document could not be parsed, the path was absent, and the path held a container rather than a scalar. It turned three different facts into one absence.

The evaluator downstream read None as field absent from the published file and returned VIOLATED — a confident accusation against the producer, from no evidence at all.

This is not a malformed-input edge case. gh api can hand you an HTML error page. A proxy can truncate a body. A producer can publish a perfectly good file at a path the contract calls JSON when it is actually TOML. I demonstrated it against a real published document — a present clause pointed at ley-line-open’s README at v0.11.3:

before:  VIOLATED  field absent from the published file
after:   UNKNOWN   published file is not parseable as JSON

The fix was to give the return type three states, one for each case:

Err        document unreadable        -> Unknown; no clause about it is decidable
Missing    read it, it is not there   -> Violated; the producer did not publish it
NotScalar  a container, not a value   -> Violated, naming which shape

Two of those three are still accusations, and that is the point. The fix did not make the checker more forgiving. It made it stop accusing anyone when it had nothing to accuse them with. NotScalar stays separate from Missing for a related reason: a producer who published a different shape has not published nothing, and a checker that conflates the two is one people learn to ignore.

The governing rule was already written down in the tool’s own contract document, and the reader had violated it since the day it was written:

An absence of fact must never be reported as a fact of absence.

I have been thinking about that sentence for a week, because it is not really about JSON.

The same error, running at scale

There are two things you are permitted to say in public about AI and code.

  1. It’s slop. A plagiarism engine that floods maintainers with fake bug reports, launders wages toward whoever owns the GPUs, and produces code that looks right and isn’t.
  2. It’s over. You haven’t hand-written a line in months, the agent ships while you sleep, and anyone still reading diffs is a farrier admiring his craft.

That’s the list. Scan for anything else and you come up empty. And the near-universal reading of that emptiness is there is no middle — the sensible position doesn’t exist, or is a rounding error, or is cowardice with a nicer name.

That is exactly the inference my evaluator made. Nothing came back, therefore the thing is not there. It is a fact of absence assembled out of an absence of fact, and the outcome is the same: a confident accusation against the producer, from no evidence at all.

Because the middle’s actual sentence — I use it for everything and verify like I trust nothing — is not missing from the discourse so much as selected against by it. Each clause is an admission against interest somewhere. Say the first half to a skeptic and you’ve confessed to slop; say the second half to a maximalist and you’re a dinosaur who hasn’t updated his priors. A statement that costs you something in every room is one people make carefully, in rooms of their choosing, and rarely in the rooms that get quoted.

What comes back empty is the channel. The conclusion drawn is about the population.

I don’t want to make the same mistake in reverse. I can’t tell you that the middle is the majority; nobody can. The record shows which statements survive the channel, not how many people hold the ones it filters out. The claim I can defend is narrower and still bad enough: the public record gives no reliable estimate of how common the conditional position is, and it is quoted constantly as though it does.

A second mechanism is preference falsification — Kuran’s term for holding one view and professing another. It operates somewhere else. A developer who tells a survey they conceal their AI use because they fear looking replaceable is not the victim of a lossy channel. They read the channel correctly and made a calculation. That is falsification, and it lives in workplaces, where there is a wage attached to the answer.

The channel may explain the shape of the public argument; falsification may explain what people say at work. Confuse them, and you end up calling people cowards when some of them were simply not speaking in public.

A third possibility is that the sentence is being said constantly, but in private places the public record cannot see. I would rather name that possibility than pretend I can prove it. Model conversations happen one person at a time, in rooms that never federate. Chats aren’t indexed and don’t get quoted; social platforms are and do. If that’s where it went, the parseable record wasn’t emptied so much as privatized, and the thing everyone samples was never the population to begin with.

I have no way to check that, which is the difficulty. Three mechanisms, then, and I can’t tell you their proportions: a channel that selects against the sentence, a workplace that prices it, and a venue that carries it privately. What I can tell you is that none of them is nobody thinks this, and that’s the one everybody reaches for.

A thing that has gotten harder in the meantime

You used to be able to read provenance off a text. Idiolect identified a person — word choice, sentence rhythm, favored constructions. That is largely gone. Style now identifies a model and a trajectory rather than an author, and the fingerprint smears across everyone who works with the model. The spike in a handful of words across published abstracts was the early signal; the tics migrate into human prose, and idiolect becomes sociolect.

This makes the authored/derived rule necessary, not just tidy. When derivation is ambient, inspection can no longer recover origin. The only provenance left standing is the kind that gets declared — a statement someone makes and can be held to, unverifiable by examination, granted rather than detected.

It is the dependency argument one level up. Signatures replaced reading for code. Testimony is replacing stylometry for text.

An observation is not a verdict

There’s a second thing the checker does, and it took longer to get right than the parser.

It has a mode that reports which consumers declare which version of a producer. Against the live graph, ley-line-open at v0.11.3 had twelve of twelve declared consumer edges behind. cloister alone pinned it at six distinct versions across seven edges, ranging from 0.4.5 to 0.10.4.

It looks like neglect, but here it means the project is moving quickly. cloister has moved faster than anything else in that graph — six pins is what it looks like when a project grows edges quickly, each one landing against whatever was current the week it was written. The consumer that never touches anything has one pin and perfect consistency.

So it looks like a damning result, and it emits no verdict at all. It cannot fail a run. It exits 0 however far behind anyone is. The rule, written in capitals in the commit because I kept forgetting it:

A stale pin is not a violation.

Declining to upgrade is legitimate. The consumer owns its own compatibility testing. The tool’s job is to produce the observation and let a separate layer decide whether any of it warrants action. Observations are not verdicts, and a system that collapses the two becomes useless immediately — because once every observation is an accusation, the only safe number of observations is zero.

More simply: UNKNOWN is an epistemic result, not yet a policy. What you do about not knowing is a separate decision, made by someone who is allowed to make it.

The fourth state, which took another argument to add

The verdict type ended up with four cases rather than three, because “we checked and it passed” and “nothing here needed checking” are not the same claim either:

Satisfied,
NotApplicable(String),  // no consumer holds the edge kind this clause serves
Violated(String),       // observed, and the property does not hold
Unknown(String),        // not observed, or the observation itself failed

NotApplicable exists because reporting resolved for something nobody evaluated conflates verified with didn’t look. A clause gated on an edge kind no consumer has is not satisfied and not unknown — it is inapplicable, and saying so is different from saying it passed.

There are three ways for a result not to be a yes. The job is to keep them separate.

That is what happened to the middle.

A reviewer who requests changes on agent-written code is making an observation: this is wrong here, on this line. In a contested shop it lands as a verdict — you don’t trust the tool, or you don’t trust me. A developer who mentions using an agent is making an observation about their afternoon. It lands as an endorsement of the entire technology, its labor economics, and its training data.

Both camps make the same mistake. The skeptic reads your usage as endorsement. The maximalist reads your checking as unbelief. Under those conditions saying less is the rational strategy, and the channel thins — not because nobody holds a position, but because holding it costs more to state than to keep.

The silence is real. The inference from it is the bug.

The same rule, applied to a project index

The same rule shows up in a place you can go check right now. A dependency graph I publish for a separate site refuses to infer a maintainer’s status from activity, because a quiet repository might be finished, abandoned, or deliberately private, and no activity signal distinguishes those — so the graph is forbidden from guessing and required to use what the maintainer actually said.

The argument about AI has no equivalent restraint. Inferring someone’s position from their activity is the standard move there, performed by both camps, and called reading the room.

The question has no answer as asked

This is the position I’m trying to describe, and why it is so hard to say.

Do you trust AI-generated code? is not a hard question. It’s a malformed one. Trust isn’t a disposition you carry between repositories like a personality trait. It’s a property of a situation. The useful first question is in which repo? — because the same engineer, on the same day, with the same model, will merge one diff after a glance and interrogate another line by line, and both are correct.

But the repo is too coarse, and the imprecision matters, because “it depends on the project” is the kind of thing people say to avoid having a position. One repository holds a README edit, a migration, and the code that checks a signature. Those do not deserve the same treatment under the same CI. The question that actually decides anything is:

Which change, under which independent controls, at what cost of being wrong?

It changes the question. Trust isn’t a dial running from distrust to trust; it depends on several axes. How precisely is the desired behavior specified? Are the checks independent of the thing they’re checking? What do they actually establish, as opposed to suggest? Is the change reversible, and how fast? What can it reach — production, secrets, other people’s data? And what would another hour of review have cost against what it would have saved?

Nobody holds one answer to that, which is exactly why nobody in the middle has a slogan.

Once you say it, reading stops being the opposite of using an agent. Reading is one instrument among several — an expensive one, sometimes irreplaceable, which does things no automated check does: it tests whether a design fits, whether the next person will understand it, whether the thing is worth having at all. The two camps then look less like opposed positions and more like two failure modes:

  • One treats manual inspection as the only legitimate evidence.
  • The other treats a green pipeline as sufficient evidence.

I don’t think either group has quietly agreed on anything, and I don’t know their motives. But both answers are fixed in advance, and a fixed answer is the one thing the question above cannot accept.

We’ve seen this pattern before, with dependencies. Almost nobody reads their dependencies. You own every CVE in node_modules and you have read approximately none of it. That did not get resolved by demanding people read the code — it got layered controls: lockfiles, pinning, signatures, provenance attestation, scanners. What those bought was not trustworthiness in the abstract. Supply-chain risk is plainly not a solved problem, and attestation establishes where something came from, never that it is correct. What they bought was ownership-without-reading becoming tractable under stated conditions, and no longer shameful.

The analogy breaks in one important place. Not-reading lodash is survivable because the scrutiny is amortized across a population: a package with a million installs gets read by someone, eventually, and a bug that ships gets caught once and fixed for everyone downstream. A freshly generated diff sitting in your own repo has none of that. N is one. Nobody else is ever going to read it, a mistake in it is not shared out across anyone, and provenance tells you which model produced it, not whether this particular diff is one of the good ones. There is no crowd quietly doing that check on your behalf.

That does not invalidate the analogy. It tells us what it supports. The dependency case didn’t prove that not-reading is fine; it proved that controls can be graduated to how much scrutiny a thing has actually received, rather than forced into a binary of trust it completely or read it yourself. lodash earns light local controls because population-scale scrutiny is real and already paid for. A diff nobody has read yet has paid for none of that, so the honest amount of received scrutiny is close to zero — which is exactly why the question from two sections ago has to reach further for a generated diff than it does for a five-year-old dependency. The single function you didn’t read is not entitled to lodash’s confidence. It is entitled to lodash’s treatment: named, scoped, and checked in proportion to what is actually known about it — which for N=1 is usually not much, and the controls should say so rather than borrow a population’s credit for an audience of zero.

Generated code deserves that graduated treatment, which is neither automatic acceptance nor automatic rejection. Provenance is one input to the decision rather than the decision itself — and it is a weak proxy for correctness, though it remains a perfectly good reason to care about licensing, accountability, spam, or who is absorbing the review burden.

Loud failure is built, not inherited

The agent wrote the tests too. If the harness is generated, what holds up what?

I had a tidy answer to this and it was wrong. The tidy answer is that harnesses fail loudly while the code they constrain fails quietly, so checking the harness is the cheap move. It sounds right. The essay you are reading opens with a counterexample to it.

That parser bug ran for as long as the reader existed. The rule against it was written down before the code was. The rule was correctly stated, correctly enforced at the point where verdicts get combined, and there was a property test — unknown_is_never_satisfied — that was green the entire time. None of it fired, because the defect was upstream of everything that was watching: in the step that decided which verdict applied. A rule can be correctly stated, correctly enforced, and still be fed a lie.

So loudness is not a property harnesses have. It is a property somebody builds into them, deliberately, and mostly after being burned. Three-state returns, mutation probes, degradation reports, tests pinned to artifacts that can change underneath you — those are all machinery for converting a silent failure into a visible one, and each was paid for.

The regress ends only at evidence that does not share a failure mode with the thing it is checking. An agent that writes an implementation and its tests from the same misreading has produced two artifacts and one opinion; running them against each other confirms the misreading. Independence is the currency, and generated tests do not automatically hold any.

What that looks like in the checker

Its unit tests cover pure decision logic — given these verdicts, what does the run report. Those the generator could have written to match its own misunderstanding, and they would still pass.

The real verification is a separate suite that shells out to gh against actual published GitHub releases, pinned to immutable ones. Its header states the arrangement plainly:

These are the binary’s REAL verification. Its unit tests cover the pure decision logic; the observation half can only be exercised against actual published releases — which is the design’s own argument turned on itself.

If ley-line ever publishes a complete v0.2.0, rejects_a_partial_release SHOULD fail — that is the test noticing the world changed, not a flake.

The independence is doing the work there, not the assertion count. Those tests are checked against a world nobody in the loop controls, so they cannot be quietly satisfied by a shared mistake. A test that would break when reality moves is evidence; a test that only agrees with its author is a mirror.

The things people file under “harness” are also not one kind of thing, and the difference matters when you are deciding what you still owe a change. Types, tests, and static analysis produce evidence — they raise or lower what you are entitled to believe. Sandboxes, least privilege, and revocable credentials produce containment — they say nothing about whether the code is right and everything about what it can reach when it isn’t. Staged rollout, monitoring, and rollback produce recovery — they shorten the time between being wrong and finding out. A change can be badly evidenced and safely contained. A migration can be beautifully tested and unrecoverable. Collapsing all three into one word is convenient and it is how people end up feeling covered while holding only one of them.

Where the argument actually ends

Not in trust, and not in any single control. The candidates that carry weight are the ones with some independence from whatever produced the change:

  • tests that predate it, or that run against artifacts outside anyone’s control
  • externally specified protocols and formats
  • type systems and static analysis, which are wrong in different ways than logic is
  • properties and invariants chosen before the implementation
  • differential and property-based checks
  • restricted capabilities, so a wrong answer has a small reachable surface
  • human review at the boundaries where being wrong is expensive, done by someone who did not write the prompt
  • staged rollout with monitoring, so the world gets a vote

That qualifier on review is not decoration. A reviewer checking the diff against the misunderstanding they themselves fed the agent is the two-artifacts-one-opinion problem again, one level up, and it is easier to miss here because the mismatch is between two humans’ actions across time rather than between two files sitting side by side. That makes staged rollout the one item on this list independent of the entire authoring process, review included — not because it is superior, but because production is the only party in the loop that was never told what you meant. It arrives late and after the cost may already be spent, which is exactly why it is filed under recovery rather than evidence: it tells you fastest, not cheapest.

None is complete. None is independent in every direction. Confidence comes from their partial independence and from matching what is left over to the cost of being wrong — which is a judgment, made per change, that no configuration makes for you.

The uncomfortable part is that a person caught the parser bug by reading a type signature, finding it dishonest, and thinking about one sentence for a week.

That reverses the claim I made earlier. Reading is not the weak harness you fall back on when the good ones are missing. It is a necessary one — expensive, invoked rarely for that reason, and sensitive to the class of failure every automated check shares a blind spot for, because everything above it can be fed a lie.

The word I first reached for was terminal, and it promotes reading too far. Reading is not the last word and it is not reliably right. People misread. Staged rollout, telemetry, and the incident nobody wanted all produce evidence after every human has signed off — and they produce it about precisely the properties review is worst at judging in advance.

What reading covers is the class that resists being written down as a predicate. That is why it should be spent on the changes where being wrong is worst, and why “just read it all” and “you don’t need to read it” are both answers from people who have not priced it.

Three things, one shape

That is three arguments — a parser, a discourse, and a green test — and I have been treating them as one because they are one. Set them side by side and the middle column never changes:

  parser
    saw    a read returned nothing
    said   the producer removed it

  discourse
    saw    nothing came back
    said   there is no middle

  harness
    saw    the test stayed green
    said   the property holds

  supported, in all three:
    I could not tell

Nothing in the left column is wrong. Every observation is accurate, and in each case somebody competent looked at it and wrote down the right-hand column, and none of them were being careless. The step that goes missing is the same step every time: between seeing nothing and concluding nothing is there, an inference happens, and it is the weakest part of the chain precisely because it does not feel like a step at all.

The parser had a rule against it written down before the code. That did not help, because the rule governed the verdict and the defect was upstream, in the part that decided which verdict applied. Rules do not protect the place where nobody thinks a decision is being made.

What this argument does not settle

I opened by describing the skeptical position as plagiarism, maintainer spam, laundered wages, and code that looks right and isn’t. Then I spent the rest of the essay on the last one. That is a subject change, and I would rather name it than have it noticed.

Everything above concerns whether a particular change is well-enough evidenced to accept. It does not touch training consent, copyright, the economics of who captures the gain, privacy, attribution, or the review burden that generated contributions push onto maintainers who never asked for them. Those are real questions and a green pipeline has nothing to say about any of them. A maintainer who bans generated patches because their queue is drowning is not making a claim about correctness, and answering them with verification infrastructure is answering a question they did not ask.

Organizations do not believe things

A company can be loudly AI-positive in public and quietly AI-skeptical in its own review queue, and I don’t think that’s usually hypocrisy.

Those two functions optimize for different outcomes and carry different kinds of risk. Public messaging carries reputational and legal exposure, and it has its own controls — legal review, approvals, a comms lead who says no. What it does not carry is the specific liability of a defect reaching production through a change someone approved. The review queue carries exactly that, and its controls are the ones this essay has been describing. Two functions, different evidence, different consequences of error, different answers.

Calling that hypocrisy is the same reading error again: taking two accurate observations from different environments and collapsing them into one contradictory verdict about what a company “believes.” Organizations do not believe things. Parts of them make decisions under local constraints, and the parts are not required to agree.

The middle builds instead of arguing

One reason this position is so underrepresented in the record is that its output is infrastructure, not discourse.

Someone who cannot answer do you trust it with a yes or a no doesn’t write a manifesto — there’s nothing to write. They go build the thing that makes the question answerable for a given change, and then they publish the tool rather than the take. That work is invisible in every channel where this argument gets sampled, because you cannot retweet a harness. The middle has a voice. Its output is the wrong shape for the medium anyone is sampling.

So the checker returns three states now, and one of them is UNKNOWN. That change added no confidence anywhere. All it did was tell the kinds of nothing apart.

One distinction from that change is worth keeping straight here too, so the ending doesn’t quietly erase it. Naming three mechanisms — a channel that selects, a workplace that prices, a venue that hides — is not nothing. It is closer to characterizing the instrument than to reporting a blank, the same way NotApplicable turned out to be a different fact from Unknown rather than a finer way of saying it. But characterizing the instrument is not the same as reading a number off it, and a number is what how common is the middle was actually asking for. Knowing the shape of your ignorance is real knowledge. It is still not the size of it.

The public record on AI and code is a document that will not parse. Whatever you conclude from reading it, the correct return value is UNKNOWN — and reporting anything stronger is a confident accusation against the producer, from no evidence at all.