Files
gps-denied-onboard/_docs/02_tasks/done/AZ-344_c3_matcher_protocol.md
T
Oleksandr Bezdieniezhnykh 89c223882b [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>
2026-05-12 05:43:33 +03:00

15 KiB
Raw Blame History

C3 CrossDomainMatcher Protocol + Factory + Composition

Task: AZ-344_c3_matcher_protocol Name: C3 CrossDomainMatcher Protocol + Factory + Composition Description: Define the public CrossDomainMatcher Protocol (PEP 544 structural interface), the C3 DTOs (CandidateMatchSet, MatchResult, MatcherHealth), the error hierarchy (MatcherError family with MatcherBackboneError, InsufficientInliersError), and the composition-root factory build_matcher_strategy(config, lightglue_runtime, ransac_filter, inference_runtime) -> CrossDomainMatcher that selects the concrete matcher at startup based on config.matcher.strategy with lazy import + BUILD_MATCHER_<variant> flag gating per ADR-002. Includes the rolling-window MatcherHealth accumulator infrastructure (constructor-injected into every concrete matcher; updated inside match after each frame). The shared LightGlueRuntime (AZ-278) and RansacFilter (AZ-282) helpers are constructor-injected — neither owned by C3. This task delivers the foundational scaffolding every concrete matcher (AZ-345..AZ-347) depends on; no concrete backbone is implemented here. Complexity: 3 points Dependencies: AZ-263_initial_structure, AZ-269_config_loader, AZ-270_compose_root, AZ-278_lightglue_runtime, AZ-282_ransac_filter, AZ-297_c7_runtime_protocol (for InferenceRuntime interface), AZ-266_log_module Component: c3_matcher (epic AZ-257 / E-C3) Tracker: AZ-344 Epic: AZ-257 (E-C3)

Document Dependencies

  • _docs/02_document/contracts/c3_matcher/cross_domain_matcher_protocol.md — the public contract this task implements (Protocol surface + DTOs + error hierarchy + factory signature + 9 invariants + test cases).
  • _docs/02_document/components/04_c3_matcher/description.md — § 1 architectural pattern (Strategy); § 2 CrossDomainMatcher interface + DTOs; § 5 error handling (drop-and-continue + below-threshold + all-failed); § 7 caveats (shared helper serial access); § 9 logging.
  • _docs/02_document/module-layout.mdc3_matcher Per-Component Mapping; BUILD_MATCHER_<variant> rows; § Layer 3.
  • _docs/02_document/architecture.md — ADR-001, ADR-002, ADR-009.
  • _docs/02_document/contracts/c2_5_rerank/rerank_strategy_protocol.mdRerankResult DTO consumed at the input boundary.
  • _docs/02_document/contracts/shared_helpers/lightglue_runtime.md — helper handle.
  • _docs/02_document/contracts/shared_helpers/ransac_filter.md — helper API consumed for inlier filtering.
  • _docs/02_document/contracts/c7_inference/inference_runtime_protocol.mdInferenceRuntime interface consumed by every concrete backbone.

Problem

Without this task, every concrete matcher (AZ-345..AZ-347) and the downstream C3.5 ConditionalRefiner (AZ-258 component) would each invent their own ad-hoc interface, breaking ADR-001 (Strategy), ADR-002 (build-time exclusion), and ADR-009 (interface-first DI). The rolling MatcherHealth accumulator also needs ONE owner; without this task, every concrete matcher would re-implement the rolling window → drift between strategies → C5's spoof-promotion gate (AC-NEW-2 / AC-NEW-7) would behave inconsistently across matchers.

Outcome

  • src/gps_denied_onboard/components/c3_matcher/interface.py defining the CrossDomainMatcher Protocol (@runtime_checkable) with match and health_snapshot. Docstring encodes all 9 invariants from the contract.
  • src/gps_denied_onboard/components/c3_matcher/__init__.py re-exporting the Protocol + DTOs (CrossDomainMatcher, MatchResult, MatcherHealth).
  • src/gps_denied_onboard/_types/matcher.py defining the three frozen + slotted dataclasses: CandidateMatchSet, MatchResult, MatcherHealth. Cross-component-consumed → lives in shared _types/.
  • src/gps_denied_onboard/components/c3_matcher/errors.py defining MatcherError, MatcherBackboneError, InsufficientInliersError.
  • src/gps_denied_onboard/components/c3_matcher/_health_window.py defining RollingHealthWindow — the 60 s rolling window with O(1) accumulators (consecutive_low_inlier, mean_inliers_60s, backbone_error_count_60s). Provided to every concrete matcher via constructor injection so they share semantics.
  • src/gps_denied_onboard/runtime_root/matcher_factory.py exporting build_matcher_strategy(config, lightglue_runtime, ransac_filter, inference_runtime) -> CrossDomainMatcher. The factory:
    1. Reads config.matcher.strategy (one of: "disk_lightglue", "aliked_lightglue", "xfeat").
    2. Lazy-imports per the strategy resolution table.
    3. ImportError "No module named" → ConfigurationError(f"BUILD_MATCHER_{strategy.upper()} is OFF..."). Other ImportErrors re-raised.
    4. Constructs the strategy via its module-level create(config, lightglue_runtime, ransac_filter, inference_runtime, health_window) factory function.
    5. Returns the instance.
  • Composition-root compose_root extension: invoke build_matcher_strategy AFTER LightGlueRuntime + RansacFilter are constructed; bind the result to the same C2.5 ingest thread. Identity-share the LightGlueRuntime instance with C2.5 (per AZ-342 AC-10).
  • Config schema extension to AZ-269: config.matcher.strategy (enum), config.matcher.min_inliers_threshold (int, default 60), config.matcher.residual_warn_threshold_px (float, default 2.5).
  • INFO log on every successful build_matcher_strategy: kind="c3.matcher.strategy_loaded" with strategy name + thresholds.
  • ERROR log on ConfigurationError (specific missing flag).

Scope

Included

  • The CrossDomainMatcher Protocol with both match and health_snapshot methods.
  • The three DTOs in _types/matcher.py.
  • The three-class error hierarchy in c3_matcher/errors.py.
  • The RollingHealthWindow accumulator in _health_window.py (constructor-injected into every matcher).
  • The composition-root factory with lazy-import + ConfigurationError mapping.
  • Config schema extension for config.matcher.{strategy, min_inliers_threshold, residual_warn_threshold_px}.
  • Strategy resolution table comment matching the contract verbatim.
  • Composition-root wiring path that identity-shares LightGlueRuntime with C2.5.
  • Unit tests covering: Protocol conformance (runtime_checkable), DTO immutability + slots, factory rejection on missing flag, factory acceptance for valid values, rolling window O(1) accumulator correctness, INFO log emission, error hierarchy catchability.
  • INFO / ERROR log emission per description.md § 9.

Excluded

  • Any concrete matcher implementation — owned by AZ-345 (DISK+LightGlue), AZ-346 (ALIKED+LightGlue), AZ-347 (XFeat).
  • The LightGlueRuntime helper — already AZ-278.
  • The RansacFilter helper — already AZ-282.
  • The C7 InferenceRuntime — owned by AZ-297.
  • The C2.5 RerankResult DTO — consumed; produced by AZ-342.
  • Component-internal acceptance tests beyond Protocol-conformance + factory-validation: C3-IT-01..05 + C3-PT-01 deferred to Step 9 / E-BBT.

Acceptance Criteria

AC-1: Protocol conformance — runtime_checkable Given a FakeMatcher test double implementing both match and health_snapshot When isinstance(fake, CrossDomainMatcher) is evaluated Then result is True; an object missing either method returns False

AC-2: DTO immutability + slots All three DTOs use frozen=True, slots=True; mutation raises FrozenInstanceError; __slots__ non-empty.

AC-3: Factory rejects missing build flag Given config.matcher.strategy = "nonexistent_matcher" When build_matcher_strategy(...) is called Then ConfigurationError("BUILD_MATCHER_NONEXISTENT_MATCHER is OFF...") is raised; ONE ERROR log kind="c3.matcher.build_flag_off" is emitted.

AC-4: Factory rejects unknown strategy at config-load time Given config.matcher.strategy = "garbage" (not in the resolution table) When load_config(...) is called Then ConfigurationError raised at config-load time; the factory is never invoked.

AC-5: Successful factory load emits INFO log Given config.matcher.strategy = "disk_lightglue" AND a valid lazy-importable test double module When build_matcher_strategy(...) is called Then a CrossDomainMatcher instance is returned; ONE INFO log kind="c3.matcher.strategy_loaded" is emitted with {strategy, min_inliers_threshold, residual_warn_threshold_px}.

AC-6: Strategy resolution — every entry resolves to its module path Given each of three valid config.matcher.strategy values When build_matcher_strategy is called for each Then resolved module path matches the contract's table verbatim.

AC-7: Error hierarchy catchability Test instances of MatcherBackboneError + InsufficientInliersError caught by except MatcherError.

AC-8: Public API surface — __init__.py re-exports Given from gps_denied_onboard.components.c3_matcher import CrossDomainMatcher, MatchResult, MatcherHealth When the import is evaluated Then all three names resolve; internal names (RollingHealthWindow, _health_window) are NOT in __all__.

AC-9: Strategy bound to single ingest thread by composition root Single-thread binding enforced; second binding attempt raises RuntimeError.

AC-10: LightGlueRuntime is identity-shared between C3 and C2.5 Given a compose_root(config) invocation that wires both C2.5 and C3 When the resulting strategies are inspected Then c3_strategy._lightglue_runtime is c2_5_strategy._lightglue_runtime (identity); ONE INFO log confirming the shared binding is emitted (the SAME log line as AZ-342 AC-10 — emitted ONCE).

AC-11: RollingHealthWindow O(1) accumulator correctness Given a RollingHealthWindow (60 s) AND a sequence of (frame_id, inlier_count, had_backbone_error) events spanning 90 s When health_snapshot() is called at t=60s, t=70s, t=90s Then consecutive_low_inlier, mean_inliers_60s, backbone_error_count_60s match an independent sliding-window computation; each health_snapshot() call is O(1) (microbench p99 ≤ 50 µs).

AC-12: RollingHealthWindow.update() API Given the window has incremental update entry-points (called from inside concrete matchers' match after each frame) When update(timestamp_ns, best_inlier_count, had_backbone_error) is called Then accumulators are updated incrementally; mean_inliers_60s is the rolling mean; consecutive_low_inlier resets to 0 when a frame's inlier_count >= min_inliers_threshold.

Non-Functional Requirements

Performance

  • build_matcher_strategy p99 ≤ 50 ms (factory itself; concrete-strategy construction cost owned by AZ-345..AZ-347).
  • RollingHealthWindow.update p99 ≤ 5 µs; health_snapshot p99 ≤ 50 µs.

Compatibility

  • Protocol method-signature changes are major version bumps (lockstep update).
  • DTO field additions are minor; field removals are major.

Reliability

  • Lazy-import via importlib.import_module; build-time-excluded matchers never load CUDA / TensorRT.
  • Single-thread invariant enforced at composition-root binding time (AC-9).
  • RollingHealthWindow is a non-thread-safe single-thread structure; matches the single-thread binding invariant.

Unit Tests

AC Ref What to Test Required Outcome
AC-1 Protocol conformance Fake passes; partial fake fails
AC-2 DTO immutability + slots FrozenInstanceError; non-empty __slots__
AC-3 Factory + missing flag ConfigurationError; ERROR log
AC-4 Config load + unknown strategy ConfigurationError at load time
AC-5 Factory + valid load Strategy returned; INFO log with structured fields
AC-6 All 3 strategy values resolve Module paths match resolution table
AC-7 Error catchability All errors caught by except MatcherError
AC-8 Public API re-exports Public names resolve; internals not in __all__
AC-9 Single-thread binding Second binding raises RuntimeError
AC-10 Identity-share with C2.5 is identity preserved
AC-11 Rolling window correctness Matches independent sliding-window computation
AC-12 update semantics consecutive_low_inlier resets on a high-inlier frame; mean_inliers_60s is rolling
NFR-perf-window RollingHealthWindow.update × 100k p99 ≤ 5 µs

Constraints

  • Lazy import is mandatory (ADR-002).
  • @runtime_checkable MUST be used.
  • DTOs MUST be frozen=True, slots=True.
  • Concrete matcher modules export create(...) as their entry-point.
  • config.matcher.strategy is an enum validated at config load.
  • The factory does NOT instantiate LightGlueRuntime or RansacFilter — runtime root constructs ONCE and shares with C2.5 (AC-10).
  • RollingHealthWindow is single-thread — no locks; matches single-thread binding invariant. Adding locks would mask binding bugs.

Risks & Mitigation

Risk 1: runtime_checkable Protocol checks have known performance cost

  • Mitigation: isinstance only at composition-root binding (AC-9), not per-frame.

Risk 2: Lazy-import error message obscures real failure mode

  • Mitigation: factory catches ImportError, inspects message; "No module named" → "BUILD flag OFF"; otherwise re-raises preserving native context.

Risk 3: RollingHealthWindow 60 s window data structure choice (deque vs ring buffer)

  • Mitigation: implementation detail; AC-11 + AC-12 + NFR-perf-window are the contract. Any structure satisfying O(1) update + O(1) snapshot is acceptable.

Risk 4: compose_root thread-binding registry / shared-helper composition not yet implemented in AZ-270

  • Mitigation: same as AZ-342 Risk 4. Keep AC-9 / AC-10; if AZ-270 lacks the registry, escalate via tracker dependency mechanism.

Runtime Completeness

  • Named capability: CrossDomainMatcher Protocol + composition-root factory + RollingHealthWindow accumulator + ADR-002 build-time exclusion.
  • Production code that must exist: real Protocol + real DTOs + real error hierarchy + real build_matcher_strategy factory + real RollingHealthWindow + real config schema extension + real composition-root wiring path that identity-shares LightGlueRuntime with C2.5.
  • Allowed external stubs: FakeMatcher, FakeLightGlueRuntime, FakeRansacFilter, FakeInferenceRuntime for tests. Production wiring uses real concretes.
  • Unacceptable substitutes: direct from .disk_lightglue import DiskLightGlueMatcher in the factory (defeats ADR-002); a Type[CrossDomainMatcher] registry that pre-imports all matchers (defeats lazy-import); making RollingHealthWindow thread-safe with locks (would mask single-thread binding bugs); skipping the identity-share with C2.5 (would double GPU memory).

Contract

This task produces/implements the contract at _docs/02_document/contracts/c3_matcher/cross_domain_matcher_protocol.md. Consumers MUST read that file — not this task spec — to discover the interface.