[AZ-329] [AZ-330] [AZ-523] [AZ-524] Batch 44 atomic refactor

Implements two new C12 services and rebalances the C11/C12 boundary
in one atomic commit:

* AZ-329 PostLandingUploadOrchestrator — gates C11 upload on the
  `flight_footer` FDR record's `clean_shutdown` field; 4 refusal
  modes; new FdrFooterReader Protocol + LocalFdrFooterReader.
* AZ-330 OperatorReLocService — AC-3.4 visual-loss re-localization
  hint; reuses shared LatLonAlt; OperatorCommandTransport Protocol
  cut (E-C8 owns the future pymavlink concrete); new FDR record
  kind `c12.reloc.requested`; log redaction (lat/lon 5 decimals,
  reason 200 chars).
* AZ-523 C11 internal flight-state gate removed (SRP refactor):
  `confirm_flight_state` / `FlightStateSignal` use /
  `FlightStateNotOnGroundError` deleted from C11; TileUploader
  contract bumped to v2.0.0 (frozen) with migration note; AZ-317
  superseded.
* AZ-524 Package rename `c12_operator_tooling` →
  `c12_operator_orchestrator` across source, tests, pyproject,
  CMake, Dockerfile, compose, CI, runtime-root services class
  (`OperatorOrchestratorServices`) + factory function
  (`build_operator_orchestrator`), logger namespaces, config slug,
  docs, and the E-C12 epic title.

Tests: 1543 passed, 80 skipped (all environment gates). Targeted
AC suite (AZ-329 + AZ-330 + FdrFooterReader): 37 passed. Cold-start
NFR-perf still ≤ 500 ms p99.

Tracker: AZ-317 → Done (superseded); AZ-319 v2.0.0 contract bump
comment; AZ-329/AZ-330 → In Testing; AZ-253 epic renamed; AZ-523
+ AZ-524 created and closed as audit-trail tickets.

See `_docs/03_implementation/batch_44_cycle1_report.md`.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 19:42:46 +03:00
parent 2d88d3d674
commit 5fe67023b2
112 changed files with 3409 additions and 1311 deletions
@@ -409,15 +409,15 @@ def compose_root(config: Config) -> RuntimeRoot:
def compose_operator(config: Config) -> OperatorRoot:
"""Compose the operator-tooling runtime graph (per contract v1.0.0)."""
"""Compose the operator-orchestrator runtime graph (per contract v1.0.0)."""
components, order = _compose(
config,
binary="operator-tooling",
binary="operator-orchestrator",
allowed_tiers=frozenset({"operator", "shared"}),
extra_required_env=("SATELLITE_PROVIDER_URL",),
)
return OperatorRoot(
binary="operator-tooling",
binary="operator-orchestrator",
profile=os.environ["GPS_DENIED_FC_PROFILE"],
components=components,
construction_order=order,
@@ -1,17 +1,15 @@
"""C11 TileManager composition-root factories (AZ-316, AZ-317, AZ-318, AZ-319).
"""C11 TileManager composition-root factories (AZ-316, AZ-318, AZ-319).
Wires the operator-side services:
* :func:`build_flight_state_gate` (AZ-317) — adapts an injected
``FlightStateSource`` (typically an E-C8 FC adapter wrapper) into
the C11 ``FlightStateGate``.
* :func:`build_per_flight_key_manager` (AZ-318) — wires the AZ-273
:class:`FdrClient` and the project ``Clock`` strategy into the
ephemeral signing-key manager.
* :func:`build_tile_uploader` (AZ-319) — composes the gate, the
key manager, the c6 storage cuts, an :class:`httpx.Client`, and
the :class:`C11Config` block into the production
:class:`HttpTileUploader`.
* :func:`build_tile_uploader` (AZ-319) — composes the key manager,
the c6 storage cuts, an :class:`httpx.Client`, and the
:class:`C11Config` block into the production
:class:`HttpTileUploader`. Flight-state confirmation is the
caller's responsibility (C12 ``PostLandingUploadOrchestrator``).
* :func:`build_tile_downloader` (AZ-316) — composes the c6 store +
metadata-store + budget-enforcer (wrapped in a single
composition-root adapter that hides c6's :class:`TileMetadata`
@@ -32,8 +30,6 @@ import httpx
from gps_denied_onboard.components.c11_tile_manager import (
C11Config,
FlightStateGate,
FlightStateSource,
HttpTileDownloader,
HttpTileUploader,
IdempotentRetryTileUploader,
@@ -50,14 +46,12 @@ if TYPE_CHECKING:
from gps_denied_onboard.config.schema import Config
__all__ = [
"build_flight_state_gate",
"build_per_flight_key_manager",
"build_tile_downloader",
"build_tile_uploader",
]
_C11_GATE_LOGGER = "c11_tile_manager.flight_state_gate"
_C11_SIGNING_LOGGER = "c11_tile_manager.signing_key"
_C11_SIGNING_PRODUCER_ID = "c11_tile_manager.signing_key"
_C11_UPLOADER_LOGGER = "c11_tile_manager.tile_uploader"
@@ -65,19 +59,6 @@ _C11_UPLOADER_PRODUCER_ID = "c11_tile_manager.tile_uploader"
_C11_DOWNLOADER_LOGGER = "c11_tile_manager.tile_downloader"
def build_flight_state_gate(*, source: FlightStateSource) -> FlightStateGate:
"""Construct a wired :class:`FlightStateGate` (AZ-317).
The ``source`` argument is the consumer-side cut over E-C8's FC
adapter; the composition root supplies a concrete adapter wrapping
the actual C8 instance once E-C8 ships. Until then operator
tooling tests inject a fake source that returns a fixed signal.
"""
logger = get_logger(_C11_GATE_LOGGER)
return FlightStateGate(source=source, logger=logger)
def build_per_flight_key_manager(
config: Config,
*,
@@ -108,7 +89,6 @@ def build_tile_uploader(
http_client: httpx.Client,
tile_store: Any,
tile_metadata_store: Any,
flight_state_gate: FlightStateGate,
key_manager: PerFlightKeyManager,
clock: ClockProtocol | None = None,
fdr_client: FdrClient | None = None,
@@ -162,7 +142,6 @@ def build_tile_uploader(
http_client=http_client,
tile_store=tile_store,
tile_metadata_store=tile_metadata_store,
flight_state_gate=flight_state_gate,
key_manager=key_manager,
fdr_client=fdr_client,
logger=logger,
@@ -235,7 +214,7 @@ def build_tile_downloader(
if not block.service_api_key:
raise ConfigError(
"build_tile_downloader: C11Config.service_api_key must be "
"set; the operator-tooling deploy MUST inject the bearer "
"set; the operator-orchestrator deploy MUST inject the bearer "
"token via env override"
)
logger = get_logger(_C11_DOWNLOADER_LOGGER)
@@ -1,4 +1,4 @@
"""Composition-root factories for C12 operator-tooling services.
"""Composition-root factories for C12 operator-orchestrator services.
* :func:`build_flights_api_client` — AZ-489 ``FlightsApiClient`` (online +
offline path).
@@ -12,14 +12,14 @@
AZ-327 / AZ-489 services. The AZ-507 cross-component cut means we
translate c11's real ``DownloadRequest`` / ``DownloadBatchReport`` to
the local ``DownloadRequestCut`` / ``DownloadBatchReportCut`` here.
* :func:`build_operator_tool` — aggregator that returns the
:class:`OperatorToolServices` dataclass the AZ-326 CLI consumes.
* :func:`build_operator_orchestrator` — aggregator that returns the
:class:`OperatorOrchestratorServices` dataclass the AZ-326 CLI consumes.
Each ``build_*`` function is intentionally tiny — there is one
production strategy per service today and the CLI wiring just plugs
the concrete instance into the same composition root method. Sibling
tasks AZ-329 / AZ-330 will each add a single field to
:class:`OperatorToolServices` without renaming or moving the
:class:`OperatorOrchestratorServices` without renaming or moving the
dataclass.
"""
@@ -30,60 +30,80 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING
from gps_denied_onboard.clock import Clock
from gps_denied_onboard.components.c12_operator_tooling.build_cache import (
from gps_denied_onboard.components.c12_operator_orchestrator.build_cache import (
BuildCacheOrchestrator,
)
from gps_denied_onboard.components.c12_operator_tooling.companion_bringup import (
from gps_denied_onboard.components.c12_operator_orchestrator.companion_bringup import (
CompanionBringup,
)
from gps_denied_onboard.components.c12_operator_tooling.file_lock import (
from gps_denied_onboard.components.c12_operator_orchestrator.fdr_footer_reader import (
LocalFdrFooterReader,
)
from gps_denied_onboard.components.c12_operator_orchestrator.file_lock import (
FilelockFileLockFactory,
)
from gps_denied_onboard.components.c12_operator_tooling.flights_api import (
from gps_denied_onboard.components.c12_operator_orchestrator.flights_api import (
FlightsApiClient,
HttpxFlightsApiClient,
)
from gps_denied_onboard.components.c12_operator_tooling.paramiko_ssh_session import (
from gps_denied_onboard.components.c12_operator_orchestrator.operator_command_transport import (
OperatorCommandTransport,
)
from gps_denied_onboard.components.c12_operator_orchestrator.operator_reloc_service import (
OperatorReLocService,
)
from gps_denied_onboard.components.c12_operator_orchestrator.paramiko_ssh_session import (
ParamikoSshSessionFactory,
)
from gps_denied_onboard.components.c12_operator_tooling.remote_c10_invoker import (
from gps_denied_onboard.components.c12_operator_orchestrator.post_landing_upload import (
PostLandingUploadOrchestrator,
)
from gps_denied_onboard.fdr_client import FdrClient
from gps_denied_onboard.components.c12_operator_orchestrator.remote_c10_invoker import (
RemoteCacheProvisionerInvoker,
)
from gps_denied_onboard.components.c12_operator_tooling.remote_sidecar_verifier import (
from gps_denied_onboard.components.c12_operator_orchestrator.remote_sidecar_verifier import (
RemoteSidecarVerifier,
)
from gps_denied_onboard.components.c12_operator_tooling.sector_classification_store import (
from gps_denied_onboard.components.c12_operator_orchestrator.sector_classification_store import (
SectorClassificationStore,
)
from gps_denied_onboard.components.c12_operator_tooling.tile_downloader_cut import (
from gps_denied_onboard.components.c12_operator_orchestrator.tile_downloader_cut import (
TileDownloaderCut,
)
from gps_denied_onboard.components.c12_operator_orchestrator.tile_uploader_cut import (
TileUploaderCut,
)
if TYPE_CHECKING:
from gps_denied_onboard.components.c12_operator_tooling.config import (
from gps_denied_onboard.components.c12_operator_orchestrator.config import (
C12Config,
)
from gps_denied_onboard.config import Config
__all__ = [
"OperatorToolServices",
"OperatorOrchestratorServices",
"build_build_cache_orchestrator",
"build_companion_bringup",
"build_flights_api_client",
"build_operator_tool",
"build_operator_reloc_service",
"build_operator_orchestrator",
"build_post_landing_upload_orchestrator",
"build_sector_classification_store",
]
_C12_LOGGER_NAME = "c12_operator_tooling"
_COMPANION_LOGGER_NAME = "c12_operator_tooling.companion_bringup"
_BUILD_CACHE_LOGGER_NAME = "c12_operator_tooling.build_cache"
_REMOTE_C10_LOGGER_NAME = "c12_operator_tooling.remote_c10_invoker"
_C12_LOGGER_NAME = "c12_operator_orchestrator"
_COMPANION_LOGGER_NAME = "c12_operator_orchestrator.companion_bringup"
_BUILD_CACHE_LOGGER_NAME = "c12_operator_orchestrator.build_cache"
_REMOTE_C10_LOGGER_NAME = "c12_operator_orchestrator.remote_c10_invoker"
_POST_LANDING_LOGGER_NAME = "c12_operator_orchestrator.post_landing_upload"
_OPERATOR_RELOC_LOGGER_NAME = "c12_operator_orchestrator.operator_reloc_service"
@dataclass(frozen=True)
class OperatorToolServices:
"""Aggregated service handles the operator-tool CLI consumes (AZ-326).
class OperatorOrchestratorServices:
"""Aggregated service handles the operator-orchestrator CLI consumes (AZ-326).
AZ-326 introduced the dataclass and now owns three services
(``flights_api_client``, ``sector_classification_store``,
@@ -103,6 +123,8 @@ class OperatorToolServices:
sector_classification_store: SectorClassificationStore
companion_bringup: CompanionBringup
build_cache_orchestrator: BuildCacheOrchestrator | None = None
post_landing_upload_orchestrator: PostLandingUploadOrchestrator | None = None
operator_reloc_service: OperatorReLocService | None = None
def build_flights_api_client(config: Config) -> FlightsApiClient:
@@ -162,7 +184,7 @@ def build_companion_bringup(
def build_build_cache_orchestrator(
config: Config,
*,
services: OperatorToolServices,
services: OperatorOrchestratorServices,
tile_downloader: TileDownloaderCut,
clock: Clock,
logger: logging.Logger | None = None,
@@ -171,7 +193,7 @@ def build_build_cache_orchestrator(
Caller (production runtime root) is responsible for translating the
real c11 ``TileDownloader`` to a :class:`TileDownloaderCut` adapter
here — ``c12_operator_tooling`` cannot import c11 directly per
here — ``c12_operator_orchestrator`` cannot import c11 directly per
AZ-507. The lockfile factory + remote-C10 invoker + SSH factory are
constructed in-place; the SSH factory MUST be the same instance as
the one wired into ``services.companion_bringup`` (single
@@ -207,51 +229,142 @@ def build_build_cache_orchestrator(
)
def build_operator_tool(
def build_post_landing_upload_orchestrator(
config: Config,
*,
tile_uploader: TileUploaderCut,
logger: logging.Logger | None = None,
) -> PostLandingUploadOrchestrator:
"""Build the AZ-329 :class:`PostLandingUploadOrchestrator` from config + a c11 uploader cut.
Caller (production suite-level runtime root) is responsible for
translating the real c11 ``HttpTileUploader`` to a
:class:`TileUploaderCut` adapter here — ``c12_operator_orchestrator``
cannot import c11 directly per AZ-507. The adapter maps
:class:`UploadRequestCut` ↔ c11's ``UploadRequest`` and
:class:`UploadBatchReportCut` ↔ c11's ``UploadBatchReport``.
"""
c12_config = _resolve_c12_config(config)
return PostLandingUploadOrchestrator(
tile_uploader=tile_uploader,
fdr_footer_reader=LocalFdrFooterReader(c12_config.post_landing.fdr_root),
logger=logger or logging.getLogger(_POST_LANDING_LOGGER_NAME),
config=c12_config.post_landing,
)
def build_operator_reloc_service(
config: Config,
*,
transport: OperatorCommandTransport,
fdr_client: FdrClient,
clock: Clock,
logger: logging.Logger | None = None,
) -> OperatorReLocService:
"""Build the AZ-330 :class:`OperatorReLocService`.
The :class:`OperatorCommandTransport` (E-C8's pymavlink-backed
``MavlinkOperatorCommandTransport`` in production; a
``LoggingOnlyOperatorCommandTransport`` in dev environments without
a companion) is resolved by the suite-level runtime root and
injected here — c12 cannot import c8 directly per AZ-507. The
``fdr_client`` is the shared AZ-273 instance keyed to producer
``c12_operator_orchestrator`` so the post-flight FDR captures the
operator's re-loc actions chronologically alongside other onboard
records.
"""
_ = config # reserved for future composition-time tuning
return OperatorReLocService(
transport=transport,
fdr_client=fdr_client,
logger=logger or logging.getLogger(_OPERATOR_RELOC_LOGGER_NAME),
clock=clock,
)
def build_operator_orchestrator(
config: Config,
*,
tile_downloader: TileDownloaderCut | None = None,
tile_uploader: TileUploaderCut | None = None,
clock: Clock | None = None,
) -> OperatorToolServices:
"""Aggregate the AZ-326 / AZ-327 / AZ-328 / AZ-489 service handles.
operator_command_transport: OperatorCommandTransport | None = None,
fdr_client: FdrClient | None = None,
) -> OperatorOrchestratorServices:
"""Aggregate the AZ-326 / AZ-327 / AZ-328 / AZ-329 / AZ-330 / AZ-489 service handles.
``tile_downloader`` and ``clock`` are optional — without them, the
``build_cache_orchestrator`` field is left as ``None`` and the CLI's
``build-cache`` subcommand short-circuits gracefully. Production
wiring (the suite-level runtime root) supplies real instances.
Optional collaborators (each gates one service field):
* ``tile_downloader`` + ``clock`` → ``build_cache_orchestrator``
(AZ-328); CLI ``build-cache`` short-circuits when missing.
* ``tile_uploader`` → ``post_landing_upload_orchestrator`` (AZ-329);
CLI ``upload-pending`` short-circuits when missing.
* ``operator_command_transport`` + ``fdr_client`` + ``clock`` →
``operator_reloc_service`` (AZ-330); CLI ``reloc-confirm``
short-circuits when missing. AC-10: lazy construction — when the
transport is not supplied, no transport instance is created
(pymavlink stays unimported).
"""
base = OperatorToolServices(
flights_api_client=build_flights_api_client(config),
sector_classification_store=build_sector_classification_store(config),
companion_bringup=build_companion_bringup(config),
flights_api_client = build_flights_api_client(config)
sector_store = build_sector_classification_store(config)
companion_bringup = build_companion_bringup(config)
base_for_build_cache = OperatorOrchestratorServices(
flights_api_client=flights_api_client,
sector_classification_store=sector_store,
companion_bringup=companion_bringup,
)
if tile_downloader is None or clock is None:
return base
orchestrator = build_build_cache_orchestrator(
config,
services=base,
tile_downloader=tile_downloader,
clock=clock,
)
return OperatorToolServices(
flights_api_client=base.flights_api_client,
sector_classification_store=base.sector_classification_store,
companion_bringup=base.companion_bringup,
build_cache_orchestrator=orchestrator,
build_cache_orchestrator: BuildCacheOrchestrator | None = None
if tile_downloader is not None and clock is not None:
build_cache_orchestrator = build_build_cache_orchestrator(
config,
services=base_for_build_cache,
tile_downloader=tile_downloader,
clock=clock,
)
post_landing_orchestrator: PostLandingUploadOrchestrator | None = None
if tile_uploader is not None:
post_landing_orchestrator = build_post_landing_upload_orchestrator(
config,
tile_uploader=tile_uploader,
)
operator_reloc_service: OperatorReLocService | None = None
if (
operator_command_transport is not None
and fdr_client is not None
and clock is not None
):
operator_reloc_service = build_operator_reloc_service(
config,
transport=operator_command_transport,
fdr_client=fdr_client,
clock=clock,
)
return OperatorOrchestratorServices(
flights_api_client=flights_api_client,
sector_classification_store=sector_store,
companion_bringup=companion_bringup,
build_cache_orchestrator=build_cache_orchestrator,
post_landing_upload_orchestrator=post_landing_orchestrator,
operator_reloc_service=operator_reloc_service,
)
def _resolve_c12_config(config: Config) -> C12Config:
from gps_denied_onboard.components.c12_operator_tooling.config import (
from gps_denied_onboard.components.c12_operator_orchestrator.config import (
C12Config,
)
block = config.components.get("c12_operator_tooling")
block = config.components.get("c12_operator_orchestrator")
if block is None:
return C12Config()
if not isinstance(block, C12Config):
raise TypeError(
"config.components['c12_operator_tooling'] must be a C12Config; got "
"config.components['c12_operator_orchestrator'] must be a C12Config; got "
f"{type(block).__name__}"
)
return block