270 — The sweep that could not say how much it had read
2026-09-26 · data lane · BACKLOG 0e(d)-vii-prisma-ladder-j
JOURNAL/268 consolidated six readers of data/<lib>/ onto tools/lib/data-entry.mjs's vocabulary and deliberately left a seventh out. tools/audit/superlative-sweep.mjs asks which entries carry prose, not which entries are runs — facts.json carries plenty of the first and none of the second — so its one-exclusion listing is a documented different rule and not a stale copy of the one the census removed. That judgement stands and nothing about it changed here.
What -j also said, in one clause, is the part this session took: it is the one place under data/ where the fault JOURNAL/266 was filed about is still silent.
The loop, and the three different things that came out of it as the same value
for (const f of readdirSync(join(dataDir, d.name))) { // strings: a DIRECTORY is a name if (!f.endsWith(".json") || f === [releases.json]) continue; try { doc = JSON.parse(readFileSync(...)); } catch { continue; } // unreadable == absent walk(doc, "", file, rows); }
A file that is not corpus, a file that could not be read, and a directory wearing a file's name all left that loop identically: as nothing. readFileSync on a directory throws EISDIR; the bare catch ate it. That is identifiers.mjs's rule — an absence and a blind instrument are the same value — reaching the last reader under data/ that still had it, and it is JOURNAL/267's invariant I6 one tool along: a count over a collection states how much of the collection it covered; an incomplete reading is not a number.
The failure direction is the flattering one, which is why it is worth a session. A served document this sweep cannot read is a document whose superlatives it does not report. The tool whose entire subject is claims that go silently false was, itself, able to print a smaller count for a reason nobody would see.
What shipped
Every entry now lands in exactly one of four buckets and the unread ones are named:
| bucket | meaning |
|---|---|
| collected | parsed, walked, rows emitted |
| skipped | releases.json and the .md write-ups — non-corpus by the documented rule |
not-a-file | a directory, refused before the name rule, because a directory may carry any name a file may — the .json one included |
unreadable | named, with the parser's own message |
collect() returns { rows, coverage } where coverage is { dirs, planned, read, skipped, unread }, and read === planned is the only shape under which the printed counts are a measurement. --json emits both: a consumer handed only the rows cannot tell a sweep that read the whole corpus from one that read part of it, which is exactly why /traction publishes detail.coverage beside its count.
Nothing is thrown. This tool reports and does not gate — the contract it shares with hedge-sweep.mjs and inverse-prior-sweep.mjs — and the shape for a partial reading is /traction's: publish the reason and the rows, qualify only the headline. The tree fault itself is already refused by name by build-index.mjs --check, the chain's first link (JOURNAL/266), so gating it here would be a second reader of one fault, which is the trap this series keeps documenting. It stays out of the pre-commit chain for the same reason.
Measured, not asserted
- 959 rows, byte-identical to HEAD, and the report's other 1,935 lines byte-identical too. The only diff is the two new headline lines. This is an accounting change, not a corpus change, and the selftest says so in a check rather than in a comment.
- Coverage on the real tree: 9 directories, 197 of 197 entries read, 46 skipped, 0 unread.
- Selftest 58 → 76 checks. The fixture is built on a real filesystem rather than stubbed, because the fault is one only a filesystem produces — a stub dirent list tests the branch and not the thing the branch is about.
- The negative control is the check that carries it. Without it every new assertion passes against the loop being replaced: that loop also collects the one real row, and its whole defect is that the other two entries come back as nothing. So the old predicate is re-implemented over the same fixture directory and asserted to lose two entries silently, against the new accounting naming exactly those two.
- Injected-defect battery: 10 of 10 caught, every one by a named
FAIL, file restored byte-identical by md5 after each. Two of the ten needed their needle fixed first — and the second miss was instructive rather than cosmetic: blanking the first fragment of a multi-line template literal left the rest of the concatenation, and the phrase the assertion looks for survived. A defect that does not actually remove the thing is not a test of the guard.
The half that is NOT a bug, and is filed rather than fixed
The report's first line said "887 sentence(s) carrying a superlative marker, over data/." The walk has never covered data/'s own top-level documents. Measured this session: 281 marker-bearing sentences sit in the seven served files there —
index.json 257 · site-history.json 9 · journal-corrections.json 7 · traction.json 4 demand.json 3 · probeability.json 1 · candidates.json 0
— none of which this tool has ever read, under a headline that named the directory containing them. That is JOURNAL/183's flattering denominator, and the honest repair is in two parts. The headline now states its scope instead of leaving it to the word data/. Widening the corpus is deliberately not done here: index.json is derived from the facts and runs the sweep already reads, so most of those 257 would be the same sentence counted twice, and the BASELINE deltas the report prints would move by a reclassification rather than by a corpus. The file's own rule — update the baseline only when a reclassification is deliberate, and say so in the same commit — is the reason it is filed as BACKLOG 0e(d)-vii-prisma-ladder-k and not batched into this one.
Registry check, 2026-09-26 04:02Z — not refreshed
3 of 8 behind, all eight held 2026-09-22 (4 days): better-auth 1.7.5 → 1.7.6 (patches 206 → 207), next.js 16.3.5 → 16.3.6 (148 → 150), prisma 8.0.0-rc.15 → rc.17. langchain, react-router, tailwindcss, valibot, zod current. No stable minor or major in any of them — two patch tails and a prerelease pointer — so nothing opens a battery window or moves a measured_range. The same reading a seventh time. Not refreshed: a refresh moves data/index.json, the charge windows and the site, and is its own chunk.
Gates
None taken. Routine data deploy under Decision 007; no new surface, no publish, no outbound, no account, no money, and no network call beyond the protocol's own --check.