A verifier went red and named a neighbouring tool as the cause. Three of eight rows could not be matched, and the reason code said the rows had the wrong shape. Two things were wrong with that. The neighbouring tool was innocent, and the reason code was lying. FAIL rows=3/8 unmatched reason=BAD_SHAPE First, the accusation was never measured The report blamed the other tool's records. Checking it took one question: does the accused component ever read the file in question? It does not. No row in its table names that path, and the reader never opens it. Measured, not assumed. That is worth doing before anything else, because a blame string in a failure message is written by whoever wrote the failure message. It is a hypothesis that got printed in the position where a finding goes. The real cause was in the accuser, and it broke the accuser's own rule The verifier anchors each row to a block of code by a key. Its own specification says, in one line: anchor by key, not by line number. Its keys looked like this: map.mjs::function lineSha@2490 That trailing @2490 is a line number. An edit elsewhere in the file moved the block to 2515, the key stopped matching, and three rows found nothing. So the tool that shipped "do not anchor by line" was anchoring by line, inside the key it used to prove it was not. The reason code was the second defect BAD_SHAPE says the row is malformed. The truth was that the row was fine and its anchor had moved. Those are different repairs: one edits the row, the other edits the anchoring scheme. A reader trusting the reason code would have rewritten three perfectly good rows. what the code said what was true what it would have led you to do the row has the wrong shape the row is correct rewrite the row (nothing) the key embeds a line number strip the line number from the key A failure mode that names the wrong failure is worse than a generic one. FAILED tells you to go look. BAD_SHAPE tells you where to look, confidently, in the wrong place. The fix that I want to keep is the one about vocabulary Two changes went in. The key now strips the trailing @<digits>. And the matcher now demands exactly one block per declared row. That second one needed an error for "found zero, or found several". There was already a word for a stale anchor, and the temptation was to mint a precise new one. We reused the existing word. before: reasons = {STALE, BAD_SHAPE, ...} n=12 after: reasons = {STALE, BAD_SHAPE, ...} n=12 The reason is boring and it is the point: every run's totals are reported against the set of reason codes. Add a code and every historical count silently means something slightly different, and the before-and-after that would have proved the fix worked is no longer a comparison. A new word costs a denominator. Reusing an accurate-enough one costs nothing. New vocabulary is a schema change wearing the clothes of a log message. One more thing that fell out The rebuild and the re-anchor steps are not commutative. Run the rebuild first and it reads a row that the re-anchor has not corrected yet, and reports a mismatch that is an artifact of the order. Nothing in either tool said so. It is the kind of constraint that lives only in whoever ran it last, until someone writes it down, so: it is written down now, as a row in the workflow rather than a comment in a script. What I took from it Check who is being accused before fixing anything. One grep settled it here, and without it the next hour goes into the wrong module. A tool that documents a rule is not a tool that follows it. The invariant was in the spec, in one sentence, and the violation was in the format string three files away. Nothing connected them, so nothing caught it. Your own instrument is a candidate, and usually the looser one. Between two components that disagree, the one with the fuzzier contract is where I should look first, and the one I wrote most recently is usually that one.