Project

General

Profile

User Story #70 » 0001-docs-architecture-developer-onboarding-doc-set-CLAUD.patch

knight8241, 08/07/2026 18:33

View differences:

CLAUDE.md
# tome-client — agent & contributor orientation
**Aether** is an indoor **sub-centimetre UWB + IMU positioning mesh**. A device ranges
its peers over Ultra-Wideband, fuses that with its IMU, and the mesh cooperatively
solves a shared, metric coordinate frame — then draws peers/anchors in a live 3-D / FPV
scene and fires geometric & pointing "predicate" events.
This repo (`tome-client`) is the device side: an **Android/Jetpack-Compose** app
(`androidApp`) over a **Kotlin-Multiplatform** engine + mesh stack (`common`). The
cloud gateway (CA, telemetry, web console) is the separate **tome-server** repo.
## Read this first
The architecture & onboarding docs live in **[`docs/architecture/`](docs/architecture/)**
— start at **[`docs/architecture/README.md`](docs/architecture/README.md)**, the index/router.
Every doc lists the code it describes under **Source of truth**, so you can jump from a
file you're touching to the doc that explains it (the manifest has the path→doc map).
## Keep the docs true (maintenance rule)
When you change code, update the `docs/architecture/` doc whose **Source of truth** globs
cover your files, and bump its **Verified against** commit. Diagrams are **Mermaid** —
edit the fenced Mermaid block, never paste a screenshot. New subsystem? Add a doc and
register it in the manifest table.
## Build · test · deliver (the essentials)
- **Modules:** `:common` (pure KMP engine + mesh + netcode; unit-tested, no Android) and
`:androidApp` (Compose UI + Android platform actuals; needs the Android Gradle Plugin).
- **Verify engine/mesh logic:** `./gradlew :common:jvmTest`. In a network-restricted env
the pinned JetBrains toolchain + AGP can't be fetched — see
[`docs/architecture/08-contributing.md`](docs/architecture/08-contributing.md) for the
offline-isolation recipe (it runs `:common:jvmTest` green with the system JDK).
- **Deliver to the dev branch (`feature/uiux-update2`):** changes are `git format-patch`
files applied with **`git am --keep-cr`** — several files (`MeshCoordinator.kt`,
`MeshMessagingService.kt`, `LinkHandshake.kt`, server `mesh.py`) are **CRLF**; plain
`git am` strips the CR and fails. Full convention in `08-contributing.md`.
## Two things that surprise everyone
1. **The netcode event seam ships OFF.** `MessagingConfig.netcodeBringUp` defaults `false`;
the mesh solves and renders without it. See `04-netcode-and-time.md`.
2. **There are two meshing stacks.** An always-on **BroadcastBand** (UDP-broadcast gossip
for presence/discovery/UWB-address + IP exchange) bootstraps the **MMNF stack** (the
real UDP data + TCP control + Raft transport). See `03-networking-and-protocols.md`.
docs/architecture/00-overview.md
# 00 · Overview — Aether as a stack of interoperable systems
> **Layer** all · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — whole repo; this doc is the map, the numbered docs are the territory
> **Related** — everything; start here then follow the reading order in [README](README.md)
## What Aether is
Aether turns a room full of phones and UWB anchors into a **shared, sub-centimetre metric
coordinate frame**. Each device measures distances (and angle-of-arrival) to its peers over
Ultra-Wideband, fuses that with its IMU in an error-state Kalman filter, and the mesh
cooperatively agrees on one anchor **constellation** everyone solves against. On top of that
frame it renders a live 3-D / first-person scene of who's where, and evaluates **predicates**
— geometric regions and "point at that" gestures — into mesh-wide events.
Two coordinated codebases:
- **tome-client** (this repo) — the device. Android/Compose UI (`androidApp`) over a
Kotlin-Multiplatform engine + mesh + netcode core (`common`, runs anywhere the JVM does,
which is why it is unit-testable headless).
- **tome-server** — a Python/Flask gateway: the mesh **certificate authority**, the
**telemetry** sink (Prometheus / InfluxDB / Loki), a web **console**, and the command plane.
## The layered stack
```mermaid
flowchart TB
subgraph SENSE["Sensing (androidApp platform actuals)"]
UWB["UWB radios — DS-TWR ranging + AoA"]
IMU["IMU — accel + gyro"]
end
subgraph ENGINE["MOFE engine (common/engine) — doc 01"]
PIPE["5-stage per-measurement pipeline<br/>pre-filter → cross-anchor fuse → multilaterate → IMU/UWB EKF → TWR verify"]
CONST["Anchor constellation<br/>bootstrap · orient · lock · re-solve"]
PIPE --> FS["FusedState<br/>(position, velocity, orientation, σ)"]
CONST --> FRAME["Shared mesh frame + MeshOperatingMode"]
end
subgraph MESH["Mesh — MMNF (common/messaging) — docs 02·03"]
MSG["MeshMessagingService<br/>data (UDP) · control (TCP) · uplink (WS)"]
RAFT["Raft consensus (pre-vote)"]
REG["Replicated state registry"]
RAFT --> REG
end
subgraph NET["Netcode + time (common/engine/netcode) — doc 04 ⚠ default OFF"]
CLK["MeshClock (NTP-style)"]
BUF["Temporal buffers + interpolation"]
EVT["Claim → leader rewind → verdict"]
end
subgraph SEC["Security & trust (common/messaging/security) — doc 05"]
HS["Link handshake (X25519+Ed25519)"]
GK["Group-key datagram AEAD"]
CRED["Mesh-CA DeviceCredential"]
end
subgraph BAND["BroadcastBand bootstrap (androidApp/band) — doc 03"]
GOSSIP["UDP-broadcast gossip:<br/>presence · UWB addrs · peer IPs"]
end
subgraph APP["App & rendering (androidApp) — doc 07"]
HOST["MofeEngineHost / MeshRepository<br/>(StateFlow bridges)"]
UI["Compose shell · 3D/FPV scene · diagnostics"]
end
subgraph SERVER["tome-server — doc 06"]
CA["Ed25519 mesh CA · /mesh/ca"]
TEL["Telemetry: Prom · Influx · Loki"]
CON["Web console"]
end
UWB --> PIPE
IMU --> PIPE
FS --> MSG
FRAME --> MSG
MSG <--> RAFT
REG --> HOST
FS --> HOST
MSG -. "fused samples (gated)" .-> BUF
CLK --- BUF --- EVT
GOSSIP -. "peer IP + UWB addr" .-> MSG
HS --- MSG
GK --- MSG
CRED --- HS
CA -. "fetch + pin" .-> CRED
HOST --> UI
MSG -- "leader uplink (WS/JSON)" --> TEL
CRED -. "enroll / mint" .-> CA
REG -. "mirror" .-> CON
classDef off stroke-dasharray:4 3;
class NET off;
```
## Five planes of communication
Aether is easiest to reason about as **five planes**, each with its own medium, format, and
trust rules. Every arrow type the brief asked about lives in one of these.
| Plane | Medium / port | Format | Trust | Carries | Doc |
|-------|---------------|--------|-------|---------|-----|
| **UWB ranging** | UWB radio (DS-TWR) | vendor PHY | physical | distance + AoA between two devices | 01 |
| **Band (bootstrap)** | UDP **broadcast :47600** | CBOR CRDT frames | Ed25519-signed lifecycle; AEAD member frames | presence, UWB addresses, peer IPs, kinematics, motion assertions | 03 |
| **Mesh data** | UDP **:47474** | CBOR envelope + group-key AEAD | group key | fused-state samples, ranging batches (best-effort, latest-wins) | 03 |
| **Mesh control** | TCP **:47475** | CBOR envelope in AEAD link | per-link X25519+Ed25519 | Raft RPCs, membership, deltas/snapshots, claims/verdicts, key distribution | 02·03·05 |
| **Uplink** | WebSocket → server | JSON frames | device credential | leader→server telemetry, alerts, state mirror; server→leader commands | 06 |
Two guarantees worth internalizing early:
- **The channel is the QoS contract.** `RANGING` is best-effort + sequence-gated; `CONTROL`
is TCP-reliable/ordered; `UPLINK` is at-least-once store-and-forward.
- **`meshEpoch` fences incarnations** and **`sequence` fences replays**; producers never set
them — the messaging hub stamps them and the `StalenessGate` enforces them inbound.
## The three role vocabularies (don't conflate them)
| Vocabulary | Values | Where it lives | Decides |
|------------|--------|----------------|---------|
| **Assembly role** | `ROOT_ANCHOR`, `ANCHOR`, `LEARNER` | `MeshNode` | which `start()` path a node takes |
| **Device label** | `MeshDeviceType {ANCHOR,CLIENT,GATEWAY,CUSTOM}`, `MeshRole {MEMBER,GATEWAY,ROOT,ANCHOR,CLIENT}` | `model/mesh/DeviceContract.kt` | membership, UI, permissions |
| **Operating mode** | `UNINITIALIZED → PRE_QUORUM → QUORUM ⇄ DEGRADED` | `model/StateTypes.kt` | whether the frame is trustworthy enough to render |
## Repo & module map
```
tome-client/
├─ CLAUDE.md ← agent orientation (points here)
├─ common/ ← Kotlin-Multiplatform core (JVM-testable, no Android)
│ └─ src/commonMain/kotlin/com/aether/mofe/
│ ├─ engine/ ← MOFE: pipeline, EKF, constellation, calibration (doc 01)
│ │ ├─ netcode/ ← time, interpolation, events (doc 04)
│ │ └─ render/ ← render-readiness + role display policy (docs 01·07)
│ ├─ messaging/ ← MMNF: hub, coordinator, Raft, registry, codec (docs 02·03)
│ │ ├─ raft/ ← Raft consensus
│ │ └─ security/ ← handshake, group keys, op signing (doc 05)
│ ├─ model/ ← data types (measurements, state, wire payloads, config)
│ └─ math/ ← Vector3D, Quaternion, Matrix, Shape geometry
├─ androidApp/ ← Compose UI + Android platform actuals (doc 07)
│ └─ src/main/java/com/aether/mofe/
│ ├─ ui/ ← AppShell, scene, HUD, manage, predicate, mesh
│ ├─ viewmodel/ ← HudViewModel (render/aim brain)
│ ├─ platform/ ← UWB/IMU/band services, MofeEngineHost, netcode bring-up
│ └─ data/ ← MeshRepository (roster + shared state)
├─ design/ ← design-decision records (history/rationale; complements docs/)
└─ docs/architecture/ ← you are here
```
`common` is the reusable brain; `androidApp` supplies the hands (radios, sensors, screen) and
the on-device assembly. iOS (`iosApp`) is scaffolded but out of scope.
## Glossary
| Term | Meaning |
|------|---------|
| **MOFE** | Multi-Observer Fusion Engine — the positioning pipeline (`common/engine`). |
| **MMNF** | The mesh networking stack: data/control planes, Raft, registry, AEAD. |
| **Band / BroadcastBand** | Always-on UDP-broadcast gossip layer that bootstraps the MMNF stack. |
| **Constellation** | The rigid set of anchor positions that defines the shared frame. |
| **Reference anchor / point** | A device with a known frame position the solver trusts (root + placed anchors). |
| **Quorum** | ≥3 anchors → replicated frame + failover (`MeshOperatingMode.QUORUM`). |
| **Netcode** | The mesh-time + interpolation + event-arbitration seam (default OFF). |
| **Predicate** | An authored condition — a geometric region or a "point-at" gesture — that fires mesh events. |
| **Uplink** | The leader's WebSocket to tome-server (telemetry + commands). |
| **Self-seed** | Bootstrapping a mobile client's self-EKF from the anchor-broadcast solved position. |
| **ZUPT / ZARU** | Zero-velocity / zero-angular-rate updates that de-bias the EKF at rest. |
docs/architecture/01-positioning-engine.md
# 01 · The MOFE Positioning Engine
> **Layer** `common/engine` (+ `platform/MofeRuntime.kt`) · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `common/src/commonMain/kotlin/com/aether/mofe/engine/**`, `common/…/platform/MofeRuntime.kt`, `common/…/model/{MeasurementTypes,StateTypes,ConfigTypes}.kt`
> **Related** — [00-overview](00-overview.md) · [02-mesh-and-consensus](02-mesh-and-consensus.md) · [04-netcode-and-time](04-netcode-and-time.md) · [07-client-app-and-rendering](07-client-app-and-rendering.md)
MOFE (**M**ulti-**O**bserver **F**usion **E**ngine) is the pure-logic, platform-agnostic
brain. Hardware flows in as `RawRangingMeasurement` + `ImuSample`; a per-target `FusedState`
(position, velocity, orientation, per-axis σ) flows out. It has two clocks: a **per-measurement
pipeline** (fast) and a **maintenance loop** (~5 s) that owns the anchor **constellation** —
the shared frame everything else solves against.
## Assembly & runtime
- **`MofeBuilder`** (`engine/MofeBuilder.kt`) — fluent build; requires `.frameManager(CoordinateFrameManager)` + `.clock(PlatformClock)`; folds per-stage config into one `MofeConfig` (`model/ConfigTypes.kt`).
- **`MofeRuntime`** (`platform/MofeRuntime.kt`) — the glue that subscribes `PlatformProvider` hardware flows and pumps the engine. `start(selfDeviceId, isRootAnchor=false)` launches five collectors: **UWB ranging** → `processRanging`, **IMU** → `processImu`, **peer observations** → `processRanging`, **maintenance** (5 s) → `performMaintenance`, **telemetry** (1 s) → `publishTelemetry`. `trackTarget`/`untrackTarget` open/close UWB sessions. A self target is always registered (fed by IMU + peers, no self-session — a device can't range itself).
- **`MultiObserverFusionEngine`** (`engine/MultiObserverFusionEngine.kt`, ~2100 lines) — the orchestrator. Owns the mesh-wide stages + one `TargetContext` per tracked device (its own `ImuFusionEKF`, TWR trigger, counters). Fans results to many `MofeListener`s and a `Flow<MeshEvent>`.
## The per-measurement pipeline
```mermaid
flowchart LR
RAW["RawRangingMeasurement<br/>(anchor,target,dist,az?,el?)"] --> S1
IMU["ImuSample"] --> S4
subgraph P["runFullPipeline (per tracked target)"]
S1["1 · MeasurementPreFilter<br/>2-state 1-D Kalman, 3σ NLOS gate"]
S2["2 · CrossAnchorFuser<br/>inverse-variance combine, triangle check"]
S3["3 · MultilaterationSolver<br/>Gauss-Newton + LM, covariance, GDOP"]
S4["4 · ImuFusionEKF<br/>15-state ES-EKF"]
S5["5 · TwrVerificationTrigger<br/>+ EventDetector / BehaviorEngine"]
S1 --> S2 --> S3 --> S4 --> S5
end
S3 -. GDOP .-> GDOP["GdopAnalyzer"]
S4 --> OUT["FusedState → listeners + frameManager"]
S5 --> EV["MeshEvent / TwrVerificationDecision"]
REF{{"target IS a reference anchor?"}} -. "yes: skip 2·3,<br/>feed known position (σ≈5cm)" .-> S4
```
| Stage | Class (`engine/…`) | In → Out |
|-------|--------------------|----------|
| 1 · Pre-filter | `MeasurementPreFilter` | `RawRangingMeasurement` → `PreFilteredMeasurement` (smooths ±10 cm→±5–7 cm; soft NLOS reject) |
| 2 · Cross-anchor fuse | `CrossAnchorFuser` | `List<AnchorObservation>` → `FusedDistanceSet` (keeps N constraints; inflates triangle-inequality violators ×10) |
| 3 · Multilateration | `MultilaterationSolver` | `FusedDistanceSet` + reference points → `PositionSolution` (position, covariance, GDOP=√trace P, residualRms) |
| 4 · IMU/UWB EKF | `ImuFusionEKF` | `PositionSolution` + `ImuSample` → **`FusedState`** |
| 5 · Verify + events | `TwrVerificationTrigger`, `EventDetector`, `BehaviorEngine` | `FusedState` → `TwrVerificationDecision`, `MeshEvent` |
**Reference short-circuit:** an anchor/root is seen by only N−1 peers and can never reach
`minAnchorsForFusion`, so when the target *is* a reference point the engine skips stages 2–3
and feeds the EKF its known frame position (`REFERENCE_ANCHOR_VARIANCE=0.0025`, σ≈5 cm).
Steady-state entry points on the engine: `processRanging` (stage 1 → collect → gate on
`canSolvePositions` → `runFullPipeline`), `processImu` (EKF predict + gravity), `processInterAnchorRanging`
(EMA-accumulate inter-anchor distances for the constellation + drift detector), `performMaintenance`
(reap → self reference → antenna cal → constellation bootstrap → periodic refine).
## The EKF — 15-state error-state filter (`ImuFusionEKF.kt`)
Error state `δx = [δp, δv, δθ, δba, δbg]` (position, velocity, orientation-error, accel-bias,
gyro-bias), kept separate from the nominal state and injected-then-reset each step. Measurement
model `H = [I₃ | 0₁₂]`.
```mermaid
flowchart TB
subgraph PRED["predict() — IMU rate (~100 Hz)"]
A["inject δx → nominal"] --> B["bias-correct IMU"]
B --> C["to world, remove gravity<br/>aW = R·a + G_WORLD"]
C --> D["integrate p,v,q"]
D --> E["P = F·P·Fᵀ + Q"]
E --> F{"stationary?<br/>(raw accel/gyro gate)"}
F -- yes --> Z["ZUPT (pin v≈0)<br/>ZARU (observe gyro bias → kill yaw drift)"]
F -- no --> G["clamp covariance/state"]
Z --> G
end
subgraph UPD["update() — UWB rate (~10 Hz)"]
H["innovation y = z − Hx"] --> I{"Mahalanobis χ²(3)<br/>≤ 7.815 ?"}
I -- reject --> J["count; force-accept after 3"]
I -- accept --> K["K = P·Hᵀ·S⁻¹<br/>(block accel-bias while moving)"]
J --> K
K --> L["δx += K·y ; P = (I−KH)P"]
end
subgraph ABS["absolute attitude side-updates"]
M["updateGravity → roll/pitch vs +Z"]
N["updateBearing (UWB AoA) → yaw"]
end
PRED -->|between fixes| UPD --> PRED
M -.-> PRED
N -.-> UPD
```
Key behaviours: **stationarity** is detected on **raw** IMU only (a single loud sample releases
instantly); **ZARU on by default**, **ZUPT armed at runtime** (trust-gated by asserted motion
mode); the **Mahalanobis gate** rejects UWB outliers but force-accepts after
`maxConsecutiveRejections=3` to escape deadlock; divergence **clamps** let a pickup/spin recover
instead of running to 1e28. Absolute orientation is observable via gravity (roll/pitch) and UWB
AoA to a known anchor (yaw) — see `AoaCalibration`.
## The anchor constellation — establishing, locking, re-solving the frame
The constellation is **distance-only authoritative** (phone UWB elevation is unusable — ±90° on
a flat table — so AoA is used only for orientation, never placement).
- **Bootstrap** `AnchorMeshBootstrap.bootstrap` — closed-form trilateration cascade (root at
origin → a1 on +X → a2 in XY → a3 in 3D → multilaterate the rest) with a **deterministic gauge**
(a1/a2/a3 chosen in id-sorted order, else the frame flips every tick).
- **Refine** `AnchorConstellationSolver.refine` — rigid-body LM fit minimizing
`Σ(‖pᵢ−pⱼ‖−dᵢⱼ)² + λ·Σ‖pᵢ−pᵢ⁰‖²`; root pinned (translation gauge), small prior (rotation gauge),
`seedOutOfPlaneIfCoplanar` escapes the elevation-less flat stationary point.
- **Orient + level** `ConstellationOrientation` + `FrameGravity` — gravity-up → world +Z, fit yaw
from AoA, resolve the mirror (both chiralities fit; lower-residual wins). Two-tier leveling:
a genuinely raised anchor (Tier 1) or a best-fit plane normal + `UpSignAccumulator` vote (Tier 2).
- **Authority** `engine.bootstrapReferenceConstellation()` runs every maintenance tick, consumes
antenna-delay-corrected distances, and **overwrites** stored positions.
### Frame-lock state machine (`ConstellationFrameState`)
```mermaid
stateDiagram-v2
[*] --> CONVERGING
CONVERGING --> LOCKED: latch gates pass
LOCKED --> RESOLVING_MOTION: reference anchor MOVING (self ZUPT / peer assertion)
LOCKED --> RESOLVING: anchor dropped / re-settling
RESOLVING_MOTION --> LOCKED: settled + re-latched
RESOLVING --> LOCKED: settled + re-latched
RESOLVING --> CONVERGING: reference set lost
note right of LOCKED
Latch gates (accuracy > speed):
oriented + full set + not-in-motion
+ complete (not fallback)
+ well-fit (residRMS ≤ 0.06 m)
+ non-degenerate (minSep ≥ 0.10 m)
+ stable (signature agrees 3 ticks)
end note
```
`LOCKED` means the frame is held — `refineAnchorConstellation` becomes a no-op, so anchors stop
jittering. A **motion override** (`anyReferenceAnchorInMotion`) releases the lock the instant a
reference anchor reports movement, tracks it, then re-latches. This machine sits under the mesh's
`MeshOperatingMode` (`UNINITIALIZED→CALIBRATING→PRE_QUORUM→QUORUM⇄DEGRADED`, in `CoordinateFrameManager`);
`canSolvePositions` = `refs ≥ 4`.
## Calibration
| Concern | Class | What it does |
|---------|-------|--------------|
| **Antenna delay** | `AntennaDelayCalibrator` | Per-device range bias from surveyed truth (`d ≈ ‖pᵢ−pⱼ‖ + bᵢ + bⱼ`, least-squares); engine subtracts `bᵢ+bⱼ` from every inter-anchor range before solving. |
| **AoA convention** | `AoaCalibration` | Live-tunable (az,el)→body mapping for yaw; `AoaBoresight` default **`Z_NEG`** (screen-normal out the back). |
| **AoA auto-cal** | `AoaAutoCalibrator` | User aims the lens (body −Z) at a known target; brute-forces the convention that maps measured AoA onto the wand. "NO FIT" ⇒ AoA unreliable / frame fault, not convention. |
| **Frame / device** | `CoordinateFrameManager` | Canonical frame, topology, quorum; `initializeAsRoot`, `registerReferenceAnchor`, `calibrateDevice` (with degeneracy pre-check), `repairReferencePosition`, `reestablishFrame`/`relevelFrame`. |
| **Survey truth** | `MeshGroundTruth` | Operator-entered surveyed poses feeding antenna-delay cal. |
## Self-localization, self-seed & render-readiness
- **`maintainSelfReferenceAnchor()`** keeps a reference anchor's self-EKF live so its own IMU can
flag a bump (the frame-lock motion signal).
- **`seedSelfFromExternalPosition(position, var)`** — the **client self-seed**: a mobile client's
UWB only ranges peers, so its self-EKF is fed the **anchor-broadcast solved position** (what the
anchors compute for it) as a `PositionSolution`; IMU predict smooths between band-rate updates.
`σ≈20 cm`. No-op if this device is itself a reference point.
- **`MeshRenderReadiness.evaluate(engineStarted, meshSolved, referenceCount, selfLocalized)`**
(`engine/render/`) → `INITIALIZING → ACQUIRING_MESH → ACQUIRING_SELF → READY`; `canRenderSpatial =
(state==READY)`. **`MeshDisplayPolicy.decide(isAnchorOrRoot, readiness, autoJoinBringup)`** →
`MeshSurface {SPATIAL_SCENE, HOLDING, DIAGNOSTICS}`: a client HOLDs before READY (never shows
unsolved locations), an anchor/root sees DIAGNOSTICS (that pre-quorum state is what its operator
watches). Consumed by the UI in [07](07-client-app-and-rendering.md).
## Diagnostics the engine emits
`MofePipelineTrace` (opt-in flight recorder taps every stage boundary, no-op by default),
`getTelemetryFor(target)` → `MofeTelemetryRecord` (~1 Hz gauges), `getDiagnostics(target)` →
`TargetDiagnostics` (counter snapshot), and health reports `interAnchorLinkReport()` /
`constellationGeometryReport()` / `antennaDelayCalibration()`. These are the values the UI's
diagnostics pane and the server telemetry tiers consume.
docs/architecture/02-mesh-and-consensus.md
# 02 · Mesh & Consensus
> **Layer** `common/messaging` (+ `messaging/raft`) · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `common/…/messaging/{MeshCoordinator,MeshStateRegistry,MeshNode}.kt`, `common/…/messaging/raft/**`, `common/…/model/mesh/DeviceContract.kt`, `common/…/model/StateTypes.kt`
> **Related** — [03-networking-and-protocols](03-networking-and-protocols.md) · [04-netcode-and-time](04-netcode-and-time.md) · [05-security-and-trust](05-security-and-trust.md)
The mesh agrees on **one replicated truth** — who the nodes are, where the frame is, which
predicates exist — using **Raft**. `MeshCoordinator` is the wiring hub; `RaftModule` is the
consensus core; `MeshStateRegistry` is the replicated key-value store. This doc is the "how the
mesh agrees" layer; the wires it runs over are [03](03-networking-and-protocols.md).
## MeshCoordinator — the wiring hub (`messaging/MeshCoordinator.kt`)
Wires messaging ⇄ engine ⇄ Raft ⇄ netcode. `isLeader` is **derived from Raft**, never assigned.
- Constructs `RaftModule(onRoleChange→onRaftRoleChange, onCommit→onCommitted)`.
- Owns the mesh clock + netcode seam: `meshClock`, `netcode = NetcodeSession(clock=meshClock)`,
`claimTracker`, `predicateShapes` (COW cache for arbitration).
- **Single side-effect path:** `registry.deltas.collect { applyLocalSideEffects(it) }` runs for
**both** roles (voters via `RaftModule.applyCommitted`, learners via re-broadcast deltas). Never
call `applyLocalSideEffects` from anywhere else.
- **Single write path:** `write(vararg ops) = raft.propose(ops)`.
- `startVoter(initialVoters, bootstrapAsLeader)` (leader/anchor path) vs `startAsMember(leader,…)`
(member/learner path).
- `onRaftRoleChange`: on becoming LEADER → **bump epoch** (incarnation fence), write the frame
record, **rotate the group key** + broadcast it, start the uplink; on losing it → stop uplink.
Assembly: **`MeshNode.build(deps).start()`** wires transports → security wrappers → hub → registry
→ group keys → `EventNotifier` → coordinator, and dispatches on `MeshNodeRole`.
## Raft (`messaging/raft/RaftModule.kt`, `RaftTypes.kt`)
Roles `FOLLOWER, PRE_CANDIDATE, CANDIDATE, LEADER, LEARNER`. Config: heartbeat 1000 ms, election
timeout 3000–5000 ms, `snapshotEvery=1024`.
```mermaid
stateDiagram-v2
[*] --> FOLLOWER
FOLLOWER --> PRE_CANDIDATE: election timeout
PRE_CANDIDATE --> FOLLOWER: pre-vote fails / leader fresh
PRE_CANDIDATE --> CANDIDATE: pre-vote majority (no term bump yet)
CANDIDATE --> LEADER: vote majority → append no-op
CANDIDATE --> FOLLOWER: higher term seen
LEADER --> FOLLOWER: higher term seen
LEARNER --> LEARNER: receives committed deltas (non-voting)
note right of PRE_CANDIDATE
Pre-Vote (§9.6): probe without
bumping term → no disruption from
a partitioned node. Grant needs
log-up-to-date AND leader-not-fresh
(leader stickiness).
end note
```
**Log ≡ registry revision.** `LogEntry(term, index, delta)` with the invariant that log index
equals registry revision (`StateDelta.prevRevision = index−1`). Commit = median `matchIndex` among
voters, **current-term entries only**; `applyCommitted` applies **every** entry including the
leader's no-op (skipping it would desync revision from index and silently wedge later deltas).
`propose(ops)` suspends until majority-commit; throws if not leader (callers redirect to `leaderId`).
**Catch-up:** `AppendEntries` does the consistency check + conflict truncation + accelerated
backoff; a follower that falls below the leader's log base gets a **`RaftInstallSnapshot`** (a full
`MeshSnapshot`) and compacts.
**Persistence** saves **only `currentTerm` + `votedFor`** (the log is *not* persisted on mobile — a
restarted node rejoins as follower and catches up via snapshot+append). ⚠ `InMemoryRaftPersistence`
is volatile (root bootstrap + tests); production needs the durable `AndroidRaftPersistence` actual.
## Replicated state registry (`messaging/MeshStateRegistry.kt`)
A revisioned KV store under a mutex. `applyDelta` → `Applied` / `Gap(expected,got)` (member then
requests a snapshot) / `AlreadyApplied`; `applySnapshot` replaces the store and emits one synthetic
delta so watchers re-evaluate. Keys (`RegistryKeys`): `mesh/mode`, `mesh/frame`, `mesh/voters`,
`mesh/fit`, `nodes/{id}`, `predicates/{id}`, `composites/{id}`, `tracking/{id}`, `roles/requests/{id}`,
`mesh/revoked/{id}`. Values are the `RegistryValue` sealed set (`NodeRecord`, `PredicateRecord`,
`VoterSetRecord`, `FrameRecord`, …).
```mermaid
flowchart TB
P["client: coordinator.write(ops)"] --> RP["raft.propose"]
RP --> C{"majority commit?"}
C -- yes --> AC["RaftModule.applyCommitted"]
AC --> V["voters: registry.applyDelta"]
AC --> OC["MeshCoordinator.onCommitted"]
OC --> L["learners: broadcastControlTo(learnerIds)"]
OC --> U["uplink.enqueueDelta → server"]
L --> LR["learner: registry.applyDelta"]
V --> W["registry.deltas (single watcher)"]
LR --> W
W --> SE["applyLocalSideEffects<br/>(engine, netcode, mode, UI)"]
LR -. "Gap detected" .-> SN["request Snapshot → applySnapshot"]
```
## Roles, quorum & operating mode
Three role vocabularies (see [00](00-overview.md)); the one that gates rendering is
**`MeshOperatingMode`** (`model/StateTypes.kt`):
```mermaid
stateDiagram-v2
[*] --> UNINITIALIZED
UNINITIALIZED --> PRE_QUORUM: root anchor + mesh points (single frame)
PRE_QUORUM --> QUORUM: ≥3 anchors (replicated frame + failover)
QUORUM --> DEGRADED: dropped below 3 anchors
DEGRADED --> QUORUM: anchor recovered / re-added
```
**Join → quorum:** the root does `startVoter(setOf(self), bootstrapAsLeader=true)` — a majority of
one self-elects, giving PRE_QUORUM. A joiner does `startAsMember` → sends `NodeHello` → leader
registers the link, proposes a `NodeRecord` (Raft), replies a **full snapshot** (one-round-trip
convergence), and starts a lease (`heartbeat × leaseMissesBeforeExpiry`). Anchors become Raft
**voters** when the leader commits `mesh/voters` (`VoterSetRecord` → `raft.updateVoters`, then the
app calls `promoteToVoter()`). Silent members are reaped by `expireLeases` (emits `LEASE_EXPIRED` +
`engine.removeAnchor`).
```mermaid
sequenceDiagram
participant M as Joiner (member)
participant L as Leader (root/anchor)
M->>L: NodeHello (control :47475, epoch-exempt)
L->>L: registerLink + raft.propose(NodeRecord)
L-->>M: MeshSnapshot (full registry, one round-trip)
loop lease
M->>L: NodeHeartbeat
L->>L: renew lease (else LEASE_EXPIRED → removeAnchor)
end
Note over L: on commit of mesh/voters → promote joiner to Raft voter
```
The render gate then opens only at **`QUORUM` + self-localized** — see
[01 · render-readiness](01-positioning-engine.md) and its UI consumption in
[07](07-client-app-and-rendering.md).
docs/architecture/03-networking-and-protocols.md
# 03 · Networking & Wire Protocols
> **Layer** `common/messaging` + `androidApp/platform/band` · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `common/…/messaging/{MeshMessagingService,MeshCodec,Transports,KtorTransports,SequenceGate}.kt`, `common/…/model/messaging/**`, `androidApp/…/platform/band/**`, `androidApp/…/platform/netcode/NetcodePeerBook.kt`
> **Related** — [02-mesh-and-consensus](02-mesh-and-consensus.md) · [04-netcode-and-time](04-netcode-and-time.md) · [05-security-and-trust](05-security-and-trust.md) · [06-server-and-telemetry](06-server-and-telemetry.md)
## Two stacks
There are **two** device-to-device networks, and conflating them is the most common onboarding
mistake:
```mermaid
flowchart LR
subgraph BAND["BroadcastBand — always on (androidApp)"]
direction TB
B["UDP broadcast :47600<br/>CBOR CRDT gossip (~750 ms)"]
end
subgraph MMNF["MMNF stack — gated (common)"]
direction TB
D["Data · UDP :47474"]
C["Control · TCP :47475"]
U["Uplink · WebSocket → server"]
end
B -->|"NetcodePeerBook.observe(nodeId, sourceIP)"| PB["PeerAddressBook<br/>→ control :47475"]
PB --> C
B -. "presence · UWB addrs · kinematics" .-> APP["roster / UI / engine"]
```
- **BroadcastBand** (`platform/band/BroadcastBand.kt`) — offline-first, runs today. It does
**presence, discovery, UWB-address exchange, and peer-IP discovery** — not fabricated ranging
(real position comes from UWB + MOFE). Every inbound datagram hands `NetcodePeerBook` the peer's
**source IP** (which the payload never carries), and `PeerAddressBook` maps it to the control port.
- **MMNF** (`messaging/**`) — the real transport once bootstrapped: UDP data, TCP control, WS uplink,
Raft, AEAD. Comes up only when trust material + a leader IP are known (see
[05](05-security-and-trust.md)).
## MeshMessagingService — the hub (`messaging/MeshMessagingService.kt`)
A pure hub: it stamps `sequence`+`meshEpoch` outbound, applies the `StalenessGate` inbound, and
demultiplexes payloads into typed hot flows. **No business logic** lives here.
### Plane / port / transport map
| Plane | `MeshChannel` | Port (`MessagingConfig`) | Transport | Direction | Delivery |
|-------|---------------|--------------------------|-----------|-----------|----------|
| **Data** | `RANGING` | `dataPort=47474` UDP | `DatagramTransport` (`KtorDatagramTransport`) | fan-out to all peers | best-effort, latest-wins, seq-gated |
| **Control** | `CONTROL` | `controlPort=47475` TCP | `ControlTransport`/`ControlLink` (length-prefixed) | leader ⇄ members | reliable, ordered |
| **Uplink** | `UPLINK` | WebSocket → server | `UplinkClient` (Ktor WS) | leader → server | at-least-once, store-and-forward |
`MessagingConfig`: `dataPort`, `controlPort`, `heartbeatIntervalMillis=1000`,
`leaseMissesBeforeExpiry=3`, `rangingPublishHz=10`, `fusedSamplePublishHz=10`,
`timeSyncIntervalMillis=5000`, **`netcodeBringUp=false`** (kill-switch, [04](04-netcode-and-time.md)).
**Typed inbound flows** (subscribe-side `SharedFlow`s): `rangingBatches`, `fusedSamples` (data);
`events`, `stateDeltas`, `snapshots`, `hellos`, `heartbeats`, `snapshotRequests`, `commands`,
`resyncNeeded`, `raftInbound` (→ RaftModule), `groupKeys` (→ GroupKeyManager), `eventClaims`,
`eventVerdicts`, `timeSyncResponses`. **Publish API:** `publishRanging` (datagram fan-out);
`sendControl`/`broadcastControl`/`sendControlTo` (control); `registerLink`/`updatePeers`.
**StalenessGate** (`SequenceGate.kt`): verdicts `ADMIT` / `DROP_STALE` (older epoch or non-increasing
RANGING seq) / `RESYNC` (newer epoch → trigger snapshot). **Sequence is enforced only on `RANGING`**
(CONTROL is TCP-ordered). `NodeHello` is **epoch-exempt** — a joiner hasn't learned the epoch yet.
## Serialization — `MeshCodec` + the envelope
- **CBOR** is the mesh wire format (`Cbor { ignoreUnknownKeys = true }`); `decodeOrNull` **never
throws** (a malformed datagram must not kill a collector loop). Handshake frames are **bare
`MeshPayload`** (pre-session, no envelope). **JSON** is used for the uplink + credential parsing.
- **`encodeCredentialBody`** is a hand-rolled **canonical JSON** (sorted keys, Python `ensure_ascii`
escaping) — a byte-for-byte cross-repo contract with tome-server's `gateway/ca.py::_canonical`
([05](05-security-and-trust.md) · [06](06-server-and-telemetry.md)).
```mermaid
flowchart LR
PL["MeshPayload<br/>(sealed, @SerialName)"] --> ENV["MessageEnvelope<br/>schemaVersion·messageId·sourceNodeId·<br/>channel·sequence·sentAtMicros·meshEpoch·payload"]
ENV -->|CBOR| SEC{{"secure wrapper"}}
SEC -->|"control"| SL["SecureControlLink AEAD<br/>(per-link keys)"]
SEC -->|"data"| SD["SecureDatagramTransport<br/>keyId‖senderHash‖seq‖AEAD"]
SL --> TCP["TCP :47475"]
SD --> UDP["UDP :47474"]
```
`MessageEnvelope`: `schemaVersion=1`, `messageId` (UUID, uplink dedup key), `sourceNodeId`,
`channel`, `sequence` (monotonic per source+channel), `sentAtMicros`, `meshEpoch` (incarnation
fence), `payload`. Producers never touch the guard fields — the hub's `envelope()` fills them.
**Wire payload catalog** (`MeshPayload` sealed interface, polymorphic by `@SerialName`):
| Group | File | Members |
|-------|------|---------|
| Data | `MeshPayloads.kt` | `RangingBatch`, `FusedStateSample` |
| Control/membership | `MeshPayloads.kt` | `NodeHello`, `NodeHeartbeat`, `StateDelta`, `SnapshotRequest`, `MeshSnapshot`, `EventNotification`, `TimeSync`, `MeshCommand`, `Ack` |
| Raft | `RaftMessages.kt` | `LogEntry`, `RaftRequestVote`, `RaftVote`, `RaftAppend`, `RaftAppendReply`, `RaftInstallSnapshot` |
| Registry values | `RegistryTypes.kt` | `RegistryOp` (Put/Delete/RoleRequest/RoleDecision), `RegistryValue.*`, `NodeDiag` |
| Security | `SecurityMessages.kt` | `DeviceCredential`, `SecHelloInit/Ack/Done`, `GroupKeyDistribution` |
| Netcode | `NetcodeWirePayloads.kt` | `WireEventClaim`, `WireEventVerdict` |
| Fragments | `WireFragments.kt` | `WireObservation`, `WireFusedSample` (incl. `orientation`) |
## The UWB band in detail (`platform/band/`)
`BroadcastBand` gossips `BandNodeRecord`s (`BandProtocol.kt`) — LWW-merged by Lamport clock in
`SharedMeshState` (a CRDT). A record carries: identity (`nodeId`, `displayName`, `deviceType`,
`meshRoles`, `ed25519Pub`, `x25519Pub`), honest world-frame `position`/`velocity`, a **peer motion
assertion** (`motionMode` — peers gear-shift ZUPT on a `STATIONARY` assertion), **UWB addresses**
(`uwbControllerAddr`/`uwbControleeAddr` + per-peer `uwbLinks`/`uwbAck` rendezvous), live
`observations` (cross-anchor fusion input), and the anchor's `solved` position map. Frames:
`Beacon`, `NodePut/Delete`, `Snapshot`, `Hello`, `MeshMetaPut`, `JoinRequestPut`, `AnchorInvitePut`,
`SealedKeyPut`, `PredicatePut/Delete`, `CompositePut/Delete`, `Event`.
**Band security:** discovery/admission frames stay plaintext; member frames are AEAD-sealed under a
mesh **group key** the owner X25519-seals per admitted device (`SealedKeyPut`); `lifecycleVerifier`
checks owner Ed25519 signatures before any CRDT state is touched (details in
[05](05-security-and-trust.md)).
docs/architecture/04-netcode-and-time.md
# 04 · Netcode & Time
> **Layer** `common/engine/netcode` (+ `MeshCoordinator` seam) · **Status** stable, **default OFF** · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `common/…/engine/netcode/**`, `common/…/messaging/MeshCoordinator.kt` (`emitClaims`, init gate), `androidApp/…/platform/netcode/NetcodeBringUp.kt`
> **Related** — [01-positioning-engine](01-positioning-engine.md) · [02-mesh-and-consensus](02-mesh-and-consensus.md) · [07-client-app-and-rendering](07-client-app-and-rendering.md)
Netcode makes the mesh feel **smooth and consistent in time**: it puts every device on one mesh
clock, buffers per-device history so you can ask "where was peer X at mesh-time T?", interpolates
10 Hz solves into a 60 Hz render, and adjudicates **events** (region crossings, aim gestures) with
a leader-authoritative rewind so optimistic client predictions can be confirmed or rolled back.
> ⚠ **Ships OFF.** `MessagingConfig.netcodeBringUp` defaults `false`. The `NetcodeSession` is still
> *constructed* (render/aim APIs return empty, never null), but the live seam — feeding samples,
> the claim/verdict transport, `emitClaims` — does not run, so a bring-up defect cannot regress
> ranging / solve / Raft. Flip it on per-device to go live; flip it off to revert without a rebuild.
## Components
| Type | File | Responsibility |
|------|------|----------------|
| `NetcodeSession` | `NetcodeSession.kt` | Holder: per-device `TemporalStateBuffer`, the `MeshClock`, reject-rate + quality windows. `recordSample`, `renderPose`, `renderPositions(nowMesh)`, `arbitrate`. Everything in **mesh time**. |
| `NetcodeAdapter` | `NetcodeAdapter.kt` | Feeds the session from `fusedSamples` + TimeSync; `onFusedSample` records a `TemporalPose`. |
| `NetcodeTransport` | `NetcodeTransport.kt` | Leader-authoritative (NOT Raft) claim/verdict routing + domain↔wire mapping. |
| `MeshClock` | `MeshClock.kt` | NTP 4-timestamp estimator: `offset=((t2−t1)+(t3−t4))/2`, best = lowest-RTT sample; `toMeshTime`/`toLocalTime`. |
| `TemporalStateBuffer` | `TemporalStateBuffer.kt` | Per-device ring buffer; `resolve(T)` → `Resolved(pose, Fidelity, age)` with `Fidelity {EXACT, INTERPOLATED, EXTRAPOLATED, CLAMPED, EMPTY}`; lerp/**slerp**; 1 s retention, 120 ms max extrapolation. |
| `RenderClock` | `RenderClock.kt` | Pure: `queryMicros = latestSampleMesh + clamp(nowWall − latestSampleWall, 0, max)` so a 60 Hz view samples **between** 10 Hz solves (kills step-function jitter). |
| `EventClaimTracker` | `EventClaimTracker.kt` | Observer-side optimistic predictions keyed by `eventId`: `track`(PENDING) → `onVerdict`(CONFIRMED/ROLLED_BACK). |
| `MofeNetcodeBridge` | `MofeNetcodeBridge.kt` | Maps engine `MeshEvent` → `EventClaim`(s). |
| `GeometricTransition` | `GeometricTransition.kt` | Rewindable, time-parameterized crossing detection; `Kind {CROSS,ENTER,EXIT,APPROACH,AIM}`. |
| `EventLifecycle` | `EventLifecycle.kt` | `EventClaim`, `EventVerdict`+`Reason`, `ReconcilePolicy` (maxLag 250 ms, timeTol 80 ms, reeval 120 ms), pure `NetcodeEvents.reevaluate/reconcile/applyVerdict`, content-addressed `eventId`. |
| `MeshAimTracker` | `MeshAimTracker.kt` | Continuous per-client aim tracking → `Track(deviceId, aimErrorRadians, reticleOffset, onReticle)`. |
## Time sync
```mermaid
sequenceDiagram
participant M as Member
participant L as Leader
M->>L: TimeSync (t1)
L->>L: stamp t2 (recv), t3 (send)
L-->>M: TimeSync (t1,t2,t3)
M->>M: t4 (recv) → offset=((t2−t1)+(t3−t4))/2, rtt
Note over M: keep lowest-RTT sample → meshClock.observe
```
Fused samples are stamped `timestamp + meshOffsetMicros` (`FusedState.toWireSample`) so every
device's history lands on one axis. The render path then advances a **wall-clock** query between
solves via `RenderClock`, so motion is smooth even though solves arrive at 10 Hz.
## Event arbitration — optimistic fire, leader rewind, verdict
```mermaid
sequenceDiagram
participant O as Observer (any device)
participant T as EventClaimTracker
participant L as Leader (arbitrator)
participant A as All devices
Note over O: engine fires MeshEvent
O->>O: EventNotifier optimistic local fire
O->>T: track(claim) = PENDING
alt member
O->>L: sendControl(WireEventClaim)
else leader
O->>L: arbitrate locally
end
L->>L: rewind subject's TemporalStateBuffer to tEvent<br/>(needs EXACT/INTERPOLATED fidelity)
L->>L: re-detect via GeometricTransition + reconcile<br/>(lag/kind/time/direction)
L->>A: broadcastControl(WireEventVerdict)
A->>T: onVerdict → CONFIRMED or ROLLED_BACK
```
Verdict reasons: `ACCEPTED / STALE / NO_TRANSITION / KIND_MISMATCH / TIME_MISMATCH /
DIRECTION_MISMATCH`. This is *separate* from Raft — it's leader-authoritative low-latency
adjudication, not replicated log consensus. Geometric predicates and pointing predicates both flow
through here (see the predicate lifecycle in [09](09-workflows.md)).
## The kill-switch (`MeshCoordinator`)
`init` gates, as one unit, (a) folding `fusedSamples` into `netcodeAdapter` and (b)
`netcodeTransport.start`; `emitClaims` early-returns when off. On-device,
`androidApp/…/platform/netcode/NetcodeBringUp.maybeStart` is the idempotent constructor of the whole
`MeshNode`, gated on `MeshTrustReadiness.Ready` + a known leader IP + a site-local IPv4. Trust
material comes from `NetcodeTrust` ([05](05-security-and-trust.md)).
> **Onboarding note:** `NetcodeBringUp` and the UI wiring that consumes netcode are delivered as
> reviewed patches but are **not compiled in the headless env** (they need AGP). The `common`
> netcode logic *is* unit-tested (`:common:jvmTest`, incl. `NetcodeBringUpGateTest`).
docs/architecture/05-security-and-trust.md
# 05 · Security & Trust
> **Layer** `common/messaging/security` + `androidApp/platform/netcode` · **Status** stable (one TODO, noted) · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `common/…/messaging/security/**`, `common/…/messaging/MeshTrustMaterial.kt`, `common/…/model/messaging/SecurityMessages.kt`, `androidApp/…/platform/netcode/NetcodeTrust.kt`
> **Related** — [03-networking-and-protocols](03-networking-and-protocols.md) · [06-server-and-telemetry](06-server-and-telemetry.md)
The trust model is a single **Ed25519 mesh CA** (in tome-server) whose public key devices
**fetch-and-pin** over web-PKI TLS at startup. From that anchor hang: signed device credentials,
an authenticated link handshake, per-link AEAD, and a rotating group key for datagram encryption.
Device **private keys never leave the device**; the CA only ever signs over a supplied public key.
## The trust chain
```mermaid
flowchart TB
CA["tome-server Ed25519 mesh CA<br/>(gateway/ca.py)"] -->|"GET /api/v1/mesh/ca (TLS)"| PIN["NetcodeTrust: pin CA key<br/>(SharedPreferences, keyId rotation-aware)"]
CA -->|"mint over device pubkey"| CRED["DeviceCredential<br/>(canonical-JSON, Ed25519 sig)"]
PIN --> GATE["MeshTrustMaterial.evaluate<br/>→ MeshTrustReadiness"]
CRED --> GATE
GATE -->|Ready| HS["LinkHandshake (per control link)<br/>X25519 ECDH + Ed25519 auth"]
HS --> LK["SecureControlLink<br/>per-direction AEAD keys"]
LEADER["leader (per Raft term)"] --> GK["GroupKeyManager.rotate()<br/>32-byte group key"]
GK -->|"unicast, sealed on each link"| DIST["GroupKeyDistribution"]
GK --> SDT["SecureDatagramTransport<br/>keyId‖senderHash‖seq‖AEAD (UDP)"]
LK -. "distributes" .-> DIST
```
## Crypto suite (`MeshCryptoProvider`)
Ed25519 (RFC 8032) · X25519 (RFC 7748) · HKDF-SHA-256 (RFC 5869) · ChaCha20-Poly1305 AEAD
(RFC 8439). Android actual = `AndroidMeshCryptoProvider` (java.security / javax.crypto). Identity is
`DeviceIdentity(deviceId, ed25519Public, ed25519Private, credential)`.
## Device credential (`SecurityMessages.kt` ↔ server `gateway/ca.py`)
A **field-for-field, byte-for-byte** cross-repo contract. Signed fields: `deviceId`, `ed25519Pub`,
`deviceType`, `deviceName`, `deviceRoles`, `meshId`, `meshName`, `notAfter` (microseconds),
`claims` (e.g. `voterEligible`), `is_emulated`; then `signature` (base64 Ed25519 over the canonical
body, **excluding** `signature`). Canonicalization = sorted keys, compact separators, Python
`ensure_ascii` escaping (`MeshCodec.encodeCredentialBody` ≡ server `_canonical`). `deviceId =
uuid5(namespace 6f4b2e2a-2a5e-5c9e-9b7a-1f3d5e7a9c11, ed25519Pub)` — declared identically in both
repos; **changing the field set requires the same change on both sides or signatures stop
verifying** (a golden-vector test guards the drift). Server side in [06](06-server-and-telemetry.md).
## Link handshake (`LinkHandshake` + `SecureControlLink`)
A 3-message X25519+Ed25519 authenticated key exchange on the raw TCP link **before any envelope**:
`SecHelloInit → SecHelloAck` (sig over transcript₁) `→ SecHelloDone` (sig over transcript₂). Both
sides verify the peer credential's **CA signature**, its **expiry**, and its **revocation**
(registry `mesh/revoked/{id}`). Directional AEAD keys derive `HKDF(x25519secret, salt=nonceM‖nonceL,
info="mmnf/link/v1"‖transcript₁, 64 B)`. `SecureControlLink` then AEAD-frames every control frame
(per-direction 64-bit counter nonce). **No downgrade path.**
## Group key & datagram encryption (`GroupKeyManager` + `SecureDatagramTransport`)
The leader `rotate()`s a 32-byte group key **per Raft term** and distributes
`GroupKeyDistribution` **unicast on each already-sealed control link** (so distribution is itself
sealed per member). Per-sender subkey `senderKey = HKDF(groupKey, info="mmnf/data/v1"‖senderNodeId)`.
Each datagram is framed `keyId(4)‖senderIdHash(8)‖seq(8)‖AEAD`, nonce `keyId‖seq`, AAD = the 20-byte
header. **Drops if no key — never sends plaintext.** The previous key stays valid for a grace window
across rotation; replays are caught by the `StalenessGate`.
## Trust readiness gate (`MeshTrustMaterial`, pure `common`)
`evaluate(...) → MeshTrustReadiness { Ready(credential), NoCaKey, NoCredential, DeviceMismatch,
MeshMismatch, Expired }` — checks the cached credential binds **this** device + the **active** mesh
and is unexpired. Offline-first: a mismatch/expiry means "re-enroll online," not a hard failure.
`NetcodeBringUp` will not construct a `MeshNode` until this returns `Ready` ([04](04-netcode-and-time.md)).
## Where each key lives
| Key | Location | Lifetime |
|-----|----------|----------|
| Mesh CA **public** key (anchor) | pinned in `NetcodeTrust` + SharedPreferences → `LinkHandshake` | until rotated (detected via `keyId`) |
| Device **Ed25519 private** | 32-byte seed (Android Keystore-backed) in `DeviceIdentity` | device lifetime |
| X25519 ephemeral | inside `LinkHandshake`, per connection | discarded after key derivation |
| Link AEAD keys | `SecureControlLink`, per connection + direction | connection |
| Group (data) key | in-memory `GroupKeyManager` (current+previous); band: X25519-sealed per member | one Raft term |
## Band admission (recap)
On the band ([03](03-networking-and-protocols.md)), discovery/admission frames are plaintext; member
frames are AEAD-sealed under the group key the owner **X25519-seals per admitted device**
(`SealedKeyPut`), and `lifecycleVerifier` checks the owner's Ed25519 signature before any CRDT state
is applied.
## Known gap to call out
`MeshOpSigning` (Ed25519 sign/verify for `RegistryOp.RoleDecision`) exists, but **verification is not
yet wired** — there is no mesh-admin-pubkey-by-deviceId directory in `common` (`TODO` in
`MeshStateRegistry.applyOps`), so a committed `RoleDecision` is currently trusted like any
Raft-committed op. Flagged so nobody assumes signed-role enforcement is live.
docs/architecture/06-server-and-telemetry.md
# 06 · Server & Telemetry (tome-server)
> **Layer** `tome-server` (separate repo) · **Status** stable (README there is stale — trust the code) · **Verified against** `bdf7e0f` (client) / tome-server `app/**`
> **Source of truth** — `tome-server/app/com/aether/tome/api/{routes,gateway,service,observability}/**`, `…/db/**`, `…/model/**`
> **Related** — [05-security-and-trust](05-security-and-trust.md) · [03-networking-and-protocols](03-networking-and-protocols.md)
tome-server is the cloud side: the mesh **certificate authority**, the **uplink** sink for a mesh
leader, a **telemetry** fan-out (Prometheus / InfluxDB / Loki), the server→mesh **command** plane,
and a web **console**. It is *not* in the ranging path — positions are solved on-device; the server
observes and administers.
> ⚠ The repo `README.md` is aspirational (Flask 2.3, JWT-in-localStorage, `/api/v1/meshes`). The
> **actual** code is Flask 3.1, session-cookie auth delegated to a separate `auth-api` service, and
> the mesh prefix is **`/api/v1/mesh`** (singular). Trust the paths below.
## Tech stack & components
Python 3.13 · Flask 3.1 (blueprints) · `flask-sock` WebSockets · gunicorn+gevent · **PostgreSQL**
(device/mesh/predicate records) · **pynacl** (Ed25519 CA) · **InfluxDB 2.x** / **Loki** /
**Prometheus** (telemetry) · session-cookie auth via external `auth-api`.
| Component | Path | Responsibility |
|-----------|------|----------------|
| App factory | `api/tome_server.py` | `create_app()`, blueprint registration, JWKS, `/metrics`, `/health` |
| Route blueprints | `api/routes/` | `mesh.py` (CA, enroll, credential, uplink WS, commands), `device.py`, `predicate*.py`, `user.py`, `discovery.py`, `agent.py`, … |
| **Gateway** | `api/gateway/` | `ca.py` (Ed25519 **MeshCA** minting), `credentials.py` (verify + possession), `frames.py` (uplink wire types), `hub.py` (`GatewayHub`: per-mesh mirror + fan-out + commands), `reconcile.py` |
| Observability | `api/observability/` | `metrics.py` (Prom), `influx_shipper.py`, `loki_shipper.py`, `events.py` |
| Services / DB | `api/service/**`, `db/**` | business logic + PostgreSQL repositories |
## Endpoints devices care about (`routes/mesh.py`)
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/v1/mesh/ca` | none | **Publish CA trust anchor** `{alg, meshCaPublicKey, keyId}` for fetch-and-pin |
| POST | `/api/v1/mesh/<m>/device/enroll` | cookie | Enroll a device by `ed25519Pub` (`deviceId=uuid5(ns,pub)`) |
| GET | `/api/v1/mesh/<m>/device/<d>/credential?pub=<b64>` | cookie (admin/owner) | **Mint** a CA-signed `DeviceCredential` |
| WS | `/api/v1/mesh/stream` | device-cred | **Leader uplink** (challenge→hello→verify→welcome, then `UplinkFrame` ingest) |
| PUT/POST | `/api/v1/mesh/<m>/{snapshot,events}` | device-cred | REST fallback for WS-less deployments |
| POST | `/api/v1/mesh/<m>/command` | cookie (mesh-admin) | Push a `ServerCommand` into the mesh |
| WS | `/api/v1/mesh/<m>/live` | cookie | Console live viewer (snapshot + deltas) |
Also unauthenticated by design: `/api/v1/agent/diag`, `/api/v1/agent/report` (device self-report),
`/api/v1/health`, `/api/v1/.well-known/jwks.json`, `/metrics`.
## CA & credential minting (`gateway/ca.py`)
`MeshCA.mint(...)` builds the credential body and appends a detached Ed25519 signature over the
**canonical JSON** (`_canonical`: `json.dumps(sort_keys=True, separators=(",",":"))`, `ensure_ascii`
default, `signature` excluded) — the byte-for-byte partner of the client's `encodeCredentialBody`
([05](05-security-and-trust.md)). Signer abstraction: `LocalSeedSigner` (dev, seed from
`MESH_CA_SEED_B64`) or `KmsSigner` (prod, **stub** — wire to HSM/KMS so key material never leaves).
`GET /mesh/ca` returns `keyId = sha256(rawKey)[:16]` so a client can pin and later detect rotation.
```mermaid
sequenceDiagram
participant D as Device (leader)
participant S as tome-server
D->>S: GET /api/v1/mesh/ca (TLS)
S-->>D: {alg:Ed25519, meshCaPublicKey, keyId}
Note over D: pin key (NetcodeTrust)
D->>S: POST /mesh/<m>/device/enroll (ed25519Pub) [cookie]
D->>S: GET /mesh/<m>/device/<d>/credential?pub=…
S->>S: verify caller = admin|owner; deviceId==uuid5(ns,pub)
S-->>D: DeviceCredential (CA-signed, canonical JSON)
Note over D: now usable for the uplink handshake + link handshake
```
## Leader uplink handshake (WS `/api/v1/mesh/stream`)
```mermaid
sequenceDiagram
participant L as Mesh leader
participant S as Server
S-->>L: {gw.challenge, nonce}
L->>S: {gw.hello, credential, nonceSignature}
S->>S: verify_credential() + verify_possession()
alt ok
S-->>L: {gw.welcome, meshId}
loop
L->>S: UplinkFrame (ALERT|STATE_DELTA|SNAPSHOT|TELEMETRY|TRACE|ACK)
S-->>L: ServerCommand / ACK
end
else bad
S-->>L: close 4401/4403 (or 4409 name conflict)
end
```
`verify_possession` requires `deviceId == uuid5(ns, ed25519Pub)` (closes key-substitution). On
welcome the server enrolls **only the connecting leader**; peers are reconciled/admin-gated, never
asserted from node records.
## Telemetry — three tiers (all from the leader uplink)
| Tier | Frame | Store | What |
|------|-------|-------|------|
| **Prometheus** | `TELEMETRY` | `/metrics` gauges | per-node signal/confidence/posErr/gdop/latencies/… (**positions never exported**) |
| **InfluxDB 2.x** | `TRACE` | `mofe` bucket, line protocol | `mofe_solve` (incl. `pos_*`, `vel_*`, `cov_*`, per-anchor rejects — **positions included** for accuracy eval), `mofe_lifecycle` |
| **Loki** | `ALERT` + events | log push | discrete mesh/membership/predicate/fleet events + HTTP access |
Both shippers are background, bounded-queue, **drop-oldest** — telemetry never backpressures ingest.
Timestamps use server ingest wall-clock (device clocks are boot-relative monotonic).
## Server → mesh commands (`frames.py`)
`POST /mesh/<m>/command` pushes a `ServerCommand` (`PREDICATE_PUT/DELETE`, `TRACK/UNTRACK_TARGET`,
`RECALIBRATE`, `SET_ROLES`, `ADD/REMOVE_NODE`, `EJECT_USER`, …) with a pending-ACK (10 s). The leader
applies it (via Raft where it mutates replicated state) and replies `ACK`.
## Gaps to know
Server-side **revocation table** is a TODO (design defers to the mesh's replicated
`mesh/revoked/{id}`); `KmsSigner` is a stub; `ADD/REMOVE_NODE` Raft-membership push is stubbed. The
whole gateway is inert unless `mesh_gateway_enabled` + a `mesh_gateway` config block are present.
docs/architecture/07-client-app-and-rendering.md
# 07 · Client App & Rendering
> **Layer** `androidApp` · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `androidApp/src/main/java/com/aether/mofe/{AetherApp.kt,ui/**,viewmodel/**,platform/**,data/MeshRepository.kt}`
> **Related** — [01-positioning-engine](01-positioning-engine.md) · [02-mesh-and-consensus](02-mesh-and-consensus.md) · [04-netcode-and-time](04-netcode-and-time.md) · [09-workflows](09-workflows.md)
The Android app supplies the **hands** (UWB/IMU/band radios), the **on-device assembly**, and the
**screen**. It reads the `common` engine + mesh through two bridge objects and renders a Filament
3-D / first-person scene gated by render-readiness.
## Boot chain
`AndroidManifest` → **`AetherApp`** (Application; builds the object graph once) → `MainActivity` →
`PermissionGateScreen` (UWB is the hard requirement) → **`AppShell(app, hud)`**.
`AetherApp.onCreate` wires everything: builds `AndroidUwbService`, `MeshRepository`, and
`MofeEngineHost` (starts the engine); resolves the device seed identity; sets up the control-plane
API + `MeshRegistrar`; and launches long-lived collectors that (a) push engine `fusedStates` →
`MeshRepository.setFusedPoses`, and (b) drive from `MeshRepository.sharedState`: set root anchor,
`NetcodeBringUp.maybeStart`, track/untrack targets, feed inter-anchor ranges, place/repair anchors,
**client self-seed**, register composites, and emit predicate-satisfied events.
## The two bridges
```mermaid
flowchart LR
subgraph HW["platform actuals"]
U["AndroidUwbService"]:::hw
I["AndroidImuService"]:::hw
B["BroadcastBand"]:::hw
end
U --> H["MofeEngineHost<br/>(wraps common engine)"]
I --> H
B --> R["MeshRepository<br/>(roster + shared state)"]
H -->|"fusedStates"| R
R -->|"track / seed / place"| H
H -->|"meshMode · anchorLinks · renderReadiness ·<br/>antennaDelays · constellationFrame · geometryReport"| SHELL["AppShell"]
R -->|"sharedState · snapshot · selfDeviceId"| SHELL
SHELL --> SCR["screens (scene · diagnostics · manage · predicates)"]
classDef hw fill:#eef;
```
- **`MofeEngineHost`** (`platform/MofeEngineHost.kt`) — application-scoped wrapper around the engine,
pinned to a single engine dispatcher. Exposes StateFlows the UI consumes: `fusedStates`, `meshMode`,
`quorum`, `anchorHealth`, `anchorLinks` (`InterAnchorLinkReport`), `antennaDelays`,
`constellationFrame`, `geometryReport`, **`renderReadiness`**, `behaviorEvents`, `selfDiag`. Key
methods: `start(selfId)` (engine build + ~60 Hz self-pose poll + 2 s maintenance refresh),
`becomeRoot`, `track`/`untrack`, **`placeReferenceAnchor`** (drives CALIBRATING→QUORUM),
**`seedSelfFromExternalPosition`** (client self-seed), `registerComposite`, `latestSelfPose()`.
- **`MeshRepository`** (`data/MeshRepository.kt`) — the band/roster/UWB orchestrator + single source
of shared state: `sharedState` (band roster), `snapshot` (HUD model: self at origin + placed peers),
`selfDeviceId`, `isAnchorRole`, `frameReferenceAnchors()`, `selfBroadcastSolvedPosition()` (fed into
self-seed). Owns UWB session orchestration, band lifecycle, and X25519+AEAD group-key admission.
## Screens (`ui/`)
| Route | Screen | File |
|-------|--------|------|
| `visualizer` (home) | 3-D / FPV scene | `ui/scene/MeshSceneScreen.kt` (+ `MeshSceneView.kt`) |
| `predicates` | pointing-predicate realizer | `ui/predicate/PredicateRealizerScreen.kt` |
| `calibrate` | mesh diagnostics / health | `ui/MeshHealthPane.kt` |
| `meshes` | create / discover / requests / gateway | `ui/manage/MeshManagerScreen.kt` |
| `settings` | app settings | `ui/SettingsScreen.kt` |
`AppShell` computes the **start destination** from `MeshDisplayPolicy.startupSurface(isAnchorOrRoot,
renderReadiness, autoJoinBringup=true)` → an anchor/root lands on **Diagnostics**, a client on the
**Visualizer**. The scene's diagnostics pane (`DiagnosticPane` in `ui/HudScreen.kt`) shows the
`SelfAttitudeDiag` rows + AoA calibration controls + the **`LayoutEditorDialog`** (the surveyed
`MeshGroundTruth` editor).
## Render & aim path
The render brain is **`HudViewModel`** (`viewmodel/HudViewModel.kt`). It owns a client-local
`NetcodeSession` (interp delay 120 ms) + `MeshAimTracker` fed from `mofeEngineHost.fusedStates`.
```mermaid
flowchart LR
FS["fusedStates"] --> NS["NetcodeSession.recordSample"]
RC["RenderClock.queryMicros<br/>(wall-clock advanced)"] --> RP["renderNodePositions()<br/>= renderPositions(query) − self world pose"]
NS --> RP
RP --> OF["MeshSceneView.onFrame<br/>slew Filament nodes (F1 smoothing)"]
LSP["latestSelfPose().orientation<br/>facing = body −Z (lens)"] --> AIM["aimTracks(cone) → Track"]
AIM --> OV["AimMarkersOverlay (reticle)"]
RG{"readiness.canRenderSpatial?"} -- no --> HOLD["MeshHoldingState (status label)"]
RG -- yes --> OF
```
- **`RenderClock`** advances a wall-clock query so the 60 Hz view interpolates *between* 10 Hz solves
(the F1 smoothness fix). `MeshSceneView.onFrame` slews retained Filament spheres to
`renderNodePositions()` each frame.
- **FPV** seats the camera at origin and `lookTowards(facing)` (body −Z), using
`FpvCameraCalibration.DEFAULT` (vertical FOV + `worldToFilament` swap) so the 3-D spheres and the
2-D overlays coincide.
- **Aim** uses the same body −Z "lens" as the engine's pointing predicate, so aim, camera, and the
gesture share one "forward."
- **Gate:** `MeshSceneScreen` draws the scene only when `readiness.canRenderSpatial`, else a labelled
`MeshHoldingState` — the client-protection safeguard from
[01 · render-readiness](01-positioning-engine.md).
## Platform bring-up (`platform/`)
`AndroidUwbService` (AndroidX-UWB, a pool of per-link DS-TWR sessions) · `AndroidImuService` ·
`BroadcastBand` (UDP :47600) · `NetcodeBringUp`/`NetcodeTrust`/`NetcodePeerBook` (the on-device MMNF
assembly, [04](04-netcode-and-time.md)/[05](05-security-and-trust.md)) · control plane
(`ControlPlaneApi`, `AuthClient`, `MeshRegistrar`, `GatewayUplinkClient`). No foreground service —
background tears down ranging, foreground re-activates.
> **Onboarding note:** `androidApp` requires the Android Gradle Plugin and can't be compiled in the
> network-restricted env. UI changes are delivered as reviewed patches; the logic they consume is
> unit-tested in `:common`. See [08](08-contributing.md).
docs/architecture/08-contributing.md
# 08 · Contributing — build, test, deliver
> **Layer** build/process · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `settings.gradle.kts`, `build.gradle.kts`, `common/build.gradle.kts`, `gradle/gradle-daemon-jvm.properties`, `common/…/messaging/support/ClusterHarness.kt`
> **Related** — [00-overview](00-overview.md) · [README](README.md)
## Module layout & what's testable where
| Module | Contains | Testable headless? |
|--------|----------|--------------------|
| `:common` | the KMP engine, mesh, netcode, security, models | **Yes** — `./gradlew :common:jvmTest` (pure JVM, no Android) |
| `:androidApp` | Compose UI + Android platform actuals | **No** here — needs the Android Gradle Plugin (AGP) |
**Put logic in `:common`.** Anything you can express platform-agnostically belongs there because it
is unit-testable without a device. `androidApp` should be thin glue (radios, sensors, screen,
assembly). This is why the render-readiness model, the display policy, the netcode kill-switch, etc.
all live in `common` with tests, and only their *wiring* lives in `androidApp`.
## The offline `:common:jvmTest` recipe
The build pins a **JetBrains-vendor JDK 21 daemon** (`gradle/gradle-daemon-jvm.properties`), a
**JDK-17 compile toolchain** (`common/build.gradle.kts` `jvmToolchain(17)`), and resolves AGP from
Google Maven. In a network-restricted environment none of those can be fetched. To run the `common`
tests with only the system JDK, neutralize the four network-coupled constraints **in a throwaway git
worktree** (never the real tree), then discard it:
```bash
git worktree add -b _verify /tmp/verify origin/feature/uiux-update2 # or any base
cd /tmp/verify
rm -f gradle/gradle-daemon-jvm.properties # daemon → JAVA_HOME
sed -i 's/jvmToolchain(17)/jvmToolchain(21)/' common/build.gradle.kts # use system JDK 21
sed -i -E '/^[[:space:]]*alias\(libs\.plugins\..*apply false/ s|^|// |' build.gradle.kts # drop AGP plugin markers
sed -i 's|^include(":androidApp")|// &|' settings.gradle.kts # exclude the Android module
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew :common:jvmTest --offline --no-daemon
cd - && git worktree remove /tmp/verify --force
```
This runs the full `:common` suite green (≈760 tests) against the system JDK. It is a **fidelity
shortcut**, not a production build: `commonMain`/`commonTest` are platform-agnostic Kotlin, so the
JDK 17-vs-21 swap does not affect test logic — but it is **not** a substitute for CI under the pinned
toolchain, and it says nothing about whether `androidApp` compiles. Use a worktree so a killed run
can never leave the real tree edited.
## The test harness — `ClusterHarness`
Mesh/consensus/netcode behaviour is tested against `common/…/messaging/support/ClusterHarness.kt`, an
in-memory N-node cluster over virtual transports (`VirtualEther`/`VirtualControlBus`) with a virtual
clock. It supports per-node scopes (`kill`/`killLeader`), fault injection (latency/loss/partition),
and `pump(rounds)` to advance the clock + scheduler in lockstep. Netcode tests opt into the seam with
`ClusterHarness(this, netcodeBringUp = true)` (default off, mirroring production). Prefer extending
this harness over mocking.
## Delivering to the dev branch (`feature/uiux-update2`)
The dev does **not** use the `claude/*` branch. Deliver changes as **`git format-patch` files** that
apply cleanly onto the dev tip, verified with `:common:jvmTest` green **against that base** (use the
worktree recipe above). Convention:
```bash
# generate: linearize your commits onto the dev tip (a worktree cherry-pick proves clean apply)
git format-patch origin/feature/uiux-update2..HEAD -o /tmp/stack
# the dev applies:
git checkout feature/uiux-update2
git am --keep-cr /tmp/stack/*.patch # ⚠ --keep-cr is REQUIRED (see below)
./gradlew :common:jvmTest # expect green
```
> ⚠ **`git am --keep-cr` is mandatory.** Several files are **CRLF**-terminated
> (`MeshCoordinator.kt`, `MeshMessagingService.kt`, `LinkHandshake.kt`, and server `mesh.py`). Plain
> `git am` strips the CR and fails with *"patch does not apply"*. `git apply` then commit also works.
Ship a short **README table** with each stack: `# | what | layer | verified`, marking `common` (✅
jvmTest) vs `androidApp` (⚠ not compilable here — mechanical, mirrors existing flows). Roadmap/design
docs are **not** bundled by default — offer them separately.
## Keeping these docs true
Each doc's **Source of truth** header lists the code it describes; the [README](README.md) has the
path→doc map. When your change touches those files, update the doc and bump **Verified against** in
the same change. Diagrams are Mermaid — edit the fenced block. New subsystem → new numbered doc +
manifest row.
docs/architecture/09-workflows.md
# 09 · End-to-end Workflows
> **Layer** cross-cutting · **Status** stable · **Verified against** `bdf7e0f` (2026-08-04)
> **Source of truth** — `androidApp/…/ui/manage/**`, `…/ui/predicate/**`, `…/platform/control/MeshRegistrar.kt`, `…/platform/netcode/**`, `…/data/MeshRepository.kt`, `common/…/messaging/MeshCoordinator.kt`, `common/…/engine/BehaviorEngine.kt`
> **Related** — all docs; this is where the layers meet. The user-facing versions of these are the layperson guide.
Three workflows carry the whole system. Each is shown as the user's steps + the under-the-hood path
+ the entry-point files, so you can follow one thread top to bottom.
## A · Set up a new mesh (root anchor → QUORUM)
```mermaid
sequenceDiagram
actor Op as Operator
participant App as App (MeshManager / Diagnostics)
participant Reg as MeshRegistrar
participant Repo as MeshRepository / Band
participant Eng as MofeEngineHost / engine
Op->>App: grant UWB/BT permissions
Op->>App: Meshes ▸ + ▸ create (name, visibility)
App->>Reg: createLocalMesh(name, visibility)
Reg->>Repo: setSelfIdentity(roles=[ROOT,ANCHOR]) + advertise BandMeshMeta (signed)
Reg-->>Eng: onBecameRoot → becomeRoot(self) (pin frame origin)
Op->>App: Requests ▸ invite anchors ; targets accept
Repo->>Eng: inter-anchor UWB ranges → feedInterAnchorRange
Op->>App: Diagnostics ▸ LAYOUT (truth) → enter surveyed X/Y/Z
App->>Eng: MeshGroundTruth.poses (antenna-delay cal)
App->>Eng: placeReferenceAnchor × N (≥4 refs)
Eng-->>App: meshMode UNINITIALIZED→CALIBRATING→PRE_QUORUM→QUORUM
Note over App: anchor/root lands on Diagnostics (MeshDisplayPolicy)
```
Entry files: `ui/manage/MeshManagerScreen.kt` (`OfflineCreateDialog`) → `MeshManagerViewModel.createLocalMesh`
→ `platform/control/MeshRegistrar.kt` → `MeshRepository` / `MofeEngineHost`; surveyed layout via
`ui/HudScreen.kt` `LayoutEditorDialog` → `engine/MeshGroundTruth.kt`. The mode machine + constellation
lock are [01](01-positioning-engine.md) / [02](02-mesh-and-consensus.md); no server is required
(offline-first).
## B · A client joins an existing mesh (→ scene unlock)
```mermaid
sequenceDiagram
actor U as User
participant App as App (Meshes ▸ Discover)
participant Reg as MeshRegistrar
participant Srv as tome-server
participant NB as NetcodeBringUp
participant Eng as engine
U->>App: Discover ▸ Join / Request
App->>Reg: requestJoinLocal(meshId) (roles=[MEMBER,CLIENT])
Note over Reg: owner approves → group key X25519-sealed to member
opt go online
App->>Srv: fetch+pin CA key ; pull/enroll DeviceCredential
end
App->>NB: maybeStart (gated: MeshTrustReadiness.Ready + leader IP + LAN)
Eng->>Eng: seedSelfFromExternalPosition(anchor-broadcast solved pos)
Eng-->>App: renderReadiness → READY (QUORUM + self-localized)
Note over App: MeshHoldingState → Filament 3-D / FPV scene
```
Entry files: `ui/manage/MeshManagerScreen.kt` (Discover) → `MeshRegistrar.requestJoinLocal`/`connectTo`
→ `MeshRepository` (self-seed via `selfBroadcastSolvedPosition`) → `MofeEngineHost.seedSelfFromExternalPosition`;
trust via `platform/netcode/NetcodeTrust.kt` + `NetcodeBringUp.kt`. A client **HOLDs** (never shows
unsolved locations) until READY; an anchor would instead see Diagnostics
([01 · MeshDisplayPolicy](01-positioning-engine.md)).
## C · An author contributes a predicate
Two families share the arbitration seam:
**Pointing predicate (the on-device authoring surface):**
```mermaid
sequenceDiagram
actor A as Author
participant PR as PredicateRealizer
participant Repo as Band (composites)
participant BE as BehaviorEngine (every device)
A->>PR: "Point at <location|peer>"
PR->>Repo: publishComposite(PcdManifest: forward→direction→angle→cone→for_duration→edge)
Repo->>BE: registerComposite(manifest, bindings) on every device
loop each fused frame
BE->>BE: tick(pose): is body −Z within the cone for the dwell?
end
BE-->>Repo: CompositeFired(pointedAtIds) → PREDICATE_SATISFIED BandEvent
Note over PR: lights ✓ as actor (source==self) or target (target==self)
```
**Geometric predicate (region entry/exit, replicated + arbitrated):**
```mermaid
sequenceDiagram
participant L as Leader
participant R as Raft / registry
participant N as Every node's engine
participant NC as Netcode (if ON)
L->>R: registerPredicateMeshWide(GeometricPredicate: Shape.Sphere/Box/…)
R-->>N: on commit → applyLocalSideEffects → predicate registered
N->>N: observer detects RegionEntry/Exit locally → optimistic fire
N->>NC: EventClaim → leader rewind arbitration → EventVerdict
NC-->>N: CONFIRMED / ROLLED_BACK
```
Entry files: `ui/predicate/PredicateRealizerScreen.kt` + `PredicteRealizerViewModel.kt`; manifests
`model/pcd/PointingPredicates.kt`; evaluation `engine/BehaviorEngine.kt`; geometric predicates
`model/EventTypes.kt` + `math/Geometry.kt` (`Shape.Sphere/Line/Box/Plane`); mesh-wide registration +
arbitration `messaging/MeshCoordinator.kt` (`registerPredicateMeshWide`, `emitClaims`) →
[04 · arbitration](04-netcode-and-time.md). The geometric arbitration path only runs when the netcode
kill-switch is on; the pointing/composite path runs on the band regardless.
docs/architecture/README.md
# Aether Architecture — Developer Onboarding
> The system as a **stack of interoperable systems**. Read these to ramp from zero to
> contributing. They are **living design artifacts**: kept in the repo, versioned with the
> code, and meant to be edited by whoever changes the code they describe (human or agent).
## How to use this set
- **Ramping up?** Read in order: [00](00-overview.md) → [01](01-positioning-engine.md) →
[02](02-mesh-and-consensus.md) → [03](03-networking-and-protocols.md) →
[04](04-netcode-and-time.md) → [05](05-security-and-trust.md) →
[06](06-server-and-telemetry.md) → [07](07-client-app-and-rendering.md) →
[08](08-contributing.md) → [09](09-workflows.md).
- **Touching a file?** Find it in the **Source-of-truth map** below → open that doc first.
- **Want the user's-eye view?** The workflows in [09](09-workflows.md) mirror the
layperson guide (mesh setup, client join, predicate authoring).
## The doc set
| # | Doc | What it covers | Primary layer |
|---|-----|----------------|---------------|
| 00 | [Overview & the stack](00-overview.md) | The layered stack, the 5 communication planes, glossary, module map | all |
| 01 | [The MOFE positioning engine](01-positioning-engine.md) | UWB→pre-filter→multilaterate→IMU-EKF→constellation solve/lock; calibration | `common/engine` |
| 02 | [Mesh & consensus](02-mesh-and-consensus.md) | Roles, Raft (pre-vote), the replicated state registry, quorum & operating modes | `common/messaging`, `raft` |
| 03 | [Networking & wire protocols](03-networking-and-protocols.md) | The two stacks, planes/ports/transports, CBOR envelope, the UWB band | `common/messaging`, `androidApp/band` |
| 04 | [Netcode & time](04-netcode-and-time.md) | Mesh clock, temporal buffers, entity interpolation, claim→arbitration→verdict, the kill-switch | `common/engine/netcode` |
| 05 | [Security & trust](05-security-and-trust.md) | Ed25519 CA, credentials, link handshake, group-key datagram AEAD, op signing | `common/messaging/security` |
| 06 | [Server & telemetry](06-server-and-telemetry.md) | tome-server: CA endpoint, uplink, three telemetry tiers, commands | `tome-server` |
| 07 | [Client app & rendering](07-client-app-and-rendering.md) | Compose shell, screens, host StateFlow bridges, render-readiness + role gate | `androidApp` |
| 08 | [Contributing](08-contributing.md) | Repo layout, build & offline-test recipe, the test harness, patch delivery, doc upkeep | build/process |
| 09 | [End-to-end workflows](09-workflows.md) | Mesh bootstrap, client join, predicate lifecycle — as dev sequences | cross-cutting |
## Source-of-truth map (path → doc)
Match the file you're editing to the most specific glob; update that doc + its **Verified
against** commit when you land the change.
| Code path glob | Doc |
|----------------|-----|
| `common/…/engine/**` (except `engine/netcode/**`, `engine/render/**`) | 01 |
| `common/…/engine/render/**` | 01 (model) + 07 (UI consumption) |
| `common/…/messaging/raft/**`, `common/…/messaging/MeshStateRegistry.kt`, `MeshCoordinator.kt` | 02 |
| `common/…/messaging/MeshMessagingService.kt`, `MeshCodec.kt`, `Transports.kt`, `model/messaging/**` | 03 |
| `androidApp/…/platform/band/**` | 03 |
| `common/…/engine/netcode/**` | 04 |
| `common/…/messaging/security/**`, `MeshTrustMaterial.kt`, `androidApp/…/platform/netcode/**` | 05 |
| `tome-server/**` | 06 |
| `androidApp/…/ui/**`, `…/platform/MofeEngineHost.kt`, `…/data/MeshRepository.kt`, `AetherApp.kt` | 07 |
## The per-doc header
Every doc opens with a metadata blockquote — human-readable **and** the contract an editor
keeps current:
```
> **Layer** <module/package> · **Status** stable|draft · **Verified against** <sha> (<date>)
> **Source of truth** — <code globs this doc describes>
> **Related** — <links to sibling docs>
```
## Maintenance protocol (for humans and agents)
1. **Edit the doc with the code.** If your diff touches a doc's *Source of truth*, edit the
doc in the same change and set *Verified against* to the new commit.
2. **Diagrams are text.** All diagrams are Mermaid fenced blocks — edit them; don't replace
them with images. They render on GitHub and in Claude artifacts natively.
3. **One topic per file, stable headings.** Link across docs by relative path + anchor so
references survive edits.
4. **Say what's aspirational.** Mark not-yet-wired or OFF-by-default behaviour explicitly
(e.g. the netcode kill-switch, the RoleDecision verify TODO) so readers don't assume it.
5. **New subsystem → new doc + manifest row + Source-of-truth-map row.**
---
*These docs were built from a read-only sweep of the codebase at the commit in each
header. Line numbers drift; treat file paths + symbol names as the durable anchors.*
    (1-1/1)