[AZ-344] C3 CrossDomainMatcher Protocol + factory + RollingHealthWindow

Defines the public `CrossDomainMatcher` Protocol (PEP 544
@runtime_checkable, two methods: `match` + `health_snapshot`),
the three frozen+slotted DTOs (`CandidateMatchSet`, `MatchResult`,
`MatcherHealth`) in the L1 `_types/matcher.py` layer, the
`MatcherError` family (`MatcherBackboneError`,
`InsufficientInliersError`), and the composition-root
`build_matcher_strategy` factory with lazy-import +
`BUILD_MATCHER_<variant>` gating per ADR-002.

`RollingHealthWindow` accumulator (60 s, amortised O(1) update,
strict O(1) snapshot) is constructed by the factory and injected
into every concrete matcher so all backbones share window
semantics; this is what backs C5's spoof-promotion gate.

Legacy placeholder `MatchResult` removed from `_types/matching.py`;
import-only consumers (`c4_pose.interface`, `c3_5_adhop.interface`)
repointed at the new `_types/matcher.py` home — zero behavioural
change to those components.

AC-9 (single-thread binding) and AC-10 (LightGlueRuntime
identity-share with C2.5) deferred to AZ-270 runtime-root
composition, mirroring the AZ-342 Risk-4 escape clause. All other
ACs + NFRs covered by 70 new conformance tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 05:43:33 +03:00
parent d6756f1855
commit 89c223882b
16 changed files with 1404 additions and 50 deletions
@@ -4,8 +4,8 @@
**Producer task**: AZ-344 (`CrossDomainMatcher` Protocol + factory + composition)
**Consumer tasks**: AZ-345 (DISK+LightGlue primary), AZ-346 (ALIKED+LightGlue secondary), AZ-347 (XFeat alternate); downstream c3_5_adhop (epic AZ-258) which consumes `MatchResult`
**Version**: 1.0.0
**Status**: draft, awaiting AZ-344 implementation
**Last Updated**: 2026-05-10
**Status**: v1.0.0 (AZ-344 implemented 2026-05-12)
**Last Updated**: 2026-05-12
**Module-layout home**: `src/gps_denied_onboard/components/c3_matcher/interface.py` (Protocol), `src/gps_denied_onboard/components/c3_matcher/__init__.py` (re-exports), `src/gps_denied_onboard/runtime_root/matcher_factory.py` (factory)
## Purpose
@@ -65,14 +65,13 @@ class CrossDomainMatcher(Protocol):
```python
from dataclasses import dataclass
from uuid import UUID
import numpy as np
@dataclass(frozen=True, slots=True)
class CandidateMatchSet:
"""Per-candidate matching outcome inside a MatchResult."""
tile_id: tuple # composite (zoomLevel, lat, lon)
tile_id: tuple[int, float, float] # composite (zoomLevel, lat, lon); mirrors VprCandidate / RerankCandidate encoding so the L1 _types layer is free of an L1→L3 import to c6_tile_cache.TileId
inlier_count: int
inlier_correspondences: np.ndarray # shape (I, 4) float32; (px_query, py_query, px_tile, py_tile)
ransac_outlier_count: int
@@ -82,8 +81,8 @@ class CandidateMatchSet:
@dataclass(frozen=True, slots=True)
class MatchResult:
"""Cross-domain match outcome for one frame. Consumed by C3.5 ConditionalRefiner."""
frame_id: UUID
per_candidate: list[CandidateMatchSet] # 0 < len <= N=3, ranked by inlier_count descending; ties broken by per_candidate_residual_px ascending
frame_id: int # mirrors NavCameraFrame.frame_id; matches AZ-336 / AZ-342 encoding
per_candidate: tuple[CandidateMatchSet, ...] # 0 < len <= N=3, ranked by inlier_count descending; ties broken by per_candidate_residual_px ascending. tuple (not list) so frozen+slots actually holds.
best_candidate_idx: int # 0 by construction (sorted)
reprojection_residual_px: float # best candidate's median residual
matched_at: int # monotonic_ns
@@ -115,6 +114,8 @@ class InsufficientInliersError(MatcherError):
"""Every candidate failed OR every candidate's inlier count is below `config.matcher.min_inliers_threshold`. Raised by `match`. C5 falls back to VIO-only."""
```
The composition-time selection error is **`StrategyNotAvailableError`** (`runtime_root.errors`), NOT a member of `MatcherError`: it surfaces when the binary lacks the requested `BUILD_MATCHER_<variant>` flag or the concrete strategy module is not built yet (AZ-345..AZ-347 pending). This matches the C2 VPR (AZ-336) and C2.5 ReRank (AZ-342) factory pattern: per-frame matcher errors live in the C3 family; composition-time selection errors live in the shared runtime-root family.
## Composition-Root Factory
```python