"""C1 VIO strategy composition-root factory (AZ-331). :func:`build_vio_strategy` selects exactly one strategy by ``config.components['c1_vio'].strategy`` and respects compile-time ``BUILD_*`` gating: requesting a strategy whose flag is OFF raises :class:`StrategyNotAvailableError` at composition time (NOT at first frame). Concrete strategy modules (``okvis2``, ``vins_mono``, ``klt_ransac``) are imported lazily — a Tier-0 workstation build with ``BUILD_OKVIS2=OFF`` MUST NOT load ``c1_vio.okvis2`` (Risk-2 / I-5; verifiable via ``sys.modules``). """ from __future__ import annotations import os from typing import TYPE_CHECKING from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError if TYPE_CHECKING: from gps_denied_onboard.components.c1_vio import ( C1VioConfig, VioStrategy, ) from gps_denied_onboard.components.c13_fdr import FdrClient from gps_denied_onboard.config.schema import Config __all__ = ["build_vio_strategy"] _STRATEGY_TO_BUILD_FLAG: dict[str, str] = { "okvis2": "BUILD_OKVIS2", "vins_mono": "BUILD_VINS_MONO", "klt_ransac": "BUILD_KLT_RANSAC", } _STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = { "okvis2": ("gps_denied_onboard.components.c1_vio.okvis2", "Okvis2Strategy"), "vins_mono": ( "gps_denied_onboard.components.c1_vio.vins_mono", "VinsMonoStrategy", ), "klt_ransac": ( "gps_denied_onboard.components.c1_vio.klt_ransac", "KltRansacStrategy", ), } def _is_build_flag_on(flag_name: str) -> bool: """Read a compile-time ``BUILD_*`` flag from the environment. ``ON`` / ``1`` / ``true`` / ``yes`` (case-insensitive) → ``True``; anything else (including unset) → ``False``. Defaults to OFF so test environments must opt-in explicitly per strategy. """ raw = os.environ.get(flag_name, "") return raw.strip().lower() in {"on", "1", "true", "yes"} def _c1_config(config: "Config") -> "C1VioConfig": """Pull the registered C1 config block. ``c1_vio.__init__`` registers it on import; a missing registration is a developer error and surfaces as ``KeyError`` rather than a silent fallback. """ return config.components["c1_vio"] def build_vio_strategy( config: "Config", *, fdr_client: "FdrClient", ) -> "VioStrategy": """Construct the :class:`VioStrategy` impl selected by config. 1. Reads ``config.components['c1_vio'].strategy``. 2. Checks the matching ``BUILD_*`` flag — if OFF, raises :class:`StrategyNotAvailableError` BEFORE any import. 3. Lazily imports the concrete strategy module. 4. Constructs and returns the strategy instance, passing ``config`` and ``fdr_client``. Raises :class:`StrategyNotAvailableError` when the compile-time flag is OFF (canonical Tier-0 path) or when the concrete strategy module has not been built yet (AZ-332 / AZ-333 / AZ-334 pending). """ block = _c1_config(config) strategy = block.strategy flag_name = _STRATEGY_TO_BUILD_FLAG.get(strategy) module_info = _STRATEGY_TO_MODULE.get(strategy) if flag_name is None or module_info is None: raise StrategyNotAvailableError( f"VioStrategy {strategy!r} is not buildable in this binary." ) if not _is_build_flag_on(flag_name): raise StrategyNotAvailableError( f"VioStrategy {strategy!r} requires {flag_name}=ON in this " "binary; the flag is OFF." ) module_name, class_name = module_info try: module = __import__(module_name, fromlist=[class_name]) except ModuleNotFoundError as exc: raise StrategyNotAvailableError( f"VioStrategy {strategy!r} is configured but its concrete impl " f"module {module_name!r} has not been built into this binary " "yet (AZ-332 / AZ-333 / AZ-334 pending)." ) from exc strategy_cls = getattr(module, class_name) return strategy_cls(config, fdr_client=fdr_client)