{{ message }}
Repository navigation
feat(adapters): UltrasonicReplayAdapter — the first adapter for Modality::Ultrasonic - #11
Merged
Merged
Conversation
…ity::Ultrasonic `Modality::Ultrasonic` has been registry code 7 since v0.1 with nothing implementing it. This is that implementation: it replays `.ultrasonic.jsonl` recordings from BatVu (https://github.com/ruvnet/batvu), a handheld sonar that runs entirely in a phone browser — a 17.5-20.5 kHz chirp out of the speaker, a matched filter over the microphone, and a range profile of echo amplitude against distance along one beam. ## The tensor is [Range], and that is an argument, not a default `[Angle, Range]` is the obvious shape for a sonar and it would be a lie. `FieldAxis::Angle` means angle-of-arrival bins — what an array produces when it has measured direction. BatVu has one microphone. The direction attached to an echo is where the operator happened to be pointing, which is pose, not signal, and it rides in `SensorDescriptor::orientation_xyzw` as the minimal rotation taking sensor-local +Z onto the beam. Roll about the boresight is unobservable with one transducer pair, so the minimal rotation is the one that asserts nothing about the axis nobody measured. ## Two output modes; the safe one is the default The full per-bin profile is a sensor frame, and RuField classifies the analogous per-subcarrier CSI frame P0. Calling ours gentler would be special pleading, so `UltrasonicOutput::RangeProfile` is P0 and edge-local. `CoarseProfile` — the default — max-pools to 32 bins and is P1, which the stock policy allows onto a network. Max-pooling rather than averaging: a mean smears a sharp echo into its neighbours and a wall stops looking like a wall. `the_stock_privacy_policy_lets_the_coarse_profile_onto_a_network_and_refuses_the_raw_one` tests that with rufield's own guard rather than by reading the class off the struct and agreeing with itself. ## A recording cannot relabel itself into a higher trust tier Every line declares its `source`, and the adapter is constructed with the source the deployment is willing to accept. A file claiming `device_capture` fed to an adapter configured for simulation is refused outright — and so is the reverse, so neither direction is a silent reinterpretation. `provenance.synthetic` is then derived from the accepted source rather than taken from a wire field. ## The parser is hostile on purpose Line length capped before serde sees the bytes, so an oversized line cannot allocate its way past the check. `deny_unknown_fields`, bounded arrays, finiteness and sign checked on every numeric, strictly increasing timestamps, identifiers rejected for control characters, and a detection that lies outside the profile it came from refused as incoherent. Each of these has a test that names the input it stops. The timestamp bound in particular is not tidiness: `TrustVerifier`'s watermark only ever advances, so one event with a saturated clock permanently blocks every honest one from that device. ## The honest negative result The adapter sets `features["range_m"]` — the key the fusion window actually reads, as against the typed `Observation::range_m` field, which it also sets and which fusion never looks at. It deliberately does NOT set `features["presence"]`, though doing so would light up the shipped `person_present` rule and make a demo work. An echo at 2.4 m is a surface; one transducer pair cannot tell a person from a coat on the back of a chair. The consequence is that with the shipped rules a BatVu scan produces no inferences at all, for two independent reasons: no rule lists "ultrasonic", and no rule could help if it did, because the engine's feature vocabulary is entirely statements about a body and `range_m` has no `feat` arm. RuField v0.1 has no predicate for static geometry. `the_shipped_rules_have_no_predicate_a_range_only_sensor_can_drive` asserts exactly that. Closing it is a change to rufield-fusion, not to an adapter. ## Cross-language conformance `tests/fixtures/batvu_living_room.ultrasonic.jsonl` is not hand-written: it is generated by BatVu's own `npm run artifacts` from a 72-ping simulated sweep and copied here verbatim. If BatVu's TypeScript emitter and this parser drift apart, a build fails in one repository or the other rather than an ingest failing in a deployment. 32 unit tests, 7 integration tests. Workspace clippy and fmt clean; no other crate is modified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRtoJRh2M4LV3vLcSg2nSX
ruvnet
marked this pull request as ready for review
August 25, 2026 23:57
ruvnet
added a commit
to ruvnet/batvu
that referenced
this pull request
Aug 25, 2026
…ire (#3) A handheld ultrasonic sonar that runs entirely in a phone browser: a 17.5-20.5 kHz chirp out of the speaker, a matched filter over the microphone, and a log-odds occupancy map painted onto a plan-position display. Everything but the transducer runs on a Linux CI box. Rust/WASM DSP core, TypeScript pipeline, @metaharness/horizon for scan control and @metaharness/flywheel for policy evolution, and an iPhone web app in 28 KB of JavaScript with no framework. The output speaks RuField MFS: @batvu/field writes .ultrasonic.jsonl, which ruvnet/rufield#11 parses, signs and drives through trust verification, the privacy guard and the fusion engine. The same recording is a test fixture in both repositories, so schema drift fails a build rather than an ingest. @batvu/memory answers "have I been here before?" without being able to answer "where am I" — a 128-number descriptor invariant under rotation about gravity, which is exactly the one degree of freedom orientation-only pose cannot know. Two correctness bugs found by adversarial review, both of which had passed every existing test: - classifyEmission was called in exactly one place, the flywheel's simulated scorer. The guard standing between a scoring function and a phone that shrieks was not on the path to the speaker. It is now the first thing AudioSession.start() does. Four NaN inputs that deleted checks rather than failing them are fixed alongside it. - The time origin took the global maximum over a record containing about four transmit blasts, so noise chose which one. It corrupts the pose tag rather than the range: echoes up to three pings old, filed under the attitude now. No test saw it because the simulator renders one blast per record. 21 ADRs, a threat model, and a committed artifacts/ bundle carrying the file, the command and the SHA-256 behind every number quoted. 90 Rust tests, 146 TypeScript tests, 20/20 end-to-end checks in a real browser.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Modality::Ultrasonichas been registry code 7 since v0.1 with nothing implementing it. This is that implementation.It replays
.ultrasonic.jsonlrecordings from BatVu — a handheld sonar that runs entirely in a phone browser: a 17.5–20.5 kHz chirp out of the speaker, a matched filter over the microphone, and a range profile of echo amplitude against distance along one beam.One new module in
rufield-adapters, itspub mod/pub uselines, a dev-dependency onrufield-privacy, a test fixture, and a README section. No other crate changes; the workspace manifest needs nothing.The tensor is
[Range], and that is an argument rather than a default[Angle, Range]is the obvious shape for a sonar and it would be a lie here.FieldAxis::Anglemeans angle-of-arrival bins — the output of an array that has measured direction. BatVu has one microphone. The direction attached to an echo is where the operator happened to be pointing the phone, which is pose, not signal.So the profile is rank-1 and the beam rides in
SensorDescriptor::orientation_xyzw, as the minimal rotation taking sensor-local+Zonto it. Roll about the boresight is unobservable with one transducer pair, so the minimal rotation is the one that asserts nothing about the axis nobody measured.Worth noting for the record:
validate_evidence_atwould have accepted any of the three candidate shapes. There is noModality::Ultrasonicbranch inrufield-coreand this PR does not add one — the choice is enforced by the adapter's own tests instead.Two output modes, and the safe one is the default
The full per-bin profile is a sensor frame, and RuField classifies the analogous per-subcarrier CSI frame
P0. Calling ours something gentler would be special pleading for our own modality.Destination::Networkunder the stock policyRangeProfileP0CoarseProfile(default)P1Max-pooling rather than averaging is deliberate: a mean smears a sharp echo into its neighbours and a wall stops looking like a wall, whereas a maximum keeps every peak the detector cared about and discards only the shape between them — which is precisely the part worth not transmitting.
the_stock_privacy_policy_lets_the_coarse_profile_onto_a_network_and_refuses_the_raw_onechecks this withDefaultPrivacyGuarditself, rather than by reading the class off the struct and agreeing with ourselves.max_privacy_classis set per mode and not as a.max()over what is emitted —PrivacyClassordersP0 < P5, so the raw mode'sP0is numerically the smallest while being the most sensitive, and a maximum would reportP1for it.A recording cannot relabel itself into a higher trust tier
Every line declares its
source, and the adapter is constructed with the source the deployment is willing to accept. A file claimingdevice_capturehanded to an adapter configured for simulation is refused outright — and so is the reverse, so neither direction is a silent reinterpretation.provenance.syntheticis then derived from the accepted source rather than taken from a field the file controls.The parser is hostile on purpose
Modelled on
quantum_rf_replay, which is the more defensive of the two existing parsers:serde_jsonsees the bytes, so an oversized line cannot allocate its way past the checkdeny_unknown_fields, so a stray key is a parse error rather than attacker-controlled CPU spent parsing and discardingFieldTensor::validate(which checks only shape and axis rank), signs fine, verifies fine, and then fails to deserialize as anf32on the far side of the wire, which is the worst place to find itTrustVerifier's watermark only ever advances, so a single event with a saturated clock permanently blocks every honest event from that devicedevice_idmidway rejected outrightEach of these has a test that names the input it stops.
The honest negative result
The adapter sets
features["range_m"]— the key the fusion window actually reads, as against the typedObservation::range_mfield, which it also sets and which fusion never looks at.It deliberately does not set
features["presence"], although doing so would light up the shippedperson_presentrule and make a demo work. An echo at 2.4 m is a surface. One transducer pair cannot tell a person from a coat on the back of a chair, and a range-only sensor reporting presence is asserting exactly that distinction.The consequence is that with the shipped
room_state.toml, a BatVu scan produces no inferences at all — for two independent reasons, both pinned bythe_shipped_rules_have_no_predicate_a_range_only_sensor_can_drive:"ultrasonic"among itsinputs, so no rule ever sees the events.featvocabulary —motion_energy,breathing_band,transient,presence,posture_sit,posture_lie— is entirely statements about a body.range_mreaches the window but has nofeatarm, and the only predicate that reads it (range_depart) additionally requiresmotion_energy > 0.4, which a phone standing still cannot supply.The gap is in the rule vocabulary, not the adapter: RuField v0.1 has no predicate for static geometry. Closing it is a change to
rufield-fusionand out of scope here.observation.attributes["evidence_kind"]is set tosynthetic_replay/captured_replayfollowingquantum_rf_support.rs, becausebearing_trust's simulation policy rejects a synthetic event whose evidence kind is<missing>. An ultrasonic event has no bearing to fuse and should never reach that engine, but "should never" is not a mechanism.Cross-language conformance
tests/fixtures/batvu_living_room.ultrasonic.jsonlis not hand-written. It is generated by BatVu's ownnpm run artifactsfrom a 72-ping simulated sweep of its living-room scene, written by the TypeScript emitter in@batvu/field, and copied here verbatim. If BatVu's emitter and this parser ever drift apart, a build fails in one repository or the other rather than an ingest failing in a deployment.tests/ultrasonic_fusion.rsdrives that fixture through the whole stack: parse → calibrate → sign →validate_evidence_at→TrustVerifier→DefaultPrivacyGuard→RuFieldFusion::ingest.Honesty
Every current BatVu recording is its own simulator's output. The events are therefore
synthetic: true, andcaptured_replay()andproduction()reject them outright — as they should. The pipeline is real and the fixture is a real cross-language check; the measurements are not yet from a phone, and the README says so in both places it introduces the adapter.Verification
cargo test --workspacegreencargo fmt --all -- --checkcleancargo clippy --workspace --all-targets -- -W clippy::allintroduces no new warnings (the twotoo_many_argumentsare pre-existing)🤖 Generated with Claude Code
https://claude.ai/code/session_01BRtoJRh2M4LV3vLcSg2nSX
Generated by Claude Code