-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sim,evidence): the 25 → 15 mm world-voxel lever, measured end to end — and it is null - #252
feat(sim,evidence): the 25 → 15 mm world-voxel lever, measured end to end — and it is null #252AdrianLlopart wants to merge 9 commits into
Conversation
f51977a to
6e60f60
Compare
`sudo apt-get update -qq` exits 100 if *any* configured index fails, and the ubuntu-24.04 runner image ships third-party lists this repo does not use. From about 17:16 UTC on 2026年09月09日, dl.google.com's chrome index returned a hash-sum mismatch — Google had republished it and the CDN had not caught up — and every run since failed at this step, four in a row on #252. None of ffmpeg, libosmesa6 or just come from that repo; all three are in the Ubuntu archive, whose index downloaded fine each time. The downstream damage is worse than one red step: the job skips every lane and `Attest lane coverage` then reports all nineteen as "selected but produced no ledger record", which reads like a coverage regression rather than an apt failure. Tolerate the update and keep `apt-get install` as the gate — a package that genuinely cannot be fetched still fails on the next line, so nothing real is suppressed. Scoping the update to the Ubuntu archive instead would assume the runner keeps its sources in `/etc/apt/sources.list` rather than deb822 `sources.list.d/ubuntu.sources`; that is unverified here, and getting it wrong leaves no index at all. `test-ros2.yml` runs its apt inside a ROS container image with no third-party lists, so it is left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015odfPTtYuixeCXEW4rZ3Gq Signed-off-by: Adrian <adrianllopart@gmail.com>
8a1ba80 to
fcfb57d
Compare
`sudo apt-get update -qq` exits 100 if *any* configured index fails, and the ubuntu-24.04 runner image ships third-party lists this repo does not use. From about 17:16 UTC on 2026年09月09日, dl.google.com's chrome index returned a hash-sum mismatch — Google had republished it and the CDN had not caught up — and every run since failed at this step, four in a row on #252. None of ffmpeg, libosmesa6 or just come from that repo; all three are in the Ubuntu archive, whose index downloaded fine each time. The downstream damage is worse than one red step: the job skips every lane and `Attest lane coverage` then reports all nineteen as "selected but produced no ledger record", which reads like a coverage regression rather than an apt failure. Tolerate the update and keep `apt-get install` as the gate — a package that genuinely cannot be fetched still fails on the next line, so nothing real is suppressed. Scoping the update to the Ubuntu archive instead would assume the runner keeps its sources in `/etc/apt/sources.list` rather than deb822 `sources.list.d/ubuntu.sources`; that is unverified here, and getting it wrong leaves no index at all. `test-ros2.yml` runs its apt inside a ROS container image with no third-party lists, so it is left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015odfPTtYuixeCXEW4rZ3Gq Signed-off-by: Adrian <adrianllopart@gmail.com>
fcfb57d to
0efeb03
Compare
...findings Rebased onto master past #257/#258, which had rewritten the same §5 the resolution branch measures. Two conflict hunks resolved by keeping both sides where they were complementary and the later fact where they were not: - PLAN §5 lever 3: this branch's cost measurements (kernel 0.825 ms, rasterize 1.60 ms, wire +55 ms p99, carry speed n=5) stand; master's "octree->grid conversion still unmeasured" was wrong the moment this branch landed and is replaced. Master's withdrawal of "drop baguette" stands over this branch's [x] for it — baguette scored 1/3 with the gate off. - "Decide #217": this branch's [x] closed-COMPLETED stands; it is a fact. - 10-tools.md: master's two detector entries plus this branch's octomap_resolution_env, markers refreshed post-hook. Then three things neither side had: 1. The lever-3 reach bound from #258 written into this branch's own item — "recovers 8.7 mm in every stop class" is true of the term, not the stops: 25->15 mm plausibly converts 5 of 20; the 9 with 23-85 mm of excess are #254's population and untouched by any resolution. 2. A finding the gate-OFF arm exposed: with the world check OFF, seven runs still carry a kernel stop, and every adjudicable one is a hull-fidelity false positive on the pair the kernel named — link5<->link7 three times at -27..-32 mm reported against +1.4..+4.9 mm certified, link1<->link7 at +87.9 mm, payload<->link1 at +25/+69 mm. No voxel in the path. These sit inside the 62.5 % ceiling and depress it. link5<->link7 is the pair #191 retired the ACM exemption for. No issue exists; filing one is the first recommendation. 3. "What to test next", ordered by what is unblocked and what each answer changes: adjudicate the self-collision false positives; answer #259's never-armed allowance question; run the 15 mm A/B with a falsifiable prediction of ~5 of 20; widen carry speed to n=13; the top-up. Refs #252, #253, #254, #256, #258, #259 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2 Signed-off-by: Adrian <adrianllopart@gmail.com>
AdrianLlopart
commented
Sep 10, 2026
Rebased onto
|
| hunk | kept | dropped, and why |
|---|---|---|
| PLAN §5 lever 3 | this branch's cost record — kernel 0.825 ms, rasterize 1.60 ms, wire +55 ms p99, carry speed n=5, the _world_voxel_max_cells correction |
master's "octree→grid conversion still unmeasured" — it was wrong the moment this branch landed |
PLAN "drop baguette" |
master's withdrawal | this branch's [x] — baguette scored 1/3 with the gate off on the fixed harness, so the 0/11 behind the strike was starved runs |
| PLAN "Decide #217" | this branch's [x] closed COMPLETED |
master's open item — it is a fact |
10-tools.md |
master's two new detector entries plus this branch's octomap_resolution_env |
nothing; markers refreshed after the hooks ran |
validation_matrix.py and the evidence ledger auto-merged; mypy --strict, ruff, and the 110 tests across both branches' suites pass on the merged result.
What the unification commit adds that neither side had
-
A reach bound on this branch's own lever-3 item. "Recovers 8.7 mm in every stop class" is true of the term, not the stops. Tracing the 20 adjudicable gate-ON stops to certified truth (fix(plan,evidence): the 18.9 mm over-approximation is bimodal, not "the cell" #258 ): only 8 have an excess a 25 mm cell can account for, 3 of those already sit under 12.99 mm, so 25 → 15 mm plausibly converts 5 of 20. The other 9 (excess 23–85 mm) are Safety-WG ruling needed: ADR-0101 modeled static fixtures — rule on 8.56 mm, not 16.2 mm #254 's population and no resolution reaches them. That's now written next to the "Actionable, with two caveats" paragraph rather than contradicting it from another file.
-
A finding the gate-OFF arm exposed. With the world check off, seven runs still carry a kernel stop, and every adjudicable one is a hull-fidelity false positive on the exact pair the kernel named:
link5↔link7three times at −27...−32 mm reported vs +1.4...+4.9 mm certified;link1↔link7at +87.9 mm; payload↔link1at +25/+69 mm. No voxel in the path. They sit inside the 62.5 % ceiling and depress it, andlink5↔link7is the pair fix(safety): remove the proven-real panda_link5<->panda_link7 ACM exemption #191 retired the exemption for. No issue exists yet. -
"What to test next", ordered by what is unblocked and what each answer changes. The 15 mm A/B this PR lists as missing is item 3, with a falsifiable prediction attached (~5 of 20 stops convert; fewer than 3 means the class-B bound is wrong).
The PR description above predates all of this; the numbers in it still hold, they are just no longer the whole picture.
I wrote that "every adjudicable one is a false positive" by comparing the
kernel's reported depth against the certified mesh distance without charging
the pair its budget. That is exactly the error hal_admissible_gap_m's docstring
warns about — "recomputing only the voxel term understates it fourfold ...
turning correct stops into false-positive" — and I made it in a safety-adjacent
entry. Run through the shipped adjudicator, every self stop that scores at all
scores within-quantization.
The three link5<->link7 stops are the kernel finding its hulls genuinely
overlapping and reporting the loose OBB bound, because it runs no
expanding-polytope step; collision.cpp names this exact pair in its own comment
("-31.97 mm for a ~1.5 mm hull interpenetration"). It discloses that with
depth_is_box_bound and the adjudicator charges the 176.4 mm box budget.
What is real, and is now #260: depth_is_box_bound means "a hull refinement was
ATTEMPTED and fell back", and is deliberately clear when none was attempted.
hal_admissible_gap_m read clear as "hull fidelity", charged a hull budget, found
no measured overhang and gave up — so every self-pair naming panda_link1, which
ships no stage-2 hull by decision (Path C withdrew its envelope for moving its
own stops by 0.0003 mm), was permanently unadjudicated. Two of seven here. The
link_link.rule the HAL publishes already says the hull term applies "when BOTH
links ship stage-2 tight geometry"; the implementation never checked it.
Recommendation item 1 is rewritten on the corrected premise.
Refs #252, #260
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2
Signed-off-by: Adrian <adrianllopart@gmail.com>
...s this branch touches Both violations predate this branch and sit on `master`: `lattice_diff` and `AGridStraddlingTheOctreesKeyRangeStillSeesEveryLeaf` wrap their arguments the way clang-format does not, and one prose line in the evidence ledger carries a trailing space. Neither is reachable by CI's quality job, so they surface only when a commit stages those files — which the next commit does. Split out per CLAUDE.md §1.15 so the feature diff stays free of drive-by reformatting. Signed-off-by: Adrian <adrianllopart@gmail.com>
`sudo apt-get update -qq` exits 100 if *any* configured index fails, and the ubuntu-24.04 runner image ships third-party lists this repo does not use. From about 17:16 UTC on 2026年09月09日, dl.google.com's chrome index returned a hash-sum mismatch — Google had republished it and the CDN had not caught up — and every run since failed at this step, four in a row on #252. None of ffmpeg, libosmesa6 or just come from that repo; all three are in the Ubuntu archive, whose index downloaded fine each time. The downstream damage is worse than one red step: the job skips every lane and `Attest lane coverage` then reports all nineteen as "selected but produced no ledger record", which reads like a coverage regression rather than an apt failure. Tolerate the update and keep `apt-get install` as the gate — a package that genuinely cannot be fetched still fails on the next line, so nothing real is suppressed. Scoping the update to the Ubuntu archive instead would assume the runner keeps its sources in `/etc/apt/sources.list` rather than deb822 `sources.list.d/ubuntu.sources`; that is unverified here, and getting it wrong leaves no index at all. `test-ros2.yml` runs its apt inside a ROS container image with no third-party lists, so it is left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015odfPTtYuixeCXEW4rZ3Gq Signed-off-by: Adrian <adrianllopart@gmail.com>
Rebuilt on current `master`. The previous branch was cut before #238 and #239 landed and had drifted into a 45-commit, 25-file duplicate of work that has since merged through review; every instrument repair, evidence producer and manifest change it carried is already on `master` in its reviewed form. What was NOT on master is the resolution-lever measurement, which is what this is. The lever was struck on an estimate, and the strike was mine and wrong by 32x (a baseline taken from a hull microbenchmark rather than a round trip, and a cubic factor applied to a branch-not-taken). All three cost terms are now measured rather than reasoned about: kernel consuming a 15 mm grid p99 0.825 ms (estimated 26.7 ms) bridge producing one 1.60 ms against a 100 ms period the message on the wire p99 68-83 ms from 19-23 ms at 25 mm Compute is not the constraint; the map's AGE is. Latency triples, and age is millimetres too, so it is directly comparable to the 8.66 mm the lever buys. Settled against measured arm speed rather than an assumption: carry-phase stops run 0.051 m/s median and 0.265 m/s max, start-state stops exactly 0. Net positive in three of four corners, and free for the whole start-state class. Also corrects this branch's own earlier cell count: 15 mm needs 141^3 = 2 803 221 cells, not 376 680, because the coverage ball is sized by the arm's 1016 mm reach. The cap consequence is real -- a kernel still reserving 614 125 rejects every grid it is sent, which reads as "no world" and is a fail-OPEN on the world check -- so `_world_voxel_max_cells` now derives the cap from the resolution. Ships no default change. The resolution is an opt-in `OPENRAL_OCTOMAP_RESOLUTION_M` override that `validation_matrix.py` records, so a round that changed it can never afterwards be mistaken for one that did not. Shipping 15 mm is a conservatism REDUCTION and needs the safety-WG ruling that `OpenRAL/management#36` (hazard-log Entry 027) puts on the table. Tests: 125 unit passed; the 5 launch-file tests pass with the overlay sourced (they skip without it); 12/12 `openral_octomap_bridge` C++ tests including the new `RasterizationCostAcrossTreeResolutions` sweep. ruff clean, mypy --strict clean over tools/ (65 files), refresh_methods_linenos.py --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
... had none The two WG rulings now have trackers (#253 for the 15 mm resolution, #254 for ADR-0101), so the plan and the issue list agree rather than the plan being the only record. Adds two findings that were prose in the evidence ledger and nobody's open item: - #256 — the ceiling battery's 31.1 % / 2.3 % are lower bounds. It ran both arms simultaneously under contention and excluded only sidecar-crash runs, not deadline-no-grasp, which turned out to be largely a host-load artifact (spark produces zero on these scenes against q-laptop's 40-64 %). The contrast survives the paired design; the absolute rates, and therefore the 29-point figure the programme is justified by, do not. - #255 — a full-tier test-ordering flake unrelated to collision work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg Signed-off-by: Adrian <adrianllopart@gmail.com>
`voxel_transport_probe.per_axis` re-derives the coverage-ball cell count from its own `RADIUS_M = 1.05` literal rather than from `deploy_e2e.launch.py::_octomap_coverage_radius`, and `stop_ee_speed.QUANTISATION_GAIN_M` writes out the difference of two `validation_matrix.quantization_budget_m` values rather than calling it. Neither can import its source — the launch file is not a package and both tools run standalone — so the copies stay, but nothing was holding them to the originals. Both failure modes are silent and both land on the numbers this PR argues from: a probe sized off a stale radius times the wrong grid and still reports a clean latency, and a drifted 8.66 mm re-argues the whole staleness trade on a figure nobody would recheck. Two asserts, verified to fail when either constant is nudged. Recorded as items 42 and 43 in docs/methods/14-duplication-watch.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015odfPTtYuixeCXEW4rZ3Gq Signed-off-by: Adrian <adrianllopart@gmail.com>
...findings Rebased onto master past #257/#258, which had rewritten the same §5 the resolution branch measures. Two conflict hunks resolved by keeping both sides where they were complementary and the later fact where they were not: - PLAN §5 lever 3: this branch's cost measurements (kernel 0.825 ms, rasterize 1.60 ms, wire +55 ms p99, carry speed n=5) stand; master's "octree->grid conversion still unmeasured" was wrong the moment this branch landed and is replaced. Master's withdrawal of "drop baguette" stands over this branch's [x] for it — baguette scored 1/3 with the gate off. - "Decide #217": this branch's [x] closed-COMPLETED stands; it is a fact. - 10-tools.md: master's two detector entries plus this branch's octomap_resolution_env, markers refreshed post-hook. Then three things neither side had: 1. The lever-3 reach bound from #258 written into this branch's own item — "recovers 8.7 mm in every stop class" is true of the term, not the stops: 25->15 mm plausibly converts 5 of 20; the 9 with 23-85 mm of excess are #254's population and untouched by any resolution. 2. A finding the gate-OFF arm exposed: with the world check OFF, seven runs still carry a kernel stop, and every adjudicable one is a hull-fidelity false positive on the pair the kernel named — link5<->link7 three times at -27..-32 mm reported against +1.4..+4.9 mm certified, link1<->link7 at +87.9 mm, payload<->link1 at +25/+69 mm. No voxel in the path. These sit inside the 62.5 % ceiling and depress it. link5<->link7 is the pair #191 retired the ACM exemption for. No issue exists; filing one is the first recommendation. 3. "What to test next", ordered by what is unblocked and what each answer changes: adjudicate the self-collision false positives; answer #259's never-armed allowance question; run the 15 mm A/B with a falsifiable prediction of ~5 of 20; widen carry speed to n=13; the top-up. Refs #252, #253, #254, #256, #258, #259 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2 Signed-off-by: Adrian <adrianllopart@gmail.com>
I wrote that "every adjudicable one is a false positive" by comparing the
kernel's reported depth against the certified mesh distance without charging
the pair its budget. That is exactly the error hal_admissible_gap_m's docstring
warns about — "recomputing only the voxel term understates it fourfold ...
turning correct stops into false-positive" — and I made it in a safety-adjacent
entry. Run through the shipped adjudicator, every self stop that scores at all
scores within-quantization.
The three link5<->link7 stops are the kernel finding its hulls genuinely
overlapping and reporting the loose OBB bound, because it runs no
expanding-polytope step; collision.cpp names this exact pair in its own comment
("-31.97 mm for a ~1.5 mm hull interpenetration"). It discloses that with
depth_is_box_bound and the adjudicator charges the 176.4 mm box budget.
What is real, and is now #260: depth_is_box_bound means "a hull refinement was
ATTEMPTED and fell back", and is deliberately clear when none was attempted.
hal_admissible_gap_m read clear as "hull fidelity", charged a hull budget, found
no measured overhang and gave up — so every self-pair naming panda_link1, which
ships no stage-2 hull by decision (Path C withdrew its envelope for moving its
own stops by 0.0003 mm), was permanently unadjudicated. Two of seven here. The
link_link.rule the HAL publishes already says the hull term applies "when BOTH
links ship stage-2 tight geometry"; the implementation never checked it.
Recommendation item 1 is rewritten on the corrected premise.
Refs #252, #260
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2
Signed-off-by: Adrian <adrianllopart@gmail.com>
Answered the open question this plan and #259 both carried. Both the declaration and the support witness arm at every placing stop. place_allowance_active is literally `allowance > 0.0` (collision.cpp:1865), and place_approach_allowance is non-zero only for a cell inside the declared target's box — so 0 on the 20 latched stops means "this cell is not the declared target", which is true of every one of them. Where a cell IS the target it works: the two advisory refusals name b=place:... and the advisory path requires allowance > 0.0, so it was live and downgraded a latch to an advisory. ADR-0098 argues for exactly this scoping. The earlier reading — "the allowance never armed at all, which would be a larger bug" — was a parsing artifact: advisory log lines omit place_allowance_active and parse_kernel_collision defaults an absent field to False. Consequence for the recommendation: lever 2 is a conservatism decision in the same class as #253 and #254, not a bug fix with free upside, and the "up to 9 of 20 stops at zero conservatism" figure is withdrawn. Refs #259 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2 Signed-off-by: Adrian <adrianllopart@gmail.com>
...o in the plan The recommendation still pointed at `ceiling_battery.sh` with a `WORKERS=2` knob that no longer exists, and said nothing about why the 2026年09月10日 attempt produced no usable numbers. Two independent instrument defects voided it: the leaked octomap pair that starved the next run on its domain (openral #265), and a runner that claimed to interleave the arms while `xargs -P` held each slot for all ROUNDS rounds, so `sink_cup` ran its arms 29 minutes apart. Records the corrected invocation, both causes, and that none of the 43 rounds already on disk are poolable with the rerun. Refs #253 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2 Signed-off-by: Adrian <adrianllopart@gmail.com>
97fa9f3 to
e7126fc
Compare
Uh oh!
There was an error while loading. Please reload this page.
What changed
OPENRAL_OCTOMAP_RESOLUTION_Mondeploy_e2e.launch.py, validated at launch and recorded per round, so a round that changed the resolution can never afterwards be mistaken for one that did not (7 tests).tools/resolution_ab.sh+tools/resolution_ab_report.py— the A/B and its offline report, with INTEGRITY (observedresolution_mper arm) and PAIRING (per-round separation) attestations.tools/voxel_transport_probe.py,tools/stop_ee_speed.py, octree-to-grid C++ tests, harness fixes invalidation_matrix.py.PLAN.md§5 updated to strike the lever.Why (the plan) — and what actually happened
The endpoint was deliberately not completion rate: against the predicted 2.7 % → 10.8 % that needs 200 runs/arm for 80 % power (~33 h), and at the 40/arm a battery gives, power is 0.11. So the primary was the paired, continuous quantity the mechanism predicts — per stop, the over-approximation, whose half-diagonal goes 21.65 mm → 12.99 mm, i.e. a −8.66 mm shift.
Three batteries were needed. The first two were voided by instrument defects found along the way, both now fixed and both with their own evidence entries:
octomap_server_node+octomap_voxel_bridgepair whose Fast-DDS lock file starved the next run on that domain to zero action chunks (fixed in fix(cli): reap the world-voxel nodes, and honour the flag that opts parallel workers out #265 , merged);xargs -Pholds a slot for allROUNDSrounds — and then, once genuinely concurrent, two deploy graphs (~4.9 GB each on a 15.4 GB host with a 9.1 GB baseline) exhausted swap and drove load averages to 225.The third ran clean: 80 rounds, 79 valid, 0 bond teardowns, both arms verified against their own observed
resolution_m, every round paired at a median separation of 0 s.The result
Comparing only rounds where both arms produced an adjudicable stop on the same party — mixing parties compares different collision events, not resolutions:
p = 1.000Wrong sign, not a weak confirmation. Do not quote the report's own PRIMARY line (
−1.6 mm): it pools pairs where the 25 mm arm stopped onpanda_link7(excess 77–89 mm) against pairs where the 15 mm arm stopped on the payload.Why it is null — and what to do instead
The carried payload is lowered to a local AABB; no hull path exists for payloads at all. Over this battery's 424 samples its box reaches a median 50.78 mm (max 88.22 mm) beyond the mesh, against a half-diagonal of 21.65 mm at 25 mm and 12.99 mm at 15 mm. The box is ×ばつ the entire quantisation term — shrinking the cell removes 8.66 mm and leaves ~50 mm beside it. Filed as #266 , which is the lever this PR's line in
PLAN.md§5 used to be.15 mm also makes the concentration worse (payload stops 79 % → 97 % of all stops) and costs the grid 0.6 → 2.8 MB republished at 10 Hz — 28 MB/s of DDS against 6.
Phase, reconciled with
PLAN.md§5. Two orthogonal axes, both real. On phase (was a place declaration live at the stop) it is 33 placing / 20 carrying / 6 pre-grasp — which is what §5's earlier 11 placing / 7 carrying census measured, and it was right. On obstacle identity it is 9 hit the declared target / 44 hit surrounding scenery. Crossed over the 53 payload stops:The 28 is the headline — the payload approaching its declared drop point and clipping the occupancy beside it, exactly the pattern #259 names, now measured at scale rather than at n=11.
utensilis 20/20 carrying; the other three scenes are placing. 0 stops occur while reaching for an ungrasped object, so picking is untouched. Lever mapping: #259 reaches the 33 placing stops and nothing else; #266 reaches all 53 regardless of phase.How tested
7 tests on the resolution knob;
tests/unit/test_resolution_ab_pairing.pydrives the real runner through a seam and asserts no two rounds are ever live at once, that the arms alternate, that each keeps its own ROS domain, and that the sidecar boot wait never falls inside an arm pair;tests/unit/test_resolution_ab_report_pairing.pycovers back-to-back lanes, alternating rounds, and data with nostarted_at.ruffandmypy --strictclean; method markers checked.Standing caveat
n = 16 same-party pairs, sd 22.2 mm. This excludes an effect the size of the predicted −8.66 mm; it could not resolve a −3 mm one. The claim is "the predicted effect is not there", not "resolution does nothing" — which is why this is parked rather than closed.
Closes nothing on its own. Refs #253 (ruling withdrawn — measured), #266 (the lever that replaces it), #259, #264.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LtssNMqKXzZLfBhW7idZx2