/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, drop their trust rows AND tombstone
/// their roster entries (this subnet's scope — the ceremony proved control of
/// THIS subnet's code, nothing about others), rewrite the disk snapshot, and
/// note the hexes in the eviction marker so the pump clears the in-memory rows
/// on its next tick. The caller persists both `trust` and `roster`.
///
/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the
/// inbound gates authorize on [`RosterStore::is_member`], not trust, so a
/// re-keyed identity that kept its roster entry would stay a member. Dropping
/// only the (no-longer-consulted) trust row would leave the superseded key
/// authorized — the gap this tombstone closes. Tombstone (not a bare remove)
/// because the roster is a grow-set: a plain delete re-merges from any peer
/// that still lists the dead key. The D3 tombstone primitive dominates +
/// suppresses reinsert; a fresh re-pair of that key would clear it (decision
/// 10), exactly as the joiner's own key is cleared at the call site.
// [impl->REQ-SUBNET-7]
// [impl->REQ-MESH-5]
pub fn repair_evict_superseded(
    trust: &mut TrustStore,
    roster: &mut spt_store::roster::RosterStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return; // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return;
    }
    for hex in &dead {
        let dropped = trust.revoke(subnet, hex);
        // The gate-effective eviction (Mesh-D5a moved the gate to the roster).
        roster.tombstone(subnet, hex, &wall_ms().to_string());
        eprintln!(
            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {}, roster tombstoned)",
            hex,
            intro.label,
            spt_net::net::registry::key_prefix(joiner_hex),
            if dropped { "dropped" } else { "absent" },
        );
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
}

/// One re-pair rekey awareness notice (Mesh-D6, REQ-MESH-5): the machine that
/// previously presented `old_hex` has re-paired under `new_hex`. The DEMOTED
/// form of the former label-anchored warn-on-change — anchored on the hashed
/// `machine_id` (not the spoofable label), surfaced as a non-blocking notice at
/// the same event as the REQ-SUBNET-7 eviction, **never a gate**. Absent a
/// machine id the superseded match never fires, so no false notice is raised.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RekeyNotice {
    /// The machine's advertised node label (display only).
    pub label: String,
    /// The hashed machine id the match anchored on.
    pub machine_id: String,
    /// The superseded key (the one last seen for this machine).
    pub old_hex: String,
    /// The fresh key the machine now presents (the joiner).
    pub new_hex: String,
}

/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, tombstone their roster entries (this
/// subnet's scope — the ceremony proved control of THIS subnet's code, nothing
/// about others), rewrite the disk snapshot, and note the hexes in the eviction
/// marker so the pump clears the in-memory rows on its next tick. The caller
/// persists `roster`. Returns one [`RekeyNotice`] per superseded identity (the
/// demoted warn-on-change, REQ-MESH-5) — empty when nothing supersedes.
///
/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the
/// inbound gates authorize on [`RosterStore::is_member`], so a re-keyed identity
/// that kept its roster entry would stay a member. Tombstone (not a bare remove)
/// because the roster is a grow-set: a plain delete re-merges from any peer
/// that still lists the dead key. The D3 tombstone primitive dominates +
/// suppresses reinsert; a fresh re-pair of that key would clear it (decision
/// 10), exactly as the joiner's own key is cleared at the call site. (Mesh-D6
/// deleted the former trust-row drop — the roster is the sole membership record.)
// [impl->REQ-SUBNET-7]
// [impl->REQ-MESH-5]
pub fn repair_evict_superseded(
    roster: &mut spt_store::roster::RosterStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) -> Vec<RekeyNotice> {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return Vec::new(); // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return Vec::new();
    }
    let mut notices = Vec::with_capacity(dead.len());
    for hex in &dead {
        // The gate-effective eviction (Mesh-D5a moved the gate to the roster).
        roster.tombstone(subnet, hex, &wall_ms().to_string());
        // The demoted warn-on-change (REQ-MESH-5): a machine_id-anchored
        // awareness notice, never a gate. Absent a machine id, `dead` is
        // empty (superseded_identities requires the id match) — so no false
        // notice is possible here.
        eprintln!(
            "MACHINE_REKEYED:{subnet} machine '{}' (id {}…): last seen as {}…, now presents {}… \
             (roster tombstoned — awareness only, not a block)",
            intro.label,
            spt_net::net::registry::key_prefix(&intro.machine_id),
            spt_net::net::registry::key_prefix(hex),
            spt_net::net::registry::key_prefix(joiner_hex),
        );
        notices.push(RekeyNotice {
            label: intro.label.clone(),
            machine_id: intro.machine_id.clone(),
            old_hex: hex.clone(),
            new_hex: joiner_hex.to_string(),
        });
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
    notices
}


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

/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, drop their trust rows AND tombstone
/// their roster entries (this subnet's scope — the ceremony proved control of
/// THIS subnet's code, nothing about others), rewrite the disk snapshot, and
/// note the hexes in the eviction marker so the pump clears the in-memory rows
/// on its next tick. The caller persists both `trust` and `roster`.
///
/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the
/// inbound gates authorize on [`RosterStore::is_member`], not trust, so a
/// re-keyed identity that kept its roster entry would stay a member. Dropping
/// only the (no-longer-consulted) trust row would leave the superseded key
/// authorized — the gap this tombstone closes. Tombstone (not a bare remove)
/// because the roster is a grow-set: a plain delete re-merges from any peer
/// that still lists the dead key. The D3 tombstone primitive dominates +
/// suppresses reinsert; a fresh re-pair of that key would clear it (decision
/// 10), exactly as the joiner's own key is cleared at the call site.
// [impl->REQ-SUBNET-7]
// [impl->REQ-MESH-5]
pub fn repair_evict_superseded(
    trust: &mut TrustStore,
    roster: &mut spt_store::roster::RosterStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return; // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return;
    }
    for hex in &dead {
        let dropped = trust.revoke(subnet, hex);
        // The gate-effective eviction (Mesh-D5a moved the gate to the roster).
        roster.tombstone(subnet, hex, &wall_ms().to_string());
        eprintln!(
            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {}, roster tombstoned)",
            hex,
            intro.label,
            spt_net::net::registry::key_prefix(joiner_hex),
            if dropped { "dropped" } else { "absent" },
        );
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
}

/// One re-pair rekey awareness notice (Mesh-D6, REQ-MESH-5): the machine that
/// previously presented `old_hex` has re-paired under `new_hex`. The DEMOTED
/// form of the former label-anchored warn-on-change — anchored on the hashed
/// `machine_id` (not the spoofable label), surfaced as a non-blocking notice at
/// the same event as the REQ-SUBNET-7 eviction, **never a gate**. Absent a
/// machine id the superseded match never fires, so no false notice is raised.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RekeyNotice {
    /// The machine's advertised node label (display only).
    pub label: String,
    /// The hashed machine id the match anchored on.
    pub machine_id: String,
    /// The superseded key (the one last seen for this machine).
    pub old_hex: String,
    /// The fresh key the machine now presents (the joiner).
    pub new_hex: String,
}

/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, tombstone their roster entries (this
/// subnet's scope — the ceremony proved control of THIS subnet's code, nothing
/// about others), rewrite the disk snapshot, and note the hexes in the eviction
/// marker so the pump clears the in-memory rows on its next tick. The caller
/// persists `roster`. Returns one [`RekeyNotice`] per superseded identity (the
/// demoted warn-on-change, REQ-MESH-5) — empty when nothing supersedes.
///
/// The roster tombstone is the load-bearing eviction since Mesh-D5a: the
/// inbound gates authorize on [`RosterStore::is_member`], so a re-keyed identity
/// that kept its roster entry would stay a member. Tombstone (not a bare remove)
/// because the roster is a grow-set: a plain delete re-merges from any peer
/// that still lists the dead key. The D3 tombstone primitive dominates +
/// suppresses reinsert; a fresh re-pair of that key would clear it (decision
/// 10), exactly as the joiner's own key is cleared at the call site. (Mesh-D6
/// deleted the former trust-row drop — the roster is the sole membership record.)
// [impl->REQ-SUBNET-7]
// [impl->REQ-MESH-5]
pub fn repair_evict_superseded(
    roster: &mut spt_store::roster::RosterStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) -> Vec<RekeyNotice> {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return Vec::new(); // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return Vec::new();
    }
    let mut notices = Vec::with_capacity(dead.len());
    for hex in &dead {
        // The gate-effective eviction (Mesh-D5a moved the gate to the roster).
        roster.tombstone(subnet, hex, &wall_ms().to_string());
        // The demoted warn-on-change (REQ-MESH-5): a machine_id-anchored
        // awareness notice, never a gate. Absent a machine id, `dead` is
        // empty (superseded_identities requires the id match) — so no false
        // notice is possible here.
        eprintln!(
            "MACHINE_REKEYED:{subnet} machine '{}' (id {}…): last seen as {}…, now presents {}… \
             (roster tombstoned — awareness only, not a block)",
            intro.label,
            spt_net::net::registry::key_prefix(&intro.machine_id),
            spt_net::net::registry::key_prefix(hex),
            spt_net::net::registry::key_prefix(joiner_hex),
        );
        notices.push(RekeyNotice {
            label: intro.label.clone(),
            machine_id: intro.machine_id.clone(),
            old_hex: hex.clone(),
            new_hex: joiner_hex.to_string(),
        });
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
    notices
}

+/// One re-pair rekey awareness notice (Mesh-D6, REQ-MESH-5): the machine that

+/// previously presented `old_hex` has re-paired under `new_hex`. The DEMOTED

+/// form of the former label-anchored warn-on-change — anchored on the hashed

+/// `machine_id` (not the spoofable label), surfaced as a non-blocking notice at

+/// the same event as the REQ-SUBNET-7 eviction, **never a gate**. Absent a

+/// machine id the superseded match never fires, so no false notice is raised.

+    /// The machine's advertised node label (display only).

+    /// The hashed machine id the match anchored on.

+    /// The superseded key (the one last seen for this machine).

+    /// The fresh key the machine now presents (the joiner).

-/// identities from the subnet's snapshot, drop their trust rows AND tombstone

-/// their roster entries (this subnet's scope — the ceremony proved control of

-/// THIS subnet's code, nothing about others), rewrite the disk snapshot, and

-/// note the hexes in the eviction marker so the pump clears the in-memory rows

-/// on its next tick. The caller persists both `trust` and `roster`.

+/// identities from the subnet's snapshot, tombstone their roster entries (this

+/// subnet's scope — the ceremony proved control of THIS subnet's code, nothing

+/// about others), rewrite the disk snapshot, and note the hexes in the eviction

+/// marker so the pump clears the in-memory rows on its next tick. The caller

+/// persists `roster`. Returns one [`RekeyNotice`] per superseded identity (the

+/// demoted warn-on-change, REQ-MESH-5) — empty when nothing supersedes.

-/// inbound gates authorize on [`RosterStore::is_member`], not trust, so a

-/// re-keyed identity that kept its roster entry would stay a member. Dropping

-/// only the (no-longer-consulted) trust row would leave the superseded key

-/// authorized — the gap this tombstone closes. Tombstone (not a bare remove)

+/// inbound gates authorize on [`RosterStore::is_member`], so a re-keyed identity

+/// that kept its roster entry would stay a member. Tombstone (not a bare remove)

-/// 10), exactly as the joiner's own key is cleared at the call site.

+/// 10), exactly as the joiner's own key is cleared at the call site. (Mesh-D6

+/// deleted the former trust-row drop — the roster is the sole membership record.)

-        return; // no gossip yet — nothing to supersede

+        return Vec::new(); // no gossip yet — nothing to supersede

+    let mut notices = Vec::with_capacity(dead.len());

-        let dropped = trust.revoke(subnet, hex);

+        // The demoted warn-on-change (REQ-MESH-5): a machine_id-anchored

+        // awareness notice, never a gate. Absent a machine id, `dead` is

+        // empty (superseded_identities requires the id match) — so no false

-            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {}, roster tombstoned)",

+            "MACHINE_REKEYED:{subnet} machine '{}' (id {}…): last seen as {}…, now presents {}… \

+             (roster tombstoned — awareness only, not a block)",

+            spt_net::net::registry::key_prefix(&intro.machine_id),

+            spt_net::net::registry::key_prefix(hex),

-            if dropped { "dropped" } else { "absent" },

+            label: intro.label.clone(),

+            machine_id: intro.machine_id.clone(),

+            new_hex: joiner_hex.to_string(),

/// The event-driven advertisement marker file (M8 decision 15, REQ-CONV-2),


Migrate the registryhost eviction unit test.


Edit

    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed (replaces a
    /// `TrustStore::record` pin; `is_member` keys on `(subnet, pubkey)`).

    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed
    /// (`is_member` keys on `(subnet, pubkey)`).


    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed (replaces a
    /// `TrustStore::record` pin; `is_member` keys on `(subnet, pubkey)`).

    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed
    /// (`is_member` keys on `(subnet, pubkey)`).

-    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed (replaces a

-    /// `TrustStore::record` pin; `is_member` keys on `(subnet, pubkey)`).

+    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed

+    /// (`is_member` keys on `(subnet, pubkey)`).

fn member(subnet: &str, pk: &str) -> spt_store::roster::RosterEntry {


    // [unit->REQ-SUBNET-7] [unit->REQ-MESH-5] the effect leg: a completed
    // re-pair drops the superseded identity's trust row AND tombstones its
    // roster entry (this subnet only) — the roster tombstone is the
    // gate-effective eviction since D5a moved the gate to `is_member`;
    // dropping trust alone would leave the re-keyed identity authorized.
    // Rewrites the disk snapshot without its rows, notes it in the marker;
    // the host's consume then clears the in-memory rows + the heard entry.
    #[test]
    fn repair_evict_drops_trust_snapshot_and_memory() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        let mut trust = TrustStore::default();
        trust.record("home", "old-key", None, None);
        trust.record("worknet", "old-key", None, None);
        // Roster membership in both subnets — the gate-relevant state (D5a).
        let mut roster = RosterStore::default();
        roster.merge_entry(member("home", "old-key"));
        roster.merge_entry(member("worknet", "old-key"));
        assert!(roster.is_member("home", "old-key"), "member before re-pair");
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        repair_evict_superseded(&mut trust, &mut roster, "home", "new-key", &intro, &snap_dir);

        assert!(
            !trust.is_trusted("home", "old-key"),
            "ceremony subnet's trust row dropped"
        );
        assert!(
            trust.is_trusted("worknet", "old-key"),
            "other subnets untouched — the ceremony proved THIS subnet's code only"
        );
        // The gate-effective eviction: tombstoned in the ceremony subnet,
        // still a member elsewhere (the ceremony proved THIS subnet only).
        assert!(
            !roster.is_member("home", "old-key"),
            "superseded identity no longer passes the is_member gate"
        );
        assert!(
            roster.is_member("worknet", "old-key"),
            "other subnets' roster membership untouched"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker minted.
        let mut t2 = TrustStore::default();
        t2.record("home", "unrelated", None, None);
        let mut r2 = RosterStore::default();
        r2.merge_entry(member("home", "unrelated"));
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        repair_evict_superseded(&mut t2, &mut r2, "home", "new-key", &other, &snap_dir);
        assert!(t2.is_trusted("home", "unrelated"));
        assert!(r2.is_member("home", "unrelated"), "no-match leaves roster intact");
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
    }

    // [unit->REQ-SUBNET-7] [unit->REQ-MESH-5] the effect leg: a completed
    // re-pair tombstones the superseded identity's roster entry (this subnet
    // only) — the gate-effective eviction since D5a moved the gate to
    // `is_member` — and returns a machine_id-anchored rekey awareness notice
    // (the demoted warn-on-change, never a block). Rewrites the disk snapshot
    // without its rows, notes it in the marker; the host's consume then clears
    // the in-memory rows + the heard entry. A no-match intro (and an intro
    // with no machine id) evicts nothing and raises no notice.
    #[test]
    fn repair_evict_tombstones_roster_snapshot_and_notices() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        // Roster membership in both subnets — the gate-relevant state (D5a).
        let mut roster = RosterStore::default();
        roster.merge_entry(member("home", "old-key"));
        roster.merge_entry(member("worknet", "old-key"));
        assert!(roster.is_member("home", "old-key"), "member before re-pair");
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        let notices =
            repair_evict_superseded(&mut roster, "home", "new-key", &intro, &snap_dir);

        // The demoted warn-on-change: one machine_id-anchored notice K1→K2.
        assert_eq!(
            notices,
            vec![RekeyNotice {
                label: "BOX".into(),
                machine_id: "mid-1".into(),
                old_hex: "old-key".into(),
                new_hex: "new-key".into(),
            }],
            "rekey awareness notice fires, anchored on machine_id"
        );
        // The gate-effective eviction: tombstoned in the ceremony subnet,
        // still a member elsewhere (the ceremony proved THIS subnet only).
        assert!(
            !roster.is_member("home", "old-key"),
            "superseded identity no longer passes the is_member gate"
        );
        assert!(
            roster.is_member("worknet", "old-key"),
            "other subnets' roster membership untouched"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker, no notice.
        let mut r2 = RosterStore::default();
        r2.merge_entry(member("home", "unrelated"));
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        let none = repair_evict_superseded(&mut r2, "home", "new-key", &other, &snap_dir);
        assert!(none.is_empty(), "no-match raises no rekey notice");
        assert!(r2.is_member("home", "unrelated"), "no-match leaves roster intact");
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());

        // Absent machine id: the superseded match requires it, so no false
        // notice (REQ-MESH-5 absent-id rule) even when the label collides.
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old-key"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");
    }


    // [unit->REQ-SUBNET-7] [unit->REQ-MESH-5] the effect leg: a completed
    // re-pair drops the superseded identity's trust row AND tombstones its
    // roster entry (this subnet only) — the roster tombstone is the
    // gate-effective eviction since D5a moved the gate to `is_member`;
    // dropping trust alone would leave the re-keyed identity authorized.
    // Rewrites the disk snapshot without its rows, notes it in the marker;
    // the host's consume then clears the in-memory rows + the heard entry.
    #[test]
    fn repair_evict_drops_trust_snapshot_and_memory() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        let mut trust = TrustStore::default();
        trust.record("home", "old-key", None, None);
        trust.record("worknet", "old-key", None, None);
        // Roster membership in both subnets — the gate-relevant state (D5a).
        let mut roster = RosterStore::default();
        roster.merge_entry(member("home", "old-key"));
        roster.merge_entry(member("worknet", "old-key"));
        assert!(roster.is_member("home", "old-key"), "member before re-pair");
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        repair_evict_superseded(&mut trust, &mut roster, "home", "new-key", &intro, &snap_dir);

        assert!(
            !trust.is_trusted("home", "old-key"),
            "ceremony subnet's trust row dropped"
        );
        assert!(
            trust.is_trusted("worknet", "old-key"),
            "other subnets untouched — the ceremony proved THIS subnet's code only"
        );
        // The gate-effective eviction: tombstoned in the ceremony subnet,
        // still a member elsewhere (the ceremony proved THIS subnet only).
        assert!(
            !roster.is_member("home", "old-key"),
            "superseded identity no longer passes the is_member gate"
        );
        assert!(
            roster.is_member("worknet", "old-key"),
            "other subnets' roster membership untouched"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker minted.
        let mut t2 = TrustStore::default();
        t2.record("home", "unrelated", None, None);
        let mut r2 = RosterStore::default();
        r2.merge_entry(member("home", "unrelated"));
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        repair_evict_superseded(&mut t2, &mut r2, "home", "new-key", &other, &snap_dir);
        assert!(t2.is_trusted("home", "unrelated"));
        assert!(r2.is_member("home", "unrelated"), "no-match leaves roster intact");
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
    }

    // [unit->REQ-SUBNET-7] [unit->REQ-MESH-5] the effect leg: a completed
    // re-pair tombstones the superseded identity's roster entry (this subnet
    // only) — the gate-effective eviction since D5a moved the gate to
    // `is_member` — and returns a machine_id-anchored rekey awareness notice
    // (the demoted warn-on-change, never a block). Rewrites the disk snapshot
    // without its rows, notes it in the marker; the host's consume then clears
    // the in-memory rows + the heard entry. A no-match intro (and an intro
    // with no machine id) evicts nothing and raises no notice.
    #[test]
    fn repair_evict_tombstones_roster_snapshot_and_notices() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        // Roster membership in both subnets — the gate-relevant state (D5a).
        let mut roster = RosterStore::default();
        roster.merge_entry(member("home", "old-key"));
        roster.merge_entry(member("worknet", "old-key"));
        assert!(roster.is_member("home", "old-key"), "member before re-pair");
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        let notices =
            repair_evict_superseded(&mut roster, "home", "new-key", &intro, &snap_dir);

        // The demoted warn-on-change: one machine_id-anchored notice K1→K2.
        assert_eq!(
            notices,
            vec![RekeyNotice {
                label: "BOX".into(),
                machine_id: "mid-1".into(),
                old_hex: "old-key".into(),
                new_hex: "new-key".into(),
            }],
            "rekey awareness notice fires, anchored on machine_id"
        );
        // The gate-effective eviction: tombstoned in the ceremony subnet,
        // still a member elsewhere (the ceremony proved THIS subnet only).
        assert!(
            !roster.is_member("home", "old-key"),
            "superseded identity no longer passes the is_member gate"
        );
        assert!(
            roster.is_member("worknet", "old-key"),
            "other subnets' roster membership untouched"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker, no notice.
        let mut r2 = RosterStore::default();
        r2.merge_entry(member("home", "unrelated"));
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        let none = repair_evict_superseded(&mut r2, "home", "new-key", &other, &snap_dir);
        assert!(none.is_empty(), "no-match raises no rekey notice");
        assert!(r2.is_member("home", "unrelated"), "no-match leaves roster intact");
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());

        // Absent machine id: the superseded match requires it, so no false
        // notice (REQ-MESH-5 absent-id rule) even when the label collides.
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old-key"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");
    }

-    // re-pair drops the superseded identity's trust row AND tombstones its

-    // roster entry (this subnet only) — the roster tombstone is the

-    // gate-effective eviction since D5a moved the gate to `is_member`;

-    // dropping trust alone would leave the re-keyed identity authorized.

-    // Rewrites the disk snapshot without its rows, notes it in the marker;

-    // the host's consume then clears the in-memory rows + the heard entry.

+    // re-pair tombstones the superseded identity's roster entry (this subnet

+    // only) — the gate-effective eviction since D5a moved the gate to

+    // `is_member` — and returns a machine_id-anchored rekey awareness notice

+    // (the demoted warn-on-change, never a block). Rewrites the disk snapshot

+    // without its rows, notes it in the marker; the host's consume then clears

+    // the in-memory rows + the heard entry. A no-match intro (and an intro

+    // with no machine id) evicts nothing and raises no notice.

-    fn repair_evict_drops_trust_snapshot_and_memory() {

+    fn repair_evict_tombstones_roster_snapshot_and_notices() {

-        let mut trust = TrustStore::default();

-        trust.record("home", "old-key", None, None);

-        trust.record("worknet", "old-key", None, None);

-        repair_evict_superseded(&mut trust, &mut roster, "home", "new-key", &intro, &snap_dir);

+            repair_evict_superseded(&mut roster, "home", "new-key", &intro, &snap_dir);

-            !trust.is_trusted("home", "old-key"),

-            "ceremony subnet's trust row dropped"

+        // The demoted warn-on-change: one machine_id-anchored notice K1→K2.

+                machine_id: "mid-1".into(),

+                old_hex: "old-key".into(),

+                new_hex: "new-key".into(),

+            "rekey awareness notice fires, anchored on machine_id"

-            trust.is_trusted("worknet", "old-key"),

-            "other subnets untouched — the ceremony proved THIS subnet's code only"

-        // No-match intro: nothing evicts, no marker minted.

-        let mut t2 = TrustStore::default();

-        t2.record("home", "unrelated", None, None);

+        // No-match intro: nothing evicts, no marker, no notice.

-        repair_evict_superseded(&mut t2, &mut r2, "home", "new-key", &other, &snap_dir);

-        assert!(t2.is_trusted("home", "unrelated"));

+        let none = repair_evict_superseded(&mut r2, "home", "new-key", &other, &snap_dir);

+        assert!(none.is_empty(), "no-match raises no rekey notice");

+        // Absent machine id: the superseded match requires it, so no false

+        // notice (REQ-MESH-5 absent-id rule) even when the label collides.

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

+        r3.merge_entry(member("home", "old-key"));

+        let no_id = spt_net::net::pairing::wire::NodeIntro {

+        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);

+        assert!(silent.is_empty(), "absent machine id → no false notice");

+        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");

// [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet


Absent-id sub-case needs fresh gossip (snapshot was already rewritten). Fix it.


        // Absent machine id: the superseded match requires it, so no false
        // notice (REQ-MESH-5 absent-id rule) even when the label collides.
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old-key"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");
    }

        // Absent machine id: a FRESH gossip snapshot with a labeled identity;
        // an intro carrying the same label but NO machine id must not supersede
        // it (REQ-MESH-5 absent-id rule) — no eviction, no notice. Fresh dir so
        // the match has a real row to (not) hit, unlike the already-evicted one.
        let snap2 = dir.path().join("registry2");
        let h2 = RegistryHost::new_at(
            "self2",
            EpochSource::load_from(&dir.path().join("epoch2.json")),
            snap2.clone(),
        );
        h2.apply_feed(
            "old2",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old2", "BOX", "mid-1", 5),
            }],
            &policy("home", "old2"),
        );
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old2"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap2);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old2"), "absent-id leaves roster intact");
    }


        // Absent machine id: the superseded match requires it, so no false
        // notice (REQ-MESH-5 absent-id rule) even when the label collides.
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old-key"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");
    }

        // Absent machine id: a FRESH gossip snapshot with a labeled identity;
        // an intro carrying the same label but NO machine id must not supersede
        // it (REQ-MESH-5 absent-id rule) — no eviction, no notice. Fresh dir so
        // the match has a real row to (not) hit, unlike the already-evicted one.
        let snap2 = dir.path().join("registry2");
        let h2 = RegistryHost::new_at(
            "self2",
            EpochSource::load_from(&dir.path().join("epoch2.json")),
            snap2.clone(),
        );
        h2.apply_feed(
            "old2",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old2", "BOX", "mid-1", 5),
            }],
            &policy("home", "old2"),
        );
        let mut r3 = RosterStore::default();
        r3.merge_entry(member("home", "old2"));
        let no_id = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: String::new(),
        };
        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap2);
        assert!(silent.is_empty(), "absent machine id → no false notice");
        assert!(r3.is_member("home", "old2"), "absent-id leaves roster intact");
    }

-        // Absent machine id: the superseded match requires it, so no false

-        // notice (REQ-MESH-5 absent-id rule) even when the label collides.

+        // Absent machine id: a FRESH gossip snapshot with a labeled identity;

+        // an intro carrying the same label but NO machine id must not supersede

+        // it (REQ-MESH-5 absent-id rule) — no eviction, no notice. Fresh dir so

+        // the match has a real row to (not) hit, unlike the already-evicted one.

+        let snap2 = dir.path().join("registry2");

+            EpochSource::load_from(&dir.path().join("epoch2.json")),

+                endpoint_id: "ling".into(),

+                instance: identified("old2", "BOX", "mid-1", 5),

-        r3.merge_entry(member("home", "old-key"));

+        r3.merge_entry(member("home", "old2"));

-        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap_dir);

+        let silent = repair_evict_superseded(&mut r3, "home", "new-key", &no_id, &snap2);

-        assert!(r3.is_member("home", "old-key"), "absent-id leaves roster intact");

+        assert!(r3.is_member("home", "old2"), "absent-id leaves roster intact");

// [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet


Now pairhost.rs — drop trust, update calls.


use spt_store::roster::{RosterEntry, RosterStore};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;

use spt_store::roster::{RosterEntry, RosterStore};
use spt_store::subnet::SubnetStore;


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

use spt_store::roster::{RosterEntry, RosterStore};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;

/// The ceremony bound: aligned with the rate limiter's own stale-slot

/// reclaim so an abandoned slot and an abandoned task expire together.


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.


    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &roster,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {

    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),
    )
    .await
    {


    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &roster,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {

    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),
    )
    .await
    {

-    let mut trust = TrustStore::load();

+        run_responder(&conn, local_pub, &subnets, &roster, &mut rate, now_secs()),


            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a
            // COMPLETED ceremony presenting the same label + machine id as
            // an existing row means this MACHINE re-paired under a fresh
            // key — the superseded identity's trust + registry rows go.
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &mut roster,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }

            // Re-pair overwrite (M8 decision 13, REQ-SUBNET-7): a COMPLETED
            // ceremony presenting the same label + machine id as an existing
            // row means this MACHINE re-paired under a fresh key — the
            // superseded identity's roster entry is tombstoned + its registry
            // rows evicted, and a machine_id-anchored rekey awareness notice is
            // raised (the demoted warn-on-change, REQ-MESH-5 — never a gate).
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            // [impl->REQ-MESH-5]
            if let Some(intro) = &outcome.intro {
                let _notices = crate::registryhost::repair_evict_superseded(
                    &mut roster,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }


            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a
            // COMPLETED ceremony presenting the same label + machine id as
            // an existing row means this MACHINE re-paired under a fresh
            // key — the superseded identity's trust + registry rows go.
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &mut roster,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }

            // Re-pair overwrite (M8 decision 13, REQ-SUBNET-7): a COMPLETED
            // ceremony presenting the same label + machine id as an existing
            // row means this MACHINE re-paired under a fresh key — the
            // superseded identity's roster entry is tombstoned + its registry
            // rows evicted, and a machine_id-anchored rekey awareness notice is
            // raised (the demoted warn-on-change, REQ-MESH-5 — never a gate).
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            // [impl->REQ-MESH-5]
            if let Some(intro) = &outcome.intro {
                let _notices = crate::registryhost::repair_evict_superseded(
                    &mut roster,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }

// paying the id-only discovery cold start.

-            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a

-            // COMPLETED ceremony presenting the same label + machine id as

-            // an existing row means this MACHINE re-paired under a fresh

-            // key — the superseded identity's trust + registry rows go.

+            // Re-pair overwrite (M8 decision 13, REQ-SUBNET-7): a COMPLETED

+            // ceremony presenting the same label + machine id as an existing

+            // row means this MACHINE re-paired under a fresh key — the

+            // superseded identity's roster entry is tombstoned + its registry

+            // rows evicted, and a machine_id-anchored rekey awareness notice is

+            // raised (the demoted warn-on-change, REQ-MESH-5 — never a gate).

-                crate::registryhost::repair_evict_superseded(

+                let _notices = crate::registryhost::repair_evict_superseded(


            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
            if let Err(e) = roster.save() {
                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");
            }

            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            // Membership is the roster (Mesh-D6): the joiner is recorded above;
            // persisting it is the pairing's durable effect (no trust write).
            match roster.save() {
                Ok(()) => eprintln!(
                    "PAIRED: joiner {} recorded in subnet '{}'",
                    outcome.joiner_hex, outcome.subnet
                ),
                Err(e) => eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}"),
            }


            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
            if let Err(e) = roster.save() {
                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");
            }

            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            // Membership is the roster (Mesh-D6): the joiner is recorded above;
            // persisting it is the pairing's durable effect (no trust write).
            match roster.save() {
                Ok(()) => eprintln!(
                    "PAIRED: joiner {} recorded in subnet '{}'",
                    outcome.joiner_hex, outcome.subnet
                ),
                Err(e) => eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}"),
            }

-                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),

-                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),

+            // Membership is the roster (Mesh-D6): the joiner is recorded above;

+            // persisting it is the pairing's durable effect (no trust write).

+                    "PAIRED: joiner {} recorded in subnet '{}'",

+                    outcome.joiner_hex, outcome.subnet

+                Err(e) => eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}"),

-            if let Err(e) = roster.save() {

-                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");

// A failed ceremony is normal traffic (wrong code, impostor probe,

// vanished peer) — logged, never fatal; the limiter charged it.


    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony.
    let mut roster = RosterStore::load();

    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony. Membership is roster-only (Mesh-D6).
    let mut roster = RosterStore::load();


    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony.
    let mut roster = RosterStore::load();

    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony. Membership is roster-only (Mesh-D6).
    let mut roster = RosterStore::load();

-    let mut trust = TrustStore::load();

-    // address after a successful ceremony.

+    // address after a successful ceremony. Membership is roster-only (Mesh-D6).

// The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +

// hashed machine id — what lets the seed-holder recognize this MACHINE


        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),

        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),


        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),

        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),


    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;

    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;


    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;

Err(e) => return Err(classify_wire_error(e)),

-        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;


propagate.rs gate. First add `is_member_any` helper to RosterStore.


    /// The **visible** members of a subnet (entry present ∧ ¬tombstoned) — what
    /// the mesh dials by.
    pub fn members_in<'a>(&'a self, subnet: &'a str) -> impl Iterator<Item = &'a RosterEntry> {

    /// Whether `pubkey_hex` is a (non-tombstoned) member of **any** subnet —
    /// the cross-subnet gate the serve paths use when the subnet is not in hand
    /// (the handshake proves a node id, not which subnet it speaks for). Mesh-D6
    /// replaced the former `trust.peers.iter().any(pubkey)` admission with this.
    // [impl->REQ-MESH-5]
    pub fn is_member_any(&self, pubkey_hex: &str) -> bool {
        self.members
            .iter()
            .any(|m| m.pubkey_hex == pubkey_hex && !self.is_tombstoned(&m.subnet, pubkey_hex))
    }

    /// The **visible** members of a subnet (entry present ∧ ¬tombstoned) — what
    /// the mesh dials by.
    pub fn members_in<'a>(&'a self, subnet: &'a str) -> impl Iterator<Item = &'a RosterEntry> {


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

    /// Whether `pubkey_hex` is a (non-tombstoned) member of **any** subnet —
    /// the cross-subnet gate the serve paths use when the subnet is not in hand
    /// (the handshake proves a node id, not which subnet it speaks for). Mesh-D6
    /// replaced the former `trust.peers.iter().any(pubkey)` admission with this.
    // [impl->REQ-MESH-5]
    pub fn is_member_any(&self, pubkey_hex: &str) -> bool {
        self.members
            .iter()
            .any(|m| m.pubkey_hex == pubkey_hex && !self.is_tombstoned(&m.subnet, pubkey_hex))
    }

    /// The **visible** members of a subnet (entry present ∧ ¬tombstoned) — what
    /// the mesh dials by.
    pub fn members_in<'a>(&'a self, subnet: &'a str) -> impl Iterator<Item = &'a RosterEntry> {

self.find(subnet, pubkey_hex).is_some() && !self.is_tombstoned(subnet, pubkey_hex)

+    /// Whether `pubkey_hex` is a (non-tombstoned) member of **any** subnet —

+    /// the cross-subnet gate the serve paths use when the subnet is not in hand

+    /// (the handshake proves a node id, not which subnet it speaks for). Mesh-D6

+    /// replaced the former `trust.peers.iter().any(pubkey)` admission with this.

+    pub fn is_member_any(&self, pubkey_hex: &str) -> bool {

+            .any(|m| m.pubkey_hex == pubkey_hex && !self.is_tombstoned(&m.subnet, pubkey_hex))


    // [unit->REQ-MESH-2] tombstones are subnet-scoped and propagate through a
    // merge (they are grow-set elements, not local-only).

    // [unit->REQ-MESH-5] is_member_any: a node present in ≥1 subnet passes the
    // cross-subnet serve gate; a tombstone in its only subnet drops it; a node
    // still listed in ANOTHER subnet stays admissible; an unknown key fails.
    #[test]
    fn is_member_any_spans_subnets_and_honors_tombstones() {
        let mut r = RosterStore::default();
        r.merge_entry(entry("home", "aa", 1));
        r.merge_entry(entry("work", "aa", 1));
        r.merge_entry(entry("home", "bb", 1));
        assert!(r.is_member_any("aa"));
        assert!(!r.is_member_any("zz"), "unknown key is no member anywhere");

        // Tombstone bb's only subnet → not a member anywhere.
        r.tombstone("home", "bb", "9");
        assert!(!r.is_member_any("bb"), "tombstoned in its only subnet");

        // Tombstone aa in home only → still a member via work.
        r.tombstone("home", "aa", "9");
        assert!(r.is_member_any("aa"), "still a member in another subnet");
    }

    // [unit->REQ-MESH-2] tombstones are subnet-scoped and propagate through a
    // merge (they are grow-set elements, not local-only).


    // [unit->REQ-MESH-5] is_member_any: a node present in ≥1 subnet passes the
    // cross-subnet serve gate; a tombstone in its only subnet drops it; a node
    // still listed in ANOTHER subnet stays admissible; an unknown key fails.
    #[test]
    fn is_member_any_spans_subnets_and_honors_tombstones() {
        let mut r = RosterStore::default();
        r.merge_entry(entry("home", "aa", 1));
        r.merge_entry(entry("work", "aa", 1));
        r.merge_entry(entry("home", "bb", 1));
        assert!(r.is_member_any("aa"));
        assert!(!r.is_member_any("zz"), "unknown key is no member anywhere");

        // Tombstone bb's only subnet → not a member anywhere.
        r.tombstone("home", "bb", "9");
        assert!(!r.is_member_any("bb"), "tombstoned in its only subnet");

        // Tombstone aa in home only → still a member via work.
        r.tombstone("home", "aa", "9");
        assert!(r.is_member_any("aa"), "still a member in another subnet");
    }

    // [unit->REQ-MESH-2] tombstones are subnet-scoped and propagate through a
    // merge (they are grow-set elements, not local-only).

assert!(!r.clear_tombstone("home", "aa"), "second clear is no-op false");

+    // [unit->REQ-MESH-5] is_member_any: a node present in ≥1 subnet passes the

+    // cross-subnet serve gate; a tombstone in its only subnet drops it; a node

+    // still listed in ANOTHER subnet stays admissible; an unknown key fails.

+    fn is_member_any_spans_subnets_and_honors_tombstones() {

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

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

+        r.merge_entry(entry("work", "aa", 1));

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

+        assert!(r.is_member_any("aa"));

+        assert!(!r.is_member_any("zz"), "unknown key is no member anywhere");

+        // Tombstone bb's only subnet → not a member anywhere.

+        r.tombstone("home", "bb", "9");

+        assert!(!r.is_member_any("bb"), "tombstoned in its only subnet");

+        // Tombstone aa in home only → still a member via work.

+        r.tombstone("home", "aa", "9");

+        assert!(r.is_member_any("aa"), "still a member in another subnet");


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 propagate.rs serve gate:


//! A query is answered only when the **handshake-proven** origin (the QUIC
//! identity from the broker's stream table, never payload bytes —
//! KNOWN-HAZARDS 7.5) is a trusted peer in ≥1 subnet; an untrusted origin is
//! refused by not offering (the up-to-date shape — fail-closed, learns
//! nothing). Sends are fire-and-forget (`op_id: None` — a journaled send

//! A query is answered only when the **handshake-proven** origin (the QUIC
//! identity from the broker's stream table, never payload bytes —
//! KNOWN-HAZARDS 7.5) is a roster member in ≥1 subnet; a non-member origin is
//! refused by not offering (the up-to-date shape — fail-closed, learns
//! nothing). Sends are fire-and-forget (`op_id: None` — a journaled send


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

//! A query is answered only when the **handshake-proven** origin (the QUIC
//! identity from the broker's stream table, never payload bytes —
//! KNOWN-HAZARDS 7.5) is a trusted peer in ≥1 subnet; an untrusted origin is
//! refused by not offering (the up-to-date shape — fail-closed, learns
//! nothing). Sends are fire-and-forget (`op_id: None` — a journaled send

//! A query is answered only when the **handshake-proven** origin (the QUIC
//! identity from the broker's stream table, never payload bytes —
//! KNOWN-HAZARDS 7.5) is a roster member in ≥1 subnet; a non-member origin is
//! refused by not offering (the up-to-date shape — fail-closed, learns
//! nothing). Sends are fire-and-forget (`op_id: None` — a journaled send

-//! KNOWN-HAZARDS 7.5) is a trusted peer in ≥1 subnet; an untrusted origin is

+//! KNOWN-HAZARDS 7.5) is a roster member in ≥1 subnet; a non-member origin is

//! starves a pump, the D5b lesson), chunks dedup by absolute offset, and a


use spt_net::net::update::{UpdDecoder, UpdRecord};
use spt_store::trust::TrustStore;

use spt_net::net::update::{UpdDecoder, UpdRecord};
use spt_store::roster::RosterStore;


use spt_net::net::update::{UpdDecoder, UpdRecord};
use spt_store::trust::TrustStore;

use spt_net::net::update::{UpdDecoder, UpdRecord};
use spt_store::roster::RosterStore;


    /// Nothing newer than the requester's version on its channel (or nothing
    /// staged at all) — the honest up-to-date answer.
    UpToDate,
    /// The origin is not a trusted peer in any subnet — refused by not
    /// offering (the same wire shape as up-to-date; the outcome is distinct
    /// so callers/tests observe the gate fired).
    Refused,

    /// Nothing newer than the requester's version on its channel (or nothing
    /// staged at all) — the honest up-to-date answer.
    UpToDate,
    /// The origin is not a roster member in any subnet — refused by not
    /// offering (the same wire shape as up-to-date; the outcome is distinct
    /// so callers/tests observe the gate fired).
    Refused,


    /// Nothing newer than the requester's version on its channel (or nothing
    /// staged at all) — the honest up-to-date answer.
    UpToDate,
    /// The origin is not a trusted peer in any subnet — refused by not
    /// offering (the same wire shape as up-to-date; the outcome is distinct
    /// so callers/tests observe the gate fired).
    Refused,

    /// Nothing newer than the requester's version on its channel (or nothing
    /// staged at all) — the honest up-to-date answer.
    UpToDate,
    /// The origin is not a roster member in any subnet — refused by not
    /// offering (the same wire shape as up-to-date; the outcome is distinct
    /// so callers/tests observe the gate fired).
    Refused,

-    /// The origin is not a trusted peer in any subnet — refused by not

+    /// The origin is not a roster member in any subnet — refused by not


/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against `trust`
/// **here**, fail-closed: an untrusted peer gets the up-to-date shape and
/// learns nothing (not even whether a release is staged).
// [impl->REQ-UPD-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_update(
    brain: &mut Brain,
    stream_id: u64,
    origin_node: &str,
    net_from_seq: u64,
    cache: &ReleaseCache,
    trust: &TrustStore,
) -> io::Result<UpdateServeOutcome> {

/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against the `roster`
/// **here**, fail-closed: a non-member peer gets the up-to-date shape and
/// learns nothing (not even whether a release is staged).
// [impl->REQ-UPD-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-MESH-5]
pub fn serve_update(
    brain: &mut Brain,
    stream_id: u64,
    origin_node: &str,
    net_from_seq: u64,
    cache: &ReleaseCache,
    roster: &RosterStore,
) -> io::Result<UpdateServeOutcome> {


/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against `trust`
/// **here**, fail-closed: an untrusted peer gets the up-to-date shape and
/// learns nothing (not even whether a release is staged).
// [impl->REQ-UPD-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_update(
    brain: &mut Brain,
    stream_id: u64,
    origin_node: &str,
    net_from_seq: u64,
    cache: &ReleaseCache,
    trust: &TrustStore,
) -> io::Result<UpdateServeOutcome> {

/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against the `roster`
/// **here**, fail-closed: a non-member peer gets the up-to-date shape and
/// learns nothing (not even whether a release is staged).
// [impl->REQ-UPD-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-MESH-5]
pub fn serve_update(
    brain: &mut Brain,
    stream_id: u64,
    origin_node: &str,
    net_from_seq: u64,
    cache: &ReleaseCache,
    roster: &RosterStore,
) -> io::Result<UpdateServeOutcome> {

-/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against `trust`

-/// **here**, fail-closed: an untrusted peer gets the up-to-date shape and

+/// (REQ-HAZARD-WAN-ORIGIN-AUTH). The origin is gated against the `roster`

+/// **here**, fail-closed: a non-member peer gets the up-to-date shape and


                            // [impl->REQ-UPD-6]
                            let trusted = trust.peers.iter().any(|p| p.pubkey_hex == origin_node);
                            let status = if trusted {

                            // [impl->REQ-UPD-6]
                            let trusted = roster.is_member_any(origin_node);
                            let status = if trusted {


                            // [impl->REQ-UPD-6]
                            let trusted = trust.peers.iter().any(|p| p.pubkey_hex == origin_node);
                            let status = if trusted {

                            // [impl->REQ-UPD-6]
                            let trusted = roster.is_member_any(origin_node);
                            let status = if trusted {

// untrusted origin gets the all-None shape and

// learns nothing (not channel, not versions).

-                            let trusted = trust.peers.iter().any(|p| p.pubkey_hex == origin_node);

+                            let trusted = roster.is_member_any(origin_node);


                            // The gate: only a peer trusted in ≥1 subnet is
                            // answered; everyone else gets the up-to-date
                            // shape (fail-closed, indistinguishable from
                            // having nothing).
                            let trusted = trust.peers.iter().any(|p| p.pubkey_hex == origin_node);
                            let staged = if trusted { cache.staged_update() } else { None };

                            // The gate: only a roster member in ≥1 subnet is
                            // answered; everyone else gets the up-to-date
                            // shape (fail-closed, indistinguishable from
                            // having nothing).
                            let trusted = roster.is_member_any(origin_node);
                            let staged = if trusted { cache.staged_update() } else { None };


                            // The gate: only a peer trusted in ≥1 subnet is
                            // answered; everyone else gets the up-to-date
                            // shape (fail-closed, indistinguishable from
                            // having nothing).
                            let trusted = trust.peers.iter().any(|p| p.pubkey_hex == origin_node);
                            let staged = if trusted { cache.staged_update() } else { None };

                            // The gate: only a roster member in ≥1 subnet is
                            // answered; everyone else gets the up-to-date
                            // shape (fail-closed, indistinguishable from
                            // having nothing).
                            let trusted = roster.is_member_any(origin_node);
                            let staged = if trusted { cache.staged_update() } else { None };

-                            // The gate: only a peer trusted in ≥1 subnet is

+                            // The gate: only a roster member in ≥1 subnet is

// Offer only a release that is actually newer on

// the requester's channel (an honest responder


Now dispatch.rs caller + import:


use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::roster::RosterStore;


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

use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::roster::RosterStore;


        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let trust = TrustStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &trust) {

        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let roster = RosterStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {


        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let trust = TrustStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &trust) {

        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let roster = RosterStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {

-            let trust = TrustStore::load();

-            match serve_update(&mut brain, stream_id, origin, 0, &cache, &trust) {

+            let roster = RosterStore::load();

+            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {


peerloop.rs — THE WIDEN. Module doc + import + PumpPaths.trust field + the push loop.


//! One daemon-hosted loop drives three cadenced pumps against every trusted
//! peer of every member subnet, each a thin call over an already-shipped,
//! idempotent driver — the pump owns *when*, the drivers own *how*:
//!
//! - **registry push** ([`RegistryHost::advertise_local`] → one feed stream
//!   per peer): the node re-advertises its own rows; peers reconverge within
//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Own rows
//!   only — no transitive gossip in v1: trust is pairwise per subnet, and a
//!   relayed third-party row would carry an origin the receiver cannot gate
//!   honestly (KNOWN-HAZARDS 7.5).

//! One daemon-hosted loop drives three cadenced pumps against every roster
//! member of every member subnet, each a thin call over an already-shipped,
//! idempotent driver — the pump owns *when*, the drivers own *how*:
//!
//! - **registry push** ([`RegistryHost::advertise_local`] → one feed stream
//!   per peer): the node re-advertises its own rows; peers reconverge within
//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Mesh-D6 widened
//!   the push target from the directly-paired set to ALL roster members — a
//!   wider DIRECT fan-out, never a relay (REQ-MESH-3). Own rows only — a
//!   relayed third-party row would carry an origin the receiver cannot gate
//!   honestly (KNOWN-HAZARDS 7.5/4.10), so every row still arrives from its
//!   author over a handshake.


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)

//! One daemon-hosted loop drives three cadenced pumps against every trusted
//! peer of every member subnet, each a thin call over an already-shipped,
//! idempotent driver — the pump owns *when*, the drivers own *how*:
//!
//! - **registry push** ([`RegistryHost::advertise_local`] → one feed stream
//!   per peer): the node re-advertises its own rows; peers reconverge within
//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Own rows
//!   only — no transitive gossip in v1: trust is pairwise per subnet, and a
//!   relayed third-party row would carry an origin the receiver cannot gate
//!   honestly (KNOWN-HAZARDS 7.5).

//! One daemon-hosted loop drives three cadenced pumps against every roster
//! member of every member subnet, each a thin call over an already-shipped,
//! idempotent driver — the pump owns *when*, the drivers own *how*:
//!
//! - **registry push** ([`RegistryHost::advertise_local`] → one feed stream
//!   per peer): the node re-advertises its own rows; peers reconverge within
//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Mesh-D6 widened
//!   the push target from the directly-paired set to ALL roster members — a
//!   wider DIRECT fan-out, never a relay (REQ-MESH-3). Own rows only — a
//!   relayed third-party row would carry an origin the receiver cannot gate
//!   honestly (KNOWN-HAZARDS 7.5/4.10), so every row still arrives from its
//!   author over a handshake.

//! Outbound peer pump (D9-1-2) — the production trigger loops every D5–D8

//! slice deferred ("rides D9 with the daemon lifecycle").

-//! One daemon-hosted loop drives three cadenced pumps against every trusted

-//! peer of every member subnet, each a thin call over an already-shipped,

+//! One daemon-hosted loop drives three cadenced pumps against every roster

+//! member of every member subnet, each a thin call over an already-shipped,

-//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Own rows

-//!   only — no transitive gossip in v1: trust is pairwise per subnet, and a

+//!   one cadence (replay-is-recovery, the D4d/D8 push posture). Mesh-D6 widened

+//!   the push target from the directly-paired set to ALL roster members — a

+//!   wider DIRECT fan-out, never a relay (REQ-MESH-3). Own rows only — a

+//!   honestly (KNOWN-HAZARDS 7.5/4.10), so every row still arrives from its

//! - **notif push** ([`crate::notifsync::emit_notif_feed`]): the full

//!   semilattice spool per subnet; the receiver's join dedups.

//! - **sync pull** ([`crate::sync::request_sync`]): anti-entropy for the


use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;


use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;


    /// `subnet.json` ([`SubnetStore::load_from`]).
    pub subnets: PathBuf,
    /// `trust/peers.json` ([`TrustStore::load_from`]).
    pub trust: PathBuf,
    /// `visibility.json` ([`VisibilityStore::load_from`]).
    pub vis: PathBuf,

    /// `subnet.json` ([`SubnetStore::load_from`]).
    pub subnets: PathBuf,
    /// `visibility.json` ([`VisibilityStore::load_from`]).
    pub vis: PathBuf,


    /// `subnet.json` ([`SubnetStore::load_from`]).
    pub subnets: PathBuf,
    /// `trust/peers.json` ([`TrustStore::load_from`]).
    pub trust: PathBuf,
    /// `visibility.json` ([`VisibilityStore::load_from`]).
    pub vis: PathBuf,

    /// `subnet.json` ([`SubnetStore::load_from`]).
    pub subnets: PathBuf,
    /// `visibility.json` ([`VisibilityStore::load_from`]).
    pub vis: PathBuf,

-    /// `trust/peers.json` ([`TrustStore::load_from`]).

/// The pump's durable open-op counter (its own file — colliding with the


            subnets: identity.join("subnet.json"),
            trust: identity.join("trust").join("peers.json"),
            vis: identity.join("visibility.json"),

            subnets: identity.join("subnet.json"),
            vis: identity.join("visibility.json"),


            subnets: identity.join("subnet.json"),
            trust: identity.join("trust").join("peers.json"),
            vis: identity.join("visibility.json"),

            subnets: identity.join("subnet.json"),
            vis: identity.join("visibility.json"),

-            trust: identity.join("trust").join("peers.json"),


Now the push-target swap in the tick loop:


        // The push target stays the directly-paired trusted set at D5 (the
        // fan-out widens to all roster members only at D6); the sync *gate*,
        // however, now authorizes on roster membership (Mesh-D5, REQ-MESH-5).
        let trust = TrustStore::load_from(&paths.trust);
        let roster_path = paths
            .subnets
            .parent()
            .map(|p| p.join("roster.json"))
            .unwrap_or_else(|| PathBuf::from("roster.json"));

        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster
        // (all members), not the directly-paired set — a wider DIRECT fan-out.
        // The one roster loaded here feeds both the push (who we dial) and the
        // sync gate (who we authorize), so they read one store.
        let roster_path = paths
            .subnets
            .parent()
            .map(|p| p.join("roster.json"))
            .unwrap_or_else(|| PathBuf::from("roster.json"));


        // The push target stays the directly-paired trusted set at D5 (the
        // fan-out widens to all roster members only at D6); the sync *gate*,
        // however, now authorizes on roster membership (Mesh-D5, REQ-MESH-5).
        let trust = TrustStore::load_from(&paths.trust);
        let roster_path = paths
            .subnets
            .parent()
            .map(|p| p.join("roster.json"))
            .unwrap_or_else(|| PathBuf::from("roster.json"));

        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster
        // (all members), not the directly-paired set — a wider DIRECT fan-out.
        // The one roster loaded here feeds both the push (who we dial) and the
        // sync gate (who we authorize), so they read one store.
        let roster_path = paths
            .subnets
            .parent()
            .map(|p| p.join("roster.json"))
            .unwrap_or_else(|| PathBuf::from("roster.json"));

-        // The push target stays the directly-paired trusted set at D5 (the

-        // fan-out widens to all roster members only at D6); the sync *gate*,

-        // however, now authorizes on roster membership (Mesh-D5, REQ-MESH-5).

-        let trust = TrustStore::load_from(&paths.trust);

+        // THE WIDEN (Mesh-D6, REQ-MESH-3): the push target is the full roster

+        // (all members), not the directly-paired set — a wider DIRECT fan-out.

+        // The one roster loaded here feeds both the push (who we dial) and the

+        // sync gate (who we authorize), so they read one store.


            for peer in trust.peers_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }

            // [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
                }


            for peer in trust.peers_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }

            // [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
                }

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

-            for peer in trust.peers_in(&sub.name) {

+            // [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) {


Build spt-daemon lib to verify the src-layer edits.


Bash

cargo build -p spt-daemon 2>&1 | tail -30; echo "===EXIT:${PIPESTATUS[0]}==="

Build spt-daemon lib


   Compiling spt-store v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-live v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.39s
===EXIT:0===


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


Daemon src layer done. Now CLI (task 7) — check existing roster usage + imports in cli.rs.


22:use spt_store::perch::{self, ParentHint};
23:use spt_store::registry;
24:use spt_store::subnet::{SubnetError, SubnetStore};
27:use crate::roster;
868:    use spt_store::contextstore::ContextStore;
978:        match id.or_else(roster::detect_self_id) {
1040:/// `--local` renders this node's perch roster instead; `--subnet` filters.
1044:    use spt_store::visibility::VisibilityStore;
1059:    if let Some(self_id) = roster::detect_self_id() {
1060:        let state = roster::enumerate()
1197:    let Some(id) = id.or_else(roster::detect_self_id) else {
1324:    // Local endpoints (the perch roster).
1325:    let perches = roster::enumerate();
1404:    let issuer = from.or_else(roster::detect_self_id).unwrap_or_default();
1595:    from.or_else(roster::detect_self_id).unwrap_or_default()
1815:/// `spt endpoint list --local` — this node's perch roster (the pre-M8 bare
1819:    let perches = roster::enumerate();
1856:    match roster::detect_self_id() {
3391:    use spt_store::access::AccessStore;
3653:    use spt_store::shellinfo;
3658:        owner.or_else(roster::detect_self_id)
4100:                use spt_store::xfer::{transfers_dir, write_progress_at, XferProgress, XferRole};
4397:    use spt_store::grants::GrantStore;
5455:        use spt_store::info::{write_info, InfoJson};
5495:        use spt_store::info::{write_info, InfoJson};
6116:        use spt_store::contextstore::{agent_branch, ContextStore, LIVE_CONTEXT_FILE};


Edit

/// Build the status rows from the subnet store + registry snapshots + the
/// trust store. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is TRUST-derived (self + pinned peers — the durable
/// pairing truth), with registry-row nodes unioned in as enrichment: a
/// member node with zero advertised endpoints (fresh box, empty owlery) is
/// still a member, and an offline peer whose rows the 4.10 eviction decayed
/// must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    trust: &spt_store::trust::TrustStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for peer in trust.peers_in(&s.name) {
                nodes.insert(peer.pubkey_hex.clone());
            }

/// Build the status rows from the subnet store + registry snapshots + the
/// member roster. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is ROSTER-derived (self + roster members — the durable mesh
/// membership record, Mesh-D6), with registry-row nodes unioned in as
/// enrichment: a member node with zero advertised endpoints (fresh box, empty
/// owlery) is still a member, and an offline peer whose rows the 4.10 eviction
/// decayed must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    roster: &spt_store::roster::RosterStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for member in roster.members_in(&s.name) {
                nodes.insert(member.pubkey_hex.clone());
            }