"""C2.5 ReRank strategy composition-root factory (AZ-342). :func:`build_rerank_strategy` selects exactly one strategy by ``config.components['c2_5_rerank'].strategy`` and respects compile-time ``BUILD_RERANK_`` gating: requesting a strategy whose flag is OFF raises :class:`StrategyNotAvailableError` at composition time (NOT at first frame). The shared :class:`LightGlueRuntime` is constructor-injected — the factory does NOT own its lifecycle. The runtime root constructs ONE ``LightGlueRuntime`` instance and passes the same reference to both this factory (C2.5) and the future C3 matcher factory (R14 fix; see ``description.md`` § 6). Concrete strategy modules are imported lazily — a Tier-0 workstation build with ``BUILD_RERANK_INLIER_COUNT=OFF`` MUST NOT load ``c2_5_rerank.inlier_based_reranker`` (ADR-002 / I-5; verifiable via ``sys.modules``). """ from __future__ import annotations import logging import os from typing import TYPE_CHECKING from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError if TYPE_CHECKING: from gps_denied_onboard.clock import Clock from gps_denied_onboard.components.c2_5_rerank import ( C2_5RerankConfig, ReRankStrategy, ) from gps_denied_onboard.components.c6_tile_cache import TileStore from gps_denied_onboard.config.schema import Config from gps_denied_onboard.fdr_client import FdrClient from gps_denied_onboard.helpers.feature_extractor import FeatureExtractor from gps_denied_onboard.helpers.lightglue_runtime import LightGlueRuntime __all__ = ["build_rerank_strategy"] _LOG = logging.getLogger("gps_denied_onboard.c2_5_rerank") # Strategy resolution table — mirrors the contract's # ``rerank_strategy_protocol.md`` v1.0.0 § Composition-Root Factory # table verbatim. ANY mutation here MUST be mirrored in the contract. _STRATEGY_TO_BUILD_FLAG: dict[str, str] = { "inlier_count": "BUILD_RERANK_INLIER_COUNT", } _STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = { "inlier_count": ( "gps_denied_onboard.components.c2_5_rerank.inlier_based_reranker", "InlierCountReRanker", ), } def _is_build_flag_on(flag_name: str) -> bool: raw = os.environ.get(flag_name, "") return raw.strip().lower() in {"on", "1", "true", "yes"} def _c2_5_config(config: "Config") -> "C2_5RerankConfig": return config.components["c2_5_rerank"] def build_rerank_strategy( config: "Config", *, tile_store: "TileStore", lightglue_runtime: "LightGlueRuntime", feature_extractor: "FeatureExtractor", clock: "Clock", fdr_client: "FdrClient | None" = None, ) -> "ReRankStrategy": """Construct the :class:`ReRankStrategy` impl selected by config. 1. Reads ``config.components['c2_5_rerank'].strategy``. 2. Checks the matching ``BUILD_RERANK_`` flag — if OFF, raises :class:`StrategyNotAvailableError` BEFORE any import. 3. Lazily imports the concrete strategy module. 4. Constructs the strategy via its module-level ``create(config, tile_store, lightglue_runtime, feature_extractor, fdr_client)`` factory function (each concrete strategy module exports ``create`` as its public entry-point; concrete constructors stay private). 5. Emits ONE INFO log ``kind="c2_5.rerank.strategy_loaded"`` with structured fields ``{strategy, top_n}``. ``feature_extractor`` is a shared L1 helper (AZ-343 scope expansion) used by the concrete strategy to extract keypoints + descriptors from each per-frame nav image AND from each candidate's tile pixels. ``clock`` is the composition-root :class:`Clock` (AZ-398) — strategies stamp :attr:`RerankResult.reranked_at` via ``clock.monotonic_ns()`` rather than calling stdlib ``time`` directly (Invariant 2 of the replay contract). ``fdr_client`` is optional — passed through to strategies that emit FDR records; ``None`` lets the strategy run without FDR emission (useful for tests). Raises: StrategyNotAvailableError: compile-time flag OFF or concrete module not yet built. """ block = _c2_5_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: # Defensive — config validation rejects unknown strategy labels # at load (C2_5RerankConfig.__post_init__). _LOG.error( "c2_5.rerank.build_flag_off", extra={"strategy": strategy, "reason": "unknown_strategy"}, ) raise StrategyNotAvailableError( f"ReRankStrategy {strategy!r} is not buildable in this binary." ) if not _is_build_flag_on(flag_name): _LOG.error( "c2_5.rerank.build_flag_off", extra={"strategy": strategy, "flag": flag_name}, ) raise StrategyNotAvailableError( f"BUILD_RERANK_{strategy.upper()} is OFF for this binary; " f"cannot select strategy={strategy}." ) module_name, class_name = module_info try: module = __import__(module_name, fromlist=[class_name]) except ModuleNotFoundError as exc: raise StrategyNotAvailableError( f"ReRankStrategy {strategy!r} is configured but its concrete impl " f"module {module_name!r} has not been built into this binary " "yet (AZ-343 pending)." ) from exc create_fn = getattr(module, "create", None) if create_fn is None: strategy_cls = getattr(module, class_name) instance = strategy_cls( config, tile_store=tile_store, lightglue_runtime=lightglue_runtime, feature_extractor=feature_extractor, clock=clock, fdr_client=fdr_client, ) else: instance = create_fn( config, tile_store=tile_store, lightglue_runtime=lightglue_runtime, feature_extractor=feature_extractor, clock=clock, fdr_client=fdr_client, ) _LOG.info( "c2_5.rerank.strategy_loaded", extra={"strategy": strategy, "top_n": block.top_n}, ) return instance