390	mod tests {
391	    use super::*;
392
393	    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
394	        EndpointRow {
395	            id: id.to_string(),
396	            group: if is_local { "local".into() } else { "sub:n".into() },
397	            node: if is_local { "LOCAL".into() } else { "n".into() },
398	            status,
399	            is_local,
400	            adapter_profile: String::new(),
401	            project_history: Vec::new(),
402	            description: String::new(),
403	            resume_rows: Vec::new(),
404	            driven_by: None,
405	            viewer_count: 0,
406	            controllable: None,
407	            endpoint_type: String::new(),
408	            is_unbound: false,
409	        }
410	    }
411
412	    // [unit->REQ-ENDPOINT-UNBOUND-ATTACH] THE REAL-DERIVATION test (the one that
413	    // would have caught the masking bug): a live UNBOUND perch reads alive=FALSE
414	    // from the roster (is_perch_alive is bound-gated) + unbound=TRUE. row_status —
415	    // the ACTUAL rule local_rows applies — must derive EpStatus::Online for it (NOT
416	    // Offline), and that Online + is_unbound must resolve through display_status to
417	    // the hollow Unbound. The prior display unit hand-set Online (a false premise),
418	    // which is exactly why it stayed green while the picker rendered gray Offline.
419	    #[test]
420	    fn row_status_drives_unbound_display() {
421	        use crate::picker::model::EpDisplay;
422	        // The derivation rule, directly: unbound (alive=false) IS online; a true
423	        // dead perch (neither) is offline.
424	        assert_eq!(row_status(false, true), EpStatus::Online, "live unbound → online");
425	        assert_eq!(row_status(true, false), EpStatus::Online, "alive → online");
426	        assert_eq!(row_status(true, true), EpStatus::Online);
427	        assert_eq!(row_status(false, false), EpStatus::Offline, "neither → offline");
428
429	        // The full seam: feed the real-rule status + the roster unbound flag into an
430	        // EndpointRow exactly as local_rows does → display_status == hollow Unbound.
431	        let mut r = row("skeleton", true, row_status(/*alive*/ false, /*unbound*/ true));
432	        r.is_unbound = true; // local_rows sets is_unbound = p.unbound
433	        r.controllable = Some(false); // would be amber HarnessOnly if not unbound
434	        assert_eq!(
435	            r.display_status(),
436	            EpDisplay::Unbound,
437	            "live unbound derives Online → display resolves to hollow Unbound (not gray Offline)"
438	        );
439	        // And it must NOT have read as offline (the bug) — no resume rows path etc.
440	        assert_ne!(r.display_status(), EpDisplay::Offline, "never gray-offline for a live unbound");
441	    }
442
443	    // [int->REQ-ENDPOINT-UNBOUND-ATTACH] the REAL render seam (the one no unit
444	    // reached — the W5 bug hid here): a real on-disk STATUS_UNBOUND perch, read
445	    // through the ACTUAL roster→local_rows→display_status pipeline (gather_endpoints,
446	    // NOT a hand-built row), renders the hollow Unbound — NOT gray Offline. Drives
447	    // the live disk derivation: roster is_perch_alive==false (bound-gated) for the
448	    // unbound perch, so row_status must lift it back to Online for display_status to
449	    // reach Unbound. Serialized + isolated SPT_HOME via the shared test guard.
450	    #[test]
451	    fn gather_renders_live_unbound() {
452	        use crate::picker::model::EpDisplay;
453	        let _home = crate::testutil::isolated_home();
454
455	        // Write a real UNBOUND skeleton perch on disk (empty session_id, live_agent,
456	        // status=unbound) — exactly what `endpoint run`'s skeleton-write produces.
457	        let id = "unbskel";
458	        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
459	        std::fs::create_dir_all(&perch_path).unwrap();
460	        let rec = spt_store::info::InfoJson::new(
461	            id,
462	            &spt_store::timefmt::now_rfc3339(),
463	            std::process::id(),
464	            "",
465	            "live_agent",
466	        );
467	        spt_store::info::write_info(&perch_path, &rec).unwrap();
468	        spt_store::info::set_status(&perch_path, spt_store::liveness::STATUS_UNBOUND).unwrap();
469
470	        // Sanity: the roster sees it as the unbound (alive=false) perch the bug
471	        // mis-derived to Offline.
472	        assert!(
473	            spt_store::liveness::is_perch_unbound(&perch_path),
474	            "precondition: the perch reads STATUS_UNBOUND"
475	        );
476	        assert!(
477	            !spt_store::liveness::is_perch_alive(&perch_path),
478	            "precondition: an unbound perch is is_perch_alive==false (bound-gated)"
479	        );
480
481	        // The REAL pipeline.
482	        let rows = gather_endpoints();
483	        let row = rows
484	            .iter()
485	            .find(|r| r.id == id)
486	            .expect("the unbound perch must appear in the gathered rows");
487	        assert_eq!(
488	            row.display_status(),
489	            EpDisplay::Unbound,
490	            "a live on-disk UNBOUND perch must render hollow Unbound through the real seam, \
491	             not gray Offline (the W5 bug)"
492	        );
493	        assert_ne!(row.display_status(), EpDisplay::Offline);
494	    }
495
496	    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,
497	    // whose stale WAN snapshot disagrees with the live roster, renders the roster
498	    // status in BOTH listings (roster = ground truth); a remote-only row is left
499	    // untouched.
500	    #[test]
501	    fn reconcile_overrides_subnet_with_local_roster() {
502	        let mut local = row("self", true, EpStatus::Online);
503	        local.controllable = Some(true);
504	        local.endpoint_type = "live_agent".into();
505	        // Stale snapshot copy says offline + unknown controllability.
506	        let stale = row("self", false, EpStatus::Offline);
507	        // An endpoint this node does NOT host — must stay as-is.
508	        let remote = row("other", false, EpStatus::Online);
509
510	        let mut rows = vec![local, stale, remote];
511	        reconcile_self_owned(&mut rows);
512
513	        // Both self listings now read the live roster status + controllability.
514	        let self_rows: Vec<&EndpointRow> = rows.iter().filter(|r| r.id == "self").collect();
515	        assert_eq!(self_rows.len(), 2, "both listings kept");
516	        for r in &self_rows {
517	            assert_eq!(r.status, EpStatus::Online, "unified to roster status");
518	            assert_eq!(r.controllable, Some(true));
519	            assert_eq!(r.endpoint_type, "live_agent");

mod tests {
    use super::*;

    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: if is_local { "local".into() } else { "sub:n".into() },
            node: if is_local { "LOCAL".into() } else { "n".into() },
            status,
            is_local,
            adapter_profile: String::new(),
            project_history: Vec::new(),
            description: String::new(),
            resume_rows: Vec::new(),
            driven_by: None,
            viewer_count: 0,
            controllable: None,
            endpoint_type: String::new(),
            is_unbound: false,
        }
    }

    // [unit->REQ-ENDPOINT-UNBOUND-ATTACH] THE REAL-DERIVATION test (the one that
    // would have caught the masking bug): a live UNBOUND perch reads alive=FALSE
    // from the roster (is_perch_alive is bound-gated) + unbound=TRUE. row_status —
    // the ACTUAL rule local_rows applies — must derive EpStatus::Online for it (NOT
    // Offline), and that Online + is_unbound must resolve through display_status to
    // the hollow Unbound. The prior display unit hand-set Online (a false premise),
    // which is exactly why it stayed green while the picker rendered gray Offline.
    #[test]
    fn row_status_drives_unbound_display() {
        use crate::picker::model::EpDisplay;
        // The derivation rule, directly: unbound (alive=false) IS online; a true
        // dead perch (neither) is offline.
        assert_eq!(row_status(false, true), EpStatus::Online, "live unbound → online");
        assert_eq!(row_status(true, false), EpStatus::Online, "alive → online");
        assert_eq!(row_status(true, true), EpStatus::Online);
        assert_eq!(row_status(false, false), EpStatus::Offline, "neither → offline");

        // The full seam: feed the real-rule status + the roster unbound flag into an
        // EndpointRow exactly as local_rows does → display_status == hollow Unbound.
        let mut r = row("skeleton", true, row_status(/*alive*/ false, /*unbound*/ true));
        r.is_unbound = true; // local_rows sets is_unbound = p.unbound
        r.controllable = Some(false); // would be amber HarnessOnly if not unbound
        assert_eq!(
            r.display_status(),
            EpDisplay::Unbound,
            "live unbound derives Online → display resolves to hollow Unbound (not gray Offline)"
        );
        // And it must NOT have read as offline (the bug) — no resume rows path etc.
        assert_ne!(r.display_status(), EpDisplay::Offline, "never gray-offline for a live unbound");
    }

    // [int->REQ-ENDPOINT-UNBOUND-ATTACH] the REAL render seam (the one no unit
    // reached — the W5 bug hid here): a real on-disk STATUS_UNBOUND perch, read
    // through the ACTUAL roster→local_rows→display_status pipeline (gather_endpoints,
    // NOT a hand-built row), renders the hollow Unbound — NOT gray Offline. Drives
    // the live disk derivation: roster is_perch_alive==false (bound-gated) for the
    // unbound perch, so row_status must lift it back to Online for display_status to
    // reach Unbound. Serialized + isolated SPT_HOME via the shared test guard.
    #[test]
    fn gather_renders_live_unbound() {
        use crate::picker::model::EpDisplay;
        let _home = crate::testutil::isolated_home();

        // Write a real UNBOUND skeleton perch on disk (empty session_id, live_agent,
        // status=unbound) — exactly what `endpoint run`'s skeleton-write produces.
        let id = "unbskel";
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = spt_store::info::InfoJson::new(
            id,
            &spt_store::timefmt::now_rfc3339(),
            std::process::id(),
            "",
            "live_agent",
        );
        spt_store::info::write_info(&perch_path, &rec).unwrap();
        spt_store::info::set_status(&perch_path, spt_store::liveness::STATUS_UNBOUND).unwrap();

        // Sanity: the roster sees it as the unbound (alive=false) perch the bug
        // mis-derived to Offline.
        assert!(
            spt_store::liveness::is_perch_unbound(&perch_path),
            "precondition: the perch reads STATUS_UNBOUND"
        );
        assert!(
            !spt_store::liveness::is_perch_alive(&perch_path),
            "precondition: an unbound perch is is_perch_alive==false (bound-gated)"
        );

        // The REAL pipeline.
        let rows = gather_endpoints();
        let row = rows
            .iter()
            .find(|r| r.id == id)
            .expect("the unbound perch must appear in the gathered rows");
        assert_eq!(
            row.display_status(),
            EpDisplay::Unbound,
            "a live on-disk UNBOUND perch must render hollow Unbound through the real seam, \
             not gray Offline (the W5 bug)"
        );
        assert_ne!(row.display_status(), EpDisplay::Offline);
    }

    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,
    // whose stale WAN snapshot disagrees with the live roster, renders the roster
    // status in BOTH listings (roster = ground truth); a remote-only row is left
    // untouched.
    #[test]
    fn reconcile_overrides_subnet_with_local_roster() {
        let mut local = row("self", true, EpStatus::Online);
        local.controllable = Some(true);
        local.endpoint_type = "live_agent".into();
        // Stale snapshot copy says offline + unknown controllability.
        let stale = row("self", false, EpStatus::Offline);
        // An endpoint this node does NOT host — must stay as-is.
        let remote = row("other", false, EpStatus::Online);

        let mut rows = vec![local, stale, remote];
        reconcile_self_owned(&mut rows);

        // Both self listings now read the live roster status + controllability.
        let self_rows: Vec<&EndpointRow> = rows.iter().filter(|r| r.id == "self").collect();
        assert_eq!(self_rows.len(), 2, "both listings kept");
        for r in &self_rows {
            assert_eq!(r.status, EpStatus::Online, "unified to roster status");
            assert_eq!(r.controllable, Some(true));
            assert_eq!(r.endpoint_type, "live_agent");


`gather_endpoints()` + `isolated_home()` give the real remote-picker seam. Checking how to seed a WAN snapshot.


Grep

fn load_snapshots|fn save_snapshot|fn store_snapshot|pub fn .*snapshot|fn gather_endpoints


crates\spt\src\wansend.rs:518:pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
crates\spt\src\wansend.rs:1009:    fn load_snapshots_degrades_on_junk() {
crates\spt\src\picker\data.rs:85:pub fn gather_endpoints() -> Vec<EndpointRow> {


5://! registry **snapshots** (`identity/registry/<subnet>.json`, the
51:/// Production entry: resolve `target` against the canonical snapshots and
58:    let regs = load_snapshots(&perch::identity_dir().join("registry"));
176:/// (`[subnet:]id[@node]`) against the canonical snapshots, dial, send one
185:    let regs = load_snapshots(&perch::identity_dir().join("registry"));
309:    let regs = load_snapshots(&perch::identity_dir().join("registry"));
515:/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
516:/// unparseable snapshot degrade to absent (the reader's stale-tolerant
518:pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
875:        acl.save().unwrap();
1006:    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,
1009:    fn load_snapshots_degrades_on_junk() {
1011:        assert!(load_snapshots(&dir.path().join("nope")).is_empty());
1021:        let map = load_snapshots(&d);


515	/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
516	/// unparseable snapshot degrade to absent (the reader's stale-tolerant
517	/// contract) — resolution then simply finds nothing there.
518	pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
519	    let mut out = BTreeMap::new();
520	    let Ok(entries) = std::fs::read_dir(dir) else {
521	        return out;
522	    };
523	    for entry in entries.flatten() {
524	        let path = entry.path();
525	        if path.extension().and_then(|e| e.to_str()) != Some("json") {
526	            continue;
527	        }
528	        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
529	            continue;
530	        };
531	        let Ok(raw) = std::fs::read_to_string(&path) else {
532	            continue;
533	        };
534	        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {
535	            continue;
536	        };
537	        out.insert(stem.to_string(), reg);
538	    }
539	    out
540	}
541
542	fn now_ms() -> u64 {
543	    std::time::SystemTime::now()
544	        .duration_since(std::time::UNIX_EPOCH)
545	        .map(|d| d.as_millis() as u64)
546	        .unwrap_or(0)
547	}
548
549	#[cfg(test)]

/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
/// unparseable snapshot degrade to absent (the reader's stale-tolerant
/// contract) — resolution then simply finds nothing there.
pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
    let mut out = BTreeMap::new();
    let Ok(entries) = std::fs::read_dir(dir) else {
        return out;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
            continue;
        };
        let Ok(raw) = std::fs::read_to_string(&path) else {
            continue;
        };
        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else {
            continue;
        };
        out.insert(stem.to_string(), reg);
    }
    out
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

#[cfg(test)]


1004	    }
1005
1006	    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,
1007	    // and non-json entries all yield absence, never an error.
1008	    #[test]
1009	    fn load_snapshots_degrades_on_junk() {
1010	        let dir = tempfile::tempdir().unwrap();
1011	        assert!(load_snapshots(&dir.path().join("nope")).is_empty());
1012	        let d = dir.path().join("registry");
1013	        std::fs::create_dir_all(&d).unwrap();
1014	        std::fs::write(
1015	            d.join("good.json"),
1016	            serde_json::to_string(&SubnetRegistry::new()).unwrap(),
1017	        )
1018	        .unwrap();
1019	        std::fs::write(d.join("bad.json"), "{{{").unwrap();
1020	        std::fs::write(d.join("ignored.txt"), "x").unwrap();
1021	        let map = load_snapshots(&d);
1022	        assert_eq!(map.len(), 1);
1023	        assert!(map.contains_key("good"));
1024	    }
1025	}
1026

    }

    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,
    // and non-json entries all yield absence, never an error.
    #[test]
    fn load_snapshots_degrades_on_junk() {
        let dir = tempfile::tempdir().unwrap();
        assert!(load_snapshots(&dir.path().join("nope")).is_empty());
        let d = dir.path().join("registry");
        std::fs::create_dir_all(&d).unwrap();
        std::fs::write(
            d.join("good.json"),
            serde_json::to_string(&SubnetRegistry::new()).unwrap(),
        )
        .unwrap();
        std::fs::write(d.join("bad.json"), "{{{").unwrap();
        std::fs::write(d.join("ignored.txt"), "x").unwrap();
        let map = load_snapshots(&d);
        assert_eq!(map.len(), 1);
        assert!(map.contains_key("good"));
    }
}


Building the remote-picker int needs a valid SubnetRegistry. Reading Instance + merge_instance + resource_projection.


68
69	/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and
70	/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).
71	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72	pub struct Instance {
73	    /// The node this instance runs on (node-qualifier in `id@node` addressing).
74	    pub node: String,
75	    /// Advertised liveness.
76	    pub status: Status,
77	    /// The author node's per-node monotonic epoch at the time of this update. The sole
78	    /// precedence key in [`merge_instance`] — strictly-greater wins.
79	    pub epoch: u64,
80	    /// The resource-advertisement blurb (D9-4, REQ-INST-14 — CONTEXT §resource
81	    /// advertisement): a free-text "yellow-pages" line describing what this
82	    /// endpoint can serve. **Not a separate registry** — a field on the
83	    /// instance row, riding the existing epoch lease + `RegistryUpdate`
84	    /// replication (a blurb update is ordered like any instance update);
85	    /// the "subnet resource registry" is a [projection](resource_projection)
86	    /// of these rows. Additive serde-default: absent = no blurb, and a
87	    /// pre-D9-4 record parses clean.
88	    #[serde(default, skip_serializing_if = "Option::is_none")]
89	    pub resources: Option<String>,
90	    /// The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this
91	    /// instance's `last_active_ms` recency stamp as of the authoring node's
92	    /// advertisement scan. **Not a separate gossip channel** — a field on the
93	    /// instance row riding the existing epoch lease + `RegistryUpdate`
94	    /// replication, exactly like [`resources`](Self::resources); subnet-wide
95	    /// `(last_active_node, last_active_endpoint, ts)` is a projection of
96	    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic
97	    /// (most-recently-active resolution) — the epoch stays the sole merge-
98	    /// precedence key; clock skew can cost a suboptimal surface target, never
99	    /// a wrong merge. Additive serde-default: a pre-D6 row parses clean.
100	    // [impl->REQ-PRES-1]
101	    #[serde(default, skip_serializing_if = "Option::is_none")]
102	    pub last_active_ms: Option<u64>,
103	    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT
104	    /// §Shell model "Discovery scope": shell adapters on *other* subnet nodes
105	    /// with `broadcast = subnet` surface to agents). **Not a shell-instance
106	    /// registry** (shell perches stay private to the owner link, CONTEXT
107	    /// "Not in the subnet registry") — the names of `kind="shell"` adapters
108	    /// REGISTERED on the authoring node that chose subnet-wide broadcast,
109	    /// riding the instance row like [`resources`](Self::resources) does
110	    /// (same epoch lease, same visibility gate, no new channel; node-level
111	    /// data carried per-row because the rows are the distribution).
112	    /// Discovery only: spawning one rides the deferred instantiate-anywhere
113	    /// gate. Additive serde-default: a pre-D8 row parses clean.
114	    // [impl->REQ-EP-5]
115	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
116	    pub shell_adapters: Vec<String>,
117	    /// The authoring node's human-readable label (M7 D2, REQ-SUBNET-3 —
118	    /// CONTEXT §node label): defaults to the node's OS hostname, re-checked
119	    /// at daemon startup (a hostname change updates the advertised label).
120	    /// Display + addressing sugar only — the pubkey stays the identity; views
121	    /// render `LABEL (keyprefix…)`. **Not a separate channel** — node-level
122	    /// data riding the instance row like [`resources`](Self::resources) does
123	    /// (the rows are the distribution; same epoch lease, same visibility
124	    /// gate). Additive serde-default: a pre-M7 row parses clean.
125	    // [impl->REQ-SUBNET-3]
126	    #[serde(default, skip_serializing_if = "Option::is_none")]
127	    pub node_label: Option<String>,
128	    /// The authoring node's stable machine identifier, **hashed** (M8
129	    /// decision 13/22, REQ-SUBNET-7): domain-separated SHA-256 of the OS
130	    /// machine id (`/etc/machine-id` / Windows `MachineGuid`; spt-minted
131	    /// persisted UUID where unreadable) — never the raw id on the wire.
132	    /// The re-pair overwrite anchor: a completed pairing ceremony presenting
133	    /// the same node label AND the same machine id as an existing trusted
134	    /// row marks that row's identity as superseded (the machine re-paired
135	    /// under a new key). Node-level data riding the instance row like
136	    /// [`node_label`](Self::node_label) (the rows are the distribution;
137	    /// same epoch lease, same visibility gate). Additive serde-default: a
138	    /// pre-M8 row parses clean.
139	    // [impl->REQ-SUBNET-7]
140	    #[serde(default, skip_serializing_if = "Option::is_none")]
141	    pub machine_id: Option<String>,
142	    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT
143	    /// §Gateway): the open-type tag this endpoint binds under
144	    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `"gateway"` for a
145	    /// Gateway surface). The cross-node `user-msg` identity gate reads this:
146	    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by
147	    /// the QUIC-proven origin node, and honors a `user-msg` only from a
148	    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only
149	    /// its **own** endpoints' types (no cross-node authorship); the field
150	    /// extends REQ-INST-7's data model and rides the existing epoch lease +
151	    /// `RegistryUpdate` replication like [`resources`](Self::resources) —
152	    /// no new channel, no new merge key. Additive serde-default: an N-1 row
153	    /// (no field) parses clean and resolves to "not provably user-backed"
154	    /// (the fail-closed re-stamp; rollout grace).
155	    // [impl->REQ-MSG-6]
156	    #[serde(default, skip_serializing_if = "Option::is_none")]
157	    pub endpoint_type: Option<String>,
158	}
159
160	/// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's
161	/// advertised hostname, carried INDEPENDENT of endpoint rows so a node with
162	/// **zero endpoints** still names itself to peers (the per-row
163	/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node
164	/// otherwise renders a bare key-prefix). Merged under the same
165	/// strictly-greater-epoch lease as instance rows; evicted with the node.
166	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167	pub struct NodeLabel {
168	    /// The node's advertised label (its OS hostname).
169	    pub label: String,
170	    /// The lease epoch (strictly-greater supersedes — the instance-row rule).
171	    pub epoch: u64,
172	}
173
174	/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,
175	/// deterministic serde ordering (no hash-seed nondeterminism across nodes).
176	#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
177	pub struct SubnetRegistry {
178	    #[serde(default)]
179	    by_id: BTreeMap<String, Vec<Instance>>,
180	    /// Node-level labels (`node_hex → label`), the endpoint-less carrier
181	    /// (REQ-SUBNET-3). Additive serde-default — old snapshots/feeds parse
182	    /// clean. Read by the `--nodes` view for a node's name; NEVER counted as
183	    /// an endpoint (no phantom `[n/total]` row, no liveness effect).
184	    #[serde(default)]
185	    node_labels: BTreeMap<String, NodeLabel>,
186	}
187
188	/// What [`merge_instance`] did with an update — useful for replication accounting and
189	/// for asserting the lease in tests.
190	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191	pub enum MergeOutcome {
192	    /// First instance for this `(endpoint_id, node)` — added.
193	    Inserted,
194	    /// A strictly-newer epoch superseded the stored instance.
195	    Updated,
196	    /// Equal or lower epoch — dropped as stale (the lease at work).
197	    Stale,
198	}
199
200	impl SubnetRegistry {
201	    /// An empty registry.
202	    pub fn new() -> SubnetRegistry {
203	        SubnetRegistry::default()
204	    }
205
206	    /// The instances known for an endpoint id (empty slice if none).
207	    pub fn instances(&self, endpoint_id: &str) -> &[Instance] {

/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and
/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Instance {
    /// The node this instance runs on (node-qualifier in `id@node` addressing).
    pub node: String,
    /// Advertised liveness.
    pub status: Status,
    /// The author node's per-node monotonic epoch at the time of this update. The sole
    /// precedence key in [`merge_instance`] — strictly-greater wins.
    pub epoch: u64,
    /// The resource-advertisement blurb (D9-4, REQ-INST-14 — CONTEXT §resource
    /// advertisement): a free-text "yellow-pages" line describing what this
    /// endpoint can serve. **Not a separate registry** — a field on the
    /// instance row, riding the existing epoch lease + `RegistryUpdate`
    /// replication (a blurb update is ordered like any instance update);
    /// the "subnet resource registry" is a [projection](resource_projection)
    /// of these rows. Additive serde-default: absent = no blurb, and a
    /// pre-D9-4 record parses clean.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resources: Option<String>,
    /// The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this
    /// instance's `last_active_ms` recency stamp as of the authoring node's
    /// advertisement scan. **Not a separate gossip channel** — a field on the
    /// instance row riding the existing epoch lease + `RegistryUpdate`
    /// replication, exactly like [`resources`](Self::resources); subnet-wide
    /// `(last_active_node, last_active_endpoint, ts)` is a projection of
    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic
    /// (most-recently-active resolution) — the epoch stays the sole merge-
    /// precedence key; clock skew can cost a suboptimal surface target, never
    /// a wrong merge. Additive serde-default: a pre-D6 row parses clean.
    // [impl->REQ-PRES-1]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_active_ms: Option<u64>,
    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT
    /// §Shell model "Discovery scope": shell adapters on *other* subnet nodes
    /// with `broadcast = subnet` surface to agents). **Not a shell-instance
    /// registry** (shell perches stay private to the owner link, CONTEXT
    /// "Not in the subnet registry") — the names of `kind="shell"` adapters
    /// REGISTERED on the authoring node that chose subnet-wide broadcast,
    /// riding the instance row like [`resources`](Self::resources) does
    /// (same epoch lease, same visibility gate, no new channel; node-level
    /// data carried per-row because the rows are the distribution).
    /// Discovery only: spawning one rides the deferred instantiate-anywhere
    /// gate. Additive serde-default: a pre-D8 row parses clean.
    // [impl->REQ-EP-5]
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub shell_adapters: Vec<String>,
    /// The authoring node's human-readable label (M7 D2, REQ-SUBNET-3 —
    /// CONTEXT §node label): defaults to the node's OS hostname, re-checked
    /// at daemon startup (a hostname change updates the advertised label).
    /// Display + addressing sugar only — the pubkey stays the identity; views
    /// render `LABEL (keyprefix…)`. **Not a separate channel** — node-level
    /// data riding the instance row like [`resources`](Self::resources) does
    /// (the rows are the distribution; same epoch lease, same visibility
    /// gate). Additive serde-default: a pre-M7 row parses clean.
    // [impl->REQ-SUBNET-3]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub node_label: Option<String>,
    /// The authoring node's stable machine identifier, **hashed** (M8
    /// decision 13/22, REQ-SUBNET-7): domain-separated SHA-256 of the OS
    /// machine id (`/etc/machine-id` / Windows `MachineGuid`; spt-minted
    /// persisted UUID where unreadable) — never the raw id on the wire.
    /// The re-pair overwrite anchor: a completed pairing ceremony presenting
    /// the same node label AND the same machine id as an existing trusted
    /// row marks that row's identity as superseded (the machine re-paired
    /// under a new key). Node-level data riding the instance row like
    /// [`node_label`](Self::node_label) (the rows are the distribution;
    /// same epoch lease, same visibility gate). Additive serde-default: a
    /// pre-M8 row parses clean.
    // [impl->REQ-SUBNET-7]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub machine_id: Option<String>,
    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT
    /// §Gateway): the open-type tag this endpoint binds under
    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `"gateway"` for a
    /// Gateway surface). The cross-node `user-msg` identity gate reads this:
    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by
    /// the QUIC-proven origin node, and honors a `user-msg` only from a
    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only
    /// its **own** endpoints' types (no cross-node authorship); the field
    /// extends REQ-INST-7's data model and rides the existing epoch lease +
    /// `RegistryUpdate` replication like [`resources`](Self::resources) —
    /// no new channel, no new merge key. Additive serde-default: an N-1 row
    /// (no field) parses clean and resolves to "not provably user-backed"
    /// (the fail-closed re-stamp; rollout grace).
    // [impl->REQ-MSG-6]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub endpoint_type: Option<String>,
}

/// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's
/// advertised hostname, carried INDEPENDENT of endpoint rows so a node with
/// **zero endpoints** still names itself to peers (the per-row
/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node
/// otherwise renders a bare key-prefix). Merged under the same
/// strictly-greater-epoch lease as instance rows; evicted with the node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeLabel {
    /// The node's advertised label (its OS hostname).
    pub label: String,
    /// The lease epoch (strictly-greater supersedes — the instance-row rule).
    pub epoch: u64,
}

/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,
/// deterministic serde ordering (no hash-seed nondeterminism across nodes).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubnetRegistry {
    #[serde(default)]
    by_id: BTreeMap<String, Vec<Instance>>,
    /// Node-level labels (`node_hex → label`), the endpoint-less carrier
    /// (REQ-SUBNET-3). Additive serde-default — old snapshots/feeds parse
    /// clean. Read by the `--nodes` view for a node's name; NEVER counted as
    /// an endpoint (no phantom `[n/total]` row, no liveness effect).
    #[serde(default)]
    node_labels: BTreeMap<String, NodeLabel>,
}

/// What [`merge_instance`] did with an update — useful for replication accounting and
/// for asserting the lease in tests.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeOutcome {
    /// First instance for this `(endpoint_id, node)` — added.
    Inserted,
    /// A strictly-newer epoch superseded the stored instance.
    Updated,
    /// Equal or lower epoch — dropped as stale (the lease at work).
    Stale,
}

impl SubnetRegistry {
    /// An empty registry.
    pub fn new() -> SubnetRegistry {
        SubnetRegistry::default()
    }

    /// The instances known for an endpoint id (empty slice if none).
    pub fn instances(&self, endpoint_id: &str) -> &[Instance] {


pub fn merge_instance|pub fn resource_projection|pub fn join_endpoint|fn isolated_home|pub fn create_subnet|hide_new_endpoints


crates\spt\src\wansend.rs:108:        .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt\src\wansend.rs:236:        .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt\src\wansend.rs:362:        .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt\src\testutil.rs:35:pub fn isolated_home() -> HomeGuard {
crates\spt\src\cli.rs:2413:                .map(|s| s.hide_new_endpoints)
crates\spt\src\cli.rs:2490:            .map(|s| s.hide_new_endpoints)
crates\spt-net\src\net\registry.rs:299:    pub fn merge_instance(&mut self, endpoint_id: &str, incoming: Instance) -> MergeOutcome {
crates\spt-net\src\net\registry.rs:368:pub fn resource_projection(
crates\spt-net\src\net\registry.rs:450:    pub fn join_endpoint(
crates\spt-net\src\net\registry.rs:564:/// `hide_new_endpoints` flag ([`spt_store::subnet::SubnetRecord`]); the policy is
crates\spt-net\src\net\registry.rs:658:    /// subnet's `hide_new_endpoints` flag.
crates\spt-net\src\net\registry.rs:1662:        // Subnet-default hiding (hide_new_endpoints ON) gates an endpoint with no
crates\spt-net\src\net\registry.rs:1675:            "subnet hide_new_endpoints gates advertisement"
crates\spt\src\picker\data.rs:264:        let hide_new = s.hide_new_endpoints;
crates\spt-store\src\visibility.rs:10://!              = S.hide_new_endpoints      (per-subnet, captured at join time)
crates\spt-store\src\visibility.rs:143:    /// directions; otherwise `subnet_hide_new` (the subnet's `hide_new_endpoints`,
crates\spt-store\src\visibility.rs:144:    /// captured at join time — [`SubnetRecord::hide_new_endpoints`]
crates\spt-store\src\visibility.rs:145:    /// (crate::subnet::SubnetRecord::hide_new_endpoints)) **OR** the endpoint's
crates\spt-store\src\visibility.rs:196:            "subnet hide_new_endpoints hides"
crates\spt-net\src\net\pairing\wire.rs:926:                hide_new_endpoints: false,
crates\spt-store\src\subnet.rs:90:    /// The subnet's `hide_new_endpoints` policy (ADR-0006 §6), **captured at join
crates\spt-store\src\subnet.rs:98:    pub hide_new_endpoints: bool,
crates\spt-store\src\subnet.rs:175:    pub fn create_subnet(&mut self, name: &str) -> Result<&SubnetRecord, SubnetError> {
crates\spt-store\src\subnet.rs:185:            hide_new_endpoints: false,
crates\spt-store\src\subnet.rs:210:            hide_new_endpoints: false,
crates\spt-store\src\subnet.rs:265:    /// Set a subnet's `hide_new_endpoints` policy (ADR-0006 §6). The flag is
crates\spt-store\src\subnet.rs:270:    pub fn set_hide_new_endpoints(&mut self, name: &str, hide: bool) -> Result<(), SubnetError> {
crates\spt-store\src\subnet.rs:276:        rec.hide_new_endpoints = hide;
crates\spt-store\src\subnet.rs:457:    // [unit->REQ-INST-12] hide_new_endpoints ships OFF, a pre-D3e subnet.json (no
crates\spt-store\src\subnet.rs:460:    fn hide_new_endpoints_defaults_off_and_persists() {
crates\spt-store\src\subnet.rs:463:            !store.create_subnet("home").unwrap().hide_new_endpoints,
crates\spt-store\src\subnet.rs:471:            !parsed.find("old").unwrap().hide_new_endpoints,
crates\spt-store\src\subnet.rs:477:        store.set_hide_new_endpoints("home", true).unwrap();
crates\spt-store\src\subnet.rs:479:            store.set_hide_new_endpoints("ghost", true).unwrap_err(),
crates\spt-store\src\subnet.rs:487:                .hide_new_endpoints,
crates\spt-daemon\src\notif.rs:66:/// `hide_new_endpoints`) and its visibility store. Loaded from the canonical
crates\spt-daemon\src\notif.rs:71:    /// `(subnet name, hide_new_endpoints)` for every subnet this node is in.
crates\spt-daemon\src\notif.rs:82:            .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt-daemon\src\pump\mod.rs:353:                .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt-daemon\src\pump\update.rs:149:                        .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt-daemon\src\sync.rs:65:/// `hide_new_endpoints` flag), its pinned peers, and its visibility/sync
crates\spt-daemon\src\sync.rs:70:    /// `(subnet name, hide_new_endpoints)` for every subnet this node is in.
crates\spt-daemon\src\sync.rs:86:            .map(|s| (s.name.clone(), s.hide_new_endpoints))
crates\spt-daemon\src\registryhost.rs:451:                    sub.hide_new_endpoints,


360
361	/// Project one subnet's registry into its resource-registry view, over
362	/// **visible** rows only: `excluded(id)` is the same visibility closure the
363	/// resolver uses ([`SubnetRegistry::resolve_visible`]), so a hidden endpoint
364	/// never appears — discovery leaks nothing a viewer couldn't reach
365	/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
366	/// skipped too: a dead instance serves nothing, so it is not a resource.
367	// [impl->REQ-INST-14]
368	pub fn resource_projection(
369	    reg: &SubnetRegistry,
370	    excluded: impl Fn(&str) -> bool,
371	) -> Vec<ResourceRow> {
372	    // Node-level label fill: an instance row carries its authoring node's label
373	    // ([`Instance::node_label`]), but a row authored before the host stamped one
374	    // (or by an endpoint-less node) may lack it — fall back to the dedicated
375	    // node-label map so the canonical "LABEL (keyprefix…)" render never degrades
376	    // to bare hex when the label IS known (REQ-PICKER-4).
377	    let node_labels: std::collections::BTreeMap<&str, &str> = reg.node_labels().collect();
378	    let mut out = Vec::new();
379	    for id in reg.endpoint_ids() {
380	        if excluded(id) {
381	            continue;
382	        }
383	        for instance in reg.instances(id) {
384	            if !instance.status.routable() {
385	                continue;
386	            }
387	            let node_label = instance
388	                .node_label
389	                .clone()
390	                .or_else(|| node_labels.get(instance.node.as_str()).map(|l| l.to_string()));
391	            out.push(ResourceRow {
392	                endpoint_id: id.to_string(),
393	                node: instance.node.clone(),
394	                status: instance.status,
395	                resources: instance.resources.clone(),
396	                node_label,
397	            });
398	        }
399	    }
400	    out
401	}
402
403	/// A join-time bare-id collision (ADR-0006 §1): the joining endpoint's id is already
404	/// held in the target subnet by a *different* node, so advertising it would create two
405	/// distinct endpoints sharing one bare name inside a single subnet. The caller surfaces
406	/// the clash for resolution (rename one — D3f) before any advertisement happens.
407	#[derive(Debug, Clone, PartialEq, Eq)]
408	pub struct JoinCollision {
409	    /// The bare id that clashed.
410	    pub endpoint_id: String,
411	    /// The nodes already holding that id in the target subnet.
412	    pub nodes: Vec<String>,
413	}
414
415	impl std::fmt::Display for JoinCollision {
416	    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417	        write!(
418	            f,
419	            "endpoint id '{}' already exists in this subnet on node(s): {}",
420	            self.endpoint_id,
421	            self.nodes.join(", ")
422	        )
423	    }
424	}
425
426	impl std::error::Error for JoinCollision {}
427
428	impl SubnetRegistry {
429	    /// Advertise a **joining** endpoint into this subnet, enforcing the join-time
430	    /// bare-id collision check (ADR-0006 §1: bare-id uniqueness holds *within* a
431	    /// subnet). Refuses — registry untouched — when the id is already held by a
432	    /// **different** node, including by an `Offline` instance (a resting endpoint still
433	    /// owns its name until renamed or removed). Rows on the *same* node are not a
434	    /// collision (the node re-advertising its own endpoint); those fall through to
435	    /// [`merge_instance`], where the epoch lease governs as usual.
436	    ///
437	    /// Scope notes: the check is **per-subnet** — the same bare name may exist as
438	    /// distinct endpoints in *different* subnets (the resolution policy forces
439	    /// `subnet:` qualification, REQ-INST-10). And it is **same-user only** for v1:
440	    /// the registry this is invoked on is the per-subnet registry of the single
441	    /// implicit user; under the cross-user seam (ADR-0006) the registry key
442	    /// generalizes to `(subnet, user)` and this check rides along unchanged, scoping
443	    /// to the one registry it is called on.
444	    ///
445	    /// Replication of an **existing** instance (the same logical endpoint reported
446	    /// from its own node, locally or via the D4 wire) goes straight through
447	    /// [`merge_instance`] — this gate is only for *joining/advertising an endpoint
448	    /// into a subnet*, the moment a name is claimed.
449	    // [impl->REQ-INST-9]

/// Project one subnet's registry into its resource-registry view, over
/// **visible** rows only: `excluded(id)` is the same visibility closure the
/// resolver uses ([`SubnetRegistry::resolve_visible`]), so a hidden endpoint
/// never appears — discovery leaks nothing a viewer couldn't reach
/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
/// skipped too: a dead instance serves nothing, so it is not a resource.
// [impl->REQ-INST-14]
pub fn resource_projection(
    reg: &SubnetRegistry,
    excluded: impl Fn(&str) -> bool,
) -> Vec<ResourceRow> {
    // Node-level label fill: an instance row carries its authoring node's label
    // ([`Instance::node_label`]), but a row authored before the host stamped one
    // (or by an endpoint-less node) may lack it — fall back to the dedicated
    // node-label map so the canonical "LABEL (keyprefix…)" render never degrades
    // to bare hex when the label IS known (REQ-PICKER-4).
    let node_labels: std::collections::BTreeMap<&str, &str> = reg.node_labels().collect();
    let mut out = Vec::new();
    for id in reg.endpoint_ids() {
        if excluded(id) {
            continue;
        }
        for instance in reg.instances(id) {
            if !instance.status.routable() {
                continue;
            }
            let node_label = instance
                .node_label
                .clone()
                .or_else(|| node_labels.get(instance.node.as_str()).map(|l| l.to_string()));
            out.push(ResourceRow {
                endpoint_id: id.to_string(),
                node: instance.node.clone(),
                status: instance.status,
                resources: instance.resources.clone(),
                node_label,
            });
        }
    }
    out
}

/// A join-time bare-id collision (ADR-0006 §1): the joining endpoint's id is already
/// held in the target subnet by a *different* node, so advertising it would create two
/// distinct endpoints sharing one bare name inside a single subnet. The caller surfaces
/// the clash for resolution (rename one — D3f) before any advertisement happens.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JoinCollision {
    /// The bare id that clashed.
    pub endpoint_id: String,
    /// The nodes already holding that id in the target subnet.
    pub nodes: Vec<String>,
}

impl std::fmt::Display for JoinCollision {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "endpoint id '{}' already exists in this subnet on node(s): {}",
            self.endpoint_id,
            self.nodes.join(", ")
        )
    }
}

impl std::error::Error for JoinCollision {}

impl SubnetRegistry {
    /// Advertise a **joining** endpoint into this subnet, enforcing the join-time
    /// bare-id collision check (ADR-0006 §1: bare-id uniqueness holds *within* a
    /// subnet). Refuses — registry untouched — when the id is already held by a
    /// **different** node, including by an `Offline` instance (a resting endpoint still
    /// owns its name until renamed or removed). Rows on the *same* node are not a
    /// collision (the node re-advertising its own endpoint); those fall through to
    /// [`merge_instance`], where the epoch lease governs as usual.
    ///
    /// Scope notes: the check is **per-subnet** — the same bare name may exist as
    /// distinct endpoints in *different* subnets (the resolution policy forces
    /// `subnet:` qualification, REQ-INST-10). And it is **same-user only** for v1:
    /// the registry this is invoked on is the per-subnet registry of the single
    /// implicit user; under the cross-user seam (ADR-0006) the registry key
    /// generalizes to `(subnet, user)` and this check rides along unchanged, scoping
    /// to the one registry it is called on.
    ///
    /// Replication of an **existing** instance (the same logical endpoint reported
    /// from its own node, locally or via the D4 wire) goes straight through
    /// [`merge_instance`] — this gate is only for *joining/advertising an endpoint
    /// into a subnet*, the moment a name is claimed.
    // [impl->REQ-INST-9]


resource_projection keeps Suspended (routable) but skips Offline — so the picker maps Suspended→Offline, dead perch shows gray. Writing the remote-picker int test.


1	//! Picker **data gather** — read-only projections over the EXISTING surfaces
2	//! the rest of the CLI already uses. No new daemon surface, no new store
3	//! (doyle ruling): adapters from the registry, endpoints from the roster + the
4	//! WAN registry snapshots, descriptions from `info.json`, project history
5	//! best-effort from the context store, resume rows from the session ledger.
6	//!
7	//! Everything here degrades to empty rather than failing — the picker is a
8	//! front-end; a missing surface means "nothing to show", never a crash.
9	// [impl->REQ-RUN-PICKER]
10
11	use std::path::Path;
12
13	use spt_store::perch::{self, ParentHint};
14	use spt_store::{info, project, sessions};
15
16	use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};
17
18	/// The cwd project name — the Project category label + the project-history match
19	/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
20	/// folder → dir name).
21	pub fn project_name() -> String {
22	    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
23	    project::project_id_for_dir(&dir)
24	}
25
26	/// The node's MEMBER subnets, MRU-ordered (most-recent home first), for the
27	/// create-new home-subnet layer (REQ-RUN-PICKER-HOME). Mirrors `decide_run_home`'s
28	/// ordering exactly: the SAME two-level MRU preference (this cwd's project list,
29	/// then the node-global list) fed through the SHARED `cli::order_by_mru`
30	/// move-to-front — so the picker's home ordering matches the CLI confirm's. A

//! Picker **data gather** — read-only projections over the EXISTING surfaces
//! the rest of the CLI already uses. No new daemon surface, no new store
//! (doyle ruling): adapters from the registry, endpoints from the roster + the
//! WAN registry snapshots, descriptions from `info.json`, project history
//! best-effort from the context store, resume rows from the session ledger.
//!
//! Everything here degrades to empty rather than failing — the picker is a
//! front-end; a missing surface means "nothing to show", never a crash.
// [impl->REQ-RUN-PICKER]

use std::path::Path;

use spt_store::perch::{self, ParentHint};
use spt_store::{info, project, sessions};

use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};

/// The cwd project name — the Project category label + the project-history match
/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
/// folder → dir name).
pub fn project_name() -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    project::project_id_for_dir(&dir)
}

/// The node's MEMBER subnets, MRU-ordered (most-recent home first), for the
/// create-new home-subnet layer (REQ-RUN-PICKER-HOME). Mirrors `decide_run_home`'s
/// ordering exactly: the SAME two-level MRU preference (this cwd's project list,
/// then the node-global list) fed through the SHARED `cli::order_by_mru`
/// move-to-front — so the picker's home ordering matches the CLI confirm's. A


Edit

// [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,

    // [int->REQ-PRESENCE-LIVENESS-TRUTH] the REMOTE half of the presence-truth
    // fix, end-to-end through the real subnet seam: a node gossips one DEAD perch
    // (now Status::Suspended, the registryhost fix — was the blanket Dormant) and
    // one WARM sibling (Status::Dormant). Loaded through the ACTUAL
    // load_snapshots → resource_projection → subnet_rows pipeline (gather_endpoints,
    // not a hand-built row), the dead perch must read NOT online (gray Offline)
    // while the warm dormant sibling stays online — so a remote picker agrees with
    // the dead perch's home node instead of painting it false-green. The pre-fix
    // Dormant would have mapped Dormant→Online here (the bug).
    #[test]
    fn remote_picker_renders_suspended_dead_perch_offline() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};

        let _home = crate::testutil::isolated_home();

        // The subnet must exist locally for subnet_rows to project its snapshot.
        let mut subnets = spt_store::subnet::SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.save().unwrap();

        let remote_node = "remote00node00hex";
        let inst = |status: Status| Instance {
            node: remote_node.to_string(),
            status,
            epoch: 5,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: Some("REMOTE".to_string()),
            machine_id: None,
            endpoint_type: None,
        };
        let mut reg = SubnetRegistry::new();
        // The dead perch the fixed daemon now self-gossips as Suspended.
        reg.merge_instance("deadep", inst(Status::Suspended));
        // A genuinely warm dormant sibling — a valid routing fallback, stays online.
        reg.merge_instance("warmep", inst(Status::Dormant));

        let snap_dir = perch::identity_dir().join("registry");
        std::fs::create_dir_all(&snap_dir).unwrap();
        std::fs::write(
            snap_dir.join("home.json"),
            serde_json::to_string(&reg).unwrap(),
        )
        .unwrap();

        let rows = gather_endpoints();
        let dead = rows
            .iter()
            .find(|r| r.id == "deadep")
            .expect("the dead perch's row is still listed (Suspended is routable)");
        let warm = rows
            .iter()
            .find(|r| r.id == "warmep")
            .expect("the warm dormant sibling is listed");
        assert_eq!(
            dead.status,
            EpStatus::Offline,
            "a dead (Suspended) remote perch reads NOT online — home-vs-remote agree"
        );
        assert_eq!(
            warm.status,
            EpStatus::Online,
            "a warm Dormant sibling is a valid routing fallback — stays online"
        );
    }

    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,


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

    // [int->REQ-PRESENCE-LIVENESS-TRUTH] the REMOTE half of the presence-truth
    // fix, end-to-end through the real subnet seam: a node gossips one DEAD perch
    // (now Status::Suspended, the registryhost fix — was the blanket Dormant) and
    // one WARM sibling (Status::Dormant). Loaded through the ACTUAL
    // load_snapshots → resource_projection → subnet_rows pipeline (gather_endpoints,
    // not a hand-built row), the dead perch must read NOT online (gray Offline)
    // while the warm dormant sibling stays online — so a remote picker agrees with
    // the dead perch's home node instead of painting it false-green. The pre-fix
    // Dormant would have mapped Dormant→Online here (the bug).
    #[test]
    fn remote_picker_renders_suspended_dead_perch_offline() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};

        let _home = crate::testutil::isolated_home();

        // The subnet must exist locally for subnet_rows to project its snapshot.
        let mut subnets = spt_store::subnet::SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.save().unwrap();

        let remote_node = "remote00node00hex";
        let inst = |status: Status| Instance {
            node: remote_node.to_string(),
            status,
            epoch: 5,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: Some("REMOTE".to_string()),
            machine_id: None,
            endpoint_type: None,
        };
        let mut reg = SubnetRegistry::new();
        // The dead perch the fixed daemon now self-gossips as Suspended.
        reg.merge_instance("deadep", inst(Status::Suspended));
        // A genuinely warm dormant sibling — a valid routing fallback, stays online.
        reg.merge_instance("warmep", inst(Status::Dormant));

        let snap_dir = perch::identity_dir().join("registry");
        std::fs::create_dir_all(&snap_dir).unwrap();
        std::fs::write(
            snap_dir.join("home.json"),
            serde_json::to_string(&reg).unwrap(),
        )
        .unwrap();

        let rows = gather_endpoints();
        let dead = rows
            .iter()
            .find(|r| r.id == "deadep")
            .expect("the dead perch's row is still listed (Suspended is routable)");
        let warm = rows
            .iter()
            .find(|r| r.id == "warmep")
            .expect("the warm dormant sibling is listed");
        assert_eq!(
            dead.status,
            EpStatus::Offline,
            "a dead (Suspended) remote perch reads NOT online — home-vs-remote agree"
        );
        assert_eq!(
            warm.status,
            EpStatus::Online,
            "a warm Dormant sibling is a valid routing fallback — stays online"
        );
    }

    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,

+    // [int->REQ-PRESENCE-LIVENESS-TRUTH] the REMOTE half of the presence-truth

+    // fix, end-to-end through the real subnet seam: a node gossips one DEAD perch

+    // (now Status::Suspended, the registryhost fix — was the blanket Dormant) and

+    // one WARM sibling (Status::Dormant). Loaded through the ACTUAL

+    // load_snapshots → resource_projection → subnet_rows pipeline (gather_endpoints,

+    // not a hand-built row), the dead perch must read NOT online (gray Offline)

+    // while the warm dormant sibling stays online — so a remote picker agrees with

+    // the dead perch's home node instead of painting it false-green. The pre-fix

+    // Dormant would have mapped Dormant→Online here (the bug).

+    fn remote_picker_renders_suspended_dead_perch_offline() {

+        use spt_net::net::registry::{Instance, Status, SubnetRegistry};

+        let _home = crate::testutil::isolated_home();

+        // The subnet must exist locally for subnet_rows to project its snapshot.

+        let mut subnets = spt_store::subnet::SubnetStore::load();

+        subnets.create_subnet("home").unwrap();

+        let remote_node = "remote00node00hex";

+        let inst = |status: Status| Instance {

+            node: remote_node.to_string(),

+            shell_adapters: Vec::new(),

+            node_label: Some("REMOTE".to_string()),

+        let mut reg = SubnetRegistry::new();

+        // The dead perch the fixed daemon now self-gossips as Suspended.

+        reg.merge_instance("deadep", inst(Status::Suspended));

+        // A genuinely warm dormant sibling — a valid routing fallback, stays online.

+        reg.merge_instance("warmep", inst(Status::Dormant));

+        let snap_dir = perch::identity_dir().join("registry");

+        std::fs::create_dir_all(&snap_dir).unwrap();

+            snap_dir.join("home.json"),

+            serde_json::to_string(&reg).unwrap(),

+            .find(|r| r.id == "deadep")

+            .expect("the dead perch's row is still listed (Suspended is routable)");

+            .find(|r| r.id == "warmep")

+            .expect("the warm dormant sibling is listed");

+            "a dead (Suspended) remote perch reads NOT online — home-vs-remote agree"

+            "a warm Dormant sibling is a valid routing fallback — stays online"

// whose stale WAN snapshot disagrees with the live roster, renders the roster

// status in BOTH listings (roster = ground truth); a remote-only row is left


W4 evidence in place. Now activate traceable stages. Reading the REQ rows to edit precisely.


1453	# docs/design/robust-wan-subnet-join.md. Glossary correction (TOTP-epoch vs TOTP-code) landed in
1454	# CONTEXT.md. Cluster doc-ratified by ADR-0030; impl/unit/int activate at the build milestone (rule 5).
1455	[[requirements]]
1456	id = "REQ-JOIN-TWO-PHASE"
1457	title = "`spt subnet join` is two-phase (meet-before-code) so the entered code is FRESH at the ceremony regardless of discovery time (ADR-0030). The meet selector is the PUBLIC `(subnet-name, TOTP-epoch)` (rendezvous_token = SHA256(domain ‖ name ‖ totp_step)); the secret TOTP-code is the SPAKE2 password ONLY, never a discovery input — so the code is collected AFTER a member is found. Extend the brain.pair_join event stream (brain.rs:1009): CLI PairMeetReq{subnet} → daemon meets (name, current-epoch) resolving the seed-holder's REAL stable pairing address → MetMember event → CLI prompts the code (cli.rs cmd_subnet_join :6236) → PairCodeSubmit{code} → daemon dials the held real-address on SPT_PAIR_ALPN + SPAKE2 → PairJoined/PairFail. Daemon holds the real-address between phases, bounded by a 5-MINUTE wait-for-code timeout; a wrong-code retry re-runs the CEREMONY ONLY against the held address (no re-search). The non-interactive `--code` path stays one-shot (no prompt; relies on REQ-NET-FAMILY-GATE fast discovery, fails loudly per REQ-JOIN-DIAGNOSTICS on staleness). Security unchanged — the meet is pre-trust/unauthenticated (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2. (next milestone)"
1458	required_stages = ["doc"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.
1459	[[requirements]]
1460	id = "REQ-NET-FAMILY-GATE"
1461	title = "spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone)"
1462	required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: per-family reachability probe at bind + bind-only-working-families + SPT_DISABLE_IPV6/IPV4 overrides; impl-verify the iroh single-family bind API + whether iroh hole-punch already fast-fails. unit = family-selection logic (both/v4-dead/v6-dead/forced) without live sockets.
1463	[[requirements]]
1464	id = "REQ-JOIN-DIAGNOSTICS"
1465	title = "`spt subnet join` never fails SILENTLY (ADR-0030; the field incident showed no output at all). (a) LIVE progress during the meet (replace the one-shot \"Searching…\" cli.rs:6268 with periodic elapsed/deadline) so silence ≠ hang; (b) DETAILED failure on meet-exhaustion — rendezvous candidates + families attempted (IPv4/IPv6) + relay-vs-direct + the last concrete error — surfaced BEFORE any code prompt (a dead subnet must not make the user fetch a code); connect_seed_holder (pairhost.rs:437) and dial_via_rendezvous (meet.rs:281) currently swallow per-attempt errors — thread the last error up with attempt context; (c) PROPAGATE the terminal event — brain.rs:1024 `_ => continue` must deliver a daemon NoSeedHolder/PairFail to the CLI as a printed error (this is WHY the user saw nothing); (d) `--verbose`/`SPT_LOG` discovery TRACE (per-probe derived id, discovery path mDNS/n0-DNS/relay, per-family timeouts), opt-in — no such knob exists today. (next milestone)"
1466	required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: search-progress + detailed-failure-before-prompt + brain.rs:1024 terminal-event propagation + --verbose/SPT_LOG trace. unit = failure-message composition (what-was-tried) + terminal-event routing (no _=>continue swallow).
1467
1468	# ── Endpoint-liveness gossip truth (diagnosed 2026-06-27, same session) — a remote node showed all of ──
1469	# another node's DEAD endpoints as ONLINE. No ADR (a correctness fix, unsurprising once seen). Seeded for
1470	# the next milestone JIT alongside the ADR-0030 join cluster.
1471	[[requirements]]
1472	id = "REQ-PRESENCE-LIVENESS-TRUTH"
1473	title = "A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone)"
1474	required_stages = []  # SEED (rule 5). next-milestone build: registryhost not-bound-alive → {Active/Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline. unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]
1475	[[requirements]]
1476	id = "REQ-SUBNET-DISPLAY-PARITY"
1477	title = "The `spt endpoint run` picker renders endpoint state IDENTICALLY for local and remote (subnet) rows — bound/unbound, controlled (+driver node), and harness-only are all visible across the subnet, not local-only. Today data.rs:293-294 reduces a remote row to plain green-filled/gray-hollow because those facts aren't propagated. (1) GOSSIP additive per-Instance fields: bound/unbound, controlled + driver-node, harness_only (Offline is never gossiped — node-down is remote-inferred). (2) Derive the full EpDisplay for subnet rows from the gossiped fields, same path as local (remove the remote-reduction). (3) PALETTE rework (fill = ACTIONABLE: filled=can act now (rc-control if online, WAKE if suspended-on-live-node) / hollow=cannot (no control seat / node gone)): green-filled=online+bound+free; blue-filled=online+controlled (desc shows `controlled by <node>`); RED-filled=online+UNBOUND (controlled-or-not; controlled-ness shown via available options not glyph) — replaces green-hollow Unbound + absorbs the dropped UnboundControlled; AMBER-HOLLOW=online+harness-only (no broker seat → can't rc) — was amber-FILLED; GRAY-FILLED=Suspended (cold, node up — wakeable) NEW; gray-hollow=Offline (node down) now REMOTE-ONLY. EpDisplay: drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, add Suspended(gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow. (next milestone)"
1478	required_stages = []  # SEED (rule 5). next-milestone build: Instance gossip fields (bound/controlled+driver/harness_only) + remove data.rs:293 remote-reduction + EpDisplay rework (drop UnboundControlled / Unbound=red-filled / HarnessOnly=amber-hollow / add Suspended=gray-filled) + desc `controlled by <node>`. unit = EpDisplay derivation over the full field matrix (online{bound,unbound}×{free,controlled}, harness-only, suspended, offline) + glyph/colour bytes. int = a remote row shows unbound/controlled/harness-only/suspended correctly (local-vs-remote parity). Design: docs/design/subnet-presence-display.md §B.
1479

# docs/design/robust-wan-subnet-join.md. Glossary correction (TOTP-epoch vs TOTP-code) landed in
# CONTEXT.md. Cluster doc-ratified by ADR-0030; impl/unit/int activate at the build milestone (rule 5).
[[requirements]]
id = "REQ-JOIN-TWO-PHASE"
title = "`spt subnet join` is two-phase (meet-before-code) so the entered code is FRESH at the ceremony regardless of discovery time (ADR-0030). The meet selector is the PUBLIC `(subnet-name, TOTP-epoch)` (rendezvous_token = SHA256(domain ‖ name ‖ totp_step)); the secret TOTP-code is the SPAKE2 password ONLY, never a discovery input — so the code is collected AFTER a member is found. Extend the brain.pair_join event stream (brain.rs:1009): CLI PairMeetReq{subnet} → daemon meets (name, current-epoch) resolving the seed-holder's REAL stable pairing address → MetMember event → CLI prompts the code (cli.rs cmd_subnet_join :6236) → PairCodeSubmit{code} → daemon dials the held real-address on SPT_PAIR_ALPN + SPAKE2 → PairJoined/PairFail. Daemon holds the real-address between phases, bounded by a 5-MINUTE wait-for-code timeout; a wrong-code retry re-runs the CEREMONY ONLY against the held address (no re-search). The non-interactive `--code` path stays one-shot (no prompt; relies on REQ-NET-FAMILY-GATE fast discovery, fails loudly per REQ-JOIN-DIAGNOSTICS on staleness). Security unchanged — the meet is pre-trust/unauthenticated (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2. (next milestone)"
required_stages = ["doc"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.
[[requirements]]
id = "REQ-NET-FAMILY-GATE"
title = "spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone)"
required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: per-family reachability probe at bind + bind-only-working-families + SPT_DISABLE_IPV6/IPV4 overrides; impl-verify the iroh single-family bind API + whether iroh hole-punch already fast-fails. unit = family-selection logic (both/v4-dead/v6-dead/forced) without live sockets.
[[requirements]]
id = "REQ-JOIN-DIAGNOSTICS"
title = "`spt subnet join` never fails SILENTLY (ADR-0030; the field incident showed no output at all). (a) LIVE progress during the meet (replace the one-shot \"Searching…\" cli.rs:6268 with periodic elapsed/deadline) so silence ≠ hang; (b) DETAILED failure on meet-exhaustion — rendezvous candidates + families attempted (IPv4/IPv6) + relay-vs-direct + the last concrete error — surfaced BEFORE any code prompt (a dead subnet must not make the user fetch a code); connect_seed_holder (pairhost.rs:437) and dial_via_rendezvous (meet.rs:281) currently swallow per-attempt errors — thread the last error up with attempt context; (c) PROPAGATE the terminal event — brain.rs:1024 `_ => continue` must deliver a daemon NoSeedHolder/PairFail to the CLI as a printed error (this is WHY the user saw nothing); (d) `--verbose`/`SPT_LOG` discovery TRACE (per-probe derived id, discovery path mDNS/n0-DNS/relay, per-family timeouts), opt-in — no such knob exists today. (next milestone)"
required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: search-progress + detailed-failure-before-prompt + brain.rs:1024 terminal-event propagation + --verbose/SPT_LOG trace. unit = failure-message composition (what-was-tried) + terminal-event routing (no _=>continue swallow).

# ── Endpoint-liveness gossip truth (diagnosed 2026-06-27, same session) — a remote node showed all of ──
# another node's DEAD endpoints as ONLINE. No ADR (a correctness fix, unsurprising once seen). Seeded for
# the next milestone JIT alongside the ADR-0030 join cluster.
[[requirements]]
id = "REQ-PRESENCE-LIVENESS-TRUTH"
title = "A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone)"
required_stages = []  # SEED (rule 5). next-milestone build: registryhost not-bound-alive → {Active/Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline. unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]
[[requirements]]
id = "REQ-SUBNET-DISPLAY-PARITY"
title = "The `spt endpoint run` picker renders endpoint state IDENTICALLY for local and remote (subnet) rows — bound/unbound, controlled (+driver node), and harness-only are all visible across the subnet, not local-only. Today data.rs:293-294 reduces a remote row to plain green-filled/gray-hollow because those facts aren't propagated. (1) GOSSIP additive per-Instance fields: bound/unbound, controlled + driver-node, harness_only (Offline is never gossiped — node-down is remote-inferred). (2) Derive the full EpDisplay for subnet rows from the gossiped fields, same path as local (remove the remote-reduction). (3) PALETTE rework (fill = ACTIONABLE: filled=can act now (rc-control if online, WAKE if suspended-on-live-node) / hollow=cannot (no control seat / node gone)): green-filled=online+bound+free; blue-filled=online+controlled (desc shows `controlled by <node>`); RED-filled=online+UNBOUND (controlled-or-not; controlled-ness shown via available options not glyph) — replaces green-hollow Unbound + absorbs the dropped UnboundControlled; AMBER-HOLLOW=online+harness-only (no broker seat → can't rc) — was amber-FILLED; GRAY-FILLED=Suspended (cold, node up — wakeable) NEW; gray-hollow=Offline (node down) now REMOTE-ONLY. EpDisplay: drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, add Suspended(gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow. (next milestone)"
required_stages = []  # SEED (rule 5). next-milestone build: Instance gossip fields (bound/controlled+driver/harness_only) + remove data.rs:293 remote-reduction + EpDisplay rework (drop UnboundControlled / Unbound=red-filled / HarnessOnly=amber-hollow / add Suspended=gray-filled) + desc `controlled by <node>`. unit = EpDisplay derivation over the full field matrix (online{bound,unbound}×{free,controlled}, harness-only, suspended, offline) + glyph/colour bytes. int = a remote row shows unbound/controlled/harness-only/suspended correctly (local-vs-remote parity). Design: docs/design/subnet-presence-display.md §B.


W4 complete. Activating REQ-PRESENCE-LIVENESS-TRUTH (impl+unit+int).


required_stages = []  # SEED (rule 5). next-milestone build: registryhost not-bound-alive → {Active/Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline. unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.17.0 W4. impl = registryhost advertised_status (not-bound-alive → {Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline). unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]


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

title = "A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone)"

-required_stages = []  # SEED (rule 5). next-milestone build: registryhost not-bound-alive → {Active/Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline. unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]

+required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.17.0 W4. impl = registryhost advertised_status (not-bound-alive → {Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline). unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]

title = "The `spt endpoint run` picker renders endpoint state IDENTICALLY for local and remote (subnet) rows — bound/unbound, controlled (+driver node), and harness-only are all visible across the subnet, not local-only. Today data.rs:293-294 reduces a remote row to plain green-filled/gray-hollow because those facts aren't propagated. (1) GOSSIP additive per-Instance fields: bound/unbound, controlled + driver-node, harness_only (Offline is never gossiped — node-down is remote-inferred). (2) Derive the full EpDisplay for subnet rows from the gossiped fields, same path as local (remove the remote-reduction). (3) PALETTE rework (fill = ACTIONABLE: filled=can act now (rc-control if online, WAKE if suspended-on-live-node) / hollow=cannot (no control seat / node gone)): green-filled=online+bound+free; blue-filled=online+controlled (desc shows `controlled by <node>`); RED-filled=online+UNBOUND (controlled-or-not; controlled-ness shown via available options not glyph) — replaces green-hollow Unbound + absorbs the dropped UnboundControlled; AMBER-HOLLOW=online+harness-only (no broker seat → can't rc) — was amber-FILLED; GRAY-FILLED=Suspended (cold, node up — wakeable) NEW; gray-hollow=Offline (node down) now REMOTE-ONLY. EpDisplay: drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, add Suspended(gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow. (next milestone)"


W4 done. Now W1 — per-family bind gate. Checking tokio features available in spt-net.


Bash

cat crates/spt-net/Cargo.toml | grep -A3 -i tokio


# embedded library consumers. The `net` feature gates the whole iroh/tokio surface
# so a `--no-default-features` lib build stays transport-free.
default = ["net"]
net = ["dep:iroh", "dep:tokio", "dep:hmac", "dep:sha1", "dep:sha2", "dep:hkdf", "dep:data-encoding", "dep:spake2", "dep:subtle", "dep:zeroize", "dep:serde_json"]

[dependencies]
spt-proto = { path = "../spt-proto" }
--
# iroh requires a tokio runtime. The daemon (D4) will host the runtime that owns the
# broker endpoint. Pulled in only with `net`.
tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros", "time", "sync"] }
# Pairing crypto (ADR-0005 / D2), all `net`-gated/optional. RFC 6238 TOTP is rolled on
# RustCrypto HMAC+SHA-1 (D2a) rather than a TOTP crate, so the raw time-step counter
# stays first-class for transcript binding (#12) and no URL/QR/image deps are pulled.


tokio has no `net` feature here — using `std::net::TcpStream::connect_timeout` in `spawn_blocking`. Adding the family-gate logic.


Edit

impl std::error::Error for NetError {}

/// A bound spt-core network endpoint: the node's reachable presence on the subnet.
pub struct NetEndpoint {
    inner: Endpoint,
}

impl std::error::Error for NetError {}

/// Which IP families a production-scope endpoint actually binds
/// (REQ-NET-FAMILY-GATE). iroh binds dual-stack by default; a half-broken family
/// (e.g. IPv6 whose AAAA resolves but whose path is dead) silently burns
/// discovery/connect time on every attempt. We probe each family once at bind and
/// drop the *dead* one — never a fixed "prefer IPv4" (an IPv6-only network must
/// still work).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BindFamilies {
    /// Both families bind (the iroh default — both healthy, or the probe was
    /// inconclusive and we must not strand the node).
    Dual,
    /// IPv4 only — IPv6 is dead or force-disabled.
    V4Only,
    /// IPv6 only — IPv4 is dead or force-disabled.
    V6Only,
}

/// WAN reachability probe targets — Cloudflare's public anycast resolver on each
/// family (no DNS step; a literal address per family). A bounded TCP connect to
/// `:443` is the same discriminator the field incident used by hand
/// (`Test-NetConnection dns.iroh.link -Port 443`): it catches a half-broken family
/// whose route exists but whose path is dead, which a mere route-table check
/// (UDP connect) would miss.
const PROBE_V4: &str = "1.1.1.1:443";
const PROBE_V6: &str = "[2606:4700:4700::1111]:443";

/// Per-family reachability probe timeout. Short so a healthy bind is not slowed
/// (a reachable family answers in tens of ms; only a DEAD family waits this out,
/// and that wait replaces a 75 s silent rendezvous failure).
const FAMILY_PROBE_TIMEOUT: Duration = Duration::from_millis(1500);

/// Parse a force-off env flag (`SPT_DISABLE_IPV6` / `SPT_DISABLE_IPV4`): present
/// and "truthy" ⇒ force the family off. Empty / `0` / `false` / `no` read as off
/// (the flag is not set), mirroring a conventional boolean env knob. Factored from
/// [`env_flag`] so the parse is unit-testable without touching the environment.
fn flag_is_on(val: Option<&str>) -> bool {
    match val {
        Some(v) => {
            let v = v.trim();
            !v.is_empty()
                && !v.eq_ignore_ascii_case("0")
                && !v.eq_ignore_ascii_case("false")
                && !v.eq_ignore_ascii_case("no")
        }
        None => false,
    }
}

/// Whether the named env var is a set boolean flag (see [`flag_is_on`]).
fn env_flag(var: &str) -> bool {
    flag_is_on(std::env::var(var).ok().as_deref())
}

/// Pure family selection from per-family reachability + the force-off overrides
/// (REQ-NET-FAMILY-GATE). No sockets — this is the unit-testable core.
///
/// - A **force-off** override is absolute: the family is never bound regardless of
///   its probe. Both forced off is an error (nothing to bind).
/// - With neither forced, a family is dropped ONLY when it probes dead AND the
///   other works. **Both-dead (or both-healthy) falls back to dual-stack** — an
///   inconclusive probe (locked-down egress, fully offline host) must not strand
///   the node by binding nothing; dual-stack is exactly today's behavior.
// [impl->REQ-NET-FAMILY-GATE]
fn select_families(
    v4_reachable: bool,
    v6_reachable: bool,
    disable_v4: bool,
    disable_v6: bool,
) -> Result<BindFamilies, NetError> {
    match (disable_v4, disable_v6) {
        (true, true) => Err(NetError::Bind(
            "both IPv4 and IPv6 force-disabled (SPT_DISABLE_IPV4 + SPT_DISABLE_IPV6) \
             — no family left to bind"
                .into(),
        )),
        // A forced family is dropped regardless of probe; the survivor is the only
        // option (so its own probe is irrelevant — honor the explicit choice).
        (true, false) => Ok(BindFamilies::V6Only),
        (false, true) => Ok(BindFamilies::V4Only),
        (false, false) => Ok(match (v4_reachable, v6_reachable) {
            (true, false) => BindFamilies::V4Only,
            (false, true) => BindFamilies::V6Only,
            // both healthy OR both inconclusive → don't strand: bind dual-stack.
            _ => BindFamilies::Dual,
        }),
    }
}

/// One family's WAN reachability: a bounded blocking TCP connect (tokio's `net`
/// feature is off in this crate, so `std::net` on a blocking thread), `true` iff
/// the handshake completes within [`FAMILY_PROBE_TIMEOUT`].
async fn probe_family(target: &str) -> bool {
    let Ok(addr) = target.parse::<std::net::SocketAddr>() else {
        return false;
    };
    tokio::task::spawn_blocking(move || {
        std::net::TcpStream::connect_timeout(&addr, FAMILY_PROBE_TIMEOUT).is_ok()
    })
    .await
    .unwrap_or(false)
}

/// Decide which families a production-scope (`BindScope::All`) endpoint binds:
/// the force-off overrides first (no probe needed — a forced choice is
/// deterministic), otherwise probe both families concurrently and drop a dead one.
/// Relay-disabled (LAN/air-gapped) binds skip the WAN probe and keep dual-stack —
/// there is no WAN to reach, so the probe would only ever read both-dead.
// [impl->REQ-NET-FAMILY-GATE]
async fn decide_bind_families(relay: &RelayPolicy) -> Result<BindFamilies, NetError> {
    let disable_v4 = env_flag("SPT_DISABLE_IPV4");
    let disable_v6 = env_flag("SPT_DISABLE_IPV6");
    if disable_v4 || disable_v6 {
        // A forced choice needs no probe.
        return select_families(true, true, disable_v4, disable_v6);
    }
    if matches!(relay, RelayPolicy::Disabled) {
        return Ok(BindFamilies::Dual);
    }
    let (v4, v6) = tokio::join!(probe_family(PROBE_V4), probe_family(PROBE_V6));
    select_families(v4, v6, false, false)
}

/// A bound spt-core network endpoint: the node's reachable presence on the subnet.
pub struct NetEndpoint {
    inner: Endpoint,
}