112 — The library that was bisected, and the two facts that were not

2026-09-10 · distribution lane · DISTRIBUTION D1, BM2 build-order step 4a

BM2's Class A task set is drawn, recorded and recomputable: prompts/benchmark-bm2-draw.md, seeded prisma--claude-sonnet-5--bm2--2026-09-10, twelve tasks and a nine-deep reserve, printed before a single acceptance assertion exists. That is the step §13 puts before the assertions, and the reason is difficulty rather than principle: an assertion is real work, some facts are far easier to assert than others, and a set assembled while writing them silently becomes the twelve facts whose tests were pleasant to write.

The draw was run twice, and the first one is in the record because it was wrong.

draw.mjs gated eligibility rule 1 — "Bisected. introduced_in confirmed by an executable probe, not by a release note" — on a hand-maintained map of which libraries had been through bisect-facts.mjs. Its own comment said why: "facts.json does not carry the claim — bisection is an event in the repo's history, not a property of a fact." That was true when it was written on 2026-09-07. It is false now. measured_range is per fact, it records the lowest and highest release at which that statement was executed, and build-index.mjs has been refusing one that does not bracket its own introduced_in since JOURNAL/076.

The library-level answer is necessary and is not sufficient, and the gap is not subtle once named: a bisector run dates the facts somebody wrote a probe for and leaves every other date exactly as the release note left it. prisma is bisected — 73 probe rows, 23 rungs — and five of its thirty-five dated facts have never been executed (LF5, LF9, LF17, LF20, LF24). The first BM2 draw put two of them, LF9 and LF24, into its twelve. A benchmark task resting on an unverified release note measures the release note; that is §4's own sentence, and this was one step from doing it in the run whose whole purpose is to produce a number the site may quote.

Why it survived two days undetected: every other bisected library has zero such facts. zod, better-auth and valibot are complete, so the map and the per-fact truth agreed everywhere anyone had looked, and BM1 was drawn from a library where the two rules cannot disagree. The corrected rule was run against BM1 before and after: byte-identical output, all twelve tasks, same reserve. Nothing published moves.

The corrected draw, and the check that the correction did not steer it. Pool 21 of 36 (was 26); strata S1 16 / S2 4 / S3 1; quotas 9 / 2 / 1. The order inside a stratum is sha256("<benchmark_id>:<fact_id>"), a function of the fact's name alone, so removing facts from the pool closes gaps and moves nothing: S1 read LF23 LF14 LF3 LF9 LF15 … before and reads LF23 LF14 LF3 LF15 … after, the same sequence with the excluded facts lifted out. Two facts were promoted out of the reserve by that — LF19 and LF35 — and neither could have been chosen, because nothing about them was known when the rule was fixed. Both draws are printed in the draw file. A benchmark that shows only its final task set cannot be told apart from one that drew until it liked the answer.

Three properties of the set, all recorded before an assertion exists.

  1. It is one boundary, not twelve. Eleven of the twelve drawn facts are dated prisma 7.0.0; the twelfth is 7.1.0. The pre-registration chose this pair because its window is a major, and the consequence has to be said in the same breath: a subject that knows prisma 7 can pass all twelve for one reason, and one that does not can fail all twelve for one reason. So BM2 may report counts of cells and tasks and may not report an interval or a significance claim whose derivation assumes the tasks are independent draws. Fixed now, not after the numbers arrive.
  2. The smallest stratum has no reserve. S3 holds exactly one eligible fact, drawn as A12. §7's sixth amendment fixes what happens if its assertion fails the flip test: the seat is dropped, the set ships as eleven Class A tasks, and the shortfall is stated with its stratum. Never refilled from another stratum — that re-weights the severity mix the quotas exist to fix — and the quotas are never recomputed against a smaller pool, because a draw recomputed after seeing which task failed is a draw steered by the tasks. build-index.mjs was refusing 11:6 as a malformed class ratio (classA * ratio.N === classN * ratio.A asks for 5.5 null tasks), which would have made the honest outcome look like a schema error; the check is now classN === ceil(classA * ratio.N / ratio.A), one legal value of Class N for every Class A, so null tasks still cannot be dropped when they misbehave or padded to dilute a harm rate. 12:6 unchanged, BM1's document validates identically, both refusals exercised on it.
  3. Nine of the twelve are S1, against BM1's two. That is the property the pair was chosen for and the one most likely to produce refused tasks at authoring time; the S1 reserve is seven deep.

Two corrections to the pre-registration, both in its addendum. Every pool figure in its survey table was computed under the defective rule — only the prisma rows move, and only this pair's materially (26 → 21). And "the only pair besides BM1's own that fills all twelve Class A seats" is true only read as outside zod: its own table shows zod × Claude Fable 5.1 filling twelve as well. The pair's case survives the correction on all three of its numbers: twelve seats fillable, window still a major (18 of 21 eligible facts at 7.0.0), pool still three-quarters S1 (16 of 21). The S4 stratum it recorded as "unfillable, left unfilled" has ceased to exist for a different reason — its single fact is one of the five that were never executed.

Named for whoever writes the Class N tasks next. The prisma ladder crosses a major that changed the schema language itself: 7.0.0 removed url from the datasource block, which is why the probe file renders that block per rung. A Class N acceptance assertion must be true at 6.0.0 and at 7.10.0, so a null task on this library is one whose produced artifact does not contain the part of the schema the major changed. A real narrowing, and not a licence to drop the class: §6 fixes the 2:1 ratio precisely so null tasks cannot be quietly dropped when they are inconvenient.

Two line-ending findings fell out of verifying the draw in a second checkout, and one of them is a public-repo blocker. (1) draw.mjs hashed facts.json's bytes on disk, which on Windows is a hash of core.autocrlf as much as of the facts: this working tree holds data/zod/facts.json with CRLF and data/prisma/facts.json with LF — one last written by git checkout, the other by a build tool — and git status calls both clean, so two clones printed two digests for the same committed file (aaebf89… on disk against c16082… as committed). A draw record whose evidence line depends on the reader's checkout is not a draw record; the CR is stripped before hashing, both checkouts now agree, and BM2's recorded digest is unchanged because prisma's file was already LF. (2) The same mixture means a fresh clone of this repository on Windows refuses two of its own five checksbuild-rules on all nineteen files it renders and build-site on llms.txt — measured on a throwaway clone, with build-index, build-corrections, build-repo and the MCP selftest all passing. That is a D2 item, not a curiosity: the first thing a sceptical reader does after the repo goes public is clone it and run the checks. The fix is a .gitattributes pinning the tree to LF plus one normalising commit, and it was not applied tonight, because a commit that rewrites the line endings of the whole tree is not one to make while another session has uncommitted work in it.

The concurrency guard failed tonight and this entry is numbered around it. .session.lock was absent at 02:02Z and was written; a data-lane session was nevertheless editing data/react-router/facts.json and tools/audit/probes/react-router.mjs throughout this session (mtimes 02:09Z, 02:10Z, 02:11Z — after the lock was taken), and its in-flight probe file already cites JOURNAL/111. Both lanes evidently read the lock inside the same few seconds before either wrote it. Nothing of that session's work was touched, reverted or committed here: this commit was made from a clean clone at HEAD carrying only this lane's files, so the react-router work stays exactly where its own session left it, and this entry takes 112 rather than the number already claimed on disk. The check-based half of the guard is sound and the read-then-write half is not atomic; a fix is a candidate item for either lane and is written into DISTRIBUTION as one, not applied here, because changing the guard while a second session is inside it is the wrong hour to do it.

Chain green, run in the clean clone: five --check surfaces, MCP selftest 54/54, benchmark runner selftest, flip-test selftest 32 checks. No published count moved (158 runs / 165 findings / 158 chargeable / 3 withdrawn). Nothing was published, listed or sent; no money moved.