mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-26 02:04:55 +00:00
ADR-262 §4 lists multi-modality as P4 and §8 question 5 leaves the choice of the second modality open, asking whether it should be `rvcsi`. This is a different answer, and the reason is that the cost collapsed. `rufield-adapters` now ships `UltrasonicReplayAdapter` — the first adapter for `Modality::Ultrasonic`, registry code 7, which had sat in the §8 registry since v0.1 with nothing implementing it. It parses, validates and signs BatVu (https://github.com/ruvnet/batvu) range profiles upstream. RuView does not have to build any of that. It has to decide what it is willing to put on a wire. ## The submodule bump `vendor/rufield` moves 43b1df3 -> 9955672, which is main. Two mechanical breakages in the existing bridge, both struct literals missing fields added upstream since the old pin: Observation + track_id, attributes, identity_evidence, channel_sounding_provenance SensorDescriptor + coordinate_frame, position_m, orientation_xyzw All left empty, and each for its own reason rather than because a default was convenient — the comments say which. The pose fields in particular are unset because a CSI link has no boresight and no surveyed position, and §6 is explicit that this surface makes no validated room-coordinate claim. A placeholder pose would be inventing exactly that claim. The ultrasonic path DOES set them, because a handheld scanner genuinely knows where it was pointing. The nine existing P1 gates pass unchanged against the bumped submodule. ## The decision this module actually makes RuView's egress rule is stricter than RuField's default guard — only P1/P2 leave the box — and the difference has to be structural rather than a runtime refusal. The adapter offers two output modes. The full per-bin frame is P0, which `network_egress_allowed` holds edge-local: correctly, but silently, as a dropped event at the end of a pipeline that did all the parsing and signing first. The 32-bin max-pooled reduction is P1 and egress-safe. So `ultrasonic.rs` does not offer the choice. It configures the coarse mode, because a consumer cannot un-coarsen a coarse profile whereas a runtime check can be reordered by someone who does not know why it is there. The gate still runs, and `gate_coarse_scan_passes_the_egress_gate_intact` asserts it drops nothing — a policy enforced only where it never fires is a policy nobody notices removing. ## Gates `tests/ultrasonic_gates.rs`, 12 tests, same shape as `p1_gates.rs`: round-trip and serde stability, signature-verify plus tamper-detection, fusion ingest, P1 on BOTH tensor and observation (the guard is conjunctive — a P0 tensor under a P1 observation is the composite leak it exists to close), structural unreachability of P4/P5, trust-tier refusal in both directions, determinism, and whole-file rejection of a malformed recording rather than a partial ingest. And one asserting the honest negative result: an ultrasonic scan produces NO fused inferences at all. The adapter declines to populate `presence` — one transducer pair cannot distinguish a person from a coat over the back of a chair — and the engine's feature vocabulary is entirely statements about a body, so `range_m` has nothing to drive. RuField v0.1 has no predicate for static geometry. That is asserted rather than papered over, because a demo that fabricated `presence` would light up `person_present` and look like success. ## The fixture `batvu_living_room.ultrasonic.jsonl` is not hand-written. It is produced by BatVu's own `npm run artifacts` and copied verbatim, and the same file is a fixture in ruvnet/rufield. A schema drift between BatVu's emitter and RuField's parser now fails a build in one of three repositories rather than an ingest in a deployment. ## Scope Not wired into the running server. P1 shipped as a library before P3 wired it in; this follows the same staging, so the translation and its gates are reviewable on their own. `UltrasonicScan::egress_events` returns exactly what `FieldSurface::emit` would need to broadcast. Honesty unchanged from P1, plus one that is BatVu's: every current recording is its own simulator's output, so events carry `synthetic: true` and are fusable only under simulation trust. `captured_replay()` and `production()` reject them outright, as they should. 27 tests pass (5 unit + 9 P1 + 12 P4 + 1 doc). clippy clean on the crate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRtoJRh2M4LV3vLcSg2nSX