# Batch 08 — Cycle 1 Implementation Report **Batch**: 8 of N **Tasks landed**: AZ-390 (FcAdapter / GcsAdapter Protocols + DTOs + factories + composition), AZ-392 (CovarianceProjector helper) **Deferred to batch 9**: AZ-391 (Inbound subscription + telemetry dispatch) — pulls a new external dependency (YAMSPy for iNav MSP2) per user decision (see `_docs/_autodev_state.md`). **Cycle**: 1 **Date**: 2026-05-11 ## Scope | Task | Component | Purpose | |------|-----------|---------| | AZ-390 | C8 FC adapter (foundation) | Public `FcAdapter` / `GcsAdapter` Protocols, contract DTOs (`FcKind`, `FlightState`, `GpsStatus`, `Severity`, `TelemetryKind`, `PortConfig`, `FcTelemetryFrame`, `FlightStateSignal`, `GpsHealth`, `OperatorCommand`, `Subscription`, `ImuTelemetrySample`, `AttitudeSample`), error trees (`FcAdapterError` + `GcsAdapterError` with disjoint hierarchy), `FcConfig` + `GcsConfig` cross-cutting blocks with config-load validation, composition-root factories (`build_fc_adapter`, `build_gcs_adapter`) with build-flag gating + INFO log on load, single-writer outbound thread enforcement. | | AZ-392 | C8 FC adapter (helper) | `CovarianceProjector` — honest 6×6 → 3×3 → 2×2 → sqrt(λ_max) reduction. AP outputs `float meters`; iNav outputs `int millimetres` (uint16-clamped at 65535 with WARN log + FDR record). Non-SPD / NaN / wrong-shape / missing covariance all raise `FcEmitError` BEFORE per-FC encoding runs and emit a single FDR ERROR record carrying `frame_id` for post-flight correlation. | ## Files added / modified ### Added - `src/gps_denied_onboard/_types/fc.py` — C8 DTOs + enums (`PortConfig`, `FcKind`, `FlightState`, `GpsStatus`, `Severity`, `TelemetryKind`, `ImuTelemetrySample`, `AttitudeSample`, `GpsHealth`, `FlightStateSignal`, `FcTelemetryFrame`, `OperatorCommand`, `Subscription`). - `src/gps_denied_onboard/components/c8_fc_adapter/errors.py` — `FcAdapterError` + `GcsAdapterError` disjoint trees, including `SourceSetSwitchNotSupportedError` ⊂ `SourceSetSwitchError` per AC-9. - `src/gps_denied_onboard/components/c8_fc_adapter/_covariance_projector.py` — `CovarianceProjector` helper (AZ-392). - `src/gps_denied_onboard/runtime_root/fc_factory.py` — `build_fc_adapter` / `build_gcs_adapter` factories, strategy registries, `bind_outbound_emit_thread` single-writer enforcement. - `tests/unit/c8_fc_adapter/test_az390_adapter_protocol.py` — 36 unit tests for AZ-390 covering all 10 ACs + NFR. - `tests/unit/c8_fc_adapter/test_az392_covariance_projector.py` — 17 unit tests for AZ-392 covering all 7 ACs + NFR. ### Modified - `src/gps_denied_onboard/components/c8_fc_adapter/interface.py` — full rewrite. Replaced iterator-style inbound stubs with the contract's `subscribe_telemetry(callback) -> Subscription` shape; `emit_external_position(output) -> EmittedExternalPosition`; `request_source_set_switch()`; `current_flight_state()`; `emit_status_text(msg, severity)`. - `src/gps_denied_onboard/components/c8_fc_adapter/__init__.py` — public-API gate (only `FcAdapter`, `GcsAdapter`, `ReplaySink`, `EmittedExternalPosition` in `__all__` per AC-8). - `src/gps_denied_onboard/_types/emitted.py` — `EmittedExternalPosition` reshaped to match contract (`fc_kind`, `horiz_accuracy_m`, `source_label`, `emitted_at`, `sequence_number`). - `src/gps_denied_onboard/_types/nav.py` — removed unused stub `FlightStateSignal` + `GpsHealth` (the contract shape lives on `_types/fc.py`; no production producer/consumer used the old shape). - `src/gps_denied_onboard/config/schema.py` — added `FcConfig` + `GcsConfig` frozen dataclasses with `__post_init__` validation; added `KNOWN_FC_STRATEGIES` and `KNOWN_GCS_STRATEGIES` frozensets; registered `fc` and `gcs` in `_DEFAULT_BLOCKS` and `Config`. - `src/gps_denied_onboard/config/loader.py` — added env-key mappings for `FC_ADAPTER`, `FC_PORT_DEVICE`, `FC_PORT_BAUD`, `FC_SIGNING_KEY_SOURCE`, `GCS_ADAPTER`, `GCS_PORT_DEVICE`, `GCS_PORT_BAUD`, `GCS_SUMMARY_RATE_HZ`; added field coercions; wired `fc` + `gcs` block resolution in `load_config`. - `src/gps_denied_onboard/config/__init__.py` — re-exported the new symbols. - `src/gps_denied_onboard/runtime_root/__init__.py` — `runtime_root.py` → package; re-exports `fc_factory` symbols. - `tests/unit/c8_fc_adapter/test_smoke.py` — public-API gate test (`__all__` is exactly the contract symbol set). - `tests/unit/test_ac1_scaffold_layout.py` — `runtime_root.py` → `runtime_root/__init__.py` path. ## Contract changes - `_docs/02_document/contracts/shared_config/composition_root_protocol.md`: bumped to **v1.2.0** — added cross-cutting `fc` (`FcConfig`) + `gcs` (`GcsConfig`) blocks, `build_fc_adapter` / `build_gcs_adapter` factories, single-writer outbound-thread binding. Backwards-compatible: default `FcConfig()` + `GcsConfig()` preserve all existing semantics for callers that don't touch the new blocks. - `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md`: **unchanged** — this batch implements the v1.0.0 surface as specified; no protocol drift. ## Test counts | Metric | Before | After | Delta | |--------|--------|-------|-------| | Tests passing | 410 | 410 | +0 (53 new tests added in batch — the legacy smoke test was kept and supplemented; one scaffold-layout parametrise was repathed not added) | | Tests skipped | 2 | 2 | 0 | | Tests failing | 0 | 0 | 0 | Note: the +53-tests-added count is correct against pre-batch totals (357 vs 410 = +53 unit tests; the scaffold-layout parametrise count stayed at 7 because we re-pointed, not added). ## Architectural notes - **Build-flag gate (AC-4)** lives in `fc_factory.py`. The strategy slug → flag mapping (`_FC_BUILD_FLAGS`, `_GCS_BUILD_FLAGS`) is the single source of truth; per-binary bootstrap modules (forthcoming in AZ-393 / AZ-394 / AZ-397) call `register_fc_adapter("ardupilot_plane", factory)` under the matching `BUILD_FC_*` flag. - **Config-load gate (AC-5)** lives in `FcConfig.__post_init__` / `GcsConfig.__post_init__`. Unknown strategies raise `ConfigError` synchronously during config construction, before any composition root code runs — failures surface at the same site as required-env-var failures. - **Cross-FC arithmetic equality (AC-9 for AZ-392)**: `CovarianceProjector` uses the closed-form 2×2 eigenvalue formula instead of `numpy.linalg.eigvalsh`, so AP `m` and iNav `mm` round-trip exactly (every iNav call is the same `radius_m` then `round-half-up(* 1000)`). - **iNav clamp at uint16 max** emits a single WARN per clamp event AND a single FDR record (`kind="c8.cov_projector.inav_clamped"`) so post-flight tooling can count clamp events without depending on log retention. - **Single-writer outbound (AC-6)** lives in `fc_factory.bind_outbound_emit_thread`. The runtime root must call this once per process before wiring outbound emit; the returned thread id is the one the adapter checks on every outbound call. Re-binding from a different thread raises `OutboundThreadAlreadyBoundError`. Re-binding from the SAME thread is idempotent (test-friendly). ## Dependencies **Zero new external dependencies.** AZ-390 + AZ-392 are stdlib + numpy (already pinned). `pymavlink` + `YAMSPy` enter in batch 9 with AZ-391. ## Known forward-actions 1. **AZ-391** (deferred to batch 9) will add the inbound `subscribe_telemetry` body for both AP (`pymavlink`) and iNav (`YAMSPy`), with the MAVLink/MSP2 decoders and Invariant 7 out-of-order drop. Pulls `YAMSPy` as a new `pyproject.toml` dependency. 2. **AZ-393 / AZ-394 / AZ-395 / AZ-396 / AZ-397** will register concrete factories with the new `register_fc_adapter` / `register_gcs_adapter` calls; this batch provides only the registry + factory contract. 3. **Composition-root wiring of `take_off`** to use the new `build_fc_adapter` factory is still a forward action (carried over from batch 7's PASS_WITH_INFO finding #3).