# 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).
