079 — The null tasks that had to look like the real ones: the harm denominator, and a decoy that charged three minors early
2026-09-07, distribution lane (DISTRIBUTION D1, step 4f). The six Class N tasks are authored and admitted. BM1's task set is complete: eighteen tasks, twelve Class A and six Class N, the pre-registered 2:1 ratio, every one of them gated against the fifteen-rung 4.x ladder. Nothing here needed a Sam gate; the first gate in this lane is still D2's repo flip.
What a null task is for, and the design rule it forced
§6 pre-registers Class N as the only place the correction pack can be caught doing harm — refusing a fine API, pinning a version nobody asked about, rewriting working code toward a rule that does not apply. Prediction B4 says harm will be non-zero in our own artifact.
That makes the obvious way to write a null task useless. A task on some unrelated corner of zod would pass in every arm and measure nothing the Class A tasks do not already measure. Harm only shows where the pack has something to say and should not say it. So each of the six sits deliberately in the neighbourhood of a fact the pack corrects, on an API whose behaviour is flat across all fifteen rungs:
| Task | Neighbour fact | What it asks for, that the correction is not about |
|---|---|---|
| N1 | LF1 / LF2 / LF3 | .extend() then .pick() on an unrefined receiver |
| N2 | LF21 / LF7 | a populated discriminated union, not an empty one and not z.xor |
| N3 | LF26 | coerced fields whose keys are all present |
| N4 | LF1 / LF2 / LF3 | a plain .refine() that is never narrowed afterwards |
| N5 | LF10 / LF27 | an ordinary object default, not a tuple's, not inside an optional |
| N6 | LF16 / LF17 | a one-way transform that is neither a codec nor a slug |
N3 is the sharpest of them. LF26 says an object with coerced fields errors on a missing key from 4.4.0 instead of silently coercing undefined — so the prompt guarantees every key is present, and the assertion never tests the missing-key case. The subject is never asked to know the fact; the pack is given every opportunity to over-apply it anyway.
N5's load-bearing check is a supplied retries: 0 surviving as 0. The two shapes a hand-rolled default takes are v.retries ?? 3 and v.retries || 3, and the second silently turns a supplied 0 into 3 — a bug in code the pack had no reason to touch, which is exactly what harm looks like.
The step-4c discipline, run first
Eight candidate propositions were run against all fifteen installed rungs before any of the six tasks was authored — the order of operations JOURNAL/073 paid for. All eight were flat, so six shipped and two (nested arrays with an indexed error path; z.enum with .optional() versus .nullable()) are spares. The gate then re-measured the committed references: +++++++++++++++ for all six.
The twelve Class A tasks were re-run in the same pass. Every profile is byte-identical to the one recorded at admission.
The decoy, and the prediction inside it that was wrong
A gate that has only ever said yes is not evidence of anything. flip-test.mjs's Class N branch had an offline selftest against synthetic verdicts and had never refused a real artifact on a real ladder, so one was built to be refused — the rule LF21 and A2 established, applied to the null gate.
N7, the decoy: "validate that a webhook endpoint is an http(s) URL." Exactly the everyday shape a null slot wants, and z.httpUrl() is the obvious way to write it. It was built expecting a boundary at 4.4.0, because the fact it smuggles in is LF8, dated 4.4.0.
The ladder put it at 4.1.0:
REFUSE N7 N null
correct ..+++++++++++++
class N must pass at every rung; it fails from 4.1.0
The refusal was right; the reason was not the one predicted. z.httpUrl() has existed since 4.1.0, and what moved at 4.4.0 is only its strictness about a missing slash after the protocol. The decoy's assertion never tests a missing slash — it tests that ftp:// is rejected, which the function has done since it existed — so it charges on the API's arrival, three minors below the fact it was meant to rest on.
LF8 is not wrong and nothing in data/zod/facts.json moves. Its change_kind is stricter, its statement dates only the missing-slash rejection to 4.4.0, and the shipped task on it, A7, rises at 4.4.0 exactly as the fact says. The reminder is about tasks, not facts: a task charges where its assertion flips, which need not be where its fact moved, and only the ladder can tell you which. Both artifacts and the assertion are kept as N7.decoy.reference.mjs and N7_DECOY, under the same naming convention as A2's LF4 and A12's LF21 refusals, so the refusal re-runs.
State
- BM1's task set is complete and frozen at eighteen: A1–A12 (Class A, five previously charged) and N1–N6 (Class N). 18 of 18 admitted; ratio 2:1 as pre-registered.
- Full pre-commit chain green: flip-test selftest 25 checks, runner selftest 8 cells, build-index (154 runs, 164 findings, 7 facts files), build-corrections (7 packs), build-site (251 pages, 6223 links), MCP server 42/42.
- No public claim was made or unlocked by this session. The claims policy stays where it was: the benchmark has an instrument and a complete task set, and no results. Nothing on the site, in any README or in any draft may say the packs help until step 5 has run and produced a number.
Next chunk: step 5, BM1 itself — the arms run through tools/benchmark/run.mjs, subjects spawned via the Agent tool, results appended to data/benchmark/. That is the first session in this lane that produces a number, and B2, B3 and B4 are all pre-registered against us.