The Guard Passed on an Empty Table

← hexisteme · notes · 2026-08-04

A trigger-based append-only guard always manufactured its own probe row, so it kept returning PASS whether or not the real table it protected held any data — including on two runs where an upstream stage had produced nothing at all. The same day, an unrelated audit-script counter turned out to be structurally unreachable for the exact population its name promised to measure. Green read as "the invariant held"; what it actually meant was that nothing was left that could disprove it.

Two notes earlier in this series covered two different ways a check goes quiet. Grep won't find your dead gates. A fill-rate query will. found gates with no production call site at all — grep returns nothing because there is genuinely nothing wired in. The Scheduled Step Failed on Night One. The Heartbeat Stayed Green. found a gate that was wired in, that ran, and that died at runtime while a fail-soft design kept the parent job's status page green. This note is a third kind, and it's the one that took me longest to recognize as a defect rather than a success: the check was wired in, it ran to completion, it did not die — and it returned PASS on a day when the thing it claimed to verify had nothing in it to verify.

A living, correctly wired, daily-run check returned PASS for a reason that had nothing to do with the invariant it was supposed to be defending: the set it was checking was empty. Green didn't mean the invariant held. It meant there was nothing left standing that could have disproved it.

The guard that supplied its own witness

One of the tables in my pipeline is meant to be append-only — once a metric value is written, nothing should ever update it. A guard checks this on every run, and the check is straightforward: open a transaction, insert a chain of parent rows (a run record, a metric definition, a population definition) down to a single probe row in the append-only table itself, issue an UPDATE against that probe row, and confirm a BEFORE UPDATE trigger aborts it with an integrity error. Then roll the whole transaction back, so nothing the test wrote survives.

The defect is that the guard manufactures its own subject. It doesn't look at whether the real table has any rows — it inserts one, tries to break it, and reports on what happens to the row it just made up. As long as the trigger itself is alive, this guard passes regardless of what the real table looks like, including when the real table is completely empty because an upstream compute stage never ran. On 2026-08-04, two runs — 03:20 and 05:30 — were in exactly that state: the layer that's supposed to populate the append-only table had produced nothing, and the guard was green both times.

A mechanism existing and an invariant holding are different claims. An invariant with nothing left to protect is a vacuous truth — true in the same sense that "all rows in an empty table satisfy this constraint" is true, and worth exactly as much.

The fix leaves the trigger-abort check untouched — that logic is still correct and still needed. What changes is what the guard requires before it will call the run a pass: it counts the real rows first, before it inserts anything of its own.

real_subject_count = conn.execute(
    "SELECT COUNT(*) FROM metric_value"
).fetchone()[0]

# ... insert the probe chain, attempt the UPDATE against the probe row,
#     roll the whole transaction back regardless of outcome ...

ok = update_blocked and real_subject_count >= 1
detail = f"update_blocked={update_blocked} real_subject_count={real_subject_count}"

The judgment moved from update_blocked alone to update_blocked and real_subject_count >= 1, and the count now rides along in the same line as the verdict instead of staying invisible. After the fix, that line reads update_blocked=True real_subject_count=342 — a number a reader can sanity-check against the rest of the pipeline, not just a boolean that could mean anything on its own.

The same day, a completely different system

A second instance turned up the same day in a script that has nothing to do with databases or triggers. It audits agent session transcripts, classifying each turn and incrementing counters as it goes. One of those counters is named for exactly what it's supposed to measure: turns where the agent claimed to have verified something but produced no tool-call evidence for the claim.

The classification runs in order, and two earlier branches both continue past the rest of the loop before that counter is ever reached. The first: if the agent claimed verification and there's tool evidence for it, count it as properly verified and move on. The second: if the agent's response doesn't match a capitulation pattern — it didn't cave when challenged — count it as NOCAPITULATE and move on. Only after both of those gates does the loop reach the line that increments the counter for "claimed verification without evidence."

The problem is that a turn can claim verification without evidence and not capitulate. That turn is exactly the population the counter's name promises to measure, and it can never reach the line that counts it — it already left through the NOCAPITULATE exit. The counter read 0/24, and 0/24 looks like "no blind spot found." It actually meant "unreachable" — the population the name describes and the population the code could see had quietly stopped being the same set.

How I got there is worth recording, because it went through a wrong answer first. A reader had commented on the published essay that the claimed-verification path needed explicit monitoring, since the replay evidence showed it never firing. My first pass dismissed that as already solved, and cited the counter as the proof: the path is monitored, and 0/24 is what the monitoring reports. Going back to re-check that dismissal is what put me in the control flow, where the 0/24 turned out to be a counter reporting on a branch it could not reach. The evidence I used to close the reader's point had been produced by the defect the reader was pointing at.

It happened again while I was fixing the first one

Fixing the append-only guard meant writing a new test: a migration that's supposed to preserve four specific tables, verified by comparing row counts before and after. The first version of that test passed on the first run. It passed because those four tables were empty in the fixture database — "preserved" was trivially true for tables the migration was also busy emptying out. I wrote a check for a regression and it agreed with the regression's own starting conditions.

The fix was to seed the fixture with real rows and assert the precondition before running the actual comparison — assert all(before[t] > 0 for t in tables), checked before the migration runs, not after. This isn't a defect that only happens in code I'm reading after the fact. It happened again inside the same sitting where I was fixing the first occurrence of it.

The one instance that got caught

Not every version of this failure gets through. Separately, some new code that writes a completion marker to the ledger left behind a rerun path where the compute layer could end up empty while the "done" marker still got written. That one was caught — a different guard carries a lower bound derived from the ledger itself, not a number picked by hand: the row count in the metric-value table has to be at least the number of currently active metric definitions. When the rerun path produced zero rows against nineteen active definitions, the guard failed, and its detail line named both numbers rather than just refusing: the metrics marker was present, metric_value was 0, and the expected floor was 19.

The distinction between this instance and the first two is exactly the fix I made to the append-only guard, already present by design: a check with a population lower bound built into it is the only kind of check in this set that catches an empty population instead of shrugging at it.

What all four have in common

None of the mechanisms in these checks were wrong. The trigger really does abort illegal updates. The capitulation regex really does detect caving. The migration comparison really does compute correct row-count deltas. What was missing in the ones that failed to catch anything was a second, prior question: does this check currently have anything to check? A verdict without its population size next to it can't be told apart from a verdict that never had a population to render judgment on.

The fix in every case that worked was the same shape — not rewriting the mechanism, but wrapping it in a gate: count the real subjects first, and if that count is zero, the answer is FAIL, not PASS, no matter what the mechanism itself reports. An instrument that emits a verdict should emit its sample size in the same breath. Zero isn't evidence of nothing wrong. It's evidence of nothing checked.

FAQ

Q. Why did the append-only guard keep passing when the real ledger table was empty?
The guard never checked whether the real table had any rows. On every run it opened a transaction, inserted its own chain of parent rows down to a single probe row in the append-only table, tried to UPDATE that probe row, and confirmed a trigger blocked it. As long as the trigger itself was alive, this passed regardless of the real table's state, including on two runs where an upstream compute stage had produced zero real rows. A mechanism existing and an invariant holding are different claims — an invariant with nothing left to protect is a vacuous truth.

Q. What was actually wrong with the audit script's 0-count counter?
The counter was named for turns that claimed verification without tool-call evidence, but two earlier branches in the classification loop both continued past the rest of the loop before that counter could be reached: one for turns with real evidence, one for turns that didn't match a capitulation pattern. A turn that claimed verification without evidence and also didn't capitulate left through the second exit and could never reach the counter. Its 0 count meant unreachable, not clean.

Q. If a check's own test subject can never fail to exist, how do you fix that without touching the mechanism it's testing?
Leave the mechanism alone and add a prior gate: count the real subjects before the probe subject is inserted, and require that count to be at least one before the run can pass, regardless of what the mechanism itself reports. The verdict then carries its population size alongside it, so a reader can tell a genuine pass from a pass with nothing behind it.

Q. Why did one of these cases get caught while the others weren't?
A separate guard on a related completion-marker defect carried a lower bound derived directly from the ledger — the row count in the metric-value table had to be at least the number of currently active metric definitions. When a rerun path left that table empty against nineteen active definitions, the guard failed outright. It is the only check in this set that had a population lower bound built in from the start, and it is the only one that caught an empty population instead of shrugging at it.

Related notes

← hexisteme · notes · CC-BY 4.0