
The Measuring Instrument Was Buggy Too
Two measurements over unchanged input disagreed. A greedy regex with re.S was swallowing turn headers.
View companion repoThe two numbers that disagreed
I was in the middle of an audit on a session corpus when the count I was about to cite refused to sit still.
I measured once and got 417. I measured again and got 377. The locked metric named that pair as 417 -> 377 across two runs of the same instrument over unchanged input.
I recorded the diagnosis in 1f17b872-3018-434d-9af7-5099dc78d40d.jsonl at line 642. The quote is the whole event, so I am leaving it unaltered:
Counts shifted 417→377 between my own two measurements — the
.{0,N}greedy match withre.Sswallows following turn headers. My measuring instrument is buggy too. Fixing it and characterizing properly rather than patching one pattern at a time.
A measurement that changes between two runs over frozen input is measuring the instrument, not the corpus. 417 was not a fact I could hang on the sessions. 377 was not a fact I could hang on the sessions. They were two outputs of one extractor, taken in sequence, from input the metric itself called unchanged.
The rest of line 642 was the method, not a third count. The cause was a greedy .{0,N} with re.S that swallows following turn headers. The operator sentence was mine: my measuring instrument is buggy too. The response was to fix the instrument and characterize properly rather than patch one pattern at a time. I did not walk out of that session with a preferred total. I walked out with a veto on citing either total.
What a greedy match with re.S actually consumes
The cause in that session line was a pattern and a flag. I am copying both from the quote, not from a reconstructed script.
.{0,N}
The session named the shape: greedy .{0,N} with re.S. It did not name a value for N. Greedy means the engine prefers the long end of the window. re.S is the flag in the quote. Under that flag the dot is allowed to match a newline. A window of up to N characters, greedy, with newlines permitted, can reach past the text you meant to capture.
The session then named the effect. The greedy match with re.S swallows following turn headers. A turn header is a landmark in a session log. Logs of this kind are a stream of turns. The header is how you tell that one turn has ended and the next has begun. If the matcher consumes that header as payload, the landmark is gone from the place you expected it.
The quote stops there. I am not adding a start-of-line anchor the citation never mentioned. I am not adding a later run in which the count sat still. Line 642 is one event: two measurements, 417 then 377, greedy .{0,N} with re.S, swallowed following turn headers. Same input. Two totals. The mechanism on the page is the swallowed header, not a second corpus.
Why the corpus was innocent
Unchanged input is the other half of the headline. Two runs of the same instrument over unchanged input. If the input did not change and the count did, the count is not about the input.
417 versus 377 was then a statement about the extractor. It was not a statement that the corpus shrank. It was not a statement that 377 was the real total and 417 was inflation, or the reverse. The citation did not include a later agreed count. It included a shift, a cause, and a sentence that the instrument was buggy too.
Calling the corpus innocent is how I kept from laundering a matcher bug into a finding about the data. If I had published 417, a later reader would have thought there were 417 of whatever I was counting. If I had published 377, they would have thought there were 377. Both publications would have named the corpus as the source of a number that the instrument produced twice, differently, from the same input.
Once the corpus was out of the dock, the work was no longer which total to keep. The work was the rest of line 642: fix the instrument and characterize properly, rather than patch one pattern at a time. I am not inventing the patch, or a third total that finally sat still. The evidence is the disagreement and the refusal to treat either number as a corpus result.
Citations that point past the end of the file
A second session, independent of 1f17b872-3018-434d-9af7-5099dc78d40d.jsonl, hit the same class of failure with a different tool.
In agent-aL3-spec-gap-caca356c97fec7d6.jsonl at line 313:
Confirmed: all 11 line references fail to resolve to their claimed content (none match), and stall_detector.py:1453 is out of range (file is 1417 lines). My banner claim is accurate. Now Task 4 — docs/known-limitations.md.
A path:line citation is an instrument. It claims that a given line of a given file holds a given piece of content. Resolution is the measurement. If the line does not contain that content, the citation did not measure the file. If the line does not exist, the citation did not even point at the file.
All 11 line references failed to resolve. None matched. That is not a noisy subset. That is a complete miss. The banner that said so was the claim that resolved. The 11 pointers behind it did not. The quote says none match. I am not going to pretend one of them landed.
stall_detector.py:1453 is the case that needs no taste. That file is 1417 lines. 1453 is past the end. There is no content at that address. A citation to a line that is not in the file is the same shape as a match count that is not a function of the corpus: a precise-looking pointer that does not land. The out-of-range line is additional to the eleven failed content matches, not a replacement for them.
I am not using 1417 as a fact about the session JSONL that produced 417 and 377. 1417 belongs only here, as the length of stall_detector.py in that independent check. Two citations, two subjects. What joins them is the reread. Open the thing you named. If the count moved, you named the wrong producer. If the line is missing, or the line is past the end of the file, you named a location that does not hold the claim. Citations that do not resolve are the same class of failure as a regex that miscounts.
Making an instrument prove itself before you cite it
Line 642 does not end on the diagnosis. It ends on the response. Fixing it and characterizing properly rather than patching one pattern at a time.
A one-pattern patch would have been the obvious next keystroke. Tighten this .{0,N}. Drop re.S on this call. Special-case this header. The session rejected that shape. Characterizing properly meant treating the turn header as a boundary the engine is not allowed to swallow, and treating a count as unpublished until the instrument is no longer the variable.
I did not have, in that citation, a patched extractor to show. I did not have a later count that agreed with itself. Inventing either would be the same class of error I am writing about. What I had was a failed reread and a rule for what not to do with it.
The rule is small. Frozen input. Same instrument. Two measurements. If the number moves, you are looking at the instrument. 417 then 377 is that check, already failed, already recorded in 1f17b872-3018-434d-9af7-5099dc78d40d.jsonl. I do not get to call either value a corpus result after that recording exists.
The citation side is the same check with a different open. Before a path:line goes into a report, the line has to exist and the content has to be there. stall_detector.py:1453 against a 1417-line file fails before anyone argues about what the line would have said. Eleven references that fail to resolve fail as a set. Zero of them are evidence. The banner that reported the miss is the only claim from that line I will carry.
Where this generalizes
Any number I was about to publish had to survive a second look at the thing it claimed to number.
If I am counting matches in a session log, and a greedy .{0,N} with re.S is in the path, I have to assume the window can swallow a following turn header until I have shown that it cannot. Two measurements over unchanged input is the showing. 417 and 377, from 1f17b872-3018-434d-9af7-5099dc78d40d.jsonl line 642, is what it looks like when the showing fails. At that point I am done investigating the corpus. The matcher is the subject.
If I am citing file:line, I have to resolve file at line to the claimed content. All 11 missing resolutions in agent-aL3-spec-gap-caca356c97fec7d6.jsonl line 313 are what it looks like when that showing fails. stall_detector.py:1453 on a 1417-line file is what it looks like when the pointer is not even on the map. At that point I am done arguing about the banner's supporting lines. They did not land.
The family is one sentence long. Verify the tool before you cite its number. A greedy window that ate the next header is not a corpus counter. A line number past the end of the file is not a source. If the second look disagrees, or the named line is not there, I do not get a finding about the subject. I get a finding about the tool.
That is the whole lesson I was willing to take off the disk. Not a preferred total. Not a patched one-off. A pair of counts that could not both be right, a regex that swallowed the next turn's header, and a second session in which eleven citations failed to resolve and one pointed past the end of the file. The instrument had to go first. The corpus could wait.
Continue the series
- 55SeriesThe Piped Exit: Why TSC_EXIT=0 Sat Next to Real Type Errorsbunx tsc --noEmit piped through tail; echo TSC_EXIT=$? reports tail's status, not the compiler's. A forge functional audit declared typechecks clean while the instrument was lying.
- 57SeriesThe Poisoned Corpus: When Agent Chatter Becomes What the User Asked ForTurn 1 of the corpus was a teammate-message orchestrator dispatch, not a human-typed turn. The is_human filter did not exclude it, so every downstream agent would read lane dispatch as user intent.
- 54SeriesDrive Don't Sweep: A Green HTTP Status Is Not Proof a Screen WorksA cookie-less HTTP sweep called every Forge route clean. Every response was the same login page. Status OK measured the server. It never measured the screen.
- 58SeriesThe Truthy Type: How One `or` Silently Dropped Half the Corpusrole = r.get("type") or m.get("role") reads "message", which is truthy, so the inner "user" is never seen. One session format parsed. The other silently yielded nothing.