Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
7.8 KiB
C5 StateEstimator Protocol + Factory + DTOs + Composition + Concrete ISam2GraphHandle
Task: AZ-381_c5_state_protocol
Name: C5 StateEstimator Protocol + Factory + DTOs + Composition + concrete ISam2GraphHandle
Description: Define the public StateEstimator Protocol (PEP 544 @runtime_checkable), the C5 DTOs (EstimatorOutput, EstimatorHealth, IsamState enum), the error hierarchy (StateEstimatorError, EstimatorDegradedError, EstimatorFatalError, StateEstimatorConfigError), the composition-root factory build_state_estimator(...) -> tuple[StateEstimator, ISam2GraphHandle], AND the CONCRETE ISam2GraphHandle implementation extending the AZ-355 Protocol stub with add_factor/update/compute_marginals/last_anchor_age_ms methods. The handle is constructed alongside the iSAM2 graph (initially empty here; populated by AZ-382 iSAM2 wiring task) and passed by reference to C4 via the runtime root. Strategy resolution per ADR-002 with BUILD_STATE_<variant> gating. Shared helpers (ImuPreintegrator AZ-276, SE3Utils AZ-277, WgsConverter AZ-279) constructor-injected. Config schema extension for state.{strategy, keyframe_window_size, spoof_promotion_min_stable_s, spoof_promotion_visual_consistency_tol_m, no_estimate_fallback_s}. No iSAM2 graph internals or factor-add logic in scope here.
Complexity: 3 points
Dependencies: AZ-263, AZ-269, AZ-270, AZ-276 (ImuPreintegrator), AZ-277 (SE3Utils), AZ-279 (WgsConverter), AZ-273 (FdrClient), AZ-355 (C4's ISam2GraphHandle Protocol stub — extended here), AZ-266
Component: c5_state (epic AZ-260 / E-C5)
Tracker: AZ-381
Epic: AZ-260 (E-C5)
Document Dependencies
_docs/02_document/contracts/c5_state/state_estimator_protocol.md— the public contract this task implements._docs/02_document/components/07_c5_state/description.md— § 1, § 2, § 5 error handling, § 9 logging._docs/02_document/architecture.md— ADR-001, ADR-002, ADR-003, ADR-009._docs/02_document/contracts/c4_pose/pose_estimator_protocol.md—ISam2GraphHandleProtocol stub source._docs/02_document/module-layout.md—c5_statePer-Component Mapping.
Problem
Without this task, C4 has no concrete ISam2GraphHandle to inject (only the Protocol stub from AZ-355) — meaning the runtime root cannot wire C4 + C5 together. The DTO surface (EstimatorOutput, EstimatorHealth) is also consumed by C8, C13, and the orthorectifier — defining it once in _types/state.py prevents drift. The eight downstream consumer tasks (iSAM2 wiring, factor adds, marginals, spoof gate, ESKF, smoothed history, AC-5.2, orthorectifier) depend on the Protocol surface + the handle being available.
Outcome
src/gps_denied_onboard/components/c5_state/interface.py—StateEstimatorProtocol with all 6 methods.src/gps_denied_onboard/components/c5_state/__init__.py— re-exportsStateEstimator,EstimatorOutput,EstimatorHealth.src/gps_denied_onboard/_types/state.py—EstimatorOutput,EstimatorHealth,IsamStateenum (frozen + slots).src/gps_denied_onboard/components/c5_state/errors.py— error hierarchy.src/gps_denied_onboard/components/c5_state/_isam2_handle.py— concreteISam2GraphHandleImpl(ISam2GraphHandle)class with all four methods. Body: empty stubs that raiseNotImplementedError("iSAM2 wiring task owns this body")until AZ-382 lands. Each method'sNotImplementedErrormessage names the responsible task ID for traceability.src/gps_denied_onboard/runtime_root/state_factory.py—build_state_estimator(...)returning the tuple. Lazy-import per ADR-002.- Composition-root extension: invoke
build_state_estimatorAFTER the shared helpers; pass the returnedISam2GraphHandletobuild_pose_estimator(C4); bind C4 + C5 to the SAME ingest thread. - Config schema extension for the five
state.*fields. - INFO log on successful build:
kind="c5.state.strategy_loaded".
Scope
Included
StateEstimatorProtocol with 6 methods.- DTOs (
EstimatorOutput,EstimatorHealth,IsamState) in_types/state.py. - Error hierarchy.
- Concrete
ISam2GraphHandleImplskeleton (body owned by AZ-382 iSAM2 wiring task). - Composition-root factory + thread binding.
- Config schema extension.
- Unit tests: Protocol conformance, DTO immutability + slots, factory rejection on unknown strategy + missing build flag, ISam2GraphHandleImpl methods exist (return
NotImplementedError), thread binding.
Excluded
- iSAM2 +
IncrementalFixedLagSmootherbody — owned by AZ-382 (next task). - Factor adds (VIO + Pose + IMU) — owned by AZ-383.
- Marginals + outputs — owned by AZ-384.
- Source-label state machine + spoof gate — owned by AZ-385.
- ESKF baseline — owned by AZ-386.
- Smoothed-history → FDR — owned by AZ-387.
- AC-5.2 fallback — owned by AZ-388.
- Orthorectifier sub-path — owned by AZ-389.
- Component-internal acceptance tests C5-IT-01..07 + C5-PT-01 + C5-ST-01 — deferred to E-BBT (AZ-262).
Acceptance Criteria
AC-1: Protocol conformance — runtime_checkable isinstance returns True for a fake with all 6 methods.
AC-2: DTOs frozen + slots — FrozenInstanceError on mutation; __slots__ non-empty.
AC-3: IsamState enum has 4 values — INIT, TRACKING, DEGRADED, LOST.
AC-4: Factory rejects missing build flag — config.state.strategy = "nonexistent" → StateEstimatorConfigError("BUILD_STATE_NONEXISTENT is OFF...").
AC-5: Factory rejects unknown strategy at config-load — config.state.strategy = "garbage" → StateEstimatorConfigError at config load.
AC-6: Factory returns the tuple — both StateEstimator AND ISam2GraphHandle are returned from a successful build; INFO log with {strategy, keyframe_window_size}.
AC-7: Thread binding — composition root binds C5 to ONE ingest thread (the same as C4); second binding raises RuntimeError.
AC-8: ISam2GraphHandleImpl skeleton — instance is isinstance(handle, ISam2GraphHandle); calling add_factor, update, compute_marginals, last_anchor_age_ms each raises NotImplementedError(f"Body owned by ...") with the correct task ID in the message.
AC-9: Public API re-exports — from gps_denied_onboard.components.c5_state import StateEstimator, EstimatorOutput, EstimatorHealth resolves; internals not in __all__.
AC-10: Error hierarchy catchability — every error caught by except StateEstimatorError.
Non-Functional Requirements
build_state_estimatorp99 ≤ 50 ms.
Constraints
@runtime_checkableon Protocol; DTOsfrozen=True, slots=True.- Lazy-import per ADR-002.
- Single-thread binding enforced (AC-7).
- The
ISam2GraphHandleImplskeleton'sNotImplementedErrormessages MUST name the responsible task ID — AZ-382 iSAM2 wiring is the receiver.
Risks & Mitigation
- Risk: AZ-382 iSAM2 task lands before this task → cycle. Mitigation: this task ships first; AZ-382 imports
ISam2GraphHandleImpland replaces method bodies. - Risk: AZ-355 stub Protocol may differ slightly from AZ-358's extension. Mitigation: this task verifies isinstance against the FINAL Protocol shape (post-AZ-358 extension) — both AZ-358 and this task update the Protocol stub in lockstep.
Runtime Completeness
- Named capability:
StateEstimatorProtocol + DTOs + factory + concreteISam2GraphHandleskeleton. - Production code: real Protocol, real DTOs, real error hierarchy, real factory, real
ISam2GraphHandleImplskeleton withNotImplementedErrorbodies, real composition wiring. - Allowed external stubs: test fakes only.
- Unacceptable substitutes: hardcoding the C5 strategy class in C4's factory (defeats ADR-009); skipping the concrete
ISam2GraphHandleImpl(would force AZ-382 iSAM2 wiring to also reshape Protocol).
Contract
Implements _docs/02_document/contracts/c5_state/state_estimator_protocol.md.