660	    // a newer-epoch row carries the updated blurb; a lagging blurb edit is
661	    // dropped stale; and the wire shape roundtrips with the blurb present
662	    // and parses a pre-D9-4 row (no field) clean.
663	    #[test]
664	    fn resource_blurb_rides_the_lease_and_serde() {
665	        let mut reg = SubnetRegistry::new();
666	        let with = |blurb: Option<&str>, epoch: u64| Instance {
667	            node: "n1".to_string(),
668	            status: Status::Active,
669	            epoch,
670	            resources: blurb.map(str::to_string),
671	            last_active_ms: None,
672	            shell_adapters: Vec::new(),
673	        };
674	        reg.merge_instance("ling", with(Some("rust builds"), 1));
675	        assert_eq!(
676	            reg.merge_instance("ling", with(Some("rust builds; code review"), 2)),
677	            MergeOutcome::Updated,
678	            "a blurb edit is an ordinary epoch-bumped update"
679	        );

    // a newer-epoch row carries the updated blurb; a lagging blurb edit is
    // dropped stale; and the wire shape roundtrips with the blurb present
    // and parses a pre-D9-4 row (no field) clean.
    #[test]
    fn resource_blurb_rides_the_lease_and_serde() {
        let mut reg = SubnetRegistry::new();
        let with = |blurb: Option<&str>, epoch: u64| Instance {
            node: "n1".to_string(),
            status: Status::Active,
            epoch,
            resources: blurb.map(str::to_string),
            last_active_ms: None,
            shell_adapters: Vec::new(),
        };
        reg.merge_instance("ling", with(Some("rust builds"), 1));
        assert_eq!(
            reg.merge_instance("ling", with(Some("rust builds; code review"), 2)),
            MergeOutcome::Updated,
            "a blurb edit is an ordinary epoch-bumped update"
        );


Edit

            resources: blurb.map(str::to_string),
            last_active_ms: None,
            shell_adapters: Vec::new(),
        };

            resources: blurb.map(str::to_string),
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
        };


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

            resources: blurb.map(str::to_string),
            last_active_ms: None,
            shell_adapters: Vec::new(),
        };

            resources: blurb.map(str::to_string),
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
        };

reg.merge_instance("ling", with(Some("rust builds"), 1));


            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
        };

            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
        };


            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
        };

            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
        };


                resources: Some("rust builds".to_string()),
                last_active_ms: None,
                shell_adapters: Vec::new(),
            },

                resources: Some("rust builds".to_string()),
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
            },


                resources: Some("rust builds".to_string()),
                last_active_ms: None,
                shell_adapters: Vec::new(),
            },

                resources: Some("rust builds".to_string()),
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
            },

reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb


Now the four daemon-side sites:


515	            let owlery = perch::owlery_dir();
516	            let ling = seed_perch(&owlery, "ling", STATUS_ONLINE, Some(1_000));
517
518	            let mut reg = spt_net::net::registry::SubnetRegistry::new();
519	            reg.merge_instance(
520	                "ling",
521	                spt_net::net::registry::Instance {
522	                    node: "remote-hex".into(),
523	                    status: spt_net::net::registry::Status::Active,
524	                    epoch: 1,
525	                    resources: None,
526	                    shell_adapters: Vec::new(),
527	                    last_active_ms: Some(9_000),
528	                },
529	            );
530	            let mut regs = std::collections::BTreeMap::new();
531	            regs.insert("home".to_string(), reg);
532

            let owlery = perch::owlery_dir();
            let ling = seed_perch(&owlery, "ling", STATUS_ONLINE, Some(1_000));

            let mut reg = spt_net::net::registry::SubnetRegistry::new();
            reg.merge_instance(
                "ling",
                spt_net::net::registry::Instance {
                    node: "remote-hex".into(),
                    status: spt_net::net::registry::Status::Active,
                    epoch: 1,
                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                },
            );
            let mut regs = std::collections::BTreeMap::new();
            regs.insert("home".to_string(), reg);


196	            info::set_last_active(&p, ts).unwrap();
197	        }
198	    }
199
200	    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {
201	        Instance {
202	            node: node.into(),
203	            status,
204	            epoch: 1,
205	            resources: None,
206	            last_active_ms: ts,
207	            shell_adapters: Vec::new(),
208	        }
209	    }
210
211	    fn open_policy(subnets: &[&str]) -> NotifSurfacePolicy {

            info::set_last_active(&p, ts).unwrap();
        }
    }

    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {
        Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
        }
    }

    fn open_policy(subnets: &[&str]) -> NotifSurfacePolicy {


305	        let owlery = home.join("owlery");
306	        let adapters = home.join("adapters");
307	        register(home, "local", &shell_toml("LocalBot", Some("subnet")));
308	        let registered = spt_runtime::registry::registered(&adapters);
309
310	        let inst = |node: &str, status: Status, shells: &[&str]| Instance {
311	            node: node.into(),
312	            status,
313	            epoch: 1,
314	            resources: None,
315	            last_active_ms: None,
316	            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
317	        };
318	        let mut reg = SubnetRegistry::new();
319	        // Two endpoints on the same remote node carry the same node-level
320	        // datum: dedupes to one entry.

        let owlery = home.join("owlery");
        let adapters = home.join("adapters");
        register(home, "local", &shell_toml("LocalBot", Some("subnet")));
        let registered = spt_runtime::registry::registered(&adapters);

        let inst = |node: &str, status: Status, shells: &[&str]| Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
        };
        let mut reg = SubnetRegistry::new();
        // Two endpoints on the same remote node carry the same node-level
        // datum: dedupes to one entry.


                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                },

                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                    node_label: None,
                },


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

                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                },

                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                    node_label: None,
                },

let mut regs = std::collections::BTreeMap::new();


            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
        }
    }

            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
        }
    }


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

            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
        }
    }

            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
        }
    }


            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
        };

            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
        };


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

            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
        };

            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
        };

// Two endpoints on the same remote node carry the same node-level


D2a: node_label additive gossip field + hostname detection at daemon startup

Instance.node_label (serde-default, skip-if-none; old rows parse clean). RegistryHost computes OS hostname at construction (daemon startup re-check), stamps every advertised row. Activate REQ-SUBNET-3 [impl,unit].

D2b: @node qualifier accepts label or key-prefix, refuse-on-ambiguity

resolve_visible @node arm: match full hex, hex prefix, or label (case-insensitive); >1 distinct node matched → Ambiguous with candidate key prefixes, never guess. Update wansend render.

D2c: subnet status --nodes per-node rows + hybrid liveness + label render

Per-node rows: `LABEL (keyprefix…)`, online/offline, [online endpoints/total]. Liveness = gossip-recency for table, probe only stale rows. Regen reference.


Now the hostname helper — new spt-store module:


Bash

grep -n "pub mod" crates/spt-store/src/lib.rs

Check lib.rs module list


Error: Exit code 2
grep: crates/spt-store/src/lib.rs: No such file or directory


ls crates/spt-store/src/lib.rs crates/spt-store/Cargo.toml 2>&1; grep -n "lib\]" -A3 crates/spt-store/Cargo.toml 2>/dev/null; find crates/spt-store -name "*.rs" -maxdepth 3 | head

Locate spt-store source layout


ls: cannot access 'crates/spt-store/src/lib.rs': No such file or directory
ls: cannot access 'crates/spt-store/Cargo.toml': No such file or directory
find: 'crates/spt-store': No such file or directory


Check working directory


/c/Users/decid/Documents/projects/spt-core/crates/spt-net
Cargo.toml
src


cd /c/Users/decid/Documents/projects/spt-core && grep -n "pub mod" crates/spt-store/src/lib.rs | head -30

List spt-store modules


14:pub mod access;
15:pub mod atomic;
16:pub mod branchstore;
17:pub mod contextmark;
18:pub mod contextstore;
20:pub mod epoch;
21:pub mod gitrun;
22:pub mod grants;
23:pub mod history;
24:pub mod home;
25:pub mod info;
26:pub mod liveness;
27:pub mod nodeid;
28:pub mod notif;
29:pub mod perch;
30:pub mod proc;
31:pub mod project;
32:pub mod registry;
33:pub mod rename;
34:pub mod seed;
35:pub mod shellinfo;
36:pub mod spool;
37:pub mod subnet;
38:pub mod syncmerge;
39:pub mod trust;
40:pub mod visibility;
41:pub mod xfer;


Write

//! The node's human-readable label: the OS hostname (M7 D2, REQ-SUBNET-3 —
//! CONTEXT §node label).
//!
//! The label is display + addressing sugar; the Ed25519 pubkey
//! ([`crate::nodeid`]) stays the node's identity. The daemon re-reads the
//! hostname at startup and stamps it onto every registry row it advertises
//! (a hostname change reaches peers as ordinary epoch-bumped updates) —
//! nothing is persisted here, so there is no stale copy to migrate.
// [impl->REQ-SUBNET-3]

/// This machine's OS hostname, if the OS will say. `None` (rather than a
/// placeholder) when it won't — an absent label renders as the bare key
/// prefix, never a lie.
pub fn os_hostname() -> Option<String> {
    let name = detect()?;
    let name = name.trim();
    (!name.is_empty()).then(|| name.to_string())
}

#[cfg(windows)]
fn detect() -> Option<String> {
    // Set by the OS for every process; the NetBIOS-style uppercase name
    // (e.g. `HFENDULEAM`) — exactly what views should render.
    std::env::var("COMPUTERNAME").ok()
}

#[cfg(unix)]
fn detect() -> Option<String> {
    // `gethostname(2)` — no env dependence (HOSTNAME is shell-managed and
    // absent in non-interactive contexts).
    let mut buf = [0u8; 256];
    let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
    if rc != 0 {
        return None;
    }
    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
    String::from_utf8(buf[..len].to_vec()).ok()
}

#[cfg(not(any(unix, windows)))]
fn detect() -> Option<String> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-SUBNET-3] supported platforms produce a non-empty,
    // whitespace-trimmed label (the actual value is the runner's hostname —
    // environment-dependent, so only the shape is asserted).
    #[test]
    fn hostname_is_nonempty_and_trimmed_on_supported_platforms() {
        if cfg!(any(unix, windows)) {
            let label = os_hostname().expect("supported OS yields a hostname");
            assert!(!label.is_empty());
            assert_eq!(label, label.trim());
        }
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\hostlabel.rs (file state is current in your context — no need to Read it back)

//! The node's human-readable label: the OS hostname (M7 D2, REQ-SUBNET-3 —
//! CONTEXT §node label).
//!
//! The label is display + addressing sugar; the Ed25519 pubkey
//! ([`crate::nodeid`]) stays the node's identity. The daemon re-reads the
//! hostname at startup and stamps it onto every registry row it advertises
//! (a hostname change reaches peers as ordinary epoch-bumped updates) —
//! nothing is persisted here, so there is no stale copy to migrate.
// [impl->REQ-SUBNET-3]

/// This machine's OS hostname, if the OS will say. `None` (rather than a
/// placeholder) when it won't — an absent label renders as the bare key
/// prefix, never a lie.
pub fn os_hostname() -> Option<String> {
    let name = detect()?;
    let name = name.trim();
    (!name.is_empty()).then(|| name.to_string())
}

#[cfg(windows)]
fn detect() -> Option<String> {
    // Set by the OS for every process; the NetBIOS-style uppercase name
    // (e.g. `HFENDULEAM`) — exactly what views should render.
    std::env::var("COMPUTERNAME").ok()
}

#[cfg(unix)]
fn detect() -> Option<String> {
    // `gethostname(2)` — no env dependence (HOSTNAME is shell-managed and
    // absent in non-interactive contexts).
    let mut buf = [0u8; 256];
    let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
    if rc != 0 {
        return None;
    }
    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
    String::from_utf8(buf[..len].to_vec()).ok()
}

#[cfg(not(any(unix, windows)))]
fn detect() -> Option<String> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-SUBNET-3] supported platforms produce a non-empty,
    // whitespace-trimmed label (the actual value is the runner's hostname —
    // environment-dependent, so only the shape is asserted).
    #[test]
    fn hostname_is_nonempty_and_trimmed_on_supported_platforms() {
        if cfg!(any(unix, windows)) {
            let label = os_hostname().expect("supported OS yields a hostname");
            assert!(!label.is_empty());
            assert_eq!(label, label.trim());
        }
    }
}


Bash

sed -i 's/^pub mod home;/pub mod home;\npub mod hostlabel;/' crates/spt-store/src/lib.rs && grep -n "hostlabel" crates/spt-store/src/lib.rs

Register hostlabel module


25:pub mod hostlabel;


279	                let reg = regs.entry(sub.name.clone()).or_default();
280	                match advertise_if_visible(
281	                    reg,
282	                    &vis,
283	                    &sub.name,
284	                    sub.hide_new_endpoints,
285	                    &id,
286	                    instance.clone(),
287	                ) {
288	                    Ok(Some(_)) => out.push(RegistryUpdate {
289	                        subnet: sub.name.clone(),
290	                        endpoint_id: id.clone(),
291	                        instance,
292	                    }),
293	                    Ok(None) => {} // hidden in this subnet — not advertised
294	                    Err(collision) => {
295	                        // Bare-id collision (another node owns the name in
296	                        // this subnet): loud skip — resolution is the user's
297	                        // rename (D3f), never a silent overwrite.
298	                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
299	                    }
300	                }
301	            }
302	        }
303	        drop(epoch);
304	        drop(regs);
305	        self.write_snapshots();
306	        out
307	    }
308
309	    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
310	    /// election's input; empty when unknown).
311	    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
312	        self.regs
313	            .lock()
314	            .unwrap()
315	            .get(subnet)
316	            .map(|r| r.instances(endpoint_id).to_vec())
317	            .unwrap_or_default()
318	    }
319
320	    /// A point-in-time clone of one subnet's registry (resolution input).
321	    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
322	        self.regs.lock().unwrap().get(subnet).cloned()
323	    }
324
325	    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
326	    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
327	    /// readers. Best-effort: a failed snapshot write must not fail the merge
328	    /// that triggered it (readers tolerate staleness by contract).
329	    fn write_snapshots(&self) {
330	        let regs = self.regs.lock().unwrap();
331	        if regs.is_empty() {
332	            return;
333	        }
334	        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
335	            return;
336	        }
337	        for (subnet, reg) in regs.iter() {
338	            let Ok(json) = serde_json::to_vec_pretty(reg) else { continue };
339	            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
340	        }
341	    }
342
343	    /// The snapshot path for one subnet (the CLI reader's contract).
344	    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
345	        dir.join(format!("{subnet}.json"))
346	    }
347	}
348
349	/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
350	fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
351	    let tmp = path.with_extension("json.tmp");
352	    std::fs::write(&tmp, bytes)?;
353	    for attempt in 0..5 {
354	        match std::fs::rename(&tmp, path) {
355	            Ok(()) => return Ok(()),
356	            Err(_) if attempt < 4 => {
357	                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
358	            }
359	            Err(e) => return Err(e),
360	        }
361	    }
362	    unreachable!("loop returns on every path")
363	}
364
365	#[cfg(test)]
366	mod tests {
367	    use super::*;
368	    use crate::test_home::with_home;
369	    use spt_store::info::{self, InfoJson};
370
371	    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
372	        RegistryUpdate {
373	            subnet: subnet.to_string(),
374	            endpoint_id: id.to_string(),
375	            instance: Instance { node: node.to_string(), status: Status::Active, epoch, resources: None, last_active_ms: None, shell_adapters: Vec::new(), node_label: None },
376	        }
377	    }
378
379	    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
380	        let mut trust = TrustStore::default();
381	        trust.record(subnet, trusted, None, None);
382	        RegistryGatePolicy { subnets: vec![subnet.to_string()], trust }
383	    }
384
385	    fn host(dir: &Path) -> RegistryHost {
386	        RegistryHost::new_at(
387	            "aa11",
388	            EpochSource::load_from(&dir.join("epoch.json")),
389	            dir.join("registry"),
390	        )
391	    }
392
393	    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
394	    // and untrusted origin write nothing; an admitted row merges and the
395	    // epoch lease still drops a stale follow-up.
396	    #[test]
397	    fn apply_feed_gates_and_merges_under_the_lease() {
398	        let dir = tempfile::tempdir().unwrap();
399	        let h = host(dir.path());
400	        let p = policy("home", "bb22");
401
402	        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
403	        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
404	        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
405	        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
406	        assert!(h.rows("home", "ling").is_empty(), "refused feeds write nothing");
407
408	        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);

                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryUpdate {
                        subnet: sub.name.clone(),
                        endpoint_id: id.clone(),
                        instance,
                    }),
                    Ok(None) => {} // hidden in this subnet — not advertised
                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
                    }
                }
            }
        }
        drop(epoch);
        drop(regs);
        self.write_snapshots();
        out
    }

    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
    /// election's input; empty when unknown).
    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .get(subnet)
            .map(|r| r.instances(endpoint_id).to_vec())
            .unwrap_or_default()
    }

    /// A point-in-time clone of one subnet's registry (resolution input).
    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
        self.regs.lock().unwrap().get(subnet).cloned()
    }

    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
    /// readers. Best-effort: a failed snapshot write must not fail the merge
    /// that triggered it (readers tolerate staleness by contract).
    fn write_snapshots(&self) {
        let regs = self.regs.lock().unwrap();
        if regs.is_empty() {
            return;
        }
        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
            return;
        }
        for (subnet, reg) in regs.iter() {
            let Ok(json) = serde_json::to_vec_pretty(reg) else { continue };
            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
        }
    }

    /// The snapshot path for one subnet (the CLI reader's contract).
    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
        dir.join(format!("{subnet}.json"))
    }
}

/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, bytes)?;
    for attempt in 0..5 {
        match std::fs::rename(&tmp, path) {
            Ok(()) => return Ok(()),
            Err(_) if attempt < 4 => {
                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
            }
            Err(e) => return Err(e),
        }
    }
    unreachable!("loop returns on every path")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::info::{self, InfoJson};

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance { node: node.to_string(), status: Status::Active, epoch, resources: None, last_active_ms: None, shell_adapters: Vec::new(), node_label: None },
        }
    }

    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
        let mut trust = TrustStore::default();
        trust.record(subnet, trusted, None, None);
        RegistryGatePolicy { subnets: vec![subnet.to_string()], trust }
    }

    fn host(dir: &Path) -> RegistryHost {
        RegistryHost::new_at(
            "aa11",
            EpochSource::load_from(&dir.join("epoch.json")),
            dir.join("registry"),
        )
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path());
        let p = policy("home", "bb22");

        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
        assert!(h.rows("home", "ling").is_empty(), "refused feeds write nothing");

        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);


Edit

pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,

pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,
    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every
    /// advertised row — a hostname change reaches peers as ordinary
    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the
    /// identity.
    node_label: Option<String>,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,


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)

pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,

pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,
    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every
    /// advertised row — a hostname change reaches peers as ordinary
    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the
    /// identity.
    node_label: Option<String>,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,

+    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,

+    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every

+    /// advertised row — a hostname change reaches peers as ordinary

+    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the

/// The node's one durable monotonic counter (the D3b lease source) — the

/// epoch on every locally-authored instance row.


    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home.
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home. Reads the OS hostname here — daemon
    /// startup IS the label's re-check point (REQ-SUBNET-3).
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
        .with_node_label(spt_store::hostlabel::os_hostname())
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir). No label by default — tests
    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            node_label: None,
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// Set the label this host stamps onto every advertised row.
    // [impl->REQ-SUBNET-3]
    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
        self.node_label = label;
        self
    }


    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home.
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home. Reads the OS hostname here — daemon
    /// startup IS the label's re-check point (REQ-SUBNET-3).
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
        .with_node_label(spt_store::hostlabel::os_hostname())
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir). No label by default — tests
    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            node_label: None,
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// Set the label this host stamps onto every advertised row.
    // [impl->REQ-SUBNET-3]
    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
        self.node_label = label;
        self
    }

-    /// snapshot dir under the spt home.

+    /// snapshot dir under the spt home. Reads the OS hostname here — daemon

+    /// startup IS the label's re-check point (REQ-SUBNET-3).

+        .with_node_label(spt_store::hostlabel::os_hostname())

-    /// "node" its own counter + snapshot dir).

+    /// "node" its own counter + snapshot dir). No label by default — tests

+    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).

+    /// Set the label this host stamps onto every advertised row.

+    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {


282	            // epoch lease, same visibility gate, no new channel. Subnet-wide
283	            // most-recently-active is a projection of these rows (D6b).
284	            // [impl->REQ-PRES-1]
285	            let last_active_ms = info.and_then(|i| i.last_active_ms);
286	            for sub in &subnets.subnets {
287	                let Ok(ep) = epoch.next_epoch() else { continue };
288	                let instance = Instance {
289	                    node: self.node_hex.clone(),
290	                    status,
291	                    epoch: ep,
292	                    resources: resources.clone(),
293	                    last_active_ms,
294	                    shell_adapters: shell_adapters.clone(),
295	                };
296	                let reg = regs.entry(sub.name.clone()).or_default();
297	                match advertise_if_visible(
298	                    reg,
299	                    &vis,
300	                    &sub.name,
301	                    sub.hide_new_endpoints,
302	                    &id,
303	                    instance.clone(),
304	                ) {
305	                    Ok(Some(_)) => out.push(RegistryUpdate {
306	                        subnet: sub.name.clone(),

            // epoch lease, same visibility gate, no new channel. Subnet-wide
            // most-recently-active is a projection of these rows (D6b).
            // [impl->REQ-PRES-1]
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                };
                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryUpdate {
                        subnet: sub.name.clone(),


                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                };

                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                    // The node label rides every row this node authors
                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
                    // startup, so a rename converges by re-advertisement.
                    // [impl->REQ-SUBNET-3]
                    node_label: self.node_label.clone(),
                };


                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                };

                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                    // The node label rides every row this node authors
                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
                    // startup, so a rename converges by re-advertisement.
                    // [impl->REQ-SUBNET-3]
                    node_label: self.node_label.clone(),
                };

+                    // The node label rides every row this node authors

+                    // (REQ-SUBNET-3) — set from the OS hostname at daemon

+                    // startup, so a rename converges by re-advertisement.

+                    // [impl->REQ-SUBNET-3]

+                    node_label: self.node_label.clone(),

let reg = regs.entry(sub.name.clone()).or_default();


Bash

cargo check --workspace --all-targets 2>&1 | grep -A2 "E0063\|^error" | grep "\-\->" | sort -u; cargo check --workspace --all-targets 2>&1 | grep -c "^error"

Recheck for remaining missing-field sites


    --> crates\spt\src\cli.rs:4066:13
    --> crates\spt\src\cli.rs:4197:17
   --> crates\spt-daemon\tests\dispatch.rs:557:9
5


4190
4191	        // A member subnet with gossip: counts come from the snapshot.
4192	        let store = store_with(&["home"]);
4193	        let mut reg = SubnetRegistry::new();
4194	        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
4195	            reg.merge_instance(
4196	                id,
4197	                Instance {
4198	                    node: node.into(),
4199	                    status: Status::Active,
4200	                    epoch: 1,
4201	                    resources: None,
4202	                    last_active_ms: None,
4203	                    shell_adapters: Vec::new(),
4204	                },
4205	            );
4206	        }
4207	        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
4208	        let rows = subnet_status_rows(&store, &regs, None);
4209	        assert_eq!(rows.len(), 1);

        // A member subnet with gossip: counts come from the snapshot.
        let store = store_with(&["home"]);
        let mut reg = SubnetRegistry::new();
        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
            reg.merge_instance(
                id,
                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                },
            );
        }
        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let rows = subnet_status_rows(&store, &regs, None);
        assert_eq!(rows.len(), 1);


552	    .unwrap();
553	    info::set_last_active(&perch_path, 1_000).unwrap();
554	    let mut reg = spt_net::net::registry::SubnetRegistry::new();
555	    reg.merge_instance(
556	        target,
557	        spt_net::net::registry::Instance {
558	            node: b_hex.clone(),
559	            status: spt_net::net::registry::Status::Active,
560	            epoch: 1,
561	            resources: None,
562	            last_active_ms: Some(9_000),
563	            shell_adapters: Vec::new(),
564	        },
565	    );
566	    let snap_dir = spt_daemon::presence::registry_snapshot_dir();
567	    std::fs::create_dir_all(&snap_dir).unwrap();
568	    std::fs::write(snap_dir.join("d6-pres.json"), serde_json::to_string(&reg).unwrap()).unwrap();
569

    .unwrap();
    info::set_last_active(&perch_path, 1_000).unwrap();
    let mut reg = spt_net::net::registry::SubnetRegistry::new();
    reg.merge_instance(
        target,
        spt_net::net::registry::Instance {
            node: b_hex.clone(),
            status: spt_net::net::registry::Status::Active,
            epoch: 1,
            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
        },
    );
    let snap_dir = spt_daemon::presence::registry_snapshot_dir();
    std::fs::create_dir_all(&snap_dir).unwrap();
    std::fs::write(snap_dir.join("d6-pres.json"), serde_json::to_string(&reg).unwrap()).unwrap();


Edit

                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                },

                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                },


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

                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                },

                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                },


            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
        },

            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
        },


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

            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
        },

            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
        },

let snap_dir = spt_daemon::presence::registry_snapshot_dir();


4058	        assert_eq!(cmd_fork("ling", "other", "ghost", false), 1, "non-member target");
4059	        assert_eq!(cmd_fork("ghost", "other", "work", false), 1, "missing source (M5 arm)");
4060	        // Target-registry collision: ANY holder (even a resting one) owns it.
4061	        let snap_dir = perch::identity_dir().join("registry");
4062	        std::fs::create_dir_all(&snap_dir).unwrap();
4063	        let mut reg = spt_net::net::registry::SubnetRegistry::new();
4064	        reg.merge_instance(
4065	            "claimed",
4066	            spt_net::net::registry::Instance {
4067	                node: "bb22".into(),
4068	                status: spt_net::net::registry::Status::Dormant,
4069	                epoch: 1,
4070	                resources: None,
4071	                last_active_ms: None,
4072	                shell_adapters: Vec::new(),
4073	            },
4074	        );
4075	        std::fs::write(snap_dir.join("work.json"), serde_json::to_string(&reg).unwrap())
4076	            .unwrap();
4077	        assert_eq!(cmd_fork("ling", "claimed", "work", false), 1, "target holder owns the name");

        assert_eq!(cmd_fork("ling", "other", "ghost", false), 1, "non-member target");
        assert_eq!(cmd_fork("ghost", "other", "work", false), 1, "missing source (M5 arm)");
        // Target-registry collision: ANY holder (even a resting one) owns it.
        let snap_dir = perch::identity_dir().join("registry");
        std::fs::create_dir_all(&snap_dir).unwrap();
        let mut reg = spt_net::net::registry::SubnetRegistry::new();
        reg.merge_instance(
            "claimed",
            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
            },
        );
        std::fs::write(snap_dir.join("work.json"), serde_json::to_string(&reg).unwrap())
            .unwrap();
        assert_eq!(cmd_fork("ling", "claimed", "work", false), 1, "target holder owns the name");


            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
            },

            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
            },


            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
            },

            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
            },


Bash

cargo check --workspace --all-targets 2>&1 | tail -3

Verify workspace compiles


    Checking spt-daemon v0.1.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.1.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.56s


D2a tests — label-rides-lease serde test + advertise stamps:


Edit

// [unit->REQ-PRES-1] the presence datum is a FIELD under the lease, not a

    // [unit->REQ-SUBNET-3] the node label is a FIELD under the lease — a
    // hostname change converges by ordinary epoch-bumped re-advertisement, a
    // lagging row never rolls it back; the wire shape roundtrips with the
    // label and a pre-M7 row (no field) parses clean (the additive-field
    // discipline the resources blurb proved).
    #[test]
    fn node_label_rides_the_lease_and_serde() {
        let mut reg = SubnetRegistry::new();
        let with = |label: Option<&str>, epoch: u64| Instance {
            node: "n1".to_string(),
            status: Status::Active,
            epoch,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
        };
        reg.merge_instance("ling", with(Some("OLDHOST"), 1));
        assert_eq!(
            reg.merge_instance("ling", with(Some("NEWHOST"), 2)),
            MergeOutcome::Updated,
            "a hostname change is an ordinary epoch-bumped update"
        );
        assert_eq!(reg.instances("ling")[0].node_label.as_deref(), Some("NEWHOST"));
        assert_eq!(reg.merge_instance("ling", with(Some("OLDHOST"), 1)), MergeOutcome::Stale);
        assert_eq!(reg.instances("ling")[0].node_label.as_deref(), Some("NEWHOST"));

        // Serde: roundtrips with the label; a pre-M7 row parses clean.
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
    }

    // [unit->REQ-PRES-1] the presence datum is a FIELD under the lease, not a


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

    // [unit->REQ-SUBNET-3] the node label is a FIELD under the lease — a
    // hostname change converges by ordinary epoch-bumped re-advertisement, a
    // lagging row never rolls it back; the wire shape roundtrips with the
    // label and a pre-M7 row (no field) parses clean (the additive-field
    // discipline the resources blurb proved).
    #[test]
    fn node_label_rides_the_lease_and_serde() {
        let mut reg = SubnetRegistry::new();
        let with = |label: Option<&str>, epoch: u64| Instance {
            node: "n1".to_string(),
            status: Status::Active,
            epoch,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
        };
        reg.merge_instance("ling", with(Some("OLDHOST"), 1));
        assert_eq!(
            reg.merge_instance("ling", with(Some("NEWHOST"), 2)),
            MergeOutcome::Updated,
            "a hostname change is an ordinary epoch-bumped update"
        );
        assert_eq!(reg.instances("ling")[0].node_label.as_deref(), Some("NEWHOST"));
        assert_eq!(reg.merge_instance("ling", with(Some("OLDHOST"), 1)), MergeOutcome::Stale);
        assert_eq!(reg.instances("ling")[0].node_label.as_deref(), Some("NEWHOST"));

        // Serde: roundtrips with the label; a pre-M7 row parses clean.
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
    }

    // [unit->REQ-PRES-1] the presence datum is a FIELD under the lease, not a

assert_eq!(old.resources, None, "absent field parses as no blurb");

+    // [unit->REQ-SUBNET-3] the node label is a FIELD under the lease — a

+    // hostname change converges by ordinary epoch-bumped re-advertisement, a

+    // lagging row never rolls it back; the wire shape roundtrips with the

+    // label and a pre-M7 row (no field) parses clean (the additive-field

+    // discipline the resources blurb proved).

+    fn node_label_rides_the_lease_and_serde() {

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

+        let with = |label: Option<&str>, epoch: u64| Instance {

+            shell_adapters: Vec::new(),

+            node_label: label.map(str::to_string),

+        reg.merge_instance("ling", with(Some("OLDHOST"), 1));

+            reg.merge_instance("ling", with(Some("NEWHOST"), 2)),

+            "a hostname change is an ordinary epoch-bumped update"

+        assert_eq!(reg.instances("ling")[0].node_label.as_deref(), Some("NEWHOST"));

+        assert_eq!(reg.merge_instance("ling", with(Some("OLDHOST"), 1)), MergeOutcome::Stale);

+        // Serde: roundtrips with the label; a pre-M7 row parses clean.

+        let json = serde_json::to_string(&reg).unwrap();

+        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();

+            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();

+        assert_eq!(old.node_label, None, "absent field parses as no label");

// feed: a newer-epoch row carries the fresher stamp; a lagging row drops

// stale (a partition can never roll recency backward through the merge);


14://! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the
215:    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
466:            h.advertise_local(&owlery);
481:            h.advertise_local(&owlery);
510:            h.advertise_local(&owlery);
525:            h.advertise_local(&owlery);
599:            h.advertise_local(&owlery);
606:            h.advertise_local(&owlery);
613:            h.advertise_local(&owlery);
620:            h.advertise_local(&owlery);
625:    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
628:    fn advertise_local_scans_and_respects_visibility() {
645:            let out = h.advertise_local(&owlery);
659:            h.advertise_local(&owlery);


625	    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
626	    // epochs, respects visibility, and returns only this node's rows.
627	    #[test]
628	    fn advertise_local_scans_and_respects_visibility() {
629	        with_home(|home| {
630	            let owlery = home.join("owlery");
631	            for id in ["vis-ag", "hid-ag"] {
632	                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
633	                std::fs::create_dir_all(&p).unwrap();
634	                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
635	                info::write_info(&p, &rec).unwrap();
636	            }
637	            let mut subnets = SubnetStore::load();
638	            subnets.create_subnet("adv").unwrap();
639	            subnets.save().unwrap();
640	            let mut vis = VisibilityStore::load();
641	            vis.set_override("hid-ag", "adv", Some(true)); // hidden ⇒ excluded
642	            vis.save().unwrap();
643
644	            let h = host(home);
645	            let out = h.advertise_local(&owlery);
646	            assert!(
647	                out.iter().any(|u| u.endpoint_id == "vis-ag" && u.subnet == "adv"),
648	                "visible perch advertised: {out:?}"
649	            );
650	            assert!(
651	                !out.iter().any(|u| u.endpoint_id == "hid-ag"),
652	                "hidden perch is EXCLUDED (ADR-0006 §6), not merely unlisted"
653	            );
654	            assert!(!h.rows("adv", "vis-ag").is_empty(), "own registry holds the row");
655	            assert!(h.rows("adv", "hid-ag").is_empty());
656
657	            // Re-advertising bumps the epoch — the lease orders restarts.
658	            let e1 = h.rows("adv", "vis-ag")[0].epoch;
659	            h.advertise_local(&owlery);
660	            let e2 = h.rows("adv", "vis-ag")[0].epoch;
661	            assert!(e2 > e1, "fresh epoch per advertisement ({e1} -> {e2})");
662
663	            // The snapshot mirror exists and parses back.
664	            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
665	            let bytes = std::fs::read(&snap).expect("snapshot written");
666	            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
667	            assert!(!parsed.instances("vis-ag").is_empty());
668	        });
669	    }
670	}
671

    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.
    #[test]
    fn advertise_local_scans_and_respects_visibility() {
        with_home(|home| {
            let owlery = home.join("owlery");
            for id in ["vis-ag", "hid-ag"] {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
                info::write_info(&p, &rec).unwrap();
            }
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("adv").unwrap();
            subnets.save().unwrap();
            let mut vis = VisibilityStore::load();
            vis.set_override("hid-ag", "adv", Some(true)); // hidden ⇒ excluded
            vis.save().unwrap();

            let h = host(home);
            let out = h.advertise_local(&owlery);
            assert!(
                out.iter().any(|u| u.endpoint_id == "vis-ag" && u.subnet == "adv"),
                "visible perch advertised: {out:?}"
            );
            assert!(
                !out.iter().any(|u| u.endpoint_id == "hid-ag"),
                "hidden perch is EXCLUDED (ADR-0006 §6), not merely unlisted"
            );
            assert!(!h.rows("adv", "vis-ag").is_empty(), "own registry holds the row");
            assert!(h.rows("adv", "hid-ag").is_empty());

            // Re-advertising bumps the epoch — the lease orders restarts.
            let e1 = h.rows("adv", "vis-ag")[0].epoch;
            h.advertise_local(&owlery);
            let e2 = h.rows("adv", "vis-ag")[0].epoch;
            assert!(e2 > e1, "fresh epoch per advertisement ({e1} -> {e2})");

            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }
}


            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }
}

            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }

    // [unit->REQ-SUBNET-3] the host stamps its node label onto every row it
    // advertises (and only its own rows — peers' labels arrive on their own
    // advertisements); an unlabeled host (with_node_label(None), the test
    // default) advertises label-less rows, never a placeholder.
    #[test]
    fn advertise_local_stamps_the_node_label() {
        with_home(|home| {
            let owlery = home.join("owlery");
            let p = perch::resolve_perch_path_in(&owlery, "lab-ag", ParentHint::Infer);
            std::fs::create_dir_all(&p).unwrap();
            let rec = InfoJson::new("lab-ag", "now", std::process::id(), "s", "ready_agent");
            info::write_info(&p, &rec).unwrap();
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("lab").unwrap();
            subnets.save().unwrap();

            let h = host(home).with_node_label(Some("MYHOST".into()));
            let out = h.advertise_local(&owlery);
            let row = out.iter().find(|u| u.endpoint_id == "lab-ag").expect("advertised");
            assert_eq!(row.instance.node_label.as_deref(), Some("MYHOST"));
            assert_eq!(h.rows("lab", "lab-ag")[0].node_label.as_deref(), Some("MYHOST"));

            let bare = host(home);
            let out = bare.advertise_local(&owlery);
            let row = out.iter().find(|u| u.endpoint_id == "lab-ag").expect("advertised");
            assert_eq!(row.instance.node_label, None, "no label ⇒ none advertised");
        });
    }
}


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 snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }
}

            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }

    // [unit->REQ-SUBNET-3] the host stamps its node label onto every row it
    // advertises (and only its own rows — peers' labels arrive on their own
    // advertisements); an unlabeled host (with_node_label(None), the test
    // default) advertises label-less rows, never a placeholder.
    #[test]
    fn advertise_local_stamps_the_node_label() {
        with_home(|home| {
            let owlery = home.join("owlery");
            let p = perch::resolve_perch_path_in(&owlery, "lab-ag", ParentHint::Infer);
            std::fs::create_dir_all(&p).unwrap();
            let rec = InfoJson::new("lab-ag", "now", std::process::id(), "s", "ready_agent");
            info::write_info(&p, &rec).unwrap();
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("lab").unwrap();
            subnets.save().unwrap();

            let h = host(home).with_node_label(Some("MYHOST".into()));
            let out = h.advertise_local(&owlery);
            let row = out.iter().find(|u| u.endpoint_id == "lab-ag").expect("advertised");
            assert_eq!(row.instance.node_label.as_deref(), Some("MYHOST"));
            assert_eq!(h.rows("lab", "lab-ag")[0].node_label.as_deref(), Some("MYHOST"));

            let bare = host(home);
            let out = bare.advertise_local(&owlery);
            let row = out.iter().find(|u| u.endpoint_id == "lab-ag").expect("advertised");
            assert_eq!(row.instance.node_label, None, "no label ⇒ none advertised");
        });
    }
}

+    // [unit->REQ-SUBNET-3] the host stamps its node label onto every row it

+    // advertises (and only its own rows — peers' labels arrive on their own

+    // advertisements); an unlabeled host (with_node_label(None), the test

+    // default) advertises label-less rows, never a placeholder.

+    fn advertise_local_stamps_the_node_label() {

+            let owlery = home.join("owlery");

+            let p = perch::resolve_perch_path_in(&owlery, "lab-ag", ParentHint::Infer);

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

+            let rec = InfoJson::new("lab-ag", "now", std::process::id(), "s", "ready_agent");

+            info::write_info(&p, &rec).unwrap();

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

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

+            let h = host(home).with_node_label(Some("MYHOST".into()));

+            let out = h.advertise_local(&owlery);

+            let row = out.iter().find(|u| u.endpoint_id == "lab-ag").expect("advertised");

+            assert_eq!(row.instance.node_label.as_deref(), Some("MYHOST"));

+            assert_eq!(h.rows("lab", "lab-ag")[0].node_label.as_deref(), Some("MYHOST"));

+            let out = bare.advertise_local(&owlery);

+            assert_eq!(row.instance.node_label, None, "no label ⇒ none advertised");


Activate REQ-SUBNET-3 + run the two crates' tests:


title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = []  # M7 D2 (plan decision 7).

title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7).


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 = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = []  # M7 D2 (plan decision 7).

title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"
required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7).

-required_stages = []  # M7 D2 (plan decision 7). Label defaults to OS hostname, re-checked at daemon startup (hostname change updates it), rides existing registry gossip as an additive serde-default field (old rows parse clean). Pubkey stays the identity; @node qualifiers accept label OR key-prefix; non-unique label resolution refuses-and-qualifies listing candidate key prefixes (CONTEXT.md §node label). Renders `HFENDULEAM (bcead52b…)` in subnet views.

+required_stages = ["impl", "unit"]  # activated M7 D2 (2026-06-05): label field + hostname stamp + addressable qualifiers land. M7 D2 (plan decision 7). Label defaults to OS hostname, re-checked at daemon startup (hostname change updates it), rides existing registry gossip as an additive serde-default field (old rows parse clean). Pubkey stays the identity; @node qualifiers accept label OR key-prefix; non-unique label resolution refuses-and-qualifies listing candidate key prefixes (CONTEXT.md §node label). Renders `HFENDULEAM (bcead52b…)` in subnet views.

title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"


Bash

cargo test -p spt-net node_label 2>&1 | grep -E "test result|node_label" && cargo test -p spt-daemon --lib advertise_local 2>&1 | grep -E "test result|advertise"

Run new D2a tests


test net::registry::tests::node_label_rides_the_lease_and_serde ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.00s
test registryhost::tests::advertise_local_scans_and_respects_visibility ... ok
test registryhost::tests::advertise_local_stamps_the_node_label ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 174 filtered out; finished in 0.04s


Updated task #6 status


Updated task #7 status


D2b — first find all `Ambiguity` match sites:


crates\spt-net\src\net\wanmsg.rs:157:        let amb = Resolution::Ambiguous(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]));
crates\spt-net\src\net\wanmsg.rs:160:            WanRoute::Refused(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]))
crates\spt-net\src\net\registry.rs:439:    AcrossNodes(Vec<String>),
crates\spt-net\src\net\registry.rs:442:    AcrossSubnets(Vec<String>),
crates\spt-net\src\net\registry.rs:494:            many => Resolution::Ambiguous(Ambiguity::AcrossNodes(
crates\spt-net\src\net\registry.rs:504:/// subnets hit → refuse with [`AcrossSubnets`](Ambiguity::AcrossSubnets) (force
crates\spt-net\src\net\registry.rs:557:        _ => Resolution::Ambiguous(Ambiguity::AcrossSubnets(
crates\spt-net\src\net\registry.rs:840:            Resolution::Ambiguous(Ambiguity::AcrossNodes(nodes)) => {
crates\spt-net\src\net\registry.rs:843:            other => panic!("expected AcrossNodes refusal, got {other:?}"),
crates\spt-net\src\net\registry.rs:884:            Resolution::Ambiguous(Ambiguity::AcrossSubnets(subs)) => {
crates\spt-net\src\net\registry.rs:887:            other => panic!("expected AcrossSubnets refusal, got {other:?}"),
crates\spt-net\src\net\registry.rs:963:            Resolution::Ambiguous(Ambiguity::AcrossSubnets(_))
crates\spt-net\src\net\registry.rs:1046:    // AcrossSubnets refusal); hidden everywhere is NotFound.


426:    /// Ambiguous — resolution refuses and forces the user to qualify (ADR-0006 §2: no
428:    Ambiguous(Ambiguity),
494:            many => Resolution::Ambiguous(Ambiguity::AcrossNodes(
549:            ambiguous @ Resolution::Ambiguous(_) => return ambiguous,
557:        _ => Resolution::Ambiguous(Ambiguity::AcrossSubnets(
840:            Resolution::Ambiguous(Ambiguity::AcrossNodes(nodes)) => {
884:            Resolution::Ambiguous(Ambiguity::AcrossSubnets(subs)) => {
963:            Resolution::Ambiguous(Ambiguity::AcrossSubnets(_))


41:    Ambiguous(String),
103:        Resolution::Ambiguous(a) => return WanSendOutcome::Ambiguous(format!("{a:?}")),
149:    Ambiguous(String),
215:        Resolution::Ambiguous(a) => return WanRestOutcome::Ambiguous(format!("{a:?}")),
255:    Ambiguous(String),
328:        Resolution::Ambiguous(a) => return WanShellLinkOutcome::Ambiguous(format!("{a:?}")),
443:    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an
488:        // Ambiguity refuses before any wire work (two subnets hold the id).
495:        assert!(matches!(out, WanSendOutcome::Ambiguous(_)), "got {out:?}");


Add the variant + matcher in registry.rs:


420	/// The outcome of resolving a bare/qualified [`Address`] against the registry.
421	#[derive(Debug, Clone, PartialEq, Eq)]
422	pub enum Resolution {
423	    /// A single live instance was selected. `subnet` is set when resolution crossed a
424	    /// per-subnet registry (multi-subnet form), `None` for single-subnet resolution.
425	    Resolved { subnet: Option<String>, instance: Instance },
426	    /// Ambiguous — resolution refuses and forces the user to qualify (ADR-0006 §2: no
427	    /// guessing). The payload says which qualifier disambiguates.
428	    Ambiguous(Ambiguity),
429	    /// No live (routable) instance matched.
430	    NotFound,
431	}
432
433	/// Why resolution refused, and how to qualify past it.
434	#[derive(Debug, Clone, PartialEq, Eq)]
435	pub enum Ambiguity {
436	    /// Several live nodes hold the id in one subnet — qualify with `id@node`. (Epochs are
437	    /// per-node monotonic and NOT comparable across nodes, so we never silently pick a
438	    /// "most-recently-active" winner here — that would be a guess.) Lists the nodes.
439	    AcrossNodes(Vec<String>),
440	    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
441	    /// subnets.
442	    AcrossSubnets(Vec<String>),
443	}
444
445	impl SubnetRegistry {
446	    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
447	    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
448	    /// `@node` exact → an instance on `local_node` (local wins) → the sole remaining live
449	    /// instance → refuse if several live nodes remain (force `@node`). `Offline`
450	    /// instances are never routed to.
451	    // [impl->REQ-INST-10]
452	    pub fn resolve(&self, address: &Address, local_node: &str) -> Resolution {
453	        self.resolve_visible(address, local_node, |_| false)
454	    }
455
456	    /// [`resolve`](Self::resolve) honoring **visibility** (D3e, ADR-0006 §6): when
457	    /// `excluded(id)` says the endpoint is hidden in this subnet, it is **not
458	    /// routable** — [`NotFound`](Resolution::NotFound) even for an explicit `@node`
459	    /// pin (excluded is a real boundary, not a listing cosmetic). Callers build the
460	    /// closure from [`spt_store::visibility::VisibilityStore::hidden`] with this
461	    /// subnet's `hide_new_endpoints` flag.
462	    // [impl->REQ-INST-12]
463	    pub fn resolve_visible(
464	        &self,
465	        address: &Address,
466	        local_node: &str,
467	        excluded: impl Fn(&str) -> bool,
468	    ) -> Resolution {
469	        if excluded(&address.id) {
470	            return Resolution::NotFound;
471	        }
472	        let live: Vec<&Instance> = self
473	            .instances(&address.id)
474	            .iter()
475	            .filter(|i| i.status.routable())
476	            .collect();
477
478	        // Explicit `@node` pins one instance (must be live).
479	        if let Some(node) = &address.node {
480	            return match live.iter().find(|i| &i.node == node) {
481	                Some(i) => Resolution::Resolved { subnet: None, instance: (*i).clone() },
482	                None => Resolution::NotFound,
483	            };
484	        }
485
486	        // Local instance wins outright (no network hop, unambiguous).
487	        if let Some(i) = live.iter().find(|i| i.node == local_node) {
488	            return Resolution::Resolved { subnet: None, instance: (*i).clone() };
489	        }
490
491	        match live.as_slice() {
492	            [] => Resolution::NotFound,
493	            [only] => Resolution::Resolved { subnet: None, instance: (*only).clone() },
494	            many => Resolution::Ambiguous(Ambiguity::AcrossNodes(
495	                many.iter().map(|i| i.node.clone()).collect(),
496	            )),
497	        }
498	    }
499	}
500
501	/// Resolve an [`Address`] across the node's visible per-subnet registries. A `subnet:`
502	/// qualifier routes straight to that subnet. Otherwise every subnet is tried and: zero
503	/// hits → [`NotFound`](Resolution::NotFound); one subnet hits → that resolution; several
504	/// subnets hit → refuse with [`AcrossSubnets`](Ambiguity::AcrossSubnets) (force

/// The outcome of resolving a bare/qualified [`Address`] against the registry.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
    /// A single live instance was selected. `subnet` is set when resolution crossed a
    /// per-subnet registry (multi-subnet form), `None` for single-subnet resolution.
    Resolved { subnet: Option<String>, instance: Instance },
    /// Ambiguous — resolution refuses and forces the user to qualify (ADR-0006 §2: no
    /// guessing). The payload says which qualifier disambiguates.
    Ambiguous(Ambiguity),
    /// No live (routable) instance matched.
    NotFound,
}

/// Why resolution refused, and how to qualify past it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ambiguity {
    /// Several live nodes hold the id in one subnet — qualify with `id@node`. (Epochs are
    /// per-node monotonic and NOT comparable across nodes, so we never silently pick a
    /// "most-recently-active" winner here — that would be a guess.) Lists the nodes.
    AcrossNodes(Vec<String>),
    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
    /// subnets.
    AcrossSubnets(Vec<String>),
}

impl SubnetRegistry {
    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
    /// `@node` exact → an instance on `local_node` (local wins) → the sole remaining live
    /// instance → refuse if several live nodes remain (force `@node`). `Offline`
    /// instances are never routed to.
    // [impl->REQ-INST-10]
    pub fn resolve(&self, address: &Address, local_node: &str) -> Resolution {
        self.resolve_visible(address, local_node, |_| false)
    }

    /// [`resolve`](Self::resolve) honoring **visibility** (D3e, ADR-0006 §6): when
    /// `excluded(id)` says the endpoint is hidden in this subnet, it is **not
    /// routable** — [`NotFound`](Resolution::NotFound) even for an explicit `@node`
    /// pin (excluded is a real boundary, not a listing cosmetic). Callers build the
    /// closure from [`spt_store::visibility::VisibilityStore::hidden`] with this
    /// subnet's `hide_new_endpoints` flag.
    // [impl->REQ-INST-12]
    pub fn resolve_visible(
        &self,
        address: &Address,
        local_node: &str,
        excluded: impl Fn(&str) -> bool,
    ) -> Resolution {
        if excluded(&address.id) {
            return Resolution::NotFound;
        }
        let live: Vec<&Instance> = self
            .instances(&address.id)
            .iter()
            .filter(|i| i.status.routable())
            .collect();

        // Explicit `@node` pins one instance (must be live).
        if let Some(node) = &address.node {
            return match live.iter().find(|i| &i.node == node) {
                Some(i) => Resolution::Resolved { subnet: None, instance: (*i).clone() },
                None => Resolution::NotFound,
            };
        }

        // Local instance wins outright (no network hop, unambiguous).
        if let Some(i) = live.iter().find(|i| i.node == local_node) {
            return Resolution::Resolved { subnet: None, instance: (*i).clone() };
        }

        match live.as_slice() {
            [] => Resolution::NotFound,
            [only] => Resolution::Resolved { subnet: None, instance: (*only).clone() },
            many => Resolution::Ambiguous(Ambiguity::AcrossNodes(
                many.iter().map(|i| i.node.clone()).collect(),
            )),
        }
    }
}

/// Resolve an [`Address`] across the node's visible per-subnet registries. A `subnet:`
/// qualifier routes straight to that subnet. Otherwise every subnet is tried and: zero
/// hits → [`NotFound`](Resolution::NotFound); one subnet hits → that resolution; several
/// subnets hit → refuse with [`AcrossSubnets`](Ambiguity::AcrossSubnets) (force


Edit

    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
    /// subnets.
    AcrossSubnets(Vec<String>),
}

    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
    /// subnets.
    AcrossSubnets(Vec<String>),
    /// The `@node` qualifier itself matched SEVERAL distinct live nodes (M7
    /// D2, REQ-SUBNET-3): a shared label, or a too-short key-prefix.
    /// Refuse-and-qualify — `candidates` lists paste-able key prefixes (8
    /// hex chars each), never a guess between machines sharing a hostname.
    NodeQualifier { qualifier: String, candidates: Vec<String> },
}

/// Does an `@node` qualifier name this instance's node (M7 D2, REQ-SUBNET-3)?
/// Three spellings, checked case-insensitively: the full pubkey hex, a hex
/// key-prefix, or the node's advertised label (Windows hostnames render
/// uppercase; users type lowercase). An empty qualifier names nothing.
// [impl->REQ-SUBNET-3]
fn node_qualifier_matches(inst: &Instance, qualifier: &str) -> bool {
    if qualifier.is_empty() {
        return false;
    }
    inst.node.to_ascii_lowercase().starts_with(&qualifier.to_ascii_lowercase())
        || inst.node_label.as_deref().is_some_and(|l| l.eq_ignore_ascii_case(qualifier))
}

/// A node's paste-able short form (the `@` qualifier views print): the first
/// 8 hex chars of the pubkey.
pub fn key_prefix(node: &str) -> String {
    node.chars().take(8).collect()
}


    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
    /// subnets.
    AcrossSubnets(Vec<String>),
}

    /// The id resolves in several visible subnets — qualify with `subnet:id`. Lists the
    /// subnets.
    AcrossSubnets(Vec<String>),
    /// The `@node` qualifier itself matched SEVERAL distinct live nodes (M7
    /// D2, REQ-SUBNET-3): a shared label, or a too-short key-prefix.
    /// Refuse-and-qualify — `candidates` lists paste-able key prefixes (8
    /// hex chars each), never a guess between machines sharing a hostname.
    NodeQualifier { qualifier: String, candidates: Vec<String> },
}

/// Does an `@node` qualifier name this instance's node (M7 D2, REQ-SUBNET-3)?
/// Three spellings, checked case-insensitively: the full pubkey hex, a hex
/// key-prefix, or the node's advertised label (Windows hostnames render
/// uppercase; users type lowercase). An empty qualifier names nothing.
// [impl->REQ-SUBNET-3]
fn node_qualifier_matches(inst: &Instance, qualifier: &str) -> bool {
    if qualifier.is_empty() {
        return false;
    }
    inst.node.to_ascii_lowercase().starts_with(&qualifier.to_ascii_lowercase())
        || inst.node_label.as_deref().is_some_and(|l| l.eq_ignore_ascii_case(qualifier))
}

/// A node's paste-able short form (the `@` qualifier views print): the first
/// 8 hex chars of the pubkey.
pub fn key_prefix(node: &str) -> String {
    node.chars().take(8).collect()
}

+    /// The `@node` qualifier itself matched SEVERAL distinct live nodes (M7

+    /// D2, REQ-SUBNET-3): a shared label, or a too-short key-prefix.

+    /// Refuse-and-qualify — `candidates` lists paste-able key prefixes (8

+    /// hex chars each), never a guess between machines sharing a hostname.

+    NodeQualifier { qualifier: String, candidates: Vec<String> },

+/// Does an `@node` qualifier name this instance's node (M7 D2, REQ-SUBNET-3)?

+/// Three spellings, checked case-insensitively: the full pubkey hex, a hex

+/// key-prefix, or the node's advertised label (Windows hostnames render

+/// uppercase; users type lowercase). An empty qualifier names nothing.

+fn node_qualifier_matches(inst: &Instance, qualifier: &str) -> bool {

+    inst.node.to_ascii_lowercase().starts_with(&qualifier.to_ascii_lowercase())

+        || inst.node_label.as_deref().is_some_and(|l| l.eq_ignore_ascii_case(qualifier))

+/// A node's paste-able short form (the `@` qualifier views print): the first

+pub fn key_prefix(node: &str) -> String {

/// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,

/// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:


    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
    /// `@node` exact → an instance on `local_node` (local wins) → the sole remaining live
    /// instance → refuse if several live nodes remain (force `@node`). `Offline`
    /// instances are never routed to.

    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
    /// `@node` (full hex, key-prefix, or label — [`node_qualifier_matches`]) → an
    /// instance on `local_node` (local wins) → the sole remaining live instance →
    /// refuse if several live nodes remain (force `@node`). `Offline` instances are
    /// never routed to.


    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
    /// `@node` exact → an instance on `local_node` (local wins) → the sole remaining live
    /// instance → refuse if several live nodes remain (force `@node`). `Offline`
    /// instances are never routed to.

    /// Resolve an [`Address`] within THIS single subnet (the `subnet:` qualifier, if any,
    /// is ignored here — see [`resolve_across`] for the multi-subnet form). Policy:
    /// `@node` (full hex, key-prefix, or label — [`node_qualifier_matches`]) → an
    /// instance on `local_node` (local wins) → the sole remaining live instance →
    /// refuse if several live nodes remain (force `@node`). `Offline` instances are
    /// never routed to.

-    /// `@node` exact → an instance on `local_node` (local wins) → the sole remaining live

-    /// instance → refuse if several live nodes remain (force `@node`). `Offline`

+    /// `@node` (full hex, key-prefix, or label — [`node_qualifier_matches`]) → an

+    /// instance on `local_node` (local wins) → the sole remaining live instance →

+    /// refuse if several live nodes remain (force `@node`). `Offline` instances are

pub fn resolve(&self, address: &Address, local_node: &str) -> Resolution {

self.resolve_visible(address, local_node, |_| false)


        // Explicit `@node` pins one instance (must be live).
        if let Some(node) = &address.node {
            return match live.iter().find(|i| &i.node == node) {
                Some(i) => Resolution::Resolved { subnet: None, instance: (*i).clone() },
                None => Resolution::NotFound,
            };
        }

        // Explicit `@node` pins one instance (must be live). The qualifier
        // accepts the full pubkey hex, a key-prefix, or the advertised node
        // label (M7 D2, REQ-SUBNET-3); several distinct matches refuse with
        // paste-able key prefixes — a shared hostname is never guessed
        // through.
        // [impl->REQ-SUBNET-3]
        if let Some(node) = &address.node {
            let hits: Vec<&&Instance> =
                live.iter().filter(|i| node_qualifier_matches(i, node)).collect();
            return match hits.as_slice() {
                [] => Resolution::NotFound,
                [one] => Resolution::Resolved { subnet: None, instance: (**one).clone() },
                many => Resolution::Ambiguous(Ambiguity::NodeQualifier {
                    qualifier: node.clone(),
                    candidates: many.iter().map(|i| key_prefix(&i.node)).collect(),
                }),
            };
        }


        // Explicit `@node` pins one instance (must be live).
        if let Some(node) = &address.node {
            return match live.iter().find(|i| &i.node == node) {
                Some(i) => Resolution::Resolved { subnet: None, instance: (*i).clone() },
                None => Resolution::NotFound,
            };
        }

        // Explicit `@node` pins one instance (must be live). The qualifier
        // accepts the full pubkey hex, a key-prefix, or the advertised node
        // label (M7 D2, REQ-SUBNET-3); several distinct matches refuse with
        // paste-able key prefixes — a shared hostname is never guessed
        // through.
        // [impl->REQ-SUBNET-3]
        if let Some(node) = &address.node {
            let hits: Vec<&&Instance> =
                live.iter().filter(|i| node_qualifier_matches(i, node)).collect();
            return match hits.as_slice() {
                [] => Resolution::NotFound,
                [one] => Resolution::Resolved { subnet: None, instance: (**one).clone() },
                many => Resolution::Ambiguous(Ambiguity::NodeQualifier {
                    qualifier: node.clone(),
                    candidates: many.iter().map(|i| key_prefix(&i.node)).collect(),
                }),
            };
        }

-        // Explicit `@node` pins one instance (must be live).

+        // Explicit `@node` pins one instance (must be live). The qualifier

+        // accepts the full pubkey hex, a key-prefix, or the advertised node

+        // label (M7 D2, REQ-SUBNET-3); several distinct matches refuse with

+        // paste-able key prefixes — a shared hostname is never guessed

-            return match live.iter().find(|i| &i.node == node) {

-                Some(i) => Resolution::Resolved { subnet: None, instance: (*i).clone() },

-                None => Resolution::NotFound,

+            let hits: Vec<&&Instance> =

+                live.iter().filter(|i| node_qualifier_matches(i, node)).collect();

+            return match hits.as_slice() {

+                [] => Resolution::NotFound,

+                [one] => Resolution::Resolved { subnet: None, instance: (**one).clone() },

+                many => Resolution::Ambiguous(Ambiguity::NodeQualifier {

+                    qualifier: node.clone(),

+                    candidates: many.iter().map(|i| key_prefix(&i.node)).collect(),


595	        )),
596	    }
597	}
598
599	#[cfg(test)]
600	mod tests {
601	    use super::*;
602
603	    fn inst(node: &str, status: Status, epoch: u64) -> Instance {
604	        Instance { node: node.to_string(), status, epoch, resources: None, last_active_ms: None, shell_adapters: Vec::new(), node_label: None }
605	    }
606
607	    fn addr(s: &str) -> Address {
608	        Address::parse(s).unwrap()
609	    }
610
611	    // [unit->REQ-INST-7] the model serde-roundtrips (the on-wire/persisted form).
612	    #[test]
613	    fn registry_serde_roundtrips() {
614	        let mut reg = SubnetRegistry::new();
615	        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
616	        reg.merge_instance("ling", inst("nodeB", Status::Dormant, 1));
617	        let json = serde_json::to_string(&reg).unwrap();
618	        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
619	        assert_eq!(reg, back);
620	    }
621
622	    // [unit->REQ-INST-7] distinct nodes for one id coexist as separate instances.
623	    #[test]
624	    fn distinct_nodes_are_separate_instances() {
625	        let mut reg = SubnetRegistry::new();
626	        assert_eq!(reg.merge_instance("ling", inst("nodeA", Status::Active, 1)), MergeOutcome::Inserted);
627	        assert_eq!(reg.merge_instance("ling", inst("nodeB", Status::Active, 1)), MergeOutcome::Inserted);
628	        assert_eq!(reg.instances("ling").len(), 2);
629	    }
630
631	    // [unit->REQ-INST-7] a strictly-newer epoch supersedes; the row stays single per node.
632	    #[test]
633	    fn newer_epoch_supersedes_same_node() {
634	        let mut reg = SubnetRegistry::new();
635	        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
636	        assert_eq!(
637	            reg.merge_instance("ling", inst("nodeA", Status::Dormant, 2)),
638	            MergeOutcome::Updated
639	        );
640	        assert_eq!(reg.instances("ling").len(), 1, "same node stays one row");
641	        assert_eq!(reg.instances("ling")[0].status, Status::Dormant);
642	    }
643
644	    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] THE lease invariant (#8): a lagging
645	    // lower-epoch Active must NOT overwrite a newer higher-epoch Offline — the resolved
646	    // status stays Offline, so a partition/skew never routes to a dead instance.
647	    #[test]
648	    fn stale_active_cannot_clobber_newer_offline() {
649	        let mut reg = SubnetRegistry::new();

        )),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn inst(node: &str, status: Status, epoch: u64) -> Instance {
        Instance { node: node.to_string(), status, epoch, resources: None, last_active_ms: None, shell_adapters: Vec::new(), node_label: None }
    }

    fn addr(s: &str) -> Address {
        Address::parse(s).unwrap()
    }

    // [unit->REQ-INST-7] the model serde-roundtrips (the on-wire/persisted form).
    #[test]
    fn registry_serde_roundtrips() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
        reg.merge_instance("ling", inst("nodeB", Status::Dormant, 1));
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
    }

    // [unit->REQ-INST-7] distinct nodes for one id coexist as separate instances.
    #[test]
    fn distinct_nodes_are_separate_instances() {
        let mut reg = SubnetRegistry::new();
        assert_eq!(reg.merge_instance("ling", inst("nodeA", Status::Active, 1)), MergeOutcome::Inserted);
        assert_eq!(reg.merge_instance("ling", inst("nodeB", Status::Active, 1)), MergeOutcome::Inserted);
        assert_eq!(reg.instances("ling").len(), 2);
    }

    // [unit->REQ-INST-7] a strictly-newer epoch supersedes; the row stays single per node.
    #[test]
    fn newer_epoch_supersedes_same_node() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
        assert_eq!(
            reg.merge_instance("ling", inst("nodeA", Status::Dormant, 2)),
            MergeOutcome::Updated
        );
        assert_eq!(reg.instances("ling").len(), 1, "same node stays one row");
        assert_eq!(reg.instances("ling")[0].status, Status::Dormant);
    }

    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] THE lease invariant (#8): a lagging
    // lower-epoch Active must NOT overwrite a newer higher-epoch Offline — the resolved
    // status stays Offline, so a partition/skew never routes to a dead instance.
    #[test]
    fn stale_active_cannot_clobber_newer_offline() {
        let mut reg = SubnetRegistry::new();


// [unit->REQ-PRES-1] the presence datum is a FIELD under the lease, not a

    // [unit->REQ-SUBNET-3] `@node` qualifiers accept a label or key-prefix
    // (case-insensitive), not just the full hex; a qualifier matching several
    // distinct nodes refuses with paste-able candidate key prefixes —
    // machines sharing a hostname are never guessed between.
    #[test]
    fn node_qualifier_accepts_label_and_prefix_refuses_shared() {
        let labeled = |node: &str, label: &str| Instance {
            node_label: Some(label.to_string()),
            ..inst(node, Status::Active, 1)
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", labeled("bcead52b00112233", "HFENDULEAM"));
        reg.merge_instance("ling", labeled("77aa11ff00112233", "GRAVITY"));

        // Label, any case.
        let r = reg.resolve(&addr("ling@hfenduleam"), "local");
        assert!(
            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("bcead52b")),
            "label resolves case-insensitively: {r:?}"
        );
        // Key-prefix.
        let r = reg.resolve(&addr("ling@77aa11ff"), "local");
        assert!(
            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("77aa11ff")),
            "key-prefix resolves: {r:?}"
        );
        // Full hex still pins exactly.
        let r = reg.resolve(&addr("ling@bcead52b00112233"), "local");
        assert!(matches!(&r, Resolution::Resolved { .. }), "full hex resolves: {r:?}");
        // Unknown qualifier finds nothing.
        assert_eq!(reg.resolve(&addr("ling@nosuch"), "local"), Resolution::NotFound);

        // A shared label refuses with candidate key prefixes — never a guess.
        let mut shared = SubnetRegistry::new();
        shared.merge_instance("ling", labeled("bcead52b00112233", "TWIN"));
        shared.merge_instance("ling", labeled("77aa11ff00112233", "TWIN"));
        match shared.resolve(&addr("ling@twin"), "local") {
            Resolution::Ambiguous(Ambiguity::NodeQualifier { qualifier, candidates }) => {
                assert_eq!(qualifier, "twin");
                assert_eq!(candidates, vec!["bcead52b".to_string(), "77aa11ff".to_string()]);
            }
            other => panic!("expected NodeQualifier refusal, got {other:?}"),
        }
        // …and each candidate prefix then resolves uniquely.
        assert!(matches!(
            shared.resolve(&addr("ling@bcead52b"), "local"),
            Resolution::Resolved { .. }
        ));
    }

    // [unit->REQ-PRES-1] the presence datum is a FIELD under the lease, not a