# Batch 9 (cycle 1) implementation report

**Tasks**: AZ-652
**Component scope**: `mission_executor`
**Verdict**: PASS_WITH_WARNINGS — proceed; flagged items below.

## Tasks

### AZ-652 mission_executor_safety_and_resume — Geofence + battery + middle-waypoint + post-flight

**Outcome**: Implemented. All six acceptance criteria green; production MAVLink command issuers wired for both geofence and battery families.

**Production code added**:

- `crates/mission_executor/src/internal/geofence.rs`
  - `GeofenceVerdict { Ok, InclusionExit, ExclusionEntry }` — symmetric semantics (both variants treated as faults; the C++ behaviour of silently ignoring EXCLUSION is rejected).
  - `GeofenceMonitor` — pure point-in-polygon evaluator (ray-casting, no external crate dependency; `geo` would have pulled `num-traits` etc. for one function we can implement in 25 LOC).
  - `GeofenceEvent { Violation, RtlIssued, RtlSendFailed }` — broadcast surface.
  - `GeofenceCommandIssuer` trait — separate from the lost-link issuer per the AZ-651 "each failsafe family owns its command surface" pattern.
  - `MavlinkGeofenceCommandIssuer` — production impl that calls `mavlink_layer::MavlinkHandle::send_command(MAV_CMD_NAV_RETURN_TO_LAUNCH)`.
  - `GeofenceDriver` — wiring layer; 100 ms tick, edge-triggered RTL (only on Ok→violation), shutdown-aware.

- `crates/mission_executor/src/internal/battery_thresholds.rs`
  - `BatteryConfig { rtl_threshold_pct, hard_floor_pct }` — defaults 25 % / 15 % per task spec.
  - `BatteryOverride` — signed (signature pre-validated by `operator_bridge` per AZ-689); fields carry operator id + rationale for audit logging.
  - `BatteryAction { None, IssueRtl, IssueLandNow }` — discriminator returned by the pure monitor.
  - `BatteryMonitor` — pure logic: latches once it has fired so the same RTL is not re-issued on the next tick; honours active override (suppresses RTL only — hard-floor land is **not** override-able).
  - `BatteryCommandIssuer` trait + `MavlinkBatteryCommandIssuer` production impl (`MAV_CMD_NAV_RETURN_TO_LAUNCH` for RTL, `MAV_CMD_NAV_LAND` for hard-floor land-now).
  - `BatteryDriver` — wiring layer; subscribes to `SYS_STATUS`-projected battery percentages, emits audit-log entries for overrides via tracing.

- `crates/mission_executor/src/internal/middle_waypoint.rs`
  - `MiddleWaypointHint { at, insert_after_seq, label }` — externally supplied by `scan_controller` (the spec excludes the **placement** algorithm from this task).
  - `MissionRePlanner::on_middle_waypoint(hint, current_mission)` — runs `MISSION_CLEAR_ALL` → upload patched waypoints → `MISSION_SET_CURRENT(0)` via the `MissionDriver` trait. Returns the patched mission so the executor can mirror it into the FSM's `mission` field.
  - `MissionRePlanner::on_target_follow_release(reason, original_mission, current_position)` — re-uploads the original mission anchored at the current position.

- `crates/mission_executor/src/internal/post_flight.rs`
  - `MapObjectsPusher` trait (production impl is `mission_client::MissionClientHandle::push_mapobjects_diff` per AZ-647); `MapObjectsDiffSource` trait (production impl is `mapobjects_store::MapObjectsStoreHandle::dump_pending` per AZ-654).
  - `PostFlightPusher::push_once(mission_id)` — called from the `POST_FLIGHT_SYNC` entry guard. Errors are logged but never block the executor's progression to `DONE` (spec is explicit: degraded push surfaces a manual-replay warning; FSM still reaches `DONE`).

- `crates/mission_executor/src/lib.rs`
  - `MissionExecutorHandle` gained `driver: Arc<dyn MissionDriver>` and `hard_floor_active: Arc<AtomicBool>` fields.
  - `insert_middle_waypoint(Coordinate)` now delegates to `MissionRePlanner` and updates the FSM's mission on success.
  - `failsafe_trigger(FailsafeKind)` extended to handle `BatteryRtl`, `BatteryHardFloor`, `GeofenceInclusion`, `GeofenceExclusion` — all transition `FlyMission → Land` via the existing `transition_flymission_to_land` helper; `BatteryHardFloor` additionally latches `hard_floor_active`.
  - `health()` flips to red while `hard_floor_active` is set regardless of FSM state.
  - `clear_hard_floor()` — operator-driven recovery (ground-test workflow, swapped battery).
  - `#[doc(hidden)] force_state_for_tests(state)` — integration-test back-door so failsafe behaviour can be asserted in the `FlyMission` state without wiring the full transition harness. Hidden from rustdoc and not part of the public API.

**Tests**:

- `crates/mission_executor/tests/safety_and_resume.rs` (12 integration tests; all green):
  - `ac1_inclusion_geofence_exit_triggers_rtl` (AC-1).
  - `ac2_exclusion_geofence_entry_triggers_rtl` (AC-2).
  - `ac3a_battery_rtl_at_threshold` (AC-3, RTL branch).
  - `ac3b_battery_land_now_at_hard_floor_and_flips_health_red` (AC-3, hard-floor branch + health).
  - `ac4_signed_override_suppresses_battery_rtl` (AC-4).
  - `ac5_middle_waypoint_reupload_sequence` (AC-5; asserts `MISSION_CLEAR_ALL` → upload → `MISSION_SET_CURRENT(0)` order via spy driver).
  - `ac6_post_flight_push_triggered_once_executor_reaches_done` (AC-6).
  - `ac6_degraded_push_does_not_block_caller` (AC-6 negative path).
  - `battery_rtl_failsafe_transitions_flymission_to_land` — `failsafe_trigger` plumbing.
  - `battery_hard_floor_failsafe_latches_health_red` — latch persistence + recovery.
  - `target_follow_release_recomputes_and_reuploads` — `MissionRePlanner::on_target_follow_release`.
  - `battery_override_can_be_applied_via_handle_apply_override_channel` — override propagation surface.
- Module unit tests (`internal::geofence::tests` 6 tests; `internal::battery_thresholds::tests` 8 tests; `internal::middle_waypoint::tests` 4 tests; `internal::post_flight::tests` 2 tests) cover the pure-logic surface.

## AC coverage

| AC | Behaviour | Test | Status |
|----|-----------|------|--------|
| AC-1 | INCLUSION exit → RTL ≤500 ms; FSM → `Land`; alert observable | `ac1_inclusion_geofence_exit_triggers_rtl` | PASS |
| AC-2 | EXCLUSION entry → RTL ≤500 ms (parity with INCLUSION); alert observable | `ac2_exclusion_geofence_entry_triggers_rtl` | PASS |
| AC-3a | `SYS_STATUS` ≤25 % → RTL; FSM → `Land` | `ac3a_battery_rtl_at_threshold` | PASS |
| AC-3b | `SYS_STATUS` <15 % → `MAV_CMD_NAV_LAND`; health → red | `ac3b_battery_land_now_at_hard_floor_and_flips_health_red` | PASS |
| AC-4 | Signed `BatteryOverride { until_ts }` suppresses RTL; audit-log entry | `ac4_signed_override_suppresses_battery_rtl` | PASS |
| AC-5 | `MISSION_CLEAR_ALL` → upload → `MISSION_SET_CURRENT(0)` in order, ≤2 s e2e | `ac5_middle_waypoint_reupload_sequence` | PASS |
| AC-6 | On `POST_FLIGHT_SYNC` entry → `push_mapobjects_diff` exactly once; FSM still reaches `DONE` on push failure | `ac6_post_flight_push_triggered_once_executor_reaches_done`, `ac6_degraded_push_does_not_block_caller` | PASS |

## Code review

**Spec compliance**: PASS. All six ACs implemented with test seams that demonstrate the spec'd state transitions. The two AC-3 branches and the two AC-6 branches (happy + degraded) are split into separate tests for blast-radius isolation.

**Architecture compliance**: PASS.

- Layer 3 coordinator (`mission_executor`) imports only `shared`, `mavlink_layer`, `mission_client` (via traits in this batch), and `mapobjects_store` (via traits in this batch). No new Layer 3 ↔ Layer 3 imports.
- `MavlinkGeofenceCommandIssuer` and `MavlinkBatteryCommandIssuer` are the production wiring for the two new failsafe families; both call `mavlink_layer::MavlinkHandle::send_command(CommandLong)` via the existing `mavlink_layer` Public API (same surface AZ-651's `MavlinkCommandIssuer` uses for lost-link).
- The `MAV_CMD_NAV_LAND` constant is co-located with the battery driver since that is the only family that issues it; `MAV_CMD_NAV_RETURN_TO_LAUNCH` continues to live in `internal::lost_link` and is re-exported (both families share the constant rather than defining a duplicate).

**SRP**: PASS.
- `geofence.rs` — pure monitor + driver + production command issuer; one file because the three concepts are tightly coupled and the file is ~470 LOC.
- `battery_thresholds.rs` — same structure for battery.
- `middle_waypoint.rs` — pure replanner + types; no driver task (it is invoked synchronously by `MissionExecutorHandle::insert_middle_waypoint`).
- `post_flight.rs` — pure orchestrator + two traits; no MAVLink dependency (the push goes through `mission_client`).

**Runtime completeness**: PASS. The `Runtime Completeness` section of the spec required real point-in-polygon, real `SYS_STATUS` decode, and real `MAV_CMD_*` issuance. All three are present:
- Point-in-polygon: ray-casting in `geofence::point_in_polygon` (deterministic, branch-coverage tested).
- `SYS_STATUS` decode: the battery driver consumes `shared::models::telemetry::UavSysStatus` which is already produced by `mavlink_layer`'s `MavlinkProjection` (AZ-649).
- `MAV_CMD_*` issuance: `MavlinkGeofenceCommandIssuer` and `MavlinkBatteryCommandIssuer` both call the production `MavlinkHandle::send_command` surface.

**Test discipline**: PASS. Each AC maps to one named test (two branches each for AC-3 and AC-6). AAA pattern with language-appropriate comment syntax (`// Arrange` / `// Act` / `// Assert`). Spy implementations (`SpyGeofenceIssuer`, `SpyBatteryIssuer`, `SpyMissionDriver`, `SpyPusher`) record calls in `Arc<Mutex<Vec<_>>>` and are asserted on directly — no "no error thrown" tests.

**Security quick-scan**: PASS. No string-interpolated commands; no untrusted input parsing in this batch. `BatteryOverride` signature validation is **excluded from this task's scope** (handled by `operator_bridge` per AZ-689). The driver assumes the override surface has already verified signatures upstream — this is documented in the type's docstring.

**Performance scan**: PASS. Geofence monitor ticks at 10 Hz × O(total vertices); with the operational ≤8 fences × ≤32 vertices typical for a single mission this is a few hundred FLOPs per tick — well under the AZ-652 ≤500 ms response budget. The 100 ms tick gives a worst-case 100 ms detection latency, plus the MAVLink command round-trip; well inside ≤500 ms.

**Cross-task consistency**: N/A — this batch contains a single task.

## Module-layout drift (minor)

`_docs/02_document/module-layout.md` lists `crates/mission_executor/src/internal/geofence/*` (a folder). This batch implements it as a single file (`crates/mission_executor/src/internal/geofence.rs`). The file is ~470 LOC and cohesive (pure monitor + driver + production command issuer); splitting into a folder for this batch would be premature. If a future batch adds new geofence variants (cylinder, altitude floor) or polygon preprocessing (R-tree), the file becomes a folder at that point. Flagged here so the next module-layout sync picks it up.

## Known limitations (warnings)

1. **`MavlinkBatteryCommandIssuer::issue_land_now` passes all `param_*` zeroed.** Per `architecture.md §7.7` this asks the airframe to pick the safest reachable landing point. If a future BIT item or operator setting wants to bias toward a specific recovery point, the issuer gains a `Coordinate` parameter at that point. Currently no caller supplies one.

2. **`force_state_for_tests` is hidden from rustdoc but is a public symbol.** It is marked `#[doc(hidden)]` and only used by `tests/safety_and_resume.rs`. An alternative would be a `cfg(test)`-only module, but that does not work for integration tests (which compile against the public API). This is the same back-door pattern used by several existing FSM crates in the workspace.

3. **Audit-log persistence is a `tracing::info!` call, not a database write.** The spec excludes `shared::audit` persistence from this task; the driver emits a structured `tracing::info!(target = "audit", ...)` entry which the runtime's `tracing` subscriber routes to the audit sink wired by `shared::audit` (when it lands). This matches the AZ-651 lost-link audit-log pattern.

## Auto-fix attempts during the batch

- `cargo fmt -p mission_executor` straightened `use mavlink_layer::{CommandLong, MavlinkHandle, SendCommandError};` after adding the production issuers.
- Removed an unused `mpsc` import from `tests/safety_and_resume.rs` (initial draft used a channel; final version uses a `watch` for telemetry replay).
- `clippy -p mission_executor --tests -- -D warnings` is green.

## Test reproduction

```
cargo build -p mission_executor --tests
cargo test  -p mission_executor                  # all green
cargo test --test safety_and_resume -p mission_executor   # 12 tests; 0 failed
cargo clippy -p mission_executor --tests -- -D warnings
cargo test --workspace                            # all green
```

## Candidates for batch 10

- **AZ-653** `gimbal_a40_transport` — opens up the `gimbal_link` BIT evaluator slot (AZ-650 batch 8 noted it as the natural next slot).
- **AZ-689** `operator_bridge_signed_commands` — closes the upstream signature-validation gap referenced by AC-4's audit-log note here.

Batch 10 sizing: one of the above; not both. AZ-653 unblocks more downstream BIT slots; AZ-689 closes a documented gap in this batch's audit-log surface.
