# W2 #345 RESOLVER — census + picks (todlando → doyle, 2026-09-26)

Branch `feat/351-w2-resolver` off 9144b8f8, worktree `.worktrees/351-w2`. No impl, no build yet (pool unclaimed). Model: RULINGS S6, CONTEXT.md:904 (already amended in this tree), REQ-INST-10.
Provenance: the sites marked (R) I read myself at 9144b8f8; the rest come from a census subagent's read and carry file:line for checking.

## Census facts that shape the picks

**One policy resolver.** (R) `resolve_among` (`spt-net/src/net/registry.rs:1049`): `@node` → local wins outright → `[only]` → `AcrossNodes` refuse. There is no active rung and no state ranking: Active, Dormant and Suspended weigh the same, because the only filter is `Status::routable()` (`:576`, `!Offline`). It does not read `activation`. Wrappers: `resolve_visible` (`:1018`, one subnet) and `resolve_across_visible` (`:1111`, a union across visible subnets deduped by node, keeping the higher epoch per node).

**Two consumers BYPASS it with a local-first leg that runs BEFORE resolution.** This is the load-bearing fact: S6 puts the active rung ABOVE local.
- (R) `spt send`, `cli.rs:13537-13546`: `try_spt_hosted_inject` / `send_windowed` run against the raw target string. `wan_send` → resolver runs only on `NoPerch` (`:13626`). A local dormant instance therefore takes every bare send today, and would still take it after a resolver-only change.
- (R) `spt rc` attach, `rc.rs:2518`: a local session is taken first when the target is bare (`wire_id == endpoint_id`). Only then `resolve_and_dial_owner`. The picker's Attach passes the bare `ep.id` even for remote rows (`picker/model.rs:1484-1497`), so the picker inherits the same bypass.
- (R) `wan_send` returns `NotFound` when the resolved node is this node (`wansend.rs:469`), because the local leg is assumed to have run.

**Consumers that already resolve first** (the new rung reaches them automatically): fork (`wansend.rs:821`; Local/NotFound fall through to a local fork), knock (`wansend.rs:610`), the rc resolver leg itself (`wansend.rs:298`).

**Consumers outside S6.**
- Qualified-only: digest (`cli.rs:2904`), shell link (`owner@node`, `wansend.rs:1279`), span (explicit `--node`).
- Rest verbs: (R) bare wake/suspend use `route_rest_event` (`resting.rs:921`): local first, then `select_rest_target` (`registry.rs:1236`), a GOAL selector (wake = ∃Active, suspend = ∀Suspended) over status only.
- Notif/consent MRA: `presence.rs:106` / `notif.rs:133` / `consent.rs:78` rank ENDPOINTS for the USER by `last_active_ms`. The exception is `addressed_target` (`presence.rs:214`), which ranks one named id's instances by MRA.
- Shell→owner traffic: a local `deliver_tcp` (`api/reporting.rs:605`). That is #350 (R2-1 shells follow active).

**State inputs available.**
- `Instance.status` (`registry.rs:78`) and `Instance.activation` (`:238`); `activation_rank()` gives `(activation, node)` (`:256`).
- `sibling_outranks` is at `resting.rs:584`. No helper answers "which node holds the active instance of X". The closest are `SiblingClaim`/`keep_highest` (`registryhost.rs:170/179`, per feed batch) and `sibling_view_from` (`resting.rs:610`, a bool).
- The own-node row in the snapshot is our own last advertisement. It can lag the local effective state by up to one round, or by one push once W1's push has fired.

**Refusals.** `render_refusal` (`wansend.rs:1509`), with prefixes `AMBIGUOUS:` (send/rest/shell), `KNOCK_AMBIGUOUS:`, and bare text for fork/digest/rc.

**Stale contract text:**
- `docs-site/src/instances/overview.md:13-17` ("no silent most recently active pick"; local → sole → refuse).
- `docs-site/src/messaging/overview.md:129`.
- `PRD.md:110`.
- ADR-0003 `:38/:48` and ADR-0006 §2 (local → MRA).
- The REQ-INST-10 note (`traceable-reqs.toml:263`) and REQ-INST-7 (`:250`).
- `CONTEXT.md:302` ("the active instance is the bare-id routing target") becomes TRUE with this change and stays.

**Tests pinning today's order:**
- `registry.rs`: `resolve_prefers_local_instance` (`:1717`), `resolve_sole_live_remote_and_skips_offline` (`:2245`), `resolve_multiple_live_nodes_refuses` (`:2257`), and 8 more `resolve_across_*` tests.
- `wansend.rs`: `wan_send_ships_to_the_resolved_node` (`:3181`), `refusals_render_copy_paste_targets` (`:3549`), and the two `resolve_and_dial_owner_*` tests.

## Picks

**(1) The rung order in `resolve_among`, as ONE function every consumer shares.** After `@node`:
- **a. ACTIVE:** the live candidates whose `status == Active`.
  - Exactly one → resolve to it.
  - Several (a transient split before the push converges) → the max `activation_rank()`, which is the ruled total order S2 converges to. It is not a guess: it names the winner the demotion is already moving toward.
  - QUESTION for you: take max-rank, or treat several actives as "no unique active" and fall through to (b)? I recommend max-rank. Falling through would let a split instant route to a local dormant that is about to stay dormant.
- **b. WARM TIER:** candidates = the Dormant rows if any exist; otherwise the Suspended rows. That is "suspended ranks below dormant" applied as a tier, not a tiebreak.
- **c. LOCAL** within that tier wins.
- **d. SOLE** within that tier resolves.
- **e. REFUSE** `AcrossNodes` over that tier's nodes.

Consequences to state plainly:
- A local SUSPENDED instance loses to a sole remote DORMANT one.
- One dormant plus one suspended remote resolves to the dormant, where today it refuses. That reverses the first comment on #345 ("keep cold instances counted toward ambiguity"); S6 overrides it, and I'm flagging the reversal in case it was not meant.
- The `@node` rung is unchanged. Offline stays excluded.

**(2) Own-node freshness.** The union gets our own row from the registry. Its status can lag the local truth by one advertisement. Pick: when a local perch exists for the id, overwrite the own-node candidate's `status` and `activation` with the local effective state (`effective_rest_state` + perch activation) before ranking. This is cheap and local, and a just-woken local instance then wins at once. Only the CLI-side wrapper does this (it has perch access); spt-net stays pure.

**(3) Kill the two local-first bypasses, bare targets only.**
- **send** (`cmd_send_verdict`): resolve BEFORE the local leg when the daemon is running and the registry is readable.
  - Resolved to a remote node → the WAN leg directly (the local leg is skipped).
  - Resolved local, or NotFound/unreadable → today's local leg unchanged. So the offline, no-registry and no-subnet behaviour is identical.
  - `Ambiguous` → today's `AMBIGUOUS:` refusal. It no longer silently lands on a local dormant.
- **Fix the gap in passing:** `id@<this node>` is qualified, so it misses the raw-target local leg, and `wan_send` then NotFounds it at `:469`. Pick: a qualified target that resolves to the own node strips the qualifier and runs the local leg. (This was read from the code, not reproduced; I'll pin it with a unit first and report if it does not fail.)
- **rc attach:** the same shape. For a bare target, resolve first; take the local session only when the resolution says local, or when there is no registry answer.
- The picker needs no change: it passes the bare id and inherits the fix.

**(4) Left alone, each stated in the docs:**
- **Rest verbs** stay on their goal selector (REQ-REST-VERB-ROUTING). `wake <id>` asks for "∃ active somewhere" and `suspend` for "all suspended". Those are state goals, not routing. Routing them through S6 would make `suspend dean` suspend only the active instance, which is a behaviour change nobody ruled. QUESTION: confirm.
- **Notif/consent MRA** stays. Its target is the USER, and it ranks endpoints, not instances (doyle's #345 comment 1; the grill did not re-rule it). CONTEXT.md:660 already says notifs route "to the user".
- **`addressed_target`** (`presence.rs:214`) ranks ONE addressed id's instances by MRA. That is an instance resolution inside the notif domain. I leave it, flag it, and do not re-rule it here.
- **Shell→owner** stays local; it is #350.

**(5) Refusal copy.** The tokens are unchanged. `render_refusal` lists candidates in rank order and appends each one's state (`dean@kitsu (dormant), dean@hfen (suspended)`), so the operator sees why nothing was picked. This is additive, and `refusals_render_copy_paste_targets` gets the state suffix.

**(6) Traceability.**
- Mint **REQ-RESOLVE-ACTIVE-FIRST**, stages doc, impl, unit, int, with the rung order of (1)+(2) as its title.
- Mint **REQ-SEND-RESOLVES-BEFORE-LOCAL**, stages impl, unit, int, covering (3) for send and rc.
- REQ-INST-10 keeps its grammar and refusal role. Its note is amended to point at the new REQ for the order.
- REQ-INST-7's note gets the same amendment.
- **Units:**
  - a table over `resolve_among` covering: active wins over local dormant; two actives → max rank; local suspended loses to remote dormant; dormant + suspended → dormant; two dormant, none local → refuse; offline excluded; `@node` still pins a dormant.
  - own-node overwrite.
  - send routing decision as an injected-I/O seam (the `route_rest_event` pattern), so the unit needs no daemon.
- **int:** a cell 5 on `twohost_axes` after cell 1. A is active and B dormant, and B resolves a bare `axes-ep` → A's node through the production `resolve_across_visible` + own-node overwrite. A second read is taken after cell 3 (active vacated, both dormant or suspended): B resolves local. This rides the rig that is already climbed. No new rig.

**(7) Docs:**
- `docs-site/src/instances/overview.md` gets the ruled order and the state-tier wording.
- `docs-site/src/messaging/overview.md` gets the AMBIGUOUS row plus the state suffix.
- `PRD.md:110` and the REQ-INST-7/10 notes are updated.
- ADR-0003/0006: an amendment NOTE pointing at RULINGS S6. The ADR bodies are not rewritten.

## Order of work (after your go on the picks)
1. REQs minted in `traceable-reqs.toml`, then `treqs check`.
2. `resolve_among` with the rung units.
3. The own-node overwrite and the send/rc routing seam with their units.
4. The render suffix.
5. The docs.
6. The axes cell 5 plus a one-box dry run, then the two-box climb (a GO per cargo leg on HFENDULEAM).
