mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 18:51:15 +00:00
[AZ-348] C3.5 ConditionalRefiner Protocol + factory + PassthroughRefiner
Defines the public `ConditionalRefiner` Protocol (PEP 544 @runtime_checkable, two methods: `refine_if_needed` + `was_invoked`), extends `MatchResult` in-place with two default-valued refinement fields (`refinement_label`, `refinement_added_latency_ms`), defines the `RefinerError` family (`RefinerBackboneError`, `RefinerConfigError`), and ships the trivial `PassthroughRefiner` reference impl. Both refiner strategies are linked unconditionally — no `BUILD_REFINER_*` flag (NOT ADR-002 territory). Runtime selection only per ADR-001. `PassthroughRefiner` returns the input `MatchResult` by reference (bit-identical correspondences per contract INV-5) and always reports `was_invoked() is False`. Documentation: renames `module-layout.md` `c3_5_adhop` Public API symbol from `AdHoPRefinementStrategy` to `ConditionalRefiner` (AC-14) so the doc agrees with `description.md` and the contract. AC-9 (single-thread binding) deferred to AZ-270 runtime-root composition, mirroring AZ-336 / AZ-342 / AZ-344 Risk-4 precedent. AC-7 for the `"adhop"` strategy stops at `ModuleNotFoundError` because the AdHoP backbone is owned by AZ-349. All other ACs + NFRs covered by 36 new conformance tests. Architectural note: `PassthroughRefiner.inference_runtime` is typed as `object` because the L3→L3 import ban (`test_az270_compose_root`) forbids c3_5_adhop from importing c7_inference; the runtime-root factory narrows the type at construction time. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""C3.5 refiner strategy composition-root factory (AZ-348).
|
||||
|
||||
:func:`build_refiner_strategy` selects exactly one strategy by
|
||||
``config.components['c3_5_adhop'].strategy``. Both concrete
|
||||
strategies are linked into the production binary
|
||||
**unconditionally** (NO ``BUILD_REFINER_*`` flag — this is NOT
|
||||
ADR-002 territory). Runtime selection only per ADR-001.
|
||||
|
||||
Strategy resolution table — mirrors the contract's
|
||||
``conditional_refiner_protocol.md`` v1.0.0 § Composition-root
|
||||
factory table verbatim:
|
||||
|
||||
* ``"adhop"`` → ``gps_denied_onboard.components.c3_5_adhop.adhop_refiner.AdHoPRefiner`` (AZ-349; placeholder today).
|
||||
* ``"passthrough"`` → ``gps_denied_onboard.components.c3_5_adhop.passthrough_refiner.PassthroughRefiner``.
|
||||
|
||||
The shared :class:`RansacFilter` and C7 :class:`InferenceRuntime`
|
||||
handles are constructor-injected — the factory does NOT own their
|
||||
lifecycles. The runtime root constructs ONE
|
||||
:class:`RansacFilter` instance and identity-shares it across C3,
|
||||
C3.5, and C4 (per ``ransac_filter.md`` v1.0.0).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from gps_denied_onboard.components.c3_5_adhop.errors import RefinerConfigError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gps_denied_onboard.components.c3_5_adhop import (
|
||||
C3_5RefinerConfig,
|
||||
ConditionalRefiner,
|
||||
)
|
||||
from gps_denied_onboard.components.c7_inference import InferenceRuntime
|
||||
from gps_denied_onboard.config.schema import Config
|
||||
from gps_denied_onboard.helpers.ransac_filter import RansacFilter
|
||||
|
||||
__all__ = ["build_refiner_strategy"]
|
||||
|
||||
|
||||
_LOG = logging.getLogger("gps_denied_onboard.c3_5_adhop")
|
||||
|
||||
|
||||
# Strategy resolution table — mirrors the contract verbatim. ANY
|
||||
# mutation of this dict MUST be mirrored in the contract.
|
||||
_STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = {
|
||||
"adhop": (
|
||||
"gps_denied_onboard.components.c3_5_adhop.adhop_refiner",
|
||||
"AdHoPRefiner",
|
||||
),
|
||||
"passthrough": (
|
||||
"gps_denied_onboard.components.c3_5_adhop.passthrough_refiner",
|
||||
"PassthroughRefiner",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _c3_5_config(config: "Config") -> "C3_5RefinerConfig":
|
||||
"""Pull the registered C3.5 config block.
|
||||
|
||||
``c3_5_adhop.__init__`` registers it on import; a missing
|
||||
registration is a developer error and surfaces as ``KeyError``
|
||||
rather than a silent fallback.
|
||||
"""
|
||||
return config.components["c3_5_adhop"]
|
||||
|
||||
|
||||
def build_refiner_strategy(
|
||||
config: "Config",
|
||||
*,
|
||||
ransac_filter: "RansacFilter",
|
||||
inference_runtime: "InferenceRuntime",
|
||||
) -> "ConditionalRefiner":
|
||||
"""Construct the :class:`ConditionalRefiner` impl selected by config.
|
||||
|
||||
1. Reads ``config.components['c3_5_adhop'].{strategy, residual_threshold_px}``.
|
||||
2. Validates ``residual_threshold_px > 0`` — defensive
|
||||
redundancy on top of the config-load-time check
|
||||
(:class:`C3_5RefinerConfig.__post_init__`); raises
|
||||
:class:`RefinerConfigError` on failure.
|
||||
3. Imports the concrete strategy module via the resolution
|
||||
table (NOT lazy — both strategies are linked
|
||||
unconditionally).
|
||||
4. Constructs the strategy via its module-level
|
||||
``create(config, ransac_filter, inference_runtime)``
|
||||
factory function.
|
||||
5. Emits ONE INFO log ``kind="c3_5.refiner.strategy_loaded"``
|
||||
with ``{strategy, residual_threshold_px}``.
|
||||
|
||||
Raises:
|
||||
RefinerConfigError: unknown strategy label OR invalid
|
||||
threshold (``<= 0``).
|
||||
"""
|
||||
block = _c3_5_config(config)
|
||||
strategy = block.strategy
|
||||
module_info = _STRATEGY_TO_MODULE.get(strategy)
|
||||
if module_info is None:
|
||||
_LOG.error(
|
||||
"c3_5.refiner.strategy_unknown",
|
||||
extra={"strategy": strategy},
|
||||
)
|
||||
raise RefinerConfigError(f"Unknown refiner strategy: {strategy}")
|
||||
if block.residual_threshold_px <= 0.0:
|
||||
# Config-load validation should have rejected this already;
|
||||
# defensive in case a caller constructed Config bypassing
|
||||
# __post_init__ (e.g., via dataclasses.replace on a partial
|
||||
# block).
|
||||
_LOG.error(
|
||||
"c3_5.refiner.invalid_threshold",
|
||||
extra={
|
||||
"strategy": strategy,
|
||||
"residual_threshold_px": block.residual_threshold_px,
|
||||
},
|
||||
)
|
||||
raise RefinerConfigError(
|
||||
"residual_threshold_px must be > 0; "
|
||||
f"got {block.residual_threshold_px}"
|
||||
)
|
||||
module_name, class_name = module_info
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
create_fn = getattr(module, "create", None)
|
||||
if create_fn is None:
|
||||
strategy_cls = getattr(module, class_name)
|
||||
instance = strategy_cls(
|
||||
ransac_filter=ransac_filter,
|
||||
inference_runtime=inference_runtime,
|
||||
)
|
||||
else:
|
||||
instance = create_fn(
|
||||
config,
|
||||
ransac_filter=ransac_filter,
|
||||
inference_runtime=inference_runtime,
|
||||
)
|
||||
_LOG.info(
|
||||
"c3_5.refiner.strategy_loaded",
|
||||
extra={
|
||||
"strategy": strategy,
|
||||
"residual_threshold_px": block.residual_threshold_px,
|
||||
},
|
||||
)
|
||||
return instance
|
||||
Reference in New Issue
Block a user