Skip to content

fix(sim): keep a simulation and a real robot off each other's ROS graph - #231

Merged
AdrianLlopart merged 3 commits into
masterfrom
fix/227-dds-scope-isolation
Sep 5, 2026
Merged

fix(sim): keep a simulation and a real robot off each other's ROS graph#231
AdrianLlopart merged 3 commits into
masterfrom
fix/227-dds-scope-isolation

Conversation

@AdrianLlopart

Copy link
Copy Markdown
Contributor

Closes #227.

A simulation must not be able to reach a real robot, and a real robot must not be able to reach a simulation. Right now neither is true: openral deploy sim sets no DDS scope, inherits an unset ROS_DOMAIN_IDdomain 0, subnet-wide multicast discovery — and joins whatever else is on the LAN.

On 2026-09-05 that was a live bimanual OpenArm on another host. /joint_states had two publishers; the sim's state assembler read openarm_left_joint1 … openarm_right_joint7 where it wanted panda_gripper; ten A/B rounds died in ~50 s each looking exactly like policy failures.

Nothing actuated — but only because the robot's stack used its own topic names and happened to have no subscriber on /openral/candidate_action. That is a property of one robot's naming, not a guarantee the design provides.

Two controls, because they fail differently

Confinement — sim path only

ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST + ROS_DOMAIN_ID=77. A sim graph lives on one host, so it is pinned to one host.

  • LOCALHOST, not OFF — measured against the live robot: LOCALHOST hid its nodes while same-host discovery was unaffected; OFF also hides the sim's own nodes from each other.
  • deploy run is deliberately not confined — a real robot's graph may legitimately span machines. It gets the guard instead.
  • setdefault, so an explicitly-exported scope still wins and attaching a dashboard from another host stays possible.

The occupancy guard — both paths

Refuses to launch onto a graph that already has a /joint_states publisher.

That signature is the point: every robot has exactly one, real or simulated, so one rule covers both directions without either side having to recognise the other's node names. It is the half confinement cannot do — a real-robot launch joining a graph a sim is already on is not something confining the sim prevents.

An unreadable graph refuses too. A guard that passes when its instrument is broken is worse than no guard — the same posture the adjudicator takes for an uncertified probe.

OPENRAL_ALLOW_SHARED_GRAPH=1 is the opt-in escape, named in every refusal.

Verified end to end against the live robot

Not only in unit tests. From spark, with the arm running on thor:

/joint_states publishers nodes visible
unconfined (domain 0 / SUBNET) 1 17
confined (LOCALHOST / domain 77) 0 0

and the guard's actual refusal:

/joint_states already has 1 publisher(s) on this ROS graph (/joint_state_broadcaster)
— you are about to start a simulation on a graph that already carries real hardware.
  scope: ROS_DOMAIN_ID=0 (unset) ROS_AUTOMATIC_DISCOVERY_RANGE=SUBNET (unset)
  nodes: /controller_manager, /joint_state_broadcaster, /openarm_left_hardware_interface, …
Run the simulation on a different host or domain, stop the other graph, or set
OPENRAL_ALLOW_SHARED_GRAPH=1 if sharing is intended.

It correctly identifies real hardware from the ros2_control signature.

The daemon trap

The ros2 CLI daemon is unusable for this check: it answers from the environment it was started with, so ros2 node list under LOCALHOST still reported the remote robot's nodes — a false negative that reads as "the setting does not work". It cost me an hour.

  • The probe runs in a subprocess under the exact launch env and talks to rclpy directly.
  • _wait_for_action_server gained --no-daemon and the launch env for the same reason — it could otherwise be satisfied by another graph's action server.

The record is now auditable

verdicts.json metadata gains ros_domain_id and ros_automatic_discovery_range. Neither was captured before, which is exactly why no earlier round can be checked for whether it shared a graph — the question the 2026-09-04 post200-2 fridge round still cannot answer, and why its cause stays recorded as not established.

How tested

  • 10 unit tests covering both directions, both refusal reasons, the escape hatch, and that the refusal names the scope it scanned.
  • Mutation-checked: failing open on an unreadable graph fails 1; removing confinement fails 2.
  • Live: the table and refusal above, against the real OpenArm.
  • mypy --strict clean on openral_core + openral_cli; ruff check / format --check clean repo-wide; refresh_methods_linenos.py --check clean; 104 unit tests pass.
  • Docs travel with it: docs/methods/08-cli.md gains the module, 00-core-schemas.md the two fields, docs/contributing/validation-matrix.md a section on the recorded scope.

The graph scan is the one substituted seam in the unit tests — a subprocess boundary onto a DDS network, which is what CLAUDE.md §1.11 permits a double for. Everything else drives the real functions and the real typed exception.

Not in this PR

The /dev/shm/fastrtps_* purge (_apply_rmw_default) still unlinks every Fast-DDS segment owned by the user, which would cut the transport of a real robot running as the same user on the same host. Left deliberately — it wants its own decision, and it is recorded in #227's body.

🤖 Generated with Claude Code

https://claude.ai/code/session_014XR5jno1Qm3fiSdtdvYgML

Adrian and others added 3 commits September 5, 2026 17:30
Closes #227.

`openral deploy sim` set no DDS scope. It inherited the environment, and
`ROS_DOMAIN_ID` is normally unset -- domain 0, with multicast discovery across
the whole subnet. On 2026-09-05 a simulation on `spark` joined the ROS graph of
a LIVE bimanual OpenArm on `thor`. `/joint_states` had two publishers; the sim's
state assembler read `openarm_left_joint1 ... openarm_right_joint7` where it
wanted `panda_gripper`; ten A/B rounds died in ~50 s each looking exactly like
policy failures.

Nothing actuated, and that was luck rather than design: the robot's stack
consumed its own topic names and happened to have no subscriber on
`/openral/candidate_action`. A sim publishing an `Action` onto a topic a
physical robot subscribes to is one name collision away.

Two controls, because they cover different directions.

CONFINEMENT (`_dds_scope.confine_sim_scope`, applied on the sim path only).
`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` + `ROS_DOMAIN_ID=77`. A sim graph
lives on one host, so it is pinned to one host. `LOCALHOST` not `OFF`: measured
against the live robot, `LOCALHOST` hid its nodes while same-host discovery was
unaffected, whereas `OFF` hides the sim's own nodes from each other. `deploy
run` is deliberately NOT confined -- a real robot's graph may legitimately span
machines -- and is covered by the guard instead. Both settings are `setdefault`,
so an explicitly-exported scope still wins and attaching a dashboard from
another host stays possible.

THE OCCUPANCY GUARD (`_dds_scope.assert_graph_unoccupied`, both paths). Refuses
to launch onto a graph that already has a `/joint_states` publisher. That
signature is deliberate: every robot has exactly one, real or simulated, so one
rule covers both directions without either side having to recognise the other's
node names. It is the half confinement cannot do -- a real-robot launch joining
a graph a sim is already on is not something confining the sim prevents. An
unreadable graph refuses too: a guard that passes when its instrument is broken
is worse than no guard. `OPENRAL_ALLOW_SHARED_GRAPH=1` is the opt-in escape,
named in every refusal.

The `ros2` CLI daemon is unusable for this check -- it answers from the
environment IT was started with, so `ros2 node list` under `LOCALHOST` still
reported the remote robot's nodes, a false negative that reads as "the setting
does not work". The probe runs in a subprocess under the exact launch env and
talks to rclpy directly. `_wait_for_action_server` gained `--no-daemon` and the
launch env for the same reason: it could otherwise be satisfied by some other
graph's action server.

VERIFIED END TO END against the live robot, not only in unit tests. From
`spark`, unconfined: 1 `/joint_states` publisher, 17 nodes
(`/openarm_left_hardware_interface`, `/controller_manager`, `/command_mux` ...),
and the guard refuses, naming the publisher and correctly reporting "real
hardware" off the ros2_control signature. Confined: 0 publishers, 0 nodes.

The round record gains `ros_domain_id` and `ros_automatic_discovery_range`.
Neither was captured before, which is why no earlier round can be checked for
whether it shared a graph -- the question the 2026-09-04 `post200-2` fridge
round still cannot answer.

10 unit tests, mutation-checked: failing open on an unreadable graph fails 1,
removing confinement fails 2. The graph scan is the one substituted seam -- a
subprocess boundary onto a DDS network, which is what CLAUDE.md 1.11 permits a
double for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XR5jno1Qm3fiSdtdvYgML
Signed-off-by: Adrian <adrian@qualiastudios.dev>
…_preflight

Two defects in the guard as first pushed, both found by CI.

1. IT REFUSED ON ANY HOST WITHOUT ROS. `_scan_graph` collapsed "the probe
   failed" and "`rclpy` is not importable" into one `None`, and the guard fails
   closed on `None`. A host with no ROS has no graph to join and cannot run
   `ros2 launch` either, so the launch fails on its own with a clearer message
   than this guard can give; refusing there blocks every non-ROS machine, CI
   included, and buys no safety because there is no robot within reach. The
   probe now reports the ImportError distinctly and the guard returns.
   The fail-closed behaviour for a probe that SHOULD have worked is unchanged.

2. IT IGNORED `run_preflight=False`. The guard is a preflight check; a caller
   that says to skip preflight means it. It cannot live inside the existing
   `run_preflight` block — it needs `venv_env`, which does not exist until
   `_prepare_launch_env` has confined the scope — so it is gated on the same
   flag where it stands.

Verified against a genuinely unimportable `rclpy` (a shadowing module on
`PYTHONPATH` that raises), not only against a substituted seam: the scan returns
`no-ros` and the guard does not refuse.

Also regenerates the exported JSON schemas for the two `ValidationRoundMetadata`
fields added in the parent commit, which is what the quality gate caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XR5jno1Qm3fiSdtdvYgML
Signed-off-by: Adrian <adrian@qualiastudios.dev>
`if found == _NO_ROS: return` does not narrow `str` out of
`dict[...] | str | None`, so the two `found.get(...)` calls below were
`union-attr` errors under `mypy --strict`. `isinstance(found, str)` narrows.

Caught by the quality gate, which runs mypy over `openral_hal` as well as
`openral_core` and `openral_cli`; I had run only the latter two locally. Every
quality step now verified against the workflow's own commands: ruff, ruff
format, mypy over all three packages, schema drift, methods line markers, and
`mkdocs build --strict`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XR5jno1Qm3fiSdtdvYgML
Signed-off-by: Adrian <adrian@qualiastudios.dev>
@AdrianLlopart
AdrianLlopart merged commit 9ca834e into master Sep 5, 2026
3 checks passed
@AdrianLlopart
AdrianLlopart deleted the fix/227-dds-scope-isolation branch September 5, 2026 16:10
AdrianLlopart added a commit that referenced this pull request Sep 7, 2026
Every scene of every `tools/validation_matrix.py` round on post-#231 `master`
reported `harness-error` — "action server never appeared" — beside a graph that
was up and healthy the whole time. Two independent defects, each sufficient on
its own, both measured on `q-laptop` against a live `robocasa_drawer_utensil`
round.

1. `_launch_env` did not apply the sim DDS scope. Since #227/#231 `openral
   deploy sim` confines itself with `confine_sim_scope` (`ROS_DOMAIN_ID=77`,
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`) so a simulation and a real robot
   cannot share a graph. The deploy applied that to itself; the harness did not,
   and polled domain 0. `ROS_DOMAIN_ID=77 ros2 action list` showed
   `/openral/execute_rskill` the whole time; unscoped showed nothing.

   `confine_sim_scope` now runs inside `_launch_env` — applied on this side
   rather than left to the child, because it uses `setdefault`, so the deploy
   inherits the harness's value instead of choosing its own and the two agree by
   construction. An operator who exports their own scope still wins on both.

2. `ros2 action list --no-daemon` cannot discover an advertised action. The poll
   passed it for a real hazard: a daemon left over from an unscoped shell
   answers from the environment *it* started with — the false reading that made
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` look broken in #227. But the
   one-shot node it builds has a discovery window too short to see an action
   that is genuinely up: against a live graph, `ros2 action list` found it and
   `--no-daemon` did not, on the same domain, repeatably. The poll could never
   succeed on any scope.

   Replaced with a one-shot `ros2 daemon stop` under the round's own `env`
   before the loop, which closes the original hazard from the other side: the
   daemon the loop then uses is started by that call, on that scope.

The same round that had reported `harness-error` twice completed with a real
outcome (`utensil`, `deadline-no-grasp`) on the first attempt after both fixes.

`docs/reference/collision-validation-evidence.md` gains the 2026-09-07 entry,
including what it invalidates: any round taken on post-#231 master before today
measures the harness, not the kernel. Rounds on earlier commits — the ceiling
battery's `80027b18` arm among them — predate #231 and are unaffected; checked,
not assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
AdrianLlopart pushed a commit that referenced this pull request Sep 7, 2026
…ord both launch failures

The §7 checklist had drifted from §5's 2026-09-07 reordering and still listed
two levers as open that §5 had already struck by measurement:

- "Lever 1: the payload bounding box" kept the pre-reordering numbering. The
  payload is still 71 % of stops, but its primitives are already tight to
  −1.5 mm beyond the voxel term, so there is no payload-geometry headroom. The
  class is the right target; the mechanism that reaches it is ADR-0101's modeled
  fixtures, not a tighter payload box. Tracked there.
- "Lever 3: voxel resolution 25 → 15 mm" is struck on measured cost: a dense
  `uint8[]` occupancy and an `O(1/res³)` window make halving the cell an 8×
  check cost — 26.7 ms at 15 mm against a 33 ms ceiling.

Both now marked struck with the number that struck them, so the checklist and
§5 say the same thing.

Adds the two launch failures found today, which are distinct and were being
conflated:

- the **harness** could not see the graph it launched, and had not since #231 —
  wrong DDS scope plus a `--no-daemon` poll that cannot discover an advertised
  action at all. This is why post-#231 rounds looked like launch failures, and
  it is a precondition for every open measurement below it.
- the **launch parser** ran under the system interpreter, so `dist-packages`
  shadowed the venv and `import pandas` aborted the whole launch on a Jetson
  AGX Thor. That is the spark-side failure, fixed on its own branch.

Closes the `baguette` scorecard item: 0/11 with the gate off means policy-bound,
so it leaves the completion scorecard while staying in the matrix. Already
recorded in the ceiling entry; the plan now points at it rather than restating
it.

`collision-validation-evidence.md` gains standing caveat 10 — the citation rule
for post-#231 rounds — because that is what the caveats list is for.

Re-derivation of ADR-0101's 94 % against the post-fix live map is unblocked: the
foreign 1.9 GB GPU process is gone (175 MiB of 8151 in use).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
AdrianLlopart pushed a commit that referenced this pull request Sep 7, 2026
…decoration

`2026-09-07-adr0101-live-1` is the first validation-matrix round since #231 to
reach a real outcome rather than `harness-error`. It confirms ADR-0101's
certified premise directly: the payload was stopped at −4.05 mm reported while
sitting +24.86 mm clear of `counter_1_right_group_main` — the exact fixture the
ADR names as its largest class.

It also turned up something the ADR does not account for. The backing probe —
with the decoration-walking fix verified live in the running process — reports
the tripping cell as `noncollidable_world`, backed solely by
`counter_1_right_group_top_visual`. Within that 25 mm cube there is no
collidable geometry at all; the collision slab is the 24.9 mm away that the
certified probe independently measured.

That is the occupancy grid faithfully recording what a depth sensor sees, which
is the visual shell, not the collision body. It cuts both ways for ADR-0101's
suppression bound: if a modeled fixture publishes collision primitives, a cell
like this is *not* geometrically explained by them and the stop survives — so
the 94 % would be optimistic; if it publishes the visual geometry instead, the
mechanism suppresses against a surface the kernel does not protect. Benign in
sim, where a non-collidable geom cannot be hit; not benign on hardware, where
what the sensor sees is what the robot hits. That is the sim→real seam the ADR
already flags, now with an instance instead of a caveat.

Recorded at n=1 and labelled as such. A 12-round batch is running to measure how
often a payload stop is backed by decoration alone; nothing here revises the
94 % in either direction yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
AdrianLlopart added a commit that referenced this pull request Sep 7, 2026
…ramme measures with (#238)

* feat(sim): measure the policy's ceiling without the world-voxel gate

After a month of collision work, completion went from 25% (2026-08-26) to
5-10% (2026-09-06) and nobody had ever measured what the policy achieves
WITHOUT the gate. The validation harness refuses to -- `no_enable_octomap_
kernel_check` is on `_SAFETY_KNOB_PATTERNS`, correctly for a validation round,
and that is exactly why the number was never taken. The survey quoted the
external version of this experiment (PACS, arXiv:2511.06385 Table I:
unfiltered 0.70 vs binary-filtered 0.04) and never asked for the in-tree one.

Measured on spark, 88 valid runs, 4 scenes x 2 arms, 10-12 per cell, same
commit and host, both arms running simultaneously so contention loads onto
each equally:

    world-voxel gate OFF   14/45   31.1%
    world-voxel gate ON     1/43    2.3%     Fisher p = 3.5e-04, power 0.97

Per scene: utensil 58% vs 0% (p=0.005), fridge 45% vs 0% (p=0.035),
sink_cup 18% vs 9%, baguette 0% vs 0%. So two scenes are almost entirely
kernel-bound, sink_cup is mixed, and **baguette is policy-bound and cannot
report on collision work at all** -- it should leave the scorecard.

This is a CEILING, not a configuration: it never lands in a scene file, a
launch default or a manifest, the harness's refusal is untouched, and 6 of 91
stops in the #204 battery were real contact. The number says how much headroom
the levers are competing for: up to 29 points, concentrated in the payload
class.

`tools/_ceiling_probe.py` deliberately does not go through the validation
harness; it reuses that harness's own `materialise_scene`, readiness gate and
dispatch tool so the only difference between arms is the gate flag, verified
in the launch argv as `enable_octomap_kernel_check:=false`.

THREE DEFECTS had to be fixed before the number was trustworthy, each of which
would have produced a confidently wrong answer, and all are recorded in
PLAN.md §7:

1. an uncaught `subprocess.TimeoutExpired` killed whole workers rather than
   single rounds, leaving the arms SCENE-CONFOUNDED (gate-off had run mostly
   fridge, which completes; gate-on mostly utensil, which then never did) --
   the interim 4/17 vs 1/20 was an artifact of scene composition;
2. `SidecarClient` reaps the sidecar IT spawned on exit, so the first crashed
   worker took the shared sidecar down and every later run was policy-free
   (30-85 s instead of 600+). Fixed structurally with a keeper process;
3. policy-free runs must be excluded by reading each run's own goal log --
   14 of 102 runs were dropped that way.

Also recorded: `openral deploy sim` cannot run concurrently with itself,
because `_kill_orphan_openral_graph_processes()` matches by argv signature and
cannot tell a concurrent sibling from a crashed orphan. Parallel workers need
`OPENRAL_SKIP_ORPHAN_REAP=1`, which is deliberately NOT committed as a
default.

PLAN.md carries the failure analysis this all came from: median true clearance
at the moment of a kernel stop is 20.1 mm, 85 of 91 stops were of a robot that
was physically clear, and 71% of stops are the carried payload.

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>
(cherry picked from commit 17fc1450d6a9c904f058342982d9ed47edac349b)

* docs(collision): land the ceiling result and reorder the levers by measurement

The ceiling entry is the durable record: `outputs/` is gitignored, so the 88
runs behind 31.1 % vs 2.3 % would otherwise vanish the way the 2026-08-26
battery's artifacts already have.

Also records two lever findings that change what to build next, both measured
rather than argued:

* **Voxel resolution is struck.** `OccupancyVoxels.occupancy` is a dense
  `uint8[]` and the per-link window is `O(1/res^3)`, so halving the cell is an
  8x check cost. 15 mm is 26.7 ms estimated against a 33 ms budget and a
  2.80 MB message (4.6x the 614,125 cap); 12.5 mm is 46 ms. 20 mm fits but buys
  4.4 mm of a 20.1 mm excess. Poor return.
* **Modeled fixtures is the lever instead.** 51 of 70 payload stops are against
  anonymous `voxel_` cells whose certified nearest body is a static kitchen
  fixture MuJoCo already knows exactly -- `counter_1_right` 25 times,
  `fridgesidebyside_main` 9, `counter_1_left` 8. The robot carries an object
  over a counter and the counter's cubes stop it at 20 mm of air. That is the
  survey's own §9 point 1, and #200 already built the machinery for the
  declared place target; this generalises it to the fixture the payload is near.

And one scene finding: **`baguette` should leave the collision scorecard.** It
is 0 % with the gate off, so it is policy-bound and cannot report on collision
work either way -- despite four of the five completions in this ledger's whole
history being baguette runs, which is what made it look like the bellwether.

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>
(cherry picked from commit 0da3e4d8f44d48f5ee43a5af375d9444afbf208c)

* fix(sim): make the ceiling probe pass mypy --strict

`quality` runs `mypy --strict tools/`, which the new probe failed two ways:
`validation_matrix` is a sibling script imported by path (no stub), and the
two `type: ignore`s written for a typed SceneSpec were unused once that import
resolved to Any.

Types the parameter as `Any` with the reason inline rather than scattering
ignores, per CLAUDE.md §2.

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>
(cherry picked from commit 36a9d2be03d097e828b986b5c474410c82f154e0)

* test(safety): give the hull narrow phase a latency surface

`tests/sim/safety/test_kernel_latency_soak.py` is the kernel's only latency
test and it publishes **no** `OccupancyVoxels` at all — it runs a synthetic
`soak_test` envelope with no `collision_geometry`, so the staged 26-DOP -> hull
narrow phase never executes. Its pass is vacuous for any change to that phase,
which is the kernel's dominant cost: the shipped benchmark puts the seven link
windows at 10 475 cells and ~5.8 ms against ~8.8 us on an empty grid. Same
class of hole #183 found in the Nav2 live tests, and it is what made the
`link3`/`link4`/`link6` change in this branch unmeasurable.

The new test goes in the fridge pin file because that is the only place in the
tree with a REAL grid: a real RoboCasa kitchen rasterised cell by cell, the
real manifest (so all seven links lower their tight geometry), the real kernel
binary, at `world_voxel_margin_m = 0.0` — the value `panda_mobile` runs.

Measured on q-laptop, 200 chunks over 5 638 occupied cells:

    median 0.1 ms      p99 2.0 ms      target 30 ms, hard ceiling 33 ms

So the narrow phase with seven hulls sits 15x under the chunk budget, which
answers the latency question this branch's manifest change raises on the
shipped configuration rather than by extrapolating the one-off benchmark table.

It also asserts >=90% of chunks come back: a kernel dropping under a real grid
would be a worse finding than a slow p99, and a p99 over a truncated sample
would hide it.

Mutation-checked by forcing the budget to 0.001 ms, which is how the 2.0 / 0.1
numbers above were read out.

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>
(cherry picked from commit e0e2e0f74e6c80da32f57ed33a3e9ebfda4bb80e)

* docs(plan): record ADR-0101 and hazard-log Entry 026

Both live in OpenRAL/management#33. Entry 026 is the record #235 owes;
ADR-0101 is the remaining lever, proposed before code because it crosses
Layer 2 -> Layer 6.

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>
(cherry picked from commit 6406ac1e606fa870d337743c1bc9effa99eb42a3)

* fix(hal): the backing probe stopped at decoration and blamed it for the stop

`voxel_backing_record` is the instrument that answers "what, if anything, is
really in the cell the kernel stopped on". `mj_ray` reports only the NEAREST
strike, and the probe took it — so a non-collidable shell in front of the
collidable surface it wraps was the only thing it ever saw, and the cell was
adjudicated `noncollidable_world`, i.e. "the map disagrees with the world".

Measured, not theorised. On the 2026-09-06 battery, **6 of the 8 stops that
carried a backing record at all** came back `noncollidable_world` naming
`counter_1_right_group_top_visual`, while the certified nearest COLLISION
surface at those same stops was ~16 mm away -- inside the same 25 mm cell. The
map was right and the diagnostic was wrong.

That matters beyond one number. Since #180 the depth cast makes exactly these
geoms transparent, so in sim decoration can no longer become occupancy at all;
a `noncollidable_world` verdict is now a statement about the probe or a stale
cell, not a live map defect. The class docstring still carried the pre-#180
justification ("the depth synth strikes these too, so they CAN become
occupancy"), which is what made the misattribution look plausible. Both the
docstring and the METHODS entry are corrected.

A ray that strikes a non-collidable geom inside the cube is now re-cast from
just past it, up to `_VOXEL_BACKING_MAX_LAYERS` (4) times, and BOTH the shell
and whatever it hides are recorded. The existing precedence then does the rest:
`solid_world` outranks `noncollidable_world`, so the cell reads as explained by
real geometry, while a cell with genuinely nothing solid behind the decoration
still reads `noncollidable_world`. Nothing is filtered away -- dropping the
shell would hide a real map defect where one exists.

Diagnostics only (CLAUDE.md §1.4): no stop is suppressed, delayed or altered.

How tested: `tests/unit/test_sim_estop_voxel_backing.py` gains a RoboCasa-shaped
fixture -- a collidable slab wearing a non-collidable shell, both inside ONE
cell, shell nearer the ray start -- and a test that the cell reads `solid_world`
with both geoms named. Mutation-checked: reverting to first-strike-only fails it
with `noncollidable_world`. 15 pass in that file, 28 across the E-stop evidence
suite; `mypy --strict -p openral_hal` clean; methods markers refreshed.

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>
(cherry picked from commit c7bd2c729f792b2b7f829c4946e5750c981337fa)

* docs(plan): record the ADR-0101 recovery measurement and the probe fix

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>
(cherry picked from commit 2997c01efad8cbdd9147459403b9b6f1307325ab)

* docs(collision): the backing probe defect, and the 32% it moves

The reconstructed layout-47 grid goes 5 638 -> 7 427 occupied cells once the
probe stops blaming decoration for cells whose solid geometry is behind it --
landing between #224's two brackets (5 638 solid-only, 9 217 counting all
decoration) exactly as it should. Every clearance number derived from that
grid was computed against a map ~32% too sparse.

Also records the gap the investigation surfaced: the backing record was
present on only 8 of 91 stops, so the diagnostic that says what the map
contains is absent from 91% of the stops it exists to explain.

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>
(cherry picked from commit f42d3d3f5e61a11a18685c8e58e7de591070a782)

* docs(collision): correct an '8 of 91' claim before it hardened into evidence

An earlier draft of the 2026-09-07 probe entry said the backing record was
present on only 8 of 91 stops. Wrong: it is present on 82. The 74 extra live
in run_gt_evidence.json, which is #177's LATE path.

But that path is unusable on this battery, and its distribution is a trap --
46 of 74 read 'unbacked', which looks like the kernel stopping on cells nothing
backs. It is instead exactly the defect 10ff989 describes: the late path
omitted grid_orientation_xyzw, took identity, and decoded a cube metres from
the stopping link, reporting unbacked with 27 rays cast and 0 hits. 10ff989
landed 2026-09-05 and NONE of the battery's commits (all 2026-09-04) carry it.

Kept as a visible correction rather than a silent edit, because the wrong
version was a more exciting finding than the right one -- which is the specific
way this ledger has been burned before.

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>
(cherry picked from commit 51c8e2e19ab6952cb9ff10a6574fde9291f6e970)

* fix(sim): the validation harness could not see the graph it launched

Every scene of every `tools/validation_matrix.py` round on post-#231 `master`
reported `harness-error` — "action server never appeared" — beside a graph that
was up and healthy the whole time. Two independent defects, each sufficient on
its own, both measured on `q-laptop` against a live `robocasa_drawer_utensil`
round.

1. `_launch_env` did not apply the sim DDS scope. Since #227/#231 `openral
   deploy sim` confines itself with `confine_sim_scope` (`ROS_DOMAIN_ID=77`,
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`) so a simulation and a real robot
   cannot share a graph. The deploy applied that to itself; the harness did not,
   and polled domain 0. `ROS_DOMAIN_ID=77 ros2 action list` showed
   `/openral/execute_rskill` the whole time; unscoped showed nothing.

   `confine_sim_scope` now runs inside `_launch_env` — applied on this side
   rather than left to the child, because it uses `setdefault`, so the deploy
   inherits the harness's value instead of choosing its own and the two agree by
   construction. An operator who exports their own scope still wins on both.

2. `ros2 action list --no-daemon` cannot discover an advertised action. The poll
   passed it for a real hazard: a daemon left over from an unscoped shell
   answers from the environment *it* started with — the false reading that made
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` look broken in #227. But the
   one-shot node it builds has a discovery window too short to see an action
   that is genuinely up: against a live graph, `ros2 action list` found it and
   `--no-daemon` did not, on the same domain, repeatably. The poll could never
   succeed on any scope.

   Replaced with a one-shot `ros2 daemon stop` under the round's own `env`
   before the loop, which closes the original hazard from the other side: the
   daemon the loop then uses is started by that call, on that scope.

The same round that had reported `harness-error` twice completed with a real
outcome (`utensil`, `deadline-no-grasp`) on the first attempt after both fixes.

`docs/reference/collision-validation-evidence.md` gains the 2026-09-07 entry,
including what it invalidates: any round taken on post-#231 master before today
measures the harness, not the kernel. Rounds on earlier commits — the ceiling
battery's `80027b18` arm among them — predate #231 and are unaffected; checked,
not assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 773560899f80d0a7797a6c4897ab210c408d6fba)

* docs(plan): reconcile the checklist with the measured levers, and record both launch failures

The §7 checklist had drifted from §5's 2026-09-07 reordering and still listed
two levers as open that §5 had already struck by measurement:

- "Lever 1: the payload bounding box" kept the pre-reordering numbering. The
  payload is still 71 % of stops, but its primitives are already tight to
  −1.5 mm beyond the voxel term, so there is no payload-geometry headroom. The
  class is the right target; the mechanism that reaches it is ADR-0101's modeled
  fixtures, not a tighter payload box. Tracked there.
- "Lever 3: voxel resolution 25 → 15 mm" is struck on measured cost: a dense
  `uint8[]` occupancy and an `O(1/res³)` window make halving the cell an 8×
  check cost — 26.7 ms at 15 mm against a 33 ms ceiling.

Both now marked struck with the number that struck them, so the checklist and
§5 say the same thing.

Adds the two launch failures found today, which are distinct and were being
conflated:

- the **harness** could not see the graph it launched, and had not since #231 —
  wrong DDS scope plus a `--no-daemon` poll that cannot discover an advertised
  action at all. This is why post-#231 rounds looked like launch failures, and
  it is a precondition for every open measurement below it.
- the **launch parser** ran under the system interpreter, so `dist-packages`
  shadowed the venv and `import pandas` aborted the whole launch on a Jetson
  AGX Thor. That is the spark-side failure, fixed on its own branch.

Closes the `baguette` scorecard item: 0/11 with the gate off means policy-bound,
so it leaves the completion scorecard while staying in the matrix. Already
recorded in the ceiling entry; the plan now points at it rather than restating
it.

`collision-validation-evidence.md` gains standing caveat 10 — the citation rule
for post-#231 rounds — because that is what the caveats list is for.

Re-derivation of ADR-0101's 94 % against the post-fix live map is unblocked: the
foreign 1.9 GB GPU process is gone (175 MiB of 8151 in use).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit e7956f77120dd02bcb9899f7f0c140e7d8151a16)

* feat(evidence): give ADR-0101's 94 % a producer, and make its denominator honest

ADR-0101 cites "48 of 51 payload-vs-`voxel_` stops (94 %) recovered" as the
measurement that justifies building the first **fail-open** mechanism in the
hazard log. That number was computed offline, by hand, and had no producer in
the repo — which is exactly the shape of claim
`docs/reference/collision-validation-evidence.md` exists to prevent.

`tools/adr0101_recovery.py` is that producer. The counterfactual it evaluates
needs no kernel code and no layer crossing: for a payload stop against an
anonymous cell, "would a modeled fixture have let this through?" is the same
question as "was the payload certifiably clear of the real surface?", and the
battery already records both halves. Pure, offline, stdlib-only, like
`tools/round_power.py`.

Two decisions carry the safety direction, and both are tested:

- **Zero is contact, not clearance.** A payload touching a fixture is stopped by
  the modeled body exactly as it was by the cube. Putting `0.0` on the clearance
  side would count real contacts as recoveries — the one class the mechanism
  must never suppress. Mutation-checked: flipping `>` to `>=` fails the test.
- **Exclusions are reported, never dropped.** A recovery rate is only as honest
  as the set it divides by, and the two errors are not symmetric: silently
  dropping an unadjudicable stop shrinks the denominator and *inflates* the
  rate. `recovery_rate` is `None` over an empty set and `render` refuses to
  print a percentage rather than showing 0 % or 100 %.

Tested against the real `2026-08-23-master-s1` round, which recorded exactly the
stop shape the tool selects but predates the probe's distance attestation
(standing caveat 8) — so it must land in `excluded`, by name and with a reason.
It does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 60dcb2ff992cb7e4c06e5e362d8a000cd82a43e8)

* fix(evidence): attribute a payload stop to the fixture, not to a robot link

The first cut of `tools/adr0101_recovery.py` read the by-fixture breakdown off
`ground_truth.nearest_pair`. That field records the closest probed pair *of any
kind*, and for a carried payload it is routinely two robot links — on the round
this was caught with, `robot0_link3` vs `robot0_link4` at −36.3 mm, while the
payload itself sat 24.9 mm clear of a counter. So the tool put `robot0_link4`
into a table of kitchen fixtures: a robot link presented as a static world body,
inside the record that argues for modelling static world bodies.

The body behind `nearest_tripping_party_m` is recorded in exactly one place —
the raw `sim.estop_ground_truth_snapshot` line's `nearest_payload_world_pairs` —
so `fixture_at_stop` reads it there and verifies the match rather than assuming
it: that list's minimum certified distance must equal the gap the stop was
adjudicated on, both being the same probe call. If they disagree the snapshot
describes some other stop and no attribution is made. The recovery *count* never
depended on this and does not now; only the breakdown did.

Verified on the live round: the fixture is `counter_1_right_group_main`, which
is `counter_1_right` — the body ADR-0101 §3 already names as the top fixture at
25 of 70 payload stops. The premise reproduces independently.

Adds the round as a fixture (one snapshot line plus `verdicts.json`, provenance
in `SOURCE.txt`) and three tests, the first of which is a regression test for
this defect. Mutation-checked: pointing the reader at `nearest_link_link_pairs`
fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit a0f5f64bdd9d8d2cdfda75d644e6a3efec2bef45)

* docs(collision): the first post-fix round, and a cell backed only by decoration

`2026-09-07-adr0101-live-1` is the first validation-matrix round since #231 to
reach a real outcome rather than `harness-error`. It confirms ADR-0101's
certified premise directly: the payload was stopped at −4.05 mm reported while
sitting +24.86 mm clear of `counter_1_right_group_main` — the exact fixture the
ADR names as its largest class.

It also turned up something the ADR does not account for. The backing probe —
with the decoration-walking fix verified live in the running process — reports
the tripping cell as `noncollidable_world`, backed solely by
`counter_1_right_group_top_visual`. Within that 25 mm cube there is no
collidable geometry at all; the collision slab is the 24.9 mm away that the
certified probe independently measured.

That is the occupancy grid faithfully recording what a depth sensor sees, which
is the visual shell, not the collision body. It cuts both ways for ADR-0101's
suppression bound: if a modeled fixture publishes collision primitives, a cell
like this is *not* geometrically explained by them and the stop survives — so
the 94 % would be optimistic; if it publishes the visual geometry instead, the
mechanism suppresses against a surface the kernel does not protect. Benign in
sim, where a non-collidable geom cannot be hit; not benign on hardware, where
what the sensor sees is what the robot hits. That is the sim→real seam the ADR
already flags, now with an instance instead of a caveat.

Recorded at n=1 and labelled as such. A 12-round batch is running to measure how
often a payload stop is backed by decoration alone; nothing here revises the
94 % in either direction yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 6bed91758410ac6b3aa9ab12a6e8251313555a42)

* docs(collision): withdraw the 'map is proud of the collision model' reading

The 2026-09-07 entry interpreted a cell backed only by a non-collidable geom as
the occupancy grid mapping the visual surface while the collision body sat
behind it. RoboCasa's asset code does not support that reading.

`robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual`
box (`group=1`, `contype=0`) and then chunks the *same* volume into collidable
geoms via `_get_chunks`, which tile it exactly — identical `pos[1]`, `pos[2]`,
identical `size[1]`, `size[2]`, `x` tiling the full span. Visual and collision
are coincident by construction, so there is no offset to be proud by.

What remains is a real three-way tension: the backing probe finds no collidable
geom in the cell after re-casting past decoration; the certified probe puts the
nearest collidable geom of the same body 24.86 mm away; the asset code says they
are coincident. All three cannot hold.

The leading candidate is a residual defect in the re-cast itself: it advances
"just past" a strike, which steps over a *coincident* collidable twin and lands
outside the cube. The fix was built for decoration in front of a slab, not
decoration sharing its surface.

This is left open and labelled, not resolved, because it changes what the
programme is optimising: if the instrument is wrong, the 20.1 mm payload excess
ADR-0101 is sized against is itself suspect. Resolving it needs a direct query
of the live model at the stop, which no current artifact records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit b76aa0e41239b405364f976eebe1bbd4b2a64003)

* fix(evidence): a permitted link overlap was stamping every stop 'real-contact'

#220 (on master since 2026-09-05) gave the HAL a link-vs-link probe so a self
stop could be scored against the pair the kernel named. That part is right, and
it is what makes standing caveat 9 closeable. But the new pairs were folded into
the adjudicator's `nearest_any`, which drives its first and most decisive rule:
any probed pair at or below 0 m is `real-contact`.

Adjacent robot links overlap permanently. They are in the robot's
allowed-collision matrix and the kernel never checks them. So from #220 onward
`nearest_any <= 0` was vacuously true and every adjudicable stop was stamped
`real-contact`, whatever the tripping party's actual clearance.

Measured on `2026-09-07-adr0101-live-1`: `robot0_link3`/`link4` at -36.3 mm,
`link5`/`link6` at -23.0 mm, `link4`/`link5` at -4.6 mm — all certified, all
permitted, none of them what the kernel stopped for — while the carried payload
it did stop for sat +24.86 mm clear of the counter.

`nearest_any` now excludes `nearest_link_link_pairs` wholesale and adds back
only the pair the kernel named, which `party_pairs` already isolates in the
`self_pair` branch. A genuine link-vs-link self stop in real overlap is still
detected as contact; a permitted overlap two joints away is not.

Re-derived over the four stops recorded today, three move
`real-contact -> within-quantization` and the one true contact (-2.32 mm) is
preserved. Verdicts are pure and offline, so affected rounds re-adjudicate
without re-running.

Two tests, both mutation-checked against restoring `+ link_link`. Standing
caveat 11 records the citation rule: no `real-contact` verdict from 2026-09-05
to 2026-09-07 is safe to cite. The error runs one way — it manufactures real
contacts, never clears one — so nothing was wrongly passed as safe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 483a82e1cdb2d9676b5a7e1ee322e343b25db0d7)

* docs(collision): the adr0101-live battery — 71 % of stops are of a clear robot

Thirteen rounds on q-laptop (utensil and fridge, seeds 1-7), run after both
harness fixes and re-adjudicated offline after the `nearest_any` fix. First
battery on this page where harness and adjudicator were both known-good at
reading time; three rounds changed verdict when re-derived.

Seven stops. Five (71 %) were of a physically clear robot, at +0.67, +11.13,
+22.01, +23.13 and +24.86 mm true clearance. Two were real contact, at -2.32
and -0.11 mm. The 71 % reproduces the #204 battery's 85-of-91 on an independent
battery, a different commit and a repaired instrument — the number has not
moved.

Four stops are the carried payload, splitting evenly clear/contact;
`adr0101_recovery` reports 2 of 4 recovered, median 17.99 mm, minimum 11.13 mm.
Far below the offline 94 %, but n=4 does not contradict it and no revision is
claimed.

Two findings the levers do not cover:

- Three of seven stops are `estop-initial-configuration` — the arm stopped at
  reset by its own start pose, before doing anything. tight_geometry, modeled
  fixtures and voxel resolution all address the carry phase; none addresses a
  base placement that starts the arm inside a counter. One is at +0.67 mm and
  would survive any geometry work.
- Five of thirteen rounds never grasped at all. With the ceiling result, roughly
  half of what reads as collision-programme failure on these scenes is the
  policy not reaching the phase where the kernel matters.

One round (fridge seed 6) completed with the gate on, against a 2.3 % gate-on
rate in the ceiling battery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 9676c931477608c46236e28adaf5d6346f7ecd54)

* docs(plan): record the repaired-instrument rate, and two classes no lever covers

The false-positive rate re-measured on a working harness and a working
adjudicator is 71 % (5 of 7 stops of a physically clear robot), reproducing the
#204 battery on an independent battery and a different commit. The headline
number has not moved.

Also records the #220 adjudicator inversion that made that measurement possible
to get wrong for two days, and adds two open items the §5 levers do not touch:

- start-state collisions, a third of all stops, where the arm is stopped at
  reset by its own pose before doing anything;
- rounds that never grasp at all, five of thirteen, which bound how much of the
  scorecard any collision work can move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit c1e2203a2e3c07383bc7a3db9f1691fa28ed8ab9)

* feat(evidence): give the stop decomposition a producer, and measure the link fix

PLAN.md §5's table is the measurement that struck two collision levers and
promoted a third: per stop class, the kernel's over-approximation with the grid's
21.65 mm half-diagonal subtracted. Like ADR-0101's 94 %, it was computed by hand
and had no producer. `tools/stop_excess.py` is that producer.

Two guards, both tested. Only stops whose probe certified its distances are
counted — an uncertified distance can be wrong by 15-108 mm, larger than the
quantity being measured. And the half-diagonal comes from each round's own
recorded `grid_resolution_m`; a stop without one is skipped rather than
defaulted to 25 mm, since assuming it would not perturb the answer but replace
it. `has_geometry_headroom` is strict at zero: a class exactly at the voxel term
cannot recover a millimetre, so calling that headroom would license wasted work.

Run over the 13-round adr0101-live battery it gives the first live read on
whether the tight_geometry work did what it was designed to do:

  payload  n=4   median excess +13.15 mm   beyond voxel  -8.50 mm
  link     n=3   median excess +25.51 mm   beyond voxel  +3.86 mm

The #204 battery measured the link class at +33.1 mm beyond voxel, with
panda_link6 holding 18 of its 29 link stops. link6 now ships tight geometry and
the class reads +3.86 mm. n=3 and a different link mix, so this is consistent
with the fix working rather than proof of it — but it is the first live evidence
in that direction, and it is what PR #235 was missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 551b7c5932929d39a685038a5c8cb1d0d5918f08)

* fix(hal): the backing probe could not see a slab coincident with its shell

`c7bd2c7` taught the probe to walk past a non-collidable strike and cast again,
which finds a collidable slab BEHIND a decoration shell. It cannot find one
coincident with it, and RoboCasa builds every counter top that way:
`robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual`
(`contype=0`) and then tiles the same volume with collidable chunks via
`_get_chunks`. Their surfaces are the same plane, so stepping `distance + eps`
past the shell lands inside the chunk, where the ray reports no further entry
surface and the chunk is never seen.

That is what produced the three-way contradiction recorded this morning. It is
now settled from the round's own certified witness points: the cell spans
z in [0.90005, 0.92505] and the collidable chunk `counter_1_right_group_top_0`
has its surface at z = 0.920. The solid geometry was inside the cell the whole
time; the certified probe and the asset code agreed and only the backing probe
was wrong.

`voxel_backing_record` now falls back to a world-AABB overlap sweep over
collidable geoms when, and only when, the rays found nothing solid. An AABB
overlap can claim a geom whose surface misses the cube, so it is deliberately a
supplement rather than a replacement — for a diagnostic whose failure mode is
calling real geometry "decoration" (#180), erring toward found is the right
direction, and it can never override a ray pass that already found something.

Reproduced with a coincident shell/chunk fixture that yields the same 9-of-27
ray signature as the live round; mutation-checked by forcing the sweep off.

Diagnostics only (CLAUDE.md §1.4) — the certified probe measures geom-to-geom
distance and never used rays, so the 71 % rate and the stop decomposition are
unaffected. What moves is the backing CLASS of cells previously read
`noncollidable_world`, which is what the "32 % too sparse" entry and ADR-0101's
"cells no real body explains" premise rest on. Both should be re-derived from a
post-fix round before being leaned on further.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 10a3469ca9885c93414e280543b2a09fe863d00f)

* docs(plan): the geometry levers are exhausted, and link1's hull is not worth it

Three closures from the 13-round battery and its decomposition.

The probe contradiction was the instrument again: the cell spans
z in [0.90005, 0.92505] and the collidable chunk's surface is at z = 0.920, so
the solid geometry was inside it all along. Fixed; diagnostics only.

panda_link1 is the only link without a stage-2 hull (1588 vertices against a 320
cap) and caused two of three start-state stops, which made it look like the next
obvious manifest edit. Measured, those stops are +3.86 and +8.68 mm beyond the
voxel term, so an exact hull recovers at most ~9 mm of a ~25 mm error while
raising the cap 5x is a hot-path change needing safety-WG review. Struck.

And the conclusion those two produce together: the link class now measures
+3.86 mm beyond voxel, down from +33.1 mm in the #204 battery whose link stops
were 18-of-29 panda_link6 — the link that now ships tight geometry. With the
payload class at -8.50 mm, both are at or below the grid term. Every remaining
millimetre of over-approximation is the 25 mm voxel grid, and refining that was
struck on measured cost. No tighter envelope anywhere can recover anything
further.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 1d116ae9cdef8d25beec9f13cd8c31fa3a5c2aed)

* fix(safety): batch the hull-overhang query so a large link cannot exhaust memory

`hull_overhang_m` fed every barycentric sample to
`trimesh.proximity.closest_point_naive` in one call. That query allocates a
`samples x mesh-faces x 3` array, and the docstring's own premise — "a few
thousand by a few hundred here" — holds only for the small panda links that
have ever reached it.

`panda_link1` is 6260 mesh vertices and ~12k triangles; a 320-vertex hull over
it is ~636 facets at 325 samples each, so the single call asks for 57.8 GiB and
raises `numpy._core._exceptions._ArrayMemoryError`. Latent until something
declared a hull for that link.

Batching at 512 samples per call bounds peak memory by `512 x faces x 3`
independently of link size, and the maximum over batches is the maximum over the
whole set, so the returned number is unchanged. `check` still reports
mesh-outside-DOP +0.000000000 mm on all seven links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit e8e07315a5be846e2d27002a87c3aa258c1d7a2a)

* feat(tools): give the generator a third option when an exact hull is over budget

`derive_tight_geometry` had two outcomes for a link: ship the exact convex hull,
or -- if it exceeds `MAX_TIGHT_HULL_VERTICES` -- fall back to the 26-DOP alone.
`panda_link1` is the second case (1588 vertices), and its DOP's support gap
against the real mesh is a median 4.52 mm and up to 25.68 mm.

`refine_dop_to_budget` builds a third thing: the DOP intersected with the exact
hull's own face planes, worst-violation first, stopping before the vertex count
would exceed the budget. Every candidate plane is tangent to `conv(mesh)`, so
containment stays *definitional* rather than fitted, and the result is
`mesh ⊆ result ⊆ DOP ⊆ box` at every step. A subset-then-expand construction
cannot promise that: expansion pushes vertices out through the DOP slabs, and
link1's DOP has 0.083 mm of room inside its manifest box. The routine refuses
rather than emit an envelope that cuts its mesh.

On link1's real mesh it reaches a support gap of 0.18 mm median / 0.65 mm max
against the DOP's 4.52 / 25.68 mm, at 320 vertices.

**No manifest declares a refined envelope, and this commit changes none.** The
envelope was generated and put under a live battery on 2026-09-07; the stops it
was predicted to clear moved by 0.0003 mm. The prediction failed because it read
a support-census deficit against the shipped *box* and attributed all of it to
the hull, when the DOP had already collected it. That refutation is recorded in
the docstring, in `docs/methods/10-tools.md` and in the evidence page, so the
next reader does not re-derive the same wrong expectation from a routine that
looks like it was built for a reason nobody wrote down.

Which is also why the test is a direct one rather than a manifest assertion: an
unexercised generator path rots. It pins the three properties a safety-WG
reviewer would otherwise re-derive by hand -- mesh inside the result, result
inside the DOP, and strictly less enclosed volume than the DOP it refines --
against the real robosuite mesh, not a fixture.

Also carries the memory bound `_OVERHANG_BATCH` / `_OVERHANG_MAX_SAMPLES` needs
to document: the single-call `hull_overhang_m` form asks for 57.8 GiB on a
320-vertex envelope over a 12k-triangle mesh.

How tested: `pytest tests/unit/test_collision_tight_geometry.py` (11.5 s for the
new case, inside the 30 s unit budget).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(plan): correct the link1 strike — the reasoning was wrong, not just the call

The earlier entry struck link1's hull because it "recovers at most ~9 mm of a
~25 mm error". That is the wrong question: what matters is whether the recovery
flips the stop, and the start-state census's deficit table says 10 mm clears 14
of 14 link1 states. It also assumed the only options were the DOP or an
over-budget hull, and that a kernel cap change would be needed. Neither held —
the refined envelope fits the existing 320-vertex budget.

Records the measured result (0.18 / 0.65 mm against the DOP's 4.52 / 25.68 mm,
p99 0.5 ms on 9891 cells) and downgrades the start-state item from "no lever
touches it" to "one lever now reaches it, unconfirmed": two of its three stops
were link1, predicted to clear but not yet observed in a post-change battery.
The third is a genuine +0.67 mm near-contact that no geometry work reaches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 7fcc7168f57786846ff51a2df1e97b7cd1ebce5c)

* docs(collision): the link1-envelope confirmation battery is void, and says so

Eight rounds were run to confirm that panda_link1's refined envelope clears the
two panda_link1 start-state stops. All eight died at 16-19 s with the XR-1
sidecar OOMing at boot: a concurrent job on this shared host held 2.0-2.5 GB of
7.53 GiB while the sidecar needs ~3.5 GB alongside the scene.

Recorded rather than discarded because the failure mode is exactly the shape of
the result being looked for. Every round shows `stop: null`, and "seeds 2 and 4
no longer produce estop-initial-configuration" is precisely what the envelope
predicts — but the policy never loaded, so the arm never moved and no kernel
check ran. Reading it as confirmation would be reading a crash as a measurement.

The prediction stands unconfirmed. Confirming it needs an uncontended window
here or a host with headroom; spark is the latter, with the standing caveat that
XR-1 has never completed an end-to-end rollout on GB10, so one smoke round must
succeed before a battery is worth running there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit f4a670f55809e56de5f163d3bf33b75c010fce4e)

* docs(collision): the link1 refined envelope does not move the stops it was built for

Three rounds on spark, utensil seeds 2/3/4, on the commit carrying panda_link1's
refined envelope — also the first end-to-end XR-1 rollout completed on GB10.

The prediction is refuted. Same seed, same scene, same stop, one commit apart:
seed 2 reads -2.37794 mm under the 26-DOP and -2.37825 mm under the refined
envelope; seed 4 reads -8.31 and -8.31495. Tightening link1 from a 25.68 mm
worst-case support gap to 0.65 mm moved the reported depth by 0.0003 mm.

The reasoning error is identifiable. The start-state census's "10 mm clears 14
of 14 link1 states" is computed with box_box_distance against the manifest OBB.
The 26-DOP shipped after that census and already collected that recovery
(53.27 -> 25.69 mm). Treating the census's OBB-relative deficit as still-
available headroom double-counted a tightening that had already landed.

This is therefore the controlled test that the geometry levers are exhausted:
the same stop under two envelopes differing by 25 mm of worst-case looseness,
moving 0.0003 mm.

Notes the untested hypothesis for the residual: the octomap bridge's README
records that a published grid can report a surface up to one full tree
resolution (25 mm) nearer than it is, and stop_excess.py subtracts only the
21.65 mm half-diagonal — so map inflation reads as geometry excess and invites
exactly the hunt this entry closes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit b55f90af3f2ae0a7a6626b0773dfefbd885121fa)

* docs(plan): the link1 envelope was refuted, and the exhaustion claim got a test

Corrects the link1 item twice over. I struck the lever for the wrong reason
(fraction of error recovered rather than whether it flips the stop), then
un-struck it for another wrong one: the census's "10 mm clears 14/14 link1
states" is computed against the manifest OBB, and the 26-DOP shipped later had
already collected that recovery. Three rounds on spark measured the same stops
under both envelopes and they moved 0.0003 mm.

That failure upgrades the exhaustion conclusion from an inference over a
decomposition to a controlled test: the same stop under two envelopes differing
by 25 mm of worst-case support gap does not move. Whatever the residual is, it
is not the collision model.

Start-state goes back to "no lever reaches it" — it is a base-placement question,
and the census shows joints 3-7 cannot change a start-state verdict at all when
link1 or link2 dominates, which is 83.3% of stopping states.

Adds the hypothesis that displaces the geometry hunt: octomap marks the cell
containing the ray endpoint, so the grid can report a surface up to a full 25 mm
tree resolution nearer than it is, and stop_excess.py subtracts only the
21.65 mm half-diagonal. Untested, and the first thing worth measuring next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 7d90a810f98239251c4198a3909c44b35bb3d5bd)

* docs(collision): the map-side reading of all three start-state stops

estop-initial-configuration was three of seven stops in the adr0101-live
battery and the class no lever in PLAN.md §5 addresses. This is the first
map-side reading of it.

Where the evidence lives matters and is counter-intuitive: a start-state
snapshot carries collision_evidence: null and evidence_voxel_backing: null,
because the E-stop fires before the kernel's safety.collision line reaches the
bridge and the freshness gate correctly refuses to attribute a cell. The record
arrives on the deferred sim.estop_ground_truth_evidence path instead
(run_gt_evidence.json, backing_after_snapshot_ns: 0). Reading only the snapshot
shows nothing and invites the conclusion that the class is un-diagnosable.

Three stops, three causes:

- utensil-s2 is textbook quantisation. The cell CONTAINS the true nearest
  surface point of the door backing it (0.00 mm), so the map is where the world
  is; +23.13 mm clear, reported -2.38 mm, excess 25.51 = 21.65 half-diagonal +
  3.86 mm. This also refutes the octomap-inflation hypothesis raised earlier the
  same day: an inflated cell would not contain the surface.
- fridge-s2 has the robot in the cell and no world geometry at all — 15 of 27
  rays struck robot0_link2_collision, the same link the kernel stopped.
- utensil-s4 shows the same signature on weaker evidence: its robot geoms come
  from the conservative AABB sweep, not rays (0/27). The same stop read
  `unbacked` on q-laptop before that fix landed, so the fix turned a blank into
  a diagnosis.

Records that the mechanism is NOT a missing exclusion — the self-filter passes
robot bodies to the synth, matches by prefix plus descendants, and marks
transparent rays so they clear rather than mark — and names the decisive test
that no artifact supports today: re-run the layout with the arm parked
elsewhere and see whether the cell persists. That needs a start-pose override
the harness lacks.

n=3 and self_occupancy_suspect is explicitly not conclusive alone, so this is
logged as a lead, not a finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit d63d6ad608321441655af3dc4bfd6ad535436427)

* fix(hal): sweep for world geometry when a cell's only solid backing is the robot

`self_occupancy_suspect` was resting on 27 rays having missed nothing. A cell
whose only collidable ray hit is a robot body produces an identical record
whether world geometry is absent or merely unsampled, and that distinction is
exactly what separates a self-occupancy stop from ordinary quantisation.

Measured on the 2026-09-07 `fridge-s2` start-state stop: 15 of 27 rays struck
`robot0_link2_collision` — the same link the kernel stopped — and no world geom
appeared. Nothing in the record said whether one was there, so the stop could
not be classified either way.

The AABB overlap sweep now runs when the rays found no collidable **world**
geometry, rather than only when they found nothing collidable at all. That
covers the original coincident-shell case unchanged and adds the robot-only
case. A cell whose world backing the rays already found is still left alone, so
this cannot change a verdict the ray pass got right.

Diagnostics only (CLAUDE.md §1.4). Verified on the fixture: a cell on real world
geometry reports `swept=False`, a cell holding only the robot reports
`swept=True` and `self_occupancy_suspect`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 448818c49a31b55c99a971529e3963eb8e9039ae)

* docs(collision): start-state resolved — all three stops are quantisation

The widened sweep answers the ambiguity the previous entry named. Both
candidate self-occupancy stops were re-run on spark at that commit, and both
resolve against the lead: fridge-s2's cell contains the fridge drawer
(fridgesidebyside_main_group_1_g96 — the same body its near-miss pair named at
+0.673 mm), and utensil-s4's contains the cabinet door. The 27 ray fans had
simply missed them. The robot geoms in both cells are real but incidental — the
arm is beside the surface, not instead of it.

So all three start-state stops are ordinary voxel quantisation against
correctly-mapped world geometry. Not self-occupancy, not map inflation
(utensil-s2's cell contains the true surface point at 0.00 mm), and not link
envelope conservatism — the panda_link1 envelope moved these same stops by
0.0003 mm the same day.

Two of the three are stops of a demonstrably clear robot (+23.13, +22.01 mm).
fridge-s2 is separated out as a genuine near-contact at +0.67 mm that no
reduction in map conservatism should clear.

This settles the last population with an unexplored root cause, and it has the
same one as the payload class: the 25 mm grid. It therefore has the same single
remaining lever, and ADR-0101 is currently scoped to the carried payload only —
extending it to bare links would cover both classes with one mechanism. A scope
observation for the WG, not a decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 115e7b2c0f79f2d6ff1516045f40339b197a7e5f)

* docs(plan): start-state is quantisation — two root causes raised and both refuted

Closes the last population with an unexplored root cause. Three start-state
stops read map-side for the first time; map inflation and self-occupancy were
each raised as causes and each refuted by measurement, as was the link envelope.

The self-occupancy refutation is the substantive one: fridge-s2 looked like the
robot in its own map (15/27 rays on robot0_link2_collision, no world geom), and
widening the backing sweep then re-running on spark found the fridge drawer in
that cell — the same body its near-miss pair named at +0.673 mm.

So the class is ordinary voxel quantisation, with the same single lever as the
payload class. ADR-0101 is scoped to the carried payload; extending it to bare
links would cover both with one mechanism. Recorded as a scope note for the WG,
not a decision.

Separates fridge-s2 as a genuine near-contact at +0.67 mm that no reduction in
map conservatism should clear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 49b4197c9503d8ea4aa06f25f049ff2931ca741d)

* docs(collision): this week lands in three slices, and one change lands nowhere

33 commits and 4 822 lines is over CLAUDE.md §4.2.5's ceiling, and it stopped
being one logical change some days ago. Split by what gates each piece:

* **A** (this branch) — the four instrument repairs, the two evidence producers,
  `refine_dop_to_budget`, the ceiling probe, the narrow-phase latency surface,
  PLAN.md and the evidence ledger. Touches no manifest and neither
  `packages/openral_safety/` nor `cpp/` (empty `git diff --stat` against both),
  so §1.4 applies and §3 does not.
* **B** `feat/216-tight-geometry-link3-4-6` — the three manifest envelopes,
  safety-WG gated on hazard-log Entry 026.
* **C** — `panda_link1`'s envelope, withdrawn. The tool is in A; the manifest is
  nowhere.

A is the one with a deadline. `master` has carried two of the four defects since
2026-09-05 — a harness that cannot discover the action server it launches, and an
adjudicator that stamps every stop `real-contact` off a permitted adjacent-link
overlap — and both corrupt the programme's primary measurement. Every round taken
on `master` since then is unusable, so nothing downstream can be measured until
this lands. That is the whole argument for splitting rather than waiting for the
WG on one PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(methods): refresh the line markers the probe and tool changes moved

`tools/refresh_methods_linenos.py --check` was reporting 46 stale `(LNN)`
markers across `01-hal.md` and `10-tools.md` -- the mechanical consequence of
the backing-probe repairs and the new generator routine shifting line numbers in
files those pages index. CLAUDE.md §4.4 requires the check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(collision): the voxel-resolution strike was an estimate, and it was wrong by 32x

PLAN.md §5 struck the 25 -> 15 mm lever on cost: 26.7 ms estimated against a
33 ms ceiling. It was the only lever in the programme struck on paper rather
than by test. Measured on the real kernel under a real grid, at four resolutions
over the same volume, 200 chunks each:

| resolution | occupied | p99 | estimate |
| ---: | ---: | ---: | ---: |
| 25 mm (shipped) | 9 891 | 0.517 ms | 5.8 ms |
| 20 mm | 17 321 | 0.597 ms | 11.3 ms |
| 15 mm | 35 828 | 0.825 ms | 26.7 ms |
| 12.5 mm | 59 948 | 0.838 ms | 46.1 ms |

Two errors compounded. The 5.8 ms baseline came from the shipped hull
microbenchmark, not from a round trip under a real grid -- which had no latency
surface until one was built the same day the strike was written; the real
baseline is 0.517 ms. And the cubic factor was applied to the wrong term: the
window loop opens with `if (grid.occupancy[idx] == 0) { continue; }`, so
`O(1/res³)` falls on a branch-not-taken while the real work scales with occupied
cells, which are a surface. Cells x7.65, occupied x6.06, p99 x1.62.

The cap objection fails too: 15 mm is 376 680 cells, under the shipped
`world_voxel_max_cells = 614 125`. §5's 2.8M figure was a whole-kitchen grid,
not the arm-neighbourhood window the kernel scans.

Un-struck, but explicitly **not yet actionable**. This measures the kernel
consuming a grid, not `openral_octomap_bridge` producing one at a finer tree
resolution, which is the other half of the cost and is unmeasured. That is the
next step on this lever, not a manifest edit.

`_RES` in the fridge pin file becomes `OPENRAL_FRIDGE_GRID_RES_M`-overridable so
the sweep reproduces from the shipped test rather than from a probe duplicating
it. The default is unchanged and is what `sim_e2e.launch.py` emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

---------

Signed-off-by: Adrian <adrianllopart@gmail.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Adrian <adrian@qualiastudios.dev>
AdrianLlopart added a commit that referenced this pull request Sep 7, 2026
)

* feat(sim): measure the policy's ceiling without the world-voxel gate

After a month of collision work, completion went from 25% (2026-08-26) to
5-10% (2026-09-06) and nobody had ever measured what the policy achieves
WITHOUT the gate. The validation harness refuses to -- `no_enable_octomap_
kernel_check` is on `_SAFETY_KNOB_PATTERNS`, correctly for a validation round,
and that is exactly why the number was never taken. The survey quoted the
external version of this experiment (PACS, arXiv:2511.06385 Table I:
unfiltered 0.70 vs binary-filtered 0.04) and never asked for the in-tree one.

Measured on spark, 88 valid runs, 4 scenes x 2 arms, 10-12 per cell, same
commit and host, both arms running simultaneously so contention loads onto
each equally:

    world-voxel gate OFF   14/45   31.1%
    world-voxel gate ON     1/43    2.3%     Fisher p = 3.5e-04, power 0.97

Per scene: utensil 58% vs 0% (p=0.005), fridge 45% vs 0% (p=0.035),
sink_cup 18% vs 9%, baguette 0% vs 0%. So two scenes are almost entirely
kernel-bound, sink_cup is mixed, and **baguette is policy-bound and cannot
report on collision work at all** -- it should leave the scorecard.

This is a CEILING, not a configuration: it never lands in a scene file, a
launch default or a manifest, the harness's refusal is untouched, and 6 of 91
stops in the #204 battery were real contact. The number says how much headroom
the levers are competing for: up to 29 points, concentrated in the payload
class.

`tools/_ceiling_probe.py` deliberately does not go through the validation
harness; it reuses that harness's own `materialise_scene`, readiness gate and
dispatch tool so the only difference between arms is the gate flag, verified
in the launch argv as `enable_octomap_kernel_check:=false`.

THREE DEFECTS had to be fixed before the number was trustworthy, each of which
would have produced a confidently wrong answer, and all are recorded in
PLAN.md §7:

1. an uncaught `subprocess.TimeoutExpired` killed whole workers rather than
   single rounds, leaving the arms SCENE-CONFOUNDED (gate-off had run mostly
   fridge, which completes; gate-on mostly utensil, which then never did) --
   the interim 4/17 vs 1/20 was an artifact of scene composition;
2. `SidecarClient` reaps the sidecar IT spawned on exit, so the first crashed
   worker took the shared sidecar down and every later run was policy-free
   (30-85 s instead of 600+). Fixed structurally with a keeper process;
3. policy-free runs must be excluded by reading each run's own goal log --
   14 of 102 runs were dropped that way.

Also recorded: `openral deploy sim` cannot run concurrently with itself,
because `_kill_orphan_openral_graph_processes()` matches by argv signature and
cannot tell a concurrent sibling from a crashed orphan. Parallel workers need
`OPENRAL_SKIP_ORPHAN_REAP=1`, which is deliberately NOT committed as a
default.

PLAN.md carries the failure analysis this all came from: median true clearance
at the moment of a kernel stop is 20.1 mm, 85 of 91 stops were of a robot that
was physically clear, and 71% of stops are the carried payload.

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>
(cherry picked from commit 17fc1450d6a9c904f058342982d9ed47edac349b)

* docs(collision): land the ceiling result and reorder the levers by measurement

The ceiling entry is the durable record: `outputs/` is gitignored, so the 88
runs behind 31.1 % vs 2.3 % would otherwise vanish the way the 2026-08-26
battery's artifacts already have.

Also records two lever findings that change what to build next, both measured
rather than argued:

* **Voxel resolution is struck.** `OccupancyVoxels.occupancy` is a dense
  `uint8[]` and the per-link window is `O(1/res^3)`, so halving the cell is an
  8x check cost. 15 mm is 26.7 ms estimated against a 33 ms budget and a
  2.80 MB message (4.6x the 614,125 cap); 12.5 mm is 46 ms. 20 mm fits but buys
  4.4 mm of a 20.1 mm excess. Poor return.
* **Modeled fixtures is the lever instead.** 51 of 70 payload stops are against
  anonymous `voxel_` cells whose certified nearest body is a static kitchen
  fixture MuJoCo already knows exactly -- `counter_1_right` 25 times,
  `fridgesidebyside_main` 9, `counter_1_left` 8. The robot carries an object
  over a counter and the counter's cubes stop it at 20 mm of air. That is the
  survey's own §9 point 1, and #200 already built the machinery for the
  declared place target; this generalises it to the fixture the payload is near.

And one scene finding: **`baguette` should leave the collision scorecard.** It
is 0 % with the gate off, so it is policy-bound and cannot report on collision
work either way -- despite four of the five completions in this ledger's whole
history being baguette runs, which is what made it look like the bellwether.

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>
(cherry picked from commit 0da3e4d8f44d48f5ee43a5af375d9444afbf208c)

* fix(sim): make the ceiling probe pass mypy --strict

`quality` runs `mypy --strict tools/`, which the new probe failed two ways:
`validation_matrix` is a sibling script imported by path (no stub), and the
two `type: ignore`s written for a typed SceneSpec were unused once that import
resolved to Any.

Types the parameter as `Any` with the reason inline rather than scattering
ignores, per CLAUDE.md §2.

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>
(cherry picked from commit 36a9d2be03d097e828b986b5c474410c82f154e0)

* test(safety): give the hull narrow phase a latency surface

`tests/sim/safety/test_kernel_latency_soak.py` is the kernel's only latency
test and it publishes **no** `OccupancyVoxels` at all — it runs a synthetic
`soak_test` envelope with no `collision_geometry`, so the staged 26-DOP -> hull
narrow phase never executes. Its pass is vacuous for any change to that phase,
which is the kernel's dominant cost: the shipped benchmark puts the seven link
windows at 10 475 cells and ~5.8 ms against ~8.8 us on an empty grid. Same
class of hole #183 found in the Nav2 live tests, and it is what made the
`link3`/`link4`/`link6` change in this branch unmeasurable.

The new test goes in the fridge pin file because that is the only place in the
tree with a REAL grid: a real RoboCasa kitchen rasterised cell by cell, the
real manifest (so all seven links lower their tight geometry), the real kernel
binary, at `world_voxel_margin_m = 0.0` — the value `panda_mobile` runs.

Measured on q-laptop, 200 chunks over 5 638 occupied cells:

    median 0.1 ms      p99 2.0 ms      target 30 ms, hard ceiling 33 ms

So the narrow phase with seven hulls sits 15x under the chunk budget, which
answers the latency question this branch's manifest change raises on the
shipped configuration rather than by extrapolating the one-off benchmark table.

It also asserts >=90% of chunks come back: a kernel dropping under a real grid
would be a worse finding than a slow p99, and a p99 over a truncated sample
would hide it.

Mutation-checked by forcing the budget to 0.001 ms, which is how the 2.0 / 0.1
numbers above were read out.

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>
(cherry picked from commit e0e2e0f74e6c80da32f57ed33a3e9ebfda4bb80e)

* docs(plan): record ADR-0101 and hazard-log Entry 026

Both live in OpenRAL/management#33. Entry 026 is the record #235 owes;
ADR-0101 is the remaining lever, proposed before code because it crosses
Layer 2 -> Layer 6.

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>
(cherry picked from commit 6406ac1e606fa870d337743c1bc9effa99eb42a3)

* fix(hal): the backing probe stopped at decoration and blamed it for the stop

`voxel_backing_record` is the instrument that answers "what, if anything, is
really in the cell the kernel stopped on". `mj_ray` reports only the NEAREST
strike, and the probe took it — so a non-collidable shell in front of the
collidable surface it wraps was the only thing it ever saw, and the cell was
adjudicated `noncollidable_world`, i.e. "the map disagrees with the world".

Measured, not theorised. On the 2026-09-06 battery, **6 of the 8 stops that
carried a backing record at all** came back `noncollidable_world` naming
`counter_1_right_group_top_visual`, while the certified nearest COLLISION
surface at those same stops was ~16 mm away -- inside the same 25 mm cell. The
map was right and the diagnostic was wrong.

That matters beyond one number. Since #180 the depth cast makes exactly these
geoms transparent, so in sim decoration can no longer become occupancy at all;
a `noncollidable_world` verdict is now a statement about the probe or a stale
cell, not a live map defect. The class docstring still carried the pre-#180
justification ("the depth synth strikes these too, so they CAN become
occupancy"), which is what made the misattribution look plausible. Both the
docstring and the METHODS entry are corrected.

A ray that strikes a non-collidable geom inside the cube is now re-cast from
just past it, up to `_VOXEL_BACKING_MAX_LAYERS` (4) times, and BOTH the shell
and whatever it hides are recorded. The existing precedence then does the rest:
`solid_world` outranks `noncollidable_world`, so the cell reads as explained by
real geometry, while a cell with genuinely nothing solid behind the decoration
still reads `noncollidable_world`. Nothing is filtered away -- dropping the
shell would hide a real map defect where one exists.

Diagnostics only (CLAUDE.md §1.4): no stop is suppressed, delayed or altered.

How tested: `tests/unit/test_sim_estop_voxel_backing.py` gains a RoboCasa-shaped
fixture -- a collidable slab wearing a non-collidable shell, both inside ONE
cell, shell nearer the ray start -- and a test that the cell reads `solid_world`
with both geoms named. Mutation-checked: reverting to first-strike-only fails it
with `noncollidable_world`. 15 pass in that file, 28 across the E-stop evidence
suite; `mypy --strict -p openral_hal` clean; methods markers refreshed.

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>
(cherry picked from commit c7bd2c729f792b2b7f829c4946e5750c981337fa)

* docs(plan): record the ADR-0101 recovery measurement and the probe fix

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>
(cherry picked from commit 2997c01efad8cbdd9147459403b9b6f1307325ab)

* docs(collision): the backing probe defect, and the 32% it moves

The reconstructed layout-47 grid goes 5 638 -> 7 427 occupied cells once the
probe stops blaming decoration for cells whose solid geometry is behind it --
landing between #224's two brackets (5 638 solid-only, 9 217 counting all
decoration) exactly as it should. Every clearance number derived from that
grid was computed against a map ~32% too sparse.

Also records the gap the investigation surfaced: the backing record was
present on only 8 of 91 stops, so the diagnostic that says what the map
contains is absent from 91% of the stops it exists to explain.

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>
(cherry picked from commit f42d3d3f5e61a11a18685c8e58e7de591070a782)

* docs(collision): correct an '8 of 91' claim before it hardened into evidence

An earlier draft of the 2026-09-07 probe entry said the backing record was
present on only 8 of 91 stops. Wrong: it is present on 82. The 74 extra live
in run_gt_evidence.json, which is #177's LATE path.

But that path is unusable on this battery, and its distribution is a trap --
46 of 74 read 'unbacked', which looks like the kernel stopping on cells nothing
backs. It is instead exactly the defect 10ff989 describes: the late path
omitted grid_orientation_xyzw, took identity, and decoded a cube metres from
the stopping link, reporting unbacked with 27 rays cast and 0 hits. 10ff989
landed 2026-09-05 and NONE of the battery's commits (all 2026-09-04) carry it.

Kept as a visible correction rather than a silent edit, because the wrong
version was a more exciting finding than the right one -- which is the specific
way this ledger has been burned before.

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>
(cherry picked from commit 51c8e2e19ab6952cb9ff10a6574fde9291f6e970)

* fix(sim): the validation harness could not see the graph it launched

Every scene of every `tools/validation_matrix.py` round on post-#231 `master`
reported `harness-error` — "action server never appeared" — beside a graph that
was up and healthy the whole time. Two independent defects, each sufficient on
its own, both measured on `q-laptop` against a live `robocasa_drawer_utensil`
round.

1. `_launch_env` did not apply the sim DDS scope. Since #227/#231 `openral
   deploy sim` confines itself with `confine_sim_scope` (`ROS_DOMAIN_ID=77`,
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`) so a simulation and a real robot
   cannot share a graph. The deploy applied that to itself; the harness did not,
   and polled domain 0. `ROS_DOMAIN_ID=77 ros2 action list` showed
   `/openral/execute_rskill` the whole time; unscoped showed nothing.

   `confine_sim_scope` now runs inside `_launch_env` — applied on this side
   rather than left to the child, because it uses `setdefault`, so the deploy
   inherits the harness's value instead of choosing its own and the two agree by
   construction. An operator who exports their own scope still wins on both.

2. `ros2 action list --no-daemon` cannot discover an advertised action. The poll
   passed it for a real hazard: a daemon left over from an unscoped shell
   answers from the environment *it* started with — the false reading that made
   `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` look broken in #227. But the
   one-shot node it builds has a discovery window too short to see an action
   that is genuinely up: against a live graph, `ros2 action list` found it and
   `--no-daemon` did not, on the same domain, repeatably. The poll could never
   succeed on any scope.

   Replaced with a one-shot `ros2 daemon stop` under the round's own `env`
   before the loop, which closes the original hazard from the other side: the
   daemon the loop then uses is started by that call, on that scope.

The same round that had reported `harness-error` twice completed with a real
outcome (`utensil`, `deadline-no-grasp`) on the first attempt after both fixes.

`docs/reference/collision-validation-evidence.md` gains the 2026-09-07 entry,
including what it invalidates: any round taken on post-#231 master before today
measures the harness, not the kernel. Rounds on earlier commits — the ceiling
battery's `80027b18` arm among them — predate #231 and are unaffected; checked,
not assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 773560899f80d0a7797a6c4897ab210c408d6fba)

* docs(plan): reconcile the checklist with the measured levers, and record both launch failures

The §7 checklist had drifted from §5's 2026-09-07 reordering and still listed
two levers as open that §5 had already struck by measurement:

- "Lever 1: the payload bounding box" kept the pre-reordering numbering. The
  payload is still 71 % of stops, but its primitives are already tight to
  −1.5 mm beyond the voxel term, so there is no payload-geometry headroom. The
  class is the right target; the mechanism that reaches it is ADR-0101's modeled
  fixtures, not a tighter payload box. Tracked there.
- "Lever 3: voxel resolution 25 → 15 mm" is struck on measured cost: a dense
  `uint8[]` occupancy and an `O(1/res³)` window make halving the cell an 8×
  check cost — 26.7 ms at 15 mm against a 33 ms ceiling.

Both now marked struck with the number that struck them, so the checklist and
§5 say the same thing.

Adds the two launch failures found today, which are distinct and were being
conflated:

- the **harness** could not see the graph it launched, and had not since #231 —
  wrong DDS scope plus a `--no-daemon` poll that cannot discover an advertised
  action at all. This is why post-#231 rounds looked like launch failures, and
  it is a precondition for every open measurement below it.
- the **launch parser** ran under the system interpreter, so `dist-packages`
  shadowed the venv and `import pandas` aborted the whole launch on a Jetson
  AGX Thor. That is the spark-side failure, fixed on its own branch.

Closes the `baguette` scorecard item: 0/11 with the gate off means policy-bound,
so it leaves the completion scorecard while staying in the matrix. Already
recorded in the ceiling entry; the plan now points at it rather than restating
it.

`collision-validation-evidence.md` gains standing caveat 10 — the citation rule
for post-#231 rounds — because that is what the caveats list is for.

Re-derivation of ADR-0101's 94 % against the post-fix live map is unblocked: the
foreign 1.9 GB GPU process is gone (175 MiB of 8151 in use).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit e7956f77120dd02bcb9899f7f0c140e7d8151a16)

* feat(evidence): give ADR-0101's 94 % a producer, and make its denominator honest

ADR-0101 cites "48 of 51 payload-vs-`voxel_` stops (94 %) recovered" as the
measurement that justifies building the first **fail-open** mechanism in the
hazard log. That number was computed offline, by hand, and had no producer in
the repo — which is exactly the shape of claim
`docs/reference/collision-validation-evidence.md` exists to prevent.

`tools/adr0101_recovery.py` is that producer. The counterfactual it evaluates
needs no kernel code and no layer crossing: for a payload stop against an
anonymous cell, "would a modeled fixture have let this through?" is the same
question as "was the payload certifiably clear of the real surface?", and the
battery already records both halves. Pure, offline, stdlib-only, like
`tools/round_power.py`.

Two decisions carry the safety direction, and both are tested:

- **Zero is contact, not clearance.** A payload touching a fixture is stopped by
  the modeled body exactly as it was by the cube. Putting `0.0` on the clearance
  side would count real contacts as recoveries — the one class the mechanism
  must never suppress. Mutation-checked: flipping `>` to `>=` fails the test.
- **Exclusions are reported, never dropped.** A recovery rate is only as honest
  as the set it divides by, and the two errors are not symmetric: silently
  dropping an unadjudicable stop shrinks the denominator and *inflates* the
  rate. `recovery_rate` is `None` over an empty set and `render` refuses to
  print a percentage rather than showing 0 % or 100 %.

Tested against the real `2026-08-23-master-s1` round, which recorded exactly the
stop shape the tool selects but predates the probe's distance attestation
(standing caveat 8) — so it must land in `excluded`, by name and with a reason.
It does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 60dcb2ff992cb7e4c06e5e362d8a000cd82a43e8)

* fix(evidence): attribute a payload stop to the fixture, not to a robot link

The first cut of `tools/adr0101_recovery.py` read the by-fixture breakdown off
`ground_truth.nearest_pair`. That field records the closest probed pair *of any
kind*, and for a carried payload it is routinely two robot links — on the round
this was caught with, `robot0_link3` vs `robot0_link4` at −36.3 mm, while the
payload itself sat 24.9 mm clear of a counter. So the tool put `robot0_link4`
into a table of kitchen fixtures: a robot link presented as a static world body,
inside the record that argues for modelling static world bodies.

The body behind `nearest_tripping_party_m` is recorded in exactly one place —
the raw `sim.estop_ground_truth_snapshot` line's `nearest_payload_world_pairs` —
so `fixture_at_stop` reads it there and verifies the match rather than assuming
it: that list's minimum certified distance must equal the gap the stop was
adjudicated on, both being the same probe call. If they disagree the snapshot
describes some other stop and no attribution is made. The recovery *count* never
depended on this and does not now; only the breakdown did.

Verified on the live round: the fixture is `counter_1_right_group_main`, which
is `counter_1_right` — the body ADR-0101 §3 already names as the top fixture at
25 of 70 payload stops. The premise reproduces independently.

Adds the round as a fixture (one snapshot line plus `verdicts.json`, provenance
in `SOURCE.txt`) and three tests, the first of which is a regression test for
this defect. Mutation-checked: pointing the reader at `nearest_link_link_pairs`
fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit a0f5f64bdd9d8d2cdfda75d644e6a3efec2bef45)

* docs(collision): the first post-fix round, and a cell backed only by decoration

`2026-09-07-adr0101-live-1` is the first validation-matrix round since #231 to
reach a real outcome rather than `harness-error`. It confirms ADR-0101's
certified premise directly: the payload was stopped at −4.05 mm reported while
sitting +24.86 mm clear of `counter_1_right_group_main` — the exact fixture the
ADR names as its largest class.

It also turned up something the ADR does not account for. The backing probe —
with the decoration-walking fix verified live in the running process — reports
the tripping cell as `noncollidable_world`, backed solely by
`counter_1_right_group_top_visual`. Within that 25 mm cube there is no
collidable geometry at all; the collision slab is the 24.9 mm away that the
certified probe independently measured.

That is the occupancy grid faithfully recording what a depth sensor sees, which
is the visual shell, not the collision body. It cuts both ways for ADR-0101's
suppression bound: if a modeled fixture publishes collision primitives, a cell
like this is *not* geometrically explained by them and the stop survives — so
the 94 % would be optimistic; if it publishes the visual geometry instead, the
mechanism suppresses against a surface the kernel does not protect. Benign in
sim, where a non-collidable geom cannot be hit; not benign on hardware, where
what the sensor sees is what the robot hits. That is the sim→real seam the ADR
already flags, now with an instance instead of a caveat.

Recorded at n=1 and labelled as such. A 12-round batch is running to measure how
often a payload stop is backed by decoration alone; nothing here revises the
94 % in either direction yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 6bed91758410ac6b3aa9ab12a6e8251313555a42)

* docs(collision): withdraw the 'map is proud of the collision model' reading

The 2026-09-07 entry interpreted a cell backed only by a non-collidable geom as
the occupancy grid mapping the visual surface while the collision body sat
behind it. RoboCasa's asset code does not support that reading.

`robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual`
box (`group=1`, `contype=0`) and then chunks the *same* volume into collidable
geoms via `_get_chunks`, which tile it exactly — identical `pos[1]`, `pos[2]`,
identical `size[1]`, `size[2]`, `x` tiling the full span. Visual and collision
are coincident by construction, so there is no offset to be proud by.

What remains is a real three-way tension: the backing probe finds no collidable
geom in the cell after re-casting past decoration; the certified probe puts the
nearest collidable geom of the same body 24.86 mm away; the asset code says they
are coincident. All three cannot hold.

The leading candidate is a residual defect in the re-cast itself: it advances
"just past" a strike, which steps over a *coincident* collidable twin and lands
outside the cube. The fix was built for decoration in front of a slab, not
decoration sharing its surface.

This is left open and labelled, not resolved, because it changes what the
programme is optimising: if the instrument is wrong, the 20.1 mm payload excess
ADR-0101 is sized against is itself suspect. Resolving it needs a direct query
of the live model at the stop, which no current artifact records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit b76aa0e41239b405364f976eebe1bbd4b2a64003)

* fix(evidence): a permitted link overlap was stamping every stop 'real-contact'

#220 (on master since 2026-09-05) gave the HAL a link-vs-link probe so a self
stop could be scored against the pair the kernel named. That part is right, and
it is what makes standing caveat 9 closeable. But the new pairs were folded into
the adjudicator's `nearest_any`, which drives its first and most decisive rule:
any probed pair at or below 0 m is `real-contact`.

Adjacent robot links overlap permanently. They are in the robot's
allowed-collision matrix and the kernel never checks them. So from #220 onward
`nearest_any <= 0` was vacuously true and every adjudicable stop was stamped
`real-contact`, whatever the tripping party's actual clearance.

Measured on `2026-09-07-adr0101-live-1`: `robot0_link3`/`link4` at -36.3 mm,
`link5`/`link6` at -23.0 mm, `link4`/`link5` at -4.6 mm — all certified, all
permitted, none of them what the kernel stopped for — while the carried payload
it did stop for sat +24.86 mm clear of the counter.

`nearest_any` now excludes `nearest_link_link_pairs` wholesale and adds back
only the pair the kernel named, which `party_pairs` already isolates in the
`self_pair` branch. A genuine link-vs-link self stop in real overlap is still
detected as contact; a permitted overlap two joints away is not.

Re-derived over the four stops recorded today, three move
`real-contact -> within-quantization` and the one true contact (-2.32 mm) is
preserved. Verdicts are pure and offline, so affected rounds re-adjudicate
without re-running.

Two tests, both mutation-checked against restoring `+ link_link`. Standing
caveat 11 records the citation rule: no `real-contact` verdict from 2026-09-05
to 2026-09-07 is safe to cite. The error runs one way — it manufactures real
contacts, never clears one — so nothing was wrongly passed as safe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 483a82e1cdb2d9676b5a7e1ee322e343b25db0d7)

* docs(collision): the adr0101-live battery — 71 % of stops are of a clear robot

Thirteen rounds on q-laptop (utensil and fridge, seeds 1-7), run after both
harness fixes and re-adjudicated offline after the `nearest_any` fix. First
battery on this page where harness and adjudicator were both known-good at
reading time; three rounds changed verdict when re-derived.

Seven stops. Five (71 %) were of a physically clear robot, at +0.67, +11.13,
+22.01, +23.13 and +24.86 mm true clearance. Two were real contact, at -2.32
and -0.11 mm. The 71 % reproduces the #204 battery's 85-of-91 on an independent
battery, a different commit and a repaired instrument — the number has not
moved.

Four stops are the carried payload, splitting evenly clear/contact;
`adr0101_recovery` reports 2 of 4 recovered, median 17.99 mm, minimum 11.13 mm.
Far below the offline 94 %, but n=4 does not contradict it and no revision is
claimed.

Two findings the levers do not cover:

- Three of seven stops are `estop-initial-configuration` — the arm stopped at
  reset by its own start pose, before doing anything. tight_geometry, modeled
  fixtures and voxel resolution all address the carry phase; none addresses a
  base placement that starts the arm inside a counter. One is at +0.67 mm and
  would survive any geometry work.
- Five of thirteen rounds never grasped at all. With the ceiling result, roughly
  half of what reads as collision-programme failure on these scenes is the
  policy not reaching the phase where the kernel matters.

One round (fridge seed 6) completed with the gate on, against a 2.3 % gate-on
rate in the ceiling battery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 9676c931477608c46236e28adaf5d6346f7ecd54)

* docs(plan): record the repaired-instrument rate, and two classes no lever covers

The false-positive rate re-measured on a working harness and a working
adjudicator is 71 % (5 of 7 stops of a physically clear robot), reproducing the
#204 battery on an independent battery and a different commit. The headline
number has not moved.

Also records the #220 adjudicator inversion that made that measurement possible
to get wrong for two days, and adds two open items the §5 levers do not touch:

- start-state collisions, a third of all stops, where the arm is stopped at
  reset by its own pose before doing anything;
- rounds that never grasp at all, five of thirteen, which bound how much of the
  scorecard any collision work can move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit c1e2203a2e3c07383bc7a3db9f1691fa28ed8ab9)

* feat(evidence): give the stop decomposition a producer, and measure the link fix

PLAN.md §5's table is the measurement that struck two collision levers and
promoted a third: per stop class, the kernel's over-approximation with the grid's
21.65 mm half-diagonal subtracted. Like ADR-0101's 94 %, it was computed by hand
and had no producer. `tools/stop_excess.py` is that producer.

Two guards, both tested. Only stops whose probe certified its distances are
counted — an uncertified distance can be wrong by 15-108 mm, larger than the
quantity being measured. And the half-diagonal comes from each round's own
recorded `grid_resolution_m`; a stop without one is skipped rather than
defaulted to 25 mm, since assuming it would not perturb the answer but replace
it. `has_geometry_headroom` is strict at zero: a class exactly at the voxel term
cannot recover a millimetre, so calling that headroom would license wasted work.

Run over the 13-round adr0101-live battery it gives the first live read on
whether the tight_geometry work did what it was designed to do:

  payload  n=4   median excess +13.15 mm   beyond voxel  -8.50 mm
  link     n=3   median excess +25.51 mm   beyond voxel  +3.86 mm

The #204 battery measured the link class at +33.1 mm beyond voxel, with
panda_link6 holding 18 of its 29 link stops. link6 now ships tight geometry and
the class reads +3.86 mm. n=3 and a different link mix, so this is consistent
with the fix working rather than proof of it — but it is the first live evidence
in that direction, and it is what PR #235 was missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 551b7c5932929d39a685038a5c8cb1d0d5918f08)

* fix(hal): the backing probe could not see a slab coincident with its shell

`c7bd2c7` taught the probe to walk past a non-collidable strike and cast again,
which finds a collidable slab BEHIND a decoration shell. It cannot find one
coincident with it, and RoboCasa builds every counter top that way:
`robocasa/models/fixtures/counter.py` emits one full-span `<name>_top_visual`
(`contype=0`) and then tiles the same volume with collidable chunks via
`_get_chunks`. Their surfaces are the same plane, so stepping `distance + eps`
past the shell lands inside the chunk, where the ray reports no further entry
surface and the chunk is never seen.

That is what produced the three-way contradiction recorded this morning. It is
now settled from the round's own certified witness points: the cell spans
z in [0.90005, 0.92505] and the collidable chunk `counter_1_right_group_top_0`
has its surface at z = 0.920. The solid geometry was inside the cell the whole
time; the certified probe and the asset code agreed and only the backing probe
was wrong.

`voxel_backing_record` now falls back to a world-AABB overlap sweep over
collidable geoms when, and only when, the rays found nothing solid. An AABB
overlap can claim a geom whose surface misses the cube, so it is deliberately a
supplement rather than a replacement — for a diagnostic whose failure mode is
calling real geometry "decoration" (#180), erring toward found is the right
direction, and it can never override a ray pass that already found something.

Reproduced with a coincident shell/chunk fixture that yields the same 9-of-27
ray signature as the live round; mutation-checked by forcing the sweep off.

Diagnostics only (CLAUDE.md §1.4) — the certified probe measures geom-to-geom
distance and never used rays, so the 71 % rate and the stop decomposition are
unaffected. What moves is the backing CLASS of cells previously read
`noncollidable_world`, which is what the "32 % too sparse" entry and ADR-0101's
"cells no real body explains" premise rest on. Both should be re-derived from a
post-fix round before being leaned on further.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 10a3469ca9885c93414e280543b2a09fe863d00f)

* docs(plan): the geometry levers are exhausted, and link1's hull is not worth it

Three closures from the 13-round battery and its decomposition.

The probe contradiction was the instrument again: the cell spans
z in [0.90005, 0.92505] and the collidable chunk's surface is at z = 0.920, so
the solid geometry was inside it all along. Fixed; diagnostics only.

panda_link1 is the only link without a stage-2 hull (1588 vertices against a 320
cap) and caused two of three start-state stops, which made it look like the next
obvious manifest edit. Measured, those stops are +3.86 and +8.68 mm beyond the
voxel term, so an exact hull recovers at most ~9 mm of a ~25 mm error while
raising the cap 5x is a hot-path change needing safety-WG review. Struck.

And the conclusion those two produce together: the link class now measures
+3.86 mm beyond voxel, down from +33.1 mm in the #204 battery whose link stops
were 18-of-29 panda_link6 — the link that now ships tight geometry. With the
payload class at -8.50 mm, both are at or below the grid term. Every remaining
millimetre of over-approximation is the 25 mm voxel grid, and refining that was
struck on measured cost. No tighter envelope anywhere can recover anything
further.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 1d116ae9cdef8d25beec9f13cd8c31fa3a5c2aed)

* fix(safety): batch the hull-overhang query so a large link cannot exhaust memory

`hull_overhang_m` fed every barycentric sample to
`trimesh.proximity.closest_point_naive` in one call. That query allocates a
`samples x mesh-faces x 3` array, and the docstring's own premise — "a few
thousand by a few hundred here" — holds only for the small panda links that
have ever reached it.

`panda_link1` is 6260 mesh vertices and ~12k triangles; a 320-vertex hull over
it is ~636 facets at 325 samples each, so the single call asks for 57.8 GiB and
raises `numpy._core._exceptions._ArrayMemoryError`. Latent until something
declared a hull for that link.

Batching at 512 samples per call bounds peak memory by `512 x faces x 3`
independently of link size, and the maximum over batches is the maximum over the
whole set, so the returned number is unchanged. `check` still reports
mesh-outside-DOP +0.000000000 mm on all seven links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTE4eEfxW8FvvBnPZL6otg
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit e8e07315a5be846e2d27002a87c3aa258c1d7a2a)

* feat(tools): give the generator a third option when an exact hull is over budget

`derive_tight_geometry` had two outcomes for a link: ship the exact convex hull,
or -- if it exceeds `MAX_TIGHT_HULL_VERTICES` -- fall back to the 26-DOP alone.
`panda_link1` is the second case (1588 vertices), and its DOP's support gap
against the real mesh is a median 4.52 mm and up to 25.68 mm.

`refine_dop_to_budget` builds a third thing: the DOP intersected with the exact
hull's own face planes, worst-violation first, stopping before the vertex count
would exceed the budget. Every candidate plane is tangent to `conv(mesh)`, so
containment stays *definitional* rather than fitted, and the result is
`mesh ⊆ result ⊆ DOP ⊆ box` at every step. A subset-then-expand construction
cannot promise that: expansion pushes vertices out through the DOP slabs, and
link1's DOP has 0.083 mm of room inside its manifest box. The routine refuses
rather than emit an envelope that cuts its mesh.

On link1's real mesh it reaches a support gap of 0.18 mm median / 0.65 mm max
against the DOP's 4.52 / 25.68 mm, at 320 vertices.

**No manifest declares a refined envelope, and this commit changes none.** The
envelope was generated and put under a live battery on 2026-09-07; the stops it
was predicted to clear moved by 0.0003 mm. The prediction failed because it read
a support-census deficit against the shipped *box* and attributed all of it to
the hull, when the DOP had already collected it. That refutation is recorded in
the docstring, in `docs/methods/10-tools.md` and in the evidence page, so the
next reader does not re-derive the same wrong expectation from a routine that
looks like it was built for a reason nobody wrote down.

Which is also why the test is a direct one rather than a manifest assertion: an
unexercised generator path rots. It pins the three properties a safety-WG
reviewer would otherwise re-derive by hand -- mesh inside the result, result
inside the DOP, and strictly less enclosed volume than the DOP it refines --
against the real robosuite mesh, not a fixture.

Also carries the memory bound `_OVERHANG_BATCH` / `_OVERHANG_MAX_SAMPLES` needs
to document: the single-call `hull_overhang_m` form asks for 57.8 GiB on a
320-vertex envelope over a 12k-triangle mesh.

How tested: `pytest tests/unit/test_collision_tight_geometry.py` (11.5 s for the
new case, inside the 30 s unit budget).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(plan): correct the link1 strike — the reasoning was wrong, not just the call

The earlier entry struck link1's hull because it "recovers at most ~9 mm of a
~25 mm error". That is the wrong question: what matters is whether the recovery
flips the stop, and the start-state census's deficit table says 10 mm clears 14
of 14 link1 states. It also assumed the only options were the DOP or an
over-budget hull, and that a kernel cap change would be needed. Neither held —
the refined envelope fits the existing 320-vertex budget.

Records the measured result (0.18 / 0.65 mm against the DOP's 4.52 / 25.68 mm,
p99 0.5 ms on 9891 cells) and downgrades the start-state item from "no lever
touches it" to "one lever now reaches it, unconfirmed": two of its three stops
were link1, predicted to clear but not yet observed in a post-change battery.
The third is a genuine +0.67 mm near-contact that no geometry work reaches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 7fcc7168f57786846ff51a2df1e97b7cd1ebce5c)

* docs(collision): the link1-envelope confirmation battery is void, and says so

Eight rounds were run to confirm that panda_link1's refined envelope clears the
two panda_link1 start-state stops. All eight died at 16-19 s with the XR-1
sidecar OOMing at boot: a concurrent job on this shared host held 2.0-2.5 GB of
7.53 GiB while the sidecar needs ~3.5 GB alongside the scene.

Recorded rather than discarded because the failure mode is exactly the shape of
the result being looked for. Every round shows `stop: null`, and "seeds 2 and 4
no longer produce estop-initial-configuration" is precisely what the envelope
predicts — but the policy never loaded, so the arm never moved and no kernel
check ran. Reading it as confirmation would be reading a crash as a measurement.

The prediction stands unconfirmed. Confirming it needs an uncontended window
here or a host with headroom; spark is the latter, with the standing caveat that
XR-1 has never completed an end-to-end rollout on GB10, so one smoke round must
succeed before a battery is worth running there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit f4a670f55809e56de5f163d3bf33b75c010fce4e)

* docs(collision): the link1 refined envelope does not move the stops it was built for

Three rounds on spark, utensil seeds 2/3/4, on the commit carrying panda_link1's
refined envelope — also the first end-to-end XR-1 rollout completed on GB10.

The prediction is refuted. Same seed, same scene, same stop, one commit apart:
seed 2 reads -2.37794 mm under the 26-DOP and -2.37825 mm under the refined
envelope; seed 4 reads -8.31 and -8.31495. Tightening link1 from a 25.68 mm
worst-case support gap to 0.65 mm moved the reported depth by 0.0003 mm.

The reasoning error is identifiable. The start-state census's "10 mm clears 14
of 14 link1 states" is computed with box_box_distance against the manifest OBB.
The 26-DOP shipped after that census and already collected that recovery
(53.27 -> 25.69 mm). Treating the census's OBB-relative deficit as still-
available headroom double-counted a tightening that had already landed.

This is therefore the controlled test that the geometry levers are exhausted:
the same stop under two envelopes differing by 25 mm of worst-case looseness,
moving 0.0003 mm.

Notes the untested hypothesis for the residual: the octomap bridge's README
records that a published grid can report a surface up to one full tree
resolution (25 mm) nearer than it is, and stop_excess.py subtracts only the
21.65 mm half-diagonal — so map inflation reads as geometry excess and invites
exactly the hunt this entry closes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit b55f90af3f2ae0a7a6626b0773dfefbd885121fa)

* docs(plan): the link1 envelope was refuted, and the exhaustion claim got a test

Corrects the link1 item twice over. I struck the lever for the wrong reason
(fraction of error recovered rather than whether it flips the stop), then
un-struck it for another wrong one: the census's "10 mm clears 14/14 link1
states" is computed against the manifest OBB, and the 26-DOP shipped later had
already collected that recovery. Three rounds on spark measured the same stops
under both envelopes and they moved 0.0003 mm.

That failure upgrades the exhaustion conclusion from an inference over a
decomposition to a controlled test: the same stop under two envelopes differing
by 25 mm of worst-case support gap does not move. Whatever the residual is, it
is not the collision model.

Start-state goes back to "no lever reaches it" — it is a base-placement question,
and the census shows joints 3-7 cannot change a start-state verdict at all when
link1 or link2 dominates, which is 83.3% of stopping states.

Adds the hypothesis that displaces the geometry hunt: octomap marks the cell
containing the ray endpoint, so the grid can report a surface up to a full 25 mm
tree resolution nearer than it is, and stop_excess.py subtracts only the
21.65 mm half-diagonal. Untested, and the first thing worth measuring next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 7d90a810f98239251c4198a3909c44b35bb3d5bd)

* docs(collision): the map-side reading of all three start-state stops

estop-initial-configuration was three of seven stops in the adr0101-live
battery and the class no lever in PLAN.md §5 addresses. This is the first
map-side reading of it.

Where the evidence lives matters and is counter-intuitive: a start-state
snapshot carries collision_evidence: null and evidence_voxel_backing: null,
because the E-stop fires before the kernel's safety.collision line reaches the
bridge and the freshness gate correctly refuses to attribute a cell. The record
arrives on the deferred sim.estop_ground_truth_evidence path instead
(run_gt_evidence.json, backing_after_snapshot_ns: 0). Reading only the snapshot
shows nothing and invites the conclusion that the class is un-diagnosable.

Three stops, three causes:

- utensil-s2 is textbook quantisation. The cell CONTAINS the true nearest
  surface point of the door backing it (0.00 mm), so the map is where the world
  is; +23.13 mm clear, reported -2.38 mm, excess 25.51 = 21.65 half-diagonal +
  3.86 mm. This also refutes the octomap-inflation hypothesis raised earlier the
  same day: an inflated cell would not contain the surface.
- fridge-s2 has the robot in the cell and no world geometry at all — 15 of 27
  rays struck robot0_link2_collision, the same link the kernel stopped.
- utensil-s4 shows the same signature on weaker evidence: its robot geoms come
  from the conservative AABB sweep, not rays (0/27). The same stop read
  `unbacked` on q-laptop before that fix landed, so the fix turned a blank into
  a diagnosis.

Records that the mechanism is NOT a missing exclusion — the self-filter passes
robot bodies to the synth, matches by prefix plus descendants, and marks
transparent rays so they clear rather than mark — and names the decisive test
that no artifact supports today: re-run the layout with the arm parked
elsewhere and see whether the cell persists. That needs a start-pose override
the harness lacks.

n=3 and self_occupancy_suspect is explicitly not conclusive alone, so this is
logged as a lead, not a finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit d63d6ad608321441655af3dc4bfd6ad535436427)

* fix(hal): sweep for world geometry when a cell's only solid backing is the robot

`self_occupancy_suspect` was resting on 27 rays having missed nothing. A cell
whose only collidable ray hit is a robot body produces an identical record
whether world geometry is absent or merely unsampled, and that distinction is
exactly what separates a self-occupancy stop from ordinary quantisation.

Measured on the 2026-09-07 `fridge-s2` start-state stop: 15 of 27 rays struck
`robot0_link2_collision` — the same link the kernel stopped — and no world geom
appeared. Nothing in the record said whether one was there, so the stop could
not be classified either way.

The AABB overlap sweep now runs when the rays found no collidable **world**
geometry, rather than only when they found nothing collidable at all. That
covers the original coincident-shell case unchanged and adds the robot-only
case. A cell whose world backing the rays already found is still left alone, so
this cannot change a verdict the ray pass got right.

Diagnostics only (CLAUDE.md §1.4). Verified on the fixture: a cell on real world
geometry reports `swept=False`, a cell holding only the robot reports
`swept=True` and `self_occupancy_suspect`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 448818c49a31b55c99a971529e3963eb8e9039ae)

* docs(collision): start-state resolved — all three stops are quantisation

The widened sweep answers the ambiguity the previous entry named. Both
candidate self-occupancy stops were re-run on spark at that commit, and both
resolve against the lead: fridge-s2's cell contains the fridge drawer
(fridgesidebyside_main_group_1_g96 — the same body its near-miss pair named at
+0.673 mm), and utensil-s4's contains the cabinet door. The 27 ray fans had
simply missed them. The robot geoms in both cells are real but incidental — the
arm is beside the surface, not instead of it.

So all three start-state stops are ordinary voxel quantisation against
correctly-mapped world geometry. Not self-occupancy, not map inflation
(utensil-s2's cell contains the true surface point at 0.00 mm), and not link
envelope conservatism — the panda_link1 envelope moved these same stops by
0.0003 mm the same day.

Two of the three are stops of a demonstrably clear robot (+23.13, +22.01 mm).
fridge-s2 is separated out as a genuine near-contact at +0.67 mm that no
reduction in map conservatism should clear.

This settles the last population with an unexplored root cause, and it has the
same one as the payload class: the 25 mm grid. It therefore has the same single
remaining lever, and ADR-0101 is currently scoped to the carried payload only —
extending it to bare links would cover both classes with one mechanism. A scope
observation for the WG, not a decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 115e7b2c0f79f2d6ff1516045f40339b197a7e5f)

* docs(plan): start-state is quantisation — two root causes raised and both refuted

Closes the last population with an unexplored root cause. Three start-state
stops read map-side for the first time; map inflation and self-occupancy were
each raised as causes and each refuted by measurement, as was the link envelope.

The self-occupancy refutation is the substantive one: fridge-s2 looked like the
robot in its own map (15/27 rays on robot0_link2_collision, no world geom), and
widening the backing sweep then re-running on spark found the fridge drawer in
that cell — the same body its near-miss pair named at +0.673 mm.

So the class is ordinary voxel quantisation, with the same single lever as the
payload class. ADR-0101 is scoped to the carried payload; extending it to bare
links would cover both with one mechanism. Recorded as a scope note for the WG,
not a decision.

Separates fridge-s2 as a genuine near-contact at +0.67 mm that no reduction in
map conservatism should clear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
(cherry picked from commit 49b4197c9503d8ea4aa06f25f049ff2931ca741d)

* docs(collision): this week lands in three slices, and one change lands nowhere

33 commits and 4 822 lines is over CLAUDE.md §4.2.5's ceiling, and it stopped
being one logical change some days ago. Split by what gates each piece:

* **A** (this branch) — the four instrument repairs, the two evidence producers,
  `refine_dop_to_budget`, the ceiling probe, the narrow-phase latency surface,
  PLAN.md and the evidence ledger. Touches no manifest and neither
  `packages/openral_safety/` nor `cpp/` (empty `git diff --stat` against both),
  so §1.4 applies and §3 does not.
* **B** `feat/216-tight-geometry-link3-4-6` — the three manifest envelopes,
  safety-WG gated on hazard-log Entry 026.
* **C** — `panda_link1`'s envelope, withdrawn. The tool is in A; the manifest is
  nowhere.

A is the one with a deadline. `master` has carried two of the four defects since
2026-09-05 — a harness that cannot discover the action server it launches, and an
adjudicator that stamps every stop `real-contact` off a permitted adjacent-link
overlap — and both corrupt the programme's primary measurement. Every round taken
on `master` since then is unusable, so nothing downstream can be measured until
this lands. That is the whole argument for splitting rather than waiting for the
WG on one PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(methods): refresh the line markers the probe and tool changes moved

`tools/refresh_methods_linenos.py --check` was reporting 46 stale `(LNN)`
markers across `01-hal.md` and `10-tools.md` -- the mechanical consequence of
the backing-probe repairs and the new generator routine shifting line numbers in
files those pages index. CLAUDE.md §4.4 requires the check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* docs(collision): the voxel-resolution strike was an estimate, and it was wrong by 32x

PLAN.md §5 struck the 25 -> 15 mm lever on cost: 26.7 ms estimated against a
33 ms ceiling. It was the only lever in the programme struck on paper rather
than by test. Measured on the real kernel under a real grid, at four resolutions
over the same volume, 200 chunks each:

| resolution | occupied | p99 | estimate |
| ---: | ---: | ---: | ---: |
| 25 mm (shipped) | 9 891 | 0.517 ms | 5.8 ms |
| 20 mm | 17 321 | 0.597 ms | 11.3 ms |
| 15 mm | 35 828 | 0.825 ms | 26.7 ms |
| 12.5 mm | 59 948 | 0.838 ms | 46.1 ms |

Two errors compounded. The 5.8 ms baseline came from the shipped hull
microbenchmark, not from a round trip under a real grid -- which had no latency
surface until one was built the same day the strike was written; the real
baseline is 0.517 ms. And the cubic factor was applied to the wrong term: the
window loop opens with `if (grid.occupancy[idx] == 0) { continue; }`, so
`O(1/res³)` falls on a branch-not-taken while the real work scales with occupied
cells, which are a surface. Cells x7.65, occupied x6.06, p99 x1.62.

The cap objection fails too: 15 mm is 376 680 cells, under the shipped
`world_voxel_max_cells = 614 125`. §5's 2.8M figure was a whole-kitchen grid,
not the arm-neighbourhood window the kernel scans.

Un-struck, but explicitly **not yet actionable**. This measures the kernel
consuming a grid, not `openral_octomap_bridge` producing one at a finer tree
resolution, which is the other half of the cost and is unmeasured. That is the
next step on this lever, not a manifest edit.

`_RES` in the fridge pin file becomes `OPENRAL_FRIDGE_GRID_RES_M`-overridable so
the sweep reproduces from the shipped test rather than from a probe duplicating
it. The default is unchanged and is what `sim_e2e.launch.py` emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>

* feat(safety): ship tight geometry for panda_link3, link4 and link6

The 120-run #204 battery decomposed every stop into what the kernel
reported and what was certifiably there. Split by class, with the 25 mm
cell half-diagonal (21.65 mm) subtracted:

    payload stops (n=62): median excess 20.1 mm -> -1.5 mm beyond the voxel
    link stops    (n=29): median excess 54.8 mm -> +33.1 mm beyond the voxel

So the payload primitives are already tight and the LINK ENVELOPES are not.
`panda_link6` alone dominates 18 of the 29 link-class stops, including the
seven stops on one fridge cell (`voxel_352030`) at 37-63 mm of certified
clearance. It shipped no `tight_geometry` at all.

This adds the DOPs and hulls for the three links that had none:

    link      box excess -> DOP excess     recovered
    link3        75.57 mm -> 23.84 mm       51.7 mm
    link4        76.12 mm -> 23.15 mm       53.0 mm
    link6        52.70 mm -> 21.53 mm       31.2 mm

link6's 31.2 mm is almost exactly the 33.1 mm of measured link-class excess,
which is the corroboration that the OBB slop IS that excess.

WHY THIS REVERSES A RECORDED DECISION. `collision-hull-narrow-phase.md` §5.2
excluded exactly these three because #159 found they "hold zero of the 72
census stops". That census measured START STATES, and it was right about
them. The #204 battery measured the CARRY phase, which is where 71% of stops
happen, and there link6 dominates. The doc row is struck and rewritten rather
than deleted, so the reasoning that was correct-for-its-data stays visible.

SAFETY. This makes the envelope TIGHTER, so it is not conservative-by-default
and needs its containment argued rather than assumed:

* containment is definitional, not fitted -- the DOP is the intersection of 26
  tangent halfspaces `u.x <= h_mesh(u)` and the hull is `conv(mesh vertices)`;
* `generate_tight_geometry.py check` reports **mesh-outside-DOP
  +0.000000000 mm** on all seven links of both manifests;
* hulls are 152/152/102 vertices, the same class as the shipped
  link2/link5/link7, well inside `kMaxTightHullVertices` -- unlike link1's
  1588, which stays stage-1 only;
* both manifests are `hal.real: null`, so the sim-margin (0.0 m) benchmark
  applies, where the staged path measures 1.04-1.09x FASTER than the box path.
  The 0.02 m real-HAL table would need re-measuring before either manifest
  gains a real HAL.

`panda_mobile_vslam` gets the same blocks: the two manifests' arm geometry is
contract-tested identical (`test_panda_mobile_arms_use_matching_mesh_enclosing_obbs`).

STILL OWED, and not faked here: a safety-WG reviewer and a hazard-log entry,
which `test_the_manifest_actually_declares_tight_geometry` names as the price
of widening this set. This commit provides the measurement half of that.

How tested: `test_collision_tight_geometry.py` (15), `test_collision_params.py`
(9), `test_collision_geometry_contracts.py`, `test_all_manifests_validate.py`
(126) all pass, with the declared set, stage-2 count, CSR vertex counts, hull
overhangs and box/DOP excesses re-pinned to measured values. The DOP-excess
method was validated by reproducing all four previously pinned links exactly
(53.27->25.69, 46.83->23.01, 45.20->18.99, 28.25->12.97).
`test_kernel_fridge_layout_pin_start_state.py` passes 5/5 against a real
kitchen and a real kernel, so the layout-47 pin, the layout-30 ordering, the
genuinely-colliding pose and the all-occupied control all still hold.

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>
(cherry picked from commit e8a1e72cb7b8b0519a0d06b932a7f16d3eebec22)

---------

Signed-off-by: Adrian <adrianllopart@gmail.com>
Signed-off-by: Adrian <adrian@qualiastudios.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Adrian <adrian@qualiastudios.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sim): deploy sim sets no DDS scope — a sim round joined a live robot's ROS graph

1 participant