Closes AZ-835 Epic C6 (docs) and folds the cycle-4 replay-input
redesign narrative (AZ-894 CSV adapter / AZ-895 auto-sync deprecation
/ AZ-896 format spec / AZ-897 UI follow-up) into the three
authoritative documents.
Modified:
- _docs/02_document/contracts/replay/replay_protocol.md: extend
Invariant 12 with sub-invariants 12.c (route-driven supersedes
bbox; ~100x tile efficiency + did-fly-vs-might-fly honesty) and
12.d (fixture failure-handling: validation/terminal re-raise;
transient -> C11 backoff x3). Add Invariant 14 with sub-
invariants 14.a-14.d covering the single canonical clock model,
the CSV-driven path, the tlog adapter's audit-only role, the
auto-sync deprecation, and the AZ-897 UI follow-up pointer.
- _docs/02_document/architecture.md: add the AZ-777 Phase 3+
superseded-by-Epic-AZ-835 supersession block + new "Replay input
redesign (cycle 4)" sub-section with the cycle-4 ticket table.
- tests/e2e/replay/README.md: top section restructured for two
distinct entry points (AZ-265/AZ-404 vs. AZ-835/AZ-840); add
full AZ-835 orchestrator-test section (env vars, skip gates,
expected runtime, verdict report path); add Imagery (c) Google
attribution + dev-only caveat; add Epic AZ-835 ticket map.
Spec deviation: AC-1b says "new Invariant 13" but Invariant 13 is
already taken (C4<->C5 pairing, AZ-776 / ADR-012), and is referenced
by number in architecture.md, c4_pose description.md, and ADR-012
prose. Cycle-4 content shipped as Invariant 14 to preserve those
cross-references; renumbering would have cascaded to 3 files outside
AZ-842's ownership envelope. Documented in batch report.
Out-of-scope hygiene gap (NOT fixed in this batch):
BUILD_CSV_REPLAY_ADAPTER flag is not yet enumerated in
_docs/02_document/module-layout.md's Build-Time Exclusion Map.
Inherited from cycle-4 AZ-894. Suggested as a cycle-5+ hygiene PBI.
AZ-835 epic file stays in todo/ until AZ-841 (backlog) is resolved.
Co-authored-by: Cursor <cursoragent@cursor.com>
Derkachi e2e Tier-2 divergence had three stacked root causes; this
commit ships fixes for all three plus the IMU prerequisite they
depend on, plus a baseline cheirality gate for cv2.recoverPose.
AZ-918 MAVLink IMU adapters now convert raw mG/mrad-s + FRD body to
SI m/s^2 + rad/s + FLU body via helpers.imu_units. Without
this the ESKF receives values ~1000x too small with wrong-
sign Y/Z and cannot function at all.
AZ-919 Composition root wires EskfNominalAltitudeProvider into the
KLT/RANSAC strategy via the AZ-331 factory introspect path;
OKVIS2 and VINS-Mono are unaffected.
AZ-920 KLT/RANSAC recovers metric translation via Ground Sampling
Distance when AGL is available; otherwise falls through with
scale_quality=direction_only/unknown (no fake scale invented).
AZ-921 VioOutput.scale_quality signal; ESKF add_vio adapts R_meas
position block based on the flag (1e6 inflation when scale is
direction_only/unknown to keep the filter consistent).
AZ-922 KLT/RANSAC cheirality gate rejects single-frame rotations
beyond a config threshold (default 30 deg), catching
cv2.recoverPose twisted-pair flips that cause immediate ESKF
divergence on low-parallax aerial scenes.
Verification:
- Tier-1 (macOS) unit suite: 2346 passed, 0 failed.
- Tier-2 (Jetson) Derkachi e2e: divergence moves from frame 5
(mahalanobis^2 3757) to frame 233 (mahalanobis^2 212). Remaining
drift is open-loop attitude accumulation, not cheirality.
Follow-up tickets filed:
- AZ-923 closed as misdiagnosed: EskfNominalAltitudeProvider was
already correct (nominal_pos.z IS the AGL when takeoff origin sits
at ground level); the early-frame AGL near zero reflects the drone
being stationary on the ground, not a provider bug.
- AZ-942 filed: cross-check VIO rotation against IMU preintegrator
(consistency gate) - more physically grounded than the coarse
AZ-922 threshold and likely required to absorb the frame-233 drift.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Adds the Layer-4 cross-cutting `replay_input/` module per ADR-011:
ReplayInputAdapter converges (video, tlog) into the standard
FrameSource + FcAdapter + Clock surfaces the airborne composition
root consumes. Owns time-alignment between video frames and tlog
IMU/attitude ticks (manual via --time-offset-ms or auto via the
AZ-405 IMU-take-off detector + Farneback motion-onset detector).
Auto-sync algorithm (auto_sync.py):
- Tlog take-off detector: sustained vertical-accel excess > 0.5 g for
>= 0.5 s + sustained attitude-rate magnitude > 1 rad/s.
- Video motion-onset detector: dense Farneback flow magnitude > 1.5 px
sustained >= 0.5 s (deterministic per AC-10).
- compute_offset combines the two; confidence = min(tlog, video).
- validate_offset_or_fail implements the AC-9 95 % frame-window match
validator with configurable threshold + window.
ReplayInputAdapter.open() ordering (AC-13):
1. Load tlog samples + fail-fast on missing RAW_IMU/SCALED_IMU2 or
ATTITUDE BEFORE any video read.
2. Resolve offset (auto-sync OR manual override; manual bypasses the
detectors entirely per AC-8).
3. Run AC-9 validator on resolved offset; raise auto-sync hard-fail
for AC-7 (CLI exit 2 mapping).
4. Build single Clock instance per pace (TlogDerived/ASAP, Wall/REAL).
5. Construct VideoFileFrameSource and TlogReplayFcAdapter with the
resolved offset baked in (replay protocol Invariant 8).
Structured log + FDR records on auto-sync detected / low-confidence /
AC-8 hard-fail kinds. Idempotent close (AC-12).
Tests: 25 unit tests across tests/unit/replay_input/ covering all 13
ACs (kernel-level synthetic fixtures for AC-1..AC-10; coordinator-
level OpenCV synthetic videos + faked pymavlink for AC-6..AC-13).
Contract update: replay_protocol.md v2.0.0 added fdr_client to the
ReplayInputAdapter __init__ signature (was missing in the prose; the
task spec already listed it in the allowed-imports section).
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-design replay mode per user direction: replay is no longer a fourth
Docker image with a reduced component set, but a `config.mode = "replay"`
branch of the single airborne binary. The pre-flight workflow (route in
suite UI -> C12 tile download via real satellite-provider -> C10
manifest+engines build) is identical between live and replay; only three
strategies swap at compose time:
FrameSource: Live <-> Video
FcAdapter: Pymavlink/MSP2 <-> TlogReplay
MavlinkTransport: Serial <-> Noop
The C8 outbound MAVLink encoders run unchanged in both modes; their
bytes hit `NoopMavlinkTransport` in replay and disappear. A new
`JsonlReplaySink` taps C5's `EstimatorOutput` stream so the parent-suite
UI sees per-tick coordinates by tailing `results.jsonl`. MAVLink 2.0
signing key remains mandatory (operator supplies a dummy file).
A new `replay_input/` Layer-4 cross-cutting coordinator owns
`(video, tlog) -> (FrameSource, FcAdapter, Clock)` convergence; the
composition root sees only standard interfaces past `.open()`.
Docs:
- architecture.md: new ADR-011 with full rationale; ADR-002 binary
narrative updated.
- contracts/replay/replay_protocol.md: bumped to v2.0.0; 12 invariants
(notably mode-agnosticism + encoder byte-equality + signing key
mandatory + real C6 cache in replay).
- module-layout.md: Build-Time Exclusion Map dropped from 4 to 3 binary
columns; replay-mode `BUILD_*` flags default ON in airborne;
`shared/replay_input` cross-cutting entry added.
- epics.md: E-DEMO-REPLAY scope reframed; story points 27-32 -> 19-24.
Task respecs:
- AZ-401: shrunk 3 -> 2 pts; `compose_root` mode branch + JSONL sink +
NoopMavlinkTransport wiring; legacy `compose_replay` export deleted.
- AZ-402: console-script wrapper that mutates `config.mode = "replay"`
and dispatches into the shared airborne main; `--mavlink-signing-key`
mandatory.
- AZ-403: CANCELLED. Moved to done/ with banner; Jira transition deferred
via `_docs/_process_leftovers/2026-05-14_az_403_cancellation_pending_tracker.md`.
- AZ-404: AC-4 reworded as mode-agnosticism AST scan + encoder
byte-equality test; new AC-8 operator-workflow rehearsal.
- AZ-405: also owns the `replay_input/` module + `ReplayInputAdapter`.
_dependencies_table.md updated: AZ-401 gains AZ-405 dep; AZ-404 drops
AZ-403 dep; AZ-403 row marked CANCELLED.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>