Files
Oleksandr Bezdieniezhnykh fa3742d582 [AZ-399] [AZ-400] C8 TlogReplayFcAdapter + ReplaySink + JsonlReplaySink
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>
2026-05-14 05:33:20 +03:00

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_state mirror the AZ-391 PymavlinkInboundDecoder implementations almost line-for-line. Today the duplication is intentional — the replay path differs from live in three observable ways: (a) received_at is the tlog timestamp + time_offset_ns (not Clock.monotonic_ns()), (b) signed=False is hard-coded per the D-CROSS-CVE-1 risk, (c) non-monotonic timestamps raise FcOpenError instead 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_messages through 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: SubscriptionBus reused via composition (Invariant 1: replay frames hit identical fan-out shape). _to_jsonable is a pure function for testability.
  • Error handling: explicit FcOpenError / FcAdapterConfigError / FcEmitError / SourceSetSwitchNotSupportedError in tlog_replay_adapter; explicit ReplaySinkError / ReplaySinkConfigError in replay_sink. No bare except.
  • 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|Assert markers.
  • 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 lazy pymavlink import means a missing C extension surfaces as FcOpenError, not an ImportError at 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.emit is orjson.dumps + write — microsecond-class.

Phase 6 — Cross-Task Consistency

  • AZ-399 and AZ-400 both live under c8_fc_adapter and route through the existing component-local plumbing (SubscriptionBus, errors.py, fdr_client).
  • The AZ-400 ReplaySink Protocol is exactly what AZ-401's compose_replay will require.
  • No DTO drift: both files consume EstimatorOutput, FcTelemetryFrame, LatLonAlt, Quat from the existing _types/* shared layer.

Phase 7 — Architecture Compliance

  • Layer direction: tlog_replay_adapter and replay_sink import from _types, helpers, fdr_client, clock, logging, plus the own-component _subscription / errors. All importees are at or above the importer's layer per module-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_WARNINGS
  • findings: 3 (1 Medium + 2 Low)
  • critical_count: 0
  • high_count: 0
  • report_path: _docs/03_implementation/reviews/batch_59_review.md