mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 11:01:13 +00:00
d7a17a8248
The 41 blackbox/e2e test tasks (AZ-406..AZ-446 under epic AZ-262) all declare Component=Blackbox Tests, but module-layout.md had no matching Per-Component Mapping entry. The implement skill's Step 4 (File Ownership) requires every batch's component to be resolvable in module-layout.md. Add a `blackbox_tests` entry in the Shared / Cross-Cutting section that owns the top-level `e2e/` directory (separate from `tests/`), documents the public-boundary discipline (no SUT imports), and clarifies that boundary-driven performance/resilience/security scenarios live under `e2e/tests/<category>/` rather than under `tests/perf|security|resilience/`. Also update Layout Rule #7 to reflect the harness split and the state file's sub_step to parse-and-detect-progress (Step 10 entry). Co-authored-by: Cursor <cursoragent@cursor.com>
508 lines
49 KiB
Markdown
508 lines
49 KiB
Markdown
# 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/`, `replay_input/`. 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>/`. In-process cross-component scenarios that import SUT source live under `tests/integration/`. The **blackbox / e2e** test harness — which MUST NOT import SUT source and exercises the system only via public boundaries (MAVLink / MSP2 / HTTP / filesystem) — lives at the repo-root `e2e/` directory and is owned by the `blackbox_tests` cross-cutting entry (Shared section). Performance, resilience, security, and resource-limit scenarios that are also boundary-driven likewise live under `e2e/tests/<category>/`; only in-process performance/security micro-tests (if any) would live under `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.
|
||
9. **AZ-507 cross-component contract surface** — the only places a `components/<X>/*.py` file may import are: its own subpackage (`gps_denied_onboard.components.<X>.*`), `_types/*`, `_types.inference_errors`, `helpers/*`, `config`, `logging`, `fdr_client`, `clock`, `frame_source` (interface only). Cross-component contracts (Protocols + typed exceptions) reach consumers through `_types/*` modules — DTOs in the canonical `_types` files (e.g. `_types.inference.EngineCacheEntry`), typed-error envelopes in `_types.inference_errors`, and consumer-side structural `Protocol` cuts defined locally inside each consuming component (e.g. `c10_provisioning.engine_compiler.CompileEngineCallable`). NEVER `from gps_denied_onboard.components.<other_component> import ...` — the AZ-270 `test_az270_compose_root.test_ac6_only_compose_root_imports_concrete_strategies` lint enforces this on every `components/**/*.py`. The composition root (`runtime_root/*`) is the single exception; it wires concrete strategies into duck-typed Protocol parameters via constructor injection. This rule is the architectural contract paired with the AZ-270 lint; see `architecture.md` § Cross-Component Contract Surface for the rationale.
|
||
|
||
## 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`, `config`, `logging`, `fdr_client`. The TileStore query surface (c6) and the InferenceRuntime surface (c7) are obtained via constructor-injected consumer-side structural Protocol cuts (see AZ-507 cross-component rule below); the composition root wires the concrete c6/c7 strategies in. NEVER `from gps_denied_onboard.components.c6_tile_cache import ...` or `from gps_denied_onboard.components.c7_inference import ...` inside `c2_vpr/*.py`.
|
||
- **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`, `RerankCandidate`, `RerankError` family, `C2_5RerankConfig`)
|
||
- `interface.py` (`ReRankStrategy` Protocol)
|
||
- `config.py` (`C2_5RerankConfig` dataclass; registered on import; `strategy`, `top_n`, `debug_per_frame_log` fields)
|
||
- `errors.py` (`RerankError`, `RerankBackboneError`, `RerankAllCandidatesFailedError`)
|
||
- **Internal**:
|
||
- `inlier_based_reranker.py` (`InlierCountReRanker` — single-pair LightGlue inlier count K=10→N=3, AZ-343; module-level `create()` factory entry-point consumed by `runtime_root.rerank_factory.build_rerank_strategy`; gated by `BUILD_RERANK_INLIER_COUNT`)
|
||
- **Owns**: `src/gps_denied_onboard/components/c2_5_rerank/**`, `src/gps_denied_onboard/runtime_root/rerank_factory.py`, `tests/unit/c2_5_rerank/**`
|
||
- **Imports from**: `_types`, `helpers.lightglue_runtime`, `helpers.feature_extractor` (AZ-343 scope expansion), `helpers.descriptor_normaliser`, `helpers.ransac_filter`, `helpers.se3_utils`, `clock`, `config`, `logging`, `fdr_client`. The `TileStore`, `TilePixelHandle`, and `TileCacheError` family from c6 are obtained via constructor-injected consumer-side structural Protocol cuts + shared DTOs in `_types` (see AZ-507 cross-component rule below); composition root wires the concrete c6 strategy in. NEVER `from gps_denied_onboard.components.c6_tile_cache import ...` inside `c2_5_rerank/*.py`.
|
||
- **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`, `MatcherHealth`, `CandidateMatchSet`, `MatcherError`, `MatcherBackboneError`, `InsufficientInliersError`, `C3MatcherConfig`)
|
||
- `interface.py` (`CrossDomainMatcher` Protocol)
|
||
- `config.py` (`C3MatcherConfig`)
|
||
- `errors.py` (error hierarchy)
|
||
- **Internal**:
|
||
- `_health_window.py` (`RollingHealthWindow` accumulator; constructor-injected into every concrete matcher)
|
||
- `disk_lightglue.py` (DISK + LightGlue, AZ-345)
|
||
- `aliked_lightglue.py` (ALIKED + LightGlue, AZ-346)
|
||
- `xfeat.py` (XFeat, AZ-347)
|
||
- `_native/`
|
||
- **Owns**: `src/gps_denied_onboard/components/c3_matcher/**`, `tests/unit/c3_matcher/**`, `src/gps_denied_onboard/runtime_root/matcher_factory.py`
|
||
- **Imports from**: `_types`, `helpers.lightglue_runtime` (R14: SHARED with C2.5 — owned by helper, NOT by C3), `helpers.ransac_filter`, `helpers.descriptor_normaliser`, `helpers.se3_utils`, `config`, `logging`, `fdr_client`. The InferenceRuntime surface (c7) is obtained via a constructor-injected consumer-side structural Protocol cut (see AZ-507 cross-component rule below); composition root wires the concrete c7 strategy in. NEVER `from gps_denied_onboard.components.c7_inference import ...` inside `c3_matcher/*.py`.
|
||
- **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 `ConditionalRefiner`, `C3_5RefinerConfig`)
|
||
- `interface.py` (`ConditionalRefiner` Protocol)
|
||
- `config.py` (`C3_5RefinerConfig`)
|
||
- `errors.py` (`RefinerError`, `RefinerBackboneError`, `RefinerConfigError` — held internal to the component; consumers reach them only via tests)
|
||
- **Internal**:
|
||
- `passthrough_refiner.py` (reference baseline; AZ-348)
|
||
- `adhop_refiner.py` (production-default; AZ-349 pending)
|
||
- **Owns**: `src/gps_denied_onboard/components/c3_5_adhop/**`, `tests/unit/c3_5_adhop/**`, `src/gps_denied_onboard/runtime_root/refiner_factory.py`
|
||
- **Imports from**: `_types`, `helpers.ransac_filter` (R14: SHARED with C3 and C4 — owned by helper, NOT by C3.5), `helpers.se3_utils`, `config`, `logging`, `fdr_client`. The InferenceRuntime surface (c7) is obtained via a constructor-injected consumer-side structural Protocol cut (see AZ-507 cross-component rule below); composition root wires the concrete c7 strategy in. NEVER `from gps_denied_onboard.components.c7_inference import ...` inside `c3_5_adhop/*.py`.
|
||
- **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` (`PoseEstimate` DTO lives here), `helpers.imu_preintegrator`, `helpers.se3_utils`, `helpers.wgs_converter`, `config`, `logging`, `fdr_client`. NEVER `from gps_denied_onboard.components.c4_pose import ...` inside `c5_state/*.py` — the `PoseEstimate` DTO is consumed exclusively via `_types`.
|
||
- **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` `__all__`; consult the module for the canonical list):
|
||
- `interface.py` (three Protocols: `TileStore`, `TileMetadataStore`, `DescriptorIndex` — query/get/put surface; concrete impls swappable)
|
||
- DTOs: `TileId`, `TileMetadata`, `TileMetadataPersistent`, `TileQualityMetadata`, `Bbox`, `SectorBoundary`, `HnswParams`, `IndexMetadata`
|
||
- Enums: `TileSource`, `FreshnessLabel`, `VotingStatus`, `SectorClassification`
|
||
- ABC: `TilePixelHandle`
|
||
- Config block: `C6TileCacheConfig` (registered on import)
|
||
- Error family rooted at `TileCacheError` with documented subtypes (`TileNotFoundError`, `TileFsError`, `TileMetadataError`, `ContentHashMismatchError`, `FreshnessRejectionError`, `CacheBudgetExhaustedError`, `IndexUnavailableError`) + sibling `IndexBuildError` (offline build envelope, not in the `TileCacheError` family)
|
||
- **Internal**:
|
||
- `faiss_descriptor_index.py` (AZ-306 — production-default `DescriptorIndex` strategy backed by the `faiss-cpu` PyPI wheel; HNSW32 search + atomic rebuild + triple-sidecar coherence + warm-up; `from_config` classmethod consumed by `runtime_root.storage_factory.build_descriptor_index`. Gated by `BUILD_FAISS_INDEX` at the factory boundary, NOT at module import.)
|
||
- `_tile_pixel_handle.py` (`TilePixelHandle` ABC)
|
||
- `_types.py` (DTOs / enums; consumed via the Public API re-exports)
|
||
- `_uuid_namespace.py` (AZ-304 — pinned `TILE_NAMESPACE_UUID` + `derive_tile_id` / `derive_location_hash` helpers; cross-repo coordinated with `satellite-provider`)
|
||
- `migrations.py` (AZ-304 — `apply_migrations(config) -> MigrationResult` runner invoked by the composition root at startup)
|
||
- `postgres_filesystem_store.py` (AZ-305 — production-default `TileStore` + `TileMetadataStore` impl over Postgres mirror + filesystem; `PostgresFilesystemStore.from_config` opens its own `psycopg_pool.ConnectionPool` and constructs an `AZ-307 FreshnessGate`)
|
||
- `freshness_gate.py` (AZ-307 — `ACTIVE_CONFLICT` reject + `STABLE_REAR` downgrade per `freshness_gate.md` v1.0.0)
|
||
- `cache_budget_enforcer.py` (AZ-308 — RESTRICT-SAT-2 10 GiB hard cap; `CacheBudgetEnforcer.reserve_headroom` + `BudgetEnforcedTileStore` write-decorator)
|
||
- `tools.py` (AZ-305 — operator dump CLI invoked via `python -m gps_denied_onboard.components.c6_tile_cache.tools ...`)
|
||
- `errors.py`, `config.py` (component plumbing)
|
||
- **Owns**: `src/gps_denied_onboard/components/c6_tile_cache/**`, `tests/unit/c6_tile_cache/**`, `db/migrations/**` (project-level Alembic env owned by c6 — `alembic.ini` at repo root points here; `0001_initial.py` shipped by AZ-263 bootstrap, `0002_c6_tile_identity_and_lru.py` and forward owned by AZ-304+ migrations). AZ-306 retired the `cpp/faiss_index/` placeholder in favour of the `faiss-cpu` PyPI wheel; the `BUILD_FAISS_INDEX` flag is preserved as a runtime/factory gate (consumed by `runtime_root.storage_factory`).
|
||
- **Imports from**: `_types`, `helpers.sha256_sidecar`, `helpers.wgs_converter`, `clock`, `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` `__all__`; consult the module for the canonical list):
|
||
- `interface.py` (`InferenceRuntime` Protocol)
|
||
- DTOs re-exported from `_types.inference` + `_types.thermal`: `BuildConfig`, `EngineCacheEntry`, `EngineHandle`, `OptimizationProfile`, `PrecisionMode`, `ThermalState`
|
||
- Component services: `EngineGate` + `HostTuple` (AZ-301), `ThermalStatePublisher` + `ThermalReading` + `ThermalSource` Protocol (AZ-302), `ManifestReader` + `ManifestReaderProtocol` + `DeploymentManifest` (AZ-301), `ArchitectureFactory` + `default_registry` + `register_architecture` (AZ-300)
|
||
- Config block: `C7InferenceConfig` (registered on import)
|
||
- Error family rooted at `RuntimeError` with documented subtypes (`InferenceError`, `EngineBuildError`, `EngineDeserializeError`, `EngineHashMismatchError`, `EngineSchemaMismatchError`, `EngineSidecarMissingError`, `CalibrationCacheError`, `OutOfMemoryError`, `TelemetryUnavailableError`)
|
||
- **Internal**:
|
||
- `architecture_registry.py` (AZ-300; family of registered `ArchitectureFactory` callables consumed by `PytorchFp16Runtime`)
|
||
- `config.py` (`C7InferenceConfig` dataclass; registered on import)
|
||
- `engine_gate.py` (AZ-301; D-C10-3 + D-C10-7 takeoff validator)
|
||
- `errors.py` (component error family)
|
||
- `manifest.py` (AZ-301; `DeploymentManifest` + `ManifestReader` for engine sidecar manifests)
|
||
- `onnx_trt_ep_runtime.py` (AZ-299; ONNX Runtime + TensorRT EP fallback strategy + per-flight ORT TRT subgraph cache + one-shot fallback WARN/FDR/GCS alert + CPU-fallback gate)
|
||
- `pytorch_fp16_runtime.py` (AZ-300; research-only / simple-baseline strategy)
|
||
- `tensorrt_runtime.py` (AZ-298; production-default TensorRT 10.3 strategy + INT8 calibration cache trust + GPU memory budget enforcement + `python -m ...tensorrt_runtime compile ...` CLI)
|
||
- `thermal_publisher.py` (AZ-302; 1 Hz background poller, jtop/NVML fallback)
|
||
- **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` + `noop_mavlink_transport.py` as gated strategies; live transport code is retrofitted as `SerialMavlinkTransport` behind a new `MavlinkTransport` Protocol seam (no behaviour change) so the C8 outbound encoders are byte-identical between live and replay (replay protocol Invariant 5)
|
||
- **Directory**: `src/gps_denied_onboard/components/c8_fc_adapter/`
|
||
- **Public API**:
|
||
- `__init__.py` (re-exports `FcAdapter`, `GcsAdapter`, `ReplaySink`, `MavlinkTransport`, `EmittedExternalPosition`)
|
||
- `interface.py` (`FcAdapter`, `GcsAdapter`, `MavlinkTransport` Protocols; `ReplaySink` Protocol lives in `replay_sink.py` per the replay contract)
|
||
- **Internal**:
|
||
- `pymavlink_ardupilot_adapter.py` (ArduPilot Plane via pymavlink)
|
||
- `msp2_inav_adapter.py` (iNav via MSP2)
|
||
- `mavlink_gcs_adapter.py` (1–2 Hz downsampled summary to QGroundControl)
|
||
- `tlog_replay_adapter.py` (replay-mode `FcAdapter`; gated `BUILD_TLOG_REPLAY_ADAPTER`; ON in airborne per ADR-011; AZ-265)
|
||
- `replay_sink.py` (`ReplaySink` interface + `JsonlReplaySink` impl; gated `BUILD_REPLAY_SINK_JSONL`; ON in airborne per ADR-011; AZ-265)
|
||
- `noop_mavlink_transport.py` (`NoopMavlinkTransport` for replay-mode outbound bytes; gated `BUILD_REPLAY_SINK_JSONL`; ON in airborne; AZ-265 / AZ-400)
|
||
- `serial_mavlink_transport.py` (`SerialMavlinkTransport` retrofit of the existing live-mode UART transport; AZ-265 / AZ-400 no-op restructure)
|
||
- **Owns**: `src/gps_denied_onboard/components/c8_fc_adapter/**`, `tests/unit/c8_fc_adapter/**`
|
||
- **Imports from**: `_types` (`EstimatorOutput` DTO lives here), `helpers.wgs_converter`, `helpers.se3_utils`, `config`, `logging`, `fdr_client`, `clock` (for replay timer-injection). NEVER `from gps_denied_onboard.components.c5_state import ...` inside `c8_fc_adapter/*.py` — the `EstimatorOutput` DTO is consumed exclusively via `_types`.
|
||
- **Consumed by**: `c1_vio` (back-channel: ImuSample, AttitudeWindow), `c5_state` (back-channel: ImuSample, FlightStateSignal, GpsHealth), `runtime_root` (live + operator binaries; replay is a mode of the airborne binary per ADR-011, not a separate composition root)
|
||
|
||
> **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`, plus AZ-321 surface: `EngineCompiler`, `BackboneSpec`, `EngineCompileRequest`, `EngineCompileResult`, `CompileOutcome`, `EngineCompileSummary`, `CompileEngineCallable`, `BackboneConfig`, `C10ProvisioningConfig`, plus AZ-322 surface: `DescriptorBatcher`, `BackboneEmbedder`, `C7EngineBackboneEmbedder`, `C10BatcherConfig`, `CorpusFilter`, `DescriptorBatchReport`, `ProgressEvent`, `TileBboxRecord`, `BatcherTile`, `TilesByBboxBatchQuery`, `TilePixelOpener`, `DescriptorIndexRebuilder`, `DescriptorBatchError`)
|
||
- `interface.py` (`CacheProvisioner` Protocol, `BackboneEmbedder` Protocol — AZ-322)
|
||
- Config block: `C10ProvisioningConfig` (registered on import)
|
||
- **Internal**:
|
||
- `engine_compiler.py` (AZ-321; per-model TRT compile + hardware-tied cache reuse + `CompileEngineCallable` structural cut of the C7 InferenceRuntime)
|
||
- `config.py` (AZ-321; `BackboneConfig` + `C10ProvisioningConfig` dataclasses)
|
||
- `descriptor_batcher.py` (AZ-322; `DescriptorBatcher` + DTOs + consumer-side Protocols `TilesByBboxBatchQuery` / `TilePixelOpener` / `DescriptorIndexRebuilder`)
|
||
- `c7_engine_embedder.py` (AZ-322; `C7EngineBackboneEmbedder` adapter wrapping AZ-297 `InferenceRuntime` + AZ-321 engine path)
|
||
- `default_provisioner.py` (engine compile + descriptors + manifest + content-hash gate, pending)
|
||
- Composition root: `runtime_root/c10_factory.py` (`build_engine_compiler`, `build_backbone_specs`, `build_manifest_builder`, `build_manifest_verifier`, `build_descriptor_batcher` + the C6→C10 adapters `c6_tile_metadata_store_to_tiles_batch_query`, `c6_tile_store_to_pixel_opener`, `c6_descriptor_index_to_rebuilder`)
|
||
- **Owns**: `src/gps_denied_onboard/components/c10_provisioning/**`, `tests/unit/c10_provisioning/**`
|
||
- **Imports from**: `_types` (cross-component DTOs `EngineCacheEntry`, `BuildConfig`, `PrecisionMode`, `OptimizationProfile`, `HostCapabilities`, `TileMetadata`, etc.), `_types.inference_errors` (AZ-507 typed-error envelope for `EngineBuildError` + `CalibrationCacheError`), `helpers.sha256_sidecar`, `helpers.engine_filename_schema`, `helpers.wgs_converter`, `config`, `logging`, `fdr_client`. The `InferenceRuntime.compile_engine` surface (c7) and the `TileMetadataStore.query_by_bbox` surface (c6) are obtained via constructor-injected consumer-side structural Protocol cuts (the `CompileEngineCallable` cut already lives in `engine_compiler.py`; AZ-323 / AZ-324 will define analogous `query_by_bbox` cuts inside `c10_provisioning/`). NEVER `from gps_denied_onboard.components.c6_tile_cache import ...` or `from gps_denied_onboard.components.c7_inference import ...` inside `c10_provisioning/*.py`.
|
||
- **Consumed by**: `c12_operator_orchestrator`, `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`, `config`, `logging`, `fdr_client`. The c6 storage surface (`TileStore`, `TileMetadataStore`) is obtained via constructor-injected consumer-side structural Protocol cuts (see AZ-507 cross-component rule below); composition root wires the concrete c6 strategy in. NEVER `from gps_denied_onboard.components.c6_tile_cache import ...` inside `c11_tile_manager/*.py`.
|
||
- **Consumed by**: `c12_operator_orchestrator`, `runtime_root` (operator binary only — `BUILD_C11_TILE_MANAGER=OFF` for airborne)
|
||
|
||
### Component: c12_operator_orchestrator
|
||
|
||
- **Epic**: AZ-253 (E-C12 Operator Pre-flight Orchestrator)
|
||
- **Directory**: `src/gps_denied_onboard/components/c12_operator_orchestrator/`
|
||
- **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_orchestrator/**`, `tests/unit/c12_operator_orchestrator/**`
|
||
- **Imports from**: `_types`, `helpers.wgs_converter`, `config`, `logging`, `fdr_client`. The c6 / c10 / c11 surfaces (`TileStore`, `TileMetadataStore`, `CacheProvisioner`, `TileDownloader`, `TileUploader`) are obtained via constructor-injected consumer-side structural Protocol cuts (see AZ-507 cross-component rule below); composition root wires the concrete c6/c10/c11 strategies in. NEVER `from gps_denied_onboard.components.c6_tile_cache import ...`, `from gps_denied_onboard.components.c10_provisioning import ...`, or `from gps_denied_onboard.components.c11_tile_manager import ...` inside `c12_operator_orchestrator/*.py`.
|
||
- **Consumed by**: `runtime_root` (operator binary only — `BUILD_C12_OPERATOR_ORCHESTRATOR=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/feature_extractor
|
||
|
||
- **Directory**: `src/gps_denied_onboard/helpers/feature_extractor.py`
|
||
- **Purpose**: Shared image → `KeypointSet` Protocol + placeholder `OpenCvOrbExtractor` impl (AZ-343 scope expansion). Lets every consumer that feeds `LightGlueRuntime.match` reach for the SAME extractor (same descriptor distribution, same `descriptor_dim`) without each strategy reinventing its own preprocessing.
|
||
- **Owned by**: AZ-343.
|
||
- **Consumed by**: c2_5_rerank (today via `InlierCountReRanker`), c3_matcher (future concrete strategies in AZ-345 / AZ-346 / AZ-347).
|
||
|
||
### 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_orchestrator.
|
||
|
||
### 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/helpers/iso_timestamps
|
||
|
||
- **File**: `src/gps_denied_onboard/helpers/iso_timestamps.py`
|
||
- **Purpose**: Single Layer-1 UTC ISO-8601 timestamp source for FDR record envelopes. Two surfaces — `iso_ts_now() -> str` (wall-clock, microsecond precision, format `YYYY-MM-DDTHH:MM:SS.ffffffZ`) and `iso_ts_from_clock(clock: Clock) -> str` (Clock-injected, nanosecond precision, format `YYYY-MM-DDTHH:MM:SS.fffffffffZ`). Both terminate with the canonical `Z` suffix matching the FDR `_TS` fixture. Replaces the duplicated private `_iso_ts_now` (AZ-508) and `_iso_ts_from_clock` (AZ-526) one-liners that previously lived inside `c6_tile_cache`, `c7_inference`, `c2_vpr`, and `c12_operator_orchestrator`. Stateless functions; stdlib + Layer-1 `gps_denied_onboard.clock.Clock` only.
|
||
- **Owned by**: AZ-264 (AZ-508 + AZ-526 consolidation tasks).
|
||
- **Consumed by**: c6_tile_cache (`cache_budget_enforcer`, `postgres_filesystem_store`, `freshness_gate`) + c7_inference (`onnx_trt_ep_runtime`, `thermal_publisher`) via `iso_ts_now`; c2_vpr (`net_vlad`, `ultra_vpr`, `_faiss_bridge`) + c12_operator_orchestrator (`operator_reloc_service`) via `iso_ts_from_clock`. Future C3 / C4 / C5 FDR producers should import the appropriate helper rather than redefining the one-liner locally.
|
||
|
||
### 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 + replay-realtime) + `TlogDerivedClock` (replay-asap). Per R-DEMO-4: production C1–C5 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) — task AZ-398 (`FrameSource` + `Clock`).
|
||
- **Consumed by**: `c1_vio`, `c5_state`, `c8_fc_adapter`, any component with timer-driven fallback logic; `runtime_root` (selects the strategy per `config.mode` + `config.replay.pace`).
|
||
|
||
### shared/replay_input
|
||
|
||
- **Directory**: `src/gps_denied_onboard/replay_input/`
|
||
- **Purpose**: Layer-4 cross-cutting coordinator that converges `(video, tlog)` inputs into the standard `FrameSource` + `FcAdapter` + `Clock` surfaces the airborne composition root consumes. Owns the time-alignment between video frames and tlog IMU/attitude ticks (manual via `--time-offset-ms` or automatic via the AZ-405 IMU-take-off detector). The composition root, in replay mode, builds a `ReplayInputAdapter`, calls `.open()`, and wires the returned `ReplayInputBundle` into the same C1–C5 pipeline as live. New under ADR-011 (replaces the v1.0.0 design where replay was a separate composition root).
|
||
- `__init__.py` (re-exports `ReplayInputAdapter`, `ReplayInputBundle`, `AutoSyncDecision`, `AutoSyncConfig`)
|
||
- `interface.py` (`ReplayInputAdapter` class declaration + `ReplayInputBundle` DTO)
|
||
- `tlog_video_adapter.py` (concrete `ReplayInputAdapter` that instantiates `VideoFileFrameSource` + `TlogReplayFcAdapter` + chosen `Clock`)
|
||
- `auto_sync.py` (AZ-405 IMU-take-off / video-motion-onset detectors + combined offset computation + AC-8 frame-window-match validator)
|
||
- `tests/`
|
||
- **Owned by**: AZ-265 (E-DEMO-REPLAY) — task AZ-405 (auto-sync + coordinator).
|
||
- **Consumed by**: `runtime_root` (replay-mode branch of `compose_root`); `cli/replay.py`. Layer-4 module: imports from Layer 1 (`frame_source` interface, `clock` interface, `_types`, `config`, `logging`, `fdr_client`, `helpers.wgs_converter`) and instantiates Layer-4 strategies (`c8_fc_adapter.tlog_replay_adapter`, `frame_source.video_file_frame_source`). Does NOT import from Layer 3 (no component-level dependencies).
|
||
|
||
### 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; serves both `config.mode == "live"` and `config.mode == "replay"` per ADR-011) and `compose_operator(config)` (operator-orchestrator). No separate `compose_replay` function — replay is a configuration of `compose_root`, not a sibling composition root.
|
||
- **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 replay-mode branch of `compose_root` is owned by AZ-401.
|
||
- **Consumed by**: the airborne binary entrypoint (live + replay modes), the operator-orchestrator binary entrypoint, and the research/comparative binary entrypoint.
|
||
|
||
### shared/cli/replay
|
||
|
||
- **File**: `src/gps_denied_onboard/cli/replay.py`
|
||
- **Purpose**: `gps-denied-replay` console-script wrapper around the airborne entrypoint. Args: `--video PATH --tlog PATH --output results.jsonl --camera-calibration calib.json --config config.yaml --mavlink-signing-key PATH --pace {realtime,asap} [--time-offset-ms N]`. Loads the config, sets `config.mode = "replay"` and the replay-specific paths, and dispatches into the SAME companion entry point as the live `gps-denied-onboard` CLI. No standalone composition root, no separate process model — just a mode-config wrapper per ADR-011.
|
||
- **Owned by**: AZ-265 (E-DEMO-REPLAY) — child task #5.
|
||
- **Consumed by**: the parent-suite UI backend (subprocess shell-out per AZ-265 architecture decision; the operator runs the same airborne Docker image with `gps-denied-replay` as the entry command).
|
||
|
||
### 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-orchestrator Dockerfile, CI smoke job.
|
||
|
||
### blackbox_tests (cross-cutting test harness)
|
||
|
||
- **Directory**: `e2e/` (repo root, **NOT** under `tests/`)
|
||
- **Purpose**: Tier-1 Docker + Tier-2 Jetson blackbox test harness. The runner image is fully separated from the SUT and exercises the system through declared public boundaries only (frame source replay, FC inbound/outbound via SITL, tile-cache mount, MAVLink via mavproxy, FDR filesystem, mock Suite Sat Service). Owns the docker-compose test environment, Jetson Tier-2 runner scripts, fixture builders, runner image, conftest, pytest plugins (csv reporter, evidence bundler), helper modules, and per-category test trees (`positive/`, `negative/`, `performance/`, `resilience/`, `security/`, `resource_limit/`).
|
||
- **Owned by**: epic AZ-262 (E-BBT) — task specs AZ-406 (infrastructure bootstrap), AZ-407..AZ-446 (fixture builders + per-scenario tests + Tier-2 harness wrapper + CSV reporter).
|
||
- **Owns (exclusive write during implementation)**: `e2e/**`
|
||
- **Imports from**: nothing inside `src/gps_denied_onboard/**`. The runner image MUST NOT import any SUT module; the only legal interaction surfaces are MAVLink / MSP2 / HTTP / filesystem. Reads RO from `_docs/00_problem/input_data/**` (bind-mounted test data) and `_docs/02_document/tests/**` (test specs that drive AC mapping). May import standard ground-side libraries (`pymavlink`, `opencv-python`, `numpy`, `scipy`, `geopy`, `pytest`, etc.) and the `msp_gps_toy` Rust binary via subprocess.
|
||
- **FORBIDDEN**: `src/gps_denied_onboard/**` (any product source), `tests/unit/**`, `tests/integration/**`, `cpp/**` (native source trees), `db/migrations/**`. Product-side tests under `tests/unit/<component>/` remain owned by the respective component per its existing Per-Component Mapping entry.
|
||
- **Consumed by**: CI matrix (Tier-1 docker-compose entrypoint, Tier-2 Jetson runner harness); operator manual Tier-2 invocation via `./e2e/jetson/run-tier2.sh`.
|
||
- **Layering note**: blackbox_tests is an external observer of the SUT — it does not sit in the production layering table. Treat it as a separate harness outside Layers 1–5. The "no Layer-3 → Layer-4 imports" and "interface-at-producer" rules do not apply (no production code lives here).
|
||
|
||
## 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` + `noop_mavlink_transport` + `serial_mavlink_transport`), c11_tile_manager, c10_provisioning, c12_operator_orchestrator, `frame_source/VideoFileFrameSource` + `frame_source/LiveCameraFrameSource`, `replay_input` | 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 + ADR-011)
|
||
|
||
Three binaries are built from this codebase: **airborne** (Tier-1 + Tier-2 production; runs BOTH live and replay modes from a single image per ADR-011), **research** (IT-12 comparative-study, links every strategy + the same replay strategies as airborne), **operator-orchestrator** (pre-flight workflows on operator workstation). There is no separate replay-cli binary.
|
||
|
||
| CMake flag | Components / native libs gated | Airborne | Research | Operator-tooling |
|
||
|-----------|-------------------------------|----------|----------|------------------|
|
||
| `BUILD_OKVIS2` | c1_vio/okvis2, cpp/okvis2 | ON | ON | OFF |
|
||
| `BUILD_VINS_MONO` | c1_vio/vins_mono, cpp/vins_mono | OFF | ON | OFF |
|
||
| `BUILD_KLT_RANSAC` | c1_vio/klt_ransac, cpp/klt_ransac | ON (mandatory baseline) | ON | OFF |
|
||
| `BUILD_VPR_<variant>` (UltraVPR, MegaLoc, MixVPR, SelaVPR, EigenPlaces, NetVLAD, SALAD) | c2_vpr/<variant> | UltraVPR ON, others OFF | all ON | OFF |
|
||
| `BUILD_TENSORRT_RUNTIME` | c7_inference/tensorrt_runtime | ON | ON | ON (operator pre-compiles engines) |
|
||
| `BUILD_PYTORCH_RUNTIME` | c7_inference/pytorch_fp16_runtime | OFF | ON | OFF |
|
||
| `BUILD_C10_PROVISIONING` | c10_provisioning | OFF | OFF | ON |
|
||
| `BUILD_C11_TILE_MANAGER` | c11_tile_manager | OFF | OFF | ON |
|
||
| `BUILD_C12_OPERATOR_ORCHESTRATOR` | c12_operator_orchestrator | OFF | OFF | ON |
|
||
| `BUILD_GTSAM_BINDINGS` | cpp/gtsam_bindings (used by c4_pose + c5_state) | ON | ON | OFF |
|
||
| `BUILD_FAISS_INDEX` | c6_tile_cache `FaissDescriptorIndex` (faiss-cpu wheel; runtime gate at `runtime_root.storage_factory` — no native target) | ON | ON | ON |
|
||
| `BUILD_VIDEO_FILE_FRAME_SOURCE` | `frame_source/VideoFileFrameSource` (AZ-265) | ON (replay mode) | ON (replay mode) | OFF |
|
||
| `BUILD_TLOG_REPLAY_ADAPTER` | `c8_fc_adapter/tlog_replay_adapter` (AZ-265) | ON (replay mode) | ON (replay mode) | OFF |
|
||
| `BUILD_REPLAY_SINK_JSONL` | `c8_fc_adapter/replay_sink` + `c8_fc_adapter/noop_mavlink_transport` (AZ-265) | ON (replay mode) | ON (replay mode) | OFF |
|
||
| `BUILD_LIVE_CAMERA_FRAME_SOURCE` | `frame_source/LiveCameraFrameSource` (AZ-265 retrofit) | ON | ON | 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). In airborne, all three replay-mode `BUILD_*` flags default ON so the same image serves both live and replay modes; an operator deployment that wishes to remove replay capability can flip them OFF at build time (the resulting binary will still run live mode normally).
|
||
|
||
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` (3 parallel build jobs: airborne, research, operator-orchestrator).
|
||
- `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` or `c12_operator_orchestrator`; the operator-orchestrator SBOM MUST NOT contain `c1_vio` or any replay strategy). Note: there is no per-replay SBOM diff under ADR-011 — replay runs from the airborne image, which is already SBOM-diffed.
|
||
|
||
## 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_orchestrator, c13_fdr).
|
||
- [x] Every shared / cross-cutting concern has a Shared section entry (_types, config, logging, fdr_client, frame_source, clock, replay_input, helpers/* × 8, runtime_root, cli/replay, healthcheck, blackbox_tests).
|
||
- [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 / ADR-011) covered: new `frame_source/` + `clock/` + `replay_input/` cross-cuttings; new C8 strategies (`tlog_replay_adapter`, `replay_sink`, `noop_mavlink_transport`, `serial_mavlink_transport`); new `cli/replay.py` console-script wrapper; replay-mode `BUILD_*` flags default ON in the airborne and research binaries (no separate replay-cli binary).
|
||
|
||
## 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.
|