Skip to content

Commit a564d62

Browse files
martex-devclaude
andcommitted
M23: the switch was declared, hashed, translated -- and read by nothing
The v6 ladder finished and refuted its prediction. The refutation adjudicates nothing: B9's `conservative_escalation` reached the kernel's Mechanisms, was handed to `_replicate(conservative=...)`, and stopped there. Neither call to `_escalate` passed it. B8 and B9 bought identical seed counts on all 180 outcomes -- not similar, identical -- so v6 measured two custody draws of one arm. M22's probe cannot see this: the field *is* on Mechanisms, so flipping it does change the object. Three checks now, each catching what the one above cannot: ruff's ARG (which names the bug in one line, and would have cost a second rather than a six-hour ladder), an AST test over the kernel that no config edit can switch off, and a slow test that runs both arms and compares what they bought. Enabling ARG also found a genuinely dead parameter in `ExperimentRunner._charge`; it is deleted rather than exempted. v6 stands on the record with its verdict, and the paper says in the same breath that the verdict is void. Its one gain is unplanned: B9 minus B8 is a negative control with a true difference of zero, and its interval covered zero -- the first calibration check this project's bootstrap has had. Protocol v7 re-registers the prediction against a switch that is connected. Two hand-maintained lists found while recording it. CI re-scored five results files by name and there were six on disk, so results.v6 would have gone unchecked; it is a glob now. And every FINDINGS.md chapter heading after the first has been glued to the preceding paragraph since the document existed -- a `{%- else %}` eating a newline, invisible to both the drift check and M18's structural test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8460649 commit a564d62

14 files changed

Lines changed: 542 additions & 29 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,19 @@ jobs:
158158
- name: every committed results file re-scores to its own summary
159159
run: |
160160
uv run python -c "
161+
import sys
161162
from pathlib import Path
162163
from nullius.benchmark.metrics import read_results
163-
for path in (Path('benchmark/results.lock.json'),
164-
Path('benchmark/results.v2.lock.json'),
165-
Path('benchmark/results.v3.lock.json'),
166-
Path('benchmark/results.v4.lock.json'),
167-
Path('benchmark/results.v5.lock.json')):
168-
if not path.exists():
169-
print(f'{path.name}: not committed yet'); continue
164+
# Globbed, not listed. A hand-maintained tuple beside the registry is
165+
# how the v6 results file would have gone unchecked here: this job
166+
# named five paths and there were six on disk. Fourth instance of the
167+
# same shape, after the job that listed protocols, the ladder that ran
168+
# eight arms under a nine-arm plan, and the paper's results-path table.
169+
paths = sorted(Path('benchmark').glob('results*.lock.json'))
170+
if not paths:
171+
sys.exit('no committed results files found; the glob matched nothing')
172+
print(f'{len(paths)} committed results file(s)')
173+
for path in paths:
170174
report, runs = read_results(path)
171175
print(f'{path.name}: protocol {report.protocol_hash[:16]}, {len(runs)} arms')
172176
print(' ' + report.prediction_reason)

BUILD_PLAN.md

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
This is the executable plan derived from [`docs/`](docs/). The design documents say *what* to build and *why*; this says *in what order*, *with what acceptance test*, and *what changes because of the machine we're actually on*.
66

7-
**Status:** M0–M22 complete; v5 landed, v6 running. The live path is wired but unspent (mock-driven throughout; the first live run awaits an API key). M12's code-generation half is blocked on both a key and Docker. Nothing below is claimed as done until its acceptance criteria are green in CI.
7+
**Status:** M0–M23 complete; v5 and v6 landed, v7 registered and not yet run. **V6's result does not adjudicate what v6 registered** — its treatment arm did not implement the mechanism it names; see M23. The live path is wired but unspent (mock-driven throughout; the first live run awaits an API key). M12's code-generation half is blocked on both a key and Docker. Nothing below is claimed as done until its acceptance criteria are green in CI.
88

99
---
1010

@@ -464,7 +464,7 @@ It lands **under half the true value 8.9% of the time**. When it does, the escal
464464

465465
**A third instance of the same bug shape.** The paper's results-path table was a dict keyed by protocol version, and it raised a `KeyError` the moment v6 was registered. That is the third time something keyed by protocol version was maintained beside the registry instead of computed from it — after the CI job that listed protocols by hand, and the ladder that ran eight arms under a nine-arm plan. It is derived now.
466466

467-
All six protocols verify and rebuild identically. **v6 has not been run**: at roughly double B8's escalation it is a ~6-hour ladder, and the v5 run has to finish first.
467+
All seven protocols verify and rebuild identically. **v6 has since been run, and refuted — against an arm that never sized anything conservatively.** The upper bound described here was built correctly and measured correctly; what was missing was the wire from the arm to it. M23 has the count of what that cost.
468468

469469
---
470470

@@ -671,6 +671,106 @@ guards it asserts the overlap still exists rather than asserting it away.
671671

672672
---
673673

674+
### M23 · The switch was declared, hashed, translated — and read by nothing ✅
675+
The v6 ladder finished: twenty-two passes, ten arms, `benchmark/results.v6.lock.json`.
676+
677+
```
678+
PREDICTION REFUTED coverage (B9-B8) = +0.0333, 95% CI [-0.0111, +0.0889]
679+
```
680+
681+
**That refutation is not about conservative sizing.** B9 is B8 with one boolean,
682+
`conservative_escalation`, which sizes the escalation from an 80% chi-square upper bound on the
683+
noise instead of the point estimate. The field is declared on the arm, hashed into protocol v6,
684+
carried by `mechanisms_for` into the kernel's `Mechanisms`, read at `kernel.py`, and handed to
685+
`_replicate(conservative=…)`. **`_replicate` never used it, and neither call to `_escalate`
686+
passed it at all.** So the flag crossed every boundary and stopped one line short of the
687+
calculation.
688+
689+
The signature is unmistakable once looked for:
690+
691+
| | B8 | B9 |
692+
|---|---|---|
693+
| seed counts, 180 outcomes | 5×60, 6×6, 7×3, 8×3, 9×3, 11×6, 14×6, 18×6, 19×6, 21×6, 23×3, 24×72 | *identical* |
694+
| total seeds bought | 2,703 | 2,703 |
695+
| outcomes where the two differ || **0 of 180** |
696+
697+
Not similar. Identical, item by item and replicate by replicate. After threading the flag
698+
through both call sites, the same two items move from 12 seeds to 21.
699+
700+
**Three checks, and the one that was missing.** Each catches what the one above it cannot.
701+
702+
1. `unread_switches()` (M22) flips a field and asks whether `Mechanisms` changes. It found
703+
`reviewer`. It cannot find this one — `conservative_escalation` *is* a field on `Mechanisms`,
704+
so flipping it does change the object.
705+
2. **A parameter accepted and never read.** `ARG` is now selected in ruff, and it names the bug
706+
in one line: `kernel.py:922 ARG002 Unused method argument: conservative`. This is the check
707+
that would have cost a second rather than a six-hour ladder. Five modules are exempted —
708+
`@detector`, `@register_view`, `@transform_op`, SQLAlchemy's `TypeDecorator` and its event
709+
listeners, where the signature *is* the interface — and `kernel.py` deliberately is not.
710+
`tests/test_switches.py` asserts the same rule over the kernel by AST, so no edit to
711+
`pyproject.toml` can switch it off where it cost something. Enabling the rule also found a
712+
genuinely dead parameter in `ExperimentRunner._charge`, which took `artifacts` and billed
713+
from `result.outputs`; it is deleted rather than exempted.
714+
3. **Running both arms and comparing what they bought.** Only an execution can prove a switch is
715+
connected — an argument can be read, passed on, and still reach nothing, which is exactly what
716+
happened here. Marked slow, two items, and it fails on the pre-fix kernel.
717+
718+
**What v6 is evidence of.** Not conservative sizing, which it never ran. B9 minus B8 is an
719+
*unplanned negative control*: two arms that were operationally identical, differing only in
720+
registration id and therefore in the Custodian's evaluation draw. Its interval is a measured
721+
noise floor for this ladder at three replicates — coverage +0.033 [−0.011, +0.089], and verdict
722+
accuracy −0.044 [−0.117, +0.028] computed afterwards and labelled as such. **The interval
723+
covered zero on a contrast whose true difference is zero**, which is the first calibration check
724+
this project's bootstrap has had. One draw cannot establish coverage, and it is not offered as
725+
if it could; it is one honest observation where there were none.
726+
727+
It also sets the scale for the neighbours. v6's other three contrasts — B4−B3 = +0.039,
728+
B6−B4 = +0.006, B6−B7 = −0.044 — are all the size of a difference now known to be zero, and all
729+
three already span zero. The bootstrap was saying so; the control is the first thing to
730+
corroborate it from outside.
731+
732+
**v6 stands on the record.** Its lock file is committed, its verdict is reported as refuted, and
733+
the paper says in the same breath that the refutation adjudicates nothing. Deleting a registered
734+
protocol's result because the run embarrassed the code is the exact move this project exists to
735+
make impossible. **Protocol v7 re-registers v6's prediction** — same ladder, same adjudicated
736+
contrast, hash `0dac6ca41fdb` — against a switch that is connected. It has not been run.
737+
738+
**The shape, for the third time.** M20: a switch that acts only through a model, reported as
739+
mechanism. M22: a switch that reaches no code at all. M23: a switch that reaches the kernel and
740+
is dropped inside it. Each was found by asking a *different* question about the same claim —
741+
that an arm's declared mechanisms are the mechanisms it runs — and each time the previous
742+
milestone's check was structurally unable to see the new case.
743+
744+
**And a fourth hand-maintained list.** The CI job that re-scores every committed results file
745+
named five paths in a tuple. There were six on disk, so `results.v6.lock.json` — the run this
746+
whole milestone is about — would have gone through CI unchecked. It is a glob now, and it fails
747+
loudly if the glob matches nothing rather than passing vacuously.
748+
749+
**And M18's bug, one shape along.** Every chapter heading in `FINDINGS.md` after the first has
750+
been rendering glued to the previous paragraph — `…rests on them.### Protocol v2` — since the
751+
document existed. A `{%- else %}` swallowed the newline that ended the footnote. M18's structural
752+
test asserts that a heading has a blank line before it, and could not see this because the line
753+
does not start with a hash; the drift check could not see it because the file and the generator
754+
were wrong together. It is fixed, and the guard is now *no hash anywhere but the start of a
755+
line*, which fails on five lines of the previously committed file.
756+
757+
**Acceptance**
758+
- `conservative` is passed at both `_escalate` call sites, and B9 buys strictly more seeds than
759+
B8 on items that escalate below the ceiling.
760+
- `ruff check` selects `ARG` and passes; the kernel is not exempted, and an AST test asserts the
761+
same rule over it independently of the config.
762+
- The AST guard fails on `git show HEAD~1:src/nullius/kernel.py` with exactly one finding,
763+
`_replicate(conservative)`.
764+
- Protocol v7 verifies, rebuilds identically, and appears in the paper as registered and not yet
765+
run.
766+
- The paper and `FINDINGS.md` carry the flaw and the limitation, and CI's drift check passes.
767+
- CI re-scores every `benchmark/results*.lock.json` found by glob — six of them, not the five
768+
the tuple named — and exits non-zero if it finds none.
769+
- No `#` appears anywhere but the start of a line in `FINDINGS.md`, which the file committed at
770+
`8460649` fails on five lines.
771+
772+
---
773+
674774
## 2. Working agreement
675775

676776
- **`main` is always green.** Every milestone lands as a PR with CI passing on Linux and Windows.

0 commit comments

Comments
 (0)