Opens E-DEMO-REPLAY (AZ-265): the two C8 strategies that let the upcoming compose_replay (AZ-401) and gps-denied-replay CLI (AZ-402) run the production C1-C5 pipeline against a recorded (.tlog, video) pair without touching live FC I/O. AZ-400 lands the contract ReplaySink Protocol (emit + close per replay_protocol.md v1.0.0) and JsonlReplaySink: orjson-serialised JSONL, fsync-on-close, build-flag gated (BUILD_REPLAY_SINK_JSONL), double-close idempotent, FDR mirror on open/close. The drifted AZ-390 stub in interface.py is removed; the canonical Protocol now lives in replay_sink.py per module-layout.md and is re-exported via __init__.py. AZ-390 conformance test widened. AZ-399 lands TlogReplayFcAdapter: full FcAdapter Protocol surface, build-flag gated (BUILD_TLOG_REPLAY_ADAPTER), pymavlink stream-parse with bounded pre-scan + fail-fast on missing required messages (R-DEMO-3), dedicated decode thread feeding the existing AZ-391 SubscriptionBus. Outbound surface raises FcEmitError per Invariant 5; request_source_set_switch raises SourceSetSwitchNotSupportedError. Pacing honours Invariant 6 via Clock.sleep_until_ns. time_offset_ms shifts every emitted received_at per Invariant 8. Non-monotonic timestamps raise FcOpenError. Test coverage: 188 c8_fc_adapter tests pass; 1 skipped (AZ-399 AC-1 500 MB tlog RSS bound, deferred to AZ-404 e2e behind RUN_REPLAY_E2E). Code review: PASS_WITH_WARNINGS — 1 Medium (mapping logic duplicates AZ-391 live decoder; intentional today, four behavioural deltas documented), 2 Low. Co-authored-by: Cursor <cursoragent@cursor.com>
7.3 KiB
Code Review Report
Batch: 59 (AZ-399, AZ-400) Date: 2026-05-14 Verdict: PASS_WITH_WARNINGS
Findings
| # | Severity | Category | File:Line | Title |
|---|---|---|---|---|
| 1 | Medium | Maintainability | src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py:560-720 | MAVLink mapping logic duplicates _inbound_mavlink.PymavlinkInboundDecoder |
| 2 | Low | Maintainability | src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py:457 | _PRESCAN_MAX_MESSAGES = 6000 is implicit and hard-codes a 30 s @ 200 Hz budget |
| 3 | Low | Maintainability | src/gps_denied_onboard/components/c8_fc_adapter/replay_sink.py:184 | noqa: SIM115 suppression with inline rationale is fine; flag for future re-evaluation if a context-managed alternative emerges |
Finding Details
F1: MAVLink mapping logic duplicates the AZ-391 live decoder (Medium / Maintainability)
- Location:
src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py:560-720 - Description:
_handle_imu,_handle_attitude,_handle_gps,_handle_heartbeat,_map_fix_type, and_map_mav_statemirror the AZ-391PymavlinkInboundDecoderimplementations almost line-for-line. Today the duplication is intentional — the replay path differs from live in three observable ways: (a)received_atis the tlog timestamp +time_offset_ns(notClock.monotonic_ns()), (b)signed=Falseis hard-coded per the D-CROSS-CVE-1 risk, (c) non-monotonic timestamps raiseFcOpenErrorinstead of being dropped, and (d) STATUSTEXT spoof-promotion is intentionally absent because the replay channel carries the recorded stream verbatim. A shared mapper module would have to expose enough seams to model all four differences and would risk losing the readability win. - Suggestion: revisit during AZ-405 (auto-sync) or AZ-403 (cross-component refactor) if a third caller appears. For now, leave the duplication and document the four behavioural deltas in the module docstring.
- Task: AZ-399
F2: _PRESCAN_MAX_MESSAGES = 6000 magic number lacks runtime override (Low / Maintainability)
- Location:
src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py:457 - Description: The pre-scan budget is sized to "≈ 30 s of telemetry at 200 Hz" in the comment, but it is a module constant. A 1 Hz HEARTBEAT-only fixture (e.g., a long-coast clip) might satisfy the IMU/attitude/GPS budget within 6000 records but never see the late HEARTBEAT — leading to a false-negative fail-fast. The current Derkachi fixture sits comfortably under the budget, so this is a future-proofing note, not a present bug.
- Suggestion: thread
prescan_max_messagesthrough the constructor with the current value as default once a real fixture pushes the budget. No change required for batch 59. - Task: AZ-399
F3: # noqa: SIM115 rationale comment (Low / Maintainability)
- Location:
src/gps_denied_onboard/components/c8_fc_adapter/replay_sink.py:184 - Description: The comment "owned for the sink lifetime" justifies the suppression — the file handle outlives the constructor by design (close happens in
JsonlReplaySink.close). Acceptable. - Suggestion: no action; keep the comment for future readers.
- Task: AZ-400
Phase Summary
Phase 1 — Context Loading
Read inputs:
_docs/02_tasks/todo/AZ-399_replay_tlog_adapter.md_docs/02_tasks/todo/AZ-400_replay_jsonl_sink.md_docs/02_document/contracts/replay/replay_protocol.md_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md_docs/02_document/module-layout.md
Phase 2 — Spec Compliance
AZ-400 — all 10 ACs covered by tests/unit/c8_fc_adapter/test_az400_replay_sink.py. Contract ReplaySink Protocol surface (emit(EstimatorOutput) -> None, close() -> None) matches replay_protocol.md v1.0.0; the prior AZ-390 stub was widened in interface.py to match this batch's contract. Module-layout home updated.
AZ-399 — AC-2 through AC-10 covered by tests/unit/c8_fc_adapter/test_az399_tlog_replay_adapter.py; AC-1 (500 MB tlog RSS bound) is present as a @pytest.mark.skip placeholder with prerequisite reason, deferred to AZ-404 e2e behind RUN_REPLAY_E2E=1 per the implement skill's "skipped tests count as Covered" rule. Contract surface (TlogReplayFcAdapter(tlog_path, target_fc_dialect, clock, wgs_converter, time_offset_ms, pace, fdr_client, source_factory)) matches the replay_protocol.md constructor; Invariants 5, 6, 8 enforced and tested.
Phase 3 — Code Quality
- SOLID:
SubscriptionBusreused via composition (Invariant 1: replay frames hit identical fan-out shape)._to_jsonableis a pure function for testability. - Error handling: explicit
FcOpenError/FcAdapterConfigError/FcEmitError/SourceSetSwitchNotSupportedErrorintlog_replay_adapter; explicitReplaySinkError/ReplaySinkConfigErrorinreplay_sink. No bareexcept. - Naming: clear (
_msg_timestamp_ns,_FRAME_PROGRESS_INTERVAL,REQUIRED_MESSAGE_TYPES). - Complexity: longest method ≈ 40 lines (
_dispatch); under the 50-line threshold. - Tests: every test follows Arrange / Act / Assert with language-appropriate
# Arrange|Act|Assertmarkers. - Dead code: none introduced.
Phase 4 — Security
- No SQL / command injection vectors.
- No hardcoded secrets.
- Tlog file path is operator-supplied and opened via
pymavlink.mavutil.mavlink_connection; the lazypymavlinkimport means a missing C extension surfaces asFcOpenError, not anImportErrorat composition time. - Output JSONL path validation already in place from AZ-400 (parent-dir check + binary-mode open).
Phase 5 — Performance
- Stream-parse via
recv_match(blocking=False)— never materialises the tlog. - Pre-scan ceiling caps file walk at
_PRESCAN_MAX_MESSAGES(Finding F2 caveat). - AC-7 throughput proxy passes (1000 frames in < 1 s on Tier-1 hardware).
JsonlReplaySink.emitisorjson.dumps+write— microsecond-class.
Phase 6 — Cross-Task Consistency
- AZ-399 and AZ-400 both live under
c8_fc_adapterand route through the existing component-local plumbing (SubscriptionBus,errors.py,fdr_client). - The AZ-400
ReplaySinkProtocol is exactly what AZ-401'scompose_replaywill require. - No DTO drift: both files consume
EstimatorOutput,FcTelemetryFrame,LatLonAlt,Quatfrom the existing_types/*shared layer.
Phase 7 — Architecture Compliance
- Layer direction:
tlog_replay_adapterandreplay_sinkimport from_types,helpers,fdr_client,clock,logging, plus the own-component_subscription/errors. All importees are at or above the importer's layer permodule-layout.md(Layer 4 component → Layer 1 cross-cutting / Layer 2 shared types). - Public API respect: imports are limited to documented public surfaces (
FdrRecord,iso_ts_now,get_logger,WgsConverter). - No new cyclic dependencies.
- No duplicate symbols across components (the AZ-391 mapping duplication noted in F1 is within the same component).
- Cross-cutting concerns (FDR enqueue, structured logging) consumed via shared helpers — not re-implemented locally.
Verdict Logic
- 0 Critical, 0 High, 1 Medium, 2 Low → PASS_WITH_WARNINGS.
Outputs
verdict: PASS_WITH_WARNINGSfindings: 3 (1 Medium + 2 Low)critical_count: 0high_count: 0report_path:_docs/03_implementation/reviews/batch_59_review.md