Decompose Step 6 snapshot: 140 task specs + contract docs

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:39:48 +03:00
parent 8171fcb29e
commit 880eabcb3f
172 changed files with 22897 additions and 35 deletions
@@ -0,0 +1,166 @@
# Contract: VioStrategy Protocol
**Component**: c1_vio
**Producer task**: AZ-331 — `_docs/02_tasks/todo/AZ-331_c1_vio_strategy_protocol.md`
**Consumer tasks**:
- AZ-332 (OKVIS2 implementation — implements)
- AZ-333 (VINS-Mono implementation — implements)
- AZ-334 (KLT/RANSAC implementation — implements)
- AZ-335 (warm-start + F8 reboot recovery wiring — invokes `reset_to_warm_start`)
- E-C5 state estimator tasks under AZ-260 (consume `VioOutput`)
- E-C13 FDR writer tasks under AZ-248 (consume `VioHealth`)
- `runtime_root` composition under AZ-270 (selects strategy by config)
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-10
## Purpose
Defines the typed boundary between the on-Jetson visual / visual-inertial odometry runtime and every downstream consumer (C5 state estimator, C13 FDR, runtime_root composition). The Protocol is the single point of contact that lets ADR-001 select between three concrete strategies (OKVIS2 production-default, VINS-Mono research-only, KLT/RANSAC mandatory simple-baseline) at startup without consumers caring which is wired. Per-frame DTOs (`VioOutput`, `VioHealth`) are frozen here so C5 fusion and C13 FDR records do not drift across implementations.
## Shape
### Protocol surface
The Protocol is `typing.Protocol` (PEP 544 structural typing) with `runtime_checkable=True`.
| Method | Signature | Throws / Errors | Blocking? |
|--------|-----------|-----------------|-----------|
| `process_frame` | `(frame: NavCameraFrame, imu: ImuWindow, calibration: CameraCalibration) -> VioOutput` | `VioInitializingError`, `VioDegradedError`, `VioFatalError` | sync (camera-ingest hot path; bound by C1-PT-01 latency budget) |
| `reset_to_warm_start` | `(hint: WarmStartPose) -> None` | `VioFatalError` (only on irrecoverable backend init failure) | sync |
| `health_snapshot` | `() -> VioHealth` | — | sync |
| `current_strategy_label` | `() -> Literal["okvis2", "vins_mono", "klt_ransac"]` | — | sync |
### DTOs
`NavCameraFrame`, `ImuSample`, `ImuWindow`, `ImuBias`, `CameraCalibration` are owned by `gps_denied_onboard._types.nav` (AZ-263). This contract owns `WarmStartPose`, `VioOutput`, `VioHealth`, `FeatureQuality`, and the `VioState` enum, all `@dataclass(frozen=True)` (or `enum.Enum`). `VioOutput` and `VioHealth` are placed in `_types/nav.py` for cross-component access; the `VioStrategy` Protocol itself lives in `components/c1_vio/interface.py`.
```python
from dataclasses import dataclass
from enum import Enum
from typing import Protocol, Literal, runtime_checkable
from gps_denied_onboard._types.nav import (
NavCameraFrame, ImuWindow, ImuBias, CameraCalibration,
)
from gps_denied_onboard._types.geom import SE3, Vector3, Matrix6
class VioState(str, Enum):
INIT = "init"
TRACKING = "tracking"
DEGRADED = "degraded"
LOST = "lost"
@dataclass(frozen=True)
class WarmStartPose:
body_T_world: SE3
velocity_b: Vector3
bias: ImuBias
captured_at_ns: int # monotonic_ns when the hint was produced
@dataclass(frozen=True)
class FeatureQuality:
tracked: int
new: int
lost: int
mean_parallax: float
mre_px: float
@dataclass(frozen=True)
class VioOutput:
frame_id: str # echoes NavCameraFrame.frame_id
relative_pose_T: SE3
pose_covariance_6x6: Matrix6
imu_bias: ImuBias
feature_quality: FeatureQuality
emitted_at_ns: int
@dataclass(frozen=True)
class VioHealth:
state: VioState
consecutive_lost: int
bias_norm: float
@runtime_checkable
class VioStrategy(Protocol):
def process_frame(
self,
frame: NavCameraFrame,
imu: ImuWindow,
calibration: CameraCalibration,
) -> VioOutput: ...
def reset_to_warm_start(self, hint: WarmStartPose) -> None: ...
def health_snapshot(self) -> VioHealth: ...
def current_strategy_label(self) -> Literal["okvis2", "vins_mono", "klt_ransac"]: ...
```
### Error hierarchy
All under `gps_denied_onboard.components.c1_vio.errors`:
```
VioError (base; subclasses Exception)
├── VioInitializingError (state == INIT; no VioOutput emitted; C5 falls back to FC IMU prior)
├── VioDegradedError (state == DEGRADED; output IS still emitted with inflated covariance — see Invariants)
└── VioFatalError (state == LOST after configurable consecutive frames; AC-5.2 fallback path)
```
`VioDegradedError` is documented but is **not raised** during normal `process_frame` returns when degraded — degraded operation returns a `VioOutput` with inflated covariance and `VioHealth.state = DEGRADED`. The error type exists for the rare case where degradation transitions to fatality and consumer wrappers want to catch the family.
### Composition-root selection
```python
def build_vio_strategy(config: Config, *, fdr_client: FdrClient) -> VioStrategy: ...
```
Lives at `src/gps_denied_onboard/runtime_root/vio_factory.py`. Selects the strategy by `config.vio.strategy` (`okvis2 | vins_mono | klt_ransac`) and respects compile-time `BUILD_*` gating (`BUILD_OKVIS2`, `BUILD_VINS_MONO`, `BUILD_KLT_RANSAC`). Requesting a strategy whose `BUILD_*` flag is OFF raises `StrategyNotAvailableError` at composition time (NOT at first frame). Lazy-imports the concrete strategy module so a Tier-0 workstation build without OKVIS2 native libs still composes successfully when only KLT/RANSAC is requested.
## Invariants
- **6×6 SPD covariance always returned**: `pose_covariance_6x6` is symmetric and positive-definite for every `VioOutput`. Implementations MUST NOT return a "tightened" covariance (smaller Frobenius norm) during a degradation event; honest covariance is the safety floor for AC-NEW-4 and AC-NEW-7. A test (covariance-monotonicity contract test, deferred to Step 9 / E-BBT) asserts this across all three strategies.
- **`frame_id` echo**: `VioOutput.frame_id` equals the input `NavCameraFrame.frame_id`. C5 relies on this for time-aligned factor insertion.
- **Single-threaded by contract**: each `VioStrategy` instance is bound to one writer thread (the camera ingest thread). Concurrent calls to `process_frame` on the same instance are undefined behaviour. The composition root binds one instance per ingest thread.
- **`reset_to_warm_start` is destructive**: clears the strategy's keyframe window, IMU integration state, and feature track buffer; subsequent `process_frame` calls re-initialise from the hint. Calling `reset_to_warm_start` mid-flight is allowed (F8 reboot recovery) but must not be issued concurrently with a `process_frame` call on the same instance.
- **`current_strategy_label()` is constant per instance**: returns the same string for the lifetime of the instance and matches `config.vio.strategy` exactly. The label is FDR-stamped on every `VioHealth` event for AC-NEW-3 audit.
- **No ambient state**: implementations MUST NOT read environment variables, wall clock, or filesystem inside `process_frame`; calibration arrives via constructor + per-call argument; logging uses the injected logger only.
- **Error envelope is closed**: `process_frame` raises only members of `VioError` (the family). Lower-level exceptions from OpenCV / OKVIS2 / VINS-Mono / GTSAM MUST be caught and rewrapped.
## Non-Goals
- IMU preintegration mathematics — owned by AZ-276 / `helpers.imu_preintegrator`. Strategies feed `ImuWindow` to the helper; they do NOT implement preintegration internally.
- Bias estimation policy — each strategy decides when to update its bias; the contract does not prescribe a schedule.
- WarmStartPose persistence (write to disk after takeoff, read after F8 reboot) — owned by the warm-start + F8 reboot recovery wiring task in this same epic. The contract here only defines the in-memory DTO and the `reset_to_warm_start` method.
- C5 fusion semantics — owned by E-C5; this contract only delivers `VioOutput`.
- Multi-camera strategies — out of scope this cycle (single nav-camera per ADR / RESTRICT-UAV-3).
## Versioning Rules
- **Breaking changes** (method renamed/removed, parameter type changed, return type changed, invariant relaxed) require a new major version + a deprecation pass through every consumer task in the header.
- **Non-breaking additions** (new optional method, new diagnostic accessor that does not mutate state, new `VioState` enum variant added at the end) require a minor version bump.
- The `VioState` enum is treated as a closed set for switch-style consumer code (C5 fusion); adding a new variant is a minor bump but consumers MUST handle the new state defensively (default branch → treat as LOST).
## Test Cases
| Case | Input | Expected | Notes |
|------|-------|----------|-------|
| protocol-conformance | three concrete strategy classes | `isinstance(impl, VioStrategy)` returns True for each | Catches drift between impl and Protocol surface |
| frozen-dto-mutation | a constructed `VioOutput` instance and an attempt to set `.relative_pose_T` | `dataclasses.FrozenInstanceError` raised | Confirms DTOs are immutable |
| error-family-catchable | each of `VioInitializingError`, `VioDegradedError`, `VioFatalError` raised | `except VioError` catches all three; `except ValueError` does NOT | Confirms error envelope |
| factory-build-flag-respected | `config.vio.strategy = "vins_mono"` and `BUILD_VINS_MONO=OFF` | `StrategyNotAvailableError` raised at composition; `sys.modules` has no `vins_mono` entry | Confirms lazy-import gating |
| current-strategy-label-exact-match | each strategy constructed via factory with matching config | `current_strategy_label()` returns the literal config value | AC-NEW-3 audit gate |
| frame-id-echoed | a `NavCameraFrame` with a known UUID fed into `process_frame` | the returned `VioOutput.frame_id` equals the input UUID | C5 alignment invariant |
| covariance-spd | inspect 100 emitted `VioOutput.pose_covariance_6x6` matrices | every matrix is symmetric and positive-definite (eigenvalues > 0) | AC-1.4 floor |
## Change Log
| Version | Date | Change | Author |
|---------|------|--------|--------|
| 1.0.0 | 2026-05-10 | Initial contract derived from `_docs/02_document/components/01_c1_vio/description.md` § 2 + AZ-254 epic child issue #1 | autodev decompose Step 2 |