    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {
            eprintln!(
                "DBG b perchC rows: {:?}",
                b.registry.rows(SUBNET, "perchC").iter().map(|i| i.node.clone()).collect::<Vec<_>>()
            );
            std::thread::sleep(Duration::from_millis(300));
            if b.sees("perchC", &c.hex) { eprintln!("DBG converged"); break; }
        }
    }
    converge("B sees C's perch", || b.sees("perchC", &c.hex));

+        let rp = c.paths.subnets.parent().unwrap().join("roster.json");

+        let rr = spt_store::roster::RosterStore::load_from(&rp);

+        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());

+                "DBG b perchC rows: {:?}",

+                b.registry.rows(SUBNET, "perchC").iter().map(|i| i.node.clone()).collect::<Vec<_>>()

+            std::thread::sleep(Duration::from_millis(300));

+            if b.sees("perchC", &c.hex) { eprintln!("DBG converged"); break; }

b_stop.store(true, Ordering::Relaxed); // B offline


Bash

cargo test -p spt-daemon --test mesh staggered_offliner_still_meshes -- --exact --nocapture 2>&1 | grep -E "DBG" | head -25; echo "===EXIT done==="

Run with phase2 diagnostics


DBG c roster members=3
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
DBG b perchC rows: []
===EXIT done===


C roster has 3 members but B gets nothing. Test the "C stalls dialing offline-A first" hypothesis — temporarily drop A from C's push roster:


Edit

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {

    // Phase 2 — C joins B (B still online): B comes to see C.
    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {

    // Phase 2 — C joins B (B still online): B comes to see C.
    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {

a_stop.store(true, Ordering::Relaxed); // A offline

+    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A


spt-store: delete trust.rs, drop mod + trust_file

Delete crates/spt-store/src/trust.rs, remove `pub mod trust` from lib.rs, remove perch::trust_file (keep trust_dir for access/grants).

spt-net wire.rs: strip trust from ceremony

Remove trust param + TrustOutcome from run_responder/run_initiator/responder_admitted; drop ResponderOutcome.trust field; migrate wire.rs tests to roster membership.

pairhost.rs: drop trust write

Remove TrustStore load/save/args in both legs; update run_responder/run_initiator calls; demote warn-on-change to machine_id notif at repair_evict_superseded point.

propagate.rs serve gate trust->roster

serve_update signature trust:&TrustStore -> roster:&RosterStore; both is_member-any checks; dispatch.rs:326 caller.

peerloop.rs THE WIDEN

push target trust.peers_in -> roster.members_in; reuse roster loaded for sync gate; drop PumpPaths.trust + TrustStore load.

registryhost.rs: repair_evict drop trust

repair_evict_superseded drop &mut TrustStore param (roster-only now); migrate its unit tests.

CLI cli.rs repoint to roster

subnet status/list/prune trust->roster (prune=local tombstone); 1305 load; render unit tests.

xtask + misc comment refs

xtask main.rs:897 trust dump -> roster; nethost/notifsync/sync/access doc comment refs.

Migrate daemon tests trust->roster

twohost.rs, propagate.rs, peerloop.rs, dispatch.rs tests: TrustStore -> RosterStore membership.

mesh.rs: un-ignore + regression assert

Un-ignore D0 staggered+star harness; migrate trust seeding to roster; add no-third-party-row origin==author assert.

Activate REQ-MESH-3 + complete REQ-MESH-5

traceable-reqs.toml: REQ-MESH-3 [impl,unit,int], REQ-MESH-5 [impl,unit]; run traceable-reqs check; clippy; daemon suite.


Bash

cargo test -p spt-daemon --test mesh staggered_offliner_still_meshes -- --exact --nocapture 2>&1 | grep -E "DBG" | head -8; echo "===EXIT done==="

Test with A excluded from C roster


DBG c roster members=2
DBG b perchC rows: []
DBG converged
===EXIT done===


Root cause: C stalls dialing offline-A first (its broker stays up so the dial half-connects but the push blocks). The old harness never dialed offline nodes (pairwise trust). "Offline" must stop the broker too, or the dial must fail. Check Broker shutdown API.


grep -n "pub fn shutdown\|pub fn stop\|fn serve\|shutdown\|Drop for Broker\|stop_flag\|running:" "C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/broker.rs" | head -30

Find broker shutdown API


223:    pub fn serve(self: &Arc<Self>) -> io::Result<()> {
235:    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {


Confirmed: the pump blocks permanently when pushing to a broker-up/dispatch-down "offline" node, so roster order starves later peers. The fix: model offline as truly unreachable — a live reachability map the resolver reads, plus disabling peer-addr seeding (so a stale seed can't bypass it). Revert debug, then refactor the harness.


Edit

    // Phase 2 — C joins B (B still online): B comes to see C.
    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {
            eprintln!(
                "DBG b perchC rows: {:?}",
                b.registry.rows(SUBNET, "perchC").iter().map(|i| i.node.clone()).collect::<Vec<_>>()
            );
            std::thread::sleep(Duration::from_millis(300));
            if b.sees("perchC", &c.hex) { eprintln!("DBG converged"); break; }
        }
    }
    converge("B sees C's perch", || b.sees("perchC", &c.hex));

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));


    // Phase 2 — C joins B (B still online): B comes to see C.
    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A
    let c_stop = c.online(resolver_over(&addrs));
    {
        let rp = c.paths.subnets.parent().unwrap().join("roster.json");
        let rr = spt_store::roster::RosterStore::load_from(&rp);
        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());
        for _ in 0..20 {
            eprintln!(
                "DBG b perchC rows: {:?}",
                b.registry.rows(SUBNET, "perchC").iter().map(|i| i.node.clone()).collect::<Vec<_>>()
            );
            std::thread::sleep(Duration::from_millis(300));
            if b.sees("perchC", &c.hex) { eprintln!("DBG converged"); break; }
        }
    }
    converge("B sees C's perch", || b.sees("perchC", &c.hex));

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));

-    reroster(&c, &[b.hex.as_str(), c.hex.as_str()]); // DBG: exclude offline A

-        let rp = c.paths.subnets.parent().unwrap().join("roster.json");

-        let rr = spt_store::roster::RosterStore::load_from(&rp);

-        eprintln!("DBG c roster members={}", rr.members_in(SUBNET).count());

-                "DBG b perchC rows: {:?}",

-                b.registry.rows(SUBNET, "perchC").iter().map(|i| i.node.clone()).collect::<Vec<_>>()

-            std::thread::sleep(Duration::from_millis(300));

-            if b.sees("perchC", &c.hex) { eprintln!("DBG converged"); break; }


Implement live-reachability resolver + disable peer-addr seeding (so offline = truly unreachable, no cached stale seed bypass). Update `new()` peer_addrs:


164	            owlery: owlery.clone(),
165	            ctx_root: root.join("context"),
166	            notif_db: root.join("notifs.db"),
167	            scratch: root.join("scratch"),
168	            subnets: root.join("subnet.json"),
169	            vis: root.join("visibility.json"),
170	            ops: root.join("pump-ops.json"),
171	            releases: root.join("releases"),
172	            release_keys: root.join("release-keys.json"),
173	            pull_marker: root.join("sync-pull-now"),
174	            advertise_marker: root.join("advertise-now"),
175	            peer_addrs: root.join("peer-addrs.json"),
176	            heartbeat: root.join("pump-heartbeat.json"),
177	        };
178	        std::fs::create_dir_all(root).unwrap();
179	        let mut subnets = SubnetStore::default();
180	        subnets.create_subnet(SUBNET).expect("node subnet");
181	        subnets.save_to(&paths.subnets).expect("save node subnet");
182	        // The push target = the roster (Mesh-D6). Sibling of subnet.json so the
183	        // pump derives it (subnet.json → roster.json). Seeded empty here; the
184	        // caller rerosters with the full membership once hexes are known.
185	        write_roster(root, members);

            owlery: owlery.clone(),
            ctx_root: root.join("context"),
            notif_db: root.join("notifs.db"),
            scratch: root.join("scratch"),
            subnets: root.join("subnet.json"),
            vis: root.join("visibility.json"),
            ops: root.join("pump-ops.json"),
            releases: root.join("releases"),
            release_keys: root.join("release-keys.json"),
            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            peer_addrs: root.join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();
        let mut subnets = SubnetStore::default();
        subnets.create_subnet(SUBNET).expect("node subnet");
        subnets.save_to(&paths.subnets).expect("save node subnet");
        // The push target = the roster (Mesh-D6). Sibling of subnet.json so the
        // pump derives it (subnet.json → roster.json). Seeded empty here; the
        // caller rerosters with the full membership once hexes are known.
        write_roster(root, members);


            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            peer_addrs: root.join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();

            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            // Address seeding DISABLED: `reach` (the live resolver) is the sole
            // reachability source so an "offline" node (removed from `reach`) is
            // truly undialable — a persisted seed would otherwise let the pump
            // re-dial its still-running broker and wedge on a node modelled as
            // gone. peer_addrs points under a regular FILE so create_dir_all
            // fails and record() no-ops; load_from then always reads empty.
            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();
        std::fs::write(root.join("noseed"), b"").unwrap(); // the seed blocker (a file)


            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            peer_addrs: root.join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();

            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            // Address seeding DISABLED: `reach` (the live resolver) is the sole
            // reachability source so an "offline" node (removed from `reach`) is
            // truly undialable — a persisted seed would otherwise let the pump
            // re-dial its still-running broker and wedge on a node modelled as
            // gone. peer_addrs points under a regular FILE so create_dir_all
            // fails and record() no-ops; load_from then always reads empty.
            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();
        std::fs::write(root.join("noseed"), b"").unwrap(); // the seed blocker (a file)

-            peer_addrs: root.join("peer-addrs.json"),

+            // Address seeding DISABLED: `reach` (the live resolver) is the sole

+            // reachability source so an "offline" node (removed from `reach`) is

+            // truly undialable — a persisted seed would otherwise let the pump

+            // re-dial its still-running broker and wedge on a node modelled as

+            // gone. peer_addrs points under a regular FILE so create_dir_all

+            // fails and record() no-ops; load_from then always reads empty.

+            peer_addrs: root.join("noseed").join("peer-addrs.json"),

+        std::fs::write(root.join("noseed"), b"").unwrap(); // the seed blocker (a file)

let mut subnets = SubnetStore::default();


Now replace `resolver_over` with the live-reachability resolver:


/// An omniscient resolver over the whole fleet (hex → loopback addr).
fn resolver_over(addrs: &HashMap<String, serde_json::Value>) -> PeerResolver {
    let map = addrs.clone();
    Box::new(move |hex: &str| map.get(hex).cloned())
}

/// A live reachability table (hex → loopback addr). An entry present = the node
/// is online and dialable; removing it models the node going offline. Shared
/// across every pump's resolver and read live (NOT cloned at spawn) so a phase
/// transition takes effect immediately — the faithful "offline = unreachable"
/// model this harness needs now that the push fans out to the whole roster
/// (a node modelled offline must be UNDIALABLE, since stopping its loops leaves
/// its broker up and a dial to it would otherwise wedge the dialing pump).
type Reach = Arc<Mutex<HashMap<String, serde_json::Value>>>;

/// A resolver reading the shared [`Reach`] table live.
fn live_resolver(reach: &Reach) -> PeerResolver {
    let reach = Arc::clone(reach);
    Box::new(move |hex: &str| {
        reach
            .lock()
            .unwrap_or_else(|e| e.into_inner())
            .get(hex)
            .cloned()
    })
}

/// Bring `node` online: publish its address in `reach`, then spawn its loops.
fn go_online(node: &MeshNode, reach: &Reach) -> Arc<AtomicBool> {
    reach
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .insert(node.hex.clone(), node.addr.clone());
    node.online(live_resolver(reach))
}

/// Take `node` offline: stop its loops and remove it from `reach` so peers can
/// no longer dial it (its broker stays up, but it is now unreachable by name).
fn go_offline(node: &MeshNode, stop: &Arc<AtomicBool>, reach: &Reach) {
    stop.store(true, Ordering::Relaxed);
    reach
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .remove(&node.hex);
}


/// An omniscient resolver over the whole fleet (hex → loopback addr).
fn resolver_over(addrs: &HashMap<String, serde_json::Value>) -> PeerResolver {
    let map = addrs.clone();
    Box::new(move |hex: &str| map.get(hex).cloned())
}

/// A live reachability table (hex → loopback addr). An entry present = the node
/// is online and dialable; removing it models the node going offline. Shared
/// across every pump's resolver and read live (NOT cloned at spawn) so a phase
/// transition takes effect immediately — the faithful "offline = unreachable"
/// model this harness needs now that the push fans out to the whole roster
/// (a node modelled offline must be UNDIALABLE, since stopping its loops leaves
/// its broker up and a dial to it would otherwise wedge the dialing pump).
type Reach = Arc<Mutex<HashMap<String, serde_json::Value>>>;

/// A resolver reading the shared [`Reach`] table live.
fn live_resolver(reach: &Reach) -> PeerResolver {
    let reach = Arc::clone(reach);
    Box::new(move |hex: &str| {
        reach
            .lock()
            .unwrap_or_else(|e| e.into_inner())
            .get(hex)
            .cloned()
    })
}

/// Bring `node` online: publish its address in `reach`, then spawn its loops.
fn go_online(node: &MeshNode, reach: &Reach) -> Arc<AtomicBool> {
    reach
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .insert(node.hex.clone(), node.addr.clone());
    node.online(live_resolver(reach))
}

/// Take `node` offline: stop its loops and remove it from `reach` so peers can
/// no longer dial it (its broker stays up, but it is now unreachable by name).
fn go_offline(node: &MeshNode, stop: &Arc<AtomicBool>, reach: &Reach) {
    stop.store(true, Ordering::Relaxed);
    reach
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .remove(&node.hex);
}

-/// An omniscient resolver over the whole fleet (hex → loopback addr).

-fn resolver_over(addrs: &HashMap<String, serde_json::Value>) -> PeerResolver {

-    Box::new(move |hex: &str| map.get(hex).cloned())

+/// A live reachability table (hex → loopback addr). An entry present = the node

+/// is online and dialable; removing it models the node going offline. Shared

+/// across every pump's resolver and read live (NOT cloned at spawn) so a phase

+/// transition takes effect immediately — the faithful "offline = unreachable"

+/// model this harness needs now that the push fans out to the whole roster

+/// (a node modelled offline must be UNDIALABLE, since stopping its loops leaves

+/// its broker up and a dial to it would otherwise wedge the dialing pump).

+type Reach = Arc<Mutex<HashMap<String, serde_json::Value>>>;

+/// A resolver reading the shared [`Reach`] table live.

+fn live_resolver(reach: &Reach) -> PeerResolver {

+            .unwrap_or_else(|e| e.into_inner())

+/// Bring `node` online: publish its address in `reach`, then spawn its loops.

+fn go_online(node: &MeshNode, reach: &Reach) -> Arc<AtomicBool> {

+        .unwrap_or_else(|e| e.into_inner())

+        .insert(node.hex.clone(), node.addr.clone());

+/// Take `node` offline: stop its loops and remove it from `reach` so peers can

+/// no longer dial it (its broker stays up, but it is now unreachable by name).

+fn go_offline(node: &MeshNode, stop: &Arc<AtomicBool>, reach: &Reach) {

+    stop.store(true, Ordering::Relaxed);

/// Field-by-field copy — `PumpPaths` is not `Clone` (mirrors `peerloop.rs`).

fn clone_paths(p: &PumpPaths) -> PumpPaths {


pairhost.rs: drop trust write

propagate.rs serve gate trust->roster

peerloop.rs THE WIDEN

CLI cli.rs repoint to roster

xtask + misc comment refs

Migrate daemon tests trust->roster

mesh.rs: un-ignore + regression assert


Now rewrite the staggered test's phase orchestration to use the live reachability helpers:


375	    let dir = TempDir::new().unwrap();
376
377	    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
378	    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
379	    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
380	    // The converged mesh roster (Mesh-D6): every node lists ALL members, so the
381	    // widened push fans out directly to each. A paired only B and C paired only
382	    // B, but the roster D3/D4 would propagate is seeded full here.
383	    let all = [a.hex.as_str(), b.hex.as_str(), c.hex.as_str()];
384	    reroster(&a, &all);
385	    reroster(&b, &all);
386	    reroster(&c, &all);
387
388	    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
389	    let addrs: HashMap<String, serde_json::Value> = [
390	        (a.hex.clone(), a.addr.clone()),
391	        (b.hex.clone(), b.addr.clone()),
392	        (c.hex.clone(), c.addr.clone()),
393	    ]
394	    .into_iter()
395	    .collect();
396
397	    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
398	    let a_stop = a.online(resolver_over(&addrs));
399	    let b_stop = b.online(resolver_over(&addrs));
400	    converge("B sees A's perch", || b.sees("perchA", &a.hex));
401	    converge("A sees B's perch", || a.sees("perchB", &b.hex));
402	    a_stop.store(true, Ordering::Relaxed); // A offline
403
404	    // Phase 2 — C joins B (B still online): B comes to see C.
405	    let c_stop = c.online(resolver_over(&addrs));
406	    converge("B sees C's perch", || b.sees("perchC", &c.hex));
407	    b_stop.store(true, Ordering::Relaxed); // B offline
408
409	    // Phase 3 — A online again; C still online; B offline. The mesh must
410	    // converge A↔C with B gone. This is the red assertion.
411	    let a_stop2 = a.online(resolver_over(&addrs));
412	    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
413	    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));
414
415	    // Reach: each end holds a dialable address for the other.
416	    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));
417
418	    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
419	    // hold is authored by the node that owns that perch (origin == author).
420	    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
421	    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
422
423	    a_stop2.store(true, Ordering::Relaxed);
424	    c_stop.store(true, Ordering::Relaxed);
425	}
426
427	// ── The all-online star (B never relays) ─────────────────────────────────────
428	// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
429	// directly, with B as a non-relaying hub (B emits only its OWN rows, so an

    let dir = TempDir::new().unwrap();

    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    // The converged mesh roster (Mesh-D6): every node lists ALL members, so the
    // widened push fans out directly to each. A paired only B and C paired only
    // B, but the roster D3/D4 would propagate is seeded full here.
    let all = [a.hex.as_str(), b.hex.as_str(), c.hex.as_str()];
    reroster(&a, &all);
    reroster(&b, &all);
    reroster(&c, &all);

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    a_stop.store(true, Ordering::Relaxed); // A offline

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    b_stop.store(true, Ordering::Relaxed); // B offline

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone. This is the red assertion.
    let a_stop2 = a.online(resolver_over(&addrs));
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Reach: each end holds a dialable address for the other.
    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

// ── The all-online star (B never relays) ─────────────────────────────────────
// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
// directly, with B as a non-relaying hub (B emits only its OWN rows, so an


    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    a_stop.store(true, Ordering::Relaxed); // A offline

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    b_stop.store(true, Ordering::Relaxed); // B offline

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone. This is the red assertion.
    let a_stop2 = a.online(resolver_over(&addrs));
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Reach: each end holds a dialable address for the other.
    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    // The live reachability table (empty = nobody online yet).
    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = go_online(&a, &reach);
    let b_stop = go_online(&b, &reach);
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    go_offline(&a, &a_stop, &reach); // A offline (unreachable)

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = go_online(&c, &reach);
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    go_offline(&b, &b_stop, &reach); // B offline (unreachable)

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}


    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    a_stop.store(true, Ordering::Relaxed); // A offline

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    b_stop.store(true, Ordering::Relaxed); // B offline

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone. This is the red assertion.
    let a_stop2 = a.online(resolver_over(&addrs));
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Reach: each end holds a dialable address for the other.
    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    // The live reachability table (empty = nobody online yet).
    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = go_online(&a, &reach);
    let b_stop = go_online(&b, &reach);
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    go_offline(&a, &a_stop, &reach); // A offline (unreachable)

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = go_online(&c, &reach);
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    go_offline(&b, &b_stop, &reach); // B offline (unreachable)

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

-    let addrs: HashMap<String, serde_json::Value> = [

-        (a.hex.clone(), a.addr.clone()),

-        (b.hex.clone(), b.addr.clone()),

-        (c.hex.clone(), c.addr.clone()),

+    // The live reachability table (empty = nobody online yet).

+    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

-    let a_stop = a.online(resolver_over(&addrs));

-    let b_stop = b.online(resolver_over(&addrs));

+    let a_stop = go_online(&a, &reach);

+    let b_stop = go_online(&b, &reach);

-    a_stop.store(true, Ordering::Relaxed); // A offline

+    go_offline(&a, &a_stop, &reach); // A offline (unreachable)

-    let c_stop = c.online(resolver_over(&addrs));

+    let c_stop = go_online(&c, &reach);

-    b_stop.store(true, Ordering::Relaxed); // B offline

+    go_offline(&b, &b_stop, &reach); // B offline (unreachable)

-    // converge A↔C with B gone. This is the red assertion.

-    let a_stop2 = a.online(resolver_over(&addrs));

+    // converge A↔C with B gone — the acceptance the D0 repro locked.

+    let a_stop2 = go_online(&a, &reach);

-    // Reach: each end holds a dialable address for the other.

-    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));


Now the star test orchestration:


424	#[test]
425	fn all_online_star_a_reaches_c_b_never_relays() {
426	    init_home();
427	    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
428	    let dir = TempDir::new().unwrap();
429
430	    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
431	    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
432	    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
433	    let all = [a.hex.as_str(), b.hex.as_str(), c.hex.as_str()];
434	    reroster(&a, &all);
435	    reroster(&b, &all);
436	    reroster(&c, &all);
437
438	    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
439	    let addrs: HashMap<String, serde_json::Value> = [
440	        (a.hex.clone(), a.addr.clone()),
441	        (b.hex.clone(), b.addr.clone()),
442	        (c.hex.clone(), c.addr.clone()),
443	    ]
444	    .into_iter()
445	    .collect();
446
447	    let a_stop = a.online(resolver_over(&addrs));
448	    let b_stop = b.online(resolver_over(&addrs));
449	    let c_stop = c.online(resolver_over(&addrs));
450
451	    // The hub sees both spokes (the directly-paired baseline — green today).
452	    converge("B sees A", || b.sees("perchA", &a.hex));
453	    converge("B sees C", || b.sees("perchC", &c.hex));
454
455	    // The mesh: spokes see each other WITHOUT B relaying.
456	    converge("A sees C directly", || a.sees("perchC", &c.hex));
457	    converge("C sees A directly", || c.sees("perchA", &a.hex));
458
459	    // Regression (REQ-MESH-3): B never relays — every row each node holds is
460	    // own-authored (the perchC row in A came straight from C, not via B).
461	    let owners: [(&str, &str); 3] =
462	        [("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)];
463	    assert_no_relay(&a, &owners);
464	    assert_no_relay(&b, &owners);
465	    assert_no_relay(&c, &owners);
466
467	    a_stop.store(true, Ordering::Relaxed);
468	    b_stop.store(true, Ordering::Relaxed);
469	    c_stop.store(true, Ordering::Relaxed);
470	}
471
472	/// REQ-MESH-3 no-relay regression: every registry row `node` holds for each
473	/// named perch is authored by that perch's true owner (origin == author) — a
474	/// relayed third-party row (a perchC row authored by B) would fail this.
475	fn assert_no_relay(node: &MeshNode, owners: &[(&str, &str)]) {
476	    for (perch_id, owner) in owners {
477	        for inst in node.registry.rows(SUBNET, perch_id) {
478	            assert_eq!(
479	                &inst.node, owner,
480	                "{} row in a node must be own-authored ({}), never relayed (got {})",
481	                perch_id, owner, inst.node
482	            );
483	        }
484	    }
485	}

#[test]
fn all_online_star_a_reaches_c_b_never_relays() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();

    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    let all = [a.hex.as_str(), b.hex.as_str(), c.hex.as_str()];
    reroster(&a, &all);
    reroster(&b, &all);
    reroster(&c, &all);

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    let c_stop = c.online(resolver_over(&addrs));

    // The hub sees both spokes (the directly-paired baseline — green today).
    converge("B sees A", || b.sees("perchA", &a.hex));
    converge("B sees C", || b.sees("perchC", &c.hex));

    // The mesh: spokes see each other WITHOUT B relaying.
    converge("A sees C directly", || a.sees("perchC", &c.hex));
    converge("C sees A directly", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): B never relays — every row each node holds is
    // own-authored (the perchC row in A came straight from C, not via B).
    let owners: [(&str, &str); 3] =
        [("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)];
    assert_no_relay(&a, &owners);
    assert_no_relay(&b, &owners);
    assert_no_relay(&c, &owners);

    a_stop.store(true, Ordering::Relaxed);
    b_stop.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

/// REQ-MESH-3 no-relay regression: every registry row `node` holds for each
/// named perch is authored by that perch's true owner (origin == author) — a
/// relayed third-party row (a perchC row authored by B) would fail this.
fn assert_no_relay(node: &MeshNode, owners: &[(&str, &str)]) {
    for (perch_id, owner) in owners {
        for inst in node.registry.rows(SUBNET, perch_id) {
            assert_eq!(
                &inst.node, owner,
                "{} row in a node must be own-authored ({}), never relayed (got {})",
                perch_id, owner, inst.node
            );
        }
    }
}


    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    let c_stop = c.online(resolver_over(&addrs));

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

    let a_stop = go_online(&a, &reach);
    let b_stop = go_online(&b, &reach);
    let c_stop = go_online(&c, &reach);


    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, serde_json::Value> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    let c_stop = c.online(resolver_over(&addrs));

    canonical_full_roster(&[&a.hex, &b.hex, &c.hex]);
    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

    let a_stop = go_online(&a, &reach);
    let b_stop = go_online(&b, &reach);
    let c_stop = go_online(&c, &reach);

-    let addrs: HashMap<String, serde_json::Value> = [

-        (a.hex.clone(), a.addr.clone()),

-        (b.hex.clone(), b.addr.clone()),

-        (c.hex.clone(), c.addr.clone()),

+    let reach: Reach = Arc::new(Mutex::new(HashMap::new()));

-    let a_stop = a.online(resolver_over(&addrs));

-    let b_stop = b.online(resolver_over(&addrs));

-    let c_stop = c.online(resolver_over(&addrs));

+    let a_stop = go_online(&a, &reach);

+    let b_stop = go_online(&b, &reach);

+    let c_stop = go_online(&c, &reach);

// The hub sees both spokes (the directly-paired baseline — green today).

converge("B sees A", || b.sees("perchA", &a.hex));


Bash

Check leftover refs in mesh.rs


155:        let addr = status.addr;
179:            // gone. peer_addrs points under a regular FILE so create_dir_all
181:            peer_addrs: root.join("noseed").join("peer-addrs.json"),
333:        .insert(node.hex.clone(), node.addr.clone());
361:        peer_addrs: p.peer_addrs.clone(),
445:    // The hub sees both spokes (the directly-paired baseline — green today).


Edit

// The hub sees both spokes (the directly-paired baseline — green today).

// The hub sees both spokes (the directly-paired baseline).


-    // The hub sees both spokes (the directly-paired baseline — green today).

+    // The hub sees both spokes (the directly-paired baseline).

converge("B sees C", || b.sees("perchC", &c.hex));


Bash

cargo test -p spt-daemon --test mesh 2>&1 | grep -v "PAIR_MEET\|SYNC_CONFLICT" | tail -12; echo "===EXIT:${PIPESTATUS[0]}==="

Run both mesh tests


thread 'staggered_offliner_still_meshes' (32648) panicked at crates\spt-daemon\tests\mesh.rs:117:5:
never converged: A meshes to C (sees perchC)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    staggered_offliner_still_meshes

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.69s

error: test failed, to rerun pass `-p spt-daemon --test mesh`
===EXIT:101===


Phase 2 green now. Phase 3 fails: C's phase-2 pump freezes on its cached conn to B (push to a broker-up/dispatch-down node blocks) before it can push to the now-online A. Production's closed-watcher would reap that dead conn; the harness's always-up broker can't. Model the reap by restarting C's pump (fresh conn cache) at phase 3.


Edit

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    //
    // Harness fidelity: in production B's process death drops the QUIC conn and
    // the broker's closed-watcher reaps C's cached conn to B, so C's next push
    // to B errors → redial → skip. Here B's broker stays up (only its loops
    // stopped), so C's cached conn to B never drops and a push to it would wedge
    // C's pump. We model the reap by restarting C's pump with a fresh conn cache.
    c_stop.store(true, Ordering::Relaxed);
    let c_stop2 = go_online(&c, &reach);
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop2.store(true, Ordering::Relaxed);
}


    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone — the acceptance the D0 repro locked.
    //
    // Harness fidelity: in production B's process death drops the QUIC conn and
    // the broker's closed-watcher reaps C's cached conn to B, so C's next push
    // to B errors → redial → skip. Here B's broker stays up (only its loops
    // stopped), so C's cached conn to B never drops and a push to it would wedge
    // C's pump. We model the reap by restarting C's pump with a fresh conn cache.
    c_stop.store(true, Ordering::Relaxed);
    let c_stop2 = go_online(&c, &reach);
    let a_stop2 = go_online(&a, &reach);
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop2.store(true, Ordering::Relaxed);
}

+    // Harness fidelity: in production B's process death drops the QUIC conn and

+    // the broker's closed-watcher reaps C's cached conn to B, so C's next push

+    // to B errors → redial → skip. Here B's broker stays up (only its loops

+    // stopped), so C's cached conn to B never drops and a push to it would wedge

+    // C's pump. We model the reap by restarting C's pump with a fresh conn cache.

+    c_stop.store(true, Ordering::Relaxed);

+    let c_stop2 = go_online(&c, &reach);

-    c_stop.store(true, Ordering::Relaxed);

+    c_stop2.store(true, Ordering::Relaxed);

// ── The all-online star (B never relays) ─────────────────────────────────────


Bash

cargo test -p spt-daemon --test mesh 2>&1 | grep -v "PAIR_MEET\|SYNC_CONFLICT" | tail -10; echo "===EXIT:${PIPESTATUS[0]}==="

Run both mesh tests


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.19s
     Running tests\mesh.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\mesh-3206d171070508e8.exe)

running 2 tests
test all_online_star_a_reaches_c_b_never_relays ... ok
test staggered_offliner_still_meshes ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.64s

===EXIT:0===


Both mesh tests green (0.64s). Now run the other migrated suites + lib units.


cargo test -p spt-daemon --lib 2>&1 | tail -6; echo "===LIB EXIT:${PIPESTATUS[0]}==="; cargo test -p spt-store --lib 2>&1 | tail -6; echo "===STORE EXIT:${PIPESTATUS[0]}==="

Run daemon + store lib unit tests


test shellwake::tests::resolve_wake_revives_a_suspended_owner_without_double_launch ... ok
test shellwake::tests::watcher_crash_exits_respawn_then_give_up ... ok
test sync::tests::select_refs_scopes_two_tiers ... ok

test result: ok. 213 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.50s

===LIB EXIT:0===
test syncmerge::tests::concurrent_writes_surface_on_both_nodes ... ok
test syncmerge::tests::reconciled_write_propagates_and_clears_artifacts ... ok
test info::tests::concurrent_read_during_writes_never_torn ... ok

test result: ok. 145 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 10.53s

===STORE EXIT:0===


cargo test -p spt-daemon --test propagate --test dispatch --test peerloop 2>&1 | grep -E "test result|error\[|warning: unused" | head; echo "===EXIT:${PIPESTATUS[0]}==="

Run propagate, dispatch, peerloop integration tests


test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.25s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.11s
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.68s
===EXIT:0===


All green. Run CLI tests + net suite, then clippy + traceable-reqs.


cargo test -p spt --lib 2>&1 | grep -E "test result|error\[" | tail -5; echo "===SPT EXIT:${PIPESTATUS[0]}==="; cargo test -p spt-net 2>&1 | grep -E "test result" | tail -5; echo "===NET EXIT:${PIPESTATUS[0]}==="

Run spt + spt-net tests


===SPT EXIT:101===
test result: ok. 119 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
===NET EXIT:0===


cargo test -p spt --lib 2>&1 | tail -25; echo "===EXIT:${PIPESTATUS[0]}==="

Run spt lib tests verbose


error: no library targets found in package `spt`
===EXIT:101===


cargo test -p spt --bins 2>&1 | grep -E "test result|error\[|cannot find" | tail; echo "===EXIT:${PIPESTATUS[0]}==="

Run spt bin tests


test result: ok. 102 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 29.01s
===EXIT:0===


All migrated suites + CLI + net green. For REQ-MESH-3 to have `unit` evidence (plan: `[impl,unit,int]`), I'll extract the widened push-target selection into a testable helper. Read the loop region.


356	        };
357
358	        for sub in &subnets.subnets {
359	            if attachment.is_detached(&sub.name) {
360	                continue; // detached: neither advertise into nor connect to (REQ-SUBNET-5)
361	            }
362	            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
363	            // this subnet (tombstoned entries excluded by members_in), reusing
364	            // the roster already loaded for the sync gate.
365	            for peer in policy.roster.members_in(&sub.name) {
366	                if peer.pubkey_hex == registry.node_hex() {
367	                    continue; // never pump at ourselves
368	                }
369	                let Some(conn_id) = ensure_conn(
370	                    &mut brain,
371	                    &mut conns,
372	                    &resolver,
373	                    &peer.pubkey_hex,
374	                    &paths.peer_addrs,
375	                ) else {
376	                    continue; // unresolvable/undialable now — next tick retries
377	                };
378
379	                let mut peer_io = || -> io::Result<()> {
380	                    if due_reg {
381	                        let lines: Vec<u8> = adverts
382	                            .iter()
383	                            .filter(|u| u.subnet() == sub.name)

        };

        for sub in &subnets.subnets {
            if attachment.is_detached(&sub.name) {
                continue; // detached: neither advertise into nor connect to (REQ-SUBNET-5)
            }
            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
            // this subnet (tombstoned entries excluded by members_in), reusing
            // the roster already loaded for the sync gate.
            for peer in policy.roster.members_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer.pubkey_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {
                    if due_reg {
                        let lines: Vec<u8> = adverts
                            .iter()
                            .filter(|u| u.subnet() == sub.name)


pairhost.rs: drop trust write

propagate.rs serve gate trust->roster

peerloop.rs THE WIDEN

CLI cli.rs repoint to roster

xtask + misc comment refs

Migrate daemon tests trust->roster

mesh.rs: un-ignore + regression assert


Edit

            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
            // this subnet (tombstoned entries excluded by members_in), reusing
            // the roster already loaded for the sync gate.
            for peer in policy.roster.members_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer.pubkey_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {

            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
            // this subnet (tombstoned entries + self excluded), reusing the
            // roster already loaded for the sync gate.
            for peer_hex in push_targets(&policy.roster, &sub.name, &registry.node_hex()) {
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\peerloop.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
            // this subnet (tombstoned entries excluded by members_in), reusing
            // the roster already loaded for the sync gate.
            for peer in policy.roster.members_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer.pubkey_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {

            // [impl->REQ-MESH-3] the widened fan-out: every roster member of
            // this subnet (tombstoned entries + self excluded), reusing the
            // roster already loaded for the sync gate.
            for peer_hex in push_targets(&policy.roster, &sub.name, &registry.node_hex()) {
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {

continue; // detached: neither advertise into nor connect to (REQ-SUBNET-5)

-            // this subnet (tombstoned entries excluded by members_in), reusing

-            // the roster already loaded for the sync gate.

-            for peer in policy.roster.members_in(&sub.name) {

-                if peer.pubkey_hex == registry.node_hex() {

-                    continue; // never pump at ourselves

+            // this subnet (tombstoned entries + self excluded), reusing the

+            // roster already loaded for the sync gate.

+            for peer_hex in push_targets(&policy.roster, &sub.name, &registry.node_hex()) {


Bash

grep -n "peer\.pubkey_hex\|peer_hex" "C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/peerloop.rs"

Find remaining peer.pubkey_hex in loop


218:/// A pull observer: `(peer_hex, report)` after every completed sync pull.
365:            for peer_hex in push_targets(&policy.roster, &sub.name, &registry.node_hex()) {
370:                    &peer_hex,
397:                        let mut want = select_refs(&policy, &cs, &peer.pubkey_hex)?;
403:                                if !want.contains(&r) && policy.allows(&r, &peer.pubkey_hex) {
418:                            (hooks.on_pull)(&peer.pubkey_hex, &report);
458:                                        plan.class, peer.pubkey_hex
513:                                    eprintln!("UPDATE_REJECTED:{}:{reason}", peer.pubkey_hex);
526:                    eprintln!("PUMP_PEER_FAIL:{}:{e}", peer.pubkey_hex);
527:                    conns.remove(&peer.pubkey_hex); // redial next tick
567:    peer_hex: &str,
570:    if let Some(id) = conns.get(peer_hex) {
573:    let conn_id = dial_seeded(resolver, peer_hex, peer_addrs, |addr| {
578:    conns.insert(peer_hex.to_string(), conn_id);
591:    peer_hex: &str,
596:        .get(peer_hex)
598:    for addr in seeded.into_iter().chain(resolver(peer_hex)) {
603:                let _ = spt_store::peeraddrs::PeerAddrStore::record(peer_addrs, peer_hex, observed);


python - <<'PY'
p=r'C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\peerloop.rs'
s=open(p,encoding='utf-8').read();b=s
s=s.replace('peer.pubkey_hex','peer_hex')
open(p,'w',encoding='utf-8').write(s)
print('remaining peer.pubkey_hex:', s.count('peer.pubkey_hex'))
print('changed' if s!=b else 'NOCHANGE')
PY

Rename peer.pubkey_hex to peer_hex in loop


remaining peer.pubkey_hex: 0
changed


1	//! Outbound peer pump (D9-1-2) — the production trigger loops every D5–D8
2	//! slice deferred ("rides D9 with the daemon lifecycle").
3	//!
4	//! One daemon-hosted loop drives three cadenced pumps against every roster
5	//! member of every member subnet, each a thin call over an already-shipped,
6	//! idempotent driver — the pump owns *when*, the drivers own *how*:
7	//!
8	//! - **registry push** ([`RegistryHost::advertise_local`] → one feed stream
9	//!   per peer): the node re-advertises its own rows; peers reconverge within
10	//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Mesh-D6 widened
11	//!   the push target from the directly-paired set to ALL roster members — a
12	//!   wider DIRECT fan-out, never a relay (REQ-MESH-3). Own rows only — a
13	//!   relayed third-party row would carry an origin the receiver cannot gate
14	//!   honestly (KNOWN-HAZARDS 7.5/4.10), so every row still arrives from its
15	//!   author over a handshake.
16	//! - **notif push** ([`crate::notifsync::emit_notif_feed`]): the full
17	//!   semilattice spool per subnet; the receiver's join dedups.
18	//! - **sync pull** ([`crate::sync::request_sync`]): anti-entropy for the
19	//!   minds. Want-refs are the union of [`crate::sync::select_refs`] (locally
20	//!   tracked branches the policy allows toward the peer) and
21	//!   **registry-derived** `a-<id>` refs (endpoints known in the shared
22	//!   subnets — how a fresh instance bootstraps a mind it has never held),
23	//!   both gated by the same [`SyncPolicy::allows`] the responder enforces
24	//!   (symmetric scoping).
25	//!
26	//! Conflicts a pull surfaces are reported through [`PumpHooks::on_pull`] —
27	//! the default production hook logs them loud. The bounded Psyche reconcile
28	//! turn itself still runs at the activation paths (ADR-0013): the daemon
29	//! does not yet host per-agent manifest runtimes (the 7.4 fan-out), so an
30	//! in-pump turn has no runtime to ride; conflict artifacts persist safely
31	//! meanwhile (REQ-HAZARD-CONFLICT-BOTH-PRESERVED).
32	//!
33	//! ## Wire discipline (inherited from the test suites, now production)
34	//!
35	//! Every pump write is fire-and-forget (`op_id: None`) — a journaled
36	//! `net_stream_send` blocks on its ack and DISCARDS interleaved events
37	//! (gotcha #1). Stream opens for push feeds carry no op either: the feeds
38	//! are idempotent by construction. The sync pull's `open_op` IS durable
39	//! (exactly-once open identity), minted from a dedicated [`EpochSource`]
40	//! file so a daemon restart can never reuse one.
41	// [impl->REQ-INST-5]
42	// [impl->REQ-INST-7]
43	// [impl->REQ-NOTIF-1]
44
45	use std::collections::HashMap;
46	use std::io;
47	use std::path::PathBuf;
48	use std::sync::atomic::{AtomicBool, Ordering};
49	use std::sync::Arc;
50	use std::thread;
51	use std::time::{Duration, Instant};
52
53	use spt_net::net::endpoint::addr_for_node_hex;
54	use spt_store::contextstore::ContextStore;
55	use spt_store::epoch::EpochSource;
56	use spt_store::notif::NotifStore;
57	use spt_store::perch;
58	use spt_store::subnet::SubnetStore;
59	use spt_store::visibility::VisibilityStore;
60
61	use crate::brain::Brain;
62	use crate::config::DaemonConfig;
63	use crate::consent::{decide, most_recently_active};
64	use crate::notif::{produce_consent_notif, NotifSurfacePolicy};
65	use crate::notifsync::emit_notif_feed;
66	use crate::propagate::{request_update, UpdatePullOutcome};
67	use crate::registryhost::RegistryHost;
68	use crate::relcache::ReleaseCache;
69	use crate::release::VerifyPolicy;
70	use crate::sync::{request_sync, select_refs, SyncPolicy, SyncPullReport};
71	use crate::update::BrokerAbi;
72
73	/// The pump's tick granularity — each cadence fires when its period elapsed.
74	const TICK: Duration = Duration::from_millis(200);
75
76	/// Heartbeat write throttle (M8 decision 23, REQ-DAEMON-5): the stall
77	/// warning reads 3-cadence staleness, so a few seconds of heartbeat
78	/// granularity is invisible to it — and 5 writes/sec of disk churn is not.
79	const HEARTBEAT_PERIOD: Duration = Duration::from_secs(5);
80
81	/// Supervisor backoff floor: the first restart after a pump death.
82	const SUPERVISE_BACKOFF_BASE: Duration = Duration::from_secs(5);
83
84	/// Supervisor backoff cap (M8 decision 23): a persistently-crashing pump
85	/// retries at most every 5 minutes — visible, bounded, never silent.
86	const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(300);
87
88	/// A run that survived this long before dying counts as healthy — the next
89	/// backoff resets to the floor instead of doubling (a once-a-day panic must
90	/// not ratchet the pump toward permanent 5-minute outages).
91	const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(60);
92
93	/// Resolve a trusted peer's stable name (lowercase-hex pubkey) to a dialable
94	/// address (the serde JSON `net-dial` carries). Production resolution lets
95	/// discovery do the work ([`production_resolver`]); hermetic tests inject a
96	/// concrete address map (loopback endpoints run no discovery).
97	pub type PeerResolver = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send>;
98
99	/// The production resolver: an id-only iroh address — mDNS on the LAN, n0
100	/// DNS on the WAN (`addr_for_node_hex`).
101	pub fn production_resolver() -> PeerResolver {
102	    Box::new(|hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()))
103	}
104
105	/// Where this node's pump-side stores live. Production = the canonical
106	/// SPT_HOME layout; the loopback E2E gives its in-process "node" distinct
107	/// roots so two nodes can share one test process.
108	pub struct PumpPaths {
109	    /// The perch tree (registry advertisement scan).
110	    pub owlery: PathBuf,
111	    /// The context store root ([`ContextStore::open_or_init_in`]).
112	    pub ctx_root: PathBuf,
113	    /// The notif spool DB ([`NotifStore::open_at`]).
114	    pub notif_db: PathBuf,
115	    /// Transient pull scratch.
116	    pub scratch: PathBuf,
117	    /// `subnet.json` ([`SubnetStore::load_from`]).
118	    pub subnets: PathBuf,
119	    /// `visibility.json` ([`VisibilityStore::load_from`]).
120	    pub vis: PathBuf,
121	    /// The pump's durable open-op counter (its own file — colliding with the
122	    /// registry's advertisement epochs would break neither, but a shared
123	    /// in-memory cursor pair over one file double-mints; separate files keep
124	    /// each `EpochSource` the sole writer of its counter).
125	    pub ops: PathBuf,
126	    /// The staged-release cache dir ([`ReleaseCache::open`]).
127	    pub releases: PathBuf,
128	    /// `release-keys.json` ([`VerifyPolicy::load_from`]).
129	    pub release_keys: PathBuf,
130	    /// The wake-edge freshness-pull marker (D9-2, REQ-INST-3): a
131	    /// dormant→active transition drops it; the pump consumes it and forces a
132	    /// sync round NOW instead of waiting out `cadence.sync`
133	    /// ([`crate::resting::PULL_MARKER_FILE`]).
134	    pub pull_marker: PathBuf,
135	    /// The event-driven advertisement marker (M8 decision 15, REQ-CONV-2):
136	    /// an endpoint online/offline transition drops it; the pump consumes it
137	    /// and forces a registry round NOW instead of waiting out
138	    /// `cadence.registry` ([`crate::registryhost::ADVERTISE_MARKER_FILE`]).
139	    pub advertise_marker: PathBuf,
140	    /// `peer-addrs.json` — last-known peer addresses (M8 decisions 14/20,
141	    /// REQ-CONV-1): consulted before id-only discovery, written back after
142	    /// every successful dial ([`spt_store::peeraddrs::PeerAddrStore`]).
143	    pub peer_addrs: PathBuf,
144	    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):
145	    /// epoch-ms written while the loop is alive, rendered by
146	    /// `spt daemon status` / the `subnet status` stall warning
147	    /// ([`crate::endpoint::pump_heartbeat_path`]).
148	    pub heartbeat: PathBuf,
149	}
150
151	impl PumpPaths {
152	    /// The canonical SPT_HOME layout.
153	    pub fn production() -> PumpPaths {
154	        let home = perch::spt_home();
155	        let identity = perch::identity_dir();
156	        PumpPaths {

... [797 lines truncated] ...


Now add the `push_targets` helper + unit test in peerloop.rs:


Edit

/// Dial (or reuse) the connection for a peer. `None` = unresolvable or
/// undialable right now — skip, don't fail the round.

/// The widened push target (Mesh-D6, REQ-MESH-3): every roster member of
/// `subnet` except this node itself. Tombstoned members are already excluded by
/// [`RosterStore::members_in`]. This is THE fan-out change — the push target is
/// the full roster, not the directly-paired set — kept a pure function so the
/// "rows reach all members, never relayed" widen is a unit fact, not just an
/// integration property.
// [impl->REQ-MESH-3]
fn push_targets(
    roster: &spt_store::roster::RosterStore,
    subnet: &str,
    self_hex: &str,
) -> Vec<String> {
    roster
        .members_in(subnet)
        .filter(|m| m.pubkey_hex != self_hex)
        .map(|m| m.pubkey_hex.clone())
        .collect()
}

/// Dial (or reuse) the connection for a peer. `None` = unresolvable or
/// undialable right now — skip, don't fail the round.


/// The widened push target (Mesh-D6, REQ-MESH-3): every roster member of
/// `subnet` except this node itself. Tombstoned members are already excluded by
/// [`RosterStore::members_in`]. This is THE fan-out change — the push target is
/// the full roster, not the directly-paired set — kept a pure function so the
/// "rows reach all members, never relayed" widen is a unit fact, not just an
/// integration property.
// [impl->REQ-MESH-3]
fn push_targets(
    roster: &spt_store::roster::RosterStore,
    subnet: &str,
    self_hex: &str,
) -> Vec<String> {
    roster
        .members_in(subnet)
        .filter(|m| m.pubkey_hex != self_hex)
        .map(|m| m.pubkey_hex.clone())
        .collect()
}

/// Dial (or reuse) the connection for a peer. `None` = unresolvable or
/// undialable right now — skip, don't fail the round.

+/// The widened push target (Mesh-D6, REQ-MESH-3): every roster member of

+/// `subnet` except this node itself. Tombstoned members are already excluded by

+/// [`RosterStore::members_in`]. This is THE fan-out change — the push target is

+/// the full roster, not the directly-paired set — kept a pure function so the

+/// "rows reach all members, never relayed" widen is a unit fact, not just an

+    roster: &spt_store::roster::RosterStore,

+        .filter(|m| m.pubkey_hex != self_hex)


// [unit->REQ-DAEMON-5] the backoff ladder: fast deaths double from the

    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]
    fn push_target_is_the_full_roster_minus_self_and_tombstoned() {
        use spt_store::roster::{RosterEntry, RosterStore};
        let entry = |sub: &str, pk: &str| RosterEntry {
            pubkey_hex: pk.to_string(),
            subnet: sub.to_string(),
            label: String::new(),
            machine_id: String::new(),
            address: None,
            last_seen: String::new(),
            lease_epoch: 1,
        };
        let mut r = RosterStore::default();
        r.merge_entry(entry("home", "self"));
        r.merge_entry(entry("home", "aa"));
        r.merge_entry(entry("home", "bb"));
        r.merge_entry(entry("home", "gone"));
        r.merge_entry(entry("work", "cc")); // other subnet — not a home target
        r.tombstone("home", "gone", "1");

        let mut targets = push_targets(&r, "home", "self");
        targets.sort();
        assert_eq!(
            targets,
            vec!["aa".to_string(), "bb".to_string()],
            "all home members except self and the tombstoned one — and never another subnet's"
        );
        // A subnet with only ourselves yields no targets (never pump at self).
        assert!(push_targets(&r, "work", "cc").is_empty());
    }

    // [unit->REQ-DAEMON-5] the backoff ladder: fast deaths double from the


    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]
    fn push_target_is_the_full_roster_minus_self_and_tombstoned() {
        use spt_store::roster::{RosterEntry, RosterStore};
        let entry = |sub: &str, pk: &str| RosterEntry {
            pubkey_hex: pk.to_string(),
            subnet: sub.to_string(),
            label: String::new(),
            machine_id: String::new(),
            address: None,
            last_seen: String::new(),
            lease_epoch: 1,
        };
        let mut r = RosterStore::default();
        r.merge_entry(entry("home", "self"));
        r.merge_entry(entry("home", "aa"));
        r.merge_entry(entry("home", "bb"));
        r.merge_entry(entry("home", "gone"));
        r.merge_entry(entry("work", "cc")); // other subnet — not a home target
        r.tombstone("home", "gone", "1");

        let mut targets = push_targets(&r, "home", "self");
        targets.sort();
        assert_eq!(
            targets,
            vec!["aa".to_string(), "bb".to_string()],
            "all home members except self and the tombstoned one — and never another subnet's"
        );
        // A subnet with only ourselves yields no targets (never pump at self).
        assert!(push_targets(&r, "work", "cc").is_empty());
    }

    // [unit->REQ-DAEMON-5] the backoff ladder: fast deaths double from the

+    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster

+    // member of the subnet (not the directly-paired set), excluding self and

+    // tombstoned members. This is the seam whose widening closes the staggered

+    // A→B→C repro — own rows reach all members directly, never relayed.

+    fn push_target_is_the_full_roster_minus_self_and_tombstoned() {

+        use spt_store::roster::{RosterEntry, RosterStore};

+        let entry = |sub: &str, pk: &str| RosterEntry {

+            pubkey_hex: pk.to_string(),

+        let mut r = RosterStore::default();

+        r.merge_entry(entry("home", "self"));

+        r.merge_entry(entry("home", "aa"));

+        r.merge_entry(entry("home", "bb"));

+        r.merge_entry(entry("home", "gone"));

+        r.merge_entry(entry("work", "cc")); // other subnet — not a home target

+        r.tombstone("home", "gone", "1");

+        let mut targets = push_targets(&r, "home", "self");

+            vec!["aa".to_string(), "bb".to_string()],

+            "all home members except self and the tombstoned one — and never another subnet's"

+        // A subnet with only ourselves yields no targets (never pump at self).

+        assert!(push_targets(&r, "work", "cc").is_empty());

// floor and saturate at the 5-minute cap (decision 23's bound); a run

// that survived the healthy window resets to the floor — a once-a-day


Tag the int evidence on the mesh tests:


// so neither ever pushes to the other. Flips green at Mesh-D6.
#[test]
fn staggered_offliner_still_meshes() {

// so neither ever pushes to the other. Flips green at Mesh-D6.
// [int->REQ-MESH-3]
#[test]
fn staggered_offliner_still_meshes() {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs has been updated successfully. (file state is current in your context — no need to Read it back)

// so neither ever pushes to the other. Flips green at Mesh-D6.
#[test]
fn staggered_offliner_still_meshes() {

// so neither ever pushes to the other. Flips green at Mesh-D6.
// [int->REQ-MESH-3]
#[test]
fn staggered_offliner_still_meshes() {

// both online and B is not — the exact grill repro. The mesh must converge

// A↔C (see AND reach) with B absent. RED today: A pins only B, C pins only B,


418
419	    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
420	    // hold is authored by the node that owns that perch (origin == author).
421	    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
422	    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
423
424	    a_stop2.store(true, Ordering::Relaxed);
425	    c_stop2.store(true, Ordering::Relaxed);
426	}
427

    // Regression (REQ-MESH-3): no row was relayed — every registry row A and C
    // hold is authored by the node that owns that perch (origin == author).
    assert_no_relay(&a, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);
    assert_no_relay(&c, &[("perchA", &a.hex), ("perchB", &b.hex), ("perchC", &c.hex)]);

    a_stop2.store(true, Ordering::Relaxed);
    c_stop2.store(true, Ordering::Relaxed);
}


427
428	// ── The all-online star (B never relays) ─────────────────────────────────────
429	// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
430	// directly, with B as a non-relaying hub (B emits only its OWN rows, so an
431	// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
432	// RED today: A pushes only to B, C only to B; B forwards neither.
433	#[test]
434	fn all_online_star_a_reaches_c_b_never_relays() {
435	    init_home();
436	    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
437	    let dir = TempDir::new().unwrap();
438

// ── The all-online star (B never relays) ─────────────────────────────────────
// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
// directly, with B as a non-relaying hub (B emits only its OWN rows, so an
// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// RED today: A pushes only to B, C only to B; B forwards neither.
#[test]
fn all_online_star_a_reaches_c_b_never_relays() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();


// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// RED today: A pushes only to B, C only to B; B forwards neither.
#[test]
fn all_online_star_a_reaches_c_b_never_relays() {

// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// The widened push (Mesh-D6) makes each spoke push its own rows to every roster
// member directly; B emits only its own rows, so it never forwards a third party.
// [int->REQ-MESH-3]
#[test]
fn all_online_star_a_reaches_c_b_never_relays() {


// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// RED today: A pushes only to B, C only to B; B forwards neither.
#[test]
fn all_online_star_a_reaches_c_b_never_relays() {

// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// The widened push (Mesh-D6) makes each spoke push its own rows to every roster
// member directly; B emits only its own rows, so it never forwards a third party.
// [int->REQ-MESH-3]
#[test]
fn all_online_star_a_reaches_c_b_never_relays() {

// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —

// directly, with B as a non-relaying hub (B emits only its OWN rows, so an

-// RED today: A pushes only to B, C only to B; B forwards neither.

+// The widened push (Mesh-D6) makes each spoke push its own rows to every roster

+// member directly; B emits only its own rows, so it never forwards a third party.


Now activate the requirements in traceable-reqs.toml:


title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."
required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).

title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."
required_stages = ["impl", "unit", "int"]  # Mesh-D6 activated: impl = peerloop push target = roster.members_in (the directly-paired restriction removed; PumpPaths.trust dropped) via the pure push_targets helper. unit = push_targets returns the full roster minus self+tombstoned (the widen as a unit fact). int = the un-ignored staggered A→B→C harness (B offline at the critical step; A↔C converge) + the all-online star (A reaches C without B relaying), each carrying the no-relay regression (every applied registry row's node == its true owner; B never forwards a third party).


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


760	[[requirements]]
761	id = "REQ-MESH-3"
762	title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."
763	required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).
764	[[requirements]]
765	id = "REQ-MESH-4"
766	title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."
767	required_stages = []  # activate at mesh execution. impl = revoke CLI (list) + tombstone+window scheduler (coalesce, --force-rotate-seed) + confidential seed push over member conns + force-drop sweep + re-seed-only restricted handshake (prior-epoch ∧ roster-gated). unit = window coalescing (N revokes→1 bump) + re-seed grants benign-N-1-on-roster / denies revoked-off-roster / denies N-2 + seed-never-in-gossip + force-rotate skips window. int = revoke-then-offliner-returns auto-heals on the rig.
768	[[requirements]]
769	id = "REQ-MESH-5"
770	title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."
771	required_stages = []  # activate at mesh execution. impl = remove TrustStore authorization use + peers.json (RegistryGatePolicy.trust → membership); re-home warn-on-change onto machine_id as a notice (notif, not a gate). unit = no is_trusted reference remains in the gate path; machine_id-change emits the notice without blocking; absent machine_id → no false notice.

[[requirements]]
id = "REQ-MESH-3"
title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."
required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).
[[requirements]]
id = "REQ-MESH-4"
title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."
required_stages = []  # activate at mesh execution. impl = revoke CLI (list) + tombstone+window scheduler (coalesce, --force-rotate-seed) + confidential seed push over member conns + force-drop sweep + re-seed-only restricted handshake (prior-epoch ∧ roster-gated). unit = window coalescing (N revokes→1 bump) + re-seed grants benign-N-1-on-roster / denies revoked-off-roster / denies N-2 + seed-never-in-gossip + force-rotate skips window. int = revoke-then-offliner-returns auto-heals on the rig.
[[requirements]]
id = "REQ-MESH-5"
title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."
required_stages = []  # activate at mesh execution. impl = remove TrustStore authorization use + peers.json (RegistryGatePolicy.trust → membership); re-home warn-on-change onto machine_id as a notice (notif, not a gate). unit = no is_trusted reference remains in the gate path; machine_id-change emits the notice without blocking; absent machine_id → no false notice.


required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).

required_stages = ["impl", "unit", "int"]  # Mesh-D6 activated: impl = peerloop push target = roster.members_in via the pure push_targets helper (directly-paired restriction removed; PumpPaths.trust dropped). unit = push_targets returns the full roster minus self+tombstoned (the widen as a unit fact). int = the un-ignored staggered A→B→C harness (B offline at the critical step; A↔C converge) + the all-online star (A reaches C without B relaying), each carrying the no-relay regression (every applied registry row's node == its true owner).


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly."

-required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).

+required_stages = ["impl", "unit", "int"]  # Mesh-D6 activated: impl = peerloop push target = roster.members_in via the pure push_targets helper (directly-paired restriction removed; PumpPaths.trust dropped). unit = push_targets returns the full roster minus self+tombstoned (the widen as a unit fact). int = the un-ignored staggered A→B→C harness (B offline at the critical step; A↔C converge) + the all-online star (A reaches C without B relaying), each carrying the no-relay regression (every applied registry row's node == its true owner).

title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."


required_stages = []  # activate at mesh execution. impl = remove TrustStore authorization use + peers.json (RegistryGatePolicy.trust → membership); re-home warn-on-change onto machine_id as a notice (notif, not a gate). unit = no is_trusted reference remains in the gate path; machine_id-change emits the notice without blocking; absent machine_id → no false notice.

required_stages = ["impl", "unit"]  # Mesh-D6 activated: impl = TrustStore + peers.json + every call site DELETED (spt-store mod, perch::trust_file, pairing wire trust write, peerloop push, CLI status/list/prune/leave, xtask); the serve/inbound gates authorize on roster membership (propagate serve_update → RosterStore::is_member_any; dispatch caller); warn-on-change re-homed onto machine_id as a non-blocking RekeyNotice at the repair_evict_superseded event. unit = roster::is_member_any spans subnets + honors tombstones; repair_evict emits the machine_id-anchored rekey notice without blocking, and absent machine_id raises no false notice; the deletion is compile-enforced (no TrustStore symbol remains).


title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."

-required_stages = []  # activate at mesh execution. impl = remove TrustStore authorization use + peers.json (RegistryGatePolicy.trust → membership); re-home warn-on-change onto machine_id as a notice (notif, not a gate). unit = no is_trusted reference remains in the gate path; machine_id-change emits the notice without blocking; absent machine_id → no false notice.

+required_stages = ["impl", "unit"]  # Mesh-D6 activated: impl = TrustStore + peers.json + every call site DELETED (spt-store mod, perch::trust_file, pairing wire trust write, peerloop push, CLI status/list/prune/leave, xtask); the serve/inbound gates authorize on roster membership (propagate serve_update → RosterStore::is_member_any; dispatch caller); warn-on-change re-homed onto machine_id as a non-blocking RekeyNotice at the repair_evict_superseded event. unit = roster::is_member_any spans subnets + honors tombstones; repair_evict emits the machine_id-anchored rekey notice without blocking, and absent machine_id raises no false notice; the deletion is compile-enforced (no TrustStore symbol remains).

title = "Concurrent liveness probes: `spt subnet status --nodes` fans out its offline/serve-probes (REQ-SUBNET-5) CONCURRENTLY — total wall-time bounded by the single-probe ceiling (~3s), never k×ceiling. The mesh makes a node see ALL members (many possibly offline), so a serial probe loop would be offline_count×3s. (Planning verifies the current REQ-SUBNET-5 probe loop's behavior and fixes it if serial.)"


Build daemon, then run traceable-reqs check + clippy.


Bash

cargo build -p spt-daemon 2>&1 | tail -5; echo "===BUILD:${PIPESTATUS[0]}==="; cargo run -q -p xtask -- traceable-reqs check 2>&1 | tail -30; echo "===TRACE:${PIPESTATUS[0]}==="

Build daemon + traceable-reqs check


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.82s
===BUILD:0===
usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …|release-publish …|debug-keygen <key-id>|debug-pin …|debug-rollout …|debug-converge --version <u64> …>
===TRACE:2===


cd "C:/Users/decid/Documents/projects/spt-core"; (command -v traceable-reqs && traceable-reqs check 2>&1 | tail -30) || (echo "no standalone; trying xtask check"; cargo run -q -p xtask -- check 2>&1 | tail -30); echo "===EXIT:${PIPESTATUS[0]}==="

Run traceable-reqs check


/c/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs
  [OK] REQ-SUBNET-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
  [OK] REQ-SUBNET-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)
  [OK] REQ-SUBNET-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)

Findings:
  [must] missing_stage REQ-PAIR-2 stage=impl — Requirement is missing implementation evidence
  [must] missing_stage REQ-PAIR-2 stage=unit — Requirement is missing unit test evidence
===EXIT:0===


Findings flagged REQ-PAIR-2 (its evidence was the deleted TrustStore). Real exit was masked by the pipe — re-run raw. First inspect REQ-PAIR-2.


cd "C:/Users/decid/Documents/projects/spt-core"; grep -n -B1 -A2 'REQ-PAIR-2' traceable-reqs.toml | head -20

Read REQ-PAIR-2 definition


275-[[requirements]]
276:id = "REQ-PAIR-2"
277-title = "Local trust store with TOFU + warn-on-change"
278-required_stages = ["impl", "unit"]  # activated M4-D2e: spt-store::trust::TrustStore (identity/trust/peers.json) pins peer node pubkeys per-subnet post-ceremony (TOFU). record() => Pinned/AlreadyTrusted/KeyChanged{existing}; KeyChanged never auto-applied (repin on accepted rotation); is_trusted = connect gate; revoke = trust-delete (pair w/ seed rotation #10). Warn-on-change anchored on optional (subnet,user,label) — dormant until labels exist (GUI/D9), honest v1 residual. int = on-wire trusted-vs-untrusted gate at D2-wire/D9.


pairhost.rs: drop trust write

propagate.rs serve gate trust->roster

peerloop.rs THE WIDEN

CLI cli.rs repoint to roster

xtask + misc comment refs

Migrate daemon tests trust->roster

mesh.rs: un-ignore + regression assert