# W1 gate — c0f48e54 (doyle, 2026-09-24 09:11Z)

nextest (spt-net+spt-daemon lib, spt bins, peer_docs_e2e, docs_bundle_e2e): 2075 run / 2075 passed (3 leaky) / 1 skipped, exit 0. treqs 0.4.1 exit 0, 944==944. Clippy --workspace --all-targets -D warnings: exit 0 (09:12Z). Review below (read-only agent, opus).

# Read-only design review — c0f48e54 vs 3672c25c (PR spt-bs-core#250)

Worktree `.worktrees/gate-c0f48e54`. No build, no cargo, no state-changing git. Paths are repo-relative to that worktree.

## 1. Serve-side roster gate + one indistinguishable refusal — VERDICT: HOLDS

`crates/spt-daemon/src/propagate.rs:180-231`. The arm runs the identical predicate the `Query` arm runs at `propagate.rs:238` (`let trusted = roster.is_member_any(origin_node);` — same expression, same `origin_node` which `dispatch.rs:1144` sources from the stream table, hazard 7.5).

```rust
let trusted = roster.is_member_any(origin_node);                       // :192
let held = if trusted {
    UpdAsset::parse(&asset).and_then(|a| cache.load_asset(&a, version)) // :194
} else { None };
let Some(bytes) = held else {
    let line = UpdRecord::Err { upd_id, message: ASSET_NOT_HELD.to_string() }.encode_line();
    brain.net_stream_send(stream_id, &line, None, true)?;               // :203
    return Ok(if trusted { UpdateServeOutcome::AssetNotHeld }
              else { UpdateServeOutcome::Refused });                    // :205-208
};
```

One record shape for all four refusal causes (untrusted / unparsable asset name / absent file / version mismatch); `ASSET_NOT_HELD = "asset not held"` at `crates/spt-net/src/net/update.rs:135`. The outcome enum distinguishes them *locally only* (never on the wire). Both arms proven end-to-end at `crates/spt/tests/peer_docs_e2e.rs:404-418` (untrusted → `Refused`, wrong version → `AssetNotHeld`, both read `AssetPullOutcome::NotHeld` at the requester).

Minor: the untrusted arm short-circuits before the disk read, so a timing side channel exists (sub-ms vs a file read). Not a design-ruling violation; note only.

## 2. Exact version match only — VERDICT: HOLDS

`crates/spt-daemon/src/relcache.rs:370-375`:

```rust
pub fn load_asset(&self, asset: &UpdAsset, version: u64) -> Option<Vec<u8>> {
    if self.staged_version() != Some(version) { return None; }
    std::fs::read(self.asset_path(asset)).ok()
}
```

No ordering comparison anywhere; no "latest" fallback. Covered by `relcache.rs:701-724` (`older version` / `newer version, never closest` / `declared but absent` / `empty cache`) and the integration arm at `peer_docs_e2e.rs:412-418`.

## 3. Requester verifies against the signed set's digest; nothing staged before the check — VERDICT: HOLDS

`propagate.rs:815-829`:

```rust
match verify_update_set_docs(meta, bytes) {
    Ok(()) => { cache.stage_docs(bytes)?; Ok(DocsPullOutcome::Staged { version: meta.version }) }
    Err(reason) => Ok(DocsPullOutcome::Rejected(reason)),
}
```

`meta` comes from the node's OWN staged signed set (`docs_wanted`, `propagate.rs:782-789`), never from the stream. `verify_update_set_docs` (`crates/spt-daemon/src/release.rs:662-678`) hashes the bytes and compares to `meta.docs.sha256`. On mismatch `stage_docs` is never called — no file written, nothing to remove; `relcache.rs:329-332` is the only writer. Unit proof at `propagate.rs:1123-1153` (`TAMPERED` → `Rejected(ArtifactMismatch)`, `cache.staged_docs().is_none()`, `staged_version()` still 8 → binary set untouched). Pump side is loud on reject: `pump/update.rs:401-403` `UPDATE_DOCS_REJECTED:{peer}:{reason}`.

Note (not a defect, pre-existing posture): `docs_wanted` trusts the on-disk `metadata_json` without re-checking `signature_hex` — same trusted-on-stage assumption `land_staged_docs` already makes. Local-FS write is already game over for the binary path.

## 4. Old-peer arm drops on the reply-read deadline as a per-peer failure — VERDICT: HOLDS

Deadline: `propagate.rs:692-696` — `brain.reply_read_deadline()`, re-armed on data for this stream, read through `read_peer_reply_until`. That is byte-for-byte the `request_update` pattern at `propagate.rs:474-480`. `reply_read_deadline` = `min(io_timeout, PEER_REPLY_READ_BUDGET)` (`brain.rs:1050`, `:2685`), and `reclassify_peer_reply_err` (`brain.rs:2696-2705`) maps the resulting `TimedOut` **out** of `TimedOut` so `peer_outcome` drops only that peer. `pump/update.rs:405-409` catches every non-`TimedOut` error, logs `UPDATE_DOCS_PULL_FAIL`, and cools the peer down; only a genuine carrier `TimedOut` re-raises (`:405`).

Test: `crates/spt/tests/peer_docs_e2e.rs:469-499` `a_silent_peer_costs_one_reply_budget_as_an_ordinary_error` — nobody serves the stream, `assert_ne!(err.kind(), TimedOut)` at `:482`, then the same brain completes a real exchange at `:493-498` (not wedged). Dispatch side of "unknown kind skipped" is the `fetch_asset` classifier at `dispatch.rs:263`; an N-1 daemon lacks it and resolves Unknown.

## 5. `UPDATE_DOCS_SKIPPED:` loud on declared-but-unstaged — VERDICT: HOLDS

`crates/spt/src/cli.rs:9745-9754` + the call at `:9784-9793`:

```rust
let Some(bundle) = cache.staged_docs() else {
    let declared = staged_meta.as_ref().and_then(|m| m.docs.as_ref());
    if let Some(line) = unstaged_docs_skip_line(declared.map(|d| d.sha256.as_str()), landed.as_deref())
    { eprintln!("{line}"); }
    return;
};
```
Line text: `"UPDATE_DOCS_SKIPPED: signed set declares docs but none staged — docs retry next fetch"` (`:9753`). Silent only for a docs-less set and for a set whose declared digest equals the recorded landed one (`:9750-9752`). Unit `cli.rs:35558-35572`; integration legs `docs_bundle_e2e.rs:260-275` (leg 4) and `peer_docs_e2e.rs:456-462` (node C against a pre-fix peer).

## 6. Retention after landing, bounded to the applied version — VERDICT: HOLDS

- `clear_staged_docs()` after landing is **gone**: `cli.rs:9869-9875` now writes `cache.record_docs_landed(&bundle_sha256)` instead. Repo-wide, the only remaining callers are the two staging paths and one test — `relcache.rs:239`, `:305`, `:633` (grep `clear_staged_docs`, 4 hits incl. the definition at `:345`).
- Bounded to one bundle: `relcache.rs:296-307` keeps a retained bundle across a restage only when the incoming set's `docs.sha256` equals `sha256_hex(bundle)` (case-insensitive), else drops it; `relcache.rs:236-239` drops it on a single-release stage. Unit `relcache.rs:660-698`.
- Serving after apply works because `spt update apply` never clears the staged set (no `clear_staged` symbol exists), so `staged_version()` still answers. Integration proof `peer_docs_e2e.rs:333-338` (A retains after a real apply) and `:420-433` (B pulls exactly those bytes).
- Re-extraction guard: `staged_docs_already_landed` (`cli.rs:9760-9766`, used at `:9811`) also requires `docs_dir.is_dir()`, so a deleted docs tree re-lands.

## 7. Hazards — VERDICT: HOLDS, with two forward-looking notes

- **D5b / fire-and-forget sends:** every new send passes `op = None` and journals nothing — `propagate.rs:203` (refusal, `finish=true`), `:219` (chunks, `finish=false`), `:227` (Done, `finish=true`), `:688` (the opener, `finish=true`). `net_stream_send` with `op=None` returns immediately without an ack wait (`brain.rs:2119-2121`). Identical to the artifact serve at `:295-303` and the status query at `:859`. The stream open is non-journaled too (`propagate.rs:659`, `net_open_stream(conn_id, None)`).
- **Unbounded brain read:** none introduced. The serve loop reuses the existing `call_deadline()` at `propagate.rs:134`; the pull loop uses `reply_read_deadline()` (`:692`). In a non-pump (`cold_start`) carrier both are `None` by construction — that is the pre-existing carrier contract, and the only production caller is the pump; the e2e requester correctly uses `Brain::cold_start_pump` (`peer_docs_e2e.rs:238-246`).
- **Unverified staging:** none. See item 3.
- **Panic/unwrap on wire data:** none found. `decode_bytes` errors are mapped (`propagate.rs:712-714`); `place_chunk` (`:747-761`) uses `usize::try_from` + `checked_add` + a `<= ASSET_PULL_MAX_BYTES` filter, so `offset = u64::MAX` is an ordinary error (unit `:1090`); the slice `bytes[end - chunk.len()..end]` cannot underflow because `end = offset + chunk.len()`. Path traversal is closed: `asset_path` routes an adapter name through `sanitize_platform` (`relcache.rs:576-587`), unit-checked at `relcache.rs:722-724` (`../../evil` → `.._.._evil.spt` under `adapters/`).
- **Note A (forward risk):** `request_asset_on` ignores `Done { total }` (`propagate.rs:715-717`, `..` binding). Harmless for docs because the digest gate catches truncation, but `UpdAsset::Bundle` / `Adapter` have no verification wrapper yet — a later wave must not call `request_asset` directly without one. Worth a `total` check or an assert at that time.
- **Note B:** the byte ceiling is requester-side only; the serve side reads the whole asset into RAM (`relcache.rs:374`) with no cap. Self-inflicted only (the node wrote the file), so not a defect.

## 8. Tests — VERDICT: HOLDS, one timing-budget flag

New/changed:

| Test | Proves |
|---|---|
| `dispatch.rs:2307` (in `classify_first_line` test) | `fetch_asset` classifies to `StreamFamily::Update`. |
| `net/update.rs:241-245` (in the round-trip test) | `FetchAsset` encodes/decodes over NDJSON. |
| `net/update.rs:269-280` `asset_wire_names_round_trip_and_unknown_is_none` | wire names round-trip; `adapter:`, `firmware`, `Docs` all parse to `None` (no case folding, no guessing). |
| `propagate.rs:1079-1094` `asset_chunks_place_by_offset_and_the_ceiling_refuses` | out-of-order + replayed chunks reassemble idempotently; ceiling and `u64` overflow are non-`TimedOut` errors that write nothing. |
| `propagate.rs:1126-1153` `pulled_docs_are_admitted_only_against_the_signed_digest` | `docs_wanted` gating; tampered bytes → `Rejected`, nothing staged; good bytes → `Staged`; binary set untouched. |
| `relcache.rs:663-698` `retained_docs_survive_only_a_set_that_signs_them` | at most one retained bundle; kept on a matching restage (incl. upper-case digest), dropped by a different-docs set, a docs-less set, and a single release. |
| `relcache.rs:704-725` `asset_loads_only_for_the_exact_staged_version` | exact-version-only serve; no traversal out of `adapters/`. |
| `relcache.rs:729-736` `docs_landed_marker_round_trips` | landed-digest marker lowercases and round-trips; absence is `None`. |
| `pump/update.rs:428-438` `docs_pull_cooldown_is_per_peer_and_per_version` | cooldown is per peer and per set version; passes at `t0 + DOCS_PULL_RETRY`. Uses synthetic `Instant` arithmetic, **not** a sleep — good. |
| `cli.rs:35561-35572` `unstaged_docs_skip_is_loud_only_for_declared_unlanded_docs` | loud exactly for declared-and-never-landed; silent for docs-less and already-landed (case-insensitive). |
| `cli.rs:35577-35583` `retained_docs_reland_only_when_needed` | no re-extract when digest matches AND the tree exists; re-lands otherwise. |
| `peer_docs_e2e.rs:313-467` `a_peer_pulled_set_lands_the_peers_version_matched_docs` | full A→B path over real brokers: A retains after a real apply, B pulls the set then the docs, both serve-gate refusal arms, B lands real docs; plus a pre-fix negative control (A2 serves nothing, C is loud). |
| `peer_docs_e2e.rs:469-499` `a_silent_peer_costs_one_reply_budget_as_an_ordinary_error` | silent peer → ordinary error, brain still usable. |
| `docs_bundle_e2e.rs:178-215` (leg 1b, inside the existing test) | repeated apply prints no `UPDATE_DOCS` token and does not re-extract (mtime unchanged). |
| `docs_bundle_e2e.rs:260-275` (leg 4) | declared-but-unstaged → loud skip, binary outcome unchanged, no docs tree. |

**Flag — one product-time-budget wait:** `peer_docs_e2e.rs:487-488`

```rust
assert!(waited >= io_timeout, "waited one reply budget: {waited:?}");
assert!(waited < io_timeout * 5, "and not much more: {waited:?}");
```

`io_timeout` is 2 s (`:473`), so the upper bound is a 10 s wall-clock assertion on a shared runner — the class of red memory records as "new test racing a product budget reds under load". The load-bearing assertion (`assert_ne!(err.kind(), TimedOut)` at `:482`) is state, so the upper bound is decoration; suggest loosening or dropping `:488` rather than shipping a box-measuring assert. `:487` (lower bound) is safe.

Also `connect_retry` / `pump_brain` / `wait_for_stream_except` poll with bounded retries and exit on first sight (`peer_docs_e2e.rs:227-259`) — correct shape, no fixed sleeps.

## 9. traceable-reqs.toml — VERDICT: HOLDS

Three new ids, all `required_stages = ["doc", "impl", "unit", "int"]` (`traceable-reqs.toml:7852-7864`), and all three added to the group roster at `:4895-4897`.

- `REQ-UPDATE-PEER-ASSET-LEG` — doc `docs-site/src/self-update/overview.md:107` (immediately above the "Docs travel with the release." paragraph) + `CHANGELOG.md:17`; impl on the real code at `net/update.rs:77,134,140`, `dispatch.rs:262`, `propagate.rs:56,83,190,634,652,746,765,781,796,814`, `relcache.rs:60,64,352,369`, `pump/update.rs:38,43,373`; unit on the tests at `net/update.rs:240,265`, `dispatch.rs:2306`, `propagate.rs:1076,1123`, `relcache.rs:701`, `pump/update.rs:423`; int `peer_docs_e2e.rs:20`.
- `REQ-UPDATE-DOCS-RETAINED-SERVABLE` — doc `overview.md:108`, `CHANGELOG.md:18`; impl `relcache.rs:69,238,298,379,389`, `cli.rs:9759,9810,9872`; unit `relcache.rs:660,727`, `cli.rs:35574`; int `docs_bundle_e2e.rs:18`, `peer_docs_e2e.rs:21`.
- `REQ-UPDATE-DOCS-UNSTAGED-SKIP-LOUD` — doc `overview.md:109`, `CHANGELOG.md:19`; impl `cli.rs:9744,9787`; unit `cli.rs:35557`; int `docs_bundle_e2e.rs:19`, `peer_docs_e2e.rs:22`.

Every `impl`/`unit` tag sits on or immediately above the item it names (I checked each line); none is a file-top coverage tag.

**One note, not a failure:** the `int->` tags in `peer_docs_e2e.rs:20-22` are in the file header rather than on either `#[test]`, and that file carries **two** tests. This matches the pre-existing convention in `docs_bundle_e2e.rs` (its `[int->REQ-DOCS-RELEASE-ASSET]` is also a file-header tag), and for an int file the module is plausibly the evidence — but a reader cannot tell from the tag which of the two tests carries `REQ-UPDATE-DOCS-UNSTAGED-SKIP-LOUD` (it is the first one, `:456-462`). Moving the tags onto the two `#[test]` fns would cost three lines and remove the ambiguity.

## Summary

All nine ruling conditions hold. Nothing blocks. Two cheap cleanups worth folding in before merge: drop or widen the `waited < io_timeout * 5` wall-clock assert (`peer_docs_e2e.rs:488`), and move the `int->` tags onto the two `#[test]` fns. One forward-looking item to carry into the bundle/adapter wave: `request_asset` is public and unverified by itself, and `Done{total}` is ignored — the next caller must bring its own digest gate.
# Gate record — releases#331 H3 (peer-rig fixtures), PR #251 @ `d3582138`

Gater: doyle, 2026-09-24 10:35Z, HFENDULEAM.
Lane: hertz (test/CI). Test-only. Base `b25a037d` (W1 landed).

## Subject verified before any green was read

- `origin/test/331-peer-rig` = `d3582138f19cda055c057bda8bf9da6b2402ada4`.
- `d3582138^` = `b25a037d` — rebased onto current main, not merged into it.
- Patch-id of `b25a037d..d3582138` == patch-id of `5744839f..bb6086cb` = `9f5031b6…`. The
  rebase carried the same change; the previously-green pre-rebase run is about the same patch.
- Diff touches only `crates/spt/tests/peer_rig/{mod,inproc,signing,adapter,bundle,wait}.rs`,
  `crates/spt/tests/peer_rig_selftest.rs`, `traceable-reqs.toml`. No product source.
- Sole file overlap with what main gained since the branch's old base is `traceable-reqs.toml`;
  `git merge-tree` onto `b25a037d` was clean (tree `6101491c`).

## Conditions

| # | condition | verdict | evidence |
|---|---|---|---|
| 1 | CI green on the REBASED sha | HOLDS | run `35984979431` on `d3582138`, 5/5: changes, traceability, lint, unit Windows, unit Linux |
| 2 | traceable-reqs headers == ids after the toml merge | HOLDS | doyle measured the MERGED tree: `[[requirements]]` 946 == ids 946. (The toml tail-union hazard is why this is measured on the merge result, not the branch.) |
| 3 | rig compiles against landed W1 | HOLDS | `cargo clippy --workspace --all-targets -D warnings` green; nextest built 236 binaries |
| 4 | negative controls exist and each mutation reds only its own arm | HOLDS AS HERTZ'S TESTIMONY | PR body: flipped archive byte -> `BadSignature`; foreign signer, same key id -> `BadSignature`; wrong docs bytes -> `ArtifactMismatch`; unrostered requester -> `Refused`, stages nothing; both timeout messages pinned with `should_panic(expected)`. Mutation proof: signing `bytes[1..]` reds only the archive arm; `StreamCursor` ignoring its snapshot hangs the in-process arm's second pull to nextest TIMEOUT (the `propagate.rs` stale-row signature) |
| 5 | the selftest EXECUTES green | NOT PROVEN BY CI — hertz's local run only | see blind spot below |

## Blind spot, recorded rather than laundered

**CI never ran this rig.** The unit leg is `cargo nextest run --workspace -E 'kind(lib) + kind(bin)'`:
`Starting 3293 tests across 28 binaries (1 test and 208 binaries skipped)`. `peer_rig_selftest` is a
`tests/` integration binary — `kind(test)` — so it was built and skipped. The 5/5 green says the rig
COMPILES against landed W1; it says nothing about the rig RUNNING.

Execution evidence is hertz's local `peer_rig_selftest` 8/8 plus
`clippy -p spt --test peer_rig_selftest -D warnings` clean, reported 2026-09-24 10:03Z. That is
testimony from the lane that owns it (dispatch split: test work is hertz's), and it is specific
enough to be falsifiable — it names per-arm mutation outcomes and a known product hang signature.
It is NOT a doyle measurement and must not be cited as one.

Where it DOES execute: golden. `golden.yml` runs `cargo nextest run --workspace --no-fail-fast`
split into phase A (`not HEAVY`) and phase B (`HEAVY`) with no `kind()` filter, so integration
binaries run there. Thin-lane PR CI skipping them is ADR-0050 by design, not a defect — nothing to
register.

Consequence, stated precisely: this rig's first CI execution anywhere will be the #331 milestone
golden run. A rig defect therefore surfaces late, inside a milestone-batch golden, where it reds a
run that is expensive to repeat and hands back to triage. Until then the only thing standing behind
the fixtures is hertz's local run. If a consumer int reds at golden, the rig is a live suspect —
"is this red mine" applies with the rig in scope, not just the product diff.

## Carried forward

- `traceable-reqs` Quality audit emitted two `[must]` findings on `REQ-TEST-PEER-RIG-FIXTURES`
  (`criterion=contains-and`; `criterion=length` — title is 144 words, want 3..=25). The
  traceability job still concluded success, so these are non-gating today. The requirement's
  *title* is carrying what belongs in its description. Routed to hertz to fix in the next test lane.
- PR #251's body still describes the pre-rebase state (`@ bb6086cb on 5744839f`, treqs 943/943).
  The landed sha is `d3582138` at 946/946. Body is stale, change is not.

## Verdict

GATE PASSES for a test-only fixtures lane, with condition 5 held as hertz's testimony and the CI
blind spot recorded above. ff-land `d3582138` onto main (`d3582138^ == b25a037d`, so the push is a
true fast-forward and tested sha == merged sha).
# Gate record — releases#331 H2 (bundle release side, #338), PR #254 @ `c185326b`

Gater: doyle, 2026-09-24 11:40Z, HFENDULEAM.
Lane: hertz. Base `b4490c4f` (my PR #253 landed 11:04Z). Branch `feat/338-bundle-release-side`.

## Subject verified before any green was read

- Head `c185326bd2b08e152cc33aa975fcea2240ca1cc8`; `git merge-base --is-ancestor origin/main
  c185326b` true against `origin/main = b4490c4f` — a true fast-forward, tested sha == merged sha.
- Five commits: `4c33c983` (release-side gate, red on purpose) / `7b28df47` (xtask
  `bundle-adapters` + `release-publish` signs) / `ac754801` (peer_rig `SetBuilder.bundle`) /
  `10ed32be` (tar call independent of image-search order — **a refactor, NOT a bugfix**;
  see the withdrawn defect below) / `c185326b` (my IR-147 rider).
- The first three patch-ids are identical to hertz's pre-rebase chain (`b6c775a0` / `582a7b2f` /
  `b2ad4a30`), and `16b44a46`->`ac754801` differed by exactly my docs +7. His reported numbers
  therefore measure the gated tree.

## Conditions

| # | condition | verdict | evidence |
|---|---|---|---|
| 1 | CI green on the gated sha | HOLDS | run `35992242546` on `c185326b`, 5/5: changes, traceability, lint, unit Linux (kitsubito), unit Windows (hfenduleam) |
| 2 | traceable-reqs clean | HOLDS | traceability job success at `c185326b`; hertz's run EXIT=0, 950/950 complete, 0 findings, headers==ids 950, checker 0.4.1. New `REQ-BUNDLE-RELEASE-ASSET` activated doc/impl/unit, int deferred to W5 |
| 3 | code read: gate + verifier + units correct and correctly tagged | HOLDS | `require_bundle`, `verify_update_set_bundle`, both unit tests read by doyle; tags sit on the evidence |
| 4 | **the bundle actually assembles, live** | HOLDS — doyle measured | see below |
| 5 | **the two int binaries CI skips execute green** | HOLDS — doyle measured | see below |

## Leg 1 — live assembly (doyle, in `.worktrees/gate-ac754801` @ `c185326b`)

`cargo run -p xtask -- bundle-adapters --out <tmp>\spt-bundled-adapters.tar.gz` exit 0, archive
sha256 `854067c06756fa43aa3d8a30b812cf5f732f8a3eccc4dbd9725c0c564ee6071d`.

Verified against the ARTIFACT, not the tool's stdout — extracted and recomputed both hashes:

| member | version | source | bytes | sha256 (recomputed from extracted bytes) |
|---|---|---|---|---|
| claude-spt | 0.41.3 | BigscreenVR/claude-spt-bs | 1546835 | `b502d355cc5f73b417d34dc77159936fdac316650690d1226b74992452fc0394` |
| PACER | 0.7.0 | BigscreenVR/spt-pacer-tool | 1094942 | `cbf88d6a062ed0fe784dcc89cbd520c0e5011eefa5f248e6048c8eef653d1b2a` |

Both match `bundle.json`'s `sha256` fields exactly, and `bundle.json`'s `name`/`version` pairs are
the two expected adapters and nothing else.

Tar member names, verbatim: `./`, `./bundle.json`, `./claude-spt.spt`, `./PACER.spt` — the `./`
prefix re-measured here, which is carry-forward (b) below.

## Leg 2 — the two int binaries CI skips (doyle, same worktree, box idle)

CI's unit leg filters `kind(lib) + kind(bin)`, so `tests/` integration binaries are built and
skipped (the H3 blind spot). Run under my own hand with `--success-output immediate`:

- `cargo nextest run -p spt --test peer_rig_selftest --success-output immediate` — **9 tests run:
  9 passed, 0 skipped** in 2.830s, exit 0. Named arms read, including
  `bundle_round_trips_members_byte_identical`,
  `set_bundle_entry_matches_the_mock_bundle_asset_and_nothing_else`,
  `signed_mock_adapter_archive_verifies_and_a_flipped_byte_does_not`,
  `inproc_pull_stages_at_a_rostered_peer_and_not_at_an_unrostered_one`.
- `cargo nextest run -p spt --test peer_docs_e2e --success-output immediate` — **2 tests run:
  2 passed, 0 skipped** in 3.902s, exit 0, including
  `a_peer_pulled_set_lands_the_peers_version_matched_docs`.

This closes, at this sha, the H3 record's condition 5 blind spot for `peer_rig_selftest`: the rig
now has an execution measurement from the gate, not only builder testimony. It remains true that
CI itself never runs these binaries — first CI execution is still the #331 golden.

Run scheduling: legs were sequenced against the box, not fired into it. The pool-claim build and
leg 1 (load-insensitive) ran capped at `-j 6` while the Windows unit held this runner; leg 2 (the
timing-sensitive rigs) waited for CI to complete. Pool claimed as lane `gate-H2-c185326b`.

## A defect I raised and WITHDREW — do not re-derive it

I measured that Git Bash resolves `tar` to GNU 1.35, which dies on an absolute Windows path
("Cannot connect to C: resolve failed"), and claimed `release-publish` could not publish from Bash.
**Wrong subject.** A rustc probe doing `Command::new("tar")` launched from Git Bash prints bsdtar
3.8.4 — Windows searches System32 before PATH, so a shell PATH probe does not measure what the
program spawns. hertz disputed it with his own live run and was right. Written up as memory
`shell-path-probe-does-not-measure-a-spawned-tool`.

Residual that IS real: bsdtar REJECTS `--force-local`, so that flag is never the portable fix.
hertz kept the relative-path shape in `10ed32be` as a refactor, pinned by
`tar_plan_never_passes_a_drive_letter`, and the commit says explicitly it is not a bugfix.

## Carried forward to W5 (#338 apply side) — both from source read, NEITHER measured

- **(a)** `verify_update_set_bundle` returns `Err` on a set with NO bundle entry, and
  `debug_rollout_meta` sets `bundle: None`. The apply side must read "no entry" as "nothing to
  apply", NEVER as a set rejection — otherwise every set published through v0.72.0 and every debug
  rollout becomes unappliable.
- **(b)** Bundle members carry a `./` tar prefix (re-measured in leg 1 above), so the reader must
  not match bare names.

## IR-147 rider

`c185326b` carries the IR-147 entry: a docs-only PR skips unit, so its merge finds no successful
unit to reuse (`UNIT_REUSE run-unit=true reason=both-unit-jobs-not-successful`) and pays a full
Windows+Linux unit on a Markdown commit — measured landing #253. My first fix candidate said
"diff `HEAD^1..HEAD`", which is WRONG for a multi-commit ff push (`HEAD^1` lands inside the lane);
the entry now says `github.event.before..github.sha`. hertz swapped the stale rider for the
corrected `e5684ecb`, verified in the pushed tree.

## Verdict

**GATE PASSES.** ff-land `c185326b` onto main. Every condition is measured at the gated sha; the
two conditions H3 had to hold as testimony (assembly, rig execution) are doyle measurements here.
# Gate record — W2 `feat/335-adapter-leg`, PR #252 @`6c95d691` (doyle, 2026-09-24)

Milestone releases#331 SEAMLESS-UPDATES. Base `d3582138` (= origin/main at gate time; merge-base
== main, so the PR is a fast-forward and the merge result is the head content). One commit.
Members: releases#335 + #278 + #62, with #329 (no code) and #2 arm 1 answered in the PR body.

## Conditions

1. **CI green on the tested sha.** HOLDS — run `35988370797` **5/5**: changes, traceability,
   lint, `unit (Linux, kitsubito)` 3269/3269, `unit (Windows, hfenduleam)` 3306/3306 in 260.7 s
   (job `107596627449`, 10:39:19→10:55:48Z, 16m29s wall against the 60-min cap). The ~1 min of
   cargo contention at its start (box note below) did NOT damage it. On Windows only 2 of the 3
   `entry_exec::tests` arms exist — the `#[cfg(unix)]` one is compiled out, which is the whole
   reason conditions 3 and 4 are argued on box 2.

**VERDICT: all five conditions hold. ff-landed 2026-09-24 10:56Z — `d3582138..6c95d691` → main,
tested sha == merged sha, no rewrite.** Post-merge run `35990141244`: `unit` **skipped** via the
IR-144 exact-PR-proof reuse, so the merge cost the box only a `lint` job on kitsubito.
2. **Merged-tree traceability.** HOLDS, doyle measurement: `traceable-reqs 0.4.1` (= CI pin)
   `check` exit 0 in an isolated worktree at `6c95d691`; `[[requirements]]` headers **949 == 949**
   ids (toml tail-union hazard: the header count is asserted separately from the id count).
   Registry moved 946 → 949, the three ids this lane mints.
3. **The evidence EXECUTES, per layer.** Split deliberately — a PR green can prove COMPILE and
   nothing about EXECUTION.
   - **unit: HOLDS as a doyle measurement.** Read out of the Linux unit job log
     (`107596627551`, "3269 tests across 28 binaries", Summary 3269 passed): all three
     `spt-runtime entry_exec::tests::*` arms ran — including the `#[cfg(unix)]`
     `force_heals_only_declared_entries_once`, which is the arm Windows can never reach —
     plus `spt-daemon crc_swap::tests::{plan_prunes_only_stale_strings_never_binaries_or_litter,
     a_rolled_back_commit_prunes_nothing}`, the three `cli::tests::adapter_fan_out_*`, and the
     `spt-proto emit::tests::*` capture arms. Named, not inferred from a total.
   - **int: DOES NOT execute in CI.** Same blind spot as H3: the thin lane filters
     `kind(lib) + kind(bin)` and both new int binaries (`adapter_fanout_e2e`, `adapter_swap_e2e`)
     are `tests/` binaries — the log's own "210 binaries skipped". `golden.yml` runs ints
     workspace-wide, so the skip is ADR-0050 by design, not an infra item; the consequence is
     that these ints' first CI execution anywhere is the #331 golden run.
     → **doyle closed that gap by hand on box 2 (kitsubito, 2026-09-24 ~10:50Z):**
     `cargo nextest run -p spt -E 'binary(adapter_swap_e2e) + binary(adapter_fanout_e2e)'
     --success-output immediate` in the lane tree at `ecd19186` (see the subject check below)
     → **"Starting 3 tests across 2 binaries", 3/3 PASS**, named:
     `adapter_swap_e2e::an_update_prunes_retired_strings_and_keeps_dropped_binaries`,
     `adapter_swap_e2e::a_declared_entry_arriving_without_its_exec_bit_is_forced_loud_then_runs`
     (the `#[cfg(unix)]` arm), `adapter_fanout_e2e::three_adapters_update_at_once_and_report_like_the_serial_sweep`.
     Evidence read out of the success output, not inferred from the green:
     `ADAPTER_ENTRY_EXEC_FORCED:xx: …/srcs/xx/entry-bin extracted 0644 — packaging defect
     upstream` appears TWICE — once on the applied update, once on the following
     `ADAPTER_UPDATE_UPTODATE` run (the mode-only heal a content swap can never see) — and the
     third run is silent, so idempotence is observed rather than asserted.
     `PARALLEL_WALL: 6.379934781s for 3 adapters (not asserted)` against todlando's serial
     mutation of 62.9 s. Log: `todlando-w2/doyle-kitsubito-int.log`.
   - **Subject check (why the box-2 run counts for this sha).** The kitsubito lane tree is at
     `ecd19186`, not the PR head. `git patch-id --stable` over each commit's own diff:
     `ecd19186` → `04595e80d57b350aa2db9ee130c3a2c64549fe81`, `6c95d691` → the SAME id. Identical
     change, so the box-2 runs (mine, and todlando's 156/156 + mutation) measure the gated
     content. Tree was clean (`git status --porcelain` empty) and unmodified by me — the run
     references nothing I edited, so it needs no baseline arm.
4. **#62 is cfg-gated, so the REACHING mutation must be proved on box 2.** todlando's raw
   `kitsubito-mut62.raw` inspected by doyle: with `force_entry_exec` neutered, the real update
   reaches `ADAPTER_UPDATE_POST_FAIL:xx: post-step did not run: failed to spawn session:
   Permission denied (os error 13)` and the int reds — the field brick reproduced on Linux.
   That is a PEER measurement whose raw log I read, not a doyle measurement; the positive arm is
   doyle's own kitsubito run under condition 3.
5. **#2 arm 1 measured, scope stated.** Record `todlando-w2/ARM1-RESULT.md` + `arm1.out` read:
   control (plain overwrite of the running exe) REFUSED with a sharing violation; `spt adapter
   update` rc=0, v2 bytes on disk, live pid surviving on the renamed `svc.exe.old`, registry
   1.1.0 — the shipped crc_swap C1 displace (ruled shape 2), so no step-aside build. Scope
   limit stated in the PR body and honoured here: **CLI-direct route only; the
   daemon-coordinated route (live endpoint / declared `[service]`) was NOT measured.**

## Findings (non-gating, recorded)

- **F1 — `entry_exec` containment check is lexical, so a `..` token escapes it (code read, not
  measured).** `declared_entry_binaries` keeps a resolved program when
  `resolved.starts_with(install_dir)`. `Path::starts_with` compares whole components without
  normalising, and `resolve_program_in_dir` returns `install_dir.join(program)` (an absolute
  filled token passes through as-is), so a declared command of the shape
  `{adapter_dir}/../victim` yields `<install>/../victim`, which passes the check and gets
  `chmod +x`. The module's own doc states the opposite contract ("one resolving outside the
  install dir is not ours to touch"), so this is a claim-vs-code mismatch rather than a
  surprise. Blast radius is small — the same manifest already gets that program SPAWNED, and
  the install dir is itself named by the adapter — so the new capability is only "force +x on a
  file outside the install". Cheap fix: canonicalise before the containment check, or refuse a
  filled token containing a `..` component. Route: todlando, next W2-adjacent lane.
- **F2 — the `REQ-ADAPTER-UPDATE-PARALLEL` title still specifies a gate the code deliberately
  does NOT implement.** The title says the int is "three mock adapters with sleeps finish in
  about max not sum". The delivered int gates on STATE (a three-way rendezvous inside the
  post-steps) and prints `PARALLEL_WALL` unasserted — which is STRONGER and is the project's own
  rule (never race a product budget on a shared box). The title should be amended to the
  evidence, or a later reader re-derives a stopwatch gate from the registry. Route: todlando,
  same lane as F1.
- **F3 — `xtask check` (docs-drift) is not in the thin lane.** The `lint` job runs
  `clippy --workspace --all-targets` + `xtask brain-read-check`; the full `xtask check` is a
  golden-only step. The three doc edits (`docs-site/.../manifest.md`, `self-update/overview.md`,
  `docs/MANIFEST.md`) rest on todlando's `xtask check OK` until golden. Known, unchanged from
  prior lanes.
- **F4 — both new int binaries mutate the process env (`SPT_HOME`) around registration.** Safe
  under nextest (process per test) and under golden, which runs ints via nextest; a bare
  `cargo test -p spt` would race the two `adapter_swap_e2e` tests against each other. Noted so a
  future reader does not "fix" it by reaching for bare `cargo test`.
- **F5 (mine, corrected).** At the H3 gate I routed two traceability Quality-audit `[must]`
  findings to hertz as lane carry-forward. Measured after: `traceable-reqs lint` reports **1496
  `[must]` findings over 825 of 949 requirements (87 %)** — length 819, contains-and 674,
  tbd-todo 3 — and W2's three new ids carry the identical pair each. A class that fires on 87 %
  of the corpus is a calibration object, not a lane defect. Withdrawn to hertz by replacement;
  filed as **IR-146** in `docs/INFRA-REGISTER.md`.

## Box note (affects how a Windows red must be read)

hertz's queued H2 compile gated on post-merge run `35988140031` COMPLETING (10:38:56Z) and
started immediately — one minute before #252's Windows unit leg started at 10:39:19Z. He
TaskStopped it at ~10:40:08Z; his first leg had already died on `0xc0000142`
STATUS_DLL_INIT_FAILED under link contention. So **~1 minute of #252's Windows unit leg ran
against a competing cargo.** If that leg reds with a DLL/link/timing signature it is a VOID leg
to be rerun, not a W2 red. Lesson recorded: *"the previous run completed" is not a box-free
signal when a new push can land in the same minute* — the box empties after the POST-MERGE run,
and a new PR push re-occupies it immediately.
