# Mesh-D6 — fan-out widen + hard cutover → MESH ON

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 6, **merged with the deferred D5b cutover** per the re-cut in `MESH-D5-PLAN.md` (2026-06-08). **This is the single commit that turns the mesh on.** The push target widens from the directly-paired set to **all roster members**, the `TrustStore` authorization path is deleted (hard cutover, no migration — decision 5), and the D0 staggered harness flips green. Activates **REQ-MESH-3** (impl, unit, int) and completes **REQ-MESH-5** (impl, unit).

## Why D5b and D6 are one commit (the coupling)

Deleting `TrustStore` and widening the fan-out are **inseparable**:

- `peerloop` derives its **push target** (who it dials + pushes rows to) from `trust.peers_in()` — the directly-paired set (`peerloop.rs:327,364`).
- Stripping pairing's trust write empties `peers.json`; the cutover deletes it. With no `peers.json`, `peerloop`'s narrow push returns nothing → directly-paired flows break.
- The only repair points `peerloop` at `roster.members_in()` — but the roster is **wide** (D4 made A learn C transitively). That swap **is** the D6 widen.

So there is no "delete `TrustStore` but keep the push narrow" intermediate. The cutover forces the widen; they ship together. D5b (shipped, `91083ef`) was the one genuinely separable, no-reach half: roster-anchoring the REQ-SUBNET-7 eviction.

## Pre-flight (carried, already CLEARED at D5)

The cutover creates a transient new↔old partition (seed-proof auth can't interop with pairwise). Acceptable because the D6 roll is a **coordinated manual fleet rebuild** (`git pull` + build + restart), out-of-band; ADR-0016 update *apply* is node-local and does **not** ride member connections. No self-update deadlock. (Re-verify nothing new since D5 rides member conns for the binary roll.)

## The widen (the load-bearing change — REQ-MESH-3)

`peerloop.rs` tick loop (~360): the push iterates `trust.peers_in(&sub.name)` → change to `roster.members_in(&sub.name)`. The roster is already loaded for the sync gate (`roster_path`, ~339) — reuse it; the push and the gate read one store. Drop `PumpPaths.trust` (`peerloop.rs:119,163`) and the `TrustStore::load_from(&paths.trust)` at ~327. `members_in` already excludes tombstoned entries (D3), so revoked/superseded keys are not dialed.

**Rows stay own-authored.** `advertise_local` already pushes only this node's own rows (no relay) — unchanged. The widen changes only *who we open a direct connection to*, never *whose rows we send*. KH 4.10 / 7.5 preserved verbatim (the milestone's whole point).

**Regression assert (REQ-MESH-3):** in the mesh int test, assert every applied registry row's `origin_node` (handshake-proven) equals the row's author — no third-party row is ever applied. This is the audit that the widen turned on *reach*, not *relay*.

## The cutover (delete the trust path — completes REQ-MESH-5)

1. **Strip trust from pairing** (`pairhost.rs`): drop `TrustStore::load` (78, 307), the `&mut trust` arg to `run_responder` (92) / `run_initiator` (336, 383), `trust.save()` (134, 407–409), and the `outcome.trust` log (135). The roster already records membership (D3: `record_roster_peer`, `refresh_self_roster`, `stamp_roster_responder`, the joiner's `merge`/adopt). Remove the `trust: &mut TrustStore` param from `run_responder`/`run_initiator` in `spt_net::net::pairing::wire` + everything they call on it (the pin write). Keep `outcome.trust` only if other fields need the struct; else drop the field.
2. **`propagate` serve gate** (`propagate.rs:105–112,133,172`): signature `trust: &TrustStore` → `roster: &RosterStore`; the two `trust.peers.iter().any(|p| p.pubkey_hex == origin_node)` checks → `roster.is_member`-in-any-subnet (iterate the node's subnets, or add a `roster.is_member_any(origin)` helper). Update the caller `dispatch.rs:326–327` (`TrustStore::load()` → `RosterStore::load()`).
3. **Demote warn-on-change** (REQ-MESH-5): the label-anchored `KeyChanged` path dies with `trust.rs`. At the REQ-SUBNET-7 re-pair detection point (`repair_evict_superseded`, which already runs on a same-(label,machine_id) different-key match), emit a **non-blocking awareness notif** — "machine M, last seen as K1, now presents K2" — never a gate. Absent machine_id ⇒ no notice (no false positive). Anchor on `intro.machine_id` (already in hand there).
4. **CLI repoint** (`spt/src/cli.rs`): the `subnet status --nodes` render (`subnet_status_rows`, ~2293/2306) + `subnet list` (~2770/2822) `trust.peers_in` → `roster.members_in`; the `1305` trust load → roster; `prune` (~2904–2961) → roster removal. Prune semantics: local **tombstone** (so the `is_member` gate drops it and a merge can't re-admit) — the *propagating, seed-rotating* revoke is D7; note that in the CLI help. Migrate the render unit tests (`cli.rs` ~6518–6693) trust→roster.
5. **Delete the store**: `crates/spt-store/src/trust.rs` (struct + 8 unit tests), `perch::trust_file` (the `peers.json` path) + its doc, the `lib.rs` `pub mod trust`. **Keep `perch::trust_dir()`** — it still homes `access.json` / `recent-outbound.json` / `grants.json` (those are not trust-store material; the dir name is incidental). `peers.json` has no writer/reader left ⇒ gone by deletion, no migration. Grep-verify zero `TrustStore` / `is_trusted` / `trust::` / `peers.json` references remain in non-comment code.
6. **Migrate tests**: `twohost.rs`, `propagate.rs`, and the `pairing/wire.rs` ceremony tests — replace `TrustStore` construction/assertions with `RosterStore` membership (seed via `merge_entry`/`upsert_self`, assert via `is_member`). `notifsync.rs`/`sync.rs`/`peerloop.rs`/`dispatch.rs` already migrated at D5a.
7. **The green flip**: un-ignore the D0 staggered harness (`mesh.rs`, `#[ignore]`); it asserts A↔C converge to see **and reach** each other (B offline at the critical step) + the all-online star (A reaches C, B never relays). It flips green here. This is REQ-MESH-3's int evidence.

## Tests

- **int (the acceptance, REQ-MESH-3):** the un-ignored `mesh.rs` staggered + star harness passes; the no-third-party-row regression assert holds (every applied row's origin == its author).
- **unit (REQ-MESH-5 completion):** `propagate` serve gate admits a roster member / refuses a non-member; the machine_id awareness notice fires on a same-machine re-key without blocking, absent machine_id ⇒ silent; CLI render reads the roster. Compile-enforced: `trust.rs` is gone, so any missed reader fails to build.
- **Regression:** D2 seed-proof int, D3 roster/wire, D4 propagation, D5b eviction, every migrated daemon test stay green; `--no-default-features` + workspace + clippy clean. Watch the **false-green trap**: read raw `cargo test`/`clippy` exit codes, never trust a piped `grep`.

## Done when

- `peerloop` push target is `roster.members_in`; the D0 harness is un-ignored and **green** (A↔C reach); the no-relay regression assert passes.
- `TrustStore` + `peers.json` + every call site are **removed, not left dead**; `trust_dir()` retained for access/grants; warn-on-change is a machine_id notice, not a gate.
- Pairing records membership in the roster only; D5b's REQ-SUBNET-7 eviction still acceptance-green.
- `traceable-reqs check` clean with **REQ-MESH-3 `[impl,unit,int]`** + **REQ-MESH-5 `[impl,unit]`** activated; REQ-SUBNET-7 still satisfied; KH 4.10 / 7.5 hazard tests still green.
- One commit: `feat(mesh)!: fan-out widen + delete TrustStore → mesh on (REQ-MESH-3, REQ-MESH-5) — Mesh-D6`.

## Explicitly NOT D6 (carry to D7+)

- **D7** — `spt subnet revoke` (propagating tombstone + timeboxed seed rotation), confidential seed push, re-seed grace, the per-conn `proven_subnets` epoch cross-check, cross-node un-tombstone.
- **D8** — `subnet status --nodes` probe concurrency.
