---
name: v0170-w5-design
description: "v0.17.0 W5 subnet display parity + palette — execute-ready plan (grounded), the LAST build wave"
metadata: 
  node_type: memory
  type: project
  originSessionId: 8c8eef1f-6837-4fc5-a65f-0633fc292f63
---

REQ-SUBNET-DISPLAY-PARITY (impl+unit+int). doyle GO'd. Design = docs/design/subnet-presence-display.md §B. Depends on W4 (done). Build on shared branch v0.16.0-update-arc, selective commits. **Largest wave — wire-touching; do in a FRESH context.** W1/W4/W2/W3 already landed (see [[v0170-w1-w4-blockers]], [[v0170-w2-design]]).

## Goal
Local and remote (subnet) picker rows render IDENTICALLY. Today picker/data.rs:293-294 reduces remote rows to plain green/gray because bound/unbound, controlled+driver, harness_only aren't gossiped. Fix = gossip those per-Instance fields + derive full EpDisplay for subnet rows the same as local.

## Palette (§B table) — fill = ACTIONABLE
- green-filled ■ = online·bound·free (unchanged)
- blue-filled ■ = online·controlled (driven_by; desc shows `controlled by <node>`)
- RED-filled ■ = online·UNBOUND (was green-HOLLOW; ABSORBS UnboundControlled)
- AMBER-HOLLOW ▢ = online·harness-only (was amber-FILLED)
- GRAY-FILLED ■ = Suspended (cold, node up — wakeable) — NEW combo
- gray-hollow ▢ = Offline (node down) — now REMOTE-ONLY

## Impl steps
1. **spt-net registry::Instance** (crates/spt-net/src/net/registry.rs ~line 72): add 3 ADDITIVE serde-default fields: `bound: bool` (#[serde(default)]), `controller_node: Option<String>` (the driver; #[serde(default, skip_serializing_if=Option::is_none)]), `harness_only: bool` (#[serde(default)]). Offline NEVER gossiped (no field). merge_instance: no merge-key change (epoch still sole precedence); the fields ride the row. **16 files construct `Instance { }` literals** — ALL must add the 3 fields (mostly tests → set false/None/false). Files: cli.rs, picker/data.rs, registryhost.rs, notif.rs, registry.rs, wansend.rs, wanmsg.rs, replicate.rs, wan.rs, shelldisc.rs, reconcile.rs, presence.rs + tests/{dispatch,pump,sync,replicate}.rs. CONSIDER a helper/Default to reduce churn, but struct literals still need fields — grep `Instance {` and fix each.
2. **registryhost.rs advertise_local** (the per-perch row build, ~line 410+ after `let status = advertised_status(...)`): populate bound (= is bound, i.e. NOT is_perch_unbound), controller_node (from info.json driven_by if any — check how local picker reads it: info::read_info driven_by), harness_only (= info.controllable==Some(false) for a live_agent). Mirror what picker/data.rs local_rows derives from info.json, but for THIS node's own perches.
3. **picker/data.rs subnet_rows** (~250-306): REMOVE the remote-reduction (controllable:None / driven_by:None / is_unbound:false at 290-301). Instead derive from the gossiped Instance fields: is_unbound = !bound, driven_by = controller_node (render via node_label_display), controllable = Some(!harness_only) (or map harness_only), endpoint_type from gossip if available else "live_agent". Status mapping: ADD Suspended handling — needs EpStatus to carry Suspended (see 4).
4. **picker/model.rs**: EpStatus is Online/Offline only — Suspended (gray-FILLED) is distinct from Offline (gray-hollow). Options: add `EpStatus::Suspended` OR an EndpointRow `suspended: bool` flag. CLEANEST: add EpStatus::Suspended; data.rs maps registry Active|Dormant→Online, Suspended→Suspended, Offline→Offline. EpDisplay rework: DROP UnboundControlled; Unbound→(red-filled, absorbs controlled-ness); HarnessOnly stays (now amber-HOLLOW via view); ADD Suspended. display_status: Offline→Offline; Suspended→Suspended; then online branch (unbound→Unbound [red, no separate controlled variant]; driven_by→Controlled; non-live→Online; live !controllable→HarnessOnly; else Online). Update label().
5. **picker/view.rs** (color_for ~21-32, square_span ~57-60): EpDisplay→color+glyph: Online→green-filled; Controlled→blue-filled; Unbound→RED-filled (■, was green-hollow); HarnessOnly→amber-HOLLOW (▢, was filled); Suspended→gray-filled (■); Offline→gray-hollow (▢). Remove UnboundControlled arms. Desc pane: controlled row shows `controlled by <node>` (already partly there ~line 372 region).
6. **Desc pane**: a controlled row shows `controlled by <node>` (driven_by render).

## Tests
- unit (model.rs): EpDisplay derivation over the full matrix (online{bound,unbound}×{free,controlled}, harness-only, suspended, offline) + glyph/colour. Update existing display_status tests (UnboundControlled gone, Suspended added, HarnessOnly now hollow).
- unit (registry.rs): Instance serde round-trip with the new fields + N-1 (absent) parses.
- int (picker/data.rs, mirror the W4 remote_picker test): a remote row with gossiped bound/controller/harness/suspended renders the SAME EpDisplay as the equivalent local row (local-vs-remote parity).

## Gotchas
- BUILD OOM → CARGO_BUILD_JOBS=2 per-crate. Orphan target/debug/spt.exe locks → kill by WORKSPACE PID before xtask (NEVER AppData prod daemon). `spt` no lib → --bin spt.
- ADR-0001: registry is wire-format-adjacent, but ADDITIVE serde-default fields are explicitly sanctioned by the design (forward/back-compat). N-1 rows must parse clean (test it).
- No CLI surface change → likely no xtask gen needed (verify). Public docs: picker status-square legend + palette ride W6.
- W6: twohost rig ON (W2 seam) + W5 adds gossip FIELDS not transport — judge at fold. Release counter 36, bump-in-PR, deployah.
