Three captainrandom articles auto-published on 2026-06-10 and 2026-06-11. The DVLAW scrubber flagged zero em-dash violations across all three, against a STYLE-GUIDE budget of ≤3 dashes per 1,000 words. The actual densities were 4.5×, 8.6×, and 12.3× that limit.
The scrubber wasn’t broken. It was running the density check correctly, per paragraph. None of the individual paragraphs crossed the threshold. All three articles passed. All three shipped with em-dash densities far outside the style guide.
That’s the shape of everything that went wrong between 10 and 13 June.
Improving on the SLA recovery analysis
Improving on the slot-failure analysis that produced the sibling-cancel + slot-fallback + missed-slot alarm: that sprint documented what failed and built recovery machinery around it. This postmortem is about why the same upstream failure mode triggered four separate bugs across 72 hours.
Every bug reduced to the same thing: a check working correctly, answering the wrong question.
Bug one: the em-dash scope
The scrubber computes em-dash density per paragraph. If a paragraph exceeds the threshold, a flag goes into the sidecar .tells.txt file. Correct logic for prose that clusters dashes in a single digression.
What it missed: even distribution. An article can have a paragraph-clean density of two dashes and an article-level density of 12× the budget, provided the dashes are spread across enough paragraphs. The 2026-06-11 auto-publish run shipped three articles exactly like that. Per-paragraph: all clear. Per-article: 4.5×, 8.6×, 12.3×.
The fix moved the check to article scope: total dashes divided by total word count, against the ≤3/1,000 budget. The three already-shipped articles would have flagged. They didn’t flag because the original check was never looking at the article. It was looking at paragraphs, one by one, finding each of them clean, and reporting no violations.
Correct answer. Wrong unit of analysis.
Bug two: G-L3 and the two faces of scope failure
G-L3 is the duplicate gate. It derives a topic_key from an article to check against already-live content, then blocks the queue row if a matching key exists.
The first failure mode was over-aggregation. The topic_key derivation was including platform tags (captainrandom, bytebridges, dvlaw) in its alias-map lookup. An article tagged [SLA, dvlaw, devops] would produce a key that incorporated dvlaw. A different article on a completely different subject, also tagged dvlaw, would match that key and get blocked. The integration test on 2026-06-12 exposed three articles in the queue that were blocked or blockable by this artefact. Queue id=36 was one of them.
Platform tags are provenance. They aren’t topics. Including them in the derivation made the gate answer the question “do these articles share a tag?” instead of “do these articles cover the same topic?” The fix stripped platform tags before passing to the alias map.
The second failure mode was the inverse: under-detection. PR #97 (“Two bots, one token, one very confusing 409”) opened at 12:00 BST on 2026-06-11 as a near-exact duplicate of the already-live 2026-05-28 article on the same incident. Same topic, same citations. The gate should have caught it. It didn’t, because the live article had been shipped outside cmd_ship. The gate’s ledger only tracked what cmd_ship had recorded. Articles shipped by other means weren’t in the ledger, so the lookup returned clean. Correct answer to the ledger query. Wrong ledger.
The fix extended the gate to check the live target regardless of how it was shipped.
Same component, G-L3. Two scope errors, opposite directions: blocking things it shouldn’t, passing things it should block. Both because it was checking the wrong set.
Bug three: the substitute pool
When a scheduled slot misses, the substitute pool should pull from future-scheduled rows. Find the next queued article, pull it forward into the expired slot, continue.
The pool was searching the current row. When the 09:00 BST slot missed on 2026-06-13 (queue id=37), the substitute logic ran against the cancelled row and found nothing available. Accurate result: id=37 was cancelled, of course it had no substitute. Fix C’s missed-slot alarm fired. The 12:00 BST slot missed for the same reason and alarmed again.
The walkthrough of id=37: schedule_sweep fired the publisher at 11:00:08Z. Publisher cancelled the row. Substitute search ran against the cancelled row. Returned empty. Alarm fired.
The search was correct. The scope was the problem. The fix makes the pool recurse over future-scheduled rows rather than looking at the row that just failed.
What 20% autonomous success means
Four of the last five scheduled slots expired before the June fixes landed. Same upstream failure mode in all four cases, all logged under B-016. id=35 cancelled at 08:00 UTC on 2026-06-13 was the third in 72 hours, after ids 36, 39, and 50.
The SLA recovery machinery (sibling cancel, slot fallback, missed-slot alarm) arrived in the same sprint as the substitute pool fix. It’s correct machinery for detecting a miss and escalating. What it couldn’t do was prevent the miss, because the miss was happening upstream, in the substitute search scope, before the alarm had anything to fire on. The alarm fired correctly while the underlying cause persisted. Correct answer at the detection layer, wrong layer.
The pattern, named
Three components, four bugs, one shape.
- Em-dash check: correct paragraph density, wrong unit of analysis.
- Topic key derivation: correct tag match, wrong tag scope.
- Duplicate gate ledger: correct ledger lookup, wrong ledger.
- Substitute pool: correct row lookup, wrong target row.
None of these threw exceptions. None returned null. None logged a warning. They all returned values that made sense given the inputs they received. The inputs were wrong. More precisely, the boundary of what each check considered its inputs was drawn at the wrong place.
That’s what makes silent scope errors hard to catch in review. The code is doing something. The outputs are plausible. There’s no stack trace pointing at the bug. You have to hold the check and its downstream consumer simultaneously: what does this function return, and is that actually what the consumer needed? Most review passes don’t do that. The check’s own body looks correct in isolation. It is correct in isolation. The mismatch is in the gap between the check and whoever reads its output.
What changed
All four scope bugs are fixed. The integration test on 2026-06-12 surfaced three of them in a single run. That’s the right tool for this category of bug: not a unit test on the check itself, but a pipeline-level test that verifies outputs against what the downstream consumer actually needs. A unit test on the per-paragraph em-dash check passes even with the bug in place. A pipeline test that runs the full scrubber against a known fixture and checks article-scope density won’t.
The review heuristic that follows: when a check returns clean, the question isn’t “is this result valid?” It almost always is. The question is “is this check measuring what the downstream consumer is actually asking about?” If answering that requires reading two separate files simultaneously, the check probably needs a scope review.
Out of scope: retroactively correcting the three articles that shipped with high em-dash density. They’re live; the audit trail has the densities recorded. Future drafts will flag correctly under the fixed check.



