# Batch 77 Report — replay_mode helpers + 13 scenario stub rewires (cycle 1, batch 11 of test phase)

**Batch**: 77
**Date**: 2026-05-17
**Context**: Test implementation (greenfield Step 10 — Implement Tests)
**Tasks**: AZ-597 (3 cp) — 1 task (scenario stub cleanup bundle)
**Cycle**: 1
**Verdict**: COMPLETE — PASS (self-reviewed; see `reviews/batch_77_review.md`)

## Summary

Closes the offline FDR-replay path that AZ-594 (b74), AZ-595 (b75),
and AZ-596 (b76) opened. After those three batches, the only remaining
`NotImplementedError` stubs in the scenario suite were a grab-bag of
local `_resolve_*` / `_drive_*` / `_push_*` helpers duplicated across
13 scenario files. They all reduced to the same FDR-replay pattern —
either a no-op counter (frame sink, FC inbound emitter, IMU replay
driver) or a JSON read from `${E2E_SITL_REPLAY_DIR}/` (per-frame GT,
single-image observation, outage frames subdir).

This batch bundles those into one shared `runner/helpers/replay_mode.py`
module + rewires the 13 scenarios off their local stubs. After the
batch:

* `grep raise NotImplementedError` under `e2e/tests/` returns **zero**
  matches.
* Once the SITL replay fixture builder lands (separate ticket), every
  scenario becomes runnable end-to-end with no further per-scenario
  edits.
* Unit-test mode is unchanged — the b75 `sitl_replay_ready` skip
  gate keeps the loaders unreached when `E2E_SITL_REPLAY_DIR` is unset.

### AZ-597 — replay_mode helpers + 13 scenario rewires (3 cp)

* **`runner/helpers/replay_mode.py`** (new):
  * `NullFrameSink` — counter-only `FrameSink` (`frames_written: int`).
  * `NullFcInboundEmitter` — counter-only `FcInboundEmitter`
    (`samples_emitted: int`).
  * `default_frame_period_ms() -> int` + `DEFAULT_FRAME_PERIOD_MS = 33`
    (30 fps).
  * `load_replay_json(filename)` — generic JSON loader. Raises
    `FileNotFoundError` (env-unset / file-missing) or `ValueError`
    (malformed, file pointer included).
  * `resolve_replay_subdir(name)` — directory loader. Raises
    `FileNotFoundError` (env-unset / subdir-missing).
  * `imu_replay_noop(csv_path)` — explicit no-op; signature mirrors
    `imu_replay.ImuReplayer.replay` for future live-mode parity.
  * Single shared `_resolve_replay_root_or_raise(reason)` enforces
    the `E2E_SITL_REPLAY_DIR` semantics exactly once.
* **13 scenarios rewired** (all `_resolve_*` / `_drive_*` / `_push_*`
  stubs deleted):
  * `_resolve_frame_sink` → `NullFrameSink()` in: FT-P-01, FT-P-02,
    FT-P-04, FT-P-05, FT-P-07, FT-P-08, FT-P-09-AP, FT-P-09-iNav,
    FT-P-10, FT-P-11, FT-N-01, FT-N-02, FT-N-03, FT-N-04.
  * `_resolve_fc_inbound_emitter` → `NullFcInboundEmitter()` in:
    FT-P-02, FT-P-04, FT-P-10.
  * `_drive_imu_replay` → `imu_replay_noop(...)` in: FT-P-07, FT-N-02.
  * `_resolve_frame_period_ms` → `default_frame_period_ms()` in:
    FT-N-03, FT-N-04.
  * `_resolve_outage_injection_frames` → `resolve_replay_subdir("outage_frames")`
    in: FT-N-03.
  * `_resolve_gt_per_frame` → `load_replay_json("gt_per_frame.json")`
    + dataclass projection in: FT-N-01.
  * `_push_single_image_and_observe` → `load_replay_json("single_image_observation.json")`
    + tuple projection in: FT-P-03/14.
* **`e2e/_unit_tests/test_directory_layout.py`** — registers the new
  `runner/helpers/replay_mode.py` path.

## Out of scope (deferred)

* The actual SITL replay fixture builder (separate ticket — will
  populate `${E2E_SITL_REPLAY_DIR}/` with `gps_state.json`,
  `gt_per_frame.json`, `single_image_observation.json`,
  `outage_frames/`, `ekf_divergence_events.json`, etc.).
* Live MAVLink router / pymavlink plumbing (separate live-mode
  infrastructure ticket).

## Test Results

* New unit tests: **17** (2 null-sink, 2 null-emitter, 1
  frame-period, 2 imu-replay-noop, 6 load_replay_json, 4
  resolve_replay_subdir).
* Full `e2e/_unit_tests` suite: **626 passed in 127 s** (previous
  cumulative: 608 → +18 net = +17 new replay_mode tests + 1 new
  directory-layout parametrize entry).
* No new linter errors.
* `grep raise NotImplementedError` under `e2e/tests/` returns
  **zero** matches.

## State

* Spec moved: `_docs/02_tasks/todo/AZ-597_scenario_stub_cleanup.md`
  → `_docs/02_tasks/done/`.
* `_docs/_autodev_state.md` advanced to `last_completed_batch: 77`.
* `last_cumulative_review` remains `batches_73-75`; next K=3
  cumulative review fires at the end of batch 78.
