User Story #70 » 0001-docs-architecture-developer-onboarding-doc-set-CLAUD.patch
| 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.*
|
||