"""C2.5 ReRankStrategy config block (AZ-342). Registered into ``config.components['c2_5_rerank']`` by the package ``__init__.py``. The composition-root factory :func:`gps_denied_onboard.runtime_root.rerank_factory.build_rerank_strategy` reads this block to select the strategy and configure the top-N cut. ``top_n`` is the strategy-side cap on the returned :attr:`RerankResult.candidates` length; the composition root binds ``n`` per-frame from this value (default 3 per the epic's K=10 → N=3 spec). """ from __future__ import annotations from dataclasses import dataclass from typing import Final from gps_denied_onboard.config.schema import ConfigError __all__ = [ "C2_5RerankConfig", "KNOWN_STRATEGIES", ] KNOWN_STRATEGIES: Final[frozenset[str]] = frozenset({"inlier_count"}) @dataclass(frozen=True) class C2_5RerankConfig: """Per-component config for C2.5 ReRank. ``strategy`` selects exactly one of the registered re-rankers (today only ``inlier_count``); the composition-root factory respects compile-time ``BUILD_RERANK_`` gating on top of this label. ``top_n`` is the per-frame N cap (1..K-1). Default 3 (the epic's K=10 → N=3 spec). ``debug_per_frame_log`` gates the two DEBUG events (``c2_5.rerank.zero_inliers`` per dropped candidate and ``c2_5.rerank.frame_done`` per frame); flooding journald at ``3 Hz × K=10 = 30 events/sec`` by default would violate description.md § 9. Operators flip this to ``True`` for the debug-build flight binary. """ strategy: str = "inlier_count" top_n: int = 3 debug_per_frame_log: bool = False def __post_init__(self) -> None: if self.strategy not in KNOWN_STRATEGIES: raise ConfigError( f"C2_5RerankConfig.strategy={self.strategy!r} not in " f"{sorted(KNOWN_STRATEGIES)}" ) if self.top_n < 1: raise ConfigError( f"C2_5RerankConfig.top_n must be >= 1; got {self.top_n}" )