- Add build_p02_fixtures.py: IMU CSV → tlog conversion (RAW_IMU + ATTITUDE pairs, centidegrees→radians yaw) and orchestrator that runs gps-denied replay against Derkachi MP4 + generated tlog, verifying ≥1 record_type="estimate" in the FDR archive. - Extract run_gps_denied_replay + FDR-parent-dir helpers into sitl_replay_builder/_common.py; refactor build_p01_fixtures.py to import from _common (b78 tests preserved). - Add 20 unit tests under e2e/_unit_tests/fixtures/test_sitl_ replay_builder_p02.py covering AC-1..AC-5; total unit suite 686/686 passing (regression gate AC-6). - README updated to document FT-P-01 + FT-P-02 builders. - Advance autodev state: last_completed_batch=79, current_batch=80; prune verbose detail blob. Co-authored-by: Cursor <cursoragent@cursor.com>
7.7 KiB
Code Review Report
Batch: 79 — AZ-599 (FT-P-02 Derkachi fixture builder + b78 helper extraction) Date: 2026-05-17 Verdict: PASS
Findings
(none blocking)
Non-blocking notes
- Roll/pitch=0 simplification in synthesised ATTITUDE: acceptable for the Derkachi fixed-wing cruise data but unrealistic for aggressive manoeuvres. Documented as a Limitation in the README; re-visit if FT-P-04 (Derkachi F2F registration) or other manoeuvre-sensitive scenarios show fusion drift from this gap.
SCALED_IMU2→RAW_IMUpass-through (no unit conversion): the builder passes scaled accelerometer (mg) and gyro (mrad/s) values straight into RAW_IMU fields. Documented as a Limitation; if the SUT's tlog parser rejects, a units conversion pass needs to be added. Surfaced as a follow-up rather than fixed speculatively.- Disk-full incident during regression gate: not a code issue; surfaced to the user, who freed space, after which the full suite (686 tests) passed cleanly.
Findings Sweep
Phase 1 — Context Loading
Read the FT-P-02 scenario to confirm: it (a) skips when
sitl_replay_ready is false; (b) calls
fdr_reader.iter_records(fdr_root) to walk the FDR archive; (c)
filters for record_type == "estimate" and projects into
apd.FdrEstimate. Confirmed FT-P-02 does NOT call
wait_for_outbound (so the b78 schema doesn't apply). Read
data_imu.csv to confirm column names + units (10 Hz; ms
timestamps; SCALED_IMU2 in mg / mrad/s; GLOBAL_POSITION_INT.hdg
in centidegrees). Inspected the production
replay_input/auto_sync.py AC-13 contract for required tlog
message types (RAW_IMU + ATTITUDE) — this drove the choice of
which messages to pack per CSV row. Verified pymavlink>=2.4
already in pyproject.toml. Read the b78
build_p01_fixtures.py to identify the helper extraction
boundary (run_gps_denied_replay + write_observer_fixture had
no FT-P-01-specific code, ideal candidates).
Phase 2 — Spec Compliance
| AC | Coverage | Status |
|---|---|---|
| AC-1 (parse every row, one pair per row) | test_convert_imu_csv_writes_pair_per_row, test_convert_imu_csv_real_pymavlink_round_trip |
Covered |
| AC-2 (ValueError on missing cols / malformed numerics / empty CSV) | test_convert_imu_csv_empty_raises, test_convert_imu_csv_missing_required_column_raises, test_convert_imu_csv_malformed_numeric_raises, test_convert_imu_csv_missing_file_raises |
Covered (4 distinct error paths) |
| AC-3 (heading centideg → rad) | test_hdg_centideg_to_rad (5 parametric cases: 0, π/2, π, 3π/2, near-2π) |
Covered |
| AC-4 (build_p02 invokes replay + verifies ≥1 estimate) | test_build_p02_end_to_end_with_mocks, test_build_p02_propagates_verify_failure, test_verify_fdr_no_estimates_raises, test_verify_fdr_counts_estimates, test_verify_fdr_tolerates_malformed_lines |
Covered |
AC-5 (_common.py shared between b78 + b79) |
test_common_module_exports_used_by_b01 + b78 suite (24/24) still passes |
Covered |
| AC-6 (full unit-test suite passes) | 686/686 in 123 s (previous: 664) | Covered |
Phase 3 — Code Quality
- Single responsibility:
convert_imu_csv_to_tlogparses CSV + packs MAVLink (one cohesive concern: CSV row → tlog pair).verify_fdr_has_estimatesdoes one thing.build_p02_fixturesis the only orchestrator. Helper extraction into_common.pyremoved duplicate definitions cleanly — no copy-pasted code remains. - No suppressed errors: every JSON-decode in
verify_fdr_has_estimatesis wrapped in atry / except json.JSONDecodeErrorthat explicitlycontinues with a comment explaining the tolerance (real FDR archives can contain partial writes at the tail). Every CSV parse error wraps inValueErrorwith row context.subprocess.runusescheck=True. - AAA test discipline: all 20 new tests use
# Arrange / # Act / # Assert; omitted when redundant. - Comments: every public function has a docstring documenting
contract + raises; no narrating comments in function bodies.
The yaw conversion uses a named helper (
_hdg_centideg_to_rad) rather than an inline magic-formula comment. - Public boundary: b79 module does NOT import any
src/gps_denied_onboardsymbol (verified by grep). pymavlink imports are deferred to function bodies so the test factory injections don't need the real library available.
Phase 4 — Security
- No new credentials / secrets / network surfaces.
convert_imu_csv_to_tlogusescsv.DictReader— noeval, no shell injection. Numeric fields go throughint(float(s))so bad input raises ValueError rather than executing arbitrary code.verify_fdr_has_estimatesusesjson.loads(safe) and silently ignores malformed lines (documented as tolerance for partial writes; not a security risk because we never act on the data).subprocessinvocation uses a list-argumentcmd(no shell injection surface; same pattern as b78).
Phase 5 — Performance
convert_imu_csv_to_tlogis O(N) over CSV rows; for the Derkachi 4,900-row file → ~5 s wall-clock estimate (pymavlink packing is the bottleneck). Bounded by input size; no hidden quadratic behaviour.verify_fdr_has_estimatesis single-pass O(N) over JSONL lines._iter_imu_rowsvalidates the header once and then streams rows; doesn't materialize the entire CSV in memory until the orchestrator asks forlist(...)(could be optimized later if Derkachi data ever grows orders of magnitude larger; currently 4,900 rows is trivial).
Phase 6 — Cross-Task Consistency
- Builder shape parity with b78:
BuilderConfig/P02BuilderConfigdataclass;build_pXX_fixtures(cfg, *, _runner=..., _mavlink_writer_factory=..., _verify_fdr=...)signature; same underscore-prefixed dependency-injection convention. A future contributor reading both side-by-side will see the same pattern. _common.pyextraction validated by both: b78's existingtest_run_gps_denied_replay_builds_correct_cmd/test_run_gps_denied_replay_creates_fdr_parent_dir/test_run_gps_denied_replay_passes_extra_args/test_write_observer_fixture_schematests pass unchanged against the moved implementation. b79 addstest_common_module_exports_used_by_b01to lock in the import relationship.- No drift in
observer_<fc_kind>_<host>.jsonschema: same payload structure as b75/b78.
Phase 7 — Architecture Compliance
- Module placement: new files under
e2e/fixtures/sitl_replay_builder/(existing package from b78); new tests undere2e/_unit_tests/fixtures/. All registered intest_directory_layout.py. - No
src/gps_denied_onboardimports. - No new top-level dependencies — pymavlink already there from
b78/b76;
csvis stdlib;mathis stdlib. __init__.pynot modified — the b78 docstring already documents why we don't re-export symbols on the package namespace, and that's still the right choice for b79.- Refactor preserves backwards compatibility:
build_p01_fixturesre-importsrun_gps_denied_replay+write_observer_fixturefrom_common.py. Any caller (test or production) doingbp01.run_gps_denied_replay(...)still works because the symbol is present in the module namespace via thefrom … import …re-export.
Test Results
- New unit tests: 20 (5
convert_imu_csv_to_tlog+ 5_hdg_centideg_to_radparametric + 4verify_fdr_has_estimates- 4
build_p02_fixturesend-to-end + 1 export-relationship - 1 missing-file negative).
- 4
- Full
e2e/_unit_testssuite: 686 passed in 123 s (previous: 664 → +22 net = +20 new + +2 directory-layout entries). - No new linter errors (
ReadLintsclean on all touched files). - No regression in the b78 builder tests (24/24 still pass after the helper extraction).