# Scenario stub cleanup (replay_mode helpers + 13 rewires)

**Task**: AZ-597_scenario_stub_cleanup
**Name**: Add `runner/helpers/replay_mode.py` + rewire 13 scenarios off local `_resolve_*` / `_drive_*` / `_push_*` stubs
**Description**: After AZ-594/595/596 landed the three core harness helpers, sitl_observer, and the fc_proxy_runtime driver, the last unimplemented layer is a grab-bag of local per-scenario stubs that all share the same FDR-replay no-op pattern. Bundle them into one shared `runner/helpers/replay_mode.py` module so the offline FDR-replay path is end-to-end executable once the SITL replay fixture builder lands.
**Complexity**: 3 points
**Dependencies**: AZ-594, AZ-595, AZ-596
**Component**: Blackbox Tests / Test Infrastructure (epic AZ-262)
**Tracker**: AZ-597
**Epic**: AZ-262 (E-BBT)

## Problem

Despite the AZ-594/595/596 arc, 13 scenarios still carry local
`_resolve_*` / `_drive_*` / `_push_*` `NotImplementedError` stubs:

| Stub | Scenarios |
|------|-----------|
| `_resolve_frame_sink()` | 13 (FT-P-01/02/04/05/07/08/09-AP/09-iNav/10/11, FT-N-01/02/03/04) |
| `_resolve_fc_inbound_emitter(fc_adapter[, host])` | 3 (FT-P-02/04/10) |
| `_drive_imu_replay(csv_path)` | 2 (FT-P-07, FT-N-02) |
| `_resolve_frame_period_ms()` | 2 (FT-N-03/04) |
| `_resolve_outage_injection_frames()` | 1 (FT-N-03) |
| `_resolve_gt_per_frame(report)` | 1 (FT-N-01) |
| `_push_single_image_and_observe(...)` | 1 (FT-P-03/14) |

These are unreachable today (the b75 `sitl_replay_ready` gate skips
before they're called) so this cleanup can land safely under the
unit-test regression gate. The value: once the SITL replay fixture
builder ships, scenarios become runnable with no further per-scenario
edits.

## Surfaces (`runner/helpers/replay_mode.py`)

* `NullFrameSink` — implements `FrameSink` protocol. `write_frame`
  is a counter; exposes `frames_written: int`.
* `NullFcInboundEmitter` — implements `FcInboundEmitter` protocol.
  `emit` is a counter; exposes `samples_emitted: int`.
* `DEFAULT_FRAME_PERIOD_MS = 33` + `default_frame_period_ms() -> int`.
* `load_replay_json(filename: str) -> dict | list` — reads
  `${E2E_SITL_REPLAY_DIR}/<filename>`. Raises `FileNotFoundError`
  when env var unset OR file missing; `ValueError` with file pointer
  on malformed JSON.
* `resolve_replay_subdir(name: str) -> Path` — returns
  `${E2E_SITL_REPLAY_DIR}/<name>/`. Raises `FileNotFoundError` when
  env var unset OR directory missing.
* `imu_replay_noop(csv_path: Path) -> None` — no-op stand-in for the
  per-scenario `_drive_imu_replay` (IMU is pre-baked into the FDR
  archive in replay mode; the CSV path is preserved as a parameter
  for diagnostic logging only).

## Per-scenario rewire pattern

```python
# Before:
def _resolve_frame_sink():
    raise NotImplementedError(...)

# After:
from runner.helpers.replay_mode import NullFrameSink
def _resolve_frame_sink():
    return NullFrameSink()
```

Same shape for the other six helpers. For the two scenarios that need
scenario-specific JSON (`_resolve_gt_per_frame`,
`_push_single_image_and_observe`), they call
`load_replay_json("gt_per_frame.json")` /
`load_replay_json("single_image_observation.json")` and project the
result into their scenario-local dataclass.

## Acceptance Criteria

**AC-1**: `NullFrameSink.write_frame` and `NullFcInboundEmitter.emit`
are pure counters.

**AC-2**: `load_replay_json` raises `FileNotFoundError` (env unset or
file missing) and `ValueError` (malformed JSON with file pointer).

**AC-3**: `resolve_replay_subdir` raises `FileNotFoundError` (env
unset or subdir missing).

**AC-4**: `default_frame_period_ms()` returns 33.

**AC-5**: All 13 scenarios have local `_resolve_*` / `_drive_*` /
`_push_*` stubs deleted and import from `runner.helpers.replay_mode`.

**AC-6**: ≥6 unit tests on `replay_mode.py`.

**AC-7**: Full e2e unit-test suite passes (regression gate).

## Out of Scope

* The actual SITL replay fixture builder.
* Live MAVLink router / pymavlink plumbing.

## Files Touched

* `e2e/runner/helpers/replay_mode.py` (new)
* `e2e/_unit_tests/helpers/test_replay_mode.py` (new)
* `e2e/_unit_tests/test_directory_layout.py` (register new module)
* 13 scenario files under `e2e/tests/{positive,negative}/`
