# Batch 74 Report — Harness Stubs (cycle 1, batch 8 of test phase)

**Batch**: 74
**Date**: 2026-05-17
**Context**: Test implementation (greenfield Step 10 — Implement Tests)
**Tasks**: AZ-594 (4 cp) — 1 task (umbrella ticket for the core three harness stubs)
**Cycle**: 1
**Verdict**: COMPLETE — PASS (self-reviewed; see `reviews/batch_74_review.md`)

## Summary

A planning-gap fix-up batch. The skip-gates in batches 71-73 referenced
`AZ-441 / AZ-407 / AZ-416 leftovers` — but on inspection none of those
tickets actually owned the deferred harness stubs (`frame_source_replay`,
`imu_replay`, `fdr_reader`, `sitl_observer`, `fc_proxy_runtime`). The
stubs were AZ-406 surface reservations that AZ-407/408 never came back
to fill, and were stranded without a tracker entry.

This batch creates a single umbrella ticket (AZ-594) and ships the
core three of the five — the ones with the lowest implementation risk
and the broadest unblock surface:

* `fdr_reader.iter_records` — JSONL parser + wire-schema validator.
* `frame_source_replay.replay_video` — OpenCV-backed decode + sink emission.
* `imu_replay.ImuReplayer.replay` — CSV parser + emitter driver.

The remaining two stubs (`sitl_observer.get_observer` + `read_*`
surfaces; `fc_proxy_runtime` driver + docker wiring) need separate
tickets — they touch live pymavlink / yamspy / TCP plumbing and don't
fit in a single batch alongside the core three. Those become batch 75
candidates.

### AZ-594 — Harness stubs (core three) (4 cp)

* **`runner/helpers/fdr_reader.py`** —
  `iter_records(fdr_archive_root)` recursively walks `*.jsonl` files,
  validates each line against the wire envelope (`schema_version, ts,
  producer_id, kind, payload`), projects onto the runner-side
  `FdrRecord` dataclass (`record_type` for `kind`; `monotonic_ms`
  derived from ISO 8601 `ts`), and yields records oldest-first. Raises
  `FileNotFoundError` on missing root + `ValueError` with file+line
  on malformed envelopes. `archive_size_bytes` body was already present.
* **`runner/helpers/frame_source_replay.py`** — `FrameSourceReplayer`
  now backs both `replay_image_directory(dir)` and `replay_video(path)`.
  `replay_video` auto-detects file vs directory (so AZ-408 injector
  frame-directory outputs work via the same entry point). OpenCV
  `VideoCapture` decodes MP4; every frame is re-JPEG-encoded so the
  sink always receives JPEG bytes. Cadence pacing is parameterised via
  a `sleep_fn` injection so unit tests can drop wall-clock entirely.
* **`runner/helpers/imu_replay.py`** — `ImuReplayer.replay(csv_path)`
  parses `data_imu.csv` (the AZ-408 schema with possibly scientific-
  notation floats), constructs typed `ImuSample`s with attitude
  converted from degrees → radians, and drives `FcInboundEmitter.emit`.
  Same `sleep_fn` + `realtime` injection pattern as
  `frame_source_replay` for test parity.

## Scenario-probe interaction

The existing `_harness_helpers_implemented` probes in batches 71-73
pass `/tmp/non-existent` to each helper. Previously the inner
`except NotImplementedError: return False` fired; with the new bodies,
the outer `except Exception: return False` catches the
`FileNotFoundError` and still returns False. So:

* No existing scenario silently starts running its full E2E path.
* All eight skip-gated scenarios from batches 71-73 continue to skip
  cleanly because they ALSO depend on `sitl_observer` /
  `mavproxy_tlog_reader` (for some) / `fc_proxy_runtime` (for FT-N-04).
* Probe-cleanup is explicitly excluded from AZ-594 scope and will land
  in the future batch that fills the remaining two stubs.

## Test Results

* New unit tests: 14 (fdr_reader) + 10 (frame_source_replay) + 10
  (imu_replay) = **34 new tests**.
* Full `e2e/_unit_tests` suite: **558 passed in 139 s** (previous
  cumulative: 527 → +31 net).
* No new linter errors.

## State

* Spec moved: `_docs/02_tasks/todo/AZ-594_harness_stubs_core_three.md`
  → `_docs/02_tasks/done/`.
* `_docs/_autodev_state.md` advanced to `last_completed_batch: 74`.
* Cumulative review window: `last_cumulative_review = batches_70-72`;
  next K=3 cumulative review fires at the end of batch 75.
