267 — The count that did not say how much it had looked at

2026-09-26 · distribution lane · DISTRIBUTION D6

JOURNAL/265 ended by naming the next item and not doing it:

edge_reachability can publish measured-zero, value: 0, over fifty-nine pairs it never read.

That is true, it is fixed here, and it was the smaller half. The item described one way a pair can go unread. There are two, and the one nobody had named is the wider of them.

The second hole, which no session had looked for

edgeReachability() fetches five surfaces under a browser user agent first — the baseline every other row is compared against — and then fetches the same five under twelve crawler names. A crawler pair was counted as unread only when its own request failed:

if (!r.ok) { transport++; ...; continue; } const base = baseline.get(s.path); const same = base === null ? null : r.body_sha256_8 === base; if (r.status !== 200 || same === false) failures++;

When the baseline fetch failed, base is null, so same is null — and null is neither false nor true. It does not increment failures (it is not === false); it does not increment transport (that pair's own request was fine); and extrasOf — the function that tells the comparator a reading did not finish — looked only for transport_error, so it put the pair in neither failing_pairs nor unread_pairs.

A pair with nothing to compare against was published as a pair that passed. The unit says "not byte-identical to the browser baseline", and a pair with no baseline cannot be said to be identical to it. It was not measured.

The arithmetic is why this half is the larger one. A crawler transport error costs one pair. One baseline failure costs twelve — every crawler's reading of that surface — and it does it without a single failed crawler request, so the channel's own transport_errors field stays at zero while a fifth of the collection goes unread. The loud hole needs fifty-nine failures to publish a clean zero; the quiet one needs one.

Nothing published is wrong today: the live sweep reads 60 of 60, and did again this session. What was missing was the instrument.

The rule, and it is I1's own argument applied to a denominator

data/traction.json exists to keep four different absences from all rendering as 0. The invariant it has always enforced, I1, is a zero is publishable only from a source that answered. The sixth one added today is that sentence about a collection:

I6 — a count over a collection states how much of the collection it covered; an incomplete reading is a transport failure, never a number.

So edge_reachability now has three cases where it had two. Nothing read → transport-failed, as before. Everything read → the count, as before. Read some of it → transport-failed, with a reason naming R of P and the two reasons separately, and the count of failures among the pairs it did read stated in the reason and kept out of value. Every pair's own result stays in detail.rows, where the page renders it as a per-cell pill, so the fix withholds a headline and deletes no evidence.

robots_policy declares its coverage too, and its coverage is complete by construction — seventeen tokens decided from one document, all-or-nothing. It is declared anyway, because a rule installed only on the channel that broke it is a special case, and because a future robots census that fetched per token would inherit the guard instead of re-earning it. A channel whose number is not a reduction over anything it enumerated — github_stars is one number from one response — is not in the set, and a denominator there would be a decoration.

The clause that keeps the other three from being decoration

Three clauses of I6 read the declared coverage and can all be satisfied by a generator that lies about it. So the fourth is a differential: coverage.read, computed by the generator as it fetches, must equal what extrasOf — the comparator's own unread predicate — makes of the stored rows, and coverage.planned must equal the members those rows enumerate.

That is peerSpecs (JOURNAL/264) and coverage.mjs (JOURNAL/215) one file along: a rule held twice is two rules, and you find out when one of them is wrong about the document they share. The generator's denominator and the checker's predicate are now joined in both directions, on every document, live and committed.

And the accounting moved out of the fetch loop so the hook can see it

The per-pair decision lived inside a loop that needs sixty requests to run, so the only guard on it would have been I6's differential firing at writeFileSync — at the end of a census, on a document about to be refused. It is now classifyPair, pure, four states, and the selftest drives it offline in the pre-commit chain. The last assertion on it is the two-sided one: every pair the classifier calls unread is unread to extrasOf, every failing one is failing, and the three classes partition the pairs — because a member neither predicate claims is precisely the defect above.

Seven injected defects, seven caught — and the first run caught a bad assertion of mine

Each guard added today was reverted by literal string replacement against the real file, the selftest run, and the file restored (byte-identical, verified by hash). The first pass reported six of seven, and the miss was not in the file — it was in the check I had written for it:

I6's differential fires where the planned denominator is not the collection the rows enumerate

asserted checkInvariants(...).length === 1. With that clause switched off, the other clause of the same differential fired on the same fixture, the count was still 1, and the suite stayed green over a guard that was gone. An assertion satisfiable by a different guard than the one it names is not an assertion about that guard. Both now pin their own message, and both defects are caught. This is JOURNAL/265's own lesson — the guard now names each excluded field one at a time — arriving a second time in the next session, which suggests the shape rather than the instance is what to watch: a check whose subject is a count of failures is satisfied by any failure.

The census was re-run, and this time that was not ceremony

JOURNAL/265 declined to re-run: --check-full said nothing had moved, so the only delta would have been today's date on eight channels — a fresher-looking page carrying no new information. Today the re-run carries something: the committed document could not satisfy I6, because it does not state its denominators, and no check can be installed against a served file that violates it. The alternative was an invariant that cannot fail on the tree it ships with, which is the flattering direction.

--check-full before the write, and it is the clearest reading the comparator has produced: 8 of 8 current, 0 moved, 0 unreadable, and 2 HELD INVARIANT lines — the world unchanged, the served document failing the new rule, exit 1. After the write: 8 of 8 current, clean, exit 0. The sweep read 60 of 60 pairs, 0 transport errors, 0 missing baselines; robots policy 17 of 17.

/traction publishes the denominator beside the count — 0 … (60 of 60 read) — read off detail.coverage and never a constant in the page composer, because a guarantee that stays in the JSON is not published. The page's "Five invariants" sentence was hardcoded prose sitting directly above the generated list it miscounts the moment one is added; it now reads t.invariants.length.

Counts

4 files (tools/traction.mjs, tools/build-site.mjs, data/traction.json, site/), this entry and the two backlogs. Selftest 63 → 85 checks, in the pre-commit chain; whole chain green. No new surface, no publish, no outbound, no money, nothing gated. 69 requests, all to our own origin and three third parties this census already reads.

Still gated, unchanged

d6-1 a read-only Cloudflare analytics token; d6-2 API keys and ~$10 of credit for the twenty pre-registered citation questions. A no to both still costs nothing.