orchestrator-sm: discard staged image on update preemption; id-check Restored

A Required corruption that preempts an in-flight update now emits DiscardStaged so the staged image is not orphaned. Recovering ignores a Restored whose id is not the current recovery target, preventing a displaced episode from mis-crediting the wrong component. Adds two tests.
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs
index 7226787..cecf4b4 100644
--- a/services/orchestrator/sm/src/lib.rs
+++ b/services/orchestrator/sm/src/lib.rs
@@ -448,11 +448,29 @@
                     ctx.emit(Effect::DiscardStaged);
                     Outcome::Transition(State::Ready)
                 }
+                Event::CorruptionDetected(id) => {
+                    let outcome = self.handle_corruption(*id, ctx);
+                    // Only a *preemption* (transition out to recovery) orphans
+                    // the staged image. An `Isolable`/`Cascading` corruption
+                    // returns `Handled` and the update continues untouched, so
+                    // its staged image must survive — do not discard then.
+                    if matches!(outcome, Outcome::Transition(State::Recovering(_))) {
+                        ctx.emit(Effect::DiscardStaged);
+                    }
+                    outcome
+                }
                 _ => Outcome::Super,
             },
 
             State::Recovering(failed) => match event {
-                Event::Restored(_) => {
+                Event::Restored(id) => {
+                    // A `Restored` for a component other than the current
+                    // recovery target (e.g. an episode displaced by a later
+                    // corruption) must not be credited to this recovery. Drop
+                    // it; the eventual re-walk re-verifies every component.
+                    if *id != failed {
+                        return Outcome::Handled;
+                    }
                     // Count this attempt against the specific component in
                     // recovery, not a global budget (CSA: exhaustion is
                     // per-device). The recovery target is the state's payload,
diff --git a/services/orchestrator/sm/src/tests.rs b/services/orchestrator/sm/src/tests.rs
index af0153f..cd79f08 100644
--- a/services/orchestrator/sm/src/tests.rs
+++ b/services/orchestrator/sm/src/tests.rs
@@ -1306,3 +1306,49 @@
     assert_eq!(orch.state(), State::Locked);
     assert!(plat.recorded.contains(&Effect::LatchLockdown));
 }
+
+/// GAP 1 (red): a `Required` corruption of another device while an update is in
+/// flight preempts the update but leaves the staged image dangling. Correct
+/// behavior emits `DiscardStaged` when leaving `Updating` for recovery.
+#[test]
+fn corruption_during_update_discards_staged() {
+    let (effects, state) = drive(
+        passive_required(&[C0, C1]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::VerificationPassed(C1), // → Ready
+            Event::UpdateRequest,          // → Updating (AuthenticateUpdate, StageUpdate)
+            Event::CorruptionDetected(C1), // Required corruption preempts the update
+        ],
+    );
+    assert_eq!(state, State::Recovering(C1));
+    assert!(
+        effects.contains(&Effect::DiscardStaged),
+        "leaving Updating for recovery must discard the staged image",
+    );
+}
+
+/// GAP 2 (red): a second `Required` corruption while already recovering clobbers
+/// the single `Recovering` slot, and because `Restored` is id-blind a restore
+/// for the *displaced* target is mis-credited to the new one. Correct behavior:
+/// a `Restored` whose id is not the recovery target does not advance recovery.
+#[test]
+fn restored_for_wrong_component_does_not_advance_recovery() {
+    let (_effects, state) = drive(
+        passive_required(&[C0, C1]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::VerificationPassed(C1), // → Ready
+            Event::CorruptionDetected(C0), // → Recovering(C0)
+            Event::CorruptionDetected(C1), // clobbers → Recovering(C1)
+            Event::Restored(C0),           // restore of the *displaced* target
+        ],
+    );
+    assert_eq!(
+        state,
+        State::Recovering(C1),
+        "a Restored for a non-target component must not be credited to the current recovery",
+    );
+}