releases#338 apply side — #331 W5. A node applying a signed set whose metadata carries the `bundle` entry installs or upgrades the bundle's members; a bundle-less set is nothing to apply.

## What changed

- **Acquisition.** `spt update fetch` fetches `spt-bundled-adapters.tar.gz` when the set declares it, verifies it against the SIGNED entry, stages it (`UPDATE_BUNDLE_SKIPPED` on any failure, binary staging unchanged). The pump pulls it from peers (`FetchAsset bundle`, W1's leg) through `pull_staged_bundle` — the `request_asset` wrap that IS the digest gate (W1 gate forward note) — with its own per-peer cooldown and learn-on-serve (`record_served(.., "bundle", ..)`).
- **Retention.** `ReleaseCache::{stage,staged,clear_staged}_bundle`; kept across a restage only when the incoming set signs exactly those bytes, dropped by a single release. Applying never clears it, so the node serves it as the `bundle` asset.
- **Apply** (`land_staged_bundle`, after `land_staged_docs` at both apply sites). No `bundle` entry → `NothingToApply`, silently, and **the verifier is never asked** (it refuses a bundle-less set as `Malformed` — that is the RELEASE path's rule and stays untouched; no change to `verify_update_set_bundle`, so no stop-and-refer). Otherwise: re-verify staged bytes vs the signed entry → extract the VERIFIED bytes → read `bundle.json` → per member:
  - absent → installed through `complete_adapter_add` (the `adapter add` tail, extracted verbatim so both share one path);
  - older → upgraded through `update_one_adapter` with the member as a `preset` candidate (same floor peek / crc swap / daemon routing / re-register / retain / post-step as any update);
  - equal or newer → untouched; removed by the operator → left removed;
  - member bytes vs `bundle.json` sha256 and the archive's own manifest identity (`check_archive_identity`) checked before anything registers.
- **`(built-in)`** — `AdapterRecord.built_in`, additive `serde(default)`, written `false` by every registration, set by `registry::mark_built_in` after the bundle's own registration. Per doyle's amendment it says where the CURRENT bytes came from: any non-bundle update re-registers and clears it. `install_source` is a separate axis and a bundle upgrade never rewrites it.
- **AUTO-SET SEAM (W7, releases#336)** — named marker at the upgrade site and in the REQ text: a newer member is applied unconditionally until `DaemonConfig.auto_classes` exists.
- **Doc** — docs-site self-update "Releases carry the built-in adapters".

## Measured while building

- **tar and the `./` prefix.** One release-shaped archive (`tar -czf b -C tree .`) on hfenduleam: recorded names `./`, `./a.spt`, `./bundle.json`. bsdtar 3.8.4 extracted the BARE `bundle.json` (rc 0); GNU tar 1.35 refused it (rc 2, "Not found in archive") and took `./bundle.json` (rc 0). So a name-addressed reader would behave differently per tar; the reader addresses no member by name — it extracts the whole archive and reads dirents (the `extract_release_archive` precedent). Row 2's negative control is the bare-name lookup among the RECORDED names, which fails on either tar.
- **A vacuous green, caught.** Row 1 first passed with a fixture that staged NO set at all (`staged_update()` needs a platform artifact file). Fixed; `stage()` now asserts the set is staged before any row runs.

## Evidence

REQ-BUNDLE-APPLY-MEMBERS minted in this commit (dispatch seed text verbatim + the two rulings appended); REQ-BUNDLE-RELEASE-ASSET `int` activated and discharged by the same int.

Verification on hfenduleam at this sha (doyle GO, box census before the legs):
- unit rows: `bundle_apply_tests` 5/5 (the four dispatch rows + doyle's set/clear row); spt-daemon/spt-runtime bundle rows 9/9; int `bundled_adapters_e2e` 1/1 (fresh node: both members registered + built-in + retained; newer claude-spt untouched byte-for-byte; bundle-less set silent; tampered bundle skipped loud, binary outcome identical).
- **negative controls (mutations, each reverted):** M1 verify a bundle-less set → row 1 alone RED; M2 upgrade on equal → row 3 alone RED; M3 skip the member sha → row 4 alone RED; M4 carry the mark across re-register → registry row + set/clear row RED.
- regression over the refactored add/update paths: 65/65 (spt adapter/update/floor/crc_swap units + adapter_fanout/peer/post_step/swap, docs_bundle, release_verify ints).
- `traceable-reqs check` 0, `cargo clippy --workspace --all-targets` 0, `xtask check` OK.

Known limit, stated: the clear rides the registration choke point. An adapter whose avenue replaces bytes outside core (delegated) without re-registering would keep a stale mark; both first members are gh_release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014j8UXzGXXsDsTJBYYUSCTC
