[AZ-894] [AZ-896] Add CSV-driven replay adapter + format docs

Replaces the tlog two-clock replay surface with a single-clock path
driven by the Derkachi-schema CSV. --imu is the new required CLI arg;
--tlog stays as a deprecated alias (warned + ignored when --imu set)
until AZ-895 deletes it.

* csv_ground_truth.py parses the 15-column schema, fails fast at
  startup on every documented schema fault (AC-5).
* CsvReplayFcAdapter slots into ReplayInputBundle.fc_adapter alongside
  the tlog sibling; mirrors Invariant-5 outbound wiring; inbound bus is
  intentionally a no-op since the loop reads CSV directly.
* _run_replay_loop branches on imu_csv_path, stamps
  VioOutput.emitted_at_ns from the CSV-derived frame_end_ns (AC-4),
  closing the AZ-848 two-clock surface for the new path.
* AZ-896 ships the operator-facing format spec at
  _docs/02_document/contracts/replay/csv_replay_format.md plus a
  20-row example CSV (AC-3 regression-locked).

Tests: 11 + 12 new unit tests, plus updates to AZ-401 import-boundary
and AZ-402 CLI suites. Full unit suite 2,327 passed / 86 skipped.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-26 18:40:29 +03:00
parent 3020779404
commit 6be207cef3
19 changed files with 1833 additions and 93 deletions
+13 -5
View File
@@ -491,12 +491,14 @@ def test_ac8_replay_branch_imports_only_public_apis() -> None:
tree = ast.parse(text)
# Allowed deep imports: into the c8_fc_adapter component (the
# noop transport + the JSONL sink) and into the `replay_input`
# cross-cutting coordinator (Layer-4). Both are documented in
# module-layout.md as the replay strategy homes.
# noop transport + the JSONL sink + the AZ-894 CSV replay adapter)
# and into the `replay_input` cross-cutting coordinator (Layer-4).
# All of these are documented in module-layout.md as the replay
# strategy homes.
allowed_deep_prefixes = (
"gps_denied_onboard.components.c8_fc_adapter.noop_mavlink_transport",
"gps_denied_onboard.components.c8_fc_adapter.replay_sink",
"gps_denied_onboard.components.c8_fc_adapter.csv_replay_adapter",
"gps_denied_onboard.replay_input.tlog_video_adapter",
)
@@ -632,9 +634,14 @@ def test_replay_branch_rejects_empty_video_path(
build_replay_components(config)
def test_replay_branch_rejects_empty_tlog_path(
def test_replay_branch_rejects_both_inputs_empty(
_airborne_replay_env: Path,
) -> None:
# AZ-894: the validation gate now accepts either imu_csv_path
# (canonical) or tlog_path (legacy) — rejecting only when both
# are empty. Keeping the historical name pattern (test_*_rejects_*)
# for grep parity but renamed to reflect the new semantics.
# Arrange
runtime_cfg = RuntimeConfig(camera_calibration_path=str(_airborne_replay_env))
config = Config(
@@ -642,6 +649,7 @@ def test_replay_branch_rejects_empty_tlog_path(
replay=ReplayConfig(
video_path="/dev/null/fake.mp4",
tlog_path="",
imu_csv_path="",
output_path="/tmp/out.jsonl",
pace="asap",
target_fc_dialect="ardupilot_plane",
@@ -650,7 +658,7 @@ def test_replay_branch_rejects_empty_tlog_path(
)
# Act / Assert
with pytest.raises(CompositionError, match="tlog_path is empty"):
with pytest.raises(CompositionError, match="imu_csv_path is empty"):
build_replay_components(config)