# W1 (#249) gate checklist — doyle, pre-staged 2026-09-07 00:50Z while todlando builds

Source of truth: `WEBSERVE-272-JIT.md` §W1 (incl. the 00:50Z unknown-node ruling) + ADR-0060
(`WEB` surface) + ADR-0056 Am.1 AS ON MAIN (router order; `ef9171a6`). Base = `c33dc521` (W0).
Gate reads go to SOURCE, never the PR body. Anchors below were read at c33dc521 and name the
lines W1 must CHANGE; re-read them at the W1 tip.

## 0. Pre-flight (before any command)
- [ ] Open GATE-TEST-INDEX.md (memory) — first touch of a gate.
- [ ] `git fetch` then assert origin/main == GitHub main (`gh api …/branches/main`).
- [ ] `git merge-base --is-ancestor origin/main <tip>` — if false, rebase BEFORE gating.
- [ ] List every other open PR with CI running; land the expensive-rerun sibling first
      (hertz's thin docs lanes — breadcrumb bd3a337b, ledger row 5707e6ee — are suite-inert).
- [ ] IR-76 three-arm golden check; record free GB beside the floor line (`xtask disk-floor`).
      W0 reap left free ≈155 GB; a gate pool costs ~68 GB.
- [ ] Worktree `.worktrees/gate-w1-<sha>` detached at the tip; own pool; claim FROM inside.
- [ ] `.worktrees/gate-w0-02adfbc1` (0 MB shell, pinned by an invisible cwd handle at
      `crates/spt-daemon`, 4 orphan spt.exe already killed 00:45Z) — retry rm; not blocking.
- [ ] Reuse `.spt/preserved/gate-w0-c33dc521/launch-battery.py` (explicit env=, identity trio
      scrubbed, child env READ BACK) and `field-w0.sh` as the field-probe template.

## 1. Mechanical legs (exit FILES read, never the harness notification)
- [ ] ONE battery per box: my legs start only after todlando's Windows battery exit files are in
      and no CI job is running on this runner. Census cargo by cwd/parent first; kill only mine.
- [ ] Sweep leaked exes before every cargo (todlando's `ws272-w0/target/debug/spt.exe` pair
      26588/18264 from 22:53Z is HIS to kill — told 00:50Z); `xtask disk-floor` read before firing;
      build xtask FROM the gate worktree.
- [ ] `traceable-reqs check` (exit 2 = did not parse — read the code). W1 mints
      **REQ-WEB-CROSS-NODE-PROXY** (doc, impl, unit, int) in the toml FIRST; no `"` in the title.
- [ ] workspace-bins prebuild (whole packages: `-p spt --bins`, `-p mock-adapter --bins`) →
      `xtask check` (docs drift + spacerun + internal-codes scan).
- [ ] clippy strict; `nextest --no-fail-fast` filtered: spt-daemon (dispatch, webserve, the new
      proxy module), spt-net (if a `Web` record family lands there), spt-store (access WEB cells,
      unchanged expected) + `webserve_e2e` + **`twohost`** (spt-daemon integration bin; it is in
      golden's HEAVY set, golden.yml:158, and its own ladder job at :1029 — the gate runs it in
      the gate pool ALONE, never beside the unit leg).
- [ ] Every nextest leg: RUN COUNT vs the expected population, `grep -c Summary` == 1 before any
      FAIL read; count `panicked at`, not FAIL lines. Expected population at c33dc521 = 2803
      (+ W1's new cells).
- [ ] Same legs on kitsubito (builder's Linux battery accepted only if the product delta to the
      gated tip is measured `git diff --stat -- . ':!.github'` and is docs-only; else run mine).
      Flock the output dir; detach on purpose; kill by remote pid; `procs-before: 0` logged.
- [ ] Mutation (pre-register BEFORE the battery; run AFTER it in the same pool, never beside):
      arm A — the owner-side `access_check(…, surface::WEB, …)` call: mutate its deny branch to
      allow → the twohost "WEB deny rule turns it into 403" cell MUST red at its 403 assert while
      the plain-fetch cell stays green (run the two cells as separate invocations so "sibling
      intact" is WITNESSED, not structural — W0's ":259 intact" lesson);
      arm B — Range pass-through: strip the `Range` header before forwarding → the 206 cell reds
      at its status assert (`left: 200, right: 206`), revert = green. Record MATCH_COUNT=1 and the
      mutated line for each; `git checkout --` revert = 0 dirty lines.

## 2. Source reads (one per REQ; note file:line in the verdict at the W1 tip)
Anchors at c33dc521: `StreamFamily` enum dispatch.rs:103 (Knock's arm shows the TAGGED shape —
an untagged record is claimed by the WanMsg arm; W1's family must be tagged) · `serve_xfer`
xfer.rs:225 = the owner-side shape: origin from `NetStreamInfo::remote_id_hex`, NEVER payload
bytes (REQ-HAZARD-WAN-ORIGIN-AUTH), `endpoint` = gate subject · `access_check` gate.rs:193 /
`access_check_with_sender` :220 · `surface::WEB` access.rs:146, row :218 `default_on: true,
attributable: false` (unit :3074-3088 already pins deny-by-rule) · the W0 placeholder arm
webserve.rs:298-303: `is_known_subnet_node` → bare node `redirect_node`, else
`BAD_GATEWAY "NODE_UNAVAILABLE: <node>: cross-node serving is not available yet"` — THIS is the
line W1 replaces; rule 2.5 root-leaf steal :298 and the unknown-label docs 404 :307 stay.
- [ ] REQ-WEB-CROSS-NODE-PROXY / request path: `/<peer>/…` for a KNOWN subnet peer opens ONE
      Iroh stream of the new `StreamFamily::Web` (tagged record family; a dispatcher arm; an N-1
      owner with no arm → the local proxy answers 502 naming the node, bounded by a deadline,
      never a hang). Bare `/<peer>` still 302s to `/<peer>/` (W0 arm kept).
- [ ] Owner side: runs `access_check(…, surface::WEB, …)` with the origin taken from the
      handshake-proven stream identity (the `serve_xfer` shape), subject = the served entry's
      origin endpoint; refusal → 403 whose body names the surface (`WEB`); the owner resolves
      the served name through the SAME registry path the local facet uses (edit visible, deleted
      → 404 carried back as 404, not 502).
- [ ] Proxy semantics: NO cache — no bytes written under `$SPT_HOME` on the requesting node
      (grep the proxy module for any write/snapshot; attachments' snapshot path is W2's, not
      here); `Range` forwarded verbatim and the owner's 206 + `Content-Range` carried back;
      `HEAD` carried through; `Content-Type` is the owner's; response streamed, not buffered whole
      (a large file must not need its size in RAM — check the body path is chunked).
- [ ] Status naming: owner unreachable / stream refused / deadline → 502 body names the NODE;
      owner deny → 403 body names the SURFACE; unknown label → docs compat 404 (Am.1 order,
      :307 unchanged, RULED 00:50Z — a "404 naming the node" body is a FINDING); served-name miss
      on the owner → the owner's `NOT_FOUND: served resource <name>` body, status 404.
- [ ] Reserved facets stay router-first for the PEER prefix too: `/<peer>/f/…`, `/docs/`, `a/`,
      `m/`, `bin/`, `install` — W0 reserved them at the router before any registry lookup
      (webserve.rs:290); W1 must not proxy a reserved facet to the owner unless the facet is
      built (none are in W1) — expect `FACET_NOT_FOUND`/`FACET_UNAVAILABLE` locally.
- [ ] Docs surface untouched: `/<local>/docs/…` byte-true, `/docs/…` compat alias byte-true
      (REQ-DOCS-LOCAL-SERVER); `?json` index twin still local-only data (the index lists the
      LOCAL registry; a peer's index is fetched as `/<peer>/?json` through the proxy).
- [ ] ADR-0060 consequences: `XFER` row still present (retires at the #246 close, W2), no schema
      change; `WEB` remains non-attributable in W1 (no sender stamp yet) — a "sender" in the
      403 body would be a finding.
- [ ] Int: `twohost` cells — kitsubito fetches a file registered on hfenduleam through
      kitsubito's own 5474 (bytes byte-equal to the source file); WEB deny rule on hfenduleam →
      403; `Range: bytes=0-3` → 206 with the 4 bytes; tagged `// [int->REQ-WEB-CROSS-NODE-PROXY]`
      ON the cells. Unit: dispatcher arm for the tagged family; the 502-on-N-1 arm.
- [ ] Docs: `docs-site/src/serving/cross-node.md` + SUMMARY + llms.txt; `networking/
      access-viewing.md` gains the WEB row text (what the row gates, default-on within subnet,
      403 shape); `reference.md` regen; `api.md` untouched; CONTEXT.md vocabulary cited not
      re-worded (node-prefixed URL, served resource, serving registry).
- [ ] CHANGELOG `[Unreleased]` entry, user-facing wording, no internal codes (`xtask check`
      scans; a `NODE_UNAVAILABLE`-style token in the changelog is a finding).
- [ ] Commit trailer `Co-authored by: todlando` (grep / raw body, never `%(trailers:)`); wip
      checkpoints squashed; PR body `Fixes BigscreenVR/spt-bs-releases#249` ONLY (#272 open).

## 3. Field acceptance (mine, two real rig daemons, browser)
Perch daemons own 5474 on both boxes (hfenduleam 14444 since 09-04; kitsubito has its own
agents), so the field runs on RIG daemons at `SPT_DOCS_PORT=5480`, isolated `SPT_HOME`, identity
trio unset, one per box, paired into a fresh subnet (`/sptc:subnet` mechanics: create on A, code,
join on B). Bins = the gated tip's `target/debug/spt` from each box's own pool. Script from
`field-w0.sh`; every assert = exact body/status, never a letter-grep.
- [ ] A (hfenduleam rig): `serve add report.md`; B (kitsubito rig): `curl -s
      http://localhost:5480/hfenduleam/report.md` byte-equal to the source (`cmp`).
- [ ] Browser on hfenduleam: `http://localhost:5480/kitsubito/` renders kitsubito's index
      (peer index through the proxy), `?json` twin 200.
- [ ] Edit `report.md` on A → B's next fetch shows the edit (no cache); `serve rm` on A → B gets
      404 `NOT_FOUND: served resource report.md` (the owner's body, not 502).
- [ ] A: `access` deny rule on `WEB` for B's node → B's fetch = 403, body names `WEB`; clear the
      rule → 200 again.
- [ ] B: `curl -r 0-3` → 206, `Content-Range: bytes 0-3/<len>`, 4 bytes; `-I` → HEAD 200 with
      `Content-Length`, empty body.
- [ ] Stop A's rig daemon → B's fetch = 502, body names `hfenduleam`, returns within the deadline
      (time it; a hang past the deadline is a finding); restart A → 200.
- [ ] B: `/<unknown-label>/x` → the docs 404 (Am.1), NOT a 502; `/hfenduleam/f/` → 404 naming
      the facet locally (no proxy hop — A's log shows no stream).
- [ ] Both rigs `node stop --force` 0; no `spt.exe` left from either rig pool (census by exe path).

## 4. Land
- [ ] CI green at the tested sha; re-run the ancestor check at land time; ff-only; tested ==
      merged; pick-audit.
- [ ] Alchemy sweep → #249 ACCEPTANCE; W2 dispatch (`#246` + `#147`) via alchemy `dispatch` —
      NO `--` tokens in the note (memory: they parse as flags and the dispatch is refused).
- [ ] Pool: todlando releases; hertz's W3 drift lane (`test/ws272-w3-drift` @1839fba8) rebases
      only when `build/ws272-w3` exists (not now).
