# v0.3.2 — platform-safe `spt update fetch` + apply platform-guard + friendly apply message

JIT plan. **Origin (v0.3.1 field corruption, 2026-06-09):** `spt update fetch` on kitsubito (Linux) staged a single-platform `SignedRelease` (the Linux binary + a platform-LESS `ReleaseMetadata`). hfenduleam (Windows) restarted → its pump pulled kitsubito's staged release over P2P (the pump serves a staged `Single` as-is) → `spt update apply` verified it clean (signature ✓, SHA-256 ✓ — it IS a valid signed release) and swapped in a **Linux ELF as `spt.exe`** → "not a valid application" (bricked). **Recovered** from `spt.exe.old-5` (the PE backup the swap left); **kitsubito cache cleared** so it stops serving the cross-platform artifact to SPT_DEV.

## Root cause
- `ReleaseMetadata` (a single `SignedRelease`, release.rs:5) carries **no platform/triple identity** — only `{version:u64, channel, expires_at_ms, key_id, artifact_sha256, brain_ipc_version, broker_resource_abi}`. So a single-release apply is **platform-blind by construction**: nothing in verify (`plan_verified`) can reject a wrong-OS artifact.
- The platform-SAFE path already exists: `SignedUpdateSet` → `UpdateSetMetadata.artifact_for(triple)` → `plan_verified_update_set(running, set, platform, artifact, policy)` (update.rs:172) selects the recipient's platform via `release::current_platform()` (release.rs:174 — `x86_64-pc-windows-msvc` / `x86_64-unknown-linux-gnu`).
- The P2P pump serves a staged `Single` platform-blind (`propagate.rs` `StagedOffer::Single`, "legacy SignedRelease remains supported"). My REQ-UPD-7 `cmd_update_fetch` staged a `Single` (`cache.stage`), so it injected a platform-blind release into P2P serving.

## Fix set (REQ-UPD-8 + the message)

### 1. `spt update fetch` stages the multi-platform SET, not a single release
- **Maintainer/release side:** the release must host a signed `update-set.json` (a `SignedUpdateSet` covering every platform). `xtask release-publish` ALREADY constructs + signs a `SignedUpdateSet` (main.rs:799) and `stage_update_set`s it locally; extend the publish to **upload `update-set.json` as a release asset** (the v0.3.1 release had only per-platform `.release.json` → confirm it isn't uploaded today, then add it in release.yml / the publish step). It must also be reachable at `releases/{latest,tag/<tag>}/download/update-set.json`.
- **`cmd_update_fetch`:** download `update-set.json` (the signed set) + EVERY platform artifact it names (`UpdateArtifactMetadata.asset_name` per triple), verify via `plan_verified_update_set` for THIS node, then `cache.stage_update_set(&set, &artifacts)` (mirror xtask:813-822). Replaces the single-`SignedRelease` fetch.
- Result: the cache holds a platform-keyed set → local apply selects `current_platform()`, P2P re-serve offers the Set, each peer selects ITS OWN platform. Reuses REQ-UPD-6 → fully platform-safe end-to-end.

### 2. Defense-in-depth platform guard on apply (the safety net — must-have)
- Never let a platform-mismatched artifact apply, even via a legacy single path. In `apply_staged` (applyhost):
  - **Set path:** `plan_verified_update_set` already selects by `current_platform()` (an absent platform → reject) → inherently safe.
  - **Single path (legacy / any future):** stamp the target triple at stage time (a `platform` sidecar in the cache) and **refuse apply if it != `current_platform()`**; a `Single` with no platform stamp (pre-v0.3.2) → **refuse to apply** (fail-safe) with a "re-fetch on v0.3.2+" message. The P2P serve must also stop offering platform-blind `Single`s cross-platform (prefer Sets; or gate the Single offer on a matching requester platform).
- This guard ALONE would have prevented the v0.3.1 brick — so it ships regardless of #1.

### 3. (c) Friendly apply message
- `cmd_update_apply` prints `APPLIED:5` (the monotonic counter — release.rs has no semver in `ReleaseMetadata`). Change to:
  ```
  Updated spt-core to v0.3.2.
  Check out the changelog at https://www.github.com/SaberMage/spt-releases/releases
  ```
- Needs the SEMVER. Add `product_version: String` (e.g. "0.3.2") to `ReleaseMetadata` + `UpdateSetMetadata` — **additive serde field** (default "" → old releases parse clean). Maintainer populates it at publish from `workspace.package.version`. `apply` reads it: `Updated spt-core to v{product_version}.` Fallback when empty: `Updated spt-core (release {counter}).` The changelog line is static (append always).

## Traceability
- Mint **REQ-UPD-8** (platform-safe origin fetch + apply platform-guard). impl on the set-staging fetch + the `apply_staged` platform guard + the publish set-upload; unit on (a) fetch set/asset derivation, (b) apply REFUSES a platform-mismatched / unstamped single (the regression that bricked hfenduleam), (c) the message render (semver present + fallback + URL). product_version + message tag under REQ-UPD-8.

## Sequencing
mint REQ-UPD-8 → add `product_version` metadata field (release.rs, additive) → xtask publish emits + UPLOADS `update-set.json` + stamps product_version → `cmd_update_fetch` stages the set → `apply_staged` platform guard + friendly message → gates (build/test/clippy/`--no-default-features`/traceable-reqs/xtask check) → push dev-freeform → **CI BOTH runners** → bump 0.3.2 + CHANGELOG → FF main → tag v0.3.2 → user signs (`release-publish --version 6`).

## Verification (the real cross-platform test — the bug's exact shape)
- Linux node (kitsubito): `spt update fetch` → stages the SET (both platform artifacts).
- Windows peer (hfenduleam) pulls → gets the **Windows** artifact (platform selection) → `spt update apply` → prints "Updated spt-core to v0.3.2." → `spt.exe` is a valid Windows **PE (MZ)**, runs. **No cross-platform brick.**
- Guard test: stage a mismatched/unstamped single → apply **refuses** (not silent corruption).

## Immediate next-session start
1. Recovery already done — hfenduleam on working **0.3.0**, kitsubito cache cleared. **DO NOT use `spt update fetch` until v0.3.2 ships** (the cross-platform brick is live in v0.3.1).
2. Start at: mint REQ-UPD-8 → add `product_version` to `ReleaseMetadata`/`UpdateSetMetadata`.
3. Confirm whether release.yml uploads `update-set.json` today (v0.3.1 assets were per-platform `.release.json` only → likely add it).
4. (Note: brain-only apply worked for 0.3.0→0.3.1, so no broker-ABI concern this hop.)
