Files
gps-denied-onboard/_docs/03_implementation/batch_08_cycle1_report.md
T
Oleksandr Bezdieniezhnykh 362e93c626 [AZ-390] [AZ-392] C8 FC/GCS adapter foundation + covariance projector
Adds the C8 foundation:
- FcAdapter / GcsAdapter / ReplaySink Protocols + contract DTOs in
  _types/fc.py (PortConfig, FcKind, FlightState, GpsStatus, Severity,
  TelemetryKind, FcTelemetryFrame, FlightStateSignal, GpsHealth,
  OperatorCommand, Subscription, Imu/Attitude samples).
- Disjoint FcAdapterError / GcsAdapterError trees with
  SourceSetSwitchNotSupportedError <: SourceSetSwitchError per AC-9.
- FcConfig + GcsConfig cross-cutting Config blocks with config-load
  validation (unknown strategy rejected at __post_init__).
- runtime_root/fc_factory.py: build_fc_adapter / build_gcs_adapter
  with BUILD_FC_*/BUILD_GCS_* flag gating + INFO log on load +
  single-writer outbound-thread binding.
- CovarianceProjector (helper, AZ-392): 6x6 -> 3x3 -> 2x2 ->
  sqrt(lambda_max) reduction; AP returns float m, iNav returns int mm
  with uint16 clamp + WARN + FDR record. Non-SPD / NaN / wrong-shape
  raise FcEmitError and emit an FDR ERROR record carrying frame_id.

Contracts:
- composition_root_protocol.md 1.1.0 -> 1.2.0 (added fc/gcs blocks +
  build_fc_adapter / build_gcs_adapter + outbound-thread binding).
- fc_adapter_protocol.md unchanged (this batch implements v1.0.0).

Tests: 410 pass / 2 skip / 0 fail (+53 new tests in batch 8).

AZ-391 (inbound subscription) deferred to batch 9 — pulls YAMSPy as
a new external dependency (iNav MSP2 decode).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:17:59 +03:00

7.7 KiB
Raw Blame History

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.pyFcAdapterError + GcsAdapterError disjoint trees, including SourceSetSwitchNotSupportedErrorSourceSetSwitchError per AC-9.
  • src/gps_denied_onboard/components/c8_fc_adapter/_covariance_projector.pyCovarianceProjector helper (AZ-392).
  • src/gps_denied_onboard/runtime_root/fc_factory.pybuild_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.pyEmittedExternalPosition 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__.pyruntime_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.pyruntime_root.pyruntime_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).