load_ground_truth_track now dispatches on truth_path.suffix:
- .csv → load_csv_ground_truth (AZ-894)
- else (.tlog, .bin, no ext) → load_tlog_ground_truth (AZ-697)
Removes the AZ-959 short-circuit in SubprocessReplayRunner.
_maybe_render_map so CSV-path replay jobs ship with the same
map.html artefact as tlog jobs. Both ground-truth DTOs expose
row-aligned (lat_deg, lon_deg) records so the renderer needs no
other changes.
Touches:
- src/gps_denied_onboard/cli/render_map.py: dispatch +
source-agnostic tooltip + --truth CLI help expanded
- src/gps_denied_onboard/replay_api/app.py: workaround removed,
truth_path resolution picks whichever input was uploaded
Tests: 44/44 green across test_az700_render_map.py +
test_az701_replay_api.py:
- 17 pre-existing render-map tests pass unchanged (AC-2)
- New test_load_ground_truth_track_dispatches_to_csv_loader (AC-1)
- New test_load_ground_truth_track_csv_propagates_schema_error
(AC-4: malformed CSV raises ReplayInputAdapterError)
- New test_cli_renders_map_with_csv_truth (AC-1 end-to-end)
- AZ-959 test_post_replay_csv_path_returns_200... extended to
assert map_html_url is now present (AC-3)
Bookkeeping: AZ-960 spec moved todo/ → done/, dep-table preamble
seventh bump documents the landing + AC coverage, state.md records
batch 6 complete with AZ-961 as next.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extend the AZ-701 replay_api POST /replay endpoint so AZ-897 (now
in ../ui repo) can drive the AZ-894 CSV-replay path. The endpoint
keeps full back-compat for tlog clients and adds:
- (video, tlog) OR (video, csv) multipart with strict XOR enforced
at the API boundary (AC-2 / AC-3 → 400 multipart_missing_field)
- validate_csv_kind: rejects malformed CSV schema at boundary by
scanning the header line for AZ-896 required tokens; messages
point at csv_replay_format.md (AC-4)
- ReplayInputs DTO: tlog_path / csv_path are now Path | None with
XOR re-enforced in __post_init__ for internal callers
- JobStorage reserves both input.tlog and input.csv paths; handler
writes exactly one
- SubprocessReplayRunner.run dispatches --imu vs --tlog argv (AC-1)
- _maybe_render_report dispatches load_csv_ground_truth vs
load_tlog_ground_truth; CsvGpsFix and TlogGpsFix have
field-compatible shapes for the GroundTruthRow adapter (AC-6)
- GET /static/example-csv serves the AZ-896 reference CSV; honours
REPLAY_API_EXAMPLE_CSV_PATH env, falls back to source-checkout
layout, returns 503 with example_csv_unavailable when neither
resolves to a readable file. No auth required (AC-5)
Tests: 27/27 unit tests green:
- 18 pre-existing tlog-path tests unchanged (AC-7)
- 9 new tests covering ACs 1-6 + validate_csv_kind isolation
Deferred (NOT silently fixed; reported to user as end-of-turn
notes for scope discipline):
- gps-denied-render-map only consumes binary tlog truth today, so
CSV-path jobs return map_html_url=None. Extending render-map to
dispatch on truth-file extension is AZ-700 follow-up territory.
- ReportContext.tlog_path field is now overloaded as the
"ground-truth source path"; the rendered report still labels
the line "Tlog: <csv_path>" which is cosmetically misleading
for CSV runs. Field rename + label fix is AZ-699 follow-up.
Bookkeeping: AZ-959 spec moved todo/ → done/, dep-table preamble
fifth bump documents what landed + what's deferred, state.md
records batch 5 complete and what comes next.
Co-authored-by: Cursor <cursoragent@cursor.com>
New replay_api component: FastAPI service wrapping the offline
gps-denied-replay pipeline. POST tlog+video (multipart) → either
sync 200 with result/map/report URLs, or async 202 + job id with
/jobs/{id} polling. Magic-byte validation, bearer auth, in-memory
JobRegistry with concurrency + queue caps (429 on overflow).
Helper accuracy_report.py promoted from tests/ to src/ because the
API needs the Markdown report writer at runtime; all AZ-699 imports
re-pointed. OpenAPI spec exported to docs.
18/18 unit tests pass (AC-1 sync, AC-2 async, AC-3 state machine,
AC-5 auth, AC-6 health, AC-8 concurrency, AC-9 magic-byte). Full
unit suite: 2251 pass, 86 skip, 1 pre-existing C12 cold-start flake
(unchanged). mypy --strict clean on the new surface.
Co-authored-by: Cursor <cursoragent@cursor.com>