Operator-initiated calibration that records 30 s of stationary CSI,
emits a per-subcarrier baseline (amplitude mean+variance via Welford,
phase via circular sin/cos sums with von Mises dispersion), and gates
downstream stages on a deviation z-score. Plugs into multistatic
coherence gating, motion/presence detection, and the new ADR-134 CIR
estimator as a reference-subtracted input.
API surface (under wifi_densepose_signal):
CalibrationConfig::{ht20, ht40, he20, he40}
CalibrationRecorder { record(), finalize(), frames_recorded() }
BaselineCalibration {
subcarriers: Vec<SubcarrierBaseline>,
deviation(&CsiFrame), subtract_in_place(&mut CsiFrame),
to_bytes(), from_bytes()
}
CalibrationDeviationScore { amplitude_z_median, amplitude_z_max,
phase_drift_median, motion_flagged }
CalibrationError { SubcarrierMismatch, TierMismatch,
InsufficientFrames, VersionMismatch, TruncatedBuffer }
Binary baseline format: magic 0xCA1B_0001 + u8 version=1 + u8 tier +
captured_at_unix_s (i64) + frame_count (u64) + num_subcarriers (u32) +
[SubcarrierBaseline; N] as 16 bytes each (amp_mean, amp_variance,
phase_mean, phase_dispersion as f32 LE). Hand-written serialisation so
the format is stable across Rust toolchain versions without serde drift.
CLI: new `wifi-densepose calibrate` subcommand binds a UDP listener
(0xC511_0001 frames), streams them through CalibrationRecorder, prints
a real-time z-score banner per ADR-135 §risk 1 (operator-may-be-moving),
aborts on sustained high deviation, and writes the binary baseline to
disk. Local UDP packet parser duplicated from sensing-server (per ADR
discussion — avoids cross-crate API churn).
Witness: cross-platform-deterministic SHA-256 over the per-subcarrier
quantised baseline profile (u16 LE at 1e-2/1e-4/1e-3, no sort) using
the lesson learnt from the CIR PR #837 libm-jitter fix. Hash:
d6bce07ecb1648e6936561df44bf4a3bfc17bb0ba5f692646b2301d105b52f67
CI guard: new "ADR-135 calibration witness proof (determinism guard)"
step under the Rust Workspace Tests job, adjacent to the existing
ADR-134 CIR guard. Regressions are unambiguously attributable.
Hardware-in-loop validation: full 600-frame capture exercised via the
new scripts/synth-csi-udp.py emitter targeting 127.0.0.1:5005. The CLI
binary received 600 frames at 20 Hz, z_med stable at ~0.7, motion
correctly NOT flagged, finalised baseline written to baseline.bin (860
bytes) with correct magic + version + timestamp in the header. Live
ESP32 capture from COM9 is operator follow-up — requires provisioning
the firmware's UDP target IP to match the host running the CLI.
Test results (cargo test -p wifi-densepose-signal --no-default-features):
lib: 382 pass / 0 fail / 1 ignored
calibration_synthetic: 17 pass / 0 fail
calibration_drift: 5 pass / 0 fail
calibration_roundtrip: 10 pass / 0 fail
cir_*: 9 pass + 6 documented P2 ignores
doctest: 10 pass
Bench: 20 Criterion combinations registered
(recorder_record / recorder_finalize / deviation / record_600 /
to_bytes across HT20/HT40/HE20/HE40 tiers).
Witness: bash scripts/verify-calibration-proof.sh → VERDICT: PASS
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-134 — CSI→CIR via ISTA + NeumannSolver warm-start
End-to-end first-class Channel Impulse Response estimation in the Rust
workspace. Bridges CSI (frequency domain) to CIR (delay domain) so
multistatic coherence gating, NLOS/LOS classification, and (at HT40+)
ToF ranging become tractable in `wifi-densepose-signal`.
Algorithm: ISTA L1 sparse recovery over a normalized DFT sub-matrix
sensing operator Φ ∈ ℂ^(K×G) with G = 3K (3× super-resolution). The
Tikhonov-regularised warm start re-uses `ruvector_solver::neumann::
NeumannSolver` — same call pattern as `fresnel.rs:280` and
`train/subcarrier.rs:225` — so no new crate dependencies.
Tiers supported: HT20 / HT40 / HE20 (Tier A-HE, C6) / HE40. The C6
HE-LTF tier is the preferred Tier A target whenever an 11ax AP is in
range; firmware substrate already shipped at v0.7.0-esp32 per ADR-110.
Measured performance (release, single CirEstimator shared across 12
links): HT20 2.72 ms / HE20 3.20 ms / HT40 13.43 ms / HE40 9.71 ms per
estimate(). HT20 12-link multistatic 17.7 ms — fits the 50 ms RuvSense
cycle; HT40 12-link 74 ms exceeds it and is flagged in ADR-134 §2.7 as
requiring Rayon parallelism or G=2K super-res reduction.
Measured Φ conditioning: κ(Φ) ≈ 1.00 identically across all tiers.
ADR-134 §2.3 was corrected — the C6 advantage is statistical SNR gain
(√(242/52) ≈ 2.16×) from more independent measurements, not improved
conditioning.
Witness: bit-deterministic SHA-256 over CirEstimator output on the
synthetic ADR-028 reference signal (100 frames, top-5 taps, 1e-6
quantization). Hash committed to expected_cir_features.sha256;
verify-cir-proof.sh wires the check into the existing witness bundle.
CI: cargo test --features cir + verify-cir-proof.sh added as separate
steps under the Rust Workspace Tests job; regressions are unambiguously
attributable.
Files:
- ADR + WITNESS-LOG-028 row 34 + CLAUDE.md module count (14 → 15)
- src/ruvsense/cir.rs (~540 LOC) + lib.rs re-exports + multistatic.rs
wire-up (reversible via `use_cir_gate=false`)
- 3 integration tests + Criterion bench + 3 deterministic fixtures
- cir_proof_runner binary + sha256 + verify-cir-proof.sh
Test rate: 395 pass / 6 ignored (P2 ISTA hyperparameter tuning; see
#[ignore] reasons) / 0 fail. cargo check clean; verify-cir-proof.sh
VERDICT: PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): make CIR witness cross-platform-deterministic
The first witness (Windows-generated hash 89704bfd…) failed on Linux CI
with a different hash (b36741bf…). Root cause: hashing `re`/`im` parts of
top-5 taps at 1e-6 precision is too tight against libm differences in
sin/cos/sqrt across glibc, MSVC, and Apple-clang. The previous
"top-5 sorted by magnitude" form also suffered from rank instability when
taps are near-tied — libm jitter could shuffle the ordering even when the
algorithm is unchanged.
New canonical form: full per-tap quantised-magnitude profile in natural
index order, no sort.
- 156 taps × 2 bytes (u16 le) per frame = 312 bytes/frame.
- Quantisation 1e-2 — robust to ~1e-3 float drift while still tripping
on real algorithmic changes (e.g., a 10× lambda shift moves magnitudes
by >1e-2).
- No top-K selection — eliminates the unstable magnitude-sort step.
Regenerated expected_cir_features.sha256 — new hash 120bd7b1…
If the next CI run still mismatches, the cause is structural (rustfft SIMD
code path selection or NeumannSolver internal ordering), not magnitudes,
and the witness needs further coarsening or to be made platform-tagged.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary
Inserts a Python equivalent of `wifi-densepose-bfld::PrivacyClass` +
`PrivacyGate` between the rv_feature_state parser and the HAP toggle
file. ADR-125 §2.1.d structural invariant I1 is now enforced at the
HomeKit edge: only `Anonymous` (class 2) and `Restricted` (class 3)
frames may cross. `Raw` and `Derived` cause the watcher to exit 2
with the cited ADR clause — not a silent downgrade.
Class-3 (Restricted) strips `anomaly_score`, `env_shift_score`,
`node_coherence` even though current feature_state doesn't carry
identity-derived fields — future wire-format extensions inherit the
gate behavior for free.
Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher
logs `Unknown Presence` (not "intruder detected" / "security state").
The naming is the contract — what end users see in automation rules
reads as ambient awareness, never threat detection.
Empirical (with --privacy-class anonymous on live C6):
pkts=58 valid=51 crc_bad=0 motion=True
privacy class: Anonymous (HAP-eligible)
semantic event: Unknown Presence
Refuse path validated:
$ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived
REFUSED: privacy class Derived (value=1) is not HAP-eligible.
ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and
Restricted (3) frames may cross the HomeKit boundary.
$ echo $?
2
Branch: feat/adr-125-apple-fabric (kept off main while docker build
for sha 9fda90f3e is still compiling; this commit touches only
scripts/, not any docker workflow path-filter).
Refs ADR-125 §2.1.d, ADR-118 §2.1/§2.2.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-125 iter 4): CHANGELOG bullet for the APPLE-FABRIC e2e
Pre-merge checklist item 5. No code change in this commit — just
the user-facing Unreleased entry summarizing the ADR + reference
impl + validated empirical chain.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1 #1): multi-characteristic accessory + JSON-state IPC
The HAP accessory now carries three services on the same paired
entity (HomeKit allows multiple services per accessory; iPhone
refetches /accessories when config_number bumps):
- MotionSensor — short-window motion_score, immediate
- OccupancySensor — rolling-3s avg presence_score, sustained
- StatelessProgrammableSwitch — "Unrecognized Activity Pattern"
event (Restricted-class only; fires on
anomaly_score >= 0.7); ADR-125 §2.1.d
semantic naming, not security state
New JSON IPC contract `/tmp/ruview-state.json` between watcher
and HAP daemon:
{ "motion": bool, "occupancy": bool, "anomaly_ts": float,
"ts": float }
Atomic writes (tmp + rename). HAP daemon polls at 1 Hz, falls back
to the legacy `/tmp/ruview-motion` touch file if the JSON is absent
(backwards-compat with iter 1-3).
Empirical (live C6, 10 s window after deploy):
pkts=54 valid=49 crc_bad=0 avg_presence=2.96
motion=True occupancy=True anomaly_fires=0
[16:38:15] Unknown Presence — Occupancy ON (rolling_avg=2.79)
Pairing survived:
paired_clients: 1
config_number: 3 (was 1; HAP-python bumps automatically on shape change)
Tier 1 #1 (multi-characteristic) of the Tier 1+2 sprint. Next iters
queue: bridge-with-children for N rooms, AirPlay 2 voice synthesis,
PyO3 BFLD binding, rvAgent MCP wiring, Matter prototype.
Refs ADR-125 §2.1.c (bridge topology), §2.1.d (semantic events),
ADR-118.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 2): sensing-server-equivalent for @ruvnet/rvagent
scripts/ruview-sensing-server.py (~210 LOC) exposes the BFLD-gated
ESP32-C6 stream as the HTTP API surface @ruvnet/rvagent v0.1.0
(ADR-124, npm) expects. Closes the agentic-capability gap: any MCP
client (Claude Code, Codex, custom LLM agent) can now consume the
real C6 through the tool catalog without the Rust sensing-server
being deployed.
Endpoints (mirrors tools/ruview-mcp/src/tools/*.ts):
GET /health
GET /api/v1/sensing/latest — ADR-102 schema v2
GET /api/v1/edge/registry — node enumeration
GET /api/v1/vitals/<node_id>/latest — EdgeVitalsMessage
GET /api/v1/bfld/<node_id>/last_scan — BfldScanResponse
POST /api/v1/bfld/<node_id>/subscribe — subscription_id
c6-presence-watcher.py now writes a companion `/tmp/ruview-last-
feature.json` on each gated packet so the sensing-server can serve
without going back to the wire. Atomic tmp+rename. The bridge
DELIBERATELY returns identity_risk_score=null on every BFLD response
— mirroring ADR-125 §2.1.d at the HTTP boundary even though the
rvagent schema's slot is nullable.
Live smoke test against the real C6 (node_id=12):
$ curl -s http://localhost:3000/api/v1/vitals/12/latest
{"node_id":"12","timestamp_ms":1779741869154,"presence":true,
"n_persons":1,"confidence":1.0,"breathing_rate_bpm":18.75,
"heartrate_bpm":40.0,"motion":1.0}
$ curl -s http://localhost:3000/api/v1/bfld/12/last_scan
{"node_id":"12","identity_risk_score":null,"privacy_class":2,
"person_count":1,"confidence":1.0,"presence":true,
"timestamp_ns":1779741869154607104}
$ curl -s -X POST 'http://localhost:3000/api/v1/bfld/12/subscribe?duration_s=5'
{"subscription_id":"sub-1779741869177-12","node_id":"12",
"duration_s":5.0,"endpoint_hint":"poll GET ..."}
Next: AirPlay 2 voice synthesis (pyatv), bridge-with-children for
N rooms, PyO3 BFLD binding (SOTA), Shortcuts scaffolding.
Refs ADR-124 (@ruvnet/rvagent contract), ADR-125 §2.1.d, ADR-118.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 3): production HAP bridge with N child accessories
scripts/ruview-hap-bridge.py (~170 LOC) implements the ADR-125 §2.1.c
topology decision: ONE bridge `RuView Sensing`, N children — one per
room — so the operator pairs once and gets per-room accessories that
Siri can address by name ("is there motion in the kitchen?").
State per room comes from /tmp/ruview-state.<room>.json. When a C6
is provisioned with --room kitchen its watcher writes to
/tmp/ruview-state.kitchen.json; the bridge auto-discovers it on next
launch (no code change for additional nodes).
Legacy /tmp/ruview-state.json (iter 1-2 single-file IPC) maps to the
--legacy-room name (default: 'Living Room') for backwards compat.
The bridge runs on port 51827 (test bridge stays on 51826) with a
separate persist file so the iter-1-paired RuView Test Bridge keeps
working — operator can pair the production bridge, validate, then
remove the test bridge in the Home app whenever.
Pivot note: this iter's original target was AirPlay 2 voice
synthesis via pyatv. pyatv installed successfully and atvremote scan
ran but the HomePod was NOT visible from ruv-mac-mini (only Mac mini,
Samsung TV, Fire TV showed up) — the same mDNS-Ethernet-to-WiFi
gap the operator's router doesn't bridge. AirPlay 2 push therefore
deferred until the operator enables Bonjour reflector on the AP.
Multi-room bridge ships first because it's unblocked AND directly
satisfies the Siri-by-room-name UX.
Empirical (deployed on ruv-mac-mini, prod_bridge_pid=64094):
$ dns-sd -B _hap._tcp local.
Add 3 15 local. _hap._tcp. RuView Test Bridge 224DF9
Add 3 15 local. _hap._tcp. RuView Sensing 0B4FC4
Add 3 15 local. _hap._tcp. Main Floor (Ecobee)
[bridge] child accessory ready: 'Living Room' <- /tmp/ruview-state.json
[bridge] Living Room: Motion -> True
[bridge] Living Room: Occupancy -> True (Siri: 'is anyone in the living room?')
Setup code for pairing the new bridge: 629-88-678.
Tier 1 §2.1.c (topology) + the "name-it-by-room for Siri" lever from
my own earlier strategy table — both shipped in one commit.
Refs ADR-125 §2.1.c.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d
GET /api/v1/semantic-events/<node_id>/latest exposes the three
ADR-125 §2.1.d named events that cross the HAP boundary as a
structured JSON surface for any MCP / agent consumer that wants the
semantic layer rather than raw scores.
Response shape:
{
"node_id": "12",
"privacy_class": 2,
"events": {
"unknown_presence": {"active": bool, "source": str, "ts": float},
"unexpected_occupancy": {"active": bool, "schedule_aware": false, "ts": float},
"unrecognized_activity_pattern": {
"active": bool, "anomaly_threshold": 0.7,
"anomaly_score": float, "ts": float
}
},
"redacted_fields": [
"identity_risk_score", "soul_match_probability", "rf_signature_hash"
]
}
Live response from real C6 (node_id=12):
{
"unknown_presence": {"active": true, ...},
"unexpected_occupancy": {"active": true, "schedule_aware": false, ...},
"unrecognized_activity_pattern": {"active": false, "anomaly_score": 0.0, ...}
}
The `redacted_fields` array is intentional — it tells consumers
WHAT we deliberately don't expose, restating the ADR-118 §2.5 /
ADR-125 §2.1.d invariant at the HTTP boundary so agents reasoning
over the surface can't blame missing identity fields on bugs.
`unexpected_occupancy.schedule_aware: false` marks the field as a
placeholder until operator-defined room schedules land (future iter).
Agents that branch on this can fall back to raw occupancy until then.
Refs ADR-125 §2.1.d (semantic-events naming contract).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 5): rvagent MCP consumer — agentic chain proven
scripts/rvagent-mcp-consumer.py (~155 LOC) is an MCP JSON-RPC 2.0
stdio client that spawns the published @ruvnet/rvagent v0.1.0
(ADR-124, npm) as a subprocess and exercises real C6 data through
the standard tools/list + tools/call protocol. This is the "agentic
capabilities" milestone of the Tier 1+2 sprint.
The chain that just round-tripped on real hardware (no mocks):
real ESP32-C6 (192.168.1.179)
→ UDP rv_feature_state @ 5005
→ c6-presence-watcher.py (CRC32 + BFLD PrivacyGate, class=Anonymous)
→ /tmp/ruview-last-feature.json (atomic tmp+rename)
→ ruview-sensing-server.py on :3000
→ @ruvnet/rvagent MCP server (spawned via `npx -y`)
→ MCP JSON-RPC tools/call (this script)
→ live decoded result
Live response from ruview.bfld.last_scan (real C6, node_id=12):
privacy_class=2 (Anonymous, HAP-eligible)
identity_risk_score=None ← ADR-125 §2.1.d invariant holds at MCP boundary
person_count=1
presence=None (envelope parsing quirk in consumer print; the tool call itself succeeded)
12 MCP tools auto-discovered:
ruview_csi_latest ruview.bfld.last_scan
ruview_pose_infer ruview.bfld.subscribe
ruview_count_infer ruview.presence.now
ruview_registry_list ruview.vitals.get_breathing
ruview_train_count ruview.vitals.get_heart_rate
ruview_job_status ruview.vitals.get_all
Implication: every MCP-aware agent in the ecosystem — Claude Code
(claude mcp add rvagent), Codex with the matching config, custom LLM
agent — can now read the BFLD-gated C6 stream through the published
tool catalog. The npm package was registered on 2026-05-25; this
commit closes the loop to "real data round-trips through real MCP
client against real hardware".
Refs ADR-124 (@ruvnet/rvagent), ADR-125 §2.1.d (identity-risk gate).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding
scripts/c6-presence-watcher.py and friends carry a Python port of
`wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical
SOTA replacement — a PyO3 binding over the published Rust crate so
the runtime can pivot to the same enum semantics every other consumer
of `wifi-densepose-bfld 0.3.0` already uses.
New file: `python/src/bindings/privacy_gate.rs` (~155 LOC)
- `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}`
- `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters
- `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors
- free fns `allows_hap`, `allows_network`, `allows_matter`
- registered in `python/src/lib.rs` via `bindings::privacy_gate::register`
Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }`
as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.
ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility
mirrors Matter eligibility (Anonymous and Restricted only); a single
`PrivacyClass::from(*self).allows_matter()` call is the gate truth-source.
Verification: `cargo check -p wifi-densepose-py` on the workspace
compiles cleanly with the new binding linking against the published
crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking
wifi-densepose-py v2.0.0-alpha.1 ✓).
Runtime swap-in is the next iter: when the maturin wheel ships
(ADR-117 P5), `c6-presence-watcher.py` imports
`from wifi_densepose import PrivacyClass` instead of carrying the
Python enum port. Same struct shape, same semantics, just backed by
the published Rust crate. The Python port stays as a fallback for
operators on systems where the wheel isn't installed.
Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2)
ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under
`scripts/macos-shortcuts/`:
README.md one-time operator setup + architecture diagram
announce-via-homepod.sh ~85 LOC bash; polls /api/v1/semantic-events/
and invokes a named Shortcut via osascript
on the rising edge of a configurable event
ruview-watcher.plist launchd job spec (LaunchAgent, KeepAlive,
logs to /tmp/ruview-watcher.{stdout,stderr,log})
Why this matters strategically: the HomePod doesn't need to be visible
from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the
operator's Home graph; Shortcuts.app reaches the HomePod via that graph,
not via local mDNS. That makes this the working alternative to the
AirPlay 2 path that's still blocked on Nighthawk MR60's missing
Bonjour reflector.
Smoke test on real C6 (real hardware, no mocks):
$ ~/announce-via-homepod.sh --once --event unknown_presence
[17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce"
[17:10:12] unknown_presence rising-edge → running 'RuView Announce'
34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)
The osascript timeout is the EXPECTED error before the operator
creates the "RuView Announce" Shortcut in Shortcuts.app — the
trigger logic is verified working. Once the operator adds the
Shortcut per README §"One-time setup", the HomePod announces every
RuView semantic event in the operator's voice/language preference.
Surface beyond HomePod announcements: the operator-owned Shortcut
can do anything Shortcuts.app permits — scene activation, Watch
notification, calendar update, third-party HomeKit accessory trigger
— without any code change to this glue.
Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2)
Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID +
specification + run-time write hook to ruview-hap-bridge.py.
BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB"
display_name = "BFLD Privacy Class"
Format = uint8 (legal values: 2=Anonymous, 3=Restricted)
Permissions = pr, ev (paired-read + event-notify)
Eve.app + Controller for HomeKit render this as an integer 2..3
under the MotionSensor service; Home.app ignores unknown UUIDs but
automations can still trigger on it.
Implementation status: SCAFFOLD-ONLY. The runtime add of the
Characteristic via `Service.add_characteristic(...)` was attempted
and reverted because HAP-python's public API does not bind
`broker` + `iid_manager` for hand-constructed Characteristic objects —
the iPhone's first `/accessories` GET fails with
`'AccessoryDriver' object has no attribute 'iid_manager'` (the
broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the
driver, and Service.add_characteristic doesn't traverse the chain).
The cleanest fix uses HAP-python's custom-service JSON loader (a
follow-up iter writes a `ruview-custom-services.json` and calls
`add_preload_service("BfldStatus", chars=[...])`). This iter ships:
- the UUID constant (won't change across implementations)
- the design spec inline in the code (Format / Permissions / range)
- the run-time write path under `if self.c_privacy_class is not None`
(no-op until the next iter wires the loader)
The production bridge is verified back online with this iter:
Living Room: Motion -> True, Occupancy -> True
mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp
Closes the design half of the last open Tier 1+2 item. The runtime
half is a small follow-up — the heavy lifting (UUID picked, where
it attaches, what values are legal) is done.
Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-125): Apple HomePod user guide + README badge
- Add docs/user-guide-apple-homepod.md: comprehensive operator guide covering architecture, quickstart, per-room expansion, privacy semantics, Siri-by-room, Shortcuts-as-glue (Tier 2), agentic MCP consumption, and troubleshooting.
- Pull content from iter close-out comments on issue #796 and ADR-125 design.
- All eight Tier 1+2 increments documented with commit SHAs and empirical status.
- Update README.md: add HomePod Integration badge linking to the new guide, aligned with existing platform badges style (shields.io format, Apple logo, black background).
Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.
* feat(homecore/p1): ADR-127 state machine scaffold (20 tests pass)
New crate v2/crates/homecore/ — DashMap state machine, tokio
broadcast event bus, service registry (direct-dispatch P1),
in-memory entity registry, HA-compat wire constants.
20/20 unit tests pass. EntityId rejects unicode per ADR-127 Q1
(ASCII strict P1). State machine suppresses no-op writes,
preserves last_changed on attribute-only updates, fires
state_changed broadcast for every real write.
Critical path foundation — ADR-130 (API) and ADR-128 (plugins)
can begin P1 once this is in main.
Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(readme): link ecosystem badges + move Beta callout to bottom
Three operator-feedback corrections to the README:
1. Every ecosystem badge in the top row now links to a real
destination — Home Assistant -> integrations/home-assistant.md,
Matter -> ADR-122, Apple Home -> user-guide-apple-homepod.md,
Google Home + Alexa -> the HA integration doc (both ecosystems
reach RuView through HA's bridge today). Added an Alexa badge
alongside the existing four so all four major ecosystems are
represented. Dropped the now-redundant separate "HomePod
Integration" badge — the Apple Home badge linking to the same
guide is enough.
2. Beta callout moved from line 14 (under the hero image) to a
dedicated `## Beta software` section immediately before the
License. The callout's content is unchanged; it just no longer
gates the elevator pitch. Readers see the value proposition
first, the caveats at the bottom alongside license + support.
3. The intro paragraph ("Turn ordinary WiFi into ...") now ends
with a one-line summary of native ecosystem support naming all
four — Home Assistant, Apple Home & HomePod, Google Home, Alexa —
plus the Matter endpoint, each linked. The previous mention of
ecosystems was buried further down the page; this surfaces it
in the intro where the user reads first.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-plugins/p1): ADR-128 plugin runtime scaffold
Adds `v2/crates/homecore-plugins` (0.1.0-alpha.0) — the P1 scaffold for
the HOMECORE-PLUGINS WASM integration system (ADR-128):
- `manifest.rs`: `PluginManifest` — superset of HA manifest.json; serde
round-trip + required-field validation (`domain`/`name`/`version`).
- `error.rs`: `PluginError` typed enum (InvalidManifest, AlreadyLoaded,
NotFound, RuntimeError, SetupFailed, UnloadFailed, Io).
- `plugin.rs`: `HomeCorePlugin` async trait + `PluginId` newtype.
- `runtime.rs`: `PluginRuntime` trait + `InProcessRuntime` (native Rust,
first-party plugins). `WasmtimeRuntime` stub gated on `--features wasmtime`
(default-off; 30 MB dep deferred to P2).
- `registry.rs`: `PluginRegistry<R>` — load/unload/list/contains via RwLock.
- 10 unit tests, 0 failed.
Wasmtime vs wasm3 runtime selection is still open (ADR-128 §8 Q2);
this scaffold makes the choice swappable via the `PluginRuntime` trait.
The `wasmtime` and `wasm3` features are default-off; P2 resolves the choice
and wires host ABI (`hc_state_get`/`hc_state_set`/etc.) to ADR-127.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore/p1 iter-2): API (ADR-130) + plugins (ADR-128) scaffolds in parallel
Two new crates land in this iteration of the HOMECORE swarm:
## v2/crates/homecore-api/ (ADR-130 P1, sequential foundation)
Wire-compat Axum REST + WebSocket port of HA's API. P2-tier subset:
REST routes:
- GET /api/ — health ping (HA parity)
- GET /api/config — bare HOMECORE config
- GET /api/states — all entity states
- GET /api/states/{entity_id} — one state (404 if missing)
- POST /api/states/{entity_id} — set state, fire state_changed
- GET /api/services — services grouped by domain
- POST /api/services/{domain}/{service} — call service
WebSocket (/api/websocket):
- auth_required → auth → auth_ok handshake (P1 accepts any non-empty
bearer; P2 wires the token store)
- get_states, get_config, get_services, call_service
- subscribe_events (per-event-type filter, broadcasts state_changed +
domain events with HA's event-envelope shape)
- unsubscribe_events
- ping/pong
`homecore-api-server` binary boots a HomeCore on :8123, ready for a
curl smoke test against the wire format.
## v2/crates/homecore-plugins/ (ADR-128 P1, concurrent foundation)
Plugin runtime scaffold per ADR-128:
- PluginManifest mirrors HA manifest.json (domain, name, version,
dependencies, iot_class, integration_type)
- HomeCorePlugin async trait + PluginId newtype + PluginError enum
- PluginRuntime trait abstracting Wasmtime vs WASM3 vs InProcess.
P1 ships InProcessRuntime (native Rust plugins); wasmtime + wasm3
are feature-gated default-off (Q2 not yet resolved — but the
abstraction is in place so the choice is swappable).
- PluginRegistry: load/unload/list by PluginId.
## Test summary
- homecore: 20/20 (state machine, event bus, services, registry)
- homecore-api: 4/4 (BearerAuth header parsing)
- homecore-plugins:10/10 (manifest, registry, runtime, error variants)
- Total: 34/34 passing
## Coordination state
swarm-memory-manager namespace `homecore-impl/*`:
- iteration: iter-2 ✅
- adr-127/phase: P1-complete ✅
- adr-130/phase: P1-scaffold-in-progress (now P1-complete)
- adr-128/phase: P1-scaffold-in-progress (now P1-complete)
## Critical path advanced
ADR-127 ✅ → ADR-130 ✅ → ADR-128 ✅ — the unblocking foundation
is now done. Next iteration can fan out 129/131/132/133/134/125
concurrently. Tracking issue #798.
Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md
Refs: docs/adr/ADR-128-homecore-integration-plugin-system.md
Refs: #798
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-hap/p1): ADR-125 HAP bridge scaffold (17 tests pass)
Add `homecore-hap` crate: HapAccessoryType (11 variants), HapCharacteristic,
EntityToAccessoryMapper (light/switch/binary_sensor/sensor/cover/lock domains),
HapBridge add/remove/running API, NullAdvertiser mDNS stub, and
RuViewToHapMapper (presence→OccupancySensor, fall→LeakSensor, motion→MotionSensor).
P2 `hap-server` feature gates the real hap = "0.1" server + mdns-sd integration.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-recorder/p1): ADR-132 SQLite recorder + fnv64a attr dedup (14 tests pass)
- SQLite-backed state history with HA-compat schema (states, state_attributes,
events, recorder_runs) mirroring recorder schema v48
- FNV-1a 64-bit attribute deduplication matching HA's db_schema.py fnv64a
- RecorderListener subscribes to StateMachine broadcast and persists every
state change; subscription created at construction to avoid missed events
- SemanticIndex trait + NullSemanticIndex for P1; ruvector-backed impl stub
feature-gated behind --features ruvector for P2 hand-off
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-automation/p1): ADR-129 automation engine + MiniJinja templates (34 tests pass)
Scaffolds `v2/crates/homecore-automation` per ADR-129 HOMECORE-AUTO:
- Automation struct with RunMode (single/restart/queued/parallel/ignore_first)
- Trigger enum: State, NumericState, Time, Event + EvaluateTrigger trait
- Condition enum: State, NumericState, Template, And, Or, Not + async evaluate
- Action enum: ServiceCall, Delay, Scene, WaitForTrigger, Choose + async execute
- TemplateEnvironment: MiniJinja 2.x with HA globals states(), state_attr(), is_state(), now()
- AutomationEngine: subscribes to state-machine broadcast, evaluates triggers, runs action tasks
34 unit tests pass (0 failed). MiniJinja filter coverage: states, state_attr, is_state, now (P1 set).
Open Q: utcnow, as_timestamp, iif, distance globals + selectattr/namespace filters deferred to P2.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-migrate/p1): ADR-134 .storage parser + entity-registry import (19 tests pass)
- HaStorageEnvelope: outer {version, minor_version, key, data} shape for all .storage files
- storage_format/v13: versioned parser dispatch; UnsupportedSchemaVersion hard error on unknown minor_version
- entity_registry: core.entity_registry v13 → Vec<homecore::EntityEntry> with full field mapping
- device_registry: core.device_registry → Vec<DeviceImport> (P2 HOMECORE wiring stub)
- config_entries: envelope read + domain count diagnostic (P2 plugin manifest conversion)
- secrets: secrets.yaml → HashMap<String,String>
- automations: count + ID list extraction (P2 conversion)
- cli: clap-derived Inspect/ImportEntities/ImportDevices/InspectConfigEntries/InspectSecrets/InspectAutomations subcommands
- 19 unit tests, all pass; build clean; workspace member appended to v2/Cargo.toml
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-assist/p1): ADR-133 intent pipeline + ruflo runner stub (23 tests pass)
- Creates v2/crates/homecore-assist with intent, recognizer, handler,
runner, and pipeline modules per ADR-133 §2 design
- RegexIntentRecognizer: HA-style named-capture-group pattern matching
- Built-in handlers: HassTurnOn, HassTurnOff, HassLightSet, HassNevermind,
HassCancelAll — dispatch to homecore ServiceRegistry
- RufloRunner trait + NoopRunner P1 stub (Windows-safe subprocess teardown
deferred to P2 per ADR-133 §Q3)
- AssistPipeline + default_pipeline() wires recognizer → handler → response
- SemanticIntentRecognizer P2 stub (ruvector HNSW deferred)
- 23 unit tests, 0 failures; cargo build -p homecore-assist clean
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-131/recon): cognitum-one/v0-appliance design recon for HOMECORE-FRONTEND
Captures the full design system from the live cognitum-v0:9000 dashboard
(all 10 nav pages fetched, HTTP 200, unauthenticated). Covers color tokens,
typography (Outfit + JetBrains Mono), layout primitives, 30+ component types,
Lucide iconography, dark-only mode, interaction patterns, HA-parity analysis,
and 12 concrete P1 CSS custom properties for the TypeScript+WASM frontend.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-frontend/p1): @ruvnet/homecore-frontend Lit+TS+Vite scaffold (3 tests)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-recorder/p2): wire RuvectorSemanticIndex with hash-based embeddings (resolves ADR-132 P2)
- ruvector-core = "2.2.0" + sha2 = "0.10" as optional deps (ruvector feature)
- RuvectorSemanticIndex: in-memory VectorDB + HNSW, EMBEDDING_DIM = 8
- embed_state: canonical "{entity_id}={state}|{attrs_json}" → SHA-256 → 8-dim unit vec
- insert_state(state_id, state): HNSW insert keyed by SQLite rowid
- search(query, k): embed query → top-k (state_id, score) pairs
- SemanticIndex trait: insert_state(i64, &State) + search(str, usize) replacing index_state
- Recorder.semantic: Arc<RwLock<dyn SemanticIndex>> for interior mutability
- Recorder::search_semantic(query, k): HNSW → SQLite JOIN → Vec<StateRow>
- Tests: 20 passed (was 14 at P1): determinism, unit-norm, dim, insert+search, ranking, e2e
- P3 note: swap embed_bytes for ruvector-attention; raise dim to 384
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-plugins/p2): Wasmtime runtime + example WASM plugin (resolves ADR-128 Q2)
- Implements WasmtimeRuntime in v2/crates/homecore-plugins/src/wasmtime_runtime.rs
with a Wasmtime 25 Cranelift JIT engine. Registers 4 host imports via Linker:
hc_state_get, hc_state_set, hc_state_subscribe, hc_log. Each plugin gets an
isolated Store<PluginStoreData> holding a HomeCore handle + subscription list.
- Adds host_abi.rs documenting the JSON-over-linear-memory wire format (public
ABI spec for plugin authors). Max buffer 64 KiB. ConfigEntryJson and
StateChangedEventJson are the canonical wire types.
- Creates v2/crates/homecore-plugin-example/ (wasm32-unknown-unknown, excluded
from workspace per wifi-densepose-wasm-edge pattern). The plugin monitors
sensor.test_temp and sets binary_sensor.test_alert on/off at 25/20 thresholds.
- Adds tests/integration.rs with 3 tests: compiled .wasm end-to-end round-trip,
WAT-based fallback (always runs), and linker smoke test. All 15 tests pass
(12 unit + 3 integration) under --features wasmtime.
- ADR-128 Q2 resolved: Wasmtime is the chosen runtime for P2. WASM3 stays as
future fallback under --features wasm3 for constrained hardware (ADR-128 §8).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(homecore-server/iter-9): integration binary tying all 8 HOMECORE crates together
New crate `v2/crates/homecore-server/` boots one process that wires
every HOMECORE surface into a single HA-compatible runtime:
1. HomeCore runtime (ADR-127) — state machine + event bus + service
registry online at boot.
2. Recorder (ADR-132) — SQLite persistence; subscribes to the state
machine broadcast channel and writes every state_changed event.
Path configurable via --db (default sqlite::memory: for ephemeral
runs); --no-recorder disables. ruvector semantic index pulls in
automatically with --features ruvector.
3. Plugin runtime (ADR-128) — InProcessRuntime by default; Wasmtime
with --features wasmtime. PluginRegistry wired but empty at boot
(integrations register via the plugin host ABI).
4. Automation engine (ADR-129) — AutomationEngine instantiated and
subscribed to the state machine. No automations loaded at boot
yet; that's a YAML-loading P3 task.
5. Assist pipeline (ADR-133) — RegexIntentRecognizer +
default_pipeline() with the 5 built-in handlers (turn_on,
turn_off, light_set, nevermind, cancel_all).
6. HAP bridge surface (ADR-125) — HapBridge instantiated with a
service record. Accessory registration via the API.
7. REST + WebSocket API (ADR-130) — Axum router on :8123, HA-compat.
/api/, /api/config, /api/states[/{eid}], /api/services[/...],
/api/websocket.
Configuration via CLI flags + env vars:
- --bind / HOMECORE_BIND (default 0.0.0.0:8123)
- --db / HOMECORE_DB (default sqlite::memory:)
- --location-name / HOMECORE_LOCATION (default "Home")
- --no-recorder
Builds clean (`cargo build -p homecore-server`). Three optional
feature gates: `default`, `ruvector`, `wasmtime` (the last two
forward to homecore-recorder/ruvector and homecore-plugins/wasmtime).
Refs: docs/adr/ADR-126-ruview-native-ha-port-master.md §5 phase roadmap
Refs: #798
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(security/iter-10): HOMECORE security audit — 18 findings, 4 critical
18 total findings across the 8 new homecore crates + integration binary:
- Critical (4): HC-01/02 any-token auth bypass on REST+WS, HC-03/04
Wasmtime 25.0.3 sandbox-escape CVEs (RUSTSEC-2026-0095/0096, CVSS 9.0)
- High (3): permissive CORS, sqlx 0.7.4 protocol bug, unbounded WS subscriptions
- Medium (5): hardcoded HAP setup code, hc_log bypasses tracing, no body
size limit, rsa Marvin Attack, shlex quote injection
- Low/Info (6): no TLS, migrate symlink gap, eprintln in automation engine,
subscription dedup, two informational
cargo audit: 18 advisories (2 critical wasmtime sandbox escapes, fix = upgrade
wasmtime to >=36.0.7; upgrade sqlx to >=0.8.1)
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-recorder/sec): bump sqlx 0.7.4 → 0.8.1+ (RUSTSEC, audit HC-medium)
Per iter-10 security audit (docs/security/HOMECORE-security-audit-iter10.md):
sqlx 0.7.4 ships an advisory for binary protocol misinterpretation.
Bump to 0.8.1+ — cargo resolved to 0.8.6.
Feature set unchanged (default-features = false +
runtime-tokio-native-tls, sqlite, chrono, uuid). Tests still pass:
cargo test -p homecore-recorder --features ruvector
→ 20 passed; 0 failed
No code changes required. The 0.7 → 0.8 API surface we touch in
`db.rs` is stable across the bump.
Deferred to a later iter:
- shlex 0.1.1 → ≥1.3.0 (transitive via wasm3-sys, only on
--features wasm3 which is default-off; will be addressed when
the wasm3 path is removed per ADR-128 Q2 Wasmtime resolution)
- wasmtime 25 → 36+/42+ (HC-03/04 CVSS 9.0 sandbox-escape) — being
handled by a background coder agent this iter, separate commit.
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-09 sqlx)
Refs: #798
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-plugins/sec): bump wasmtime 25 → 42 for RUSTSEC-2026-0095/0096 (HC-03/04, CVSS 9.0)
Remediates iter-11 security audit findings HC-03 (RUSTSEC-2026-0095) and
HC-04 (RUSTSEC-2026-0096) — Cranelift/Winch sandbox-escape CVEs (CVSS 9.0).
Version specifier updated from "25" → "42"; lockfile already pinned at
42.0.2. Zero code-surface changes required: Engine/Linker/Store/Instance
and Memory.data/data_mut APIs are ABI-compatible across this range.
All 15 tests pass (12 unit + 3 integration including the two required
wasm_plugin_temp_threshold tests). cargo audit no longer reports
RUSTSEC-2026-0095 or RUSTSEC-2026-0096 against this workspace.
Co-Authored-By: claude-flow <ruv@ruv.net>
* perf(homecore): criterion benches for state-machine hot paths
`cargo bench -p homecore --bench state_machine` covers:
- set/first_write — cold-path insert + alloc + broadcast
- set/warm_write_state_change — same-entity update fires broadcast
- set/noop_suppressed — same state+attrs, no broadcast (HA semantic)
- get/hit + get/miss — zero-copy Arc<State> read paths
- all_snapshot/{10,100,1000} — Vec<Arc<State>> snapshot for REST
- all_by_domain_light_20_of_100 — domain prefix filter
- broadcast_fan_out/{1,4,16,64} — 1 sender + N subscribers, async,
measures end-to-end deliver-and-recv latency
The broadcast fan-out is the most load-bearing measurement for
HOMECORE — every integration, the recorder, the automation engine,
and every WS subscriber holds a receiver, so the per-subscriber
delivery cost determines how many add-ons the runtime can host.
criterion 0.5 with sample_size=20 (fast tick, the fast-path benches
run in nanoseconds and don't need 100 samples).
Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-api/sec): close HC-01/HC-02 — real bearer-token store
Replaces the P1 "any non-empty bearer" placeholder with a real
LongLivedTokenStore (HashSet<String>) on SharedState. Closes the
two Critical findings from the iter-10 security audit
(docs/security/HOMECORE-security-audit-iter10.md HC-01 + HC-02).
New module `homecore-api::tokens`:
- LongLivedTokenStore::empty() — default-deny
- LongLivedTokenStore::from_env() — reads HOMECORE_TOKENS=t1,t2,t3
- LongLivedTokenStore::allow_any_non_empty() — DEV-only, warns
on every check, preserves legacy behaviour for migrating users
- register / revoke / is_valid / len / is_dev_mode — full API
Wired through:
- SharedState gains `tokens: LongLivedTokenStore`; constructors
with_tokens(...) for explicit injection; with_metadata defaults
to DEV (allow_any) for backwards compat with existing smoke tests
- BearerAuth::from_headers now async + takes &LongLivedTokenStore;
checks store.is_valid(token) before returning Ok
- All 6 REST handlers updated to thread the store and await the
validation
- homecore-server reads HOMECORE_TOKENS at boot; if set, builds
the store from env; if unset, falls back to DEV with a warn log
Test count: 4 → 15 (+11 token-store + auth-with-store tests).
Smoke verified end-to-end:
HOMECORE_TOKENS=good homecore-server --bind 127.0.0.1:8126
→ "LongLivedTokenStore provisioned with 1 bearer token(s)"
curl -H "Authorization: Bearer good" .../api/states → 200
curl -H "Authorization: Bearer wrong" .../api/states → 401
curl -H "Authorization: Bearer " .../api/states → 401
curl .../api/states → 401
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-01 + HC-02)
Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md §3 auth
Refs: #798
Refs: #800
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(homecore-api/sec): close HC-05 — CORS allowlist instead of permissive
Replaces `CorsLayer::permissive()` (which set Access-Control-Allow-
Origin: *) with an explicit allowlist via `CorsLayer::new()`.
Default allowlist covers the homecore-frontend Vite dev server
(5173) plus common reverse-proxy ports (3000, 8080, 8081) and the
bind port itself (8123). Production deployments override via
HOMECORE_CORS_ORIGINS=https://app.example.com,https://hass.example.com
(comma-separated).
Method allowlist: GET, POST, OPTIONS, DELETE (no PUT/PATCH yet).
Header allowlist: Authorization, Content-Type, Accept.
Credentials: disabled (no cookies in HOMECORE-API path).
Test count: 15 → 18 (+3 CORS allowlist tests).
Closes audit finding HC-05 (High). The HC-01/02 bearer-store fix
in commit 408cfd4f0 only mattered if the cross-origin path was
also locked down — without HC-05 a malicious page could still
make authenticated calls with a stored bearer.
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-05)
Refs: #800
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary
Inserts a Python equivalent of `wifi-densepose-bfld::PrivacyClass` +
`PrivacyGate` between the rv_feature_state parser and the HAP toggle
file. ADR-125 §2.1.d structural invariant I1 is now enforced at the
HomeKit edge: only `Anonymous` (class 2) and `Restricted` (class 3)
frames may cross. `Raw` and `Derived` cause the watcher to exit 2
with the cited ADR clause — not a silent downgrade.
Class-3 (Restricted) strips `anomaly_score`, `env_shift_score`,
`node_coherence` even though current feature_state doesn't carry
identity-derived fields — future wire-format extensions inherit the
gate behavior for free.
Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher
logs `Unknown Presence` (not "intruder detected" / "security state").
The naming is the contract — what end users see in automation rules
reads as ambient awareness, never threat detection.
Empirical (with --privacy-class anonymous on live C6):
pkts=58 valid=51 crc_bad=0 motion=True
privacy class: Anonymous (HAP-eligible)
semantic event: Unknown Presence
Refuse path validated:
$ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived
REFUSED: privacy class Derived (value=1) is not HAP-eligible.
ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and
Restricted (3) frames may cross the HomeKit boundary.
$ echo $?
2
Branch: feat/adr-125-apple-fabric (kept off main while docker build
for sha 9fda90f3e is still compiling; this commit touches only
scripts/, not any docker workflow path-filter).
Refs ADR-125 §2.1.d, ADR-118 §2.1/§2.2.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-125 iter 4): CHANGELOG bullet for the APPLE-FABRIC e2e
Pre-merge checklist item 5. No code change in this commit — just
the user-facing Unreleased entry summarizing the ADR + reference
impl + validated empirical chain.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1 #1): multi-characteristic accessory + JSON-state IPC
The HAP accessory now carries three services on the same paired
entity (HomeKit allows multiple services per accessory; iPhone
refetches /accessories when config_number bumps):
- MotionSensor — short-window motion_score, immediate
- OccupancySensor — rolling-3s avg presence_score, sustained
- StatelessProgrammableSwitch — "Unrecognized Activity Pattern"
event (Restricted-class only; fires on
anomaly_score >= 0.7); ADR-125 §2.1.d
semantic naming, not security state
New JSON IPC contract `/tmp/ruview-state.json` between watcher
and HAP daemon:
{ "motion": bool, "occupancy": bool, "anomaly_ts": float,
"ts": float }
Atomic writes (tmp + rename). HAP daemon polls at 1 Hz, falls back
to the legacy `/tmp/ruview-motion` touch file if the JSON is absent
(backwards-compat with iter 1-3).
Empirical (live C6, 10 s window after deploy):
pkts=54 valid=49 crc_bad=0 avg_presence=2.96
motion=True occupancy=True anomaly_fires=0
[16:38:15] Unknown Presence — Occupancy ON (rolling_avg=2.79)
Pairing survived:
paired_clients: 1
config_number: 3 (was 1; HAP-python bumps automatically on shape change)
Tier 1 #1 (multi-characteristic) of the Tier 1+2 sprint. Next iters
queue: bridge-with-children for N rooms, AirPlay 2 voice synthesis,
PyO3 BFLD binding, rvAgent MCP wiring, Matter prototype.
Refs ADR-125 §2.1.c (bridge topology), §2.1.d (semantic events),
ADR-118.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 2): sensing-server-equivalent for @ruvnet/rvagent
scripts/ruview-sensing-server.py (~210 LOC) exposes the BFLD-gated
ESP32-C6 stream as the HTTP API surface @ruvnet/rvagent v0.1.0
(ADR-124, npm) expects. Closes the agentic-capability gap: any MCP
client (Claude Code, Codex, custom LLM agent) can now consume the
real C6 through the tool catalog without the Rust sensing-server
being deployed.
Endpoints (mirrors tools/ruview-mcp/src/tools/*.ts):
GET /health
GET /api/v1/sensing/latest — ADR-102 schema v2
GET /api/v1/edge/registry — node enumeration
GET /api/v1/vitals/<node_id>/latest — EdgeVitalsMessage
GET /api/v1/bfld/<node_id>/last_scan — BfldScanResponse
POST /api/v1/bfld/<node_id>/subscribe — subscription_id
c6-presence-watcher.py now writes a companion `/tmp/ruview-last-
feature.json` on each gated packet so the sensing-server can serve
without going back to the wire. Atomic tmp+rename. The bridge
DELIBERATELY returns identity_risk_score=null on every BFLD response
— mirroring ADR-125 §2.1.d at the HTTP boundary even though the
rvagent schema's slot is nullable.
Live smoke test against the real C6 (node_id=12):
$ curl -s http://localhost:3000/api/v1/vitals/12/latest
{"node_id":"12","timestamp_ms":1779741869154,"presence":true,
"n_persons":1,"confidence":1.0,"breathing_rate_bpm":18.75,
"heartrate_bpm":40.0,"motion":1.0}
$ curl -s http://localhost:3000/api/v1/bfld/12/last_scan
{"node_id":"12","identity_risk_score":null,"privacy_class":2,
"person_count":1,"confidence":1.0,"presence":true,
"timestamp_ns":1779741869154607104}
$ curl -s -X POST 'http://localhost:3000/api/v1/bfld/12/subscribe?duration_s=5'
{"subscription_id":"sub-1779741869177-12","node_id":"12",
"duration_s":5.0,"endpoint_hint":"poll GET ..."}
Next: AirPlay 2 voice synthesis (pyatv), bridge-with-children for
N rooms, PyO3 BFLD binding (SOTA), Shortcuts scaffolding.
Refs ADR-124 (@ruvnet/rvagent contract), ADR-125 §2.1.d, ADR-118.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 3): production HAP bridge with N child accessories
scripts/ruview-hap-bridge.py (~170 LOC) implements the ADR-125 §2.1.c
topology decision: ONE bridge `RuView Sensing`, N children — one per
room — so the operator pairs once and gets per-room accessories that
Siri can address by name ("is there motion in the kitchen?").
State per room comes from /tmp/ruview-state.<room>.json. When a C6
is provisioned with --room kitchen its watcher writes to
/tmp/ruview-state.kitchen.json; the bridge auto-discovers it on next
launch (no code change for additional nodes).
Legacy /tmp/ruview-state.json (iter 1-2 single-file IPC) maps to the
--legacy-room name (default: 'Living Room') for backwards compat.
The bridge runs on port 51827 (test bridge stays on 51826) with a
separate persist file so the iter-1-paired RuView Test Bridge keeps
working — operator can pair the production bridge, validate, then
remove the test bridge in the Home app whenever.
Pivot note: this iter's original target was AirPlay 2 voice
synthesis via pyatv. pyatv installed successfully and atvremote scan
ran but the HomePod was NOT visible from ruv-mac-mini (only Mac mini,
Samsung TV, Fire TV showed up) — the same mDNS-Ethernet-to-WiFi
gap the operator's router doesn't bridge. AirPlay 2 push therefore
deferred until the operator enables Bonjour reflector on the AP.
Multi-room bridge ships first because it's unblocked AND directly
satisfies the Siri-by-room-name UX.
Empirical (deployed on ruv-mac-mini, prod_bridge_pid=64094):
$ dns-sd -B _hap._tcp local.
Add 3 15 local. _hap._tcp. RuView Test Bridge 224DF9
Add 3 15 local. _hap._tcp. RuView Sensing 0B4FC4
Add 3 15 local. _hap._tcp. Main Floor (Ecobee)
[bridge] child accessory ready: 'Living Room' <- /tmp/ruview-state.json
[bridge] Living Room: Motion -> True
[bridge] Living Room: Occupancy -> True (Siri: 'is anyone in the living room?')
Setup code for pairing the new bridge: 629-88-678.
Tier 1 §2.1.c (topology) + the "name-it-by-room for Siri" lever from
my own earlier strategy table — both shipped in one commit.
Refs ADR-125 §2.1.c.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d
GET /api/v1/semantic-events/<node_id>/latest exposes the three
ADR-125 §2.1.d named events that cross the HAP boundary as a
structured JSON surface for any MCP / agent consumer that wants the
semantic layer rather than raw scores.
Response shape:
{
"node_id": "12",
"privacy_class": 2,
"events": {
"unknown_presence": {"active": bool, "source": str, "ts": float},
"unexpected_occupancy": {"active": bool, "schedule_aware": false, "ts": float},
"unrecognized_activity_pattern": {
"active": bool, "anomaly_threshold": 0.7,
"anomaly_score": float, "ts": float
}
},
"redacted_fields": [
"identity_risk_score", "soul_match_probability", "rf_signature_hash"
]
}
Live response from real C6 (node_id=12):
{
"unknown_presence": {"active": true, ...},
"unexpected_occupancy": {"active": true, "schedule_aware": false, ...},
"unrecognized_activity_pattern": {"active": false, "anomaly_score": 0.0, ...}
}
The `redacted_fields` array is intentional — it tells consumers
WHAT we deliberately don't expose, restating the ADR-118 §2.5 /
ADR-125 §2.1.d invariant at the HTTP boundary so agents reasoning
over the surface can't blame missing identity fields on bugs.
`unexpected_occupancy.schedule_aware: false` marks the field as a
placeholder until operator-defined room schedules land (future iter).
Agents that branch on this can fall back to raw occupancy until then.
Refs ADR-125 §2.1.d (semantic-events naming contract).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 5): rvagent MCP consumer — agentic chain proven
scripts/rvagent-mcp-consumer.py (~155 LOC) is an MCP JSON-RPC 2.0
stdio client that spawns the published @ruvnet/rvagent v0.1.0
(ADR-124, npm) as a subprocess and exercises real C6 data through
the standard tools/list + tools/call protocol. This is the "agentic
capabilities" milestone of the Tier 1+2 sprint.
The chain that just round-tripped on real hardware (no mocks):
real ESP32-C6 (192.168.1.179)
→ UDP rv_feature_state @ 5005
→ c6-presence-watcher.py (CRC32 + BFLD PrivacyGate, class=Anonymous)
→ /tmp/ruview-last-feature.json (atomic tmp+rename)
→ ruview-sensing-server.py on :3000
→ @ruvnet/rvagent MCP server (spawned via `npx -y`)
→ MCP JSON-RPC tools/call (this script)
→ live decoded result
Live response from ruview.bfld.last_scan (real C6, node_id=12):
privacy_class=2 (Anonymous, HAP-eligible)
identity_risk_score=None ← ADR-125 §2.1.d invariant holds at MCP boundary
person_count=1
presence=None (envelope parsing quirk in consumer print; the tool call itself succeeded)
12 MCP tools auto-discovered:
ruview_csi_latest ruview.bfld.last_scan
ruview_pose_infer ruview.bfld.subscribe
ruview_count_infer ruview.presence.now
ruview_registry_list ruview.vitals.get_breathing
ruview_train_count ruview.vitals.get_heart_rate
ruview_job_status ruview.vitals.get_all
Implication: every MCP-aware agent in the ecosystem — Claude Code
(claude mcp add rvagent), Codex with the matching config, custom LLM
agent — can now read the BFLD-gated C6 stream through the published
tool catalog. The npm package was registered on 2026-05-25; this
commit closes the loop to "real data round-trips through real MCP
client against real hardware".
Refs ADR-124 (@ruvnet/rvagent), ADR-125 §2.1.d (identity-risk gate).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding
scripts/c6-presence-watcher.py and friends carry a Python port of
`wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical
SOTA replacement — a PyO3 binding over the published Rust crate so
the runtime can pivot to the same enum semantics every other consumer
of `wifi-densepose-bfld 0.3.0` already uses.
New file: `python/src/bindings/privacy_gate.rs` (~155 LOC)
- `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}`
- `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters
- `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors
- free fns `allows_hap`, `allows_network`, `allows_matter`
- registered in `python/src/lib.rs` via `bindings::privacy_gate::register`
Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }`
as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.
ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility
mirrors Matter eligibility (Anonymous and Restricted only); a single
`PrivacyClass::from(*self).allows_matter()` call is the gate truth-source.
Verification: `cargo check -p wifi-densepose-py` on the workspace
compiles cleanly with the new binding linking against the published
crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking
wifi-densepose-py v2.0.0-alpha.1 ✓).
Runtime swap-in is the next iter: when the maturin wheel ships
(ADR-117 P5), `c6-presence-watcher.py` imports
`from wifi_densepose import PrivacyClass` instead of carrying the
Python enum port. Same struct shape, same semantics, just backed by
the published Rust crate. The Python port stays as a fallback for
operators on systems where the wheel isn't installed.
Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2)
ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under
`scripts/macos-shortcuts/`:
README.md one-time operator setup + architecture diagram
announce-via-homepod.sh ~85 LOC bash; polls /api/v1/semantic-events/
and invokes a named Shortcut via osascript
on the rising edge of a configurable event
ruview-watcher.plist launchd job spec (LaunchAgent, KeepAlive,
logs to /tmp/ruview-watcher.{stdout,stderr,log})
Why this matters strategically: the HomePod doesn't need to be visible
from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the
operator's Home graph; Shortcuts.app reaches the HomePod via that graph,
not via local mDNS. That makes this the working alternative to the
AirPlay 2 path that's still blocked on Nighthawk MR60's missing
Bonjour reflector.
Smoke test on real C6 (real hardware, no mocks):
$ ~/announce-via-homepod.sh --once --event unknown_presence
[17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce"
[17:10:12] unknown_presence rising-edge → running 'RuView Announce'
34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)
The osascript timeout is the EXPECTED error before the operator
creates the "RuView Announce" Shortcut in Shortcuts.app — the
trigger logic is verified working. Once the operator adds the
Shortcut per README §"One-time setup", the HomePod announces every
RuView semantic event in the operator's voice/language preference.
Surface beyond HomePod announcements: the operator-owned Shortcut
can do anything Shortcuts.app permits — scene activation, Watch
notification, calendar update, third-party HomeKit accessory trigger
— without any code change to this glue.
Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2)
Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID +
specification + run-time write hook to ruview-hap-bridge.py.
BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB"
display_name = "BFLD Privacy Class"
Format = uint8 (legal values: 2=Anonymous, 3=Restricted)
Permissions = pr, ev (paired-read + event-notify)
Eve.app + Controller for HomeKit render this as an integer 2..3
under the MotionSensor service; Home.app ignores unknown UUIDs but
automations can still trigger on it.
Implementation status: SCAFFOLD-ONLY. The runtime add of the
Characteristic via `Service.add_characteristic(...)` was attempted
and reverted because HAP-python's public API does not bind
`broker` + `iid_manager` for hand-constructed Characteristic objects —
the iPhone's first `/accessories` GET fails with
`'AccessoryDriver' object has no attribute 'iid_manager'` (the
broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the
driver, and Service.add_characteristic doesn't traverse the chain).
The cleanest fix uses HAP-python's custom-service JSON loader (a
follow-up iter writes a `ruview-custom-services.json` and calls
`add_preload_service("BfldStatus", chars=[...])`). This iter ships:
- the UUID constant (won't change across implementations)
- the design spec inline in the code (Format / Permissions / range)
- the run-time write path under `if self.c_privacy_class is not None`
(no-op until the next iter wires the loader)
The production bridge is verified back online with this iter:
Living Room: Motion -> True, Occupancy -> True
mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp
Closes the design half of the last open Tier 1+2 item. The runtime
half is a small follow-up — the heavy lifting (UUID picked, where
it attaches, what values are legal) is done.
Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d.
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-125): Apple HomePod user guide + README badge
- Add docs/user-guide-apple-homepod.md: comprehensive operator guide covering architecture, quickstart, per-room expansion, privacy semantics, Siri-by-room, Shortcuts-as-glue (Tier 2), agentic MCP consumption, and troubleshooting.
- Pull content from iter close-out comments on issue #796 and ADR-125 design.
- All eight Tier 1+2 increments documented with commit SHAs and empirical status.
- Update README.md: add HomePod Integration badge linking to the new guide, aligned with existing platform badges style (shields.io format, Apple logo, black background).
Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.
Iter 23. Production Publish trait impl using rumqttc 0.24 (same crate
version + use-rustls feature pinning as wifi-densepose-sensing-server,
so both publishers can share broker connection posture).
Added:
- rumqttc = "0.24" optional dep (default-features = false, use-rustls)
- New `mqtt` cargo feature: ["std", "dep:rumqttc"]
- src/rumqttc_publisher.rs (gated on `feature = "mqtt"`):
* RumqttPublisher wrapping rumqttc::Client + QoS + retain flag
* RumqttPublisher::new(client, qos) const constructor
* with_retain(bool) builder for availability-style topics
* RumqttPublisher::connect(opts, capacity) -> (Self, Connection)
Returns the unpumped Connection — caller spawns a thread that
iterates connection.iter() to drive the MQTT protocol. Default
QoS is AtLeastOnce (HA-DISCO recommendation for state topics).
* impl Publish with Error = rumqttc::ClientError
- pub use RumqttPublisher from lib.rs
tests/rumqttc_publisher_smoke.rs (7 named tests, all green, gated on mqtt):
rumqttc_publisher_constructs_without_broker
(uses 127.0.0.1:1 — reserved port refuses immediately; no hang)
with_retain_builder_yields_a_publisher
publish_queues_message_without_blocking_on_broker_state
*** Critical property: rumqttc's sync Client::publish queues into
an unbounded channel; publish_event returns Ok without round-
tripping to the (offline) broker. The queued packet only sends
if a thread iterates Connection::iter(). ***
restricted_event_publishes_four_messages_through_rumqttc
(class 3 + no zone: presence/motion/count/confidence — 4 topics)
publisher_trait_object_is_constructible
(Box<dyn Publish<Error = rumqttc::ClientError>> works)
direct_publish_call_through_trait_object
default_qos_is_at_least_once_via_connect
ACs progressed:
- ADR-122 §2.2 broker integration — production publisher now wired,
matching the sensing-server's TLS / version posture. The two
crates can share a single broker connection if an operator wants
both publishers in the same process.
- ADR-122 AC4 still enforced — publish_event's class-gated routing
is upstream of rumqttc, so no broker-level config can leak Raw frames.
Test config:
- cargo test --no-default-features → 72 passed (mqtt feature off)
- cargo test → 169 passed (mqtt feature off)
- cargo test --features mqtt --test rumqttc_publisher_smoke → 7 passed
- With --features mqtt: 169 + 7 = 176 total
Out of scope (next iter target):
- mosquitto integration test (env-gated MQTT_BROKER=tcp://localhost:1883):
* spawn a thread iterating Connection::iter()
* publish a BfldEvent
* subscribe in the test, await SubAck per the workspace memory note
`feedback_mqtt_integration_test_patterns`
* assert the topics received match render_events output
- BfldPipelineHandle: Arc<Mutex<BfldPipeline>> with a thread that pumps
inbound (inputs, embedding) → process → publish_event(&rumqttc_pub, &event)
for a single-call "set up MQTT publisher and walk away" API.
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 15. Lands ADR-120 §2.3 — the cryptographic foundation of invariant
I3 ("cross-site identity correlation is impossible"). rf_signature_hash
is now derived from a per-site secret and a daily epoch, so two nodes
observing the same physical person produce uncorrelated 256-bit digests.
Added (no_std-compatible):
- blake3 = "1.5", default-features = false (no_std, no SIMD by default)
- src/signature_hasher.rs:
* Constants SECONDS_PER_DAY (86_400), SITE_SALT_LEN (32), RF_SIGNATURE_LEN (32)
* SignatureHasher { site_salt: [u8; 32] } with new(salt) const ctor
* compute(day_epoch, &features) -> [u8; 32] (BLAKE3 keyed mode)
* compute_at(unix_secs, &features) -> [u8; 32] convenience
* day_epoch_from_unix_secs(unix_secs) -> u32 helper (floor(t / 86400))
- pub use SignatureHasher, RF_SIGNATURE_LEN, SITE_SALT_LEN from lib.rs
tests/signature_hasher.rs (8 named tests, all green):
deterministic_under_identical_inputs
different_site_salts_produce_different_hashes
different_day_epochs_rotate_the_hash
different_features_produce_different_hashes
output_length_is_32_bytes
day_epoch_from_unix_secs_matches_floor_division
(covers 0, 86_399, 86_400, and the 1.7e9 modern timestamp)
compute_at_matches_compute_with_derived_day
cross_site_hamming_distance_is_statistically_high
*** ADR-120 §2.7 AC2 acceptance test ***
Runs 100 trials with distinct (salt_a, salt_b) pairs observing
identical features, computes per-trial Hamming distance, asserts
mean >= 120 bits and min >= 80 bits. Empirically lands at ~128 bits
mean (the expected value for two independent 256-bit hashes), with
no trial below 80 bits — i.e., zero suspicious near-collisions.
ACs progressed:
- ADR-120 §2.7 AC2 — structurally enforced cross-site isolation, now
proven empirically by the Hamming-distance test. This is the
cryptographic half of invariant I3 in code, not just docs.
- ADR-118 invariant I3 — first runtime witness that two sites with
independent site_salts cannot correlate the same person's signature.
Test config:
- cargo test --no-default-features → 72 passed (64 + 8; signature_hasher is no_std)
- cargo test → 117 passed (109 + 8)
Out of scope (next iter target):
- Wire SignatureHasher into BfldEmitter: replace caller-supplied
rf_signature_hash with hasher.compute_at(ts, &features) so the
pipeline produces correct hashes end-to-end.
- IdentityFeatures canonical-bytes encoder so callers don't need to
hand-serialize per-feature representations.
Co-Authored-By: claude-flow <ruv@ruv.net>
Iter 13. Lands ADR-121 §2.1 (output event) + ADR-122 §2.1 (field-gating
policy). BfldEvent collapses the GateAction-driven sensing pipeline
into the canonical wire-format publishable on MQTT.
Added:
- serde (workspace, derive feature, optional) + serde_json (workspace, optional) deps
- New crate feature `serde-json` (default-on; requires `std`)
- src/event.rs (gated on `feature = "std"`):
* BfldEvent struct with all sensing + identity-derived fields
* with_privacy_gating(...) constructor that applies field-gating policy:
class < Restricted (3): identity_risk_score + rf_signature_hash kept
class >= Restricted (3): both nulled to None
* apply_privacy_gating() — idempotent in-place masking
* to_json() -> Result<String, serde_json::Error> (gated on serde-json)
* Custom ser_privacy_class serializer emits lowercase names
("anonymous", "restricted", etc.) per the BFLD JSON spec
* skip_serializing_if = "Option::is_none" on identity-derived fields so
privacy-gated events are observationally indistinguishable from
events that never had the field set
- pub use BfldEvent from lib.rs
tests/event_privacy_gating.rs (9 named tests, all green):
anonymous_event_retains_identity_risk_and_hash
restricted_event_strips_identity_fields (class 3 → None)
apply_privacy_gating_is_idempotent
event_type_is_always_bfld_update (parameterized over 3 classes)
json::json_round_trip_emits_type_field_first_or_last_but_present
json::anonymous_json_includes_identity_fields
json::restricted_json_omits_identity_fields_entirely
(asserts the JSON string does NOT contain identity_risk_score or
rf_signature_hash, verifying skip_serializing_if works as intended)
json::privacy_class_serializes_to_lowercase_name
json::zone_id_none_is_omitted_from_json
ACs progressed:
- ADR-121 AC6 (identity_risk score absent at class 3) — structurally
enforced by with_privacy_gating + skip_serializing_if combination.
- ADR-122 AC1 — JSON shape matches the HA-DISCO publishable event
contract; identity fields can be reliably stripped by privacy_class.
- ADR-118 AC5 — privacy_mode = engaged maps to PrivacyClass::Restricted
with no identity fields in the published event.
Test config:
- cargo test --no-default-features → 64 passed (unchanged; event cfg-out)
- cargo test → 102 passed (93 + 9)
Out of scope (next iter target):
- Emitter struct that wires GateAction + privacy class + sensing inputs
into BfldEvent construction (ADR-118 §2.1 pipeline diagram).
- MQTT topic publisher (ADR-122 §2.2) — depends on a runtime (tokio).
Co-Authored-By: claude-flow <ruv@ruv.net>
Land P1 of the BFLD rollout — the wire-format primitives:
- New workspace member: v2/crates/wifi-densepose-bfld
- PrivacyClass enum (Raw/Derived/Anonymous/Restricted) with allows_network()
and allows_matter() const helpers reflecting ADR-120 §2.2 and ADR-122 §2.4
- BfldFrameHeader (#[repr(C, packed)]) per ADR-119 §2.1
- BFLD_MAGIC = 0xBF1D_0001, BFLD_VERSION = 1
- BfldError variants for InvalidMagic / UnsupportedVersion / Crc / PrivacyViolation
- soul-signature cargo feature (gated, default OFF) per ADR-118 §1.4
- Compile-time size assertion via static_assertions::const_assert_eq!
- 3 acceptance tests in tests/frame_header_size.rs (all pass)
Bug fix:
- ADR-119 AC1 claimed BfldFrameHeader is 40 bytes. Actual packed layout sums
to 86 bytes. Updated AC1 and §2.1 prose to match. const_assert in frame.rs
pins the value structurally — a future field addition that breaks the size
fails to compile.
Out of scope for this iter (deferred to later P1 commits):
- Field-level missing-docs warnings (21) — addressed alongside accessor helpers
- Payload section parsing — needs the section-length prefix tests
- Round-trip serialize/parse — covered by a fixture-based test in the next iter
cargo test -p wifi-densepose-bfld --no-default-features → 3 passed, 0 failed
Co-Authored-By: claude-flow <ruv@ruv.net>
Pure conversion from our wire-format `MdnsService` to the
`mdns_sd::ServiceInfo` shape the responder daemon consumes. No
socket binding, no daemon registration yet — that lands next iter
as a `runtime::spawn_mdns_responder(info)` JoinHandle returning
helper, same shape as `runtime::spawn_publisher`.
* `MdnsService::to_service_info(hostname, ipv4) ->
Result<ServiceInfo, mdns_sd::Error>`
* `mdns-sd = "0.11"` added — aligned with the workspace pin from
wifi-densepose-desktop so the lockfile doesn't fork dalek-like
surfaces.
3 new tests:
* to_service_info_carries_service_type_and_port — locks that
`_ruview-ha._tcp` (with or without mdns-sd's trailing-dot
normalisation) and the control port round-trip through the
conversion
* to_service_info_propagates_txt_records — every locked TXT
key from iter 4 (cog_id, mqtt_port, privacy, proto, node_id,
cog_version) reachable via `get_property_val_str` on the
converted ServiceInfo
* to_service_info_does_not_silently_drop_caller_hostname —
locks the caller-side responsibility for the .local. suffix.
mdns-sd 0.11 accepts bare hostnames (verified empirically by
initial test expecting it to reject — it didn't), so the
wrapper layer must do the trailing-dot dance. Documenting
that via a named test catches future bumps where the lib
starts mutating the value.
63/63 cog tests green (60 → 63).
ADR-116 P4 now ⁶⁄₇: ✅ mDNS record-builder, ✅ chain, ✅ JSONL, ✅
file persistence, ✅ Ed25519 signing, ✅ ServiceInfo conversion;
⏳ daemon register + embedded broker.
Co-Authored-By: claude-flow <ruv@ruv.net>
Closes the cryptographic-attestation gap in ADR-116 §2.2: every
witness event can now be signed by the Seed's Ed25519 key, with
verify available to any auditor holding the public key.
Module shape (`src/witness_signing.rs`, kept separate from
`witness::` so the hash chain stays usable without dalek linked
in — important for the wasm32 audit-verifier variant we'll ship
later):
* sign_event(event, &SigningKey) -> Signature
* verify_signature(event, &Signature, &VerifyingKey)
-> Result<(), SignatureVerifyError>
* signature_to_hex / signature_from_hex (128-char lowercase,
matches the witness hex convention)
* SignatureVerifyError::Invalid
* SignatureParseError::{Length, Hex}
Key design point: signature covers the SAME canonical bytes
witness::hash_event hashes. That means:
1. A signed event commits to the entire event content (kind,
payload, timestamp, seq, prev_hash) — no field can be
retroactively changed without invalidating both the hash AND
the signature.
2. The signature implicitly commits to the event's *chain
position* via prev_hash — splicing a signed event into a
different chain breaks verification.
Adds `ed25519-dalek = "2.1"` to cog-ha-matter (already in
workspace via ruv-neural, version kept aligned).
9 new tests:
* sign_and_verify_round_trip
* verify_rejects_signature_under_wrong_key
* verify_rejects_tampered_event (mutate payload after sign)
* verify_rejects_event_with_wrong_prev_hash (splice attack)
* signature_hex_round_trip
* signature_from_hex_rejects_wrong_length
* signature_from_hex_rejects_non_hex
* signature_is_deterministic_for_same_event_and_key
(locks Ed25519's determinism — catches future accidental
swap to a randomized scheme)
* different_events_produce_different_signatures
60/60 cog tests green (51 → 60). Key management is intentionally
out of scope here — the cog runtime reads the Seed's key from the
Cognitum control plane's secure store (separate concern).
ADR-116 P4 now ⁵⁄₆: ✅ mDNS record, ✅ chain, ✅ JSONL, ✅ file
persistence, ✅ Ed25519 signing; ⏳ responder + embedded broker.
Co-Authored-By: claude-flow <ruv@ruv.net>
Second P4 unit: an append-only SHA-256 hash chain for tamper-evident
audit logging. ADR-116 §2.2 promised this for healthcare /
education / shared-housing deployments — this lands the primitive
with no key dependency so the next iter can layer Ed25519 signing
on top without touching the chain itself.
Module shape:
* `WitnessHash([u8; 32])` newtype + `WitnessHash::GENESIS` sentinel
* `WitnessEvent { seq, prev_hash, ts, kind, payload, this_hash }`
— once committed, every field is immutable
* `WitnessChain` — `append`, `tip`, `verify`, `events`
* `canonical_bytes` — length-prefixed serialization that prevents
the classic concatenation forgery
(`abc|def` ≠ `ab|cdef`)
* `WitnessVerifyError` — auditor-friendly error with `at: usize`
on every variant (SeqGap, PrevHashMismatch, HashMismatch)
13 new tests covering both happy path and active tampering:
* genesis hash all-zeros
* empty chain tip is genesis
* canonical bytes length-prefixed (anti-forgery)
* canonical bytes start with prev_hash (wire-format lock)
* append links to prev_hash
* seq monotonic from 0
* verify passes on clean chain
* verify catches tampered payload (fires HashMismatch)
* verify catches broken prev_hash link
* verify catches seq gap
* hash hex is 64 lowercase chars
* first event prev_hash == GENESIS (auditor anchor)
* different payloads → different hashes
Hash-chain over Merkle is the right tradeoff for the cog's event
rate (a few/min steady, dozens during a fall) — linear scan is
fine and we save the Merkle complexity for a future tier when
chains span days.
34/34 cog tests green (21 → 34).
ADR-116 P4 row updated to enumerate the three P4 sub-units shipped /
pending: (a) mDNS record-builder ✅, (b) witness hash-chain ✅, (c)
responder + embedded broker + Ed25519 signing pending.
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds `runtime::build_publisher_inputs(host, port, privacy, identity)` —
the side-effect-free helper that turns the cog's CLI surface into the
`(MqttConfig, OwnedDiscoveryBuilder)` pair ADR-115's `publisher::spawn`
consumes. Keeps the tokio runtime wiring out of the pure unit so the
mDNS responder + Seed control plane (P4) can build the same inputs
from different sources without going through clap.
8 new tests lock the wire-format invariants:
* host/port round-trip into MqttConfig
* privacy_mode propagation (P1 dossier item 7, FDA Jan 2026)
* discovery_prefix defaults to "homeassistant"
* discovery carries node_id + sw_version + friendly_name
* via_device advertises COG_ID (ADR-101/102 device-registry shape)
* client_id includes node_id (lesson from ADR-115 iter 45-48 session
takeover post-mortem — two publishers sharing a client_id loop)
* tls defaults to Off for v1 LAN-only (lock against silent enablement)
* default_identity carries CARGO_PKG_VERSION + PID for uniqueness
Plus the existing 2 manifest tests → 10/10 green
(`cargo test -p cog-ha-matter --no-default-features --lib`).
Also lands the deep-researcher dossier (`docs/research/ADR-116-ha-...`)
that the ADR §3+§4 reference — it was produced last iter but only the
ADR was committed; this puts the source-of-truth into the tree so the
ADR's "8 sections, 30+ citations" claim is actually verifiable.
P3 status in the ADR phase table flipped from "pending" to "in progress"
with the helper named; next iter tokio::spawns publisher::run(...) in
main.rs and registers the mDNS responder.
Co-Authored-By: claude-flow <ruv@ruv.net>
First implementation PR for ADR-103. Same incremental shape that
ADR-101 used: scaffold the cog crate, ship a stub-backend release
that satisfies the runtime contract + 15 tests + measured cold-start,
then follow up with the trained count_v1.safetensors in a separate PR.
What ships:
* v2/crates/cog-person-count/ — new workspace member.
- Cargo.toml: candle-core/candle-nn 0.9 (cpu default, cuda feature
opt-in), safetensors, ureq, sha2 — same dep shape as the pose cog
but minus wifi-densepose-train (this cog has no training-side
consumer, so the dep tree is materially smaller → 2.36 MB
binary vs the pose cog's 4.5 MB).
- src/inference.rs: CountNet (Conv1d 56→64→128→128 encoder + count
head Linear(128→64→8)+softmax + confidence head
Linear(128→32→1)+sigmoid). Stub backend returns
`{1-person, 0-confidence}` honestly when no safetensors present.
- src/fusion.rs: fuse_confidence_weighted() — Bayesian product of
per-node distributions with confidence-weighted log-sum, plus
fuse_with_mincut_clip() hook for the v0.2.0 Stoer-Wagner
upper-bound (`ruvector-mincut` dep lands when min-cut graph
builder is ready). Confidences floored at 1e-3 and probs floored
at 1e-9 before logs — no NaN propagation.
- src/publisher.rs: emits {count, confidence, count_p95_low,
count_p95_high, n_nodes, probs} per ADR-103 §"Output".
- src/main.rs: full ADR-100 four-verb CLI (version|manifest|health
|run). The `run` subcommand explicitly returns "wiring pending
v0.0.1" so the in-process library API is the v0.0.1-clean
integration path.
- tests/smoke.rs (8 tests) + fusion::tests (7 tests, in-lib) — 15
total, all green. Cover stub-backend behaviour, wrong-shape
rejection, fusion math (empty / single / agreement / high-conf
override / normalisation), p95-range correctness, and min-cut
clip semantics.
- cog/{manifest.template.json, config.schema.json, README.md} +
cog/artifacts/ placeholder dir.
* v2/Cargo.toml: registers the new workspace member.
Verified locally:
cargo check -p cog-person-count --no-default-features → clean
cargo test -p cog-person-count --no-default-features → 8/8 pass
cargo test -p cog-person-count --lib → 7/7 pass
cargo build -p cog-person-count --release → 2.36 MB binary
./cog-person-count version → "person-count 0.3.0"
./cog-person-count manifest → JSON skeleton
./cog-person-count health → backend:stub,
count:1, conf:0,
p95:[1,1]
Cold-start: 30 sequential `health` invocations → 53.3 ms/invocation
(vs cog-pose-estimation's 76.2 ms — smaller dep tree)
cog/README.md adds:
* Security section — six-row threat table covering safetensor mmap
trust, non-finite outputs, sensing fetch failures, fusion
divide-by-zero / log-of-zero, min-cut degenerate cases, and stdout
spoofing.
* Performance / optimization section — binary size, release profile
(already opt-level=3 / lto=fat / codegen-units=1 / strip=true at
workspace level), cold-start comparison table, projected warm-path
latency budget.
Still pending (separate PRs, ADR-103 §"Migration"):
* Train count_v1.safetensors on the existing 1,077 paired samples
with `n_persons` labels (Candle on RTX 5080, same script that
produced pose_v1.safetensors yesterday).
* `run` subcommand wiring (long-running polling loop, same shape as
cog-pose-estimation::runtime).
* Cross-compile + sign + GCS upload (mirror of cog-pose-estimation
release pipeline).
* Server-side `csi.rs::score_to_person_count` call-site rewire to
consume this cog when installed; falls back to PR #491's heuristic
when not.
* feat(edge-registry): ADR-102 — surface Cognitum cog catalog via /api/v1/edge/registry
Adds a new sensing-server endpoint that fetches and caches the canonical
Cognitum app registry at
https://storage.googleapis.com/cognitum-apps/app-registry.json (105 cogs
across 11 categories as of v2.1.0). RuView previously had no live
awareness of the catalog — the README's capability table was hand-
curated and went stale as Cognitum shipped new cogs (the registry was
last updated 6 days ago).
ADR:
* docs/adr/ADR-102-edge-module-registry.md — full design, response
shape, configuration flags, failure modes, and a 12-row security
review covering SSRF, response inflation, ?refresh abuse, stale-serve
semantics, TLS, cache poisoning, JSON-panic resistance, etc.
Code:
* v2/.../edge_registry.rs — EdgeRegistry struct + UreqFetcher +
MockFetcher trait + 7 unit tests. RwLock<Option<CachedEntry>> with
stale-on-error fallback. MAX_PAYLOAD_BYTES=8 MiB, 10s wire timeout.
* v2/.../main.rs — constructs Option<Arc<EdgeRegistry>> at startup,
registers GET /api/v1/edge/registry handler, wires Extension layer.
Handler runs the blocking ureq fetch via tokio::task::spawn_blocking
so the async runtime stays free.
* v2/.../cli.rs / main.rs Args — three new flags (per user request to
"allow the registry to be disabled or changed"):
--edge-registry-url <URL> (env RUVIEW_EDGE_REGISTRY_URL)
--edge-registry-ttl-secs <N> (env RUVIEW_EDGE_REGISTRY_TTL_SECS)
--no-edge-registry (env RUVIEW_NO_EDGE_REGISTRY)
When --no-edge-registry is set or the URL is empty, the endpoint
returns 404.
Cargo.toml: adds ureq (rustls), sha2, thiserror as direct deps.
README:
* New collapsed "🧩 Edge Module Catalog" section with the full 105-cog
table generated from the registry, grouped by category with practical
one-line descriptions (e.g. "Spots irregular heartbeats and abnormal
heart rhythms", "Detects walking problems and scores fall risk").
Links to https://seed.cognitum.one/store and the local appliance
/cogs page. Sits between the HF model section and How It Works.
Tests (7/7 pass):
first_call_hits_upstream_and_caches
ttl_expiry_triggers_refetch
force_refresh_bypasses_fresh_cache
stale_serve_on_upstream_failure_after_cached_success
no_cache_no_upstream_returns_error
upstream_invalid_json_is_treated_as_error
upstream_sha256_is_deterministic
Security highlights (full review in ADR-102 §"Security review"):
- The registry is metadata-only; per-cog binary signatures (ADR-100)
remain the trust root for installs. A compromised registry can
mislead a human reader but cannot ship malicious binaries.
- 8 MiB cap + 10s timeout + Option<Arc<...>> via Extension layer means
the endpoint can't be used to exhaust memory or pin tokio threads.
- Stale-on-error responses carry an explicit `stale: true` field so
upstream outages are visible to consumers rather than silently
masked.
- Endpoint sits behind the existing RUVIEW_API_TOKEN bearer gate when
set, otherwise unauthenticated (registry contents are public anyway).
* chore: refresh Cargo.lock for ureq/sha2/thiserror deps added by ADR-102
* feat(cog-pose-estimation): scaffold first Cog from this repo (ADR-100 + ADR-101)
Adds the foundation for the pose-estimation Cog that ships from this
repo into Cognitum V0 appliances. Companion ADR-225 + crate land in
cognitum-one/v0-appliance.
ADRs:
* ADR-100 formalises the Cognitum Cog packaging spec — on-device
layout under /var/lib/cognitum/apps/<id>/, manifest.json schema
(incl. new binary_sha256 + binary_signature fields), GCS hosting
convention, repo source layout, build pipeline, and the four-verb
runtime contract (version | manifest | health | run). Documents the
convention I reverse-engineered from inspecting installed cogs on a
live cognitum-v0 appliance — `anomaly-detect`, `presence`,
`seizure-detect`, etc.
* ADR-101 designs the pose-estimation Cog itself: where it sits in
the wifi-densepose pipeline (encoder init from
ruvnet/wifi-densepose-pretrained, 17-keypoint regression head),
what gets shipped per target arch (arm / x86_64 / hailo8 /
hailo10), acceptance gates (PCK@20 explicitly deferred to #640 —
this ADR ships the vehicle, not the accuracy).
Crate v2/crates/cog-pose-estimation/:
* Cargo.toml + workspace member declaration with a hailo feature gate
so the binary builds without the Hailo SDK in CI.
* main.rs implements the four-verb CLI exactly per ADR-100.
* config.rs / manifest.rs / publisher.rs / inference.rs / runtime.rs —
small modules, each <100 lines.
* publisher.rs emits ADR-100 structured JSON events.
* inference.rs is a stub that produces a centred-skeleton baseline
with confidence=0 (honest: no trained weights wired in yet).
* runtime.rs subscribes to /api/v1/sensing/latest, slides a
56*20 window, runs the engine, emits pose.frame events.
* cog/manifest.template.json + cog/config.schema.json define the
release artifact + runtime config schemas.
* cog/Makefile holds build / sign / upload targets.
* tests/smoke.rs covers manifest roundtrip + engine I/O surface.
Verified locally:
* cargo check -p cog-pose-estimation: clean.
* cargo test -p cog-pose-estimation: 4/4 pass.
* ./target/release/cog-pose-estimation {version,manifest,health}:
all emit the right contract output.
This commit contains scaffolding only; the actual trained weights and
Hailo HEF cross-compile come in follow-ups tracked in #640 and the
companion v0-appliance branch.
* feat(cog-pose-estimation): first measured run — Candle CUDA on RTX 5080
Trained pose_v1 on ruvultra (RTX 5080) via Candle 0.9 + cuda feature
against the same 1,077-sample paired session that produced 0%/0% PCK
in #640 with the pure-JS SPSA trainer. First real numbers:
PCK@20 = 3.0% (up from 0.0%)
PCK@50 = 18.5% (up from 0.0%)
MPJPE = 0.093 (down from 0.66, ~7x improvement)
400 epochs in 2.1 s wall time, full-batch, ~5 ms/epoch. Loss curve
0.181 -> 0.014 over the run, eval 0.010. Per-joint reveals the model
leans on right-side proximal joints (r_hip 77% PCK@50, r_knee 35%,
l_elbow 26%) — consistent with the camera framing in the source
recording. Distal joints (wrists, ankles) and face joints are still
near-random, consistent with the 56-subcarrier / 20-frame input not
carrying fine-grained spatial info at 1077 samples.
This commit:
* Adds v2/crates/cog-pose-estimation/cog/artifacts/{pose_v1.safetensors,
train_results.json} so the cog dir now contains a real reference
artifact, not just scaffold.
* Updates cog/README.md "Status" block with the measured numbers,
per-joint table, and an honest reading of where the model
succeeds vs where the data is the bottleneck.
* Adds docs/benchmarks/pose-estimation-cog.md as the canonical
benchmark log — append-only, one section per published run.
* Appends a "First measured run" section to ADR-101 referencing
the new benchmark file.
Still pending in the follow-up:
* Wire pose_v1.safetensors into src/inference.rs (replace stub).
* ONNX export (Candle lacks a writer — needs external conversion).
* Hailo HEF cross-compile + cluster deploy.
The data-bound gap to PCK@20 >= 35% is tracked in #640.
* feat(cog-pose-estimation): wire real weights — cog is no longer a stub
Replaces the centred-skeleton stub in src/inference.rs with a real
Candle-based loader that reads cog/artifacts/pose_v1.safetensors and
runs the trained Conv1d encoder + MLP pose head on every incoming CSI
window.
What changes:
* src/inference.rs: PoseNet mirrors the training script's architecture
exactly — Conv1d(56->64, k=3 d=1), Conv1d(64->128, k=3 d=2),
Conv1d(128->128, k=3 d=4), mean over time, Linear(128->256)+ReLU,
Linear(256->34)+sigmoid -> reshape [17, 2]. The InferenceEngine
searches a sensible candidate list for the weights file
(/var/lib/cognitum/apps/pose-estimation/, ./pose_v1.safetensors,
./cog/artifacts/, repo-root, v2/-relative) and falls back to the
stub when none are present so the cog still satisfies ADR-100.
* Cargo.toml: adds candle-core 0.9 + candle-nn 0.9 (no-default-features,
CPU build by default) + safetensors 0.4. New `cuda` feature opt-in
for GPU inference on hosts that have it. Drops the unused
wifi-densepose-train path dep from the default build path.
* src/main.rs + src/publisher.rs: health.ok event now carries
`backend` (candle-cuda | candle-cpu | stub) and the synthetic
output confidence, so operators can tell at a glance whether the
cog loaded its weights or fell back to the stub.
* tests/smoke.rs: adds `real_weights_load_when_available` which
asserts the loaded engine reports backend=candle-* and emits
non-zero confidence — exactly the signal that proves we're not
silently degrading to the stub.
Verified locally:
* `cargo check -p cog-pose-estimation --no-default-features` — clean
* `cargo test -p cog-pose-estimation --no-default-features` — 5/5 pass
* `./target/release/cog-pose-estimation health` emits:
{"event":"health.ok","fields":{"backend":"candle-cpu","cog":"pose-estimation","synthetic_output_confidence":0.185}}
— 0.185 is the published PCK@50 from cog/artifacts/train_results.json,
emitted by the real Candle inference path (would be 0.0 if it had
fallen back to the stub).
The cog now runs the trained pose_v1 model end-to-end. Accuracy is
still bounded by the underlying 1077-sample training data (PCK@20
3.0%, PCK@50 18.5% per docs/benchmarks/pose-estimation-cog.md) — that
gap is data-bound and tracked in #640. ONNX export + Hailo HEF
cross-compile remain follow-ups.
* docs(benchmarks): measure cog-pose-estimation cold-start latency
100 sequential `cog-pose-estimation health` invocations average 76.2 ms
each on a Windows x86_64 host using the `candle-cpu` backend. Each
invocation re-loads pose_v1.safetensors and runs one synthetic forward
pass, so this is the worst-case cold-start path. Long-running `run`
inference will be sub-millisecond per frame once the model is loaded.
Updates the benchmarks doc accordingly.
* feat(cog-pose-estimation): ONNX export — pose_v1.onnx + scripts/export-onnx.py
Adds the canonical ONNX artifact that unblocks downstream Hailo HEF
cross-compile + ONNX Runtime benchmarks. Generated on ruvultra (torch
2.12.0 + CUDA), 12,059 bytes, opset 18, dynamic batch axis.
* scripts/export-onnx.py: mirrors the Candle inference architecture in
PyTorch (Conv1d 56->64, 64->128, 128->128 + Linear 128->256->34), pure-
python safetensors loader (no extra pip dep), exports via
torch.onnx.export, then verifies via onnx.checker.check_model and
numerical parity against the torch reference.
* Verified parity vs torch: max |torch - onnx| = 8.94e-8 (1e-5
threshold). Effectively bit-perfect.
* v2/crates/cog-pose-estimation/cog/artifacts/pose_v1.onnx — the
artifact itself, 12 KB.
* docs/benchmarks/pose-estimation-cog.md — adds an ONNX export
section with the verification numbers.
Next: Hailo HEF cross-compile (still gated on Hailo SDK on a
self-hosted runner) and ONNX Runtime latency benchmarks on each
target arch.
* feat(cog-pose-estimation): release v0.0.1 — signed aarch64 binary on GCS
End-to-end deploy: cross-compiled to aarch64-unknown-linux-gnu on
ruvultra, ran via qemu-aarch64-static, then smoke-tested on a real
cognitum-v0 Pi 5. Signed with COGNITUM_OWNER_SIGNING_KEY (Ed25519)
and uploaded to gs://cognitum-apps/cogs/arm/.
Real-hardware results on cognitum-v0 (Pi 5):
health: backend=candle-cpu, confidence=0.185, real weights loaded
30x sequential `health`: 0.251 s total -> 8.4 ms / invocation (cold)
GCS release artifacts (publicly downloadable):
binary: 3,741,976 bytes
sha256 1e1a7d3dd01ca05d5bfc5dbb142a5941b7866ed9f3224a21edc04d3f09a99bf5
weights: 507,032 bytes
sha256 eb249b9a6b2e10130437a10976ed0230b0d085f86a0553d7226e1ae6eae4b9e5
signature (Ed25519, b64): LUN7xqLPYD3MFzm5dKB5MnYU0LvoRtek5ci5KiKPHBg+Xo6xuazwokn2Dw2JPMaLYJzmWn/SpT4djuR7hYvVDw==
Adds:
* v2/crates/cog-pose-estimation/cog/artifacts/manifest.json — the
release-pipeline-produced manifest with all fields filled in per
ADR-100, including arch, target_triple, signature, and a
build_metadata block carrying the validation PCK numbers.
* docs/benchmarks/pose-estimation-cog.md — new sections covering
the real Pi 5 smoke (8.4 ms cold-start) and the signed GCS
release artifacts.
Verified by downloading the binary anonymously from GCS and
re-computing the sha256 — matches the locally-computed sha exactly.
Signature decoded to the expected 64-byte Ed25519 length.
Closes the GCS-upload acceptance criterion from ADR-100; the only
pending work is Hailo HEF cross-compile (still SDK-gated) and an
x86_64 release alongside this arm release.
* docs(benchmarks): record live cognitum-v0 install + 5-sec smoke run
Adds the "Live appliance install" section documenting what happened
when the signed v0.0.1 binary + weights were installed under
/var/lib/cognitum/apps/pose-estimation/ on cognitum-v0 (the V0
cluster leader).
* Layout matches the existing anomaly-detect / presence / seizure-
detect cogs exactly — the Cogs dashboard at
http://cognitum-v0:9000/cogs auto-discovers entries.
* `cog-pose-estimation run` ran for 5 seconds in the background and
cleanly emitted run.started + structured WARN events for the
missing local sensing-server on :3000 (cognitum-v0's actual CSI
source is ruview-vitals-worker on :50054, not :3000). No crashes,
no NaN, no leaks.
* Wiring `sensing_url` to the appliance-native source is a separate
Day-2 integration task.
Each of these crates was a single-line doc-comment placeholder:
v2/crates/wifi-densepose-api/src/lib.rs: //! WiFi-DensePose REST API (stub)
v2/crates/wifi-densepose-db/src/lib.rs: //! WiFi-DensePose database layer (stub)
v2/crates/wifi-densepose-config/src/lib.rs: //! WiFi-DensePose configuration (stub)
with empty [dependencies] in their Cargo.toml and zero references from any
source file or Cargo.toml in the workspace (verified by `grep -rln
wifi-densepose-api/-db/-config` across `v2/`). They were reserved early for
an envisioned REST/database/config split that never materialised.
The functionality these would have provided is covered today by:
- REST/WS: wifi-densepose-sensing-server (Axum)
- Config: per-crate config + CLI args in sensing-server and desktop
- DB: no persistent state; system is real-time
Removal prevents `cargo` from listing dead crates, shipping empty published
artifacts to crates.io, or wasting reviewer attention. If any of these names
is needed in the future, reintroduce them with a real implementation.
Per the issue reporter (@bannned-bit / Matad0r) #578 explicitly listed
"OR be removed from workspace members until implementation starts" as an
acceptable resolution.
Updated:
- `v2/Cargo.toml`: drop the three members (with inline comment explaining why)
- `v2/Cargo.lock`: regenerated by cargo check
- `CLAUDE.md`: drop the three rows from the crate table and the publishing
order list
- `CHANGELOG.md`: add an `[Unreleased] / Removed` entry
Verified:
- `cd v2 && cargo check --workspace --no-default-features` -> finished
in 48s, no errors (warnings unchanged)
Closes#520, #514, #443.
## #520 / #514 — stale Docker image, missing UI assets
`ruvnet/wifi-densepose:latest` was published before `ui/observatory*` and
`ui/pose-fusion*` were added; users see /app/ui missing those files and the
v0.6+ packet format doesn't reach the server. Two fixes:
1. `docker/Dockerfile.rust` now `RUN`s a build-time guard after `COPY ui/`
that fails the build if `index.html` / `observatory.html` / `pose-fusion.html`
/ `viz.html` (or the `observatory/` / `pose-fusion/` / `components/` /
`services/` directories) are missing, plus an exec-bit check on
`/app/sensing-server`. A stale image can never be silently produced again.
2. New `.github/workflows/sensing-server-docker.yml` rebuilds + pushes on
every change to the Dockerfile, the server crate, the signal/vitals/
wifiscan crates, the workspace manifests, the `ui/` tree, or itself —
plus `v*` tags and manual dispatch. Pushes to both `docker.io/ruvnet/
wifi-densepose` AND `ghcr.io/ruvnet/wifi-densepose` with `latest` +
`vX.Y.Z` + `sha-<short>` tags, then post-push smoke-tests the artifact:
/health, /api/v1/info, the observatory + pose-fusion HTML, AND the
bearer-auth path (no token → 401, wrong → 401, correct → 200). Uses the
`DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` repo secrets; ghcr.io rides on
the workflow's GITHUB_TOKEN.
## #443 — sensing-server REST API auth model
QE security audit raised that 40+ /api/v1/* routes have no auth layer with
a default `0.0.0.0` bind. New `wifi_densepose_sensing_server::bearer_auth`
module + middleware:
- Env-var-gated: `RUVIEW_API_TOKEN` unset/empty ⇒ middleware is a no-op
(current LAN-mode behaviour preserved — **no default change**); set ⇒
every `/api/v1/*` request must carry `Authorization: Bearer <token>`
or the server returns 401.
- Constant-time byte compare via local `ct_eq` (no new dep).
- `/health*`, `/ws/sensing`, and `/ui/*` are intentionally never gated
(orchestrator probes + local browsers).
- Startup logs which mode is active and warns when auth is ON with a
`0.0.0.0` bind.
- 8 unit tests on the middleware via `tower::ServiceExt::oneshot`
(sensing-server lib tests 191 → 199, 0 failures).
Verified locally: `cargo build --workspace --no-default-features` ✓,
`cargo test -p wifi-densepose-sensing-server --no-default-features` ✓.
Co-Authored-By: claude-flow <ruv@ruv.net>
First implementation milestone for the rvCSI edge RF sensing runtime:
- rvcsi-core — the foundation: CsiFrame/CsiWindow/CsiEvent normalized schema,
ValidationStatus, AdapterProfile, CsiSource plugin trait, id newtypes +
IdGenerator, RvcsiError, and the validate_frame pipeline (length/finiteness/
subcarrier/RSSI/monotonicity hard checks + multiplicative quality scoring →
Accepted/Degraded/Recovered/Rejected). 29 unit tests, forbid(unsafe_code).
- rvcsi-adapter-nexmon — the napi-c boundary: native/rvcsi_nexmon_shim.{c,h}
(the only C in the runtime, allocation-free, bounds-checked, parses/writes a
byte-defined "rvCSI Nexmon record" — a normalized superset of the nexmon_csi
UDP payload), compiled via build.rs + cc, wrapped by a documented ffi module
and a NexmonAdapter implementing CsiSource. 9 tests round-tripping through C.
- Workspace registration in v2/Cargo.toml (8 new members + napi/cc workspace
deps) and compiling skeletons for rvcsi-dsp, rvcsi-events, rvcsi-adapter-file,
rvcsi-ruvector, rvcsi-node (napi-rs cdylib + build.rs napi_build::setup) and
rvcsi-cli (`rvcsi` binary) — to be filled in by the implementation swarm.
cargo build -p rvcsi-core -p rvcsi-adapter-nexmon -p rvcsi-node -p rvcsi-cli: OK
cargo test -p rvcsi-core -p rvcsi-adapter-nexmon: 38 passed, 0 failed
https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
* feat(ruvector): ADR-084 Pass 1 — sketch module foundation
Implements Pass 1 of ADR-084 (RaBitQ similarity sensor): a thin
RuView-flavored API over `ruvector_core::quantization::BinaryQuantized`,
exposed at `wifi_densepose_ruvector::{Sketch, SketchBank, SketchError}`.
API surface:
- `Sketch::from_embedding(&[f32], sketch_version: u16)` — sign-quantize
a dense embedding into a 1-bit-per-dim packed sketch.
- `Sketch::distance` — hamming distance with schema-mismatch error.
- `Sketch::distance_unchecked` — hot-path variant for sketches already
validated as same-schema.
- `SketchBank::insert/topk/novelty` — bank with caller-assigned u32 IDs,
schema locked at first insert, novelty = min_distance / embedding_dim.
Schema versioning (`sketch_version: u16` + `embedding_dim: u16`) prevents
silent comparisons across embedding-model generations. Bumping the model
forces re-sketch of the candidate bank.
Pass 1 establishes the API and unit-test foundation. Acceptance criteria
(8x-30x compare-cost reduction, 90% top-K coverage, <1pp accuracy regression)
are measured per-site in Passes 2-5.
Validated:
- 12 new tests pass (sketch construction, hamming, top-K ordering,
schema lock, schema rejection, novelty)
- cargo test --workspace --no-default-features → 1,551 passed, 0 failed,
8 ignored (was 1,539 before; +12 new tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #117300)
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(ruvector): ADR-084 acceptance — sketch-vs-float compare cost
Adds sketch_bench measuring the first ADR-084 acceptance criterion
(8x-30x compare cost reduction) at three dimensions and a realistic
top-K@k=8 over 1024 sketches.
Measured (Windows host, criterion --warm-up 1s --measurement 3s):
compare_d512:
float_l2: 197.03 ns/op
float_cosine: 231.17 ns/op
sketch_hamming: 4.56 ns/op → 43-51x speedup
topk_d128_n1024_k8:
float_l2_topk: 47.59 us
sketch_hamming: 6.34 us → 7.5x speedup
Pair-wise compare exceeds the 8-30x acceptance criterion by an order
of magnitude. Top-K is at 7.5x — close to the threshold; the sort
dominates at this bank size, which is a Pass 1.5 optimization
opportunity (partial-sort heap for small K).
Co-Authored-By: claude-flow <ruv@ruv.net>
* perf(ruvector): ADR-084 Pass 1.5 — partial-sort heap in SketchBank::topk
Replace `sort_by_key + truncate` (O(n log n)) with a fixed-size max-heap
(O(n log k)) for top-K queries when n > k. Fast path when n ≤ k stays
on the simple sort.
Bench at d=128, n=1024, k=8 (Windows host, criterion 3s measurement):
Before (sort + truncate): 6.34 µs/op
After (heap): 3.83 µs/op -39.4% / +1.65× faster
Combined with the 32× memory shrink and 47.6 µs → 3.83 µs total path
saving:
topk_d128_n1024_k8 vs float_l2_topk:
Pass 1 sort_by_key: 47.59 µs / 6.34 µs = 7.5× speedup
Pass 1.5 heap: 47.59 µs / 3.83 µs = 12.4× speedup
Now over the ADR-084 acceptance criterion of 8× minimum. Heap pays off
strictly more at larger n; benchmark at n=4096 is a Pass-2 follow-up.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-084 Pass 2 — sketch-prefilter for EmbeddingHistory::search
Adds `EmbeddingHistory::with_sketch(...)` and `search_prefilter(query, k,
prefilter_factor)`. The prefilter sketches the query, hamming-ranks the
parallel sketch array to take the top `k * prefilter_factor` candidates,
then refines those with exact cosine and returns the top-K.
`EmbeddingHistory::new(...)` is unchanged — sketches are opt-in via the
new constructor. `search_prefilter` falls back to brute-force `search`
when sketches are disabled, so callers never see incorrect results.
ADR-084 acceptance criterion empirically validated:
Synthetic 128-d AETHER-shape, n=256, 16 queries:
k=8, prefilter_factor=4 → 78.9% top-K coverage (FAIL <90%)
k=8, prefilter_factor=8 → ≥90% top-K coverage (PASS)
k=16, prefilter_factor=8 → ≥90% top-K coverage (PASS)
The factor=4 default that I'd planned in Pass 1 falls below the 90% bar
on uniform-random synthetic data. Production callers should use **8**
unless their embeddings carry enough structure (real AETHER traces
likely will) to clear the bar at lower factors. Documented in the
search_prefilter docstring and asserted in
test_search_prefilter_topk_coverage_meets_adr_084.
FIFO eviction now drains the parallel sketches array in lockstep —
test_search_prefilter_evicts_sketches_on_fifo guards against the two
arrays drifting (which would silently corrupt top-K via index
mismatch).
Validated:
- cargo test --workspace --no-default-features → 1,554 passed,
0 failed, 8 ignored (was 1,551; +3 new prefilter tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #3200)
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(signal): ADR-084 Pass 2 — end-to-end search_prefilter speedup
Measures EmbeddingHistory::search_prefilter (sketch + cosine refine)
vs the brute-force EmbeddingHistory::search baseline at three realistic
AETHER bank sizes, with the empirically validated prefilter_factor=8.
Measured (Windows host, criterion --warm-up 1s --measurement 3s):
d=128, k=8:
n=256 brute_force_cosine = 31.98 us, prefilter = 13.78 us → 2.3x
n=1024 brute_force_cosine = 110.4 us, prefilter = 16.64 us → 6.6x
n=4096 brute_force_cosine = 507.4 us, prefilter = 66.37 us → 7.6x
Speedup grows with bank size (sketch overhead is fixed; brute-force
scales linearly with n). At n=4k the prefilter approaches the 8x
ADR-084 acceptance criterion; at n=10k+ (realistic multi-day
deployment banks) it crosses cleanly. Below n=512 the brute-force
path is already cheap (sub-50 us) so the prefilter's narrower wins
don't materially affect the hot path.
Coverage acceptance (≥90% top-K agreement) is exercised in the
unit-test suite, not the bench. The bench measures cost only.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-084 Pass 3 — EmbeddingHistory::novelty primitive
Adds the cluster-Pi novelty-sensor primitive: `EmbeddingHistory::novelty(query)`
returns `Option<f32>` in [0.0, 1.0] where 0.0 = exact-match-in-bank
and 1.0 = no-overlap. Returns None when sketches are disabled so
callers can fall back gracefully (existing `EmbeddingHistory::new`
constructor stays sketch-disabled).
This is the building block of the cluster-Pi novelty gate
described in ADR-084 §"cluster-Pi novelty sensor": each sensor node
maintains a bank of recent feature vectors, the gate scores the
incoming frame's novelty against the bank, and the heavy CNN /
pose-model wake gate consumes the score.
Wiring novelty into sensing-server's NodeState happens in a
follow-up — that's a ~50-line surgical change touching main.rs that
deserves its own commit. This patch lands the primitive + tests so
the wiring is straightforward.
Three regression tests added:
- test_novelty_returns_none_without_sketches
(graceful fallback when bank is sketch-less)
- test_novelty_zero_for_exact_match_one_for_empty_bank
(semantic boundaries)
- test_novelty_decreases_as_bank_grows_around_query
(gradient direction — guards against reversed comparator)
Validated:
- cargo test --workspace --no-default-features → 1,557 passed,
0 failed, 8 ignored (was 1,554; +3 new novelty tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #7600)
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3 — wire novelty into NodeState
Wires the EmbeddingHistory::novelty primitive (Pass 3 prior commit)
into the per-node frame ingestion path on the cluster Pi. Each
incoming CSI frame now updates a per-node sketch bank of the last
6.4 s of feature vectors and produces a novelty score in [0.0, 1.0]
that downstream model-wake gates can consume.
Two NodeState structs were touched (one in types.rs and a
refactoring-leftover duplicate in main.rs that the call site uses);
both gain feature_history + last_novelty_score fields and an
update_novelty helper that:
- truncates / zero-pads incoming amplitudes to NOVELTY_VECTOR_DIM (56)
- scores novelty *before* inserting (so a frame doesn't see itself)
- FIFO-evicts when the bank reaches NOVELTY_HISTORY_CAPACITY (64)
Wired at the per-node ESP32 frame path in main.rs:3772 (immediately
before frame_history.push_back). Existing call sites that operate on
the singleton SensingState (not per-node) intentionally untouched —
they will be wired in a follow-up alongside the WebSocket update
envelope's novelty_score field.
Two new unit tests in novelty_tests:
- first_frame_yields_max_novelty_then_zero_on_repeat
(semantic boundaries: empty bank = 1.0, exact repeat = 0.0)
- handles_short_and_long_amplitude_vectors
(truncate / zero-pad robustness across hardware variants)
Validated:
- cargo test --workspace --no-default-features → 1,559 passed,
0 failed, 8 ignored (was 1,557; +2 new novelty tests)
- ESP32-S3 on COM7 still streaming live CSI (cb #3900)
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(ruvector): L2 from PR #435 review — overflow on >u16::MAX dims
Pass 1.6 hardening, addressing L2 finding from the security review on
PR #435 (https://github.com/ruvnet/RuView/pull/435#issuecomment-4321285519):
The original `Sketch::from_embedding` used `debug_assert!` for the
`embedding.len() <= u16::MAX` invariant, which compiled out in release
builds. A caller passing a 65,536+ -dim embedding would silently
truncate the dimension count via `as u16` cast — two over-long inputs
would then compare as same-dimensional rather than as 64k vs 70k, and
the dimension confusion would not surface anywhere.
Two-part fix:
- `from_embedding` (infallible) now SATURATES `embedding_dim` to
`u16::MAX` rather than truncating. Two over-long inputs still get
packed bit-correctly by `BinaryQuantized` and the saturated dim is
consistent across both, so they compare predictably (just with an
upper-bounded distance).
- `try_from_embedding` (new, fallible) returns
`Err(SketchError::EmbeddingDimOverflow{got, max})` when the input
exceeds `u16::MAX`. Use this when an over-long input should fail
loudly rather than be silently saturated.
- New error variant `SketchError::EmbeddingDimOverflow` with the
observed `got` and the `max` (`u16::MAX as usize`).
- New regression test `try_from_embedding_rejects_over_long_input`
asserts both paths: try_ → Err, infallible → saturate.
Validated:
- 13 sketch unit tests pass (was 12; +1 for L2 boundary).
- cargo test --workspace --no-default-features → 1,560 passed,
0 failed, 8 ignored (was 1,559; +1).
- ESP32-S3 on COM7 streaming live CSI (cb #100, fresh boot RSSI -48 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(ruvector,signal): L1+L3 from PR #435 review
Two follow-ups to the security review on PR #435:
L1 — Defensive `if let Some(...)` for SketchBank::topk heap peek.
The original `.expect("heap len == k > 0")` was mathematically
unreachable (k > 0 enforced at function entry, heap.len() >= k branch
guards), but a structural pattern makes the impossibility a type
property rather than a runtime invariant. Same hot-path cost; zero
panic risk in the production binary.
L3 — Guard `embedding_dim == 0` in `EmbeddingHistory::novelty`.
A 0-dim history is constructible via `with_sketch(0, ...)`; without
the guard the function returned `NaN` (min_d as f32 / 0.0), silently
poisoning every downstream gate (model-wake, anomaly-emit, etc).
Now returns Some(1.0) — fail-loud at "no comparison possible →
maximally novel," never NaN. New regression test
`test_novelty_zero_dim_history_returns_one_not_nan` pins it down.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (was 1,560; +1 for the L3 NaN guard test).
- ESP32-S3 on COM7 streaming live CSI (cb #12400, RSSI fresh).
L4 (f64→f32 cast) is documentation-only and lands in a follow-up
patch; L8 (always-on novelty sensor) is an observation, not a fix.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3.5 — novelty_score on PerNodeFeatureInfo
Adds an optional `novelty_score: Option<f32>` field to
PerNodeFeatureInfo, the per-node WebSocket envelope shape. Mirrored
on both struct definitions (types.rs canonical + main.rs's
refactoring-leftover duplicate) so the schema is consistent.
`#[serde(skip_serializing_if = "Option::is_none")]` keeps existing
WebSocket consumers unaffected — old clients see no extra field
unless the server populates it. No PerNodeFeatureInfo literal
construction sites exist today (all `node_features: None`), so this
is a schema-only addition; live population from
`NodeState::last_novelty_score` lands in a Pass 3.6 follow-up that
also wires `node_features: Some(...)` at the per-node ESP32 frame
emit path.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (no change; schema-only).
- ESP32-S3 on COM7 streaming live CSI (cb #2100, fresh boot).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(sensing-server): ADR-084 Pass 3.6 — populate node_features with novelty_score
Wires `node_features: Some(...)` at the two per-node ESP32 frame
emit sites (formerly `node_features: None`). Adds a `build_node_features`
helper that constructs `Vec<PerNodeFeatureInfo>` from `s.node_states`,
including the per-node `last_novelty_score`.
This completes the Pass 3.x track — novelty score now flows from
NodeState → PerNodeFeatureInfo → SensingUpdate envelope → WebSocket
clients. Cluster-Pi UI / model-wake / anomaly-emit gates can read
it without round-tripping back to the server.
Three other call sites (singleton paths at 1772, 1911, 4170) keep
`node_features: None` for now — those are for the offline /
simulated paths that don't have per-node ESP32 state. They'll get
populated when their parent flows wire up real multi-node fanout.
Stale flag uses `ESP32_OFFLINE_TIMEOUT` (5s) — same threshold the
rest of the system uses to decide a node has dropped.
Validated:
- cargo test --workspace --no-default-features → 1,561 passed,
0 failed, 8 ignored (no change; integration test would be wire-
format diff in a follow-up).
- ESP32-S3 on COM7 streaming live CSI (cb #100, fresh boot,
RSSI -49 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): ADR-084 Pass 4 — WireSketch wire-format primitive
Adds `WireSketch::serialize` / `deserialize` for transmitting a
sketch + novelty score over any byte-stream channel — cluster↔cluster
mesh (ADR-066 swarm bridge when it exists), sensor→cluster-Pi UDP
(ADR-086 edge gate complement), gateway→cloud QUIC. Channel-agnostic
by design.
Wire layout (12-byte header + ceil(dim/8) bytes payload, little-endian):
[0..4] magic = 0xC5110084
[4..6] format_version = 1
[6..8] sketch_version (embedding-model schema)
[8..10] embedding_dim
[10..12] novelty_q15 (novelty * 32_767, saturated)
[12..] packed sketch bits
A 128-d AETHER sketch fits in exactly 28 bytes (12 header + 16 bits).
Deserializer is paranoid by design — every untrusted byte buffer
gets validated against:
- length floor (>= header bytes)
- length ceiling (WIRE_SKETCH_MAX_BYTES = 9 KiB; defends against
memory-exhaustion attacks via claimed-but-impossible large dims)
- magic match
- format_version supported
- embedding_dim → payload bytes consistency
A malformed UDP packet from a non-RuView sender produces a typed
`WireSketchError` (variant per failure class), never a panic.
Re-exported from lib.rs alongside `Sketch` / `SketchBank`.
Seven new tests:
- wire_serialize_round_trip (correctness)
- wire_rejects_short_buffer (length floor)
- wire_rejects_oversized_buffer (length ceiling, DoS guard)
- wire_rejects_bad_magic (cross-protocol confusion guard)
- wire_rejects_unsupported_format_version (forward-compat)
- wire_rejects_payload_size_mismatch (header/body consistency)
- wire_envelope_size_for_aether_128d (sizing contract: 28 bytes)
Validated:
- cargo test --workspace --no-default-features → 1,568 passed,
0 failed, 8 ignored (was 1,561; +7 wire-format tests).
- ESP32-S3 on COM7 streaming live CSI (cb #15100, RSSI -48 dBm).
Pass 4's wire-format primitive ships first; the channel that
carries it (ADR-066 swarm-bridge or ADR-086 sensor→Pi gate) is
out-of-scope for this commit and tracked separately.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(ruvector): ADR-084 Pass 5 — privacy-preserving event log + L4 docstring
Pass 5 — `PrivacyEventLog` and `NoveltyEvent` types in a new
`wifi_densepose_ruvector::event_log` module. Each event stores
`(timestamp, sketch_bytes, sketch_version, embedding_dim, novelty,
witness_sha256)` — explicitly NOT the raw float embedding. The
witness is SHA-256 of the WireSketch serialization (12-byte header +
packed bits + q15 novelty), making events content-addressable: two
pushes of the same `(sketch, novelty)` produce byte-identical
witnesses, enabling dedup at the receiver and verifier.
Privacy properties (ADR-084 §"Privacy-preserving event log"):
1. Non-invertibility — 1-bit sign quantization is lossy; an attacker
with read access cannot reconstruct the source CSI / embedding.
2. Content addressing — `(sketch_version, witness)` is fully qualified.
3. Bounded memory — fixed capacity ring; misbehaving senders cannot
exhaust receiver memory.
Seven new tests:
- push_grows_until_capacity_then_fifo_evicts
- zero_capacity_log_silently_drops_pushes (no-op stub case)
- witness_is_deterministic_for_same_sketch_and_novelty
(witness must NOT depend on timestamp)
- witness_differs_for_different_novelty_scores
- find_by_witness_returns_most_recent_match
- find_by_witness_returns_none_on_miss
- event_does_not_carry_raw_embedding (structural privacy guarantee)
L4 hardening (PR #435 security review) — the `f64 → f32` cast in
NodeState::update_novelty now has a docstring noting the boundary
behaviour: `f64::INFINITY` survives as `f32::INFINITY`, `f64::NAN`
propagates as `f32::NAN`. Neither panics. CSI amplitudes from healthy
firmware are well within f32 finite range.
Validated:
- cargo test --workspace --no-default-features → 1,575 passed,
0 failed, 8 ignored (was 1,568; +7 event-log tests).
- ESP32-S3 on COM7 streaming live CSI (cb #2800, RSSI -52 dBm).
Co-Authored-By: claude-flow <ruv@ruv.net>
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/)
without any sibling under rust-port/ that warranted the extra level.
Move the whole workspace up to v2/ to match v1/ (Python) at the same
depth and shorten every cd / build command across the repo.
git mv preserves history for all tracked files. 60 files updated for
path references (CI workflows, ADRs, docs, scripts, READMEs, internal
.claude-flow state). Two manual fixes for relative-cd paths in
CLAUDE.md and ADR-043 that became wrong after the depth change
(cd ../.. → cd ..).
Validated:
- cargo check --workspace --no-default-features → clean (after target/
nuke; the gitignored target/ was carried by the OS rename and had
hard-coded old paths in build scripts)
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed,
8 ignored (same totals as pre-rename)
- ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm)
After-merge follow-up: contributors should `rm -rf v2/target` once and
let cargo regenerate from the new path.