[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
+217
View File
@@ -0,0 +1,217 @@
# GPS-Denied Onboard Pose Estimation — Planning Report
> Generated 2026-05-09 at the close of greenfield Plan (Step 3) — Final / Quality Checklist phase.
> Sources: `architecture.md`, `glossary.md`, `system-flows.md`, `data_model.md`, `risk_mitigations.md`, `epics.md`, `tests/traceability-matrix.md`, `tests/*.md`, `components/**/*.md`, `common-helpers/*.md`, `deployment/*.md`.
## Executive Summary
A Jetson Orin Nano Super-hosted Python+C++ companion replaces real GPS for fixed-wing UAVs in GPS-denied/spoofed environments by fusing pre-flight-cached `satellite-provider` tiles, live nav-camera frames (3 Hz), and FC IMU/attitude (100200 Hz) through a hierarchical pipeline `VIO → VPR → re-rank → matcher → AdHoP-conditional refinement → pose → state` and emits a WGS84 fix with honest 6×6 covariance + provenance label to the FC at 5 Hz, all inside a 400 ms p95 latency budget. Plan decomposes the system into **14 components + 8 common helpers**, **22 numbered system-level Mermaid flows across F1F10**, **39 acceptance criteria + 20 restrictions** with **92.4 % inclusive / 89.8 % strict test coverage**, and **19 Jira epics (AZ-244..AZ-262)** with a total estimated effort of **~295477 story points** (range across t-shirt sizes).
## Problem Statement
Fixed-wing UAVs operating in eastern/southern Ukraine must continue to navigate when the FC's GPS is denied or actively spoofed. The companion provides a drop-in replacement WGS84 fix derived from pre-cached `satellite-provider` imagery + onboard vision + FC IMU, with honest covariance and a `{satellite_anchored | visual_propagated | dead_reckoned}` provenance label, while remaining read-only against `satellite-provider` in flight (post-landing tile uploads run from the operator workstation only). Mission profile: 8 h flights, ~60 km/h cruise, ≤1 km AGL, ≤400 km² total cached area, 25 W TDP, -20 °C to +50 °C envelope.
## Architecture Overview
Single Python-with-C++-extensions monolithic process per binary track on the Jetson Orin Nano Super (8 GB shared LPDDR5, 25 W, JetPack 6.2). Cross-component coupling routes through a shared GTSAM substrate so posterior covariance is recovered natively (`Marginals.marginalCovariance`, D-C5-5 = (c)). Interface-first components live under `src/components/<component>/` with constructor-injected dependencies; per-binary CMake `BUILD_*` flags select which `Strategy` implementations are linked into each artifact (`production-binary` = OKVIS2 + KltRansac + UltraVPR + …; `research-binary` = all candidate strategies for the IT-12 comparative study). The operator-side Tile Manager (C11) is a separate binary excluded from the airborne CMake target — process-level isolation is the primary defence for ADR-004 (no in-flight `satellite-provider` I/O in either direction).
**Technology stack**: Python 3.10 + C++17, TensorRT 10.3 (JetPack 6.2 pin) / ONNX Runtime + TRT EP / pure PyTorch FP16 baseline; OpenCV ≥ 4.12.0; GTSAM iSAM2 + `IncrementalFixedLagSmoother`; FAISS HNSW; PostgreSQL 16 (`tiles` schema mirrored from `satellite-provider`); pymavlink + MAVLink 2.0 signing for ArduPilot, YAMSPy + INAV-Toolkit MSP2 for iNav.
**Deployment**: two execution tiers — Tier-1 workstation Docker (fast/cheap, AC-bound jobs that do not require Jetson hardware) and Tier-2 self-hosted Jetson runner (AC-NEW-1 / NFT-PERF / NFT-LIM); pre-flight + post-landing operator tooling runs on the operator workstation; companion image is bare JetPack 6.2 (no Docker on Tier-2 to keep INT8 calibration cache trustworthy per D-C10-6).
See `architecture.md` for the full ADR set (ADR-001..ADR-009), 12 architectural principles, and per-component intent statements.
## Component Summary
| # | Component | Purpose | Dependencies | Epic |
|---|-----------|---------|-------------|------|
| 01 | C1 VIO | Pluggable `VioStrategy` (OKVIS2 default, VINS-Mono research, KltRansac mandatory simple-baseline) producing per-frame `VioOutput` | C7, helpers (IMU preintegrator, SE3 utils) | AZ-254 |
| 02 | C2 VPR | Pre-cached satellite-tile retrieval (UltraVPR primary, MegaLoc/MixVPR/SelaVPR/EigenPlaces/NetVLAD/SALAD candidates) behind `VprStrategy` | C6, C7 | AZ-255 |
| 03 | C2.5 Re-rank | Single-pair LightGlue inlier-count rerank K=10 → N=3 | C2, C7, helper LightGlue runtime | AZ-256 |
| 04 | C3 Matcher | DISK+LightGlue cross-domain matching + RANSAC + reprojection residual filter | C2.5, C7, helper RANSAC filter | AZ-257 |
| 05 | C3.5 AdHoP | Conditional refinement when initial reprojection residual exceeds threshold | C3, C7 | AZ-258 |
| 06 | C4 Pose | OpenCV `solvePnPRansac` (IPPE) wrapped in GTSAM `Marginals` for native 6×6 covariance | C3.5, C5 (shared GTSAM substrate) | AZ-259 |
| 07 | C5 State | GTSAM iSAM2 + `IncrementalFixedLagSmoother` (K=1020); spoof-promotion gate; AC-4.5 internal smoothing | C1, C4, C8 inbound, C13 | AZ-260 |
| 08 | C6 Tile Cache | PostgreSQL btree spatial index + filesystem `./tiles/{zoom}/{x}/{y}.jpg` mirroring `satellite-provider`; FAISS HNSW VPR descriptor index | E-BOOT, E-CC-LOG, E-CC-CONF | AZ-250 |
| 09 | C7 Inference Runtime | TensorRT 10.3 engines (Polygraphy / trtexec / IBuilderConfig hybrid); ORT+TRT EP fallback; PyTorch FP16 baseline | E-BOOT, E-CC-CONF, E-CC-FDR-CLIENT | AZ-249 |
| 10 | C8 FC + GCS Adapter | `pymavlink` `GPS_INPUT` for ArduPilot (signed) + `MSP2_SENSOR_GPS` for iNav (unsigned, accepted residual risk); honest 6×6 → 2×2 covariance projection; GCS 12 Hz downsampled telemetry | C5, E-CC-CONF, E-CC-LOG | AZ-261 |
| 11 | C10 Pre-flight Cache Provisioning | Builds model-derived cache (descriptors, engines, manifest, content hashes); F2 takeoff verifier; does NOT touch `satellite-provider` (network I/O lives in C11) | C6, C7, E-CC-LOG | AZ-252 |
| 12 | C11 Tile Manager | Operator-side `TileDownloader` (pre-flight) + `TileUploader` (post-landing, gated `flight_state == ON_GROUND`); excluded from airborne image | C6, E-CC-CONF, E-CC-LOG | AZ-251 |
| 13 | C12 Operator Pre-flight Tooling | CLI subcommands (`download`, `build-cache`, `upload-pending`, `reloc-confirm`); sector classification UI hook; FDR retrieval helpers | C10, C11, E-CC-LOG | AZ-253 |
| 14 | C13 Flight Data Recorder | Per-flight ≤64 GB NVM ring (estimates + IMU + emitted MAVLink + health + mid-flight tiles + ≤0.1 Hz failed-tile thumbnails); raw nav/AI-cam frames excluded | E-BOOT, E-CC-LOG, E-CC-CONF, E-CC-FDR-CLIENT | AZ-248 |
**Cross-cutting epics** (not components, but shared concerns): E-BOOT (AZ-244), E-CC-LOG (AZ-245), E-CC-CONF (AZ-246), E-CC-FDR-CLIENT (AZ-247).
**System-level test epic**: E-BBT (AZ-262) parents the FT/NFT scenarios across all components.
**Implementation order** (from dependency graph in `epics.md`):
1. **Phase 1 (foundation, parallel)**: E-BOOT (AZ-244) → E-CC-LOG (AZ-245), E-CC-CONF (AZ-246), E-CC-FDR-CLIENT (AZ-247).
2. **Phase 2 (data + runtime + recorder)**: E-C13 (AZ-248), E-C7 (AZ-249), E-C6 (AZ-250).
3. **Phase 3 (operator tooling chain)**: E-C11 (AZ-251) → E-C10 (AZ-252) → E-C12 (AZ-253).
4. **Phase 4 (perception pipeline)**: E-C1 (AZ-254), E-C2 (AZ-255) → E-C2.5 (AZ-256) → E-C3 (AZ-257) → E-C3.5 (AZ-258) → E-C4 (AZ-259).
5. **Phase 5 (state + emission)**: E-C5 (AZ-260) → E-C8 (AZ-261).
6. **Phase 6 (system tests)**: E-BBT (AZ-262) — hardens behind every other epic.
## System Flows
| Flow | Description | Key Components |
|------|-------------|---------------|
| F1 | Pre-flight cache provisioning | C11 (TileDownloader), C6, C10, C12 |
| F2 | Takeoff load (cold-start TTFF, AC-NEW-1) | C10, C7, C6, C8, C5 |
| F3 | Steady-state per-frame estimation (the F3 hot path) | C1, C2, C2.5, C3, C3.5, C4, C5, C8, C13 |
| F4 | Mid-flight tile generation + local cache write | C5 (orthorectifier subpath), C6, C13 |
| F5 | Visual blackout + spoofed-GPS failsafe (AC-NEW-8) | C1, C5, C8 (STATUSTEXT) |
| F6 | Sharp-turn / disconnected-segment re-localization | C2, C2.5, C3, C5 |
| F7 | Spoofing-promotion via EKF source-set switch (AC-NEW-2) | C5 (SourceLabelStateMachine), C8 (AP D-C8-2) |
| F8 | Companion reboot recovery | C13 (FDR replay), C5, C8 |
| F9 | GCS telemetry stream (AC-6.1) | C8 (GcsAdapter) |
| F10 | Post-landing tile upload (D-PROJ-2 contract) | C11 (TileUploader), C6, C12 |
Reference `system-flows.md` for the per-flow Mermaid sequence diagrams + flowcharts; `_docs/02_document/diagrams/components.drawio` is the visual companion for component boundaries.
## Risk Summary
| Level | Count | Key Risks |
|-------|-------|-----------|
| Critical | 0 | — |
| High | 3 | R01 (D-PROJ-2 ingest endpoint not yet shipped), R03 (MAVLink-2.0 signing handshake no precedent — IT-3 gated), R11 (AC-NEW-4/AC-NEW-7 multi-flight statistical headroom — D-PROJ-3 deferred) |
| Medium | 8 | R02 (ADR-004 process-isolation regression), R04 (TRT engine SM/JP/TRT mismatch), R05 (iSAM2 silent factor-add failure), R06 (VPR top-1 false positive), R07 (premature spoof re-promotion), R08 (tile freshness drift in active-conflict sectors), R10 (`Marginals` latency under thermal throttle), R12 (single deployment camera) |
| Low | 3 | R09 (per-flight signing key compromise), R13 (FDR queue overrun), R14 (C2.5 ↔ C3 LightGlue circular dependency — resolved via shared helper) |
**Iterations completed**: 1 (initial Step 4 risk pass; R02 enforcement scope extended, R14 resolved via helper ownership).
**All Critical/High risks mitigated**: Yes — R01 has e2e mock-suite-sat-service fixture + leftover tracking; R03 is gated by IT-3 with documented D-C8-2-FALLBACK options (ADR-008); R11 is mitigated by AC-text relaxation 2026-05-09 (Monte-Carlo-with-CI) plus D-PROJ-3 carryforward.
Reference `risk_mitigations.md` for the full register, per-risk trigger conditions, and detailed mitigations.
## Test Coverage
The test suite is organised as scenario specs (no source code yet). Per-component tests live under each component's `tests.md`; cross-component / system-level scenarios live under `_docs/02_document/tests/`.
### Per-component scenario counts (from each `components/*/tests.md`)
| Component | Component-internal tests file |
|-----------|------------------------------|
| C1 | `components/01_c1_vio/tests.md` |
| C2 | `components/02_c2_vpr/tests.md` |
| C2.5 | `components/03_c2_5_rerank/tests.md` |
| C3 | `components/04_c3_matcher/tests.md` |
| C3.5 | `components/05_c3_5_adhop/tests.md` |
| C4 | `components/06_c4_pose/tests.md` |
| C5 | `components/07_c5_state/tests.md` |
| C6 | `components/08_c6_tile_cache/tests.md` |
| C7 | `components/09_c7_inference/tests.md` |
| C8 | `components/10_c8_fc_adapter/tests.md` |
| C10 | `components/11_c10_provisioning/tests.md` |
| C11 | `components/12_c11_tilemanager/tests.md` |
| C12 | `components/13_c12_operator_tooling/tests.md` |
| C13 | `components/14_c13_fdr/tests.md` |
### System-level scenario suites (`_docs/02_document/tests/`)
| File | Scenario family |
|------|-----------------|
| `tests/blackbox-tests.md` | FT-P-* (positive functional) + FT-N-* (negative functional) |
| `tests/performance-tests.md` | NFT-PERF-* (Tier-2) |
| `tests/resource-limit-tests.md` | NFT-LIM-* |
| `tests/security-tests.md` | NFT-SEC-* |
| `tests/resilience-tests.md` | NFT-RES-* |
| `tests/test-data.md` | Fixture inventory (Derkachi flight + AerialVL S03 + e2e mock-suite-sat-service) |
| `tests/environment.md` | Test environment + Tier-1/Tier-2 split |
| `tests/traceability-matrix.md` | Single source of truth for AC ↔ scenario coverage |
### Acceptance Criteria + Restrictions coverage (from `tests/traceability-matrix.md`, revised 2026-05-09)
| Category | Total | Covered | PARTIAL | Not Covered | Coverage % (PARTIAL counted half) |
|----------|-------|---------|---------|-------------|------------------------------------|
| Acceptance Criteria | 39 | 35 | 2 | 2 | 92.3 % |
| Restrictions | 20 | 18 | 1 | 1 | 92.5 % |
| **Total** | **59** | **53** | **3** | **3** | **92.4 %** (strict 89.8 %) |
Both the inclusive reading (PARTIAL = covered) and the strict reading clear the 75 % gate with margin. Remaining PARTIAL / NOT COVERED rows: AC-8.6 scene-change subset (labeled change-pair dataset not available), AC-NEW-5 hot-soak chamber (physical hardware), AC-7.1 / AC-7.2 + RESTRICT-CAM-2 (no AI-camera fixture), RESTRICT-HW-2 chamber portion (paired with AC-NEW-5).
## Epic Roadmap
| Order | Epic | Component | T-shirt | Story points | Dependencies |
|-------|------|-----------|---------|--------------|-------------|
| 1 | AZ-244: E-BOOT — Bootstrap & Initial Structure | repo scaffolding | M | 1321 | — |
| 2 | AZ-245: E-CC-LOG — Structured JSON Logging | cross-cutting | S | 58 | E-BOOT |
| 3 | AZ-246: E-CC-CONF — Configuration & Composition Root | cross-cutting | S | 58 | E-BOOT |
| 4 | AZ-247: E-CC-FDR-CLIENT — FDR Producer Client | cross-cutting | M | 813 | E-BOOT, E-CC-LOG |
| 5 | AZ-248: E-C13 — Flight Data Recorder | C13 | L | 2134 | E-BOOT, E-CC-LOG, E-CC-CONF, E-CC-FDR-CLIENT |
| 6 | AZ-249: E-C7 — On-Jetson Inference Runtime | C7 | L | 2134 | E-BOOT, E-CC-CONF, E-CC-FDR-CLIENT |
| 7 | AZ-250: E-C6 — Tile Cache + Spatial Index | C6 | M | 1321 | E-BOOT, E-CC-LOG, E-CC-CONF |
| 8 | AZ-251: E-C11 — Tile Manager | C11 | M | 1321 | E-C6, E-CC-CONF, E-CC-LOG |
| 9 | AZ-252: E-C10 — Pre-flight Cache Provisioning | C10 | M | 1321 | E-C6, E-C7, E-CC-LOG |
| 10 | AZ-253: E-C12 — Operator Pre-flight Tooling | C12 | M | 1321 | E-C10, E-C11, E-CC-LOG |
| 11 | AZ-254: E-C1 — Visual / Visual-Inertial Odometry | C1 | XL | 3455 | E-BOOT, E-CC-FDR-CLIENT, E-C7 |
| 12 | AZ-255: E-C2 — Visual Place Recognition | C2 | L | 2134 | E-C6, E-C7, E-CC-FDR-CLIENT |
| 13 | AZ-256: E-C2.5 — Inlier-based Re-rank | C2.5 | S | 58 | E-C2, E-C7, E-C6 (shared LightGlue helper) |
| 14 | AZ-257: E-C3 — Cross-Domain Matcher | C3 | L | 2134 | E-C2.5, E-C7 |
| 15 | AZ-258: E-C3.5 — AdHoP-Conditional Refinement | C3.5 | M | 813 | E-C3, E-C7 |
| 16 | AZ-259: E-C4 — Pose Estimator | C4 | M | 1321 | E-C3.5, E-C5 (shared GTSAM substrate) |
| 17 | AZ-260: E-C5 — State Estimator | C5 | XL | 3455 | E-C1, E-C4 (shared graph), E-CC-FDR-CLIENT |
| 18 | AZ-261: E-C8 — FC + GCS Adapter | C8 | L | 2134 | E-C5, E-CC-CONF, E-CC-LOG |
| 19 | AZ-262: E-BBT — Blackbox Tests (FT/NFT scenarios) | system-level tests | M | 1321 | every component epic |
**Total estimated effort**: 295477 story points across 19 epics (sum of t-shirt-band lower / upper bounds from `epics.md`). The two XL epics (E-C1 = AZ-254 and E-C5 = AZ-260) carry ~22 % of the upper-bound estimate alone.
## Key Decisions Made
| # | Decision | Rationale | Alternatives Rejected |
|---|----------|-----------|----------------------|
| 1 | Single Python+C++ monolith per binary track (ADR-001) | Latency budget (400 ms p95) + GTSAM substrate sharing + Jetson 8 GB shared memory | Multi-process (D-C1-1-SUB-A=(b)) — too much IPC overhead; ROS — adds runtime weight without flight-controller benefit |
| 2 | Build-time exclusion of unused `Strategy` implementations via per-binary CMake `BUILD_*` flags (ADR-002) | Binary size on 8 GB shared Jetson, AC-NEW-1 30 s boot budget, accidental-selection risk | Runtime-only selection — bigger binary, larger attack surface, harder cold-start; component licenses do not drive this decision |
| 3 | Shared GTSAM substrate between C4 (pose) and C5 (state) (ADR-003, D-C5-5 = (c)) | Native posterior covariance via `Marginals.marginalCovariance`; one numerical foundation | Independent EKF in C5 + Jacobian-only covariance — cannot deliver honest 6×6 |
| 4 | Process-level isolation of operator-side Tile Manager (ADR-004) — C11 not linked into airborne image | AC-8.4 in-flight no-write; defeats reflection / config-error attack vectors | Runtime gate alone — reflection / DI bug could re-introduce code path |
| 5 | Two execution tiers (ADR-005): Tier-1 workstation Docker + Tier-2 self-hosted Jetson | Cost (Jetson runner saturated) + AC-NEW-1 must run on real hardware | Single-tier Jetson-only — too slow / expensive for pre-merge CI |
| 6 | D-CROSS-LATENCY-1 hybrid (ADR-006): K=3 baseline auto-degrades to K=2 + Jacobian covariance under thermal throttle | Preserves AC-4.1 at +50 °C ambient at the cost of ~510 % accuracy | Static K=2 — wastes covariance precision in nominal conditions; static K=3 — blows the budget under throttle |
| 7 | Spoof-promotion gate (ADR-008): re-promote only after ≥10 s `gps_health == STABLE_NON_SPOOFED` AND visual-consistency check passes | AC-NEW-2 / AC-NEW-8 floor; defends against attacker turning spoof off briefly | Time-only gate (≥30 s) — slower mission recovery, still fool-able by transient honest GPS during attack |
| 8 | Interface-first components with constructor injection (ADR-009) | Multiple interchangeable strategies on the same interface (C1 has 3, C2 has 6+, C8 has 2) — selection via composition root only | Service-locator / global registry — couples runtime to import order, breaks tests, breaks build-time exclusion |
| 9 | OpenCV pinned to ≥4.12.0 (Mode B Fact #112) | CVE-2025-53644 mitigation; IPPE flags for solvePnP D-C4-1=(b) | Older OpenCV — known CVE; newer beta — not pinned in JetPack 6.2 |
| 10 | DISK + LightGlue replaces SuperPoint+SuperGlue for cross-domain matching (D-C3-1 = (a)) | License — SP+SG is Magic Leap noncommercial canonical; DISK+LightGlue is BSD-3-Clause | SuperPoint+SuperGlue — license incompatibility; XFeat — promising but unproven cross-domain |
| 11 | AC-NEW-4 / AC-NEW-7 text relaxed 2026-05-09 to Monte-Carlo-over-current-data with stated 95 % CI | D-PROJ-3 multi-flight fixture acquisition is out of scope this cycle; literal "≥100 flights" wording cannot be met | Block planning on D-PROJ-3 — cycle would not close; relaxed wording is documented residual risk in R11 |
## Open Questions
| # | Question | Impact | Assigned To |
|---|----------|--------|-------------|
| 1 | D-PROJ-2: parent-suite `satellite-provider` ingest endpoint + multi-flight voting layer not yet implemented service-side | F10 post-landing upload depends on this; R01; e2e mock-suite-sat-service fixture stands in for tests | Parent suite (cross-workspace; tracked in `_docs/_process_leftovers/2026-05-09_satellite-provider-design-tasks.md`) |
| 2 | D-PROJ-3: multi-flight fixture acquisition (Maxar Open Data Ukraine + AerialVL S03 + own multi-flight data) | AC-NEW-4 / AC-NEW-7 statistical headroom; R11 carryforward to next Plan cycle | Project lead |
| 3 | D-C8-2 runtime gate: companion-driven `MAV_CMD_SET_EKF_SOURCE_SET` switch lacks deployed precedent | R03 — IT-3 ArduPilot SITL validation is the lock gate; D-C8-2-FALLBACK options recorded in ADR-008 | Onboard team (gated by IT-3) |
| 4 | D-C2-12: DINOv2-feature-based matcher evaluation as a possible C3 backbone alternative | Could close D-C3-1 retrain cost concern; carryforward to next Plan cycle | Onboard team |
| 5 | AC-NEW-5 hot-soak chamber test (25 W @ +50 °C, 8 h, no throttle) | Physical hardware required; release-tag-blocking gate; out of scope for data-acquisition this cycle | Project lead |
| 6 | AC-7.1 / AC-7.2 / RESTRICT-CAM-2: AI-camera object localization pipeline | Out of scope for this cycle; deferred to a follow-up Plan cycle scoped to AI-camera integration | Future cycle (next Plan) |
| 7 | AC-8.6 scene-change subset: needs labeled change-pair dataset | Stale-tile match in active-conflict sectors — scene-change recall unmeasured (scale-ratio half is covered) | Future cycle |
## Artifact Index
| File | Description |
|------|-------------|
| `architecture.md` | System architecture, ADR set (ADR-001..ADR-009), 12 architectural principles, technology stack |
| `glossary.md` | Canonical project terminology (locked Phase 2a.0) |
| `system-flows.md` | F1F10 Mermaid sequence diagrams + flowcharts + per-flow data flow tables |
| `data_model.md` | PostgreSQL `tiles` (mirrored from `satellite-provider`) + flight + manifest schema; FAISS / TRT artifact layout; FDR record schema |
| `risk_mitigations.md` | 14-row risk register (R01..R14) with per-risk mitigation + contingency |
| `epics.md` | Local plan E-* IDs, t-shirt sizes, story-point ranges, dependencies, child-issue breakdowns; canonical `E-*``AZ-NN` mapping |
| `components/01_c1_vio/description.md``components/14_c13_fdr/description.md` | Per-component spec (interface, implementations, dependencies, ACs, NFRs, risks) |
| `components/01_c1_vio/tests.md``components/14_c13_fdr/tests.md` | Per-component test scenarios |
| `common-helpers/01_helper_imu_preintegrator.md``common-helpers/08_helper_descriptor_normaliser.md` | Shared helpers (IMU preintegrator, SE3 utils, LightGlue runtime, WGS converter, SHA-256 sidecar, engine filename schema, RANSAC filter, descriptor normaliser) |
| `tests/traceability-matrix.md` | AC ↔ scenario coverage; restriction ↔ scenario coverage |
| `tests/blackbox-tests.md`, `tests/performance-tests.md`, `tests/security-tests.md`, `tests/resource-limit-tests.md`, `tests/resilience-tests.md` | System-level FT / NFT scenario specs |
| `tests/test-data.md`, `tests/environment.md` | Fixture inventory + Tier-1/Tier-2 environment definition |
| `deployment/containerization.md`, `deployment/ci_cd_pipeline.md`, `deployment/environment_strategy.md`, `deployment/observability.md`, `deployment/deployment_procedures.md` | Deployment plan |
| `diagrams/components.drawio` | Component-level diagram (visual companion to `components/`) |
| `diagrams/flows/00_index.md` | Per-flow index pointing into `system-flows.md` |
## Quality Checklist Verification
All 8 checklist sections from `.cursor/skills/plan/steps/07_quality-checklist.md` pass for this cycle:
- **Blackbox Tests**: every AC + restriction in `tests/traceability-matrix.md`; restrictions verified by ≥1 scenario each (RESTRICT-CAM-2 deferred with documented mitigation); positive (FT-P) + negative (FT-N) balanced; Tier-1 Docker + Tier-2 Jetson defined in `tests/environment.md`; consumer treats system as black box (FC + GCS contracts only); CI integration in `deployment/ci_cd_pipeline.md`.
- **Architecture**: covers all `solution.md` capabilities; technology choices justified in § 2 of `architecture.md` + ADR-001..ADR-009; deployment model in `deployment/`; blackbox findings F6F10 reflected (Tier split, mock-suite-sat-service, two-binary CI emit).
- **Data Model**: every persistent entity from `architecture.md` § 4 defined in `data_model.md`; relationships have explicit FK cardinality; migration strategy is additive-only with ADR-recorded deprecation requirement; seed data = `satellite-provider` mirror of `tiles`; backward compatibility = `tiles` schema frozen on canonical columns.
- **Deployment**: containerization covers Tier-1 workstation Docker; CI/CD pipeline includes lint, test, security (SBOM diff, ASan), build, deploy stages; environment strategy covers dev / staging / production (Tier-1 + Tier-2 + Jetson production image); observability covers structured JSON logging + FDR + GCS STATUSTEXT; deployment procedures include rollback (per-flight key zeroisation, FDR rollover) and health checks (F2 takeoff verifier).
- **Components**: 14 components each follow SRP; no circular dependencies (R14 resolved via shared LightGlue helper); inter-component interfaces defined as `Protocol`/`ABC` per ADR-009; no orphan components — every component appears in at least one F1F10 flow; every blackbox scenario traceable through the component dependency graph.
- **Risks**: all High risks have mitigations + contingency (R01, R03, R11); mitigations reflected in `architecture.md` (ADR-004 enforcement scope, ADR-008 D-C8-2-FALLBACK), `tests/security-tests.md` (NFT-SEC-02 egress test), and `tests/traceability-matrix.md` (AC-text relaxation).
- **Tests**: every AC + restriction covered by ≥1 test (with PARTIAL/NOT-COVERED items having documented mitigation); 4 test types per component represented where applicable (unit/contract inside per-component `tests.md` + integration/performance/security/resilience at system level); test data management defined in `tests/test-data.md`.
- **Epics**: E-BOOT (AZ-244) "Bootstrap & Initial Structure" present; E-BBT (AZ-262) "Blackbox Tests" present; every component maps to a component epic (C1..C8, C10..C13 → AZ-254, AZ-255, AZ-256, AZ-257, AZ-258, AZ-259, AZ-260, AZ-261, AZ-252, AZ-251, AZ-253, AZ-248); dependency order matches `epics.md` "Implementation order"; acceptance criteria are measurable (per-epic IT/PT/ST IDs trace back to `traceability-matrix.md`).
+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:
+433
View File
@@ -0,0 +1,433 @@
# Module Layout
**Language**: python (with C++ native libraries linked via pybind11 from a parallel `cpp/` tree)
**Layout Convention**: src-layout (single top-level package `src/gps_denied_onboard/`)
**Root**: `src/gps_denied_onboard/`
**Last Updated**: 2026-05-10
This file is the authoritative file-ownership map consumed by the `/implement` skill (Step 4 File Ownership). Per-task specs in `_docs/02_tasks/` remain purely behavioral — they do NOT carry file paths. All component → filesystem mapping lives here.
Bootstrap reference: `_docs/02_tasks/todo/AZ-263_initial_structure.md`. Architecture reference: `_docs/02_document/architecture.md` (ADR-001 monolith, ADR-002 build-time exclusion, ADR-009 interface-first DI).
## Layout Rules
1. The single top-level Python package is `src/gps_denied_onboard/`. All imports are rooted there. No sibling packages live under `src/`.
2. Each component owns ONE folder under `src/gps_denied_onboard/components/`. Folder name = component slug (lowercase, snake_case, e.g. `c1_vio`, `c2_vpr`, `c2_5_rerank`).
3. Cross-cutting concerns own ONE folder each directly under `src/gps_denied_onboard/`: `_types/`, `helpers/`, `config/`, `logging/`, `fdr_client/`, `frame_source/`, `clock/`. Plus `runtime_root.py` and `healthcheck.py` at the package root.
4. Native (C++) libraries live under `cpp/` (parallel to `src/`, NOT nested), built by CMake; per-component pybind11 wrappers live at `src/gps_denied_onboard/components/<component>/_native/<name>.py` and import the resulting `.so` from a CMake-known path.
5. **Public API surface per component** = the files listed in each component's `Public API` list below. Anything not listed is internal and MUST NOT be imported from another component.
6. The composition root is `src/gps_denied_onboard/runtime_root.py`. It is the ONLY place that may import concrete strategy implementations across components — every other cross-component dependency is constructor-injected against an interface (ADR-009).
7. Tests mirror the component graph 1:1 at `tests/unit/<component>/`. Cross-component scenarios live in `tests/integration/`, `tests/e2e/`, `tests/perf/`, `tests/security/`, `tests/resilience/`.
8. Build-time exclusion (ADR-002): each `<component>/_native/` and the corresponding `cpp/<lib>/` carry a CMake `BUILD_<NAME>` flag. The composition root validator refuses to wire a strategy whose flag is OFF.
## Per-Component Mapping
### Component: c1_vio
- **Epic**: AZ-254 (E-C1 VIO)
- **Directory**: `src/gps_denied_onboard/components/c1_vio/`
- **Public API**:
- `src/gps_denied_onboard/components/c1_vio/__init__.py` (re-exports `VioStrategy`, `VioOutput`)
- `src/gps_denied_onboard/components/c1_vio/interface.py` (`VioStrategy` Protocol)
- **Internal (do NOT import from other components)**:
- `src/gps_denied_onboard/components/c1_vio/okvis2.py` (production-default; links `cpp/okvis2/`)
- `src/gps_denied_onboard/components/c1_vio/vins_mono.py` (research-only; gated by `BUILD_VINS_MONO=ON`)
- `src/gps_denied_onboard/components/c1_vio/klt_ransac.py` (mandatory simple-baseline)
- `src/gps_denied_onboard/components/c1_vio/_native/`
- **Owns (exclusive write during implementation)**: `src/gps_denied_onboard/components/c1_vio/**`, `cpp/okvis2/**`, `cpp/vins_mono/**`, `cpp/klt_ransac/**`, `tests/unit/c1_vio/**`
- **Imports from**: `_types`, `helpers.imu_preintegrator`, `helpers.se3_utils`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c2_vpr`, `c5_state`, `c13_fdr`, `runtime_root`
### Component: c2_vpr
- **Epic**: AZ-255 (E-C2 VPR)
- **Directory**: `src/gps_denied_onboard/components/c2_vpr/`
- **Public API**:
- `__init__.py` (re-exports `VprStrategy`, `VprQuery`, `VprResult`)
- `interface.py` (`VprStrategy` Protocol)
- **Internal**:
- `ultra_vpr.py` (primary), `mega_loc.py`, `mix_vpr.py`, `sela_vpr.py`, `eigen_places.py`, `net_vlad.py`, `salad.py`
- `_native/`
- **Owns**: `src/gps_denied_onboard/components/c2_vpr/**`, `tests/unit/c2_vpr/**`
- **Imports from**: `_types`, `helpers.descriptor_normaliser`, `components.c6_tile_cache` (Public API only — TileStore query interface), `components.c7_inference` (InferenceRuntime), `config`, `logging`, `fdr_client`
- **Consumed by**: `c2_5_rerank`, `runtime_root`
### Component: c2_5_rerank
- **Epic**: AZ-256 (E-C2.5 Rerank)
- **Directory**: `src/gps_denied_onboard/components/c2_5_rerank/`
- **Public API**:
- `__init__.py` (re-exports `RerankStrategy`, `RerankResult`)
- `interface.py` (`RerankStrategy` Protocol)
- **Internal**:
- `inlier_based_reranker.py` (single-pair LightGlue inlier count K=10→N=3)
- **Owns**: `src/gps_denied_onboard/components/c2_5_rerank/**`, `tests/unit/c2_5_rerank/**`
- **Imports from**: `_types`, `helpers.lightglue_runtime`, `helpers.descriptor_normaliser`, `helpers.ransac_filter`, `helpers.se3_utils`, `components.c6_tile_cache` (Public API), `components.c7_inference`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c3_matcher`, `runtime_root`
### Component: c3_matcher
- **Epic**: AZ-257 (E-C3 Cross-Domain Matcher)
- **Directory**: `src/gps_denied_onboard/components/c3_matcher/`
- **Public API**:
- `__init__.py` (re-exports `CrossDomainMatcher`, `MatchResult`)
- `interface.py` (`CrossDomainMatcher` Protocol)
- **Internal**:
- `disk_lightglue.py` (DISK + LightGlue)
- `aliked_lightglue.py` (ALIKED + LightGlue)
- `xfeat.py`
- `_native/`
- **Owns**: `src/gps_denied_onboard/components/c3_matcher/**`, `tests/unit/c3_matcher/**`
- **Imports from**: `_types`, `helpers.lightglue_runtime` (R14: SHARED with C2.5 — owned by helper, NOT by C3), `helpers.descriptor_normaliser`, `helpers.se3_utils`, `components.c7_inference`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c3_5_adhop`, `runtime_root`
### Component: c3_5_adhop
- **Epic**: AZ-258 (E-C3.5 AdHoP Refinement)
- **Directory**: `src/gps_denied_onboard/components/c3_5_adhop/`
- **Public API**:
- `__init__.py` (re-exports `AdHoPRefinementStrategy`)
- `interface.py` (`AdHoPRefinementStrategy` Protocol)
- **Internal**: `default_refiner.py`
- **Owns**: `src/gps_denied_onboard/components/c3_5_adhop/**`, `tests/unit/c3_5_adhop/**`
- **Imports from**: `_types`, `helpers.ransac_filter`, `helpers.se3_utils`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c4_pose`, `runtime_root`
### Component: c4_pose
- **Epic**: AZ-259 (E-C4 Pose Estimator)
- **Directory**: `src/gps_denied_onboard/components/c4_pose/`
- **Public API**:
- `__init__.py` (re-exports `PoseEstimator`, `PoseEstimate`, `EstimatorOutput`)
- `interface.py` (`PoseEstimator` Protocol)
- **Internal**:
- `opencv_pnp_estimator.py` (OpenCV `solvePnPRansac` + GTSAM Marginals for covariance)
- `_native/` (GTSAM bindings via `cpp/gtsam_bindings/`)
- **Owns**: `src/gps_denied_onboard/components/c4_pose/**`, `cpp/gtsam_bindings/**` (shared with c5_state — see ownership note below), `tests/unit/c4_pose/**`
- **Imports from**: `_types`, `helpers.ransac_filter`, `helpers.se3_utils`, `helpers.wgs_converter`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c5_state`, `runtime_root`
> **Joint native ownership note**: `cpp/gtsam_bindings/` is a thin pybind11 wrapper used by both C4 (Marginals for covariance) and C5 (iSAM2 + IncrementalFixedLagSmoother). Implementation task for `cpp/gtsam_bindings/` is owned by **c5_state** (the heavier consumer); c4_pose imports it READ-ONLY. See Layering table below.
### Component: c5_state
- **Epic**: AZ-260 (E-C5 State Estimator)
- **Directory**: `src/gps_denied_onboard/components/c5_state/`
- **Public API**:
- `__init__.py` (re-exports `StateEstimator`, `EstimatorOutput`, `EstimatorHealth`)
- `interface.py` (`StateEstimator` Protocol)
- **Internal**:
- `gtsam_isam2_estimator.py` (production-default; iSAM2 + IncrementalFixedLagSmoother)
- `eskf_baseline.py` (mandatory simple-baseline)
- `_native/`
- **Owns**: `src/gps_denied_onboard/components/c5_state/**`, `cpp/gtsam_bindings/**` (primary owner; see joint-native note above), `tests/unit/c5_state/**`
- **Imports from**: `_types`, `helpers.imu_preintegrator`, `helpers.se3_utils`, `helpers.wgs_converter`, `components.c4_pose` (Public API: `PoseEstimate`), `config`, `logging`, `fdr_client`
- **Consumed by**: `c8_fc_adapter`, `c13_fdr`, `runtime_root`
### Component: c6_tile_cache
- **Epic**: AZ-250 (E-C6 Tile Cache & Vector Index)
- **Directory**: `src/gps_denied_onboard/components/c6_tile_cache/`
- **Public API**:
- `__init__.py` (re-exports `TileStore`, `Tile`, `TileQualityMetadata`, `TileRecord`, `SectorClassification`)
- `interface.py` (`TileStore` Protocol — query/get/put surface; concrete impls swappable)
- **Internal**:
- `postgres_filesystem_store.py` (Postgres mirror + filesystem mmap + FAISS HNSW; production-default)
- `_native/` (`cpp/faiss_index/` wrapper)
- `_alembic/` (migration scripts; `0001_initial.sql` shipped in bootstrap)
- **Owns**: `src/gps_denied_onboard/components/c6_tile_cache/**`, `cpp/faiss_index/**`, `tests/unit/c6_tile_cache/**`
- **Imports from**: `_types`, `helpers.sha256_sidecar`, `helpers.wgs_converter`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c2_vpr`, `c2_5_rerank`, `c3_matcher`, `c10_provisioning`, `c11_tile_manager`, `runtime_root`
### Component: c7_inference
- **Epic**: AZ-249 (E-C7 Inference Runtime)
- **Directory**: `src/gps_denied_onboard/components/c7_inference/`
- **Public API**:
- `__init__.py` (re-exports `InferenceRuntime`, `EngineCacheEntry`)
- `interface.py` (`InferenceRuntime` Protocol)
- **Internal**:
- `tensorrt_runtime.py` (production-default; TensorRT 10.3)
- `onnx_trt_runtime.py` (ONNX Runtime + TensorRT EP)
- `pytorch_fp16_runtime.py` (research-only baseline)
- **Owns**: `src/gps_denied_onboard/components/c7_inference/**`, `tests/unit/c7_inference/**`
- **Imports from**: `_types`, `helpers.engine_filename_schema`, `helpers.sha256_sidecar`, `config`, `logging`, `fdr_client`
- **Consumed by**: `c2_vpr`, `c2_5_rerank`, `c3_matcher`, `c10_provisioning`, `runtime_root`
### Component: c8_fc_adapter
- **Epic**: AZ-261 (E-C8 FC + GCS Adapter)
- **Replay extensions epic**: AZ-265 (E-DEMO-REPLAY) — adds `tlog_replay_adapter.py` + `replay_sink.py` as gated strategies
- **Directory**: `src/gps_denied_onboard/components/c8_fc_adapter/`
- **Public API**:
- `__init__.py` (re-exports `FcAdapter`, `GcsAdapter`, `ReplaySink`, `EmittedExternalPosition`)
- `interface.py` (`FcAdapter`, `GcsAdapter`, `ReplaySink` Protocols)
- **Internal**:
- `pymavlink_ardupilot_adapter.py` (ArduPilot Plane via pymavlink)
- `msp2_inav_adapter.py` (iNav via MSP2)
- `mavlink_gcs_adapter.py` (12 Hz downsampled summary to QGroundControl)
- `tlog_replay_adapter.py` (replay-only `FcAdapter`; gated `BUILD_TLOG_REPLAY_ADAPTER`; AZ-265)
- `replay_sink.py` (`ReplaySink` interface + `JsonlReplaySink` impl; gated `BUILD_REPLAY_SINK_JSONL`; AZ-265)
- **Owns**: `src/gps_denied_onboard/components/c8_fc_adapter/**`, `tests/unit/c8_fc_adapter/**`
- **Imports from**: `_types`, `helpers.wgs_converter`, `helpers.se3_utils`, `components.c5_state` (Public API: `EstimatorOutput`), `config`, `logging`, `fdr_client`, `clock` (for replay timer-injection)
- **Consumed by**: `c1_vio` (back-channel: ImuSample, AttitudeWindow), `c5_state` (back-channel: ImuSample, FlightStateSignal, GpsHealth), `runtime_root` (live + operator + replay binaries)
> **Back-channel note**: C8 is the source of inbound IMU / attitude / GPS-health signals from the FC. C1 and C5 receive these via constructor-injected `FcAdapter` (typed against the interface, not the concrete adapter). This is NOT a layering violation — C8's role spans both the outbound emit path AND the inbound telemetry source.
### Component: c10_provisioning
- **Epic**: AZ-252 (E-C10 Cache Provisioner)
- **Directory**: `src/gps_denied_onboard/components/c10_provisioning/`
- **Public API**:
- `__init__.py` (re-exports `CacheProvisioner`, `Manifest`, `EngineCacheEntry`)
- `interface.py` (`CacheProvisioner` Protocol)
- **Internal**:
- `default_provisioner.py` (engine compile + descriptors + manifest + content-hash gate)
- **Owns**: `src/gps_denied_onboard/components/c10_provisioning/**`, `tests/unit/c10_provisioning/**`
- **Imports from**: `_types`, `helpers.sha256_sidecar`, `helpers.engine_filename_schema`, `helpers.wgs_converter`, `components.c6_tile_cache` (Public API), `components.c7_inference` (Public API: engine compile surface), `config`, `logging`, `fdr_client`
- **Consumed by**: `c12_operator_tooling`, `runtime_root` (operator binary only — excluded from airborne via `BUILD_C10_PROVISIONING=OFF` for airborne build per ADR-002)
### Component: c11_tile_manager
- **Epic**: AZ-251 (E-C11 Tile Downloader/Uploader)
- **Directory**: `src/gps_denied_onboard/components/c11_tile_manager/`
- **Public API**:
- `__init__.py` (re-exports `TileDownloader`, `TileUploader`)
- `interface.py` (`TileDownloader`, `TileUploader` Protocols)
- **Internal**:
- `satellite_provider_downloader.py` (REST client against parent-suite `satellite-provider`)
- `satellite_provider_uploader.py` (post-landing batch upload, D-PROJ-2 ingest contract)
- **Owns**: `src/gps_denied_onboard/components/c11_tile_manager/**`, `tests/unit/c11_tile_manager/**`
- **Imports from**: `_types`, `helpers.sha256_sidecar`, `helpers.wgs_converter`, `components.c6_tile_cache` (Public API), `config`, `logging`, `fdr_client`
- **Consumed by**: `c12_operator_tooling`, `runtime_root` (operator binary only — `BUILD_C11_TILE_MANAGER=OFF` for airborne)
### Component: c12_operator_tooling
- **Epic**: AZ-253 (E-C12 Operator Pre-flight Tooling)
- **Directory**: `src/gps_denied_onboard/components/c12_operator_tooling/`
- **Public API**:
- `__init__.py` (re-exports `CacheBuildWorkflow`, `OperatorReLocService`)
- `interface.py`
- **Internal**:
- `cache_build_workflow.py` (CLI orchestrator)
- `operator_reloc_service.py` (CLI; GUI deferred per epic)
- `sector_classifier.py` (operator sets `SectorClassification` → C6)
- **Owns**: `src/gps_denied_onboard/components/c12_operator_tooling/**`, `tests/unit/c12_operator_tooling/**`
- **Imports from**: `_types`, `helpers.wgs_converter`, `components.c6_tile_cache` (Public API), `components.c10_provisioning` (Public API), `components.c11_tile_manager` (Public API), `config`, `logging`, `fdr_client`
- **Consumed by**: `runtime_root` (operator binary only — `BUILD_C12_OPERATOR_TOOLING=OFF` for airborne)
### Component: c13_fdr
- **Epic**: AZ-248 (E-C13 FDR Writer)
- **Directory**: `src/gps_denied_onboard/components/c13_fdr/`
- **Public API**:
- `__init__.py` (re-exports `FdrWriter`)
- `interface.py` (`FdrWriter` Protocol)
- **Internal**:
- `default_fdr_writer.py` (writer thread + segment rotation + ≤64 GB cap)
- **Owns**: `src/gps_denied_onboard/components/c13_fdr/**`, `tests/unit/c13_fdr/**`
- **Imports from**: `_types`, `fdr_client.records` (FdrRecord schema only — schema lives in cross-cutting fdr_client; the consumer-side writer lives here), `config`, `logging`
- **Consumed by**: `runtime_root` (every component's `fdr_client` producer ultimately writes to the C13 writer at process root)
> **C13 / fdr_client split**: the producer-side `FdrClient` (lock-free SPSC queue + record schema) lives in `src/gps_denied_onboard/fdr_client/` (cross-cutting; AZ-247 / E-CC-FDR-CLIENT). The consumer-side `FdrWriter` (writer thread + segment rotation) lives in `components/c13_fdr/` (AZ-248 / E-C13). This split is intentional: every component depends on the producer interface, but only the writer process implements the consumer.
## Shared / Cross-Cutting
### shared/_types
- **Directory**: `src/gps_denied_onboard/_types/`
- **Purpose**: Cross-component DTOs (NavCameraFrame, ImuSample, ImuWindow, AttitudeWindow, FlightStateSignal, GpsHealth, VioOutput, VprQuery, VprResult, RerankResult, MatchResult, PoseEstimate, EstimatorOutput, EstimatorHealth, Tile, TileQualityMetadata, TileRecord, SectorClassification, CameraCalibration, EmittedExternalPosition, Manifest, EngineCacheEntry). **Type-only stubs**: zero implementation logic.
- **Owned by**: AZ-263 (Bootstrap task); subsequent additions are type-only edits owned by the proposing component task.
- **Consumed by**: every component, every cross-cutting module, the composition root.
### shared/config
- **Directory**: `src/gps_denied_onboard/config/`
- **Purpose**: YAML config loader + validation + dataclass schemas (per-flight config + camera calibration JSON loader).
- **Owned by**: AZ-263 (Bootstrap); subsequent schema fields added by the consuming component task touching `schema.py` only.
- **Consumed by**: composition root + every component constructor that reads config.
### shared/logging
- **Directory**: `src/gps_denied_onboard/logging/`
- **Purpose**: Structured JSON logging (one JSON object per line; no narrative log lines).
- **Owned by**: AZ-245 (E-CC-LOG — Cross-Cutting Logging) — bootstrap creates the entrypoint stub satisfying the contract.
- **Consumed by**: every component (via `from gps_denied_onboard.logging.structured import get_logger`).
### shared/fdr_client
- **Directory**: `src/gps_denied_onboard/fdr_client/`
- **Purpose**: Producer-side API for FDR records (lock-free SPSC queue per producer, drop-oldest on overrun) + the FdrRecord schema.
- **Owned by**: AZ-247 (E-CC-FDR-CLIENT — Cross-Cutting FDR Client).
- **Consumed by**: every component's producer code path; the consumer (writer thread) is C13.
### shared/helpers/imu_preintegrator
- **Directory**: `src/gps_denied_onboard/helpers/imu_preintegrator.py`
- **Purpose**: IMU preintegration utility (see `_docs/02_document/common-helpers/01_helper_imu_preintegrator.md`).
- **Owned by**: AZ-264 (E-CC-HELPERS — Common Helpers); per-helper tasks live under that epic.
- **Consumed by**: c1_vio, c5_state.
### shared/helpers/se3_utils
- **Directory**: `src/gps_denied_onboard/helpers/se3_utils.py`
- **Purpose**: SE(3) math utilities (`02_helper_se3_utils.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c1_vio, c2_5_rerank, c3_matcher, c3_5_adhop, c4_pose, c5_state, c8_fc_adapter.
### shared/helpers/lightglue_runtime
- **Directory**: `src/gps_denied_onboard/helpers/lightglue_runtime.py`
- **Purpose**: Shared LightGlue inference runtime (`03_helper_lightglue_runtime.md`). **R14 fix**: this helper is the single owner; both C2.5 (single-pair inlier counter) and C3 (matcher) import it. Neither depends on the other.
- **Owned by**: AZ-264.
- **Consumed by**: c2_5_rerank, c3_matcher.
### shared/helpers/wgs_converter
- **Directory**: `src/gps_denied_onboard/helpers/wgs_converter.py`
- **Purpose**: WGS84 ↔ local-tangent-plane conversion utilities (`04_helper_wgs_converter.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c4_pose, c5_state, c6_tile_cache, c8_fc_adapter, c10_provisioning, c11_tile_manager, c12_operator_tooling.
### shared/helpers/sha256_sidecar
- **Directory**: `src/gps_denied_onboard/helpers/sha256_sidecar.py`
- **Purpose**: Content-hash sidecar files (D-C10-3 content-hash gate; `05_helper_sha256_sidecar.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c6_tile_cache, c7_inference, c10_provisioning, c11_tile_manager.
### shared/helpers/engine_filename_schema
- **Directory**: `src/gps_denied_onboard/helpers/engine_filename_schema.py`
- **Purpose**: Self-describing TensorRT engine filename schema (D-C10-7; `06_helper_engine_filename_schema.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c7_inference, c10_provisioning.
### shared/helpers/ransac_filter
- **Directory**: `src/gps_denied_onboard/helpers/ransac_filter.py`
- **Purpose**: Generic RANSAC inlier filter (`07_helper_ransac_filter.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c2_5_rerank, c3_5_adhop, c4_pose.
### shared/helpers/descriptor_normaliser
- **Directory**: `src/gps_denied_onboard/helpers/descriptor_normaliser.py`
- **Purpose**: Descriptor normalisation utility (`08_helper_descriptor_normaliser.md`).
- **Owned by**: AZ-264.
- **Consumed by**: c2_vpr, c2_5_rerank, c3_matcher.
### shared/frame_source
- **Directory**: `src/gps_denied_onboard/frame_source/`
- **Purpose**: `FrameSource` interface (formalised cross-cutting; previously implicit "camera ingest thread" in architecture) + `LiveCameraFrameSource` (existing live path, retrofitted) + `VideoFileFrameSource` (replay-only; reads `.mp4` / `.h264` and emits `NavCameraFrame` at configured FPS).
- **Owned by**: AZ-265 (E-DEMO-REPLAY); the interface itself + `LiveCameraFrameSource` retrofit are cycle-1 deliverables under AZ-265 child task #1 (Decompose Step 2 amendment — interface was previously implicit).
- **Consumed by**: `c1_vio` (constructor-injected), `runtime_root` (composes the right strategy per binary).
### shared/clock
- **Directory**: `src/gps_denied_onboard/clock/`
- **Purpose**: `Clock` interface + `WallClock` (live) + `TlogDerivedClock` (replay). Per R-DEMO-4: production C1C5 paths bake real-time-cadence assumptions (e.g., AC-5.2 3 s no-estimate fallback timer); injected `Clock` lets replay mode trip those timers consistently against tlog timestamps rather than wall-clock.
- **Owned by**: AZ-265 (E-DEMO-REPLAY) — child task #4 (`compose_replay` + `Clock` injection).
- **Consumed by**: `c1_vio`, `c5_state`, `c8_fc_adapter`, any component with timer-driven fallback logic; `runtime_root` (selects WallClock for live/research/operator, TlogDerivedClock for replay).
### shared/runtime_root
- **File**: `src/gps_denied_onboard/runtime_root.py`
- **Purpose**: Composition root — config → strategy resolution → graph wiring (ADR-009). The ONLY place that may import concrete strategy classes across components. Per-binary CMake `BUILD_*` flags + composition root validator enforce ADR-002 build-time exclusion. Hosts `compose_root(config)` (airborne), `compose_operator(config)` (operator), and `compose_replay(config)` (replay-cli).
- **Owned by**: AZ-263 (Bootstrap stub); per-component additions that wire a new strategy are owned jointly by the bootstrap epic and the consuming component task (touching `runtime_root.py` is allowed only via the explicit "wire-in" task in each component's epic). The `compose_replay` extension is owned by AZ-265 child task #4.
- **Consumed by**: the airborne binary entrypoint + the operator-tooling binary entrypoint + the research/comparative binary entrypoint + the replay-cli binary entrypoint.
### shared/cli/replay
- **File**: `src/gps_denied_onboard/cli/replay.py`
- **Purpose**: `gps-denied-replay` CLI entrypoint. Args: `--video PATH --tlog PATH --output results.jsonl --camera-calibration calib.json --config config.yaml --pace {realtime,asap} [--time-offset-ms N]`.
- **Owned by**: AZ-265 (E-DEMO-REPLAY) — child task #5.
- **Consumed by**: the `gps-denied-replay-cli` Docker image entrypoint; parent-suite UI backend (subprocess shell-out per AZ-265 architecture decision).
### shared/healthcheck
- **File**: `src/gps_denied_onboard/healthcheck.py`
- **Purpose**: Importable healthcheck callable used by Dockerfile `HEALTHCHECK CMD` and CI smoke.
- **Owned by**: AZ-263.
- **Consumed by**: companion-tier1 Dockerfile, operator-tooling Dockerfile, CI smoke job.
## Allowed Dependencies (Layering)
Read top-to-bottom; an upper layer may import from a lower layer but NEVER the reverse. Cross-layer violations are **Architecture** findings in code-review (High severity).
| Layer | Components / Modules | May import from |
|-------|---------------------|-----------------|
| 5. Entry / Composition | `runtime_root`, `cli/replay`, `healthcheck` | 1, 2, 3, 4 |
| 4. Adapters | c8_fc_adapter (incl. `tlog_replay_adapter` + `replay_sink`), c11_tile_manager, c10_provisioning, c12_operator_tooling, `frame_source/VideoFileFrameSource` + `frame_source/LiveCameraFrameSource` | 1, 2, 3 (limited — see notes) |
| 3. Domain (runtime path) | c1_vio, c2_vpr, c2_5_rerank, c3_matcher, c3_5_adhop, c4_pose, c5_state, c13_fdr | 1, 2 |
| 2. Infrastructure | c6_tile_cache, c7_inference | 1 |
| 1. Foundation (shared) | `_types`, `config`, `logging`, `fdr_client`, `helpers/*`, `frame_source` (interface only), `clock` | (none) |
**Layer-specific notes**:
- **Layer 3 → Layer 4 is BANNED**. Domain components must not import adapter-layer components. C1's reception of FC telemetry happens via a constructor-injected `FcAdapter` interface (the interface lives in `c8_fc_adapter` Public API) — C1 imports the *interface* from a Layer-4 component's Public API, which is technically a downward-pointing import on the dependency graph, but the runtime data flow is Layer 4 → Layer 3 (FC → C1). This is the standard "interface lives at the producer" Hexagonal pattern; flagged here so the cross-verification step (Step 4) doesn't false-positive it.
- **C3 → C2.5 is BANNED at runtime** (R14): both must import `helpers.lightglue_runtime` instead. Enforced by the absence of any `from gps_denied_onboard.components.c2_5_rerank import ...` line inside `c3_matcher/`.
- **`runtime_root.py` may import any component's concrete impl**; everywhere else, cross-component imports go through the consumed component's Public API only.
## Build-Time Exclusion Map (ADR-002)
Four binaries are built from this codebase: **airborne** (Tier-1 + Tier-2 production), **research** (IT-12 comparative-study, links every strategy), **operator-tooling** (pre-flight workflows on operator workstation), **replay-cli** (offline `gps-denied-replay` against video + tlog; AZ-265).
| CMake flag | Components / native libs gated | Airborne | Research | Operator-tooling | Replay-cli |
|-----------|-------------------------------|----------|----------|------------------|------------|
| `BUILD_OKVIS2` | c1_vio/okvis2, cpp/okvis2 | ON | ON | OFF | ON |
| `BUILD_VINS_MONO` | c1_vio/vins_mono, cpp/vins_mono | OFF | ON | OFF | OFF |
| `BUILD_KLT_RANSAC` | c1_vio/klt_ransac, cpp/klt_ransac | ON (mandatory baseline) | ON | OFF | ON |
| `BUILD_VPR_<variant>` (UltraVPR, MegaLoc, MixVPR, SelaVPR, EigenPlaces, NetVLAD, SALAD) | c2_vpr/<variant> | UltraVPR ON, others OFF | all ON | OFF | UltraVPR ON, others OFF |
| `BUILD_TENSORRT_RUNTIME` | c7_inference/tensorrt_runtime | ON | ON | ON (operator pre-compiles engines) | ON |
| `BUILD_PYTORCH_RUNTIME` | c7_inference/pytorch_fp16_runtime | OFF | ON | OFF | OFF |
| `BUILD_C10_PROVISIONING` | c10_provisioning | OFF | OFF | ON | OFF |
| `BUILD_C11_TILE_MANAGER` | c11_tile_manager | OFF | OFF | ON | OFF |
| `BUILD_C12_OPERATOR_TOOLING` | c12_operator_tooling | OFF | OFF | ON | OFF |
| `BUILD_GTSAM_BINDINGS` | cpp/gtsam_bindings (used by c4_pose + c5_state) | ON | ON | OFF | ON |
| `BUILD_FAISS_INDEX` | cpp/faiss_index (used by c6_tile_cache) | ON | ON | ON | OFF (replay reads pre-built cache only) |
| `BUILD_VIDEO_FILE_FRAME_SOURCE` | `frame_source/VideoFileFrameSource` (AZ-265) | OFF | OFF | OFF | ON |
| `BUILD_TLOG_REPLAY_ADAPTER` | `c8_fc_adapter/tlog_replay_adapter` (AZ-265) | OFF | OFF | OFF | ON |
| `BUILD_REPLAY_SINK_JSONL` | `c8_fc_adapter/replay_sink` (AZ-265) | OFF | OFF | OFF | ON |
| `BUILD_REPLAY_CLI` | `cli/replay.py` entrypoint + `compose_replay` wiring (AZ-265) | OFF | OFF | OFF | ON |
| `BUILD_LIVE_CAMERA_FRAME_SOURCE` | `frame_source/LiveCameraFrameSource` (AZ-265 retrofit) | ON | ON | OFF | OFF |
The composition root validator at startup refuses to wire a strategy whose `BUILD_*` flag is OFF (raises `ConfigurationError` pointing at the offending strategy name + the missing flag).
Build-time exclusion is enforced by:
- CMake reading `cmake/build_options.cmake` per binary target.
- Per-binary CI matrix entry in `.github/workflows/ci.yml` (4 parallel build jobs).
- `ci/sbom_diff.py` step asserting each binary's SBOM contains exactly the expected component set (e.g., the airborne SBOM MUST NOT contain `c11_tile_manager`; the replay-cli SBOM MUST contain C1C5 + replay strategies and MUST NOT contain `c10_provisioning`).
## Layout Conventions (reference)
| Language | Root | Per-component path | Public API file | Test path |
|----------|------|-------------------|-----------------|-----------|
| Python (this project) | `src/gps_denied_onboard/` | `src/gps_denied_onboard/components/<component>/` | `src/gps_denied_onboard/components/<component>/__init__.py` (re-exports) + `interface.py` | `tests/unit/<component>/` |
| Python (generic) | `src/<pkg>/` | `src/<pkg>/<component>/` | `src/<pkg>/<component>/__init__.py` | `tests/<component>/` |
| C# (.NET) | `src/` | `src/<Component>/` | `src/<Component>/<Component>.cs` | `tests/<Component>.Tests/` |
| Rust | `crates/` | `crates/<component>/` | `crates/<component>/src/lib.rs` | `crates/<component>/tests/` |
| TypeScript / React | `packages/` or `src/` | `src/<component>/` | `src/<component>/index.ts` | `src/<component>/__tests__/` |
| Go | `./` | `internal/<component>/` or `pkg/<component>/` | `internal/<component>/doc.go` | `internal/<component>/*_test.go` |
## Self-Verification Checklist
- [x] Every component in `_docs/02_document/components/` has a Per-Component Mapping entry (14 components: c1_vio, c2_vpr, c2_5_rerank, c3_matcher, c3_5_adhop, c4_pose, c5_state, c6_tile_cache, c7_inference, c8_fc_adapter, c10_provisioning, c11_tile_manager, c12_operator_tooling, c13_fdr).
- [x] Every shared / cross-cutting concern has a Shared section entry (_types, config, logging, fdr_client, frame_source, clock, helpers/* × 8, runtime_root, cli/replay, healthcheck).
- [x] Layering table covers every component; foundation at Layer 1.
- [x] No component's `Imports from` list points at a component in a higher layer (back-channel exception for C8 → C1/C5 documented as interface-at-producer pattern).
- [x] Paths follow Python `src/`-layout convention with single top-level package `gps_denied_onboard/`.
- [x] No two components own overlapping paths. Joint native ownership of `cpp/gtsam_bindings/` resolved: c5_state is primary owner; c4_pose READ-ONLY.
- [x] Replay-mode additions (AZ-265) covered: new `frame_source/` and `clock/` cross-cuttings, new C8 strategies (`tlog_replay_adapter`, `replay_sink`), new `cli/replay.py` entrypoint, and a fourth `replay-cli` binary added to the Build-Time Exclusion Map.
## How the implement skill consumes this
The `/implement` skill's Step 4 (File Ownership) reads this file and, for each task in the batch:
1. Resolve the task's Component field to a Per-Component Mapping entry.
2. Set OWNED = the component's `Owns` glob.
3. Set READ-ONLY = the Public API files of every component listed in `Imports from`, plus all `shared/*` Public API files.
4. Set FORBIDDEN = every other component's Owns glob.
Execution inside a batch is already sequential. This mapping is still required because it enforces scope discipline per task — preventing a task from drifting into files that belong to another component.