Files
gps-denied-onboard/_docs/02_tasks/todo/AZ-402_replay_cli.md
T
Oleksandr Bezdieniezhnykh 880eabcb3f Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components
(C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446
plus the _dependencies_table.md and component contract documents.

State file updated to greenfield Step 7 (Implement), not_started.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:39:48 +03:00

6.7 KiB

Replay — gps-denied-replay CLI entrypoint + arg parser + calibration loader

Task: AZ-402_replay_cli Name: gps-denied-replay CLI entrypoint + argparse + camera-calibration loader Description: Implement the gps-denied-replay console script: argparse-based CLI accepting --video PATH --tlog PATH --output results.jsonl --camera-calibration calib.json --config config.yaml [--pace {realtime,asap}] [--time-offset-ms N]. Load and validate the camera-calibration JSON (project's standard pinhole + distortion-coefficients schema, reusable via the config loader from AZ-269/AZ-270 if practical, otherwise a small dedicated loader); construct the Config object; invoke compose_replay(config) -> ReplayRoot; call replay_root.runtime_loop(); map the returned exit code to the process exit code (0 = success per AC-1 of the epic; 2 = sync-impossible per AC-8; 1 = any other error). Set up structured logging (stdout JSON per project convention) and FDR client. Exit-code mapping documented inline. CLI registered as a console_script entrypoint in pyproject.toml under [project.scripts] (or equivalent build-config). Complexity: 3 points Dependencies: AZ-401 (compose_replay + ReplayRoot.runtime_loop); AZ-269 / AZ-270 (config); AZ-263; AZ-266; AZ-272 (FDR record schema); AZ-273 (FdrClient) Component: replay-cli (epic AZ-265 / E-DEMO-REPLAY) — CLI entrypoint at src/gps_denied_onboard/cli/replay.py Tracker: AZ-402 Epic: AZ-265 (E-DEMO-REPLAY)

Document Dependencies

  • _docs/02_document/contracts/replay/replay_protocol.md — CLI surface specification.
  • _docs/02_document/architecture.md — § 5 (binary topology; replay-cli is the fourth Docker image).

Problem

Without this task, the compose_replay composition root has no entrypoint — the parent-suite UI cannot shell out to a replay run. The CLI is the user-facing surface (and CI-test surface) of the replay binary.

Outcome

  • src/gps_denied_onboard/cli/replay.pymain() entrypoint:
    • argparse setup with all 7 args + the 2 optional ones.
    • calibration loader (small JSON loader; pinhole + distortion-coefficients schema) — module-internal.
    • config loader invocation (re-use AZ-269 / AZ-270 plumbing).
    • compose_replay(config) invocation.
    • replay_root.runtime_loop() invocation; exit code propagated.
    • Structured logging + FDR client setup.
    • Top-level try/except: logs the error class + message + suggested next step before exiting 1.
  • pyproject.toml (or equivalent) registers gps-denied-replay = "gps_denied_onboard.cli.replay:main".
  • INFO log at startup: kind="replay.cli.started" with all CLI args (sanitised — no key bytes per E-C8 signing invariants, but replay has no signing).
  • INFO log at exit: kind="replay.cli.exited" with {exit_code, frames_processed, lines_written}.
  • Unit tests: argparse defaults + overrides, calibration loader rejects malformed JSON, config loader passes-through to compose_replay, exit-code mapping on each known runtime_loop return value.

Scope

Included

  • argparse + arg-validation (file existence, output-parent existence).
  • camera-calibration JSON loader + schema validation.
  • compose_replay invocation + runtime_loop dispatch.
  • Exit-code mapping.
  • Top-level error handling (catch + log + exit 1 on unexpected exception).
  • Console-script registration in pyproject.toml.
  • Unit tests for argparse + calibration loader + exit-code mapping.

Excluded

  • Auto-sync IMU take-off detection — owned by AZ-405 (this task accepts --time-offset-ms and forwards to config/replay; the auto-sync TASK computes the default value).
  • Dockerfile + CI matrix — owned by Docker task.
  • E2E replay fixture test — owned by E2E task.

Acceptance Criteria

AC-1: All required args parsed — invoke with --video v.mp4 --tlog t.tlog --output o.jsonl --camera-calibration c.json --config conf.yaml; assert all five values reach compose_replay's Config object.

AC-2: --pace default ASAP — invoke without --pace; assert config has pace=ReplayPace.ASAP.

AC-3: --pace realtime — invoke with --pace realtime; assert config has pace=ReplayPace.REALTIME.

AC-4: --time-offset-ms forwarded — invoke with --time-offset-ms 5000; assert config has time_offset_ms=5000.

AC-5: Missing required arg → exit 2 + helpful message — invoke without --video; assert exit code 2 (argparse default) + stderr message names the missing arg.

AC-6: Calibration loader rejects malformed JSON — pass a corrupt calib.json; assert ReplayCliError("camera-calibration JSON malformed: <details>") + exit 1.

AC-7: Calibration schema validation — pass a calib.json missing intrinsics key; assert ReplayCliError("camera-calibration schema invalid: missing 'intrinsics'").

AC-8: Output parent dir validation--output /nonexistent/out.jsonlReplayCliError("output parent directory does not exist") + exit 1 (consistent with JsonlReplaySink behaviour).

AC-9: Exit-code mapping — wire a FakeReplayRoot whose runtime_loop returns 0 / 1 / 2; assert process exit code matches each.

AC-10: Console script registered — install the package in a fresh venv; assert gps-denied-replay --help runs and prints the argparse usage.

Non-Functional Requirements

  • CLI startup p99 ≤ 5 s (cold-start NFT from the epic).
  • argparse + calibration loading p99 ≤ 100 ms (excluding compose_replay itself).

Constraints

  • argparse (stdlib) — no new CLI framework.
  • JSON for calibration (already the project convention).
  • Exit codes: 0 = success; 2 = AC-8 sync-impossible (or argparse missing-arg); 1 = any other error.
  • Console-script registration in pyproject.toml [project.scripts].

Risks & Mitigation

  • Risk: argparse exit code 2 conflicts with epic AC-8 exit code 2Mitigation: documented; argparse exit 2 is for "missing-required-arg / arg-parse-error" — operator can distinguish via stderr; AC-8 exit 2 is for runtime-sync-impossible.
  • Risk: calibration JSON schema driftMitigation: schema-validate at load time; AC-7 enforces.
  • Risk: top-level error swallowing makes debugging hardMitigation: top-level except logs the FULL traceback (via logger.exception); the exit code is 1 but the operator sees the traceback in stderr.

Runtime Completeness

  • Named capability: gps-denied-replay CLI.
  • Production code: real argparse, real calibration loader, real compose_replay dispatch, real exit-code propagation.
  • Allowed external stubs: test fakes only.
  • Unacceptable substitutes: a click-based or typer-based CLI (adds a dependency for no gain over stdlib argparse).

Contract

Implements _docs/02_document/contracts/replay/replay_protocol.md — CLI surface.