mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 21:01:13 +00:00
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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# 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) 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.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_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 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-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.
|
||||
Reference in New Issue
Block a user