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>
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.py—main()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) registersgps-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_replayinvocation + 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-msand 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.jsonl → ReplayCliError("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_replayitself).
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 2 — Mitigation: 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 drift — Mitigation: schema-validate at load time; AC-7 enforces.
- Risk: top-level error swallowing makes debugging hard — Mitigation: 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-replayCLI. - Production code: real argparse, real calibration loader, real
compose_replaydispatch, 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.