[AZ-263] [AZ-264] [AZ-265] Decompose: layout, helpers epic, replay epic

Decompose Step 1 + Step 1.5 + new cycle-1 epics:

- Step 1 (Bootstrap): AZ-263 spec at _docs/02_tasks/todo/. Single
  top-level Python package src/gps_denied_onboard/ + nested
  components/ subpackage per user feedback (replaces earlier
  src/gps_denied/ + sibling src/components/ split).
- Step 1.5 (Module Layout): _docs/02_document/module-layout.md is
  the file-ownership map consumed by /implement Step 4. Covers all
  14 components + cross-cuttings (_types, config, logging,
  fdr_client, helpers x8, frame_source, clock, runtime_root,
  cli/replay, healthcheck), 5-layer layering, and the Build-Time
  Exclusion Map for all 4 binaries (airborne, research,
  operator-tooling, replay-cli).
- New epic AZ-264 (E-CC-HELPERS): re-homes the 8 shared helpers
  from per-component child-issues into a single cross-cutting
  epic per the decompose skill cross-cutting rule. R14
  (LightGlue circular dep) is structurally prevented because
  both C2.5 and C3 import gps_denied_onboard.helpers.lightglue_runtime.
- New epic AZ-265 (E-DEMO-REPLAY): offline replay mode (video +
  tlog -> per-tick coordinate stream). 8 child tasks, 27-32 pts.
  Reuses C8 FcAdapter via TlogReplayFcAdapter strategy + new
  VideoFileFrameSource + JsonlReplaySink + compose_replay
  composition root + gps-denied-replay CLI + auto-sync via IMU
  take-off detection (per how_to_test.md). NO ROS dependency.
- Plan Final report at FINAL_report.md.
- _autodev_state.md updated with handoff notes for Step 2
  execution in a fresh chat (~290 MCP calls expected; epic
  ordering documented).

Step 2 task PLAN approved (97 implementation tasks across 18
epics) but EXECUTION deferred per user choice to a fresh chat.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-10 03:14:42 +03:00
parent 64542d32fc
commit 8171fcb29e
6 changed files with 1287 additions and 44 deletions
+214 -39
View File
@@ -1,6 +1,6 @@
# Work-Item Epics — gps-denied-onboard Plan cycle 1
This file is the local epic draft for Plan Step 6. Tracker IDs (`AZ-XXX`) are populated when each epic is created in Jira (project `AZ`). Until then, every entry carries `Tracker: pending`.
This file is the local epic draft for Plan Step 6. Tracker IDs (`AZ-XXX`) are now populated for every epic — they live in Jira project `AZ`. The canonical `E-*``AZ-NN` mapping below is the source of truth referenced from each Jira epic's description.
## Conventions
@@ -10,29 +10,35 @@ This file is the local epic draft for Plan Step 6. Tracker IDs (`AZ-XXX`) are po
- **Cross-cutting epics** parent exactly one shared implementation task; component epics consuming the concern declare a dependency, never re-implement locally.
- **Dependency rule**: no epic depends on a later one in this index.
## Decompose-time amendment (cycle 1, dated 2026-05-10)
Row 20 (E-CC-HELPERS / AZ-264) was added during Decompose Step 2 to comply with the cross-cutting rule. The 8 shared helpers (`ImuPreintegrator`, `SE3Utils`, `LightGlueRuntime`, `WgsConverter`, `Sha256Sidecar`, `EngineFilenameSchema`, `RansacFilter`, `DescriptorNormaliser`) were originally listed as child issues inside their largest-consumer component epics (e.g., `ImuPreintegrator` under E-C1 child #5, `LightGlueRuntime` under E-C2.5 child #2). Those child-issue listings are now superseded — helper ownership moves to E-CC-HELPERS, and component epics consume helpers as dependencies. The original component epic descriptions in Jira still reference the helpers in their child-issue tables; those will be reconciled at the next epic-edit pass (or at Step 4 cross-verification).
## Index
| # | Epic ID | Title | Type | Tracker | T-shirt | Story Pts | Depends on |
|---|---------|-------|------|---------|---------|-----------|------------|
| 1 | E-BOOT | Bootstrap & Initial Structure | bootstrap | Tracker: pending | M | 1321 | — |
| 2 | E-CC-LOG | Cross-Cutting: Structured JSON Logging | cross-cutting | Tracker: pending | S | 58 | E-BOOT |
| 3 | E-CC-CONF | Cross-Cutting: Configuration & Composition Root | cross-cutting | Tracker: pending | S | 58 | E-BOOT |
| 4 | E-CC-FDR-CLIENT | Cross-Cutting: FDR Producer Client (lock-free queue + record schema) | cross-cutting | Tracker: pending | M | 813 | E-BOOT, E-CC-LOG |
| 5 | E-C13 | C13 Flight Data Recorder (writer thread + segments + cap) | component | Tracker: pending | L | 2134 | E-BOOT, E-CC-LOG, E-CC-CONF, E-CC-FDR-CLIENT |
| 6 | E-C7 | C7 On-Jetson Inference Runtime | component | Tracker: pending | L | 2134 | E-BOOT, E-CC-CONF, E-CC-FDR-CLIENT |
| 7 | E-C6 | C6 Tile Cache + Spatial Index | component | Tracker: pending | M | 1321 | E-BOOT, E-CC-LOG, E-CC-CONF |
| 8 | E-C11 | C11 Tile Manager (TileDownloader + TileUploader) | component | Tracker: pending | M | 1321 | E-C6, E-CC-CONF, E-CC-LOG |
| 9 | E-C10 | C10 Pre-flight Cache Provisioning | component | Tracker: pending | M | 1321 | E-C6, E-C7, E-CC-LOG |
| 10 | E-C12 | C12 Operator Pre-flight Tooling | component | Tracker: pending | M | 1321 | E-C10, E-C11, E-CC-LOG |
| 11 | E-C1 | C1 Visual / Visual-Inertial Odometry | component | Tracker: pending | XL | 3455 | E-BOOT, E-CC-FDR-CLIENT, E-C7 |
| 12 | E-C2 | C2 Visual Place Recognition | component | Tracker: pending | L | 2134 | E-C6, E-C7, E-CC-FDR-CLIENT |
| 13 | E-C2.5 | C2.5 Inlier-based Re-rank | component | Tracker: pending | S | 58 | E-C2, E-C7, E-C6 (LightGlue helper shared with C3) |
| 14 | E-C3 | C3 Cross-Domain Matcher | component | Tracker: pending | L | 2134 | E-C2.5, E-C7 |
| 15 | E-C3.5 | C3.5 AdHoP-Conditional Refinement | component | Tracker: pending | M | 813 | E-C3, E-C7 |
| 16 | E-C4 | C4 Pose Estimator | component | Tracker: pending | M | 1321 | E-C3.5, E-C5 (shared GTSAM substrate; co-developed) |
| 17 | E-C5 | C5 State Estimator | component | Tracker: pending | XL | 3455 | E-C1, E-C4 (shared graph), E-CC-FDR-CLIENT |
| 18 | E-C8 | C8 FC + GCS Adapter | component | Tracker: pending | L | 2134 | E-C5, E-CC-CONF, E-CC-LOG |
| 19 | E-BBT | Blackbox Tests (FT/NFT scenarios) | tests | Tracker: pending | M | 1321 | every component epic ships its component-internal tests under its own epic; this one parents the suite-level FT/NFT scenarios in `_docs/02_document/tests/*.md` |
| 1 | E-BOOT | Bootstrap & Initial Structure | bootstrap | AZ-244 | M | 1321 | — |
| 2 | E-CC-LOG | Cross-Cutting: Structured JSON Logging | cross-cutting | AZ-245 | S | 58 | E-BOOT |
| 3 | E-CC-CONF | Cross-Cutting: Configuration & Composition Root | cross-cutting | AZ-246 | S | 58 | E-BOOT |
| 4 | E-CC-FDR-CLIENT | Cross-Cutting: FDR Producer Client (lock-free queue + record schema) | cross-cutting | AZ-247 | M | 813 | E-BOOT, E-CC-LOG |
| 5 | E-C13 | C13 Flight Data Recorder (writer thread + segments + cap) | component | AZ-248 | L | 2134 | E-BOOT, E-CC-LOG, E-CC-CONF, E-CC-FDR-CLIENT |
| 6 | E-C7 | C7 On-Jetson Inference Runtime | component | AZ-249 | L | 2134 | E-BOOT, E-CC-CONF, E-CC-FDR-CLIENT |
| 7 | E-C6 | C6 Tile Cache + Spatial Index | component | AZ-250 | M | 1321 | E-BOOT, E-CC-LOG, E-CC-CONF |
| 8 | E-C11 | C11 Tile Manager (TileDownloader + TileUploader) | component | AZ-251 | M | 1321 | E-C6, E-CC-CONF, E-CC-LOG |
| 9 | E-C10 | C10 Pre-flight Cache Provisioning | component | AZ-252 | M | 1321 | E-C6, E-C7, E-CC-LOG |
| 10 | E-C12 | C12 Operator Pre-flight Tooling | component | AZ-253 | M | 1321 | E-C10, E-C11, E-CC-LOG |
| 11 | E-C1 | C1 Visual / Visual-Inertial Odometry | component | AZ-254 | XL | 3455 | E-BOOT, E-CC-FDR-CLIENT, E-C7 |
| 12 | E-C2 | C2 Visual Place Recognition | component | AZ-255 | L | 2134 | E-C6, E-C7, E-CC-FDR-CLIENT |
| 13 | E-C2.5 | C2.5 Inlier-based Re-rank | component | AZ-256 | S | 58 | E-C2, E-C7, E-C6 (LightGlue helper shared with C3) |
| 14 | E-C3 | C3 Cross-Domain Matcher | component | AZ-257 | L | 2134 | E-C2.5, E-C7 |
| 15 | E-C3.5 | C3.5 AdHoP-Conditional Refinement | component | AZ-258 | M | 813 | E-C3, E-C7 |
| 16 | E-C4 | C4 Pose Estimator | component | AZ-259 | M | 1321 | E-C3.5, E-C5 (shared GTSAM substrate; co-developed) |
| 17 | E-C5 | C5 State Estimator | component | AZ-260 | XL | 3455 | E-C1, E-C4 (shared graph), E-CC-FDR-CLIENT |
| 18 | E-C8 | C8 FC + GCS Adapter | component | AZ-261 | L | 2134 | E-C5, E-CC-CONF, E-CC-LOG |
| 19 | E-BBT | Blackbox Tests (FT/NFT scenarios) | tests | AZ-262 | M | 1321 | every component epic ships its component-internal tests under its own epic; this one parents the suite-level FT/NFT scenarios in `_docs/02_document/tests/*.md` |
| 20 | E-CC-HELPERS | Cross-Cutting: Common Helpers (8 shared utilities) | cross-cutting | AZ-264 | M | 1321 | E-BOOT, E-CC-LOG (added in Decompose Step 2 — supersedes per-component helper child-issues from cycle 1) |
| 21 | E-DEMO-REPLAY | Offline replay mode (video + tlog → per-tick coordinate stream) | feature | AZ-265 | M | 2227 | E-C1, E-C2, E-C2.5, E-C3, E-C3.5, E-C4, E-C5, E-C8, E-CC-CONF (added in Decompose Step 2 — enables parent-suite UI demo via subprocess + JSONL streaming) |
## High-level component dependency diagram
@@ -57,10 +63,13 @@ flowchart TB
C5[E-C5 State]
C8[E-C8 FC Adapter]
BBT[E-BBT Blackbox Tests]
HELP[E-CC-HELPERS Common Helpers]
DEMO[E-DEMO-REPLAY Offline Replay Mode]
BOOT --> LOG --> FDRC --> C13
BOOT --> CONF --> C13
BOOT --> CONF --> C7
BOOT --> LOG --> HELP
C13 -.-> C7
CONF --> C6 --> C11
C6 --> C10
@@ -77,13 +86,30 @@ flowchart TB
FDRC --> C5
C8 --> BBT
C12 --> BBT
HELP -.-> C1
HELP -.-> C2
HELP -.-> C25
HELP -.-> C3
HELP -.-> C35
HELP -.-> C4
HELP -.-> C5
HELP -.-> C6
HELP -.-> C7
HELP -.-> C8
HELP -.-> C10
HELP -.-> C11
HELP -.-> C12
C1 --> DEMO
C5 --> DEMO
C8 --> DEMO
CONF --> DEMO
```
---
## E-BOOT — Bootstrap & Initial Structure
**Tracker**: pending
**Tracker**: AZ-244
**Type**: bootstrap
**T-shirt**: M | **Story points**: 1321
**Owner**: onboard team
@@ -188,7 +214,7 @@ T-shirt M; 1321 story points across child PBIs (each ≤ 5 points).
## E-CC-LOG — Cross-Cutting: Structured JSON Logging
**Tracker**: pending
**Tracker**: AZ-245
**Type**: cross-cutting
**T-shirt**: S | **Story points**: 58
@@ -294,7 +320,7 @@ T-shirt S; 58 points.
## E-CC-CONF — Cross-Cutting: Configuration & Composition Root
**Tracker**: pending
**Tracker**: AZ-246
**Type**: cross-cutting
**T-shirt**: S | **Story points**: 58
@@ -390,7 +416,7 @@ T-shirt S; 58 points.
## E-CC-FDR-CLIENT — Cross-Cutting: FDR Producer Client
**Tracker**: pending
**Tracker**: AZ-247
**Type**: cross-cutting
**T-shirt**: M | **Story points**: 813
@@ -494,7 +520,7 @@ T-shirt M; 813 points.
## E-C13 — C13 Flight Data Recorder
**Tracker**: pending | **Type**: component | **T-shirt**: L | **Story points**: 2134
**Tracker**: AZ-248 | **Type**: component | **T-shirt**: L | **Story points**: 2134
### System context
@@ -598,7 +624,7 @@ T-shirt L; 2134 points.
## E-C7 — C7 On-Jetson Inference Runtime
**Tracker**: pending | **Type**: component | **T-shirt**: L | **Story points**: 2134
**Tracker**: AZ-249 | **Type**: component | **T-shirt**: L | **Story points**: 2134
### System context
@@ -706,7 +732,7 @@ T-shirt L; 2134 points.
## E-C6 — C6 Tile Cache + Spatial Index
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-250 | **Type**: component | **T-shirt**: M | **Story points**: 1321
### System context
@@ -810,7 +836,7 @@ Per `components/08_c6_tile_cache/tests.md`.
## E-C11 — C11 Tile Manager (TileDownloader + TileUploader)
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-251 | **Type**: component | **T-shirt**: M | **Story points**: 1321
### System context
@@ -921,7 +947,7 @@ Per `components/12_c11_tilemanager/tests.md`.
## E-C10 — C10 Pre-flight Cache Provisioning
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-252 | **Type**: component | **T-shirt**: M | **Story points**: 1321
### System context
@@ -1023,7 +1049,7 @@ Per `components/11_c10_provisioning/tests.md`.
## E-C12 — C12 Operator Pre-flight Tooling
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-253 | **Type**: component | **T-shirt**: M | **Story points**: 1321
### System context
@@ -1124,7 +1150,7 @@ Per `components/13_c12_operator_tooling/tests.md`.
## E-C1 — C1 Visual / Visual-Inertial Odometry
**Tracker**: pending | **Type**: component | **T-shirt**: XL | **Story points**: 3455
**Tracker**: AZ-254 | **Type**: component | **T-shirt**: XL | **Story points**: 3455
### System context
@@ -1230,7 +1256,7 @@ Per `components/01_c1_vio/tests.md` + suite-level FT-P-02 / FT-P-04 / FT-P-05.
## E-C2 — C2 Visual Place Recognition
**Tracker**: pending | **Type**: component | **T-shirt**: L | **Story points**: 2134
**Tracker**: AZ-255 | **Type**: component | **T-shirt**: L | **Story points**: 2134
### System context
@@ -1331,7 +1357,7 @@ Per `components/02_c2_vpr/tests.md`.
## E-C2.5 — C2.5 Inlier-based Re-rank
**Tracker**: pending | **Type**: component | **T-shirt**: S | **Story points**: 58
**Tracker**: AZ-256 | **Type**: component | **T-shirt**: S | **Story points**: 58
### System context
@@ -1425,7 +1451,7 @@ Per `components/03_c2_5_rerank/tests.md`.
## E-C3 — C3 Cross-Domain Matcher
**Tracker**: pending | **Type**: component | **T-shirt**: L | **Story points**: 2134
**Tracker**: AZ-257 | **Type**: component | **T-shirt**: L | **Story points**: 2134
### System context
@@ -1523,7 +1549,7 @@ Per `components/04_c3_matcher/tests.md`.
## E-C3.5 — C3.5 AdHoP-Conditional Refinement
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 813
**Tracker**: AZ-258 | **Type**: component | **T-shirt**: M | **Story points**: 813
### System context
@@ -1617,7 +1643,7 @@ Per `components/05_c3_5_adhop/tests.md`.
## E-C4 — C4 Pose Estimator
**Tracker**: pending | **Type**: component | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-259 | **Type**: component | **T-shirt**: M | **Story points**: 1321
### System context
@@ -1720,7 +1746,7 @@ Per `components/06_c4_pose/tests.md`.
## E-C5 — C5 State Estimator
**Tracker**: pending | **Type**: component | **T-shirt**: XL | **Story points**: 3455
**Tracker**: AZ-260 | **Type**: component | **T-shirt**: XL | **Story points**: 3455
### System context
@@ -1835,7 +1861,7 @@ Per `components/07_c5_state/tests.md`.
## E-C8 — C8 FC + GCS Adapter
**Tracker**: pending | **Type**: component | **T-shirt**: L | **Story points**: 2134
**Tracker**: AZ-261 | **Type**: component | **T-shirt**: L | **Story points**: 2134
### System context
@@ -1948,7 +1974,7 @@ Per `components/10_c8_fc_adapter/tests.md`.
## E-BBT — Blackbox Tests (FT/NFT scenarios)
**Tracker**: pending | **Type**: tests | **T-shirt**: M | **Story points**: 1321
**Tracker**: AZ-262 | **Type**: tests | **T-shirt**: M | **Story points**: 1321
### System context
@@ -2042,6 +2068,155 @@ This epic IS the testing strategy for system-level scenarios. Per-component test
---
## E-DEMO-REPLAY — Offline replay mode (video + tlog → per-tick coordinate stream)
**Tracker**: AZ-265
**Type**: feature (deployment-adjacent)
**T-shirt**: M | **Story points**: 2732
**Added**: Decompose Step 2 (cycle 1, 2026-05-10)
**Source notes**: `_docs/how_to_test.md` (user-written demo requirements — auto-sync incorporated as child task #8)
### System context
Demonstrate the GPS-denied positioning pipeline against historical flight data: a video file from the nav camera + a `.tlog` file from the FC. The replay mode runs the **same C1C5 inference pipeline** the airborne binary runs; only the input transport (live camera → video file; live MAVLink → tlog) and output sink (FC MAVLink emit → JSONL) differ. NO ROS dependency is added — replay reuses the existing C8 `FcAdapter` interface via the strategy pattern.
```mermaid
flowchart LR
subgraph LIVE[Airborne mode — unchanged]
CAM[Live camera] --> C1L[C1 VIO]
FCL[Live FC MAVLink] --> C8L[C8 inbound]
C8L --> C1L
C1L --> C2L[C2..C5]
C2L --> C8OL[C8 outbound] --> FCL
end
subgraph REPLAY[Replay mode — this epic]
VID[Video file .mp4/.h264] --> VFFS[VideoFileFrameSource] --> C1R[C1 VIO]
TLOG[tlog file] --> TLR[TlogReplayFcAdapter] --> C1R
C1R --> C2R[C2..C5]
C2R --> RSINK[JsonlReplaySink] --> JSONL[results.jsonl - one EstimatorOutput per tick]
end
```
### Problem / Context
The parent-suite UI (in `ui/` workspace, out of scope for this repo) needs to demo the GPS-denied positioning end-to-end. Per-component fixtures or simulators would not give the demo end-to-end fidelity. Instead, replay mode runs the production pipeline against historical inputs — demo confidence equals field test confidence on the same footage.
ROS as the input transport was considered and rejected: the system is MAVLink-native; introducing ROS would (a) add a major new dependency, (b) split production vs. demo code paths, and (c) duplicate code. Reusing the existing C8 `FcAdapter` interface with a tlog-replay strategy is strictly better.
### Scope
**In scope**:
- `FrameSource` interface (formalised cross-cutting; previously implicit "camera ingest thread") + `VideoFileFrameSource` strategy + `LiveCameraFrameSource` retrofit (no-op restructure of existing camera plumbing).
- `TlogReplayFcAdapter` strategy (new C8 `FcAdapter` impl) parsing pymavlink `.tlog` files and emitting `ImuWindow` / `AttitudeWindow` / `GpsHealth` / `FlightStateSignal` at tlog timestamp cadence.
- `ReplaySink` interface + `JsonlReplaySink` impl (one `EstimatorOutput` per line).
- `compose_replay(config) -> ReplayRoot` composition root extending E-CC-CONF (AZ-246).
- `Clock` injection (per R-DEMO-4) so timer-driven logic in C1C5 works in both wall-clock (live) and tlog-simulated (replay) modes.
- `gps-denied-replay` CLI: `--video PATH --tlog PATH --output results.jsonl --camera-calibration calib.json --config config.yaml --pace {realtime,asap} [--time-offset-ms N]`.
- Fourth Docker image `gps-denied-replay-cli` (Python + C1C5 + cpp/* + replay strategies; NO C6/C10/C11/C12; NO HTTP server).
- E2E replay test on a 12 min Derkachi clip + matching tlog asserting estimated track within ≤ 100 m of ground-truth GPS for ≥ 80 % of ticks.
**Out of scope**:
- ROS / ROS2 dependency.
- HTTP wrapper microservice (parent-suite UI backend shells out to the CLI; defer until subprocess-shape is proven insufficient).
- Modifying any C1C5 component to be replay-aware — they MUST remain mode-agnostic.
- C6 mid-flight write path (replay reads a pre-built tile cache; doesn't write).
### Architecture notes
- ADR-001 / ADR-002 / ADR-009 all apply unchanged.
- New `BUILD_*` flags: `BUILD_VIDEO_FILE_FRAME_SOURCE`, `BUILD_TLOG_REPLAY_ADAPTER`, `BUILD_REPLAY_SINK_JSONL`. Default ON for the new replay-cli binary; OFF for airborne, research, and operator-tooling.
- New cross-cutting `FrameSource` interface lives at `src/gps_denied_onboard/frame_source/` (Layer 1 Foundation per `module-layout.md` § layering).
- `compose_replay` lives in `runtime_root.py` alongside `compose_root` and `compose_operator`.
### Interface specification
```python
class FrameSource(Protocol):
def next_frame(self) -> NavCameraFrame | None: ...
def close(self) -> None: ...
class VideoFileFrameSource(FrameSource):
def __init__(self, video_path: Path, frame_rate_hz: float, camera_id: str): ...
class TlogReplayFcAdapter(FcAdapter): # FcAdapter from AZ-261 / E-C8
def __init__(self, tlog_path: Path, target_fc_dialect: enum {ARDUPILOT, INAV}): ...
class ReplaySink(Protocol):
def emit(self, output: EstimatorOutput) -> None: ...
def close(self) -> None: ...
class JsonlReplaySink(ReplaySink):
def __init__(self, output_path: Path): ...
def compose_replay(config: Config) -> ReplayRoot: ...
```
### Data flow
Startup → load config / calibration → process tlog + video timestamp-aligned → for each frame: camera-ingest → C1 → C2 → C2.5 → C3 → C3.5 → C4 → C5 → emit `EstimatorOutput` to `JsonlReplaySink`. End of input → close sink → exit.
`--pace realtime` paces frames at wall-clock; `--pace asap` runs uncapped (default). The injected `Clock` is wall-clock-derived in `realtime` mode and tlog-timestamp-derived in `asap` mode so component fallback timers (e.g., AC-5.2 3 s no-estimate fallback) trigger consistently in both.
### Dependencies
- E-C1, E-C2, E-C2.5, E-C3, E-C3.5, E-C4, E-C5, E-C8 (every per-frame component).
- E-CC-CONF (AZ-246) for `compose_root` extension.
- E-CC-HELPERS (AZ-264) for `WgsConverter` (tlog GPS → local-tangent-plane).
- Does NOT depend on E-C6 / E-C10 / E-C11 / E-C12 (replay reads pre-built cache; no operator-side workflows).
### Acceptance criteria
- AC-1: CLI exits 0 on a valid 1-min fixture and produces JSONL with one `EstimatorOutput` line per tlog tick (within ±5 % of `GLOBAL_POSITION_INT` count).
- AC-2: Each line is a valid JSON object matching the `EstimatorOutput` schema.
- AC-3: For a fixture with known ground-truth GPS, the L2 horizontal distance ≤ 100 m for ≥ 80 % of ticks (matches AC-1.3 cumulative-drift bound).
- AC-4: Replay binary contains C1C5 + replay strategies; SBOM diff CI step verifies absence of C6/C10/C11/C12.
- AC-5: Same input → same output (deterministic) within ≤ 1e-6 float drift in position fields.
- AC-6: `--pace realtime` runs the 1-min fixture in 60 ± 5 s; `--pace asap` in ≤ 30 s on Tier-1 hardware.
- AC-7: Without `--time-offset-ms`, the CLI auto-detects the video ↔ tlog offset by correlating video motion-onset (or first-frame timestamp) with the tlog IMU take-off pattern (sustained vertical accel > 0.5 g + change in attitude rate > 1 rad/s lasting ≥ 0.5 s, matching the typical quadcopter take-off signature). On a fixture with known correct offset, the auto-detected offset is within ± 200 ms of ground truth. If auto-detect confidence is < 80 % the CLI logs a WARN and proceeds with the best-guess offset; `--time-offset-ms N` always overrides the auto-detect.
- AC-8: If neither auto-detect nor manual offset can produce > 95 % of frames with at least one matching IMU window within ± 100 ms, the CLI exits with code 2 and prints both the auto-detected offset (if any) and the percentage of frames-with-IMU-window so the operator can debug.
### Non-functional requirements
- Cold-start ≤ 5 s (not subject to AC-NEW-1's 30 s budget — that's airborne-only).
- Throughput ≥ 5 × real time on Jetson AGX Orin for `--pace asap`.
- Memory ≤ 4 GB resident (lean image; no FAISS index unless tile lookup is needed).
### Risks & mitigations
- **R-DEMO-1**: Tlog ↔ video timestamp drift across long flights, AND the more-common case that recordings on the operator workstation are not synchronised at all (camera and FC start independently, often minutes apart). **Mitigation**: auto-sync via IMU take-off detection (AC-7) is the default; `--time-offset-ms N` is the manual override. If take-off pattern is ambiguous (e.g., fixed-wing hand-launch instead of quadcopter, or tlog includes pre-arm motion), CLI WARNs and falls back to the manual override.
- **R-DEMO-2**: Pymavlink slow on multi-GB tlogs. **Mitigation**: stream-parse, never materialise; benchmark + document throughput floor.
- **R-DEMO-3**: Demo footage missing required FC messages (HIL mode etc.). **Mitigation**: CLI fails fast at startup listing missing message types and the components that need them.
- **R-DEMO-4**: Production C1C5 paths bake real-time-cadence assumptions (e.g., 5 s fallback timer). **Mitigation**: `Clock` injection (wall-clock for live, tlog-derived for replay); documented as ADR amendment in next architecture-doc cycle.
### Effort
T-shirt M; 2732 points across 8 child tasks.
### Child issues
| # | Title | Pts |
|---|-------|-----|
| 1 | `FrameSource` interface (cross-cutting) + `VideoFileFrameSource` strategy + `LiveCameraFrameSource` retrofit | 3 |
| 2 | `TlogReplayFcAdapter` strategy (pymavlink stream parser → inbound DTOs) | 5 |
| 3 | `ReplaySink` interface + `JsonlReplaySink` impl | 3 |
| 4 | `compose_replay(config)` + `Clock` injection (per R-DEMO-4) | 3 |
| 5 | `gps-denied-replay` CLI entrypoint + arg parser + camera-calibration loader | 3 |
| 6 | `gps-denied-replay-cli` Dockerfile + GitHub Actions matrix entry + SBOM diff (excludes C6/C10/C11/C12) | 3 |
| 7 | E2E replay fixture test (Derkachi 12 min clip + tlog; AC-3 ≤100 m ≥ 80 % assertion) | 5 |
| 8 | Auto-sync of video ↔ tlog via IMU take-off detection (AC-7 / AC-8; `--time-offset-ms` remains the manual override) | 5 |
### Key constraints
- ADR-001 / ADR-002 / ADR-009.
- C1C5 components MUST remain mode-agnostic; replay-aware logic lives only in the composition root, the new strategies, and the CLI.
- No HTTP server in any companion binary (airborne or replay); HTTP wrapper, if added later, lives in operator-tooling per `module-layout.md` Layer-4 placement.
### Testing strategy
Unit tests under `tests/unit/frame_source/`, `tests/unit/c8_fc_adapter/test_tlog_replay_adapter.py`, `tests/unit/c8_fc_adapter/test_replay_sink.py`, `tests/unit/cli/test_replay_cli.py`. E2E under `tests/e2e/replay/` running the CLI against the Derkachi fixture (Tier-1 capable; gated by `RUN_REPLAY_E2E=1` in CI). No FT/NFT scenarios at this epic — those live in E-BBT.
---
## Lessons applied (Step 6 step-0 retrospective)
`_docs/LESSONS.md` does not yet exist (this is the project's first cycle), so no prior estimation/architecture/dependencies lessons were folded into the sizing above. When this cycle ends, the Final step's quality checklist should propose a lessons file capturing: