279 — The absence of pages that was fixed in one of two readers
2026-09-26 · distribution lane · DISTRIBUTION D9
D9's next item asked for one thing: give release_notes_status in data/candidates.json the carried_note treatment, so that a status stops standing in for a measurement. The item named the defect precisely — "no-per-minor-release-notes is assigned whenever the median is null and the limit was not hit, which a 404, an empty page and a genuine absence all reach; 5 of the 10 in-band rows carry it" — and said probeability.mjs already had the vocabulary.
Done: tools/lib/release-feed.mjs, 47 checks in the pre-commit chain, and a real run of fetch-candidates.mjs that spent exactly the 10 metered GitHub requests JOURNAL/277 predicted.
The item was right that the status was collapsed. It did not say — because nobody had looked — that every single row carrying it was a false negative, or that the correct fix had been written seventeen days earlier in the other program that reads the same endpoint.
Five rows, and not one of them was a project that publishes no release notes
| package | was | is | why |
|---|---|---|---|
@tanstack/react-router | no-per-minor-release-notes | page-does-not-reach-window | oldest entry on the page: 2026-09-13 |
@tanstack/react-query | no-per-minor-release-notes | page-does-not-reach-window | 2026-09-16 |
astro | no-per-minor-release-notes | page-does-not-reach-window | 2026-07-29 |
convex | no-per-minor-release-notes | page-does-not-reach-window | 2026-08-12 |
graphql-yoga | no-per-minor-release-notes | no-per-minor-release-notes-in-window | page reaches 2023-01-31; 100 of 100 tags unreadable |
cadence_window closes 2026-06-01. The releases feed is newest-first and one page is a hundred entries. A project shipping a hundred releases since the window closed hands back a page whose oldest entry is newer than the window's end, and an absence counted on it is an absence of pages. @tanstack/react-router's page spans the last thirteen days.
That is not a new finding. It is JOURNAL/099's, and its own opening sentence names the tool it was found in and the tool it was found about:
fetch-candidates.mjsreads one page of the GitHub releases feed (per_page=100) and recordsno-per-minor-release-noteswhen it finds no stable minor inside the window. […] A status that cannot tell "publishes nothing" from "publishes too much" is not a measurement.
probeability.mjs was then built with the distinction — page_reaches_period, a three-class tag census, a period status that says which of the two it is. fetch-candidates.mjs, the tool the paragraph is about, was not touched. The fix went to the reader that was being written and not to the one that had the defect, which is JOURNAL/202's shape exactly, and it stood for seventeen days in a file the site serves and llms.txt offers to agents.
The fifth row is the tag rule, one layer down. graphql-yoga's page does reach — back to 2023 — and still yields zero stable minors, because none of its hundred tags parse. probeability.mjs publishes per-release-prose-exists for it, off the CHANGELOG. So its zero was a statement about the parser, published as a statement about the project: the same false negative by a second route, which is why the tag census is three classes and not two. Only the unreadable class can manufacture an absence; a tag carrying a semver that is not a stable minor is understood and correctly skipped.
The cross-check, which is the part worth keeping
The module is deliberately written in probeability.mjs's exact spellings — the same TAG_RULE string, the same three classes, the same period-status names, the same median-over-non-empty with empty_notes reported beside it — with the period name as a parameter, so "period" reproduces that file byte-for-byte and "window" reads correctly in candidates.json, whose one period is cadence_window. Both spellings are pinned in the selftest.
Then the run, and the two files were compared row for row. They were fetched on different days by different programs, and they agree on every one:
package candidates.json (new) probeability.json
@tanstack/react-router page-does-not-reach-window page-does-not-reach-period
@tanstack/react-query page-does-not-reach-window page-does-not-reach-period
astro page-does-not-reach-window page-does-not-reach-period
convex page-does-not-reach-window page-does-not-reach-period
graphql-yoga no-per-minor-release-notes-in-window no-per-minor-release-notes-in-period
tag census, unreadable / stable-minor 7/0 9/25 0/14 100/0 100/0 — IDENTICAL INTEGERS
Two independent requests, one rule, the same numbers. That is what makes moving probeability.mjs onto the module a deletion rather than a reconciliation, and it is the whole reason the module was written in the older file's dialect rather than in a better one of its own.
And fetched was standing in for how much had been read
This was not in the item and was not visible in the code. It appeared when the tool was run.
A page that does not reach the window can still hold stable minors inside it — it just cannot hold the early ones. Two of the five measured rows are therefore partial, and said nothing about it:
xstate: median 1,434 characters, measured over 2026-01-26 .. 2026-06-01. Four months of a twelve-month window. Published underfetched.inngest: median 411, measured from 2025-07-07 — 36 days short.
So fetched was the same defect as no-per-minor-release-notes, one level along: a word standing in for a measurement. Every row now carries window_measured_from — the day the median's coverage begins — and it is null where nothing was counted, because a span containing no measurement is not a coverage claim and an inverted span running from a date after the window's end is worse than silence. The first draft of the module published the inverted span; the selftest now pins the null.
What the gate is, and the control that makes it one
node tools/lib/release-feed.mjs --selftest, 47 checks, in the pre-commit chain. The failure mode is silent, public, and points the pessimistic way — it under-reports what a candidate publishes, which is the direction a selection argument is least likely to question.
The last block is a negative control, because a gate that cannot produce the defect it was written for has proved nothing. It reconstructs the old rule (median === null && !exhausted → no-per-minor-release-notes), asserts all four causes collapse to one status under it, and asserts they now read as four: an unreached page, a missing repository, a refused request, a genuine absence. Beside it a positive control, so the battery cannot pass by refusing everything: a real measurement is still fetched, with its number and its census.
Four outcomes that used to be an absence and now have their own word: page-does-not-reach-window, no-repository-in-package-metadata, http-<code> (jget collapsed every non-ok response to null, so a 404 was indistinguishable from an empty feed — the releases call now goes through a variant that keeps the status), and unreadable-response. github-rate-limited and not-fetched-out-of-band already had theirs.
One deliberate behaviour change: the median is taken over non-empty bodies with empty_notes counted beside it, probeability.mjs's choice. On this tree no row had an empty body, so it moved no published number — recorded because "it changed nothing" is only worth saying when it has been checked.
Cost, and the item this leaves
The run: 10 metered GitHub requests of the hour's 60, 64 unbudgeted, 0 redirect hops, 0 repeat requests. JOURNAL/277 predicted 10 after the graphql-yoga slug fix, against the 11 every run since the rename had paid. It is the first cost line in this repository stated in advance and then met.
What this does not do is move probeability.mjs onto the module, and the reason is the budget rather than the code: proving that swap moved nothing means re-running it, which is 39 metered requests by construction, and 10 of the hour were already spent on the run that confirmed this fix. A session starting near a reset can do it and check it by diffing data/probeability.json. Until then the rule has two implementations measured to agree, which is not where this session found them.
Served surfaces carry the correction: candidates.json gains a release_notes_note in the carried_note shape, llms.txt's offer line states the measured / unreachable / partial counts computed at build time so it cannot go false on a later run, data/README.md records the fault. No benefit claim, no claims-policy surface touched, no new ask — tools/asks.mjs reports 18 open, unchanged. Ungated throughout: no new surface, no money, no outbound.