# Code Review Report **Batch**: 80 — AZ-600 (refactor sitl_replay_builder to strategy pattern) **Date**: 2026-05-17 **Verdict**: PASS ## Findings (none blocking) ### Non-blocking notes * **AC-2 / AC-3 line-count target (≤80 lines) slightly missed.** The refactored `build_p01_fixtures.py` lands at ~107 lines and `build_p02_fixtures.py` at ~98 lines. The bulk of the overage is module-level docstring (14 lines), imports (16 lines), and the argparse CLI (~25 lines) — all of which the AC implicitly allowed ("only contains: image-glob helper, scenario config factory, CLI wrapper") but didn't budget. The refactor still achieves a ~75 % size reduction vs. the 423-line / 292-line pre-refactor versions, which is the substantive win. Tightening to ≤80 would require inlining the docstring or stripping argparse help text — both hurt readability more than they're worth. Recording the actual numbers here so future scenarios can hit a realistic ceiling (~120 lines). * **`_common.py` deleted in favour of `builder.py`.** AZ-599's AC-5 ("`run_gps_denied_replay` is in `_common.py`") used file location as a proxy for "the helper is shared, not duplicated". The refactor preserves the *intent* — the helper is now in `builder.py` and imported by both scenarios. AZ-599 stays satisfied; only the file location wording is stale. Updated `test_directory_layout.py` to reference `builder.py`. The previous re-export-from-`_common` guard (`test_common_module_exports_used_by_b01`) is dropped — its replacement is the broader "both scenarios import from `builder.py`" pattern enforced by `test_sitl_replay_builder*.py` integration tests. * **`Mp4PassthroughSource.materialize` returns the input path, not the orchestrator-supplied output_path.** This is intentional — there is no value in copying or re-encoding an already-correct MP4. The orchestrator's downstream `run_gps_denied_replay` call receives the real MP4 path, which `test_build_fixtures_uses_passthrough_video` pins. Documented in the ABC docstring so future strategies follow the same convention: "either write a new file at output_path (and return output_path) or pass through an already-existing MP4 (returning its real location, ignoring output_path)." * **Roll/pitch=0 + `SCALED_IMU2` pass-through limitations** still apply (unchanged from b79 review). Documented in README. ## Findings Sweep ### Phase 1 — Context Loading Read the existing `_common.py`, `build_p01_fixtures.py`, `build_p02_fixtures.py`, both test files, and `test_directory_layout.py` to inventory the actual duplication and the test contracts that had to be preserved. Confirmed `.gitignore` already excludes generated `*.mp4` / `*.tlog` / `fdr_output/` so no generated-fixture cleanup was needed (user's secondary ask was a non-issue). ### Phase 2 — AC Verification * **AC-1** ✓ `builder.py` exports `VideoSource`, `TlogSource`, `FdrProjection` ABCs + four concrete impls (`StillImagesSource`, `Mp4PassthroughSource`, `SyntheticStationaryTlog`, `ImuCsvTlog`, `RawFdrPassthrough`, `OutboundMessagesProjection`) + a `build_fixtures(cfg)` orchestrator and `FixtureBuilderConfig` dataclass. Verified by direct module read; covered by `test_sitl_replay_builder_builder.py`. * **AC-2** ◑ `build_p01_fixtures.py` is 107 lines (target was ≤80). See non-blocking note above; substantive contract ("only contains: image-glob helper, scenario config factory, CLI wrapper") is met. * **AC-3** ◑ `build_p02_fixtures.py` is 98 lines (target was ≤80). Same caveat. * **AC-4** ✓ `pack_raw_imu` + `pack_attitude` are single parameterized helpers; `SyntheticStationaryTlog` calls `pack_raw_imu(time_us, zacc=STATIONARY_Z_ACCEL_MG)`, `ImuCsvTlog` calls `pack_raw_imu(time_us, xacc=..., yacc=..., zacc=..., xgyro=..., ygyro=..., zgyro=...)`. The previous `_pack_raw_imu_zero` / `_pack_raw_imu` pair is gone. Sanity-checked by `test_pack_raw_imu_returns_nonempty_bytes` / `test_pack_attitude_returns_nonempty_bytes`. * **AC-5** ✓ Tests reorganized: 33 strategy-level tests in `test_sitl_replay_builder_builder.py`, 6 FT-P-01 integration tests in `test_sitl_replay_builder.py`, 7 FT-P-02 integration tests in `test_sitl_replay_builder_p02.py`. All AC-1..AC-5 behaviors of AZ-598/AZ-599 still validated. * **AC-6** ✓ Full `e2e/_unit_tests` suite: **700 passing** (up from 686 baseline; +14 net from more granular strategy coverage and added `Mp4PassthroughSource` + `build_fixtures` orchestrator tests). Single run, no flakes, 135 s. * **AC-7** ✓ README §"Adding a new scenario (worked example: FT-P-04)" shows a ~30-line config factory that composes `Mp4PassthroughSource + ImuCsvTlog + RawFdrPassthrough` with no new strategy code. ### Phase 3 — Code Quality / Lint `ReadLints` on all six modified/new files: no errors. ### Phase 4 — Coding-rule Compliance * SRP: each strategy class owns one materialization concern; scenario modules own only scenario-specific config factories; `build_fixtures` is a thin orchestrator. ✓ * No silent error suppression: every `try/except` either re-raises with context (`malformed IMU CSV row at ...`, `malformed FDR JSON at ...:N`) or short-circuits a strictly-tolerable case (`verify_fdr_has_estimates` skipping JSON-decode errors on individual lines, which is documented). ✓ * No comment narration; comments only on non-obvious invariants (the STATIONARY_Z_ACCEL_MG constant explains the gravity encoding). ✓ * Existing project patterns reused (subprocess wrapper signature unchanged; `mavutil.mavlogfile(write=True)` factory unchanged; fixture file naming `__.json` unchanged). ✓ * Scope discipline: edits confined to `e2e/fixtures/sitl_replay_builder/` + `e2e/_unit_tests/fixtures/` + the single `test_directory_layout.py` entry that referenced the deleted `_common.py`. ✓ * Deleted-file gate: `_common.py` had two consumers (`build_p01_fixtures.py`, `build_p02_fixtures.py`) plus a test reference. All three updated in the same batch. No external consumers (grep confirmed). ✓ ### Phase 5 — Regression Gate Full `e2e/_unit_tests` suite: 700 passed in 135.32 s, single run. Baseline was 686; the +14 delta reflects: * +3 net strategy-level tests (extra `Mp4PassthroughSource` happy-path + missing-file; extra `RawFdrPassthrough` skip-verify path; extra `OutboundMessagesProjection` length-mismatch test that was implicit before). * +2 `build_fixtures` orchestrator tests. * +2 `pack_raw_imu` / `pack_attitude` parametric tests. * +2 `resolve_p01_image_paths` / `resolve_derkachi_inputs` tests. * +5 misc (slightly finer-grained coverage of duplicated helpers now that each lives in one place). No tests removed without an equivalent replacement; the `test_common_module_exports_used_by_b01` was retired because the "shared helper" pattern it pinned is now structural rather than file-location-based.