Files
ruvnet--RuView/docs/adr/ADR-167-ddd-bounded-contexts.md
T
rUv 42dcf49f4d fix(adr): resolve duplicate ADR numbers + close ADR-080 security + ADR-154 M1 signal backlog (#1051)
* fix(signal): circular phase variance for ghost-tap guard (ADR-154 §7.4 #1)

`phase_variance` computed a LINEAR sample variance over phase angles that
wrap at ±π, so a tightly-clustered set straddling the branch cut reported
spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap guard on
real, tightly-clustered CIR taps.

Replace with Mardia's circular variance V = 1 − R̄, bounded [0,1] and
invariant to where the cluster sits on the circle. Re-derive the guard
against the bounded metric via a named const
`GHOST_TAP_CIRCULAR_VARIANCE_MAX` (the old TAU-scaled threshold is
meaningless on [0,1]).

Grade: metric fix MEASURED; threshold value DATA-GATED — a clean single-path
ramp also sweeps the circle, so V alone cannot separate clean from
unsanitized without labelled frames. Conservative default (0.99) errs toward
never false-rejecting, strictly more permissive at the wrap boundary than the
buggy linear guard.

Fails-on-old test: `phase_variance_circular_not_fooled_by_branch_cut` —
inlines the old linear variance to show it exceeds TAU on wrap-straddling
phases while circular V≈0 and the guard no longer trips. Plus
`phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical,
V≈1 uniform).

cargo test -p wifi-densepose-signal --no-default-features --features cir --lib
→ 432 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(signal): pin Welford n=0/n=1 finiteness guard (ADR-154 §7.4 #10)

The shared `WelfordStats` (field_model.rs, used by longitudinal.rs and others)
relies on `count < 2` guards in `variance`/`sample_variance`/`std_dev`/
`z_score` to stay finite at the boundaries. The guards existed but the n=0
boundary was UNTESTED — exactly the §4 divide-by-(n−1) family the ADR groups
this with.

Add `welford_finite_at_n0_and_n1` asserting every statistic is finite and
returns the documented sentinel (0.0) at n=0 and n=1, plus load-bearing doc
comments on the two guards.

Fails-on-old proof: with the `sample_variance` guard removed, the test FAILS
with "attempt to subtract with overflow" at the `(self.count - 1)` underflow
(0usize − 1); `variance` would similarly yield 0.0/0.0 = NaN. The guard is
restored; the test pins it so a future regression is caught.

Grade: MEASURED (boundary finiteness is asserted; the guard is the §4-family
fix made testable).

cargo test -p wifi-densepose-signal --no-default-features --lib field_model
→ 22 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic adversarial thresholds + boundary tests (ADR-154 §7.4 #13)

Lift the bare numeric literals buried in `check`/`check_consistency` into
named, documented module consts (FIELD_MODEL_GINI_VIOLATION=0.8,
ENERGY_RATIO_HIGH_VIOLATION=2.0, ENERGY_RATIO_LOW_VIOLATION=0.1,
CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1, SCORE_W_* weights). VALUES UNCHANGED —
each const equals the original literal; only names + pinning tests are new.

Grade: DATA-GATED. The operating values stay empirical (defensible values need
labelled spoofed/clean CSI — Wi-Spoof, §6.2/§7.3). The de-magicking +
characterization tests are MEASURED: `tuning_consts_unchanged_from_literals`,
`energy_ratio_high_boundary`, `energy_ratio_low_boundary`,
`field_model_gini_boundary`, `consistency_active_fraction_boundary` pin the
decision boundaries at/just-below/just-above each threshold, so a future
data-driven retune is a visible, tested change.

Fails-on-change proof: bumping ENERGY_RATIO_HIGH_VIOLATION 2.0→3.0 makes
`energy_ratio_high_boundary` FAIL (restored). Operating values explicitly
NOT changed.

cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::adversarial
→ 20 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic coherence drift/gate thresholds (ADR-154 §7.4 #9)

Lift the bare detection literals in `coherence.rs::classify_drift`
(DRIFT_STABLE_SCORE=0.85, DRIFT_STEP_CHANGE_MAX_STALE=10) and the
`coherence_gate.rs` Default impl (DEFAULT_ACCEPT_THRESHOLD=0.85,
DEFAULT_REJECT_THRESHOLD=0.5, DEFAULT_MAX_STALE_FRAMES=200,
DEFAULT_PREDICT_ONLY_NOISE=3.0) into named, documented consts. VALUES
UNCHANGED. The gate already exposed these via GatePolicyConfig (config seam);
this names + pins the defaults.

Grade: DATA-GATED. Operating values stay empirical (defensible Z-score
thresholds need labelled stable/drifting coherence traces). De-magicking +
boundary tests are MEASURED: `classify_drift_stable_score_boundary`,
`classify_drift_stale_count_boundary` pin the at/just-below/just-above
decisions; `drift_consts_unchanged_from_literals` /
`gate_default_consts_unchanged_from_literals` pin the values. Operating values
explicitly NOT changed.

cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::coherence
→ 40 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-154): mark §7.4 P1 backlog cleared — Milestone-1 (#1,#10 RESOLVED; #9,#13 DATA-GATED)

Update ADR-154 §7.4 backlog rows #1, #9, #10, #13 with commit refs + grades,
the §7.4 intro count (four P1 items cleared, ~41 P2/P3 remain), the
Horizon-ledger one-liner (Milestone-1 DONE), and the §8 honest-limits #1 line
(metric now correct; threshold still DATA-GATED). Add CHANGELOG [Unreleased]
entry.

Grades: #1 RESOLVED (MEASURED metric / DATA-GATED threshold), #10 RESOLVED
(MEASURED), #9 & #13 RESOLVED-PARTIAL (DATA-GATED — de-magicked + boundary
tested, operating values unchanged).

Validation: cargo test --workspace --no-default-features → 2057 passed, 0
failed; wifi-densepose-signal lib → 442 passed (no-default + --features cir);
python archive/v1/data/proof/verify.py → VERDICT: PASS, hash f8e76f21…46f7a
UNCHANGED (CIR ghost-tap guard is not on the deterministic proof path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(sensing-server): stop leaking internal errors in HTTP responses (ADR-080 #2)

Six handlers in `main.rs` serialized the internal error `Display` straight
into the JSON response body, leaking server internals to any client (ADR-080
finding #2, CWE-209; reframed onto the Rust boundary by ADR-164 G11):

  - edge_registry_endpoint: a panicked spawn_blocking `JoinError`
    ("task … panicked") in a 500, and the raw upstream error in a 503
  - delete_model / delete_recording / start_recording: std::io::Error
    strings carrying OS detail / filesystem paths
  - calibration_start / calibration_stop: the FieldModel error chain

New `error_response` module: `internal_error` / `internal_error_json` /
`upstream_unavailable` log the full detail server-side only (tagged with a
correlation id) and return a generic body
(`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file
paths, no Debug chain. The correlation id lets an operator join a client
report to the exact server log line without ever shipping the detail.

Pinned by 5 error_response tests, incl. a leak-substring guard
(internal_error_body_does_not_leak_detail) verified to FAIL on the reverted
old body (returns the panic message / path / "os error"). The HOMECORE sweep
(ADR-161) covered homecore-server, not this crate.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(sensing-server): pin XFF-immunity + no-query-token (ADR-080 #1, #3)

Findings #1 (XFF-spoofing bypass) and #3 (JWT-in-URL, CWE-598) were logged
against the Python v1 API but are VERIFIED ABSENT on the current Rust
sensing-server, so they get regression tests rather than redundant fixes:

  - #1 XFF: there is no IP-based rate-limiter or IP-allowlist to bypass, and
    neither security middleware reads a forwarded header. Added
    bearer_auth::xff_header_never_affects_auth_decision (spoofed
    X-Forwarded-For never flips a 401<->200 decision) and
    host_validation::forwarded_headers_never_bypass_host_allowlist (spoofed
    X-Forwarded-Host: localhost never lets Host: evil.com past the allowlist).

  - #3 JWT-in-URL: require_bearer reads the token only from the Authorization
    header; WS handlers take no query token; the sole Query extractor
    (EdgeRegistryParams) is a non-secret refresh flag. Added
    bearer_auth::query_string_token_is_never_accepted — ?token= / ?access_token=
    in the URL never authenticates (stays 401) while the header path still 200s.
    Verified to FAIL when a query-token path is injected into require_bearer.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-080): mark P0 security findings #1-#3 RESOLVED; close ADR-164 G11

- ADR-080: Status note + per-finding closure (#1 XFF and #3 JWT-in-URL
  verified absent + regression-pinned; #2 leaked errors fixed via the
  error_response module). Records the v1-vs-Rust boundary distinction
  explicitly: v1 paths remain archived; this closure governs the shipped
  Rust sensing-server.
- ADR-164: Gap Register G11 and the Open/Gated Backlog entry marked
  RESOLVED with the fix + branch reference.
- CHANGELOG: [Unreleased] -> ### Security entry covering all three findings.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): renumber 6 displaced ADRs to resolve duplicate-number collisions (ADR-164 G1)

Resolves the 5 duplicate ADR numbers (6 displaced files) flagged by ADR-164
Gap Register item G1. Canonical keeper per number = first file committed at
that number (date tie-broken by inbound cross-reference count / parent-appendix
relationship). Displaced files renumbered to the next free numbers (166-171):

  050 keeps provisioning-tool-enhancements (5 refs vs 1)
    -> ADR-166-quality-engineering-security-hardening
  052 keeps tauri-desktop-frontend (parent ADR)
    -> ADR-167-ddd-bounded-contexts (its appendix)
  147 keeps nvidia-cosmos/OccWorld (the actual ADR, has Status header)
    -> ADR-168-benchmark-proof (proof companion, no Status)
    -> ADR-169-adam-mode-light-theme (was untracked)
  148 keeps drone-swarm-control-system (committed #862)
    -> ADR-170-yoga-mode-pose-system (was untracked)
  149 keeps public-community-leaderboard-huggingface (committed 16:47 vs 17:38)
    -> ADR-171-swarm-benchmarking-evaluation-methodology

Updates in-file `# ADR-NNN` headers and intra-file self-references (yoga-modes

* docs(adr): repoint inbound cross-references to renumbered ADRs (166-171)

Follow-up to the ADR renumbering (ADR-164 G1). Updates every inbound reference
that pointed at a displaced ADR, disambiguating shared numbers by title/slug so
only references to the DISPLACED topic move and keeper references stay put.

ADR-168 (was 147 benchmark-proof): README, CHANGELOG, user-guide,
  proof-of-capabilities, research docs 00/03 — all path/label refs updated.
ADR-169 (was 147 adam-mode) / ADR-170 (was 148 yoga-mode): docs/adr/README index.
ADR-171 (was 149 swarm-benchmarking): all ruview-swarm eval code+docs
  (Cargo.toml, evals/, eval_swarm.rs, metrics/mod/report/runner.rs), research
  doc 03 (every §-ref matched ADR-171 sections, not AetherArena), 00-system-review,
  series README, CHANGELOG, and ADR-148's forward/"open issues" pointers.
ADR-166 (was 050 quality-engineering / security-hardening): disambiguated from the
  ADR-050 provisioning KEEPER by topic. The HMAC/secure_tdm, directory-traversal,
  bind-address, and OTA-PSK-auth references in code comments
  (wifi-densepose-hardware Cargo.toml + secure_tdm.rs, sensing-server main.rs) and
  in ADR-052-tauri / ADR-167 all describe the security-hardening ADR -> ADR-166.
ADR-167 (was 052 ddd-appendix): inbound appendix references.

Index/registry updates: docs/adr/README.md, gap-analysis/census.md (rows +
header count), gap-analysis/lens-findings.md (collision table marked RESOLVED),
and ADR-164 Gap Register G1 marked RESOLVED with the full renumber map.

Keeper references deliberately untouched: all ADR-147 OccWorld code, all ADR-148
drone-swarm code/docs, all ADR-149 AetherArena refs (incl. ADR-150's SSL/resampling
refs, which ADR-150 explicitly binds to the AetherArena benchmark), ADR-050
provisioning refs, ADR-052 tauri refs. The frozen GitHub blob URLs in
docs/adr/.issue-177-body.md (pinned to an old branch) are left as historical.

Comment-only code edits; no behavior change. wifi-densepose-hardware compiles
clean; the sensing-server build's sole blocker is the pre-existing upstream
midstreamer-temporal-compare@0.2.1 registry crate, unrelated to these edits.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 14:31:38 -04:00

26 KiB

ADR-167 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend

Appendix to ADR-052. Renumbered from ADR-052 to ADR-167 to resolve the ADR-052 duplicate-number collision (per ADR-164 Gap Register G1); the parent decision remains ADR-052.

This document maps out the domain model for the RuView Tauri desktop application described in ADR-052. It defines bounded contexts, their aggregates, entities, value objects, and the domain events flowing between them.

Context Map

+-------------------+       +---------------------+       +--------------------+
|                   |       |                     |       |                    |
|  Device Discovery |------>| Firmware Management |------>| Configuration /    |
|                   |       |                     |       | Provisioning       |
+-------------------+       +---------------------+       +--------------------+
        |                           |                             |
        |                           |                             |
        v                           v                             v
+-------------------+       +---------------------+       +--------------------+
|                   |       |                     |       |                    |
| Sensing Pipeline  |<------| Edge Module         |       | Visualization      |
|                   |       | (WASM)              |       |                    |
+-------------------+       +---------------------+       +--------------------+

Relationship types:
  -----> Upstream/Downstream (upstream publishes events, downstream consumes)
  <----- Conformist (downstream conforms to upstream's model)

1. Device Discovery Context

Purpose: Find, identify, and monitor ESP32 CSI nodes on the local network.

Upstream of: Firmware Management, Configuration, Sensing Pipeline, Visualization

Aggregates

NodeRegistry (Aggregate Root)

Maintains the authoritative list of all known nodes. Merges discovery results from multiple strategies (mDNS, UDP probe, HTTP sweep) and deduplicates by MAC address.

Field Type Description
nodes Map<MacAddress, Node> All discovered nodes keyed by MAC
scan_state ScanState Idle, Scanning, Error
last_scan DateTime<Utc> Timestamp of last completed scan

Invariant: No two nodes may share the same MAC address. If a node is discovered via multiple strategies, the most recent data wins.

Persistence: The registry is persisted to ~/.ruview/nodes.db (SQLite via rusqlite). On startup, all previously known nodes are loaded as Offline and reconciled against a fresh discovery scan. This means the app remembers the mesh across restarts — critical for field deployments where nodes may be temporarily powered off.

Node (Entity)

Field Type Description
mac MacAddress (VO) IEEE 802.11 MAC address (unique identity)
ip IpAddr Current IP address (may change on DHCP renewal)
hostname Option<String> mDNS hostname
node_id u8 NVS-provisioned node ID
firmware_version Option<SemVer> Firmware version string
health HealthStatus (VO) Online / Offline / Degraded
discovery_method DiscoveryMethod (VO) How this node was found
last_seen DateTime<Utc> Last successful contact
tdm_config Option<TdmConfig> (VO) TDM slot assignment
edge_tier Option<u8> Edge processing tier (0/1/2)

Value Objects

  • MacAddress — 6-byte hardware address, formatted as AA:BB:CC:DD:EE:FF
  • HealthStatus — enum: Online, Offline, Degraded(reason: String)
  • DiscoveryMethod — enum: Mdns, UdpProbe, HttpSweep, Manual
  • TdmConfig{ slot_index: u8, total_nodes: u8 }
  • SemVer — semantic version major.minor.patch

Domain Events

Event Payload Consumers
NodeDiscovered { node: Node } Firmware Mgmt (check for updates), Visualization (add to mesh graph)
NodeWentOffline { mac: MacAddress, last_seen: DateTime } Visualization (gray out node), Sensing Pipeline (remove from active set)
NodeCameOnline { node: Node } Visualization (restore node), Sensing Pipeline (re-add)
NodeHealthChanged { mac: MacAddress, old: HealthStatus, new: HealthStatus } Visualization (update indicator)
ScanCompleted { found: usize, new: usize, lost: usize } Dashboard (update summary)

Anti-Corruption Layer

When receiving data from the ESP32 OTA status endpoint (GET /ota/status), the response format is owned by the firmware and may change across firmware versions. The ACL translates the raw JSON response into Node entity fields:

/// ACL: Translate ESP32 OTA status response to Node fields.
fn translate_ota_status(raw: &serde_json::Value) -> Result<NodePatch, AclError> {
    NodePatch {
        firmware_version: raw["version"].as_str().map(SemVer::parse).transpose()?,
        uptime_secs: raw["uptime_s"].as_u64(),
        free_heap: raw["free_heap"].as_u64(),
        // Firmware may add fields in future versions — unknown fields are ignored
    }
}

2. Firmware Management Context

Purpose: Flash, update, and verify firmware on ESP32 nodes.

Upstream of: Configuration (a fresh flash triggers provisioning) Downstream of: Device Discovery (needs node list and serial port info)

Aggregates

FlashSession (Aggregate Root)

Represents a single firmware flashing operation from start to completion. Each session has a lifecycle: Created -> Connecting -> Erasing -> Writing -> Verifying -> Completed | Failed.

Field Type Description
id Uuid Session identifier
port SerialPort (VO) Target serial port
firmware FirmwareBinary (Entity) The binary being flashed
chip ChipType (VO) Target chip (ESP32, ESP32-S3, ESP32-C3)
phase FlashPhase (VO) Current phase of the flash operation
progress Progress (VO) Bytes written / total, speed
started_at DateTime<Utc> When the session started
error Option<String> Error message if failed

Invariant: Only one FlashSession may be active per serial port at a time.

FirmwareBinary (Entity)

Field Type Description
path PathBuf Filesystem path to the .bin file
size_bytes u64 Binary size
version Option<SemVer> Extracted from ESP32 image header
chip_type Option<ChipType> Detected from image magic bytes
checksum Sha256Hash (VO) SHA-256 of the binary

OtaSession (Aggregate Root)

Represents an over-the-air firmware update to a running node.

Field Type Description
id Uuid Session identifier
target_node MacAddress Target node MAC
target_ip IpAddr Target node IP
firmware FirmwareBinary The binary being pushed
psk Option<SecureString> PSK for authentication (ADR-166)
phase OtaPhase Uploading / Rebooting / Verifying / Done / Failed
progress Progress Upload progress

BatchOtaSession (Aggregate Root)

Coordinates rolling firmware updates across multiple mesh nodes. Prevents all nodes from rebooting simultaneously, which would collapse the sensing network.

Field Type Description
id Uuid Batch session identifier
firmware FirmwareBinary The binary being deployed
strategy OtaStrategy Sequential, TdmSafe, Parallel
max_concurrent usize Max nodes updating at once
batch_delay_secs u64 Delay between batches
fail_fast bool Abort remaining on first failure
node_states Map<MacAddress, BatchNodeState> Per-node progress

Invariant: In TdmSafe mode, adjacent TDM slots are never updated concurrently. Even-slot nodes update first, then odd-slot nodes.

Lifecycle: Planning → InProgress → Completed | PartialFailure | Aborted

  • BatchNodeState — enum: Queued, Uploading(Progress), Rebooting, Verifying, Done, Failed(String), Skipped
  • OtaStrategy — enum:
    • Sequential — one node at a time, wait for rejoin
    • TdmSafe — update non-adjacent slots to maintain sensing coverage
    • Parallel — all at once (development only)

Value Objects

  • SerialPort{ name: String, vid: u16, pid: u16, manufacturer: Option<String> }
  • ChipType — enum: Esp32, Esp32s3, Esp32c3
  • FlashPhase — enum: Connecting, Erasing, Writing, Verifying, Completed, Failed
  • OtaPhase — enum: Uploading, Rebooting, Verifying, Completed, Failed
  • Progress{ bytes_done: u64, bytes_total: u64, speed_bps: u64 }
  • Sha256Hash — 32-byte hash
  • SecureString — zeroized-on-drop string for PSK tokens

Domain Events

Event Payload Consumers
FlashStarted { session_id, port, firmware_version } UI (show progress)
FlashProgress { session_id, phase, progress } UI (update progress bar)
FlashCompleted { session_id, duration_secs } Configuration (trigger provisioning prompt)
FlashFailed { session_id, error } UI (show error)
OtaStarted { session_id, target_mac, firmware_version } Discovery (mark node as updating)
OtaCompleted { session_id, target_mac, new_version } Discovery (refresh node info)
OtaFailed { session_id, target_mac, error } UI (show error)
BatchOtaStarted { batch_id, strategy, node_count } UI (show batch progress)
BatchNodeUpdated { batch_id, mac, state } UI (update per-node status), Discovery (refresh)
BatchOtaCompleted { batch_id, succeeded, failed, skipped } UI (show summary), Discovery (full rescan)

Anti-Corruption Layer

The espflash crate has its own error types and progress reporting model. The ACL translates these into domain events:

/// ACL: Translate espflash progress callbacks to domain FlashProgress events.
impl From<espflash::ProgressCallbackMessage> for FlashProgress {
    fn from(msg: espflash::ProgressCallbackMessage) -> Self {
        match msg {
            espflash::ProgressCallbackMessage::Connecting => FlashProgress {
                phase: FlashPhase::Connecting,
                progress: Progress::indeterminate(),
            },
            espflash::ProgressCallbackMessage::Erasing { addr, total } => FlashProgress {
                phase: FlashPhase::Erasing,
                progress: Progress::new(addr as u64, total as u64),
            },
            // ... etc
        }
    }
}

3. Configuration / Provisioning Context

Purpose: Manage NVS configuration for ESP32 nodes — WiFi credentials, network targets, TDM mesh settings, edge intelligence parameters, WASM security keys.

Downstream of: Device Discovery (needs serial port), Firmware Management (post-flash provisioning)

Aggregates

ProvisioningSession (Aggregate Root)

Represents a single NVS write or read operation on a connected ESP32.

Field Type Description
id Uuid Session identifier
port SerialPort (VO) Target serial port
config NodeConfig (Entity) Configuration to write
direction Direction Read or Write
phase ProvisionPhase Generating / Flashing / Verifying / Done

NodeConfig (Entity)

The full set of NVS key-value pairs for a single node. Maps directly to the firmware's nvs_config_t struct (see firmware/esp32-csi-node/main/nvs_config.h).

Field Type NVS Key Description
wifi_ssid Option<String> ssid WiFi SSID
wifi_password Option<SecureString> password WiFi password
target_ip Option<IpAddr> target_ip Aggregator IP
target_port Option<u16> target_port Aggregator UDP port
node_id Option<u8> node_id Node identifier
tdm_slot Option<u8> tdm_slot TDM slot index
tdm_total Option<u8> tdm_nodes Total TDM nodes
edge_tier Option<u8> edge_tier Processing tier
hop_count Option<u8> hop_count Channel hop count
channel_list Option<Vec<u8>> chan_list Channel sequence
dwell_ms Option<u32> dwell_ms Hop dwell time
power_duty Option<u8> power_duty Power duty cycle
presence_thresh Option<u16> pres_thresh Presence threshold
fall_thresh Option<u16> fall_thresh Fall detection threshold
vital_window Option<u16> vital_win Vital sign window
vital_interval_ms Option<u16> vital_int Vital sign interval
top_k_count Option<u8> subk_count Top-K subcarriers
wasm_max_modules Option<u8> wasm_max Max WASM modules
wasm_verify Option<bool> wasm_verify Require WASM signature
wasm_pubkey Option<[u8; 32]> wasm_pubkey Ed25519 public key
ota_psk Option<SecureString> ota_psk OTA pre-shared key

Invariant: tdm_slot < tdm_total when both are set. Invariant: channel_list.len() == hop_count when both are set. Invariant: 10 <= power_duty <= 100.

MeshConfig (Entity)

A mesh-level configuration that generates per-node NodeConfig instances. Corresponds to ADR-044 Phase 2 (config file provisioning).

Field Type Description
common NodeConfig Shared settings (WiFi, target IP, edge tier)
nodes Vec<MeshNodeEntry> Per-node overrides (port, node_id, tdm_slot)
pub struct MeshNodeEntry {
    pub port: String,
    pub node_id: u8,
    pub tdm_slot: u8,
    // All other fields inherited from common
}

Invariant: tdm_total is automatically computed as nodes.len().

Value Objects

  • ProvisionPhase — enum: Generating, Flashing, Verifying, Completed, Failed
  • Direction — enum: Read, Write
  • Preset — enum: Basic, Vitals, Mesh3, Mesh6Vitals (ADR-044 Phase 3)

Domain Events

Event Payload Consumers
NodeProvisioned { port, node_id, config_summary } Discovery (trigger re-scan), UI (show success)
NvsReadCompleted { port, config: NodeConfig } UI (populate form)
ProvisionFailed { port, error } UI (show error)
MeshProvisionStarted { node_count } UI (show batch progress)
MeshProvisionCompleted { success_count, fail_count } UI (show summary)

4. Sensing Pipeline Context

Purpose: Control the sensing server process, receive real-time CSI data, and manage the signal processing pipeline.

Downstream of: Device Discovery (needs node IPs for data attribution)

Aggregates

SensingServer (Aggregate Root)

Represents the managed sensing server child process.

Field Type Description
state ServerState (VO) Stopped / Starting / Running / Stopping / Crashed
config ServerConfig (VO) Port configuration, log level, model paths
pid Option<u32> OS process ID when running
started_at Option<DateTime<Utc>> Start timestamp
log_buffer RingBuffer<LogEntry> Last N log lines
ws_url Option<Url> WebSocket URL for live data

Invariant: Only one SensingServer process may be managed at a time.

SensingSession (Entity)

An active connection to the sensing server's WebSocket for receiving real-time data.

Field Type Description
connection_state WsState Connecting / Connected / Disconnected
frames_received u64 Total CSI frames received this session
last_frame_at Option<DateTime<Utc>> Timestamp of last received frame
subscriptions HashSet<DataChannel> Which data streams are active

Value Objects

  • ServerState — enum: Stopped, Starting, Running, Stopping, Crashed(exit_code: i32)
  • ServerConfig{ http_port: u16, ws_port: u16, udp_port: u16, model_dir: PathBuf, log_level: Level }
  • LogEntry{ timestamp: DateTime, level: Level, target: String, message: String }
  • DataChannel — enum: CsiFrames, PoseUpdates, VitalSigns, ActivityClassification
  • WsState — enum: Connecting, Connected, Disconnected(reason: String)

Domain Events

Event Payload Consumers
ServerStarted { pid, ports: ServerConfig } UI (enable sensing view), Discovery (start health polling via WS)
ServerStopped { exit_code, uptime_secs } UI (disable sensing view)
ServerCrashed { exit_code, last_log_lines } UI (show crash report)
CsiFrameReceived { node_id, timestamp, subcarrier_count } Visualization (update charts)
PoseUpdated { persons: Vec<PersonPose> } Visualization (draw skeletons)
VitalSignUpdate { node_id, bpm, breath_rate } Visualization (update vitals chart)
ActivityDetected { label, confidence } Visualization (show activity)

5. Edge Module (WASM) Context

Purpose: Upload, manage, and monitor WASM edge processing modules running on ESP32 nodes.

Downstream of: Device Discovery (needs node IPs and WASM capability info) Upstream of: Sensing Pipeline (WASM modules emit edge-processed events)

Aggregates

ModuleRegistry (Aggregate Root)

Tracks all WASM modules across all nodes.

Field Type Description
modules Map<(MacAddress, ModuleId), WasmModule> Per-node module inventory

WasmModule (Entity)

Field Type Description
id ModuleId (VO) Node-assigned module identifier
name String Filename of the uploaded .wasm
size_bytes u64 Module size
status ModuleStatus (VO) Loaded / Running / Stopped / Error
node_mac MacAddress Which node this module runs on
uploaded_at DateTime<Utc> Upload timestamp
signed bool Whether the module has an Ed25519 signature

Value Objects

  • ModuleId — string identifier assigned by the node firmware
  • ModuleStatus — enum: Loaded, Running, Stopped, Error(String)

Domain Events

Event Payload Consumers
ModuleUploaded { node_mac, module_id, name, size } UI (refresh list)
ModuleStarted { node_mac, module_id } UI (update status)
ModuleStopped { node_mac, module_id } UI (update status)
ModuleUnloaded { node_mac, module_id } UI (remove from list)
ModuleError { node_mac, module_id, error } UI (show error)

Anti-Corruption Layer

The ESP32 WASM management HTTP API (/wasm/* on port 8032) returns raw JSON with firmware-specific field names. The ACL normalizes these:

/// ACL: Translate ESP32 WASM list response to domain WasmModule entities.
fn translate_wasm_list(raw: &[serde_json::Value]) -> Vec<WasmModule> {
    raw.iter().filter_map(|entry| {
        Some(WasmModule {
            id: ModuleId(entry["id"].as_str()?.to_string()),
            name: entry["name"].as_str().unwrap_or("unknown").to_string(),
            size_bytes: entry["size"].as_u64().unwrap_or(0),
            status: match entry["state"].as_str() {
                Some("running") => ModuleStatus::Running,
                Some("stopped") => ModuleStatus::Stopped,
                Some("loaded")  => ModuleStatus::Loaded,
                other => ModuleStatus::Error(
                    format!("Unknown state: {:?}", other)
                ),
            },
            // ...
        })
    }).collect()
}

6. Visualization Context

Purpose: Render real-time and historical sensing data — CSI heatmaps, pose skeletons, vital sign charts, mesh topology graphs.

Downstream of: Sensing Pipeline (receives data events), Device Discovery (needs node metadata for labeling)

This context is purely presentational and contains no domain logic. It transforms domain events from other contexts into visual representations.

Aggregates

None — this context is a Query Model (CQRS read side). It subscribes to domain events and projects them into view models.

View Models

DashboardView

Field Source Context Description
nodes Device Discovery Node cards with health, version, signal quality
server Sensing Pipeline Server status, uptime, port info
recent_activity All contexts Timeline of recent events

SignalView

Field Source Context Description
csi_heatmap Sensing Pipeline Subcarrier amplitude x time matrix
signal_field Sensing Pipeline 2D signal strength grid
activity_label Sensing Pipeline Current classification
confidence Sensing Pipeline Classification confidence

PoseView

Field Source Context Description
persons Sensing Pipeline Array of detected person skeletons
zones Sensing Pipeline Active zones in the sensing area

VitalsView

Field Source Context Description
breathing_rate_bpm Sensing Pipeline Per-node breathing rate time series
heart_rate_bpm Sensing Pipeline Per-node heart rate time series

MeshView

Field Source Context Description
nodes Device Discovery Positioned nodes for graph layout
edges Device Discovery Inter-node visibility/connectivity
tdm_timeline Device Discovery TDM slot schedule visualization
sync_status Sensing Pipeline Per-node sync status with server

Cross-Context Event Flow

                            NodeDiscovered
Device Discovery  ─────────────────────────────────> Firmware Management
        │                                                    │
        │  NodeDiscovered                                    │ FlashCompleted
        │  NodeHealthChanged                                 │
        ├──────────────────> Visualization                   v
        │                                           Configuration
        │  NodeDiscovered                                    │
        ├──────────────────> Sensing Pipeline                │ NodeProvisioned
        │                                                    │
        │                                                    v
        │                                           Device Discovery
        │                                           (re-scan triggered)
        │
        │  NodeDiscovered
        └──────────────────> Edge Module (WASM)
                                    │
                                    │ ModuleUploaded, ModuleStarted
                                    │
                                    v
                             Sensing Pipeline
                                    │
                                    │ CsiFrameReceived, PoseUpdated, VitalSignUpdate
                                    │
                                    v
                             Visualization

Implementation Notes

  1. Event Bus: Domain events are dispatched via Tauri's event system (app_handle.emit("event-name", payload)). The frontend subscribes using listen("event-name", callback). This provides natural cross-context communication without coupling contexts directly.

  2. State Isolation: Each bounded context maintains its own State<'_, T> managed by Tauri. Contexts do not share mutable state directly — they communicate exclusively through events.

  3. Module Organization: Each bounded context maps to a Rust module under src/commands/ and src/domain/:

    src/
      commands/           # Tauri command handlers (application layer)
        discovery.rs      # Device Discovery context commands
        flash.rs          # Firmware Management context commands
        ota.rs            # Firmware Management context commands
        provision.rs      # Configuration context commands
        server.rs         # Sensing Pipeline context commands
        wasm.rs           # Edge Module context commands
      domain/             # Domain models (pure Rust, no Tauri dependency)
        discovery/
          mod.rs
          node.rs         # Node entity, MacAddress VO
          registry.rs     # NodeRegistry aggregate
          events.rs       # Discovery domain events
        firmware/
          mod.rs
          binary.rs       # FirmwareBinary entity
          flash.rs        # FlashSession aggregate
          ota.rs          # OtaSession aggregate
          events.rs
        config/
          mod.rs
          nvs.rs          # NodeConfig entity
          mesh.rs         # MeshConfig entity
          provision.rs    # ProvisioningSession aggregate
          events.rs
        sensing/
          mod.rs
          server.rs       # SensingServer aggregate
          session.rs      # SensingSession entity
          events.rs
        wasm/
          mod.rs
          module.rs       # WasmModule entity
          registry.rs     # ModuleRegistry aggregate
          events.rs
      acl/                # Anti-corruption layers
        ota_status.rs     # ESP32 OTA status response translator
        wasm_api.rs       # ESP32 WASM API response translator
        espflash.rs       # espflash crate adapter
    
  4. Testing Strategy: Domain modules under src/domain/ have no Tauri dependency and can be tested with standard cargo test. Command handlers under src/commands/ require Tauri test utilities for integration testing.

  5. Shared Kernel: The MacAddress, SemVer, and SecureString value objects are shared across contexts. They live in a src/domain/shared.rs module. This is acceptable because they are immutable value objects with no behavior beyond validation and formatting.