Skip to content

feat(gateway): ingest RuView RF context via /api/field (ADR-264 §8) - #2

Closed
fallen-pc wants to merge 3 commits into
ruvnet:mainfrom
fallen-pc:feat/rf-context-ingest
Closed

feat(gateway): ingest RuView RF context via /api/field (ADR-264 §8)#2
fallen-pc wants to merge 3 commits into
ruvnet:mainfrom
fallen-pc:feat/rf-context-ingest

Conversation

@fallen-pc

Copy link
Copy Markdown

Summary

Adds an opt-in RF-context bridge to rucelium-gateway: --rf-upstream <url> [--rf-poll-ms <ms>] polls a RuView wifi-densepose-sensing-server's GET /api/field (ADR-262 P3), verifies each event's provenance receipt (rufield_provenance::is_fusable), and keeps a bounded ring of the resulting RfContexts at GET /api/rf-context.

Why storage-only, not fusion

rucelium_worldgraph::fuse_rf_context requires pairing an RfContext against a specific existing environmental sample's WorldGraph node to compute a Plausibility verdict — and nothing in this codebase currently defines which biome zone or sensor a given RuView device's RF context is supposed to corroborate. Inventing that pairing here would be exactly the kind of fabricated correlation ADR-264 §8 (and this project's honesty discipline generally) exists to prevent.

So this PR deliberately stops at: get real, verified RF context flowing and visible. Every ingested event is tagged verified: bool — an unverified event is stored (visible, like a forged-data badge) but never silently dropped or upgraded — and /api/rf-context reports "fused": false explicitly, so a consumer never has to infer that from absence. The zone-pairing design for actual fusion is deliberate follow-up work, not guessed at.

A deliberate design choice, informed by #1

This bridge's ApiFieldPayload only declares the one field it needs (events). RuView's real /api/field response also carries signer_pubkey_hex and dev_signing_key, but this struct doesn't declare either — serde drops unknown fields by default, so it's inert to whatever shape those two informational fields take. That sidesteps the exact class of bug fixed in #1, where rufield-viewer's consumer of this same endpoint over-specified those two fields (wrong name on one, wrong type on the other) and broke on every real payload.

Test plan

  • cargo test -p rucelium-gateway — 63/63 passing (52 lib + 1 e2e + 4 push-federation + 6 restart), no regressions
  • New unit tests: verified/unverified tagging, non-WifiCsi events correctly excluded from RF context (still counted as received), bounded-ring eviction, poll-failure counting never touching the ring, --rf-upstream/--rf-poll-ms CLI parsing
  • Live-verified end to end against a real RuView instance (real ed25519-signed events, real WDP_RUFIELD_SIGNING_SEED): 2,240 events ingested across 35 polls, 0 unverified, 0 poll failures, all correctly labelled synthetic/simulated_node_* per the RuView-side fix in fix(rufield): simulated source now feeds /api/field, honestly RuView#1720.

Adds an opt-in bridge (`--rf-upstream <url> [--rf-poll-ms <ms>]`) that
polls a RuView `wifi-densepose-sensing-server`'s `GET /api/field`
(ADR-262 P3), verifies each event's provenance receipt
(`rufield_provenance::is_fusable`), and keeps a bounded ring of the
resulting `RfContext`s at `GET /api/rf-context`.

Deliberately storage-only, not fusion. `rucelium_worldgraph::fuse_rf_context`
requires pairing an RfContext against a *specific* existing environmental
sample's WorldGraph node to compute a Plausibility verdict, and nothing
in this codebase defines which biome zone/sensor a given RuView device's
RF context is supposed to corroborate. Inventing that pairing here would
be exactly the fabricated correlation ADR-264 Sec8 and this project's
honesty discipline exist to prevent - the zone-pairing design is
deliberate follow-up work, not guessed at. Every ingested event is
tagged verified: bool (unverified events are stored and visible, never
silently dropped or upgraded), and /api/rf-context reports "fused":
false explicitly so a consumer never has to infer that from absence.

The ApiFieldPayload this bridge decodes only declares the one field it
needs (`events`) - the two informational fields on RuView's real
response (`signer_pubkey_hex`, `dev_signing_key`) are deliberately left
undeclared, since serde drops unknown fields by default. That sidesteps
the exact class of bug fixed in ruvnet#1, where a *different*
consumer of this same endpoint over-specified those two fields (wrong
name on one, wrong type on the other) and broke on every real payload.

Live-verified end to end against a real RuView instance (real
ed25519-signed events, real WDP_RUFIELD_SIGNING_SEED): 2,240 events
ingested across 35 polls, 0 unverified, 0 poll failures, all correctly
labelled synthetic/simulated_node_* per the RuView-side fix in
ruvnet/RuView#1720.

New tests: verified/unverified tagging, non-WifiCsi events correctly
excluded from RF context (still counted as received), bounded-ring
eviction, poll-failure counting never touching the ring, plus CLI flag
parsing for --rf-upstream/--rf-poll-ms.
Extends the RF-context bridge to also run every verified event through
rufield_fusion::RuFieldFusion, so /api/rf-context now returns real
room-state inferences (person_present, breathing, etc.) alongside the
raw context ring - not just stored data, but RuField's own room-state
rules actually firing on it.

This is deliberately the OTHER kind of fusion from what the previous
commit ruled out. RuFieldFusion needs nothing external - no
environmental sample, no biome, no zone mapping - it's a
temporal-window fusion purely within the RF domain, over this
upstream's own events. There is nothing to fabricate here, unlike
rucelium_worldgraph::fuse_rf_context (still not called - see the
module docs for why that cross-domain pairing is still deliberate
follow-up work).

Only verified events reach the engine, mirroring rufield-viewer's own
rule: unverified events stay visible in the ring (flagged) but are
excluded from fusion entirely. /api/rf-context now reports
"fused_into_worldgraph": false explicitly (renamed from the previous
commit's "fused" for clarity, since this commit adds a within-RF
fusion that IS real) so a consumer never has to infer the WorldGraph
boundary from absence.

New tests: a verified presence-bearing event produces a real
person_present inference from RuField's default room-state rules; an
unverified event never reaches the engine at all (empty inferences).

Live-verified end to end against a real RuView instance: person_present
(confidence 1.0), breathing (0.92), and nocturnal_scratch (0.8), each
with real supporting_events pointing at actual RuView event ids.
@fallen-pc

Copy link
Copy Markdown
Author

Added a second commit to this PR: wires the ingested RF context into rufield_fusion::RuFieldFusion (RuField's own within-domain temporal fusion engine).

To be clear about scope — this is a different fusion from rucelium_worldgraph::fuse_rf_context, which the PR description explains this bridge still deliberately does not call (that one needs a biome zone/sensor pairing decision this codebase hasn't made yet). RuFieldFusion needs nothing external — it only combines this RF upstream's own events over a short window — so there's no fabricated correlation risk here, and it's the same engine rufield-viewer already runs on the identical event stream.

/api/rf-context now also returns room_state_inferences. Live-verified against a real RuView instance: person_present (confidence 1.0), breathing (0.92), nocturnal_scratch (0.8), each with real supporting_events.

Room-state inferences produced by the previous commit only ever
existed for the ~2s their producing rule's TTL covers - computed live,
on request, from whatever the fusion engine's window holds *right
now*. A person_present blip that fired between two polls of
/api/rf-context was gone by the next request, even one second later.
Every other durable thing in this gateway (ObservationStore,
EventStore) doesn't behave that way.

Real fix, not the full one: this is in-memory history, not disk
durability. RF inferences still have no biome node_id/sequence to cite
into EventStore the way a flood alert does (same structural gap as
fuse_rf_context and the earlier zone-pairing problem - checked
pipeline.rs's maybe_alert()/EvidenceRef before writing this, and
citing RF evidence there would mean fabricating a registry entry that
doesn't exist). So: a bounded 32-entry ring of timestamped
InferenceSnapshots, recorded only when a poll actually fed the fusion
engine something new (an empty or all-unverified poll is not history,
it's noise), exposed as inference_history at /api/rf-context.
Survives across polls; does not survive a restart.

New tests: a verified batch captures a snapshot with the right
timestamp and inference; an empty or all-unverified batch records
nothing; the ring is bounded and drops oldest first.

Live-verified: 28 distinct snapshots accumulated over ~10s against a
real RuView instance, growing toward the 32-entry cap - confirmed real
accumulating history, not a single instantaneous read relabelled.
@fallen-pc

Copy link
Copy Markdown
Author

Third commit: room-state inferences only existed for their ~2s TTL, computed live on request — a `person_present` blip between two polls was gone even a second later, unlike everything else durable in this gateway. Added a bounded 32-entry in-memory history ring (`inference_history` at `/api/rf-context`), recorded only when a poll actually fed the engine something new.

Checked `pipeline.rs`'s `maybe_alert`/`EvidenceRef` first to see if RF inferences could raise real signed local alerts the way flood readings do — they can't without fabricating a biome `node_id`/`sequence` registry entry for RF sources, which is the same structural gap as the zone-pairing problem this PR already defers. So this stays in-memory, not disk-durable; that's still open.

Live-verified: 28 distinct snapshots accumulated over ~10s against a real RuView instance.

@fallen-pc

Copy link
Copy Markdown
Author

Superseded by #3, which now folds this branch's rf_bridge work in directly (the two PRs didn't compose correctly as separate heads — see the review discussion on #3). Closing in favor of the consolidated PR.

@fallen-pc fallen-pc closed this Aug 28, 2026
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.

1 participant