# W4 sketch — update sources on the W3 substrate (todlando, 2026-09-24 ~13:05Z)

## Invariant (doyle 74TRPRNO)
sources.json is an ORDERING HINT, never a trust input. Every byte from a pinned/learned
source still passes the same gate: plan_verified / verify_update_set_docs / verify_peer_adapter.
No code path reads sources.json inside a verify function; unit asserts a pinned source
serving tampered adapter bytes is still REJECTED.

## Store — `ReleaseCache::sources()` / `record_served()` / `pin()` / `unpin()` (relcache.rs, beside W3 retain_*)
`releases/sources.json`: `[{node, kind: pinned|learned, last_served: {what: core|docs|bundle|adapter:<n>, version, at_ms}?}]`
- learned = written ONLY on a VERIFIED stage from that node (learn-on-serve), never on offer/roster.
- pinned = operator verb only; pin of an already-learned node upgrades it; unpin drops the pin (keeps learned history? -> drop to learned if it has last_served, else remove).
- rank(node) = 0 pinned, 1 learned, 2 other. Pure fn `source_rank(&Sources, node)`; `order_by_source(Vec<T>, key)` stable sort.

## Learn-on-serve sites (all AFTER the verify that admitted the bytes)
1. pump/update.rs `UpdatePullOutcome::Updated` (core set, served by `peer_hex`).
2. pump/update.rs `DocsPullOutcome::Staged` (docs).
3. W5 bundle stage (hook left for W5; W4 exposes the fn).
4. W3 CLI: `peer_update_candidate` + `install_via_subnet` -> needs the SERVING node hex back:
   re-add `PeerAdapterFetched.node` (W3 lint dropped it as unread; W4 gives it the reader).

## Where the ladder is consulted
- CLI adapters (W3 path): `adapter_holders()` result ordered by (source rank, then highest version).
  Today W3 sorts highest-version-first; W4 makes the key (rank, Reverse(version)). A pinned
  source holding an OLDER-but-still-newer-than-installed copy is asked first (operator intent),
  never-downgrade unchanged.
- Pump core/docs — READ (pump/mod.rs 705-770, 1000-1030): visit order is NOT fan_targets order.
  fan_targets is grouped into `peer_subnets: HashMap` (hash order) for already-connected peers,
  and freshly dialled peers run on their dial-COMPLETION event. So sorting fan_targets buys
  nothing. Proposal = WORKER-LOCAL defer in UpdateWorker (no pump-shell change):
  pre_round snapshots the ladder; peer_step on a peer of rank r pulls only if no source of rank
  < r is still "owed" this round — owed = ranked source not yet asked this round AND not in its
  failure cooldown (reuse the docs_pull_backoff shape: per-node, per-version, 10 min). A deferred
  peer is asked on a later round once the sources were asked or cooled down, so a dead pinned
  source costs at most one cooldown, never a stall. Pure fn `ask_now(rank, owed_ranks)` unit-tested.
  Other workers untouched.
- Channel always last; `--remote` = channel only; `--via-subnet` = peers only.

## Composite `spt update --remote | --via-subnet`
- `--remote`: core leg = today's channel fetch; adapters leg = cmd_adapter_update(remote=true) (W3 flag).
- `--via-subnet`: core leg = CLI peer pull of the staged set over the broker (request_update on a
  dialled conn, same shape as W3 pull_adapter_from_peers; ladder order; learn-on-serve), NO channel;
  adapters leg = peers only (AdapterRoute{peers:true, channel:false} for gh_release too — new
  `route` input). Refuse `--remote --via-subnet` together (clap conflicts_with).
- Default composite = ladder: peers (ranked) then channel for core? CONTEXT says order everywhere,
  but today the composite core leg is channel-only and peers arrive via pump. Proposal: default
  composite core leg stays channel (pump already covers peers in the background); only
  `--via-subnet` pulls core from peers in the CLI. Flag for ruling — CONTEXT "every path honors
  the order" argues for peers-first in the composite core leg too.

## Verbs
`spt update source list [--json]` (node label via snapshots, kind, last served what/version/age),
`spt update source pin <node>` (label | key prefix | full hex, resolved through snapshots like @node),
`spt update source unpin <node>`; `spt update status` gains `sources: 1 pinned (A), 2 learned (B, C)`.

## Evidence plan
- unit: rank/order pure fns; learn only on verified stage; pin/unpin transitions; pinned-source
  tamper still rejected; clap conflict.
- int (two-daemon rig, W3 adapter_peer_e2e shape): B has A + C holding newer; B learns A after
  one served set; next pull asks A first (observe which peer served via ADAPTER_UPDATE_SOURCE /
  UPDATE_STAGED from-line). Negative control first, per W3.

## doyle RULINGS 12:59Z (MZSZAE5B) — BINDING, supersede the sections above where they differ
- Invariants (1) hint-only / never read in verify, (2) learn-on-serve AFTER admitting verify: ENDORSED; state BOTH as invariants in the PR body. PeerAdapterFetched.node re-added ONLY in the same commit as its reader.
- (a) worker-local DEFER REFUSED. Approved instead: SKIP-TO-NEXT-ROUND, never wait. A lower-ranked peer is not asked this round while a higher-ranked source is still owed. Owed clears on ask completion, round end, or dial FAILURE (immediately). NO 10-min cooldown. Comment at the site, verbatim: "Nothing in the pump ever blocks on another peer's in-flight dial." Ladder is by CLASS across rounds (pinned -> learned -> peers -> channel); rounds do the sequencing.
- (b) composite core leg PEERS-FIRST too (CONTEXT: every path honours the order). Verify UNCHANGED (signed-set gate). --remote forces channel. GitHub fetch STAYS in the CLI (no HTTP in daemon). Pump + composite both using the ladder is intended.
- Branch: HOLD for doyle's landed W3 sha; branch once.
